diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs index f6073a4f6..1b5d3a11e 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -41,32 +41,40 @@ namespace Squidex.Domain.Apps.Entities.Rules switch (command) { case CreateRule createRule: - return CreateAsync(createRule, async c => + return CreateReturnAsync(createRule, async c => { await GuardRule.CanCreate(c, appProvider); Create(c); + + return await GetRawStateAsync(); }); case UpdateRule updateRule: - return UpdateAsync(updateRule, async c => + return UpdateReturnAsync(updateRule, async c => { await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider); Update(c); + + return await GetRawStateAsync(); }); case EnableRule enableRule: - return UpdateAsync(enableRule, c => + return UpdateReturnAsync(enableRule, async c => { GuardRule.CanEnable(c, Snapshot.RuleDef); Enable(c); + + return await GetRawStateAsync(); }); case DisableRule disableRule: - return UpdateAsync(disableRule, c => + return UpdateReturnAsync(disableRule, async c => { GuardRule.CanDisable(c, Snapshot.RuleDef); Disable(c); + + return await GetRawStateAsync(); }); case DeleteRule deleteRule: return UpdateAsync(deleteRule, c => @@ -123,6 +131,11 @@ namespace Squidex.Domain.Apps.Entities.Rules } } + public Task GetRawStateAsync() + { + return Task.FromResult(Snapshot); + } + public Task> GetStateAsync() { return J.AsTask(Snapshot); diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 9b0a57b95..9987e0e89 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -178,12 +178,10 @@ namespace Squidex.Areas.Api.Controllers.Assets var assetFile = await CheckAssetFileAsync(file); var command = new CreateAsset { File = assetFile }; - var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - var response = AssetDto.FromAsset(result.Asset, this, app, result.IsDuplicate); + var response = await InvokeCommand(app, command); - return StatusCode(201, response); + return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); } /// @@ -202,6 +200,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpPut] [Route("apps/{app}/assets/{id}/content/")] + [ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(AssetDto), 200)] [ApiPermission(Permissions.AppAssetsUpdate)] [ApiCosts(1)] @@ -210,10 +209,8 @@ namespace Squidex.Areas.Api.Controllers.Assets var assetFile = await CheckAssetFileAsync(file); var command = new UpdateAsset { File = assetFile, AssetId = id }; - var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - var response = AssetDto.FromAsset(result, this, app); + var response = await InvokeCommand(app, command); return Ok(response); } @@ -225,12 +222,13 @@ namespace Squidex.Areas.Api.Controllers.Assets /// The id of the asset. /// The asset object that needs to updated. /// - /// 204 => Asset updated. + /// 200 => Asset updated. /// 400 => Asset name not valid. /// 404 => Asset or app not found. /// [HttpPut] [Route("apps/{app}/assets/{id}/")] + [ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(AssetDto), 200)] [AssetRequestSizeLimit] [ApiPermission(Permissions.AppAssetsUpdate)] @@ -238,10 +236,8 @@ namespace Squidex.Areas.Api.Controllers.Assets public async Task PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request) { var command = request.ToCommand(id); - var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - var response = AssetDto.FromAsset(result, this, app); + var response = await InvokeCommand(app, command); return Ok(response); } @@ -266,6 +262,16 @@ namespace Squidex.Areas.Api.Controllers.Assets return NoContent(); } + private async Task InvokeCommand(string app, AssetCommand command) + { + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = AssetDto.FromAsset(result, this, app); + + return response; + } + private async Task CheckAssetFileAsync(IReadOnlyList file) { if (file.Count != 1) diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs index 4ee256530..f0894b1ae 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs @@ -14,11 +14,12 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; +using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules.Models { - public sealed class RuleDto : IGenerateETag + public sealed class RuleDto : Resource, IGenerateETag { /// /// The id of the rule. @@ -70,19 +71,48 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models [JsonConverter(typeof(RuleActionConverter))] public RuleAction Action { get; set; } - public static RuleDto FromRule(IRuleEntity rule) + public static RuleDto FromRule(IRuleEntity rule, ApiController controller, string app) { - var response = new RuleDto(); + var result = new RuleDto(); - SimpleMapper.Map(rule, response); - SimpleMapper.Map(rule.RuleDef, response); + SimpleMapper.Map(rule, result); + SimpleMapper.Map(rule.RuleDef, result); if (rule.RuleDef.Trigger != null) { - response.Trigger = RuleTriggerDtoFactory.Create(rule.RuleDef.Trigger); + result.Trigger = RuleTriggerDtoFactory.Create(rule.RuleDef.Trigger); } - return response; + return CreateLinks(result, controller, app); + } + + private static RuleDto CreateLinks(RuleDto result, ApiController controller, string app) + { + var values = new { app, id = result.Id }; + + if (controller.HasPermission(Permissions.AppRulesDisable, app)) + { + if (result.IsEnabled) + { + result.AddPutLink("disable", controller.Url(x => nameof(x.DisableRule), values)); + } + else + { + result.AddPutLink("enable", controller.Url(x => nameof(x.EnableRule), values)); + } + } + + if (controller.HasPermission(Permissions.AppRulesUpdate)) + { + result.AddPutLink("update", controller.Url(x => nameof(x.PutRule), values)); + } + + if (controller.HasPermission(Permissions.AppRulesDelete)) + { + result.AddPutLink("delete", controller.Url(x => nameof(x.DeleteRule), values)); + } + + return result; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs index f577f405a..498ffd560 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs @@ -11,10 +11,11 @@ using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Infrastructure.Reflection; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules.Models { - public sealed class RuleEventDto + public sealed class RuleEventDto : Resource { /// /// The id of the event. @@ -63,14 +64,25 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models /// public RuleJobResult JobResult { get; set; } - public static RuleEventDto FromRuleEvent(IRuleEventEntity ruleEvent) + public static RuleEventDto FromRuleEvent(IRuleEventEntity ruleEvent, ApiController controller, string app) { - var response = new RuleEventDto(); + var result = new RuleEventDto(); - SimpleMapper.Map(ruleEvent, response); - SimpleMapper.Map(ruleEvent.Job, response); + SimpleMapper.Map(ruleEvent, result); + SimpleMapper.Map(ruleEvent.Job, result); - return response; + return CreateLinks(result, controller, app); + } + + private static RuleEventDto CreateLinks(RuleEventDto result, ApiController controller, string app) + { + var values = new { app, id = result.Id }; + + result.AddPutLink("update", controller.Url(x => nameof(x.PutEvent), values)); + + result.AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteEvent), values)); + + return result; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs index ef320b78d..add2af47f 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs @@ -5,14 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules.Models { - public sealed class RuleEventsDto + public sealed class RuleEventsDto : Resource { /// /// The rule events. @@ -25,9 +27,22 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models /// public long Total { get; set; } - public static RuleEventsDto FromRuleEvents(IReadOnlyList items, long total) + public static RuleEventsDto FromRuleEvents(IReadOnlyList items, long total, ApiController controller, string app) { - return new RuleEventsDto { Total = total, Items = items.Select(RuleEventDto.FromRuleEvent).ToArray() }; + var result = new RuleEventsDto + { + Total = total, + Items = items.Select(x => RuleEventDto.FromRuleEvent(x, controller, app)).ToArray() + }; + + return CreateLinks(result, controller, app); + } + + private static RuleEventsDto CreateLinks(RuleEventsDto result, ApiController controller, string app) + { + result.AddSelfLink(controller.Url(x => nameof(x.GetEvents), new { app })); + + return result; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs new file mode 100644 index 000000000..bf4eea27b --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Rules.Models +{ + public sealed class RulesDto : Resource + { + /// + /// The rules. + /// + [Required] + public RuleDto[] Items { get; set; } + + public string GenerateEtag() + { + return Items.ToManyEtag(0); + } + + public static RulesDto FromRules(IEnumerable items, ApiController controller, string app) + { + var result = new RulesDto + { + Items = items.Select(x => RuleDto.FromRule(x, controller, app)).ToArray() + }; + + return CreateLinks(result, controller, app); + } + + private static RulesDto CreateLinks(RulesDto result, ApiController controller, string app) + { + var values = new { app }; + + result.AddSelfLink(controller.Url(x => nameof(x.GetRules), values)); + + if (controller.HasPermission(Permissions.AppRulesCreate, app)) + { + result.AddPostLink("create", controller.Url(x => nameof(x.PostRule), values)); + } + + if (controller.HasPermission(Permissions.AppRulesEvents, app)) + { + result.AddGetLink("events", controller.Url(x => nameof(x.GetEvents), values)); + } + + return result; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index b657ef1cf..23c09c944 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -15,6 +15,7 @@ using NodaTime; using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure; @@ -76,16 +77,16 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpGet] [Route("apps/{app}/rules/")] - [ProducesResponseType(typeof(RuleDto[]), 200)] + [ProducesResponseType(typeof(RulesDto), 200)] [ApiPermission(Permissions.AppRulesRead)] [ApiCosts(1)] public async Task GetRules(string app) { var entities = await appProvider.GetRulesAsync(AppId); - var response = entities.Select(RuleDto.FromRule).ToArray(); + var response = RulesDto.FromRules(entities, this, app); - Response.Headers[HeaderNames.ETag] = response.ToManyEtag(0); + Response.Headers[HeaderNames.ETag] = response.GenerateEtag(); return Ok(response); } @@ -108,10 +109,9 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task PostRule(string app, [FromBody] CreateRuleDto request) { - var context = await CommandBus.PublishAsync(request.ToCommand()); + var command = request.ToCommand(); - var result = context.Result>(); - var response = EntityCreatedDto.FromResult(result); + var response = await InvokeCommand(app, command); return CreatedAtAction(nameof(GetRules), new { app }, response); } @@ -123,7 +123,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// The id of the rule to update. /// The rule object that needs to be added to the app. /// - /// 204 => Rule updated. + /// 200 => Rule updated. /// 400 => Rule is not valid. /// 404 => Rule or app not found. /// @@ -132,14 +132,17 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpPut] [Route("apps/{app}/rules/{id}/")] - [ProducesResponseType(typeof(ErrorDto), 400)] + [ProducesResponseType(typeof(ErrorDto), 200)] + [ProducesResponseType(typeof(RuleDto), 400)] [ApiPermission(Permissions.AppRulesUpdate)] [ApiCosts(1)] public async Task PutRule(string app, Guid id, [FromBody] UpdateRuleDto request) { - await CommandBus.PublishAsync(request.ToCommand(id)); + var command = request.ToCommand(id); - return NoContent(); + var response = await InvokeCommand(app, command); + + return Ok(response); } /// @@ -148,19 +151,23 @@ namespace Squidex.Areas.Api.Controllers.Rules /// The name of the app. /// The id of the rule to enable. /// - /// 204 => Rule enabled. + /// 200 => Rule enabled. /// 400 => Rule already enabled. /// 404 => Rule or app not found. /// [HttpPut] [Route("apps/{app}/rules/{id}/enable/")] + [ProducesResponseType(typeof(ErrorDto), 200)] + [ProducesResponseType(typeof(RuleDto), 400)] [ApiPermission(Permissions.AppRulesDisable)] [ApiCosts(1)] public async Task EnableRule(string app, Guid id) { - await CommandBus.PublishAsync(new EnableRule { RuleId = id }); + var command = new EnableRule { RuleId = id }; - return NoContent(); + var response = await InvokeCommand(app, command); + + return Ok(response); } /// @@ -169,19 +176,23 @@ namespace Squidex.Areas.Api.Controllers.Rules /// The name of the app. /// The id of the rule to disable. /// - /// 204 => Rule disabled. + /// 200 => Rule disabled. /// 400 => Rule already disabled. /// 404 => Rule or app not found. /// [HttpPut] [Route("apps/{app}/rules/{id}/disable/")] + [ProducesResponseType(typeof(ErrorDto), 200)] + [ProducesResponseType(typeof(RuleDto), 400)] [ApiPermission(Permissions.AppRulesDisable)] [ApiCosts(1)] public async Task DisableRule(string app, Guid id) { - await CommandBus.PublishAsync(new DisableRule { RuleId = id }); + var command = new DisableRule { RuleId = id }; - return NoContent(); + var response = await InvokeCommand(app, command); + + return Ok(response); } /// @@ -226,7 +237,7 @@ namespace Squidex.Areas.Api.Controllers.Rules await Task.WhenAll(taskForItems, taskForCount); - var response = RuleEventsDto.FromRuleEvents(taskForItems.Result, taskForCount.Result); + var response = RuleEventsDto.FromRuleEvents(taskForItems.Result, taskForCount.Result, this, app); return Ok(response); } @@ -284,5 +295,15 @@ namespace Squidex.Areas.Api.Controllers.Rules return NoContent(); } + + private async Task InvokeCommand(string app, RuleCommand command) + { + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = RuleDto.FromRule(result, this, app); + + return response; + } } } \ No newline at end of file diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.html b/src/Squidex/app/features/administration/pages/users/user-page.component.html index 673c8958e..bec006082 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.html @@ -16,7 +16,7 @@ - + diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/src/Squidex/app/features/administration/pages/users/user-page.component.ts index 0abdd7eed..ee7498713 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.ts @@ -29,8 +29,6 @@ export class UserPageComponent extends ResourceOwner implements OnInit { public user?: UserDto; public userForm = new UserForm(this.formBuilder); - public isReadOnly = false; - constructor( public readonly usersState: UsersState, private readonly formBuilder: FormBuilder, @@ -49,9 +47,9 @@ export class UserPageComponent extends ResourceOwner implements OnInit { if (selectedUser) { this.userForm.load(selectedUser); - this.isReadOnly = !hasLink(this.user, 'update'); + this.canUpdate = hasLink(this.user, 'update'); - if (this.isReadOnly) { + if (!this.canUpdate) { this.userForm.form.disable(); } } @@ -59,7 +57,7 @@ export class UserPageComponent extends ResourceOwner implements OnInit { } public save() { - if (this.isReadOnly) { + if (!this.canUpdate) { return; } diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts index ba384f5d1..2a4c32c14 100644 --- a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts +++ b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts @@ -79,7 +79,7 @@ describe('EventConsumersState', () => { eventConsumersState.load().subscribe(); }); - it('should update evnet consumer when started', () => { + it('should update event consumer when started', () => { const updated = createEventConsumer(2, '_new'); eventConsumersService.setup(x => x.putStart(eventConsumer2)) diff --git a/src/Squidex/app/features/administration/state/users.state.spec.ts b/src/Squidex/app/features/administration/state/users.state.spec.ts index 3000bf6f6..279ff67c2 100644 --- a/src/Squidex/app/features/administration/state/users.state.spec.ts +++ b/src/Squidex/app/features/administration/state/users.state.spec.ts @@ -168,7 +168,7 @@ describe('UsersState', () => { expect(usersState.snapshot.selectedUser).toBeNull(); }); - it('should update user selected user when locked', () => { + it('should update user and selected user when locked', () => { const updated = createUser(2, '_new'); usersService.setup(x => x.lockUser(user2)) @@ -177,9 +177,9 @@ describe('UsersState', () => { usersState.select(user2.id).subscribe(); usersState.lock(user2).subscribe(); - const newUser2 = usersState.snapshot.users.at(1); + const user2New = usersState.snapshot.users.at(1); - expect(newUser2).toBe(usersState.snapshot.selectedUser!); + expect(user2New).toBe(usersState.snapshot.selectedUser!); }); it('should update user and selected user when unlocked', () => { @@ -191,10 +191,10 @@ describe('UsersState', () => { usersState.select(user2.id).subscribe(); usersState.unlock(user2).subscribe(); - const newUser2 = usersState.snapshot.users.at(1); + const user2New = usersState.snapshot.users.at(1); - expect(newUser2).toEqual(updated); - expect(newUser2).toBe(usersState.snapshot.selectedUser!); + expect(user2New).toEqual(updated); + expect(user2New).toBe(usersState.snapshot.selectedUser!); }); it('should update user and selected user when updated', () => { @@ -208,10 +208,10 @@ describe('UsersState', () => { usersState.select(user2.id).subscribe(); usersState.update(user2, request).subscribe(); - const newUser2 = usersState.snapshot.users.at(1); + const user2New = usersState.snapshot.users.at(1); - expect(newUser2).toEqual(updated); - expect(newUser2).toBe(usersState.snapshot.selectedUser!); + expect(user2New).toEqual(updated); + expect(user2New).toBe(usersState.snapshot.selectedUser!); }); it('should add user to snapshot when created', () => { diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/src/Squidex/app/features/administration/state/users.state.ts index 04a4bc80a..d638c5e0c 100644 --- a/src/Squidex/app/features/administration/state/users.state.ts +++ b/src/Squidex/app/features/administration/state/users.state.ts @@ -60,10 +60,6 @@ export class UsersState extends State { this.changes.pipe(map(x => x.usersPager), distinctUntilChanged()); - public links = - this.changes.pipe(map(x => x.links), - distinctUntilChanged()); - public selectedUser = this.changes.pipe(map(x => x.selectedUser), distinctUntilChanged()); @@ -72,6 +68,10 @@ export class UsersState extends State { this.changes.pipe(map(x => !!x.isLoaded), distinctUntilChanged()); + public links = + this.changes.pipe(map(x => x.links), + distinctUntilChanged()); + constructor( private readonly dialogs: DialogService, private readonly usersService: UsersService @@ -131,7 +131,7 @@ export class UsersState extends State { selectedUser = users.find(x => x.id === selectedUser!.id) || selectedUser; } - return { ...s, users, usersPager, links, selectedUser, isLoaded: true }; + return { ...s, users, usersPager, selectedUser, isLoaded: true, links }; }); }), shareSubscribed(this.dialogs)); diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html index 422b0a98f..ecb11c85b 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -106,12 +106,12 @@ - + - + diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts index c9b1a77a2..fd7dcd668 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts @@ -5,11 +5,12 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { Form, + hasLink, ImmutableArray, RuleDto, RuleElementDto, @@ -26,7 +27,7 @@ export const MODE_EDIT_ACTION = 'EditAction'; styleUrls: ['./rule-wizard.component.scss'], templateUrl: './rule-wizard.component.html' }) -export class RuleWizardComponent implements OnInit { +export class RuleWizardComponent implements AfterViewInit, OnInit { public actionForm = new Form(new FormGroup({})); public actionType: string; public action: any = {}; @@ -35,6 +36,8 @@ export class RuleWizardComponent implements OnInit { public triggerType: string; public trigger: any = {}; + public canUpdate: boolean; + public step = 1; @Output() @@ -61,6 +64,8 @@ export class RuleWizardComponent implements OnInit { } public ngOnInit() { + this.canUpdate = !this.rule || hasLink(this.rule, 'update'); + if (this.mode === MODE_EDIT_ACTION) { this.step = 4; @@ -74,6 +79,14 @@ export class RuleWizardComponent implements OnInit { } } + public ngAfterViewInit() { + if (!this.canUpdate) { + this.actionForm.form.disable(); + + this.triggerForm.form.disable(); + } + } + public emitComplete() { this.complete.emit(); } @@ -132,6 +145,10 @@ export class RuleWizardComponent implements OnInit { } private updateTrigger() { + if (!this.canUpdate) { + return; + } + this.rulesState.updateTrigger(this.rule, this.trigger) .subscribe(() => { this.emitComplete(); @@ -143,6 +160,10 @@ export class RuleWizardComponent implements OnInit { } private updateAction() { + if (!this.canUpdate) { + return; + } + this.rulesState.updateAction(this.rule, this.action) .subscribe(() => { this.emitComplete(); diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html index d6102c1be..0e97c61f2 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -6,16 +6,19 @@ - - - + + + + + @@ -23,7 +26,7 @@
No rule created yet. -
@@ -48,10 +51,11 @@ - + @@ -37,12 +38,12 @@
-
- +
diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts index a3e1f140b..0e18f523f 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts @@ -29,6 +29,9 @@ export class ContentChangedTriggerComponent implements OnInit { @Input() public schemas: ImmutableArray; + @Input() + public canUpdate: boolean; + @Input() public trigger: any; diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/src/Squidex/app/framework/angular/forms/toggle.component.ts index 7105ada32..33e53d9bf 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.ts +++ b/src/Squidex/app/framework/angular/forms/toggle.component.ts @@ -28,6 +28,10 @@ export class ToggleComponent extends StatefulControlComponent { const version = new Version('1'); @@ -107,7 +107,7 @@ describe('RulesService', () => { it('should make get request to get app rules', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - let rules: RuleDto[]; + let rules: RulesDto; rulesService.getRules('my-app').subscribe(result => { rules = result; @@ -118,49 +118,18 @@ describe('RulesService', () => { expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush([ - { - id: 'id1', - created: '2016-12-12T10:10', - createdBy: 'CreatedBy1', - lastModified: '2017-12-12T10:10', - lastModifiedBy: 'LastModifiedBy1', - url: 'http://squidex.io/hook', - version: '1', - trigger: { - param1: 1, - param2: 2, - triggerType: 'ContentChanged' - }, - action: { - param3: 3, - param4: 4, - actionType: 'Webhook' - }, - isEnabled: true - } - ]); + req.flush({ + items: [ + ruleResponse(12), + ruleResponse(13) + ] + }); expect(rules!).toEqual( - [ - new RuleDto('id1', 'CreatedBy1', 'LastModifiedBy1', - DateTime.parseISO_UTC('2016-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T10:10'), - version, - true, - { - param1: 1, - param2: 2, - triggerType: 'ContentChanged' - }, - 'ContentChanged', - { - param3: 3, - param4: 4, - actionType: 'Webhook' - }, - 'Webhook') - ]); + new RulesDto(2, [ + createRule(12), + createRule(13) + ])); })); it('should make post request to create rule', @@ -179,7 +148,7 @@ describe('RulesService', () => { } }; - let rule: Versioned; + let rule: RuleDto; rulesService.postRule('my-app', dto).subscribe(result => { rule = result; @@ -190,18 +159,13 @@ describe('RulesService', () => { expect(req.request.method).toEqual('POST'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush({ id: 'id1' }, { + req.flush(ruleResponse(12), { headers: { etag: '1' } }); - expect(rule!).toEqual({ - payload: { - id: 'id1' - }, - version - }); + expect(rule!).toEqual(createRule(12)); })); it('should make put request to update rule', @@ -216,46 +180,88 @@ describe('RulesService', () => { } }; - rulesService.putRule('my-app', '123', dto, version).subscribe(); + const resource: Resource = { + _links: { + update: { method: 'PUT', href: '/api/apps/my-app/rules/123' } + } + }; + + let rule: RuleDto; + + rulesService.putRule('my-app', resource, dto, version).subscribe(result => { + rule = result; + }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toEqual(version.value); - req.flush({}); + req.flush(ruleResponse(123)); + + expect(rule!).toEqual(createRule(123)); })); it('should make put request to enable rule', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - rulesService.enableRule('my-app', '123', version).subscribe(); + const resource: Resource = { + _links: { + enable: { method: 'PUT', href: '/api/apps/my-app/rules/123/enable' } + } + }; + + let rule: RuleDto; + + rulesService.enableRule('my-app', resource, version).subscribe(result => { + rule = result; + }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/enable'); expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toEqual(version.value); - req.flush({}); + req.flush(ruleResponse(123)); + + expect(rule!).toEqual(createRule(123)); })); it('should make put request to disable rule', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - rulesService.disableRule('my-app', '123', version).subscribe(); + const resource: Resource = { + _links: { + disable: { method: 'PUT', href: '/api/apps/my-app/rules/123/disable' } + } + }; + + let rule: RuleDto; + + rulesService.disableRule('my-app', resource, version).subscribe(result => { + rule = result; + }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/disable'); expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toEqual(version.value); - req.flush({}); + req.flush(ruleResponse(123)); + + expect(rule!).toEqual(createRule(123)); })); it('should make delete request to delete rule', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - rulesService.deleteRule('my-app', '123', version).subscribe(); + const resource: Resource = { + _links: { + delete: { method: 'DELETE', href: '/api/apps/my-app/rules/123' } + } + }; + + rulesService.deleteRule('my-app', resource, version).subscribe(); const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); @@ -322,7 +328,13 @@ describe('RulesService', () => { it('should make put request to enqueue rule event', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - rulesService.enqueueEvent('my-app', '123').subscribe(); + const resource: Resource = { + _links: { + update: { method: 'PUT', href: '/api/apps/my-app/rules/events/123' } + } + }; + + rulesService.enqueueEvent('my-app', resource).subscribe(); const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events/123'); @@ -335,7 +347,13 @@ describe('RulesService', () => { it('should make delete request to cancel rule event', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - rulesService.cancelEvent('my-app', '123').subscribe(); + const resource: Resource = { + _links: { + delete: { method: 'DELETE', href: '/api/apps/my-app/rules/events/123' } + } + }; + + rulesService.cancelEvent('my-app', resource).subscribe(); const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events/123'); @@ -344,4 +362,58 @@ describe('RulesService', () => { req.flush({}); })); -}); \ No newline at end of file + + function ruleResponse(id: number, suffix = '') { + return { + id: `id${id}`, + created: `${id % 1000 + 2000}-12-12T10:10`, + createdBy: `creator-${id}`, + lastModified: `${id % 1000 + 2000}-11-11T10:10`, + lastModifiedBy: `modifier-${id}`, + isEnabled: id % 2 === 0, + trigger: { + param1: 1, + param2: 2, + triggerType: `ContentChanged${id}${suffix}` + }, + action: { + param3: 3, + param4: 4, + actionType: `Webhook${id}${suffix}` + }, + version: id, + _links: { + update: { method: 'PUT', href: `/rules/${id}` } + } + }; + } +}); + +export function createRule(id: number, suffix = '') { + const result = new RuleDto( + `id${id}`, + `creator-${id}`, + `modifier-${id}`, + DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10`), + DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10`), + new Version(`${id}`), + id % 2 === 0, + { + param1: 1, + param2: 2, + triggerType: `ContentChanged${id}${suffix}` + }, + `ContentChanged${id}${suffix}`, + { + param3: 3, + param4: 4, + actionType: `Webhook${id}${suffix}` + }, + `Webhook${id}${suffix}`); + + result._links['update'] = { + method: 'PUT', href: `/rules/${id}` + }; + + return result; +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/rules.service.ts b/src/Squidex/app/shared/services/rules.service.ts index 7af1acfb7..c0a7030de 100644 --- a/src/Squidex/app/shared/services/rules.service.ts +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -15,12 +15,13 @@ import { ApiUrlConfig, DateTime, HTTP, - mapVersioned, Model, pretifyError, + Resource, + ResourceLinks, ResultSet, Version, - Versioned + withLinks } from '@app/framework'; export const ALL_TRIGGERS = { @@ -74,7 +75,13 @@ export class RuleElementPropertyDto { } } +export class RulesDto extends ResultSet { + public readonly _links: ResourceLinks = {}; +} + export class RuleDto extends Model { + public readonly _links: ResourceLinks = {}; + constructor( public readonly id: string, public readonly createdBy: string, @@ -92,9 +99,13 @@ export class RuleDto extends Model { } } -export class RuleEventsDto extends ResultSet { } +export class RuleEventsDto extends ResultSet { + public readonly _links: ResourceLinks = {}; +} export class RuleEventDto extends Model { + public readonly _links: ResourceLinks = {}; + constructor( public readonly id: string, public readonly created: DateTime, @@ -115,10 +126,6 @@ export interface UpsertRuleDto { readonly action: RuleAction; } -export interface RuleCreatedDto { - readonly id: string; -} - export type RuleAction = { actionType: string } & any; export type RuleTrigger = { triggerType: string } & any; @@ -167,77 +174,87 @@ export class RulesService { pretifyError('Failed to load Rules. Please reload.')); } - public getRules(appName: string): Observable { + public getRules(appName: string): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); return HTTP.getVersioned(this.http, url).pipe( map(({ payload }) => { - const items: any[] = payload.body; + const items: any[] = payload.body.items; - const rules = items.map(item => - new RuleDto( - item.id, - item.createdBy, - item.lastModifiedBy, - DateTime.parseISO_UTC(item.created), - DateTime.parseISO_UTC(item.lastModified), - new Version(item.version.toString()), - item.isEnabled, - item.trigger, - item.trigger.triggerType, - item.action, - item.action.actionType)); - - return rules; + const rules = items.map(item => parseRule(item)); + + return withLinks(new RulesDto(rules.length, rules), payload.body); }), pretifyError('Failed to load Rules. Please reload.')); } - public postRule(appName: string, dto: UpsertRuleDto): Observable> { + public postRule(appName: string, dto: UpsertRuleDto): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); - return HTTP.postVersioned(this.http, url, dto).pipe( - mapVersioned(({ body }) => body!), + return HTTP.postVersioned(this.http, url, dto).pipe( + map(({ payload }) => { + return parseRule(payload.body); + }), tap(() => { this.analytics.trackEvent('Rule', 'Created', appName); }), pretifyError('Failed to create rule. Please reload.')); } - public putRule(appName: string, id: string, dto: Partial, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); + public putRule(appName: string, resource: Resource, dto: Partial, version: Version): Observable { + const link = resource._links['update']; - return HTTP.putVersioned(this.http, url, dto, version).pipe( + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + map(({ payload }) => { + return parseRule(payload.body); + }), tap(() => { this.analytics.trackEvent('Rule', 'Updated', appName); }), pretifyError('Failed to update rule. Please reload.')); } - public enableRule(appName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/enable`); + public enableRule(appName: string, resource: Resource, version: Version): Observable { + const link = resource._links['enable']; - return HTTP.putVersioned(this.http, url, {}, version).pipe( + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( + map(({ payload }) => { + return parseRule(payload.body); + }), tap(() => { - this.analytics.trackEvent('Rule', 'Updated', appName); + this.analytics.trackEvent('Rule', 'Enabled', appName); }), pretifyError('Failed to enable rule. Please reload.')); } - public disableRule(appName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/disable`); + public disableRule(appName: string, resource: Resource, version: Version): Observable { + const link = resource._links['disable']; - return HTTP.putVersioned(this.http, url, {}, version).pipe( + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe( + map(({ payload }) => { + return parseRule(payload.body); + }), tap(() => { - this.analytics.trackEvent('Rule', 'Updated', appName); + this.analytics.trackEvent('Rule', 'Disabled', appName); }), pretifyError('Failed to disable rule. Please reload.')); } - public deleteRule(appName: string, id: string, version: Version): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); + public deleteRule(appName: string, resource: Resource, version: Version): Observable { + const link = resource._links['delete']; - return HTTP.deleteVersioned(this.http, url, version).pipe( + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version).pipe( + map(({ payload }) => { + return parseRule(payload.body); + }), tap(() => { this.analytics.trackEvent('Rule', 'Deleted', appName); }), @@ -270,23 +287,44 @@ export class RulesService { pretifyError('Failed to load events. Please reload.')); } - public enqueueEvent(appName: string, id: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`); + public enqueueEvent(appName: string, resource: Resource): Observable { + const link = resource._links['update']; + + const url = this.apiUrl.buildUrl(link.href); - return HTTP.putVersioned(this.http, url, {}).pipe( + return HTTP.requestVersioned(this.http, link.method, url).pipe( tap(() => { this.analytics.trackEvent('Rule', 'EventEnqueued', appName); }), pretifyError('Failed to enqueue rule event. Please reload.')); } - public cancelEvent(appName: string, id: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`); + public cancelEvent(appName: string, resource: Resource): Observable { + const link = resource._links['delete']; - return HTTP.deleteVersioned(this.http, url).pipe( + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url).pipe( tap(() => { this.analytics.trackEvent('Rule', 'EventDequeued', appName); }), pretifyError('Failed to cancel rule event. Please reload.')); } +} + +function parseRule(resource: any) { + return withLinks( + new RuleDto( + resource.id, + resource.createdBy, + resource.lastModifiedBy, + DateTime.parseISO_UTC(resource.created), + DateTime.parseISO_UTC(resource.lastModified), + new Version(resource.version.toString()), + resource.isEnabled, + resource.trigger, + resource.trigger.triggerType, + resource.action, + resource.action.actionType), + resource); } \ No newline at end of file diff --git a/src/Squidex/app/shared/state/asset-uploader.state.spec.ts b/src/Squidex/app/shared/state/asset-uploader.state.spec.ts index 17413beff..c3a185314 100644 --- a/src/Squidex/app/shared/state/asset-uploader.state.spec.ts +++ b/src/Squidex/app/shared/state/asset-uploader.state.spec.ts @@ -22,7 +22,7 @@ import { createAsset } from './../services/assets.service.spec'; import { TestValues } from './_test-helpers'; -describe('AssetsState', () => { +describe('AssetUploaderState', () => { const { app, appsState @@ -155,7 +155,7 @@ describe('AssetsState', () => { it('should update status when uploading asset completes', () => { const file: File = { name: 'my-file' }; - let updated = createAsset(1, undefined, '-new'); + let updated = createAsset(1, undefined, '_new'); assetsService.setup(x => x.replaceFile(app, asset, file, asset.version)) .returns(() => of(10, 20, updated)).verifiable(); diff --git a/src/Squidex/app/shared/state/assets.state.spec.ts b/src/Squidex/app/shared/state/assets.state.spec.ts index c9f045a02..f8df2c6d7 100644 --- a/src/Squidex/app/shared/state/assets.state.spec.ts +++ b/src/Squidex/app/shared/state/assets.state.spec.ts @@ -41,6 +41,9 @@ describe('AssetsState', () => { dialogs = Mock.ofType(); assetsService = Mock.ofType(); + assetsService.setup(x => x.getTags(app)) + .returns(() => of({ tag1: 1, shared: 2, tag2: 1 })).verifiable(Times.atLeastOnce()); + assetsState = new AssetsState(appsState.object, assetsService.object, dialogs.object); }); @@ -50,12 +53,9 @@ describe('AssetsState', () => { describe('Loading', () => { it('should load assets', () => { - assetsService.setup(x => x.getAssets(app, 30, 0, undefined, [])) + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]))) .returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable(); - assetsService.setup(x => x.getTags(app)) - .returns(() => of({ tag1: 1, shared: 2, tag2: 1 })).verifiable(); - assetsState.load().subscribe(); expect(assetsState.snapshot.assets.values).toEqual([asset1, asset2]); @@ -66,11 +66,8 @@ describe('AssetsState', () => { }); it('should show notification on load when reload is true', () => { - assetsService.setup(x => x.getAssets(app, 30, 0, undefined, [])) - .returns(() => of(new AssetsDto(200, [asset1, asset2]))); - - assetsService.setup(x => x.getTags(app)) - .returns(() => of({ tag1: 1, shared: 2, tag2: 1 })).verifiable(); + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]))) + .returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable(); assetsState.load(true).subscribe(); @@ -80,20 +77,20 @@ describe('AssetsState', () => { }); it('should load with tags when tag toggled', () => { - assetsService.setup(x => x.getAssets(app, 30, 0, undefined, ['tag1'])) - .returns(() => of(new AssetsDto(0, []))); + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue(['tag1']))) + .returns(() => of(new AssetsDto(0, []))).verifiable(); assetsState.toggleTag('tag1').subscribe(); expect(assetsState.isTagSelected('tag1')).toBeTruthy(); }); - it('should load without tags when tag toggled', () => { - assetsService.setup(x => x.getAssets(app, 30, 0, undefined, ['tag1'])) - .returns(() => of(new AssetsDto(0, []))); + it('should load without tags when tag untoggled', () => { + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue(['tag1']))) + .returns(() => of(new AssetsDto(0, []))).verifiable(); - assetsService.setup(x => x.getAssets(app, 30, 0, undefined, [])) - .returns(() => of(new AssetsDto(0, []))); + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]))) + .returns(() => of(new AssetsDto(0, []))).verifiable(); assetsState.toggleTag('tag1').subscribe(); assetsState.toggleTag('tag1').subscribe(); @@ -102,8 +99,8 @@ describe('AssetsState', () => { }); it('should load with tags when tags selected', () => { - assetsService.setup(x => x.getAssets(app, 30, 0, undefined, ['tag1', 'tag2'])) - .returns(() => of(new AssetsDto(0, []))); + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue(['tag1', 'tag2']))) + .returns(() => of(new AssetsDto(0, []))).verifiable(); assetsState.selectTags(['tag1', 'tag2']).subscribe(); @@ -111,8 +108,8 @@ describe('AssetsState', () => { }); it('should load without tags when tags reset', () => { - assetsService.setup(x => x.getAssets(app, 30, 0, undefined, [])) - .returns(() => of(new AssetsDto(0, []))); + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]))) + .returns(() => of(new AssetsDto(0, []))).verifiable(); assetsState.resetTags().subscribe(); @@ -120,9 +117,13 @@ describe('AssetsState', () => { }); it('should load next page and prev page when paging', () => { - assetsService.setup(x => x.getAssets(app, 30, 30, undefined, [])) - .returns(() => of(new AssetsDto(200, []))); + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]))) + .returns(() => of(new AssetsDto(200, []))).verifiable(Times.exactly(2)); + assetsService.setup(x => x.getAssets(app, 30, 30, undefined, It.isValue([]))) + .returns(() => of(new AssetsDto(200, []))).verifiable(); + + assetsState.load().subscribe(); assetsState.goNext().subscribe(); assetsState.goPrev().subscribe(); @@ -130,8 +131,8 @@ describe('AssetsState', () => { }); it('should load with query when searching', () => { - assetsService.setup(x => x.getAssets(app, 30, 0, 'my-query', [])) - .returns(() => of(new AssetsDto(0, []))); + assetsService.setup(x => x.getAssets(app, 30, 0, 'my-query', It.isValue([]))) + .returns(() => of(new AssetsDto(0, []))).verifiable(); assetsState.search('my-query').subscribe(); @@ -141,12 +142,9 @@ describe('AssetsState', () => { describe('Updates', () => { beforeEach(() => { - assetsService.setup(x => x.getAssets(app, 30, 0, undefined, [])) + assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]))) .returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable(); - assetsService.setup(x => x.getTags(app)) - .returns(() => of({ tag1: 1, shared: 2, tag2: 1 })).verifiable(); - assetsState.load(true).subscribe(); }); @@ -165,7 +163,7 @@ describe('AssetsState', () => { }); it('should update asset when updated', () => { - const update = createAsset(1, ['new'], '-new'); + const update = createAsset(1, ['new'], '_new'); assetsState.update(update); diff --git a/src/Squidex/app/shared/state/rule-events.state.spec.ts b/src/Squidex/app/shared/state/rule-events.state.spec.ts index 764654588..5adcd6f4a 100644 --- a/src/Squidex/app/shared/state/rule-events.state.spec.ts +++ b/src/Squidex/app/shared/state/rule-events.state.spec.ts @@ -76,24 +76,24 @@ describe('RuleEventsState', () => { }); it('should call service when enqueuing event', () => { - rulesService.setup(x => x.enqueueEvent(app, oldRuleEvents[0].id)) + rulesService.setup(x => x.enqueueEvent(app, oldRuleEvents[0])) .returns(() => of({})); ruleEventsState.enqueue(oldRuleEvents[0]).subscribe(); expect().nothing(); - rulesService.verify(x => x.enqueueEvent(app, oldRuleEvents[0].id), Times.once()); + rulesService.verify(x => x.enqueueEvent(app, oldRuleEvents[0]), Times.once()); }); it('should call service when cancelling event', () => { - rulesService.setup(x => x.cancelEvent(app, oldRuleEvents[0].id)) + rulesService.setup(x => x.cancelEvent(app, oldRuleEvents[0])) .returns(() => of({})); ruleEventsState.cancel(oldRuleEvents[0]).subscribe(); expect().nothing(); - rulesService.verify(x => x.cancelEvent(app, oldRuleEvents[0].id), Times.once()); + rulesService.verify(x => x.cancelEvent(app, oldRuleEvents[0]), Times.once()); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/rule-events.state.ts b/src/Squidex/app/shared/state/rule-events.state.ts index b9bf61996..8f777b4d5 100644 --- a/src/Squidex/app/shared/state/rule-events.state.ts +++ b/src/Squidex/app/shared/state/rule-events.state.ts @@ -82,7 +82,7 @@ export class RuleEventsState extends State { } public enqueue(event: RuleEventDto): Observable { - return this.rulesService.enqueueEvent(this.appsState.appName, event.id).pipe( + return this.rulesService.enqueueEvent(this.appsState.appName, event).pipe( tap(() => { this.dialogs.notifyInfo('Events enqueued. Will be resend in a few seconds.'); }), @@ -90,7 +90,7 @@ export class RuleEventsState extends State { } public cancel(event: RuleEventDto): Observable { - return this.rulesService.cancelEvent(this.appsState.appName, event.id).pipe( + return this.rulesService.cancelEvent(this.appsState.appName, event).pipe( tap(() => { return this.next(s => { const ruleEvents = s.ruleEvents.replaceBy('id', setCancelled(event)); diff --git a/src/Squidex/app/shared/state/rules.state.spec.ts b/src/Squidex/app/shared/state/rules.state.spec.ts index 6b8a8cdd5..90ed19ada 100644 --- a/src/Squidex/app/shared/state/rules.state.spec.ts +++ b/src/Squidex/app/shared/state/rules.state.spec.ts @@ -12,30 +12,27 @@ import { RulesState } from './rules.state'; import { DialogService, - RuleDto, + RulesDto, RulesService, versioned } from '@app/shared/internal'; +import { createRule } from '../services/rules.service.spec'; + import { TestValues } from './_test-helpers'; describe('RulesState', () => { const { app, appsState, - authService, - creation, - creator, - modified, - modifier, newVersion, version } = TestValues; - const oldRules = [ - new RuleDto('id1', creator, creator, creation, creation, version, false, {}, 'trigger1', {}, 'action1'), - new RuleDto('id2', creator, creator, creation, creation, version, true, {}, 'trigger2', {}, 'action2') - ]; + const rule1 = createRule(1); + const rule2 = createRule(2); + + const newRule = createRule(3); let dialogs: IMock; let rulesService: IMock; @@ -45,7 +42,7 @@ describe('RulesState', () => { dialogs = Mock.ofType(); rulesService = Mock.ofType(); - rulesState = new RulesState(appsState.object, authService.object, dialogs.object, rulesService.object); + rulesState = new RulesState(appsState.object, dialogs.object, rulesService.object); }); afterEach(() => { @@ -55,11 +52,11 @@ describe('RulesState', () => { describe('Loading', () => { it('should load rules', () => { rulesService.setup(x => x.getRules(app)) - .returns(() => of(oldRules)).verifiable(); + .returns(() => of(new RulesDto(2, [rule1, rule2]))).verifiable(); rulesState.load().subscribe(); - expect(rulesState.snapshot.rules.values).toEqual(oldRules); + expect(rulesState.snapshot.rules.values).toEqual([rule1, rule2]); expect(rulesState.snapshot.isLoaded).toBeTruthy(); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); @@ -67,7 +64,7 @@ describe('RulesState', () => { it('should show notification on load when reload is true', () => { rulesService.setup(x => x.getRules(app)) - .returns(() => of(oldRules)).verifiable(); + .returns(() => of(new RulesDto(2, [rule1, rule2]))).verifiable(); rulesState.load(true).subscribe(); @@ -81,89 +78,85 @@ describe('RulesState', () => { describe('Updates', () => { beforeEach(() => { rulesService.setup(x => x.getRules(app)) - .returns(() => of(oldRules)).verifiable(); + .returns(() => of(new RulesDto(2, [rule1, rule2]))).verifiable(); rulesState.load().subscribe(); }); it('should add rule to snapshot when created', () => { - const newRule = new RuleDto('id3', modifier, modifier, modified, modified, version, true, { value: 3 }, 'trigger3', { value: 1 }, 'action3'); - const request = { trigger: { triggerType: 'trigger3', value: 3 }, action: { actionType: 'action3', value: 1 } }; rulesService.setup(x => x.postRule(app, request)) - .returns(() => of(versioned(version, { id: 'id3' }))); + .returns(() => of(newRule)); - rulesState.create(request, modified).subscribe(); + rulesState.create(request).subscribe(); - expect(rulesState.snapshot.rules.values).toEqual([...oldRules, newRule]); + expect(rulesState.snapshot.rules.values).toEqual([rule1, rule2, newRule]); }); - it('should update action and update and user info when updated action', () => { + it('should update rule when updated action', () => { const newAction = {}; - rulesService.setup(x => x.putRule(app, oldRules[0].id, It.isAny(), version)) - .returns(() => of(versioned(newVersion))).verifiable(); + const updated = createRule(1, 'new'); + + rulesService.setup(x => x.putRule(app, rule1, It.isAny(), version)) + .returns(() => of(updated)).verifiable(); - rulesState.updateAction(oldRules[0], newAction, modified).subscribe(); + rulesState.updateAction(rule1, newAction).subscribe(); - const rule_1 = rulesState.snapshot.rules.at(0); + const newRule1 = rulesState.snapshot.rules.at(0); - expect(rule_1.action).toBe(newAction); - expectToBeModified(rule_1); + expect(newRule1).toEqual(updated); }); - it('should update trigger and update and user info when updated trigger', () => { + it('should update rule when updated trigger', () => { const newTrigger = {}; - rulesService.setup(x => x.putRule(app, oldRules[0].id, It.isAny(), version)) - .returns(() => of(versioned(newVersion))).verifiable(); + const updated = createRule(1, 'new'); - rulesState.updateTrigger(oldRules[0], newTrigger, modified).subscribe(); + rulesService.setup(x => x.putRule(app, rule1, It.isAny(), version)) + .returns(() => of(updated)).verifiable(); - const rule_1 = rulesState.snapshot.rules.at(0); + rulesState.updateTrigger(rule1, newTrigger).subscribe(); - expect(rule_1.trigger).toBe(newTrigger); - expectToBeModified(rule_1); + const rule1New = rulesState.snapshot.rules.at(0); + + expect(rule1New).toEqual(updated); }); - it('should mark as enabled and update and user info when enabled', () => { - rulesService.setup(x => x.enableRule(app, oldRules[0].id, version)) - .returns(() => of(versioned(newVersion))).verifiable(); + it('should update rule when enabled', () => { + const updated = createRule(1, 'new'); + + rulesService.setup(x => x.enableRule(app, rule1, version)) + .returns(() => of(updated)).verifiable(); - rulesState.enable(oldRules[0], modified).subscribe(); + rulesState.enable(rule1).subscribe(); - const rule_1 = rulesState.snapshot.rules.at(0); + const rule1New = rulesState.snapshot.rules.at(0); - expect(rule_1.isEnabled).toBeTruthy(); - expectToBeModified(rule_1); + expect(rule1New).toEqual(updated); }); - it('should mark as disabled and update and user info when disabled', () => { - rulesService.setup(x => x.disableRule(app, oldRules[1].id, version)) - .returns(() => of(versioned(newVersion))).verifiable(); + it('should update rule when disabled', () => { + const updated = createRule(1, 'new'); + + rulesService.setup(x => x.disableRule(app, rule1, version)) + .returns(() => of(updated)).verifiable(); - rulesState.disable(oldRules[1], modified).subscribe(); + rulesState.disable(rule1).subscribe(); - const rule_1 = rulesState.snapshot.rules.at(1); + const rule1New = rulesState.snapshot.rules.at(0); - expect(rule_1.isEnabled).toBeFalsy(); - expectToBeModified(rule_1); + expect(rule1New).toEqual(updated); }); it('should remove rule from snapshot when deleted', () => { - rulesService.setup(x => x.deleteRule(app, oldRules[0].id, version)) + rulesService.setup(x => x.deleteRule(app, rule1, version)) .returns(() => of(versioned(newVersion))).verifiable(); - rulesState.delete(oldRules[0]).subscribe(); + rulesState.delete(rule1).subscribe(); - expect(rulesState.snapshot.rules.values).toEqual([oldRules[1]]); + expect(rulesState.snapshot.rules.values).toEqual([rule2]); }); - - function expectToBeModified(rule_1: RuleDto) { - expect(rule_1.lastModified).toEqual(modified); - expect(rule_1.lastModifiedBy).toEqual(modifier); - expect(rule_1.version).toEqual(newVersion); - } }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/rules.state.ts b/src/Squidex/app/shared/state/rules.state.ts index 1a0bcac0f..2412433df 100644 --- a/src/Squidex/app/shared/state/rules.state.ts +++ b/src/Squidex/app/shared/state/rules.state.ts @@ -10,20 +10,16 @@ import { Observable } from 'rxjs'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { - DateTime, DialogService, ImmutableArray, + ResourceLinks, shareSubscribed, - State, - Version, - Versioned + State } from '@app/framework'; -import { AuthService} from './../services/auth.service'; import { AppsState } from './apps.state'; import { - RuleCreatedDto, RuleDto, RulesService, UpsertRuleDto @@ -33,6 +29,9 @@ interface Snapshot { // The current rules. rules: RulesList; + // The resource links. + links: ResourceLinks; + // Indicates if the rules are loaded. isLoaded?: boolean; } @@ -49,13 +48,16 @@ export class RulesState extends State { this.changes.pipe(map(x => !!x.isLoaded), distinctUntilChanged()); + public links = + this.changes.pipe(map(x => x.links), + distinctUntilChanged()); + constructor( private readonly appsState: AppsState, - private readonly authState: AuthService, private readonly dialogs: DialogService, private readonly rulesService: RulesService ) { - super({ rules: ImmutableArray.empty() }); + super({ rules: ImmutableArray.empty(), links: {} }); } public load(isReload = false): Observable { @@ -64,23 +66,22 @@ export class RulesState extends State { } return this.rulesService.getRules(this.appName).pipe( - tap(payload => { + tap(({ items, _links: links }) => { if (isReload) { this.dialogs.notifyInfo('Rules reloaded.'); } this.next(s => { - const rules = ImmutableArray.of(payload); + const rules = ImmutableArray.of(items); - return { ...s, rules, isLoaded: true }; + return { ...s, rules, isLoaded: true, links }; }); }), shareSubscribed(this.dialogs)); } - public create(request: UpsertRuleDto, now?: DateTime): Observable { + public create(request: UpsertRuleDto): Observable { return this.rulesService.postRule(this.appName, request).pipe( - map(payload => createRule(request, payload, this.user, now)), tap(created => { this.next(s => { const rules = s.rules.push(created); @@ -92,7 +93,7 @@ export class RulesState extends State { } public delete(rule: RuleDto): Observable { - return this.rulesService.deleteRule(this.appName, rule.id, rule.version).pipe( + return this.rulesService.deleteRule(this.appName, rule, rule.version).pipe( tap(() => { this.next(s => { const rules = s.rules.removeAll(x => x.id === rule.id); @@ -103,36 +104,32 @@ export class RulesState extends State { shareSubscribed(this.dialogs)); } - public updateAction(rule: RuleDto, action: any, now?: DateTime): Observable { - return this.rulesService.putRule(this.appName, rule.id, { action }, rule.version).pipe( - map(({ version }) => updateAction(rule, action, this.user, version, now)), + public updateAction(rule: RuleDto, action: any): Observable { + return this.rulesService.putRule(this.appName, rule, { action }, rule.version).pipe( tap(updated => { this.replaceRule(updated); }), shareSubscribed(this.dialogs)); } - public updateTrigger(rule: RuleDto, trigger: any, now?: DateTime): Observable { - return this.rulesService.putRule(this.appName, rule.id, { trigger }, rule.version).pipe( - map(({ version }) => updateTrigger(rule, trigger, this.user, version, now)), + public updateTrigger(rule: RuleDto, trigger: any): Observable { + return this.rulesService.putRule(this.appName, rule, { trigger }, rule.version).pipe( tap(updated => { this.replaceRule(updated); }), shareSubscribed(this.dialogs)); } - public enable(rule: RuleDto, now?: DateTime): Observable { - return this.rulesService.enableRule(this.appName, rule.id, rule.version).pipe( - map(({ version }) => setEnabled(rule, true, this.user, version, now)), + public enable(rule: RuleDto): Observable { + return this.rulesService.enableRule(this.appName, rule, rule.version).pipe( tap(updated => { this.replaceRule(updated); }), shareSubscribed(this.dialogs)); } - public disable(rule: RuleDto, now?: DateTime): Observable { - return this.rulesService.disableRule(this.appName, rule.id, rule.version).pipe( - map(({ version }) => setEnabled(rule, false, this.user, version, now)), + public disable(rule: RuleDto): Observable { + return this.rulesService.disableRule(this.appName, rule, rule.version).pipe( tap(updated => { this.replaceRule(updated); }), @@ -150,57 +147,4 @@ export class RulesState extends State { private get appName() { return this.appsState.appName; } - - private get user() { - return this.authState.user!.token; - } -} - -const updateTrigger = (rule: RuleDto, trigger: any, user: string, version: Version, now?: DateTime) => - rule.with({ - trigger, - triggerType: trigger.triggerType, - lastModified: now || DateTime.now(), - lastModifiedBy: user, - version - }); - -const updateAction = (rule: RuleDto, action: any, user: string, version: Version, now?: DateTime) => - rule.with({ - action, - actionType: action.actionType, - lastModified: now || DateTime.now(), - lastModifiedBy: user, - version - }); - -const setEnabled = (rule: RuleDto, isEnabled: boolean, user: string, version: Version, now?: DateTime) => - rule.with({ - isEnabled, - lastModified: now || DateTime.now(), - lastModifiedBy: user, - version - }); - -function createRule(request: UpsertRuleDto, { payload, version }: Versioned, user: string, now?: DateTime) { - now = now || DateTime.now(); - - const { triggerType, ...trigger } = request.trigger; - - const { actionType, ...action } = request.action; - - const rule = new RuleDto( - payload.id, - user, - user, - now, - now, - version, - true, - trigger, - triggerType, - action, - actionType); - - return rule; } \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index d26a8335c..ee40fbd6e 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -76,9 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = context.Result(); - Assert.Equal(assetId, result.IdOrValue); - Assert.Contains("tag1", result.Tags); - Assert.Contains("tag2", result.Tags); + Assert.Equal(assetId, result.Asset.Id); + Assert.Contains("tag1", result.Asset.Tags); + Assert.Contains("tag2", result.Asset.Tags); AssertAssetHasBeenUploaded(0, context.ContextId); AssertAssetImageChecked(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs index eb2c0ac26..4f3773f9b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var result = await sut.ExecuteAsync(CreateRuleCommand(command)); - result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 0)); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppId, sut.Snapshot.AppId.Id); @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var result = await sut.ExecuteAsync(CreateRuleCommand(command)); - result.ShouldBeEquivalent(new EntitySavedResult(2)); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.RuleDef.IsEnabled); @@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var result = await sut.ExecuteAsync(CreateRuleCommand(command)); - result.ShouldBeEquivalent(new EntitySavedResult(1)); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.RuleDef.IsEnabled);