From 42e6b3feabb80494f1df9144a1251a02a9de9b2b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 3 Sep 2019 22:07:26 +0200 Subject: [PATCH 1/3] Feature/contributor import (#409) * Variable naming fixed. * Tests improved and contributor import dialog. --- .../Apps/Commands/AssignContributor.cs | 6 +- .../Apps/Guards/GuardAppContributors.cs | 44 +++++---- .../Invitation/InviteUserCommandMiddleware.cs | 26 ++--- .../Assets/AssetGrain.cs | 8 +- .../Apps/Models/AssignContributorDto.cs | 5 +- .../schema/schema-edit-form.component.scss | 2 +- .../app/features/settings/declarations.ts | 1 + src/Squidex/app/features/settings/module.ts | 2 + .../pages/backups/backups-page.component.html | 10 +- .../pages/backups/backups-page.component.scss | 29 +----- .../contributors-page.component.html | 10 ++ .../contributors-page.component.ts | 15 +-- .../import-contributors-dialog.component.html | 43 +++++++++ .../import-contributors-dialog.component.scss | 10 ++ .../import-contributors-dialog.component.ts | 96 +++++++++++++++++++ .../angular/forms/form-hint.component.ts | 9 +- .../angular/status-icon.component.html | 14 +++ .../angular/status-icon.component.scss | 41 ++++++++ .../angular/status-icon.component.ts | 25 +++++ src/Squidex/app/framework/declarations.ts | 7 +- src/Squidex/app/framework/module.ts | 3 + .../app/shared/state/contributors.forms.ts | 60 +++++++++++- .../app/shared/state/contributors.state.ts | 4 +- .../Apps/Guards/GuardAppContributorsTests.cs | 6 +- .../InviteUserCommandMiddlewareTests.cs | 48 ++++++---- ...SchemasByAppIndexCommandMiddlewareTests.cs | 7 +- .../ETagCommandMiddlewareTests.cs | 13 ++- .../EnrichWithActorCommandMiddlewareTests.cs | 15 ++- .../EnrichWithAppIdCommandMiddlewareTests.cs | 15 ++- ...nrichWithSchemaIdCommandMiddlewareTests.cs | 19 ++-- 30 files changed, 446 insertions(+), 147 deletions(-) create mode 100644 src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html create mode 100644 src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss create mode 100644 src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts create mode 100644 src/Squidex/app/framework/angular/status-icon.component.html create mode 100644 src/Squidex/app/framework/angular/status-icon.component.scss create mode 100644 src/Squidex/app/framework/angular/status-icon.component.ts diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs index e31a6a55d..dd612658c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs @@ -13,12 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { public string ContributorId { get; set; } - public string Role { get; set; } = Roles.Developer; + public string Role { get; set; } = Roles.Editor; public bool IsRestore { get; set; } - public bool IsInviting { get; set; } - - public bool IsCreated { get; set; } + public bool Invite { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs index b43d4f502..962198cf4 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs @@ -32,35 +32,39 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards if (string.IsNullOrWhiteSpace(command.ContributorId)) { e(Not.Defined("Contributor id"), nameof(command.ContributorId)); - return; } - - var user = await users.FindByIdOrEmailAsync(command.ContributorId); - - if (user == null) + else { - throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); - } - - command.ContributorId = user.Id; + var user = await users.FindByIdOrEmailAsync(command.ContributorId); - if (!command.IsRestore) - { - if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) + if (user == null) { - throw new DomainForbiddenException("You cannot change your own role."); + throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); } - if (contributors.TryGetValue(command.ContributorId, out var existing)) + command.ContributorId = user.Id; + + if (!command.IsRestore) { - if (existing == command.Role) + if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) { - e(Not.New("Contributor", "role"), nameof(command.Role)); + throw new DomainForbiddenException("You cannot change your own role."); + } + + if (contributors.TryGetValue(command.ContributorId, out var role)) + { + if (role == command.Role) + { + e(Not.New("Contributor", "role"), nameof(command.Role)); + } + } + else + { + if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) + { + e("You have reached the maximum number of contributors for your plan."); + } } - } - else if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) - { - e("You have reached the maximum number of contributors for your plan."); } } }); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs index 7bde0a4cd..084d1270c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs @@ -27,24 +27,26 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation public async Task HandleAsync(CommandContext context, Func next) { - if (context.Command is AssignContributor assignContributor) + if (context.Command is AssignContributor assignContributor && ShouldInvite(assignContributor)) { - if (assignContributor.IsInviting && assignContributor.ContributorId.IsEmail()) - { - assignContributor.IsCreated = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId, true); - - await next(); + var created = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId, true); - if (assignContributor.IsCreated && context.PlainResult is IAppEntity app) - { - context.Complete(new InvitedResult { App = app }); - } + await next(); - return; + if (created && context.PlainResult is IAppEntity app) + { + context.Complete(new InvitedResult { App = app }); } } + else + { + await next(); + } + } - await next(); + private bool ShouldInvite(AssignContributor assignContributor) + { + return assignContributor.Invite && assignContributor.ContributorId.IsEmail(); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index 86ffb3bcf..23c80b139 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -52,8 +52,8 @@ namespace Squidex.Domain.Apps.Entities.Assets switch (command) { - case CreateAsset createRule: - return CreateReturnAsync(createRule, async c => + case CreateAsset createAsset: + return CreateReturnAsync(createAsset, async c => { GuardAsset.CanCreate(c); @@ -63,8 +63,8 @@ namespace Squidex.Domain.Apps.Entities.Assets return Snapshot; }); - case UpdateAsset updateRule: - return UpdateReturn(updateRule, c => + case UpdateAsset updateAsset: + return UpdateReturn(updateAsset, c => { GuardAsset.CanUpdate(c); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs index ede15b4d8..0df2db061 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs @@ -8,6 +8,7 @@ using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Reflection; +using Roles = Squidex.Domain.Apps.Core.Apps.Role; namespace Squidex.Areas.Api.Controllers.Apps.Models { @@ -22,7 +23,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// /// The role of the contributor. /// - public string Role { get; set; } + public string Role { get; set; } = Roles.Developer; /// /// Set to true to invite the user if he does not exist. @@ -31,7 +32,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public AssignContributor ToCommand() { - return SimpleMapper.Map(this, new AssignContributor { IsInviting = Invite }); + return SimpleMapper.Map(this, new AssignContributor()); } } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss b/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss index 8b410ef66..eae569083 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss +++ b/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss @@ -1,6 +1,6 @@ @import '_vars'; @import '_mixins'; -texarea { +textarea { resize: none; } \ No newline at end of file diff --git a/src/Squidex/app/features/settings/declarations.ts b/src/Squidex/app/features/settings/declarations.ts index 1a0553935..32b8bfd08 100644 --- a/src/Squidex/app/features/settings/declarations.ts +++ b/src/Squidex/app/features/settings/declarations.ts @@ -10,6 +10,7 @@ export * from './pages/backups/pipes'; export * from './pages/clients/client.component'; export * from './pages/clients/clients-page.component'; export * from './pages/contributors/contributors-page.component'; +export * from './pages/contributors/import-contributors-dialog.component'; export * from './pages/languages/language.component'; export * from './pages/languages/languages-page.component'; export * from './pages/more/more-page.component'; diff --git a/src/Squidex/app/features/settings/module.ts b/src/Squidex/app/features/settings/module.ts index 031f1a72e..18d5347c5 100644 --- a/src/Squidex/app/features/settings/module.ts +++ b/src/Squidex/app/features/settings/module.ts @@ -21,6 +21,7 @@ import { ClientComponent, ClientsPageComponent, ContributorsPageComponent, + ImportContributorsDialogComponent, LanguageComponent, LanguagesPageComponent, MorePageComponent, @@ -203,6 +204,7 @@ const routes: Routes = [ ClientComponent, ClientsPageComponent, ContributorsPageComponent, + ImportContributorsDialogComponent, LanguageComponent, LanguagesPageComponent, MorePageComponent, diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html index f55d7a783..562999057 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html @@ -34,15 +34,7 @@
-
- -
-
- -
-
- -
+
diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss b/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss index 7d14e7dd0..fbb752506 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss @@ -1,29 +1,2 @@ @import '_vars'; -@import '_mixins'; - -$circle-size: 2.8rem; - -.backup-status { - & { - @include circle($circle-size); - line-height: $circle-size + .1rem; - text-align: center; - font-size: .4 * $circle-size; - font-weight: normal; - background: $color-border; - color: $color-dark-foreground; - vertical-align: middle; - } - - &-pending { - color: inherit; - } - - &-failed { - background: $color-theme-error; - } - - &-success { - background: $color-theme-green; - } -} \ No newline at end of file +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html index d59f1c7ca..011e94b55 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html @@ -74,6 +74,10 @@
+ + + Big team? Hide many contributors at once + @@ -92,4 +96,10 @@ + + + + + \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts index 0d5e85e6b..6a262f677 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts @@ -16,10 +16,9 @@ import { AutocompleteSource, ContributorDto, ContributorsState, + DialogModel, DialogService, RolesState, - Types, - UserDto, UsersService } from '@app/shared'; @@ -57,6 +56,8 @@ export class UsersDataSource implements AutocompleteSource { export class ContributorsPageComponent implements OnInit { public assignContributorForm = new AssignContributorForm(this.formBuilder); + public importDialog = new DialogModel(); + constructor( public readonly appsState: AppsState, public readonly contributorsState: ContributorsState, @@ -89,15 +90,7 @@ export class ContributorsPageComponent implements OnInit { const value = this.assignContributorForm.submit(); if (value) { - let user = value.user; - - if (Types.is(user, UserDto)) { - user = user.id; - } - - const requestDto = { contributorId: user, role: 'Editor', invite: true }; - - this.contributorsState.assign(requestDto) + this.contributorsState.assign(value) .subscribe(isCreated => { this.assignContributorForm.submitCompleted(); diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html b/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html new file mode 100644 index 000000000..714e44347 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html @@ -0,0 +1,43 @@ +
+ + + Import contributors + + + +
+ +
+
+ {{status.email}} +
+ +
+ + +
+
+
+ + + + +
+ + + + Emails detected: {{emails}} + + +   + +
+ + + + + +
+
diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss b/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss new file mode 100644 index 000000000..21a9d0e8e --- /dev/null +++ b/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss @@ -0,0 +1,10 @@ +@import '_vars'; +@import '_mixins'; + +textarea { + resize: none; +} + +.content { + min-height: 300px; +} \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts b/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts new file mode 100644 index 000000000..170a7a244 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts @@ -0,0 +1,96 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { empty, of } from 'rxjs'; +import { catchError, mergeMap, tap } from 'rxjs/operators'; + +import { + ContributorsState, + ErrorDto, + ImportContributorsForm +} from '@app/shared'; + +interface ImportStatus { + email: string; + result: 'Pending' | 'Failed' | 'Success'; + resultText: string; +} + +@Component({ + selector: 'sqx-import-contributors-dialog', + styleUrls: ['./import-contributors-dialog.component.scss'], + templateUrl: './import-contributors-dialog.component.html' +}) +export class ImportContributorsDialogComponent { + @Output() + public close = new EventEmitter(); + + public importForm = new ImportContributorsForm(this.formBuilder); + public importStatus: ImportStatus[]; + + constructor( + private readonly formBuilder: FormBuilder, + private readonly contributorsState: ContributorsState + ) { + } + + public import() { + const contributors = this.importForm.submit(); + + if (contributors && contributors.length > 0) { + this.importStatus = []; + + for (let contributor of contributors) { + this.importStatus.push({ + email: contributor.contributorId, + result: 'Pending', + resultText: 'Pending' + }); + } + + of(...contributors).pipe( + mergeMap(c => + this.contributorsState.assign(c, { silent: true }).pipe( + tap(created => { + let status = this.importStatus.find(x => x.email === c.contributorId); + + if (status) { + status.resultText = getSuccess(created); + status.result = 'Success'; + } + }), + catchError((error: ErrorDto) => { + let status = this.importStatus.find(x => x.email === c.contributorId); + + if (status) { + status.resultText = getError(error); + status.result = 'Failed'; + } + + return empty(); + }) + ), 1) + ).subscribe(); + } + } + + public emitClose() { + this.close.emit(); + } +} + +function getError(error: ErrorDto): string { + return error.details[0]; +} + +function getSuccess(created: boolean | undefined): string { + return created ? + 'User has been invited and assigned.' : + 'User has been assigned'; +} diff --git a/src/Squidex/app/framework/angular/forms/form-hint.component.ts b/src/Squidex/app/framework/angular/forms/form-hint.component.ts index 5e1da1d1f..00e241b2a 100644 --- a/src/Squidex/app/framework/angular/forms/form-hint.component.ts +++ b/src/Squidex/app/framework/angular/forms/form-hint.component.ts @@ -5,14 +5,17 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; @Component({ selector: 'sqx-form-hint', template: ` - + `, changeDetection: ChangeDetectionStrategy.OnPush }) -export class FormHintComponent {} \ No newline at end of file +export class FormHintComponent { + @Input() + public class: string; +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/status-icon.component.html b/src/Squidex/app/framework/angular/status-icon.component.html new file mode 100644 index 000000000..92f203ff9 --- /dev/null +++ b/src/Squidex/app/framework/angular/status-icon.component.html @@ -0,0 +1,14 @@ + +
+ +
+
+ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/status-icon.component.scss b/src/Squidex/app/framework/angular/status-icon.component.scss new file mode 100644 index 000000000..54a53a174 --- /dev/null +++ b/src/Squidex/app/framework/angular/status-icon.component.scss @@ -0,0 +1,41 @@ +@import '_mixins'; +@import '_vars'; + +$circle-size-sm: 1.6rem; + +$circle-size-lg: 2.8rem; + +.status { + & { + text-align: center; + background: $color-border; + color: $color-dark-foreground; + vertical-align: middle; + } + + &.sm { + @include circle($circle-size-sm); + line-height: $circle-size-sm + .1rem; + font-size: .5 * $circle-size-sm; + font-weight: normal; + } + + &.lg { + @include circle($circle-size-lg); + line-height: $circle-size-lg + .1rem; + font-size: .5 * $circle-size-lg; + font-weight: normal; + } + + &-pending { + color: inherit; + } + + &-failed { + background: $color-theme-error; + } + + &-success { + background: $color-theme-green; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/status-icon.component.ts b/src/Squidex/app/framework/angular/status-icon.component.ts new file mode 100644 index 000000000..0ea13974e --- /dev/null +++ b/src/Squidex/app/framework/angular/status-icon.component.ts @@ -0,0 +1,25 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'sqx-status-icon', + styleUrls: ['./status-icon.component.scss'], + templateUrl: './status-icon.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class StatusIconComponent { + @Input() + public status: 'Started' | 'Failed' | 'Success' | 'Completed' | 'Failed' | 'Pending'; + + @Input() + public statusText: string; + + @Input() + public size: 'lg' | 'sm' = 'lg'; +} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 1a1fe2b79..d6fbdd908 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -55,18 +55,19 @@ export * from './angular/routers/parent-link.directive'; export * from './angular/code.component'; export * from './angular/external-link.directive'; -export * from './angular/hover-background.directive'; export * from './angular/highlight.pipe'; +export * from './angular/hover-background.directive'; export * from './angular/ignore-scrollbar.directive'; export * from './angular/image-source.directive'; -export * from './angular/panel.component'; -export * from './angular/panel-container.directive'; export * from './angular/pager.component'; +export * from './angular/panel-container.directive'; +export * from './angular/panel.component'; export * from './angular/popup-link.directive'; export * from './angular/safe-html.pipe'; export * from './angular/scroll-active.directive'; export * from './angular/shortcut.component'; export * from './angular/sorted.directive'; +export * from './angular/status-icon.component'; export * from './angular/stop-click.directive'; export * from './angular/sync-scrolling.directive'; export * from './angular/template-wrapper.directive'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index dd6854957..03c71419e 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -82,6 +82,7 @@ import { ShortTimePipe, SortedDirective, StarsComponent, + StatusIconComponent, StopClickDirective, SyncScollingDirective, TagEditorComponent, @@ -159,6 +160,7 @@ import { ShortTimePipe, SortedDirective, StarsComponent, + StatusIconComponent, StopClickDirective, SyncScollingDirective, TagEditorComponent, @@ -230,6 +232,7 @@ import { ShortTimePipe, SortedDirective, StarsComponent, + StatusIconComponent, StopClickDirective, SyncScollingDirective, TagEditorComponent, diff --git a/src/Squidex/app/shared/state/contributors.forms.ts b/src/Squidex/app/shared/state/contributors.forms.ts index e709f5637..6cc83a631 100644 --- a/src/Squidex/app/shared/state/contributors.forms.ts +++ b/src/Squidex/app/shared/state/contributors.forms.ts @@ -6,12 +6,20 @@ */ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { map } from 'rxjs/operators'; -import { Form, hasNoValue$ } from '@app/framework'; +import { + Form, + hasNoValue$, + Types, + value$ +} from '@app/framework'; + +import { AssignContributorDto } from '../services/contributors.service'; import { UserDto } from './../services/users.service'; -export class AssignContributorForm extends Form { +export class AssignContributorForm extends Form { public hasNoUser = hasNoValue$(this.form.controls['user']); constructor(formBuilder: FormBuilder) { @@ -23,4 +31,50 @@ export class AssignContributorForm extends Form { + public numberOfEmails = value$(this.form.controls['import']).pipe(map(v => extractEmails(v).length)); + + public hasNoUser = this.numberOfEmails.pipe(map(v => v === 0)); + + constructor(formBuilder: FormBuilder) { + super(formBuilder.group({ + import: ['', + [ + Validators.required + ] + ] + })); + } + + protected transformSubmit(value: { import: string }) { + return extractEmails(value.import); + } +} + +function extractEmails(value: string) { + let result: AssignContributorDto[] = []; + + if (value) { + let emails = value.match(EMAIL_REGEX); + + if (emails) { + for (let match of emails) { + result.push({ contributorId: match, role: 'Editor', invite: true }); + } + } + } + + return result; +} + +const EMAIL_REGEX = /(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*/gim; \ No newline at end of file diff --git a/src/Squidex/app/shared/state/contributors.state.ts b/src/Squidex/app/shared/state/contributors.state.ts index cfcd6d80f..c4168c74e 100644 --- a/src/Squidex/app/shared/state/contributors.state.ts +++ b/src/Squidex/app/shared/state/contributors.state.ts @@ -94,7 +94,7 @@ export class ContributorsState extends State { shareSubscribed(this.dialogs)); } - public assign(request: AssignContributorDto): Observable { + public assign(request: AssignContributorDto, options?: { silent: boolean }): Observable { return this.contributorsService.postContributor(this.appName, request, this.version).pipe( catchError(error => { if (Types.is(error, ErrorDto) && error.statusCode === 404) { @@ -106,7 +106,7 @@ export class ContributorsState extends State { tap(({ version, payload }) => { this.replaceContributors(version, payload); }), - shareMapSubscribed(this.dialogs, x => x.payload._meta && x.payload._meta['isInvited'] === '1')); + shareMapSubscribed(this.dialogs, x => x.payload._meta && x.payload._meta['isInvited'] === '1', options)); } private replaceContributors(version: Version, payload: ContributorsPayload) { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs index 6fecf76ef..a316b110d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { var command = new AssignContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Editor); + var contributors_1 = contributors_0.Assign("1", Role.Developer); await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); } @@ -159,8 +159,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards var command = new AssignContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Editor); - var contributors_2 = contributors_1.Assign("2", Role.Editor); + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("2", Role.Developer); await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs index 2f504edc3..9e808a16d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs @@ -20,6 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation public class InviteUserCommandMiddlewareTests { private readonly IUserResolver userResolver = A.Fake(); + private readonly IAppEntity app = Mocks.App(NamedId.Of(Guid.NewGuid(), "my-app")); private readonly ICommandBus commandBus = A.Fake(); private readonly InviteUserCommandMiddleware sut; @@ -31,19 +32,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation [Fact] public async Task Should_invite_user_and_change_result() { - var command = new AssignContributor { ContributorId = "me@email.com", IsInviting = true }; - var context = new CommandContext(command, commandBus); + var context = + new CommandContext(new AssignContributor { ContributorId = "me@email.com", Invite = true }, commandBus) + .Complete(app); A.CallTo(() => userResolver.CreateUserIfNotExists("me@email.com", true)) .Returns(true); - var result = Mocks.App(NamedId.Of(Guid.NewGuid(), "my-app")); - - context.Complete(result); - await sut.HandleAsync(context); - Assert.Same(context.Result().App, result); + Assert.Same(context.Result().App, app); A.CallTo(() => userResolver.CreateUserIfNotExists("me@email.com", true)) .MustHaveHappened(); @@ -52,34 +50,50 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation [Fact] public async Task Should_invite_user_and_not_change_result_if_not_added() { - var command = new AssignContributor { ContributorId = "me@email.com", IsInviting = true }; - var context = new CommandContext(command, commandBus); + var context = + new CommandContext(new AssignContributor { ContributorId = "me@email.com", Invite = true }, commandBus) + .Complete(app); A.CallTo(() => userResolver.CreateUserIfNotExists("me@email.com", true)) .Returns(false); - var result = Mocks.App(NamedId.Of(Guid.NewGuid(), "my-app")); - - context.Complete(result); - await sut.HandleAsync(context); - Assert.Same(context.Result(), result); + Assert.Same(context.Result(), app); A.CallTo(() => userResolver.CreateUserIfNotExists("me@email.com", true)) .MustHaveHappened(); } [Fact] - public async Task Should_not_calls_user_resolver_if_not_email() + public async Task Should_not_call_user_resolver_if_not_email() + { + var context = + new CommandContext(new AssignContributor { ContributorId = "123", Invite = true }, commandBus) + .Complete(app); + + await sut.HandleAsync(context); + + A.CallTo(() => userResolver.CreateUserIfNotExists(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_call_user_resolver_if_not_inviting() { - var command = new AssignContributor { ContributorId = "123", IsInviting = true }; - var context = new CommandContext(command, commandBus); + var context = + new CommandContext(new AssignContributor { ContributorId = "123", Invite = false }, commandBus) + .Complete(app); await sut.HandleAsync(context); A.CallTo(() => userResolver.CreateUserIfNotExists(A.Ignored, A.Ignored)) .MustNotHaveHappened(); } + + private CommandContext Context(AssignContributor command) + { + return new CommandContext(command, commandBus); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs index 06ddb1d39..58045e36a 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs @@ -38,10 +38,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes [Fact] public async Task Should_add_schema_to_index_on_create() { - var command = new CreateSchema { SchemaId = schemaId.Id, Name = schemaId.Name, AppId = appId }; - var context = new CommandContext(command, commandBus); - - context.Complete(); + var context = + new CommandContext(new CreateSchema { SchemaId = schemaId.Id, Name = schemaId.Name, AppId = appId }, commandBus) + .Complete(); await sut.HandleAsync(context); diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs index a9f8145fe..05b58b31b 100644 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs +++ b/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs @@ -38,7 +38,7 @@ namespace Squidex.Web.CommandMiddlewares .Returns(null); var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -51,7 +51,7 @@ namespace Squidex.Web.CommandMiddlewares httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -64,7 +64,7 @@ namespace Squidex.Web.CommandMiddlewares httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13"; var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -75,7 +75,7 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_add_etag_header_to_response() { var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); context.Complete(new EntitySavedResult(17)); @@ -83,5 +83,10 @@ namespace Squidex.Web.CommandMiddlewares Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } } } diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs index 8ecdb1e15..fb1ab5baa 100644 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs +++ b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs @@ -37,7 +37,7 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_throw_security_exception_when_no_subject_or_client_is_found() { var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await Assert.ThrowsAsync(() => sut.HandleAsync(context)); } @@ -49,7 +49,7 @@ namespace Squidex.Web.CommandMiddlewares .Returns(null); var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -62,7 +62,7 @@ namespace Squidex.Web.CommandMiddlewares httpContext.User = CreatePrincipal(OpenIdClaims.Subject, "me"); var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -75,7 +75,7 @@ namespace Squidex.Web.CommandMiddlewares httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -88,13 +88,18 @@ namespace Squidex.Web.CommandMiddlewares httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); var command = new CreateContent { Actor = new RefToken("subject", "me") }; - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); Assert.Equal(new RefToken("subject", "me"), command.Actor); } + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + private static ClaimsPrincipal CreatePrincipal(string claimType, string claimValue) { var claimsPrincipal = new ClaimsPrincipal(); diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs index 76dfb4b41..bf70f36df 100644 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -47,7 +47,7 @@ namespace Squidex.Web.CommandMiddlewares appContext.App = null; var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await Assert.ThrowsAsync(() => sut.HandleAsync(context)); } @@ -56,7 +56,7 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_assign_app_id_and_name_to_app_command() { var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -67,7 +67,7 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_assign_app_id_to_app_self_command() { var command = new ChangePlan(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -78,7 +78,7 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_not_override_app_id() { var command = new ChangePlan { AppId = Guid.NewGuid() }; - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -89,11 +89,16 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_not_override_app_id_and_name() { var command = new CreateContent { AppId = NamedId.Of(Guid.NewGuid(), "other-app") }; - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); Assert.NotEqual(appId, command.AppId); } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } } } diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs index e3959732f..5ebe6fe24 100644 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs +++ b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs @@ -72,7 +72,7 @@ namespace Squidex.Web.CommandMiddlewares actionContext.RouteData.Values["name"] = "other-schema"; var command = new CreateContent { AppId = appId }; - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await Assert.ThrowsAsync(() => sut.HandleAsync(context)); } @@ -81,7 +81,7 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_do_nothing_when_route_has_no_parameter() { var command = new CreateContent(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -94,7 +94,7 @@ namespace Squidex.Web.CommandMiddlewares actionContext.RouteData.Values["name"] = schemaId.Name; var command = new CreateContent { AppId = appId }; - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -107,7 +107,7 @@ namespace Squidex.Web.CommandMiddlewares actionContext.RouteData.Values["name"] = schemaId.Id; var command = new CreateContent { AppId = appId }; - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -120,7 +120,7 @@ namespace Squidex.Web.CommandMiddlewares actionContext.RouteData.Values["name"] = schemaId.Name; var command = new UpdateSchema(); - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -131,7 +131,7 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_not_override_schema_id() { var command = new CreateSchema { SchemaId = Guid.NewGuid() }; - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); @@ -142,11 +142,16 @@ namespace Squidex.Web.CommandMiddlewares public async Task Should_not_override_schema_id_and_name() { var command = new CreateContent { SchemaId = NamedId.Of(Guid.NewGuid(), "other-schema") }; - var context = new CommandContext(command, commandBus); + var context = Ctx(command); await sut.HandleAsync(context); Assert.NotEqual(appId, command.AppId); } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } } } From a85d49c25c470e49dc9b151332b974135bb412f8 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 3 Sep 2019 22:07:49 +0200 Subject: [PATCH 2/3] Pagination for contributors. (#408) * Pagination for contributors. --- .../DefaultUserResolver.cs | 8 ++ .../UserManagerExtensions.cs | 9 +++ src/Squidex.Shared/Users/IUserResolver.cs | 3 + .../Controllers/Apps/AppClientsController.cs | 9 ++- .../Apps/AppContributorsController.cs | 19 +++-- .../Apps/AppLanguagesController.cs | 9 ++- .../Controllers/Apps/AppPatternsController.cs | 9 ++- .../Controllers/Apps/AppRolesController.cs | 9 ++- .../Apps/AppWorkflowsController.cs | 9 ++- .../Controllers/Apps/Models/AppLanguageDto.cs | 6 +- .../Apps/Models/AppLanguagesDto.cs | 6 +- .../Api/Controllers/Apps/Models/ClientDto.cs | 6 +- .../Api/Controllers/Apps/Models/ClientsDto.cs | 10 ++- .../Controllers/Apps/Models/ContributorDto.cs | 30 +++++++- .../Apps/Models/ContributorsDto.cs | 38 +++++++-- .../Api/Controllers/Apps/Models/RoleDto.cs | 20 +++-- .../Api/Controllers/Apps/Models/RolesDto.cs | 11 ++- .../Controllers/Apps/Models/WorkflowDto.cs | 9 ++- .../Controllers/Apps/Models/WorkflowsDto.cs | 10 ++- .../Contents/Models/ContentsDto.cs | 9 +-- .../services/event-consumers.service.spec.ts | 8 +- .../contributors-page.component.html | 72 ++++++++++------- .../contributors-page.component.ts | 12 +++ .../angular/forms/dropdown.component.ts | 16 ++-- .../app/framework/angular/highlight.pipe.ts | 10 ++- .../shared/services/assets.service.spec.ts | 8 +- .../shared/services/contents.service.spec.ts | 8 +- .../services/contributors.service.spec.ts | 6 +- .../shared/services/contributors.service.ts | 2 + .../app/shared/services/rules.service.spec.ts | 8 +- .../shared/services/schemas.service.spec.ts | 16 ++-- src/Squidex/app/shared/state/clients.state.ts | 8 +- .../app/shared/state/contents.state.ts | 2 +- .../shared/state/contributors.state.spec.ts | 77 ++++++++++++++++--- .../app/shared/state/contributors.state.ts | 75 ++++++++++++++++-- src/Squidex/app/shared/state/rules.state.ts | 7 +- src/Squidex/app/theme/_lists.scss | 8 ++ tools/LoadTest/TestUtils.cs | 31 ++++++++ tools/LoadTest/WritingBenchmarks.cs | 1 - 39 files changed, 475 insertions(+), 139 deletions(-) create mode 100644 tools/LoadTest/TestUtils.cs diff --git a/src/Squidex.Domain.Users/DefaultUserResolver.cs b/src/Squidex.Domain.Users/DefaultUserResolver.cs index 49723af01..b03abae64 100644 --- a/src/Squidex.Domain.Users/DefaultUserResolver.cs +++ b/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().ToList(); } + + public async Task> QueryManyAsync(string[] ids) + { + var result = await userManager.QueryByIdsAync(ids); + + return result.OfType().ToDictionary(x => x.Id); + } } } diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs index 94176bfc0..4217194c0 100644 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -85,6 +85,15 @@ namespace Squidex.Domain.Users return Task.FromResult(count); } + public static async Task> QueryByIdsAync(this UserManager 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> QueryByEmailAsync(this UserManager userManager, string email = null, int take = 10, int skip = 0) { var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList(); diff --git a/src/Squidex.Shared/Users/IUserResolver.cs b/src/Squidex.Shared/Users/IUserResolver.cs index 29f3b541a..4406107a8 100644 --- a/src/Squidex.Shared/Users/IUserResolver.cs +++ b/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 FindByIdOrEmailAsync(string idOrEmail); Task> QueryByEmailAsync(string email); + + Task> QueryManyAsync(string[] ids); } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 7ce6c79b8..fb444d2a3 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/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(); - var response = ClientsDto.FromApp(result, this); + var response = GetResponse(result); return response; } + + private ClientsDto GetResponse(IAppEntity app) + { + return ClientsDto.FromApp(App, this); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index fda51ba82..7d5e1ed37 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/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; } /// @@ -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(), appPlansProvider, this, false); + return await GetResponseAsync(context.Result(), false); } } + + private Task GetResponseAsync(IAppEntity app, bool invited) + { + return ContributorsDto.FromAppAsync(app, this, userResolver, appPlansProvider, invited); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 43498aa82..f76da205d 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/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(); - 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 diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs index 31b788e83..da58aade8 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs +++ b/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(); - var response = PatternsDto.FromApp(result, this); + var response = GetResponse(result); return response; } + + private PatternsDto GetResponse(IAppEntity result) + { + return PatternsDto.FromApp(result, this); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index 2961a91b7..8a3aef0ba 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/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(); - var response = RolesDto.FromApp(result, this); + var response = GetResponse(result); return response; } + + private RolesDto GetResponse(IAppEntity result) + { + return RolesDto.FromApp(result, this); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index 9f798add0..09c8c93d1 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/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(); - var response = await WorkflowsDto.FromAppAsync(workflowsValidator, result, this); + var response = await GetResponse(result); return response; } + + private async Task GetResponse(IAppEntity result) + { + return await WorkflowsDto.FromAppAsync(workflowsValidator, result, this); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs index 85f6ef7ce..43992079e 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs @@ -46,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// 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 }; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs index 77580f5e9..11c2063f9 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs +++ b/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() - .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() }; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs index a7a344251..6781ee20b 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs @@ -38,14 +38,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models ///
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 }; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs index 7116e99f2..1d456251e 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs +++ b/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) diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs index 4929ed76b..9efa1133a 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs +++ b/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 -"; + /// /// The id of the user that contributes to the app. /// [Required] public string ContributorId { get; set; } + /// + /// The display name. + /// + [Required] + public string ContributorName { get; set; } + /// /// The role of the contributor. /// 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 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)) { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs index 540725c75..805a7e227 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs +++ b/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; } /// - /// The metadata. + /// The metadata to provide information about this request. /// [JsonProperty("_meta")] public ContributorsMetadata Metadata { get; set; } - public static ContributorsDto FromApp(IAppEntity app, IAppPlansProvider plans, ApiController controller, bool isInvited) + public static async Task 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) diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs index ff0ddf201..1617c69a6 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs @@ -44,23 +44,33 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models [Required] public IEnumerable 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 }; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs index b43c32041..c7daa77df 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs +++ b/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) diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs index 1601340cc..b026d6dba 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs +++ b/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 /// 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)) { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs index 5e3515eba..d95aa9a3d 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs @@ -31,16 +31,22 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public static async Task 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) diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 2f2b2afcc..83ed3a14b 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/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 FromContentsAsync(IResultList contents, - Context context, ApiController controller, ISchemaEntity schema, IContentWorkflow contentWorkflow) + public static async Task FromContentsAsync(IResultList 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(); } diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts b/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts index 6815d1093..8bb0607c2 100644 --- a/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts +++ b/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}`); } \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html index 011e94b55..3242e97b4 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html @@ -6,11 +6,17 @@ - + +
+ +
@@ -21,33 +27,43 @@ - - - - - - - - - - - -
- - - {{contributor.contributorId | sqxUserName}} - - - - -
+ + + + + + + + + + + + +
+ + + + + + + +
+ + +
+ + +
+ No contributors found. +
+