Browse Source

Pagination for contributors. (#408)

* Pagination for contributors.
pull/407/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
a85d49c25c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/Squidex.Domain.Users/DefaultUserResolver.cs
  2. 9
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  3. 3
      src/Squidex.Shared/Users/IUserResolver.cs
  4. 9
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  5. 19
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  6. 9
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  7. 9
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  8. 9
      src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
  9. 9
      src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs
  10. 6
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs
  11. 6
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs
  12. 6
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  13. 10
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs
  14. 30
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs
  15. 38
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  16. 20
      src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  17. 11
      src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs
  18. 9
      src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs
  19. 10
      src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs
  20. 9
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  21. 8
      src/Squidex/app/features/administration/services/event-consumers.service.spec.ts
  22. 24
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  23. 12
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  24. 16
      src/Squidex/app/framework/angular/forms/dropdown.component.ts
  25. 10
      src/Squidex/app/framework/angular/highlight.pipe.ts
  26. 8
      src/Squidex/app/shared/services/assets.service.spec.ts
  27. 8
      src/Squidex/app/shared/services/contents.service.spec.ts
  28. 6
      src/Squidex/app/shared/services/contributors.service.spec.ts
  29. 2
      src/Squidex/app/shared/services/contributors.service.ts
  30. 8
      src/Squidex/app/shared/services/rules.service.spec.ts
  31. 16
      src/Squidex/app/shared/services/schemas.service.spec.ts
  32. 8
      src/Squidex/app/shared/state/clients.state.ts
  33. 2
      src/Squidex/app/shared/state/contents.state.ts
  34. 77
      src/Squidex/app/shared/state/contributors.state.spec.ts
  35. 73
      src/Squidex/app/shared/state/contributors.state.ts
  36. 7
      src/Squidex/app/shared/state/rules.state.ts
  37. 8
      src/Squidex/app/theme/_lists.scss
  38. 31
      tools/LoadTest/TestUtils.cs
  39. 1
      tools/LoadTest/WritingBenchmarks.cs

8
src/Squidex.Domain.Users/DefaultUserResolver.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -69,5 +70,12 @@ namespace Squidex.Domain.Users
return result.OfType<IUser>().ToList();
}
public async Task<Dictionary<string, IUser>> QueryManyAsync(string[] ids)
{
var result = await userManager.QueryByIdsAync(ids);
return result.OfType<IUser>().ToDictionary(x => x.Id);
}
}
}

9
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -85,6 +85,15 @@ namespace Squidex.Domain.Users
return Task.FromResult(count);
}
public static async Task<List<UserWithClaims>> QueryByIdsAync(this UserManager<IdentityUser> userManager, string[] ids)
{
var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList();
var result = await userManager.ResolveUsersAsync(users);
return result.ToList();
}
public static async Task<List<UserWithClaims>> QueryByEmailAsync(this UserManager<IdentityUser> userManager, string email = null, int take = 10, int skip = 0)
{
var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList();

3
src/Squidex.Shared/Users/IUserResolver.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -17,5 +18,7 @@ namespace Squidex.Shared.Users
Task<IUser> FindByIdOrEmailAsync(string idOrEmail);
Task<List<IUser>> QueryByEmailAsync(string email);
Task<Dictionary<string, IUser>> QueryManyAsync(string[] ids);
}
}

9
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = Deferred.Response(() =>
{
return ClientsDto.FromApp(App, this);
return GetResponse(App);
});
Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -143,9 +143,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = ClientsDto.FromApp(result, this);
var response = GetResponse(result);
return response;
}
private ClientsDto GetResponse(IAppEntity app)
{
return ClientsDto.FromApp(App, this);
}
}
}

19
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Entities.Apps.Invitation;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Shared.Users;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
@ -26,11 +27,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
public sealed class AppContributorsController : ApiController
{
private readonly IAppPlansProvider appPlansProvider;
private readonly IUserResolver userResolver;
public AppContributorsController(ICommandBus commandBus, IAppPlansProvider appPlansProvider)
public AppContributorsController(ICommandBus commandBus, IAppPlansProvider appPlansProvider, IUserResolver userResolver)
: base(commandBus)
{
this.appPlansProvider = appPlansProvider;
this.userResolver = userResolver;
}
/// <summary>
@ -48,9 +52,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)]
public IActionResult GetContributors(string app)
{
var response = Deferred.Response(() =>
var response = Deferred.AsyncResponse(() =>
{
return ContributorsDto.FromApp(App, appPlansProvider, this, false);
return GetResponseAsync(App, false);
});
Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -112,12 +116,17 @@ namespace Squidex.Areas.Api.Controllers.Apps
if (context.PlainResult is InvitedResult invited)
{
return ContributorsDto.FromApp(invited.App, appPlansProvider, this, true);
return await GetResponseAsync(invited.App, true);
}
else
{
return ContributorsDto.FromApp(context.Result<IAppEntity>(), appPlansProvider, this, false);
return await GetResponseAsync(context.Result<IAppEntity>(), false);
}
}
private Task<ContributorsDto> GetResponseAsync(IAppEntity app, bool invited)
{
return ContributorsDto.FromAppAsync(app, this, userResolver, appPlansProvider, invited);
}
}
}

9
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = Deferred.Response(() =>
{
return AppLanguagesDto.FromApp(App, this);
return GetResponse(App);
});
Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -132,11 +132,16 @@ namespace Squidex.Areas.Api.Controllers.Apps
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = AppLanguagesDto.FromApp(result, this);
var response = GetResponse(result);
return response;
}
private AppLanguagesDto GetResponse(IAppEntity result)
{
return AppLanguagesDto.FromApp(result, this);
}
private static Language ParseLanguage(string language)
{
try

9
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = Deferred.Response(() =>
{
return PatternsDto.FromApp(App, this);
return GetResponse(App);
});
Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -137,9 +137,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = PatternsDto.FromApp(result, this);
var response = GetResponse(result);
return response;
}
private PatternsDto GetResponse(IAppEntity result)
{
return PatternsDto.FromApp(result, this);
}
}
}

9
src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs

@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = Deferred.Response(() =>
{
return RolesDto.FromApp(App, this);
return GetResponse(App);
});
Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -160,9 +160,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = RolesDto.FromApp(result, this);
var response = GetResponse(result);
return response;
}
private RolesDto GetResponse(IAppEntity result)
{
return RolesDto.FromApp(result, this);
}
}
}

9
src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs

@ -50,7 +50,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = Deferred.AsyncResponse(() =>
{
return WorkflowsDto.FromAppAsync(workflowsValidator, App, this);
return GetResponse(App);
});
Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -135,9 +135,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = await WorkflowsDto.FromAppAsync(workflowsValidator, result, this);
var response = await GetResponse(result);
return response;
}
private async Task<WorkflowsDto> GetResponse(IAppEntity result)
{
return await WorkflowsDto.FromAppAsync(workflowsValidator, result, this);
}
}
}

6
src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs

@ -46,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public bool IsOptional { get; set; }
public static AppLanguageDto FromLanguage(LanguageConfig language, IAppEntity app, ApiController controller)
public static AppLanguageDto FromLanguage(LanguageConfig language, IAppEntity app)
{
var result = SimpleMapper.Map(language.Language,
new AppLanguageDto
@ -56,10 +56,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
Fallback = language.LanguageFallbacks.ToArray()
});
return result.CreateLinks(controller, app);
return result;
}
private AppLanguageDto CreateLinks(ApiController controller, IAppEntity app)
public AppLanguageDto WithLinks(ApiController controller, IAppEntity app)
{
var values = new { app = app.Name, language = Iso2Code };

6
src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs

@ -27,9 +27,9 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
var result = new AppLanguagesDto
{
Items = app.LanguagesConfig.OfType<LanguageConfig>()
.Select(x => AppLanguageDto.FromLanguage(x, app, controller))
.OrderByDescending(x => x.IsMaster)
.ThenBy(x => x.Iso2Code)
.Select(x => AppLanguageDto.FromLanguage(x, app))
.Select(x => x.WithLinks(controller, app))
.OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code)
.ToArray()
};

6
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -38,14 +38,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public string Role { get; set; }
public static ClientDto FromClient(string id, AppClient client, ApiController controller, string app)
public static ClientDto FromClient(string id, AppClient client)
{
var result = SimpleMapper.Map(client, new ClientDto { Id = id });
return result.CreateLinks(controller, app);
return result;
}
private ClientDto CreateLinks(ApiController controller, string app)
public ClientDto WithLinks(ApiController controller, string app)
{
var values = new { app, id = Id };

10
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs

@ -23,12 +23,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public static ClientsDto FromApp(IAppEntity app, ApiController controller)
{
var appName = app.Name;
var result = new ClientsDto
{
Items = app.Clients.Select(x => ClientDto.FromClient(x.Key, x.Value, controller, app.Name)).ToArray()
Items =
app.Clients
.Select(x => ClientDto.FromClient(x.Key, x.Value))
.Select(x => x.WithLinks(controller, appName))
.ToArray()
};
return result.CreateLinks(controller, app.Name);
return result.CreateLinks(controller, appName);
}
private ClientsDto CreateLinks(ApiController controller, string app)

30
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs

@ -5,33 +5,57 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Shared;
using Squidex.Shared.Users;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ContributorDto : Resource
{
private const string NotFound = "- not found -";
/// <summary>
/// The id of the user that contributes to the app.
/// </summary>
[Required]
public string ContributorId { get; set; }
/// <summary>
/// The display name.
/// </summary>
[Required]
public string ContributorName { get; set; }
/// <summary>
/// The role of the contributor.
/// </summary>
public string Role { get; set; }
public static ContributorDto FromIdAndRole(string id, string role, ApiController controller, string app)
public static ContributorDto FromIdAndRole(string id, string role)
{
var result = new ContributorDto { ContributorId = id, Role = role };
return result.CreateLinks(controller, app);
return result;
}
public ContributorDto WithUser(IDictionary<string, IUser> users)
{
if (users.TryGetValue(ContributorId, out var user))
{
ContributorName = user.DisplayName();
}
else
{
ContributorName = NotFound;
}
return this;
}
private ContributorDto CreateLinks(ApiController controller, string app)
public ContributorDto WithLinks(ApiController controller, string app)
{
if (!controller.IsUser(ContributorId))
{

38
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -5,12 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Shared;
using Squidex.Shared.Users;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -29,29 +32,50 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public int MaxContributors { get; set; }
/// <summary>
/// The metadata.
/// The metadata to provide information about this request.
/// </summary>
[JsonProperty("_meta")]
public ContributorsMetadata Metadata { get; set; }
public static ContributorsDto FromApp(IAppEntity app, IAppPlansProvider plans, ApiController controller, bool isInvited)
public static async Task<ContributorsDto> FromAppAsync(IAppEntity app, ApiController controller, IUserResolver userResolver, IAppPlansProvider plans, bool invited)
{
var users = await userResolver.QueryManyAsync(app.Contributors.Keys.ToArray());
var result = new ContributorsDto
{
Items = app.Contributors.Select(x => ContributorDto.FromIdAndRole(x.Key, x.Value, controller, app.Name)).ToArray(),
Items =
app.Contributors
.Select(x => ContributorDto.FromIdAndRole(x.Key, x.Value))
.Select(x => x.WithUser(users))
.Select(x => x.WithLinks(controller, app.Name))
.OrderBy(x => x.ContributorName)
.ToArray()
};
result.WithInvited(invited);
result.WithPlan(app, plans);
return result.CreateLinks(controller, app.Name);
}
private ContributorsDto WithPlan(IAppEntity app, IAppPlansProvider plans)
{
MaxContributors = plans.GetPlanForApp(app).MaxContributors;
return this;
}
private ContributorsDto WithInvited(bool isInvited)
{
if (isInvited)
{
result.Metadata = new ContributorsMetadata
Metadata = new ContributorsMetadata
{
IsInvited = isInvited.ToString()
};
}
result.MaxContributors = plans.GetPlanForApp(app).MaxContributors;
return result.CreateLinks(controller, app.Name);
return this;
}
private ContributorsDto CreateLinks(ApiController controller, string app)

20
src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs

@ -44,23 +44,33 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
public IEnumerable<string> Permissions { get; set; }
public static RoleDto FromRole(Role role, IAppEntity app, ApiController controller)
public static RoleDto FromRole(Role role, IAppEntity app)
{
var permissions = role.Permissions.WithoutApp(app.Name);
var result = new RoleDto
{
Name = role.Name,
NumClients = app.Clients.Count(x => Role.IsRole(x.Value.Role, role.Name)),
NumContributors = app.Contributors.Count(x => Role.IsRole(x.Value, role.Name)),
NumClients = GetNumClients(role, app),
NumContributors = GetNumContributors(role, app),
Permissions = permissions.ToIds(),
IsDefaultRole = Role.IsDefaultRole(role.Name)
};
return result.CreateLinks(controller, app.Name);
return result;
}
private RoleDto CreateLinks(ApiController controller, string app)
private static int GetNumContributors(Role role, IAppEntity app)
{
return app.Contributors.Count(x => Role.IsRole(x.Value, role.Name));
}
private static int GetNumClients(Role role, IAppEntity app)
{
return app.Clients.Count(x => Role.IsRole(x.Value.Role, role.Name));
}
public RoleDto WithLinks(ApiController controller, string app)
{
var values = new { app, name = Name };

11
src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs

@ -23,12 +23,19 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public static RolesDto FromApp(IAppEntity app, ApiController controller)
{
var appName = app.Name;
var result = new RolesDto
{
Items = app.Roles.Values.Select(x => RoleDto.FromRole(x, app, controller)).OrderBy(x => x.Name).ToArray()
Items =
app.Roles.Values
.Select(x => RoleDto.FromRole(x, app))
.Select(x => x.WithLinks(controller, appName))
.OrderBy(x => x.Name)
.ToArray()
};
return result.CreateLinks(controller, app.Name);
return result.CreateLinks(controller, appName);
}
private RolesDto CreateLinks(ApiController controller, string app)

9
src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
@ -44,7 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public Status Initial { get; set; }
public static WorkflowDto FromWorkflow(Guid id, Workflow workflow, ApiController controller, string app)
public static WorkflowDto FromWorkflow(Guid id, Workflow workflow)
{
var result = SimpleMapper.Map(workflow, new WorkflowDto
{
@ -54,12 +55,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
Id = id
});
return result.CreateLinks(controller, app, id);
return result;
}
private WorkflowDto CreateLinks(ApiController controller, string app, Guid id)
public WorkflowDto WithLinks(ApiController controller, string app)
{
var values = new { app, id };
var values = new { app, id = Id };
if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app))
{

10
src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs

@ -31,16 +31,22 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public static async Task<WorkflowsDto> FromAppAsync(IWorkflowsValidator workflowsValidator, IAppEntity app, ApiController controller)
{
var appName = app.Name;
var result = new WorkflowsDto
{
Items = app.Workflows.Select(x => WorkflowDto.FromWorkflow(x.Key, x.Value, controller, app.Name)).ToArray(),
Items =
app.Workflows
.Select(x => WorkflowDto.FromWorkflow(x.Key, x.Value))
.Select(x => x.WithLinks(controller, appName))
.ToArray()
};
var errors = await workflowsValidator.ValidateAsync(app.Id, app.Workflows);
result.Errors = errors.ToArray();
return result.CreateLinks(controller, app.Name);
return result.CreateLinks(controller, appName);
}
private WorkflowsDto CreateLinks(ApiController controller, string app)

9
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -36,8 +36,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
[Required]
public StatusInfoDto[] Statuses { get; set; }
public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents,
Context context, ApiController controller, ISchemaEntity schema, IContentWorkflow contentWorkflow)
public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents, Context context, ApiController controller, ISchemaEntity schema, IContentWorkflow workflow)
{
var result = new ContentsDto
{
@ -45,14 +44,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray()
};
await result.AssignStatusesAsync(contentWorkflow, schema);
await result.AssignStatusesAsync(workflow, schema);
return result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name);
}
private async Task AssignStatusesAsync(IContentWorkflow contentWorkflow, ISchemaEntity schema)
private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema)
{
var allStatuses = await contentWorkflow.GetAllAsync(schema);
var allStatuses = await workflow.GetAllAsync(schema);
Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray();
}

8
src/Squidex/app/features/administration/services/event-consumers.service.spec.ts

@ -139,10 +139,10 @@ describe('EventConsumersService', () => {
function eventConsumerResponse(id: number) {
return {
name: `event-consumer${id}`,
position: `position-${id}`,
position: `position${id}`,
isStopped: true,
isResetting: true,
error: `failure-${id}`,
error: `failure${id}`,
_links: {
reset: { method: 'PUT', href: `/event-consumers/${id}/reset` }
}
@ -159,6 +159,6 @@ export function createEventConsumer(id: number, suffix = '') {
`event-consumer${id}`,
true,
true,
`failure-${id}${suffix}`,
`position-${id}${suffix}`);
`failure${id}${suffix}`,
`position${id}${suffix}`);
}

24
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -6,11 +6,17 @@
</ng-container>
<ng-container menu>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh contributors (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary mr-2" (click)="reload()" title="Refresh contributors (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<div class="form-inline">
<input class="form-control" placeholder="Search"
[ngModel]="contributorsState.query | async"
(ngModelChange)="search($event)" />
</div>
</ng-container>
<ng-container content>
@ -21,15 +27,16 @@
</div>
</ng-container>
<ng-container *ngIf="contributorsState.contributors | async; let contributors">
<ng-container *ngIf="contributorsState.contributorsPaged | async; let contributors">
<ng-container *ngIf="contributors.length > 0; else noContributors">
<table class="table table-items table-fixed" *ngIf="rolesState.roles | async; let roles">
<tbody *ngFor="let contributor of contributors; trackBy: trackByContributor">
<tr>
<td class="cell-user">
<img class="user-picture" title="{{contributor.contributorId | sqxUserName}}" [attr.src]="contributor.contributorId | sqxUserPicture" />
<img class="user-picture" title="{{contributor.contributorName}}" [attr.src]="contributor.contributorId | sqxUserPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{contributor.contributorId | sqxUserName}}</span>
<span class="user-name table-cell" [innerHTML]="contributor.contributorName | sqxHighlight:contributorsState.snapshot.queryRegex"></span>
</td>
<td class="cell-time">
<select class="form-control"
@ -49,6 +56,15 @@
</tbody>
</table>
<sqx-pager [pager]="contributorsState.contributorsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager>
</ng-container>
<ng-template #noContributors>
<div class="table-items-row table-items-row-empty">
No contributors found.
</div>
</ng-template>
<ng-container *ngIf="contributorsState.canCreate | async">
<div class="table-items-footer">
<sqx-form-alert marginTop="0" marginBottom="2" white="true">

12
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -78,6 +78,18 @@ export class ContributorsPageComponent implements OnInit {
this.contributorsState.load(true);
}
public goPrev() {
this.contributorsState.goPrev();
}
public goNext() {
this.contributorsState.goNext();
}
public search(query: string) {
this.contributorsState.search(query);
}
public remove(contributor: ContributorDto) {
this.contributorsState.revoke(contributor);
}

16
src/Squidex/app/framework/angular/forms/dropdown.component.ts

@ -24,7 +24,7 @@ interface State {
suggestedItems: any[];
selectedItem: any;
selectedIndex: number;
query?: string;
query?: RegExp;
}
@Component({
@ -65,19 +65,17 @@ export class DropdownComponent extends StatefulControlComponent<State, any[]> im
public ngOnInit() {
this.own(
this.queryInput.valueChanges.pipe(
map((query: string) => {
if (!this.items || !query) {
return { query, items: this.items };
map((queryText: string) => {
if (!this.items || !queryText) {
return { query: undefined, items: this.items };
} else {
query = query.trim().toLocaleLowerCase();
const query = new RegExp(queryText, 'i');
const items = this.items.filter(x => {
if (Types.isString(x)) {
return x.toLocaleLowerCase().indexOf(query) >= 0;
return query.test(x);
} else {
const value: string = x[this.searchProperty];
return value && value.toLocaleLowerCase().indexOf(query) >= 0;
return query.test(x[this.searchProperty]);
}
});

10
src/Squidex/app/framework/angular/highlight.pipe.ts

@ -9,16 +9,22 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Types } from '@app/framework/internal';
@Pipe({
name: 'sqxHighlight',
pure: false
})
export class HighlightPipe implements PipeTransform {
public transform(text: string, highlight: string): string {
public transform(text: string, highlight: string | RegExp | undefined): string {
if (!highlight) {
return text;
}
return text.replace(new RegExp(highlight, 'i'), s => `<b>${s}</b>`);
if (Types.isString(highlight)) {
highlight = new RegExp(highlight, 'i');
}
return text.replace(highlight, s => `<b>${s}</b>`);
}
}

8
src/Squidex/app/shared/services/assets.service.spec.ts

@ -308,9 +308,9 @@ describe('AssetsService', () => {
return {
id: `id${id}`,
created: `${id % 1000 + 2000}-12-12T10:10:00`,
createdBy: `creator-${id}`,
createdBy: `creator${id}`,
lastModified: `${id % 1000 + 2000}-11-11T10:10:00`,
lastModifiedBy: `modifier-${id}`,
lastModifiedBy: `modifier${id}`,
fileName: `My Name${id}${suffix}.png`,
fileHash: `My Hash${id}${suffix}`,
fileType: 'png',
@ -344,8 +344,8 @@ export function createAsset(id: number, tags?: string[], suffix = '') {
return new AssetDto(links, meta,
`id${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier${id}`,
`My Name${id}${suffix}.png`,
`My Hash${id}${suffix}`,
'png',

8
src/Squidex/app/shared/services/contents.service.spec.ts

@ -356,9 +356,9 @@ describe('ContentsService', () => {
status: `Status${id}`,
statusColor: 'black',
created: `${id % 1000 + 2000}-12-12T10:10:00`,
createdBy: `creator-${id}`,
createdBy: `creator${id}`,
lastModified: `${id % 1000 + 2000}-11-11T10:10:00`,
lastModifiedBy: `modifier-${id}`,
lastModifiedBy: `modifier${id}`,
scheduleJob: {
status: 'Draft',
scheduledBy: `Scheduler${id}`,
@ -385,8 +385,8 @@ export function createContent(id: number, suffix = '') {
`id${id}`,
`Status${id}${suffix}`,
'black',
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier${id}`,
new ScheduleDto('Draft', `Scheduler${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`)),
true,
{},

6
src/Squidex/app/shared/services/contributors.service.spec.ts

@ -120,7 +120,9 @@ describe('ContributorsService', () => {
function contributorsResponse(...ids: number[]) {
return {
items: ids.map(id => ({
contributorId: `id${id}`, role: id % 2 === 0 ? 'Owner' : 'Developer',
contributorId: `id${id}`,
contributorName: `name${id}`,
role: id % 2 === 0 ? 'Owner' : 'Developer',
_links: {
update: { method: 'PUT', href: `/contributors/id${id}` }
}
@ -155,5 +157,5 @@ export function createContributor(id: number) {
update: { method: 'PUT', href: `/contributors/id${id}` }
};
return new ContributorDto(links, `id${id}`, id % 2 === 0 ? 'Owner' : 'Developer');
return new ContributorDto(links, `id${id}`, `name${id}`, id % 2 === 0 ? 'Owner' : 'Developer');
}

2
src/Squidex/app/shared/services/contributors.service.ts

@ -41,6 +41,7 @@ export class ContributorDto {
constructor(
links: ResourceLinks,
public readonly contributorId: string,
public readonly contributorName: string,
public readonly role: string
) {
this._links = links;
@ -112,6 +113,7 @@ function parseContributors(response: any) {
const items = raw.map(item =>
new ContributorDto(item._links,
item.contributorId,
item.contributorName,
item.role));
const { maxContributors, _links, _meta } = response;

8
src/Squidex/app/shared/services/rules.service.spec.ts

@ -359,9 +359,9 @@ describe('RulesService', () => {
return {
id: `id${id}`,
created: `${id % 1000 + 2000}-12-12T10:10`,
createdBy: `creator-${id}`,
createdBy: `creator${id}`,
lastModified: `${id % 1000 + 2000}-11-11T10:10`,
lastModifiedBy: `modifier-${id}`,
lastModifiedBy: `modifier${id}`,
isEnabled: id % 2 === 0,
trigger: {
param1: 1,
@ -404,8 +404,8 @@ export function createRule(id: number, suffix = '') {
return new RuleDto(links,
`id${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier${id}`,
new Version(`${id}`),
id % 2 === 0,
{

16
src/Squidex/app/shared/services/schemas.service.spec.ts

@ -612,9 +612,9 @@ describe('SchemasService', () => {
isSingleton: id % 2 === 0,
isPublished: id % 3 === 0,
created: `${id % 1000 + 2000}-12-12T10:10:00`,
createdBy: `creator-${id}`,
createdBy: `creator${id}`,
lastModified: `${id % 1000 + 2000}-11-11T10:10:00`,
lastModifiedBy: `modifier-${id}`,
lastModifiedBy: `modifier${id}`,
properties: {
label: `label${id}${suffix}`,
hints: `hints${id}${suffix}`
@ -634,9 +634,9 @@ describe('SchemasService', () => {
isSingleton: id % 2 === 0,
isPublished: id % 3 === 0,
created: `${id % 1000 + 2000}-12-12T10:10:00`,
createdBy: `creator-${id}`,
createdBy: `creator${id}`,
lastModified: `${id % 1000 + 2000}-11-11T10:10:00`,
lastModifiedBy: `modifier-${id}`,
lastModifiedBy: `modifier${id}`,
properties: {
label: `label${id}${suffix}`,
hints: `hints${id}${suffix}`
@ -816,8 +816,8 @@ export function createSchema(id: number, suffix = '') {
new SchemaPropertiesDto(`label${id}${suffix}`, `hints${id}${suffix}`),
id % 2 === 0,
id % 3 === 0,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier${id}`,
new Version(`${id}`));
}
@ -833,8 +833,8 @@ export function createSchemaDetails(id: number, version: Version, suffix = '') {
new SchemaPropertiesDto(`label${id}${suffix}`, `hints${id}${suffix}`),
id % 2 === 0,
id % 3 === 0,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier${id}`,
version,
[
new RootFieldDto({}, 11, 'field11', createProperties('Array'), 'language', true, true, true, [

8
src/Squidex/app/shared/state/clients.state.ts

@ -110,7 +110,13 @@ export class ClientsState extends State<Snapshot> {
const clients = ImmutableArray.of(items);
this.next(s => {
return { ...s, clients, isLoaded: true, version, canCreate };
return {
...s,
canCreate,
clients,
isLoaded: true,
version
};
});
}

2
src/Squidex/app/shared/state/contents.state.ts

@ -96,7 +96,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
private readonly contentsService: ContentsService,
private readonly dialogs: DialogService
) {
super({ contents: ImmutableArray.of(), contentsPager: new Pager(0), contentsQueryJson: '' });
super({ contents: ImmutableArray.empty(), contentsPager: new Pager(0), contentsQueryJson: '' });
}
public select(id: string | null): Observable<ContentDto | null> {

77
src/Squidex/app/shared/state/contributors.state.spec.ts

@ -9,6 +9,7 @@ import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import {
ContributorDto,
ContributorsPayload,
ContributorsService,
ContributorsState,
@ -28,7 +29,13 @@ describe('ContributorsState', () => {
version
} = TestValues;
const oldContributors = createContributors(1, 2, 3);
let allIds: number[] = [];
for (let i = 1; i <= 20; i++) {
allIds.push(i);
}
const oldContributors = createContributors(...allIds);
let dialogs: IMock<DialogService>;
let contributorsService: IMock<ContributorsService>;
@ -39,6 +46,9 @@ describe('ContributorsState', () => {
contributorsService = Mock.ofType<ContributorsService>();
contributorsState = new ContributorsState(contributorsService.object, appsState.object, dialogs.object);
contributorsService.setup(x => x.getContributors(app))
.returns(() => of(versioned(version, oldContributors))).verifiable();
});
afterEach(() => {
@ -47,9 +57,6 @@ describe('ContributorsState', () => {
describe('Loading', () => {
it('should load contributors', () => {
contributorsService.setup(x => x.getContributors(app))
.returns(() => of(versioned(version, oldContributors))).verifiable();
contributorsState.load().subscribe();
expect(contributorsState.snapshot.contributors.values).toEqual(oldContributors.items);
@ -60,10 +67,63 @@ describe('ContributorsState', () => {
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
});
it('should show notification on load when reload is true', () => {
contributorsService.setup(x => x.getContributors(app))
.returns(() => of(versioned(version, oldContributors))).verifiable();
it('should only current page of contributors', () => {
contributorsState.load().subscribe();
let contributors: ContributorDto[];
contributorsState.contributorsPaged.subscribe(result => {
contributors = result.values;
});
expect(contributors!).toEqual(oldContributors.items.slice(0, 10));
expect(contributorsState.snapshot.page).toEqual(0);
});
it('should show next of contributors when going next', () => {
contributorsState.load().subscribe();
contributorsState.goNext();
let contributors: ContributorDto[];
contributorsState.contributorsPaged.subscribe(result => {
contributors = result.values;
});
expect(contributors!).toEqual(oldContributors.items.slice(10, 20));
expect(contributorsState.snapshot.page).toEqual(1);
});
it('should show next of contributors when going prev', () => {
contributorsState.load().subscribe();
contributorsState.goNext();
contributorsState.goPrev();
let contributors: ContributorDto[];
contributorsState.contributorsPaged.subscribe(result => {
contributors = result.values;
});
expect(contributors!).toEqual(oldContributors.items.slice(0, 10));
expect(contributorsState.snapshot.page).toEqual(0);
});
it('should show filtered contributors when searching', () => {
contributorsState.load().subscribe();
contributorsState.search('4');
let contributors: ContributorDto[];
contributorsState.contributorsPaged.subscribe(result => {
contributors = result.values;
});
expect(contributors!).toEqual(createContributors(4, 14).items);
expect(contributorsState.snapshot.page).toEqual(0);
});
it('should show notification on load when reload is true', () => {
contributorsState.load(true).subscribe();
expect().nothing();
@ -74,9 +134,6 @@ describe('ContributorsState', () => {
describe('Updates', () => {
beforeEach(() => {
contributorsService.setup(x => x.getContributors(app))
.returns(() => of(versioned(version, oldContributors))).verifiable();
contributorsState.load().subscribe();
});

73
src/Squidex/app/shared/state/contributors.state.ts

@ -6,13 +6,14 @@
*/
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { combineLatest, Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import {
DialogService,
ErrorDto,
ImmutableArray,
Pager,
shareMapSubscribed,
shareSubscribed,
State,
@ -39,6 +40,15 @@ interface Snapshot {
// The maximum allowed users.
maxContributors: number;
// The current page.
page: number;
// The search query.
query?: string;
// Query regex.
queryRegex?: RegExp;
// The app version.
version: Version;
@ -53,21 +63,39 @@ export class ContributorsState extends State<Snapshot> {
public contributors =
this.project(x => x.contributors);
public isLoaded =
this.project(x => !!x.isLoaded);
public page =
this.project(x => x.page);
public query =
this.project(x => x.query);
public queryRegex =
this.project(x => x.queryRegex);
public maxContributors =
this.project(x => x.maxContributors);
public isLoaded =
this.project(x => !!x.isLoaded);
public canCreate =
this.project(x => !!x.canCreate);
public filtered =
combineLatest(this.queryRegex, this.contributors, (q, c) => getFilteredContributors(c, q));
public contributorsPaged =
combineLatest(this.page, this.filtered, (p, c) => getPagedContributors(c, p));
public contributorsPager =
combineLatest(this.page, this.filtered, (p, c) => new Pager(c.length, p, PAGE_SIZE));
constructor(
private readonly contributorsService: ContributorsService,
private readonly appsState: AppsState,
private readonly dialogs: DialogService
) {
super({ contributors: ImmutableArray.empty(), version: Version.EMPTY, maxContributors: -1 });
super({ contributors: ImmutableArray.empty(), page: 0, maxContributors: -1, version: Version.EMPTY });
}
public load(isReload = false): Observable<any> {
@ -86,6 +114,18 @@ export class ContributorsState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public goNext() {
this.next(s => ({ ...s, page: s.page + 1 }));
}
public goPrev() {
this.next(s => ({ ...s, page: s.page - 1 }));
}
public search(query: string) {
this.next(s => ({ ...s, query, queryRegex: new RegExp(query, 'i') }));
}
public revoke(contributor: ContributorDto): Observable<any> {
return this.contributorsService.deleteContributor(this.appName, contributor, this.version).pipe(
tap(({ version, payload }) => {
@ -115,7 +155,14 @@ export class ContributorsState extends State<Snapshot> {
const contributors = ImmutableArray.of(items);
return { ...s, contributors, maxContributors, isLoaded: true, version, canCreate };
return {
canCreate,
contributors,
isLoaded: true,
maxContributors,
page: 0,
version
};
});
}
@ -127,3 +174,19 @@ export class ContributorsState extends State<Snapshot> {
return this.snapshot.version;
}
}
const PAGE_SIZE = 10;
function getPagedContributors(contributors: ContributorsList, page: number) {
return ImmutableArray.of(contributors.values.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE));
}
function getFilteredContributors(contributors: ContributorsList, query?: RegExp) {
let filtered = contributors;
if (query) {
filtered = filtered.filter(x => query.test(x.contributorName));
}
return filtered;
}

7
src/Squidex/app/shared/state/rules.state.ts

@ -76,7 +76,12 @@ export class RulesState extends State<Snapshot> {
this.next(s => {
const rules = ImmutableArray.of(items);
return { ...s, rules, isLoaded: true, canCreate, canReadEvents };
return { ...s,
canCreate,
canReadEvents,
isLoaded: true,
rules
};
});
}),
shareSubscribed(this.dialogs));

8
src/Squidex/app/theme/_lists.scss

@ -144,11 +144,19 @@
}
// Footer that typically contains an add-item-form.
&-header,
&-footer {
padding: 1rem 1.25rem;
background: $color-table-footer;
border: 1px solid $color-border;
border-bottom-width: 2px;
}
&-header {
margin-bottom: .8rem;
}
&-footer {
margin-top: .8rem;
}

31
tools/LoadTest/TestUtils.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using LoadTest.Model;
using Squidex.ClientLibrary.Management;
using Xunit;
namespace LoadTest
{
public sealed class TestUtils
{
[Fact]
public async Task GenerateAppManyContributorsAsync()
{
var client = TestClient.ClientManager.CreateAppsClient();
for (var i = 0; i < 200; i++)
{
await client.PostContributorAsync("test", new AssignContributorDto
{
ContributorId = $"hello{i}@squidex.io", Invite = true, Role = "Editor"
});
}
}
}
}

1
tools/LoadTest/WritingBenchmarks.cs

@ -9,7 +9,6 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LoadTest.Model;
using LoadTest.Utils;
using Xunit;
namespace LoadTest

Loading…
Cancel
Save