From a38a0358a878f355cc744dcc669c7944f9f3ddd9 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 9 Sep 2017 17:47:04 +0200 Subject: [PATCH 01/10] 1) Restore implemented 2) Custom history view --- .../Contents/ContentRestored.cs | 17 +++ .../Contents/MongoContentEntity.cs | 4 + .../MongoContentRepository_EventHandling.cs | 17 ++- .../Contents/Visitors/FindExtensions.cs | 3 +- .../History/MongoHistoryEventEntity.cs | 2 +- .../History/MongoHistoryEventRepository.cs | 8 +- .../History/ParsedHistoryEvent.cs | 5 + .../Contents/ContentHistoryEventsCreator.cs | 13 ++- .../Contents/ContentQueryService.cs | 1 + .../Contents/IContentEntity.cs | 2 + .../History/IHistoryEventEntity.cs | 2 + .../Contents/Commands/RestoreContent.cs | 14 +++ .../Contents/ContentCommandMiddleware.cs | 8 ++ .../Contents/ContentDomainObject.cs | 24 +++++ .../Api/History/Models/HistoryEventDto.cs | 5 + .../ContentApi/ContentsController.cs | 15 +++ .../Generator/SchemaSwaggerGenerator.cs | 33 +++--- .../ContentApi/Models/ContentDto.cs | 12 ++- .../app/features/content/declarations.ts | 1 + src/Squidex/app/features/content/module.ts | 5 +- .../content/content-history.component.html | 29 +++++ .../content/content-history.component.scss | 39 +++++++ .../content/content-history.component.ts | 101 ++++++++++++++++++ .../app/features/content/pages/messages.ts | 8 ++ .../shared/components/history.component.scss | 4 - .../shared/services/contents.service.spec.ts | 14 +-- .../app/shared/services/contents.service.ts | 8 +- .../shared/services/history.service.spec.ts | 6 +- .../app/shared/services/history.service.ts | 2 + .../Contents/ContentCommandMiddlewareTests.cs | 15 +++ .../Contents/ContentDomainObjectTests.cs | 25 +++++ 31 files changed, 398 insertions(+), 44 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs create mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs create mode 100644 src/Squidex/app/features/content/pages/content/content-history.component.html create mode 100644 src/Squidex/app/features/content/pages/content/content-history.component.scss create mode 100644 src/Squidex/app/features/content/pages/content/content-history.component.ts diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs new file mode 100644 index 000000000..f2a7659ed --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ContentRestored.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Contents +{ + [EventType(nameof(ContentRestored))] + public sealed class ContentRestored : ContentEvent + { + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs index 97112ef82..74acd25db 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs @@ -47,6 +47,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents [BsonElement("pu")] public bool IsPublished { get; set; } + [BsonRequired] + [BsonElement("dl")] + public bool IsDeleted { get; set; } + [BsonRequired] [BsonElement("dt")] public string DataText { get; set; } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 2c3ef026e..918593ea8 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -62,6 +62,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified)); await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsPublished)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted)); await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); }); } @@ -114,6 +115,17 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents }); } + protected Task On(ContentRestored @event, EnvelopeHeaders headers) + { + return ForAppIdAsync(@event.AppId.Id, collection => + { + return collection.UpdateAsync(@event, headers, x => + { + x.IsDeleted = false; + }); + }); + } + protected Task On(ContentDeleted @event, EnvelopeHeaders headers) { return ForAppIdAsync(@event.AppId.Id, async collection => @@ -124,7 +136,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); - await collection.DeleteOneAsync(x => x.Id == headers.AggregateId()); + await collection.UpdateAsync(@event, headers, x => + { + x.IsDeleted = true; + }); }); } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs index c6fa3880a..47b6a56ba 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs @@ -67,7 +67,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors { var filters = new List> { - Filter.Eq(x => x.SchemaId, schemaId) + Filter.Eq(x => x.SchemaId, schemaId), + Filter.Eq(x => x.IsDeleted, false) }; if (!nonPublished) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs index e8ea33263..b0563ae70 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History [BsonRequired] [BsonElement] - public int SessionEventIndex { get; set; } + public long Version { get; set; } [BsonRequired] [BsonElement] diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs index ba3ef3fe7..f597ea9fd 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Events; @@ -25,7 +24,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History { private readonly List creators; private readonly Dictionary texts = new Dictionary(); - private int sessionEventCount; public string Name { @@ -64,7 +62,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History .Ascending(x => x.AppId) .Ascending(x => x.Channel) .Descending(x => x.Created) - .Descending(x => x.SessionEventIndex)), + .Descending(x => x.Version)), collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) })); } @@ -72,7 +70,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History { var historyEventEntities = await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix) - .SortByDescending(x => x.Created).ThenByDescending(x => x.SessionEventIndex).Limit(count) + .SortByDescending(x => x.Created).ThenBy(x => x.Version).Limit(count) .ToListAsync(); return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); @@ -90,7 +88,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History { entity.Id = Guid.NewGuid(); - entity.SessionEventIndex = Interlocked.Increment(ref sessionEventCount); + entity.Version = @event.Headers.EventStreamNumber(); entity.Channel = message.Channel; entity.Message = message.Message; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs index cee3487f7..461af2374 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs @@ -44,6 +44,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History get { return inner.LastModified; } } + public long Version + { + get { return inner.Version; } + } + public string Channel { get { return inner.Channel; } diff --git a/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs index 0943801d9..4a6c25ba3 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs @@ -20,19 +20,22 @@ namespace Squidex.Domain.Apps.Read.Contents : base(typeNameRegistry) { AddEventMessage( - "created content element."); + "created content item."); AddEventMessage( - "updated content element."); + "updated content item."); AddEventMessage( - "deleted content element."); + "deleted content item."); + + AddEventMessage( + "restored content item."); AddEventMessage( - "published content element."); + "published content item."); AddEventMessage( - "unpublished content element."); + "unpublished content item."); } protected override Task CreateEventCoreAsync(Envelope @event) diff --git a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs index 732d698ba..1177c3620 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs @@ -156,6 +156,7 @@ namespace Squidex.Domain.Apps.Read.Contents public Guid Id { get; set; } public Guid AppId { get; set; } public long Version { get; set; } + public bool IsDeleted { get; set; } public bool IsPublished { get; set; } public Instant Created { get; set; } public Instant LastModified { get; set; } diff --git a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs index 4e1f355a5..64f2b3970 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs @@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Read.Contents { bool IsPublished { get; } + bool IsDeleted { get; } + NamedContentData Data { get; } } } diff --git a/src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs index 5a7df96e9..ab35d743b 100644 --- a/src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Read.History string Message { get; } + long Version { get; } + RefToken Actor { get; } } } diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs new file mode 100644 index 000000000..3563aa716 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// RestoreContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Contents.Commands +{ + public sealed class RestoreContent : ContentCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs index 7cc97d1f4..4f055b2e9 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs @@ -150,6 +150,14 @@ namespace Squidex.Domain.Apps.Write.Contents }); } + protected Task On(RestoreContent command, CommandContext context) + { + return handler.UpdateAsync(context, content => + { + content.Restore(command); + }); + } + public async Task HandleAsync(CommandContext context, Func next) { if (!await this.DispatchActionAsync(context.Command, context)) diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs index df6ae3a99..d4812c129 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs @@ -72,6 +72,11 @@ namespace Squidex.Domain.Apps.Write.Contents isDeleted = true; } + protected void On(ContentRestored @event) + { + isDeleted = false; + } + public ContentDomainObject Create(CreateContent command) { Guard.Valid(command, nameof(command), () => "Cannot create content"); @@ -99,6 +104,17 @@ namespace Squidex.Domain.Apps.Write.Contents return this; } + public ContentDomainObject Restore(RestoreContent command) + { + Guard.NotNull(command, nameof(command)); + + VerifyDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentRestored())); + + return this; + } + public ContentDomainObject Publish(PublishContent command) { Guard.NotNull(command, nameof(command)); @@ -159,6 +175,14 @@ namespace Squidex.Domain.Apps.Write.Contents } } + private void VerifyDeleted() + { + if (!isDeleted) + { + throw new DomainException("Content has not been deleted."); + } + } + private void VerifyCreatedAndNotDeleted() { if (isDeleted || !isCreated) diff --git a/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs b/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs index 4eff213d3..92bfeb91e 100644 --- a/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs +++ b/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs @@ -35,5 +35,10 @@ namespace Squidex.Controllers.Api.History.Models /// The time when the event happened. /// public Instant Created { get; set; } + + /// + /// The version identifier. + /// + public long Version { get; set; } } } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 2484c7028..5ac516a9b 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -208,6 +208,21 @@ namespace Squidex.Controllers.ContentApi return NoContent(); } + [MustBeAppEditor] + [HttpPut] + [Route("content/{app}/{name}/{id}/restore")] + [ApiCosts(1)] + public async Task RestoreContent(string name, Guid id) + { + await contentQuery.FindSchemaAsync(App, name); + + var command = new RestoreContent { ContentId = id, User = User }; + + await CommandBus.PublishAsync(command); + + return NoContent(); + } + [MustBeAppEditor] [HttpDelete] [Route("content/{app}/{name}/{id}")] diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs index 7bc4e2f3c..53e649ea1 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs @@ -92,6 +92,7 @@ namespace Squidex.Controllers.ContentApi.Generator GenerateSchemaGetOperation(), GenerateSchemaUpdateOperation(), GenerateSchemaPatchOperation(), + GenerateSchemaRestoreOperation(), GenerateSchemaPublishOperation(), GenerateSchemaUnpublishOperation(), GenerateSchemaDeleteOperation() @@ -109,6 +110,7 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Query{schemaKey}Contents"; operation.Summary = $"Queries {schemaName} contents."; + operation.Security = ReaderSecurity; operation.Description = SchemaQueryDescription; @@ -119,8 +121,6 @@ namespace Squidex.Controllers.ContentApi.Generator operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition."); operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema)); - - operation.Security = ReaderSecurity; }); } @@ -130,10 +130,9 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Get{schemaKey}Content"; operation.Summary = $"Get a {schemaName} content."; + operation.Security = ReaderSecurity; operation.AddResponse("200", $"{schemaName} content found.", contentSchema); - - operation.Security = ReaderSecurity; }); } @@ -143,13 +142,12 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Create{schemaKey}Content"; operation.Summary = $"Create a {schemaName} content."; + operation.Security = EditorSecurity; operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content."); operation.AddResponse("201", $"{schemaName} created.", contentSchema); - - operation.Security = EditorSecurity; }); } @@ -159,12 +157,11 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Update{schemaKey}Content"; operation.Summary = $"Update a {schemaName} content."; + operation.Security = EditorSecurity; operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); operation.AddResponse("201", $"{schemaName} element updated.", dataSchema); - - operation.Security = EditorSecurity; }); } @@ -174,12 +171,11 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Path{schemaKey}Content"; operation.Summary = $"Patchs a {schemaName} content."; + operation.Security = EditorSecurity; operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription); operation.AddResponse("201", $"{schemaName} element patched.", dataSchema); - - operation.Security = EditorSecurity; }); } @@ -189,10 +185,9 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Publish{schemaKey}Content"; operation.Summary = $"Publish a {schemaName} content."; + operation.Security = EditorSecurity; operation.AddResponse("204", $"{schemaName} element published."); - - operation.Security = EditorSecurity; }); } @@ -202,10 +197,21 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Unpublish{schemaKey}Content"; operation.Summary = $"Unpublish a {schemaName} content."; + operation.Security = EditorSecurity; operation.AddResponse("204", $"{schemaName} element unpublished."); + }); + } + private SwaggerOperations GenerateSchemaRestoreOperation() + { + return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/restore", operation => + { + operation.OperationId = $"Restore{schemaKey}Content"; + operation.Summary = $"Restore a {schemaName} content."; operation.Security = EditorSecurity; + + operation.AddResponse("204", $"{schemaName} element restored."); }); } @@ -215,10 +221,9 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Delete{schemaKey}Content"; operation.Summary = $"Delete a {schemaName} content."; + operation.Security = EditorSecurity; operation.AddResponse("204", $"{schemaName} content deleted."); - - operation.Security = EditorSecurity; }); } diff --git a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs index 3457d10e8..37469c6c3 100644 --- a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs +++ b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs @@ -52,15 +52,25 @@ namespace Squidex.Controllers.ContentApi.Models public Instant LastModified { get; set; } /// - /// Indicates if the content element is publihed. + /// Indicates if the content element is published. /// public bool? IsPublished { get; set; } + /// + /// Indicates if the content element is deleted. + /// + public bool IsDeleted { get; set; } + /// /// The version of the content. /// public long Version { get; set; } + public bool ShouldSerializeIsDeleted() + { + return IsDeleted; + } + public static ContentDto Create(CreateContent command, EntityCreatedResult result) { var now = SystemClock.Instance.GetCurrentInstant(); diff --git a/src/Squidex/app/features/content/declarations.ts b/src/Squidex/app/features/content/declarations.ts index 25ee9dac3..61875624f 100644 --- a/src/Squidex/app/features/content/declarations.ts +++ b/src/Squidex/app/features/content/declarations.ts @@ -6,6 +6,7 @@ */ export * from './pages/content/content-field.component'; +export * from './pages/content/content-history.component'; export * from './pages/content/content-page.component'; export * from './pages/contents/contents-page.component'; export * from './pages/contents/search-form.component'; diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index 5a605c7ea..7b91bb002 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -11,7 +11,6 @@ import { DndModule } from 'ng2-dnd'; import { CanDeactivateGuard, - HistoryComponent, ResolveAppLanguagesGuard, ResolveContentGuard, ResolvePublishedSchemaGuard, @@ -22,6 +21,7 @@ import { import { AssetsEditorComponent, ContentFieldComponent, + ContentHistoryComponent, ContentPageComponent, ContentItemComponent, ContentsPageComponent, @@ -76,7 +76,7 @@ const routes: Routes = [ children: [ { path: 'history', - component: HistoryComponent, + component: ContentHistoryComponent, data: { channel: 'contents.{contentId}' } @@ -112,6 +112,7 @@ const routes: Routes = [ declarations: [ AssetsEditorComponent, ContentFieldComponent, + ContentHistoryComponent, ContentItemComponent, ContentPageComponent, ContentsPageComponent, diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.html b/src/Squidex/app/features/content/pages/content/content-history.component.html new file mode 100644 index 000000000..7175d30f3 --- /dev/null +++ b/src/Squidex/app/features/content/pages/content/content-history.component.html @@ -0,0 +1,29 @@ + +
+
+

Activity

+
+ + + + +
+ +
+
+
+
+ +
+
+
+ {{event.actor | sqxUserNameRef:'I'}} +
+
{{event.created | sqxFromNow}}
+ + Load this Version +
+
+
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.scss b/src/Squidex/app/features/content/pages/content/content-history.component.scss new file mode 100644 index 000000000..358649609 --- /dev/null +++ b/src/Squidex/app/features/content/pages/content/content-history.component.scss @@ -0,0 +1,39 @@ +@import '_vars'; +@import '_mixins'; + +.event { + & { + @include flex-box; + margin-bottom: 1rem; + } + + &-main { + @include flex-grow(1); + } + + &-load { + & { + font-size: .9rem; + font-weight: normal; + cursor: pointer; + color: $color-theme-blue !important; + } + + &:focus, + &:hover { + text-decoration: underline !important; + } + } + + &-left { + min-width: 2.8rem; + max-width: 2.8rem; + margin-top: .3rem; + } + + &-created { + font-size: .65rem; + font-weight: normal; + color: $color-text-decent; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.ts b/src/Squidex/app/features/content/pages/content/content-history.component.ts new file mode 100644 index 000000000..e6df00a82 --- /dev/null +++ b/src/Squidex/app/features/content/pages/content/content-history.component.ts @@ -0,0 +1,101 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { + allParams, + AppComponentBase, + AppsStoreService, + DialogService, + HistoryChannelUpdated, + HistoryEventDto, + HistoryService, + MessageBus, + UsersProviderService +} from 'shared'; + +const REPLACEMENT_TEMP = '$TEMP$'; + +@Component({ + selector: 'sqx-history', + styleUrls: ['./content-history.component.scss'], + templateUrl: './content-history.component.html' +}) +export class ContentHistoryComponent extends AppComponentBase { + public get channel(): string { + let channelPath = this.route.snapshot.data['channel']; + + if (channelPath) { + const params = allParams(this.route); + + for (let key in params) { + if (params.hasOwnProperty(key)) { + const value = params[key]; + + channelPath = channelPath.replace(`{${key}}`, value); + } + } + } + + return channelPath; + } + + public events: Observable = + Observable.timer(0, 10000) + .merge(this.messageBus.of(HistoryChannelUpdated).delay(1000)) + .switchMap(() => this.appNameOnce()) + .switchMap(app => this.historyService.getHistory(app, this.channel).retry(2)); + + constructor(appsStore: AppsStoreService, dialogs: DialogService, + private readonly users: UsersProviderService, + private readonly historyService: HistoryService, + private readonly messageBus: MessageBus, + private readonly route: ActivatedRoute + ) { + super(dialogs, appsStore); + } + + private userName(userId: string): Observable { + const parts = userId.split(':'); + + if (parts[0] === 'subject') { + return this.users.getUser(parts[1], 'Me').map(u => u.displayName); + } else { + if (parts[1].endsWith('client')) { + return Observable.of(parts[1]); + } else { + return Observable.of(`${parts[1]}-client`); + } + } + } + + public format(message: string): Observable { + let foundUserId: string | null = null; + + message = message.replace(/{([^\s:]*):([^}]*)}/, (match: string, type: string, id: string) => { + if (type === 'user') { + foundUserId = id; + return REPLACEMENT_TEMP; + } else { + return id; + } + }); + + message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => { + return `${marker}`; + }); + + if (foundUserId) { + return this.userName(foundUserId).map(t => message.replace(REPLACEMENT_TEMP, `${t}`)); + } + + return Observable.of(message); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 580651082..569c201ad 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -26,4 +26,12 @@ export class ContentDeleted { public readonly content: ContentDto ) { } +} + +export class ContentVersionSelected { + constructor( + public readonly id: string, + public readonly version: number + ) { + } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/history.component.scss b/src/Squidex/app/shared/components/history.component.scss index 0fe973d59..5e93d36ed 100644 --- a/src/Squidex/app/shared/components/history.component.scss +++ b/src/Squidex/app/shared/components/history.component.scss @@ -7,10 +7,6 @@ margin-bottom: 1rem; } - &-message { - font-size: .9rem; - } - &-main { @include flex-grow(1); } diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 4c3c2db15..41eaa39cc 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -22,7 +22,7 @@ describe('ContentDto', () => { it('should update data property and user info when updating', () => { const now = DateTime.now(); - const content_1 = new ContentDto('1', false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); + const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); const content_2 = content_1.update({ data: 2 }, 'me', now); expect(content_2.data).toEqual({ data: 2 }); @@ -33,7 +33,7 @@ describe('ContentDto', () => { it('should update isPublished property and user info when publishing', () => { const now = DateTime.now(); - const content_1 = new ContentDto('1', false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); + const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); const content_2 = content_1.publish('me', now); expect(content_2.isPublished).toBeTruthy(); @@ -44,7 +44,7 @@ describe('ContentDto', () => { it('should update isPublished property and user info when unpublishing', () => { const now = DateTime.now(); - const content_1 = new ContentDto('1', true, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); + const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); const content_2 = content_1.unpublish('me', now); expect(content_2.isPublished).toBeFalsy(); @@ -115,12 +115,12 @@ describe('ContentsService', () => { expect(contents).toEqual( new ContentsDto(10, [ - new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', + new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {}, new Version('11')), - new ContentDto('id2', true, 'Created2', 'LastModifiedBy2', + new ContentDto('id2', true, false, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), {}, @@ -205,7 +205,7 @@ describe('ContentsService', () => { }); expect(content).toEqual( - new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', + new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {}, @@ -263,7 +263,7 @@ describe('ContentsService', () => { }); expect(content).toEqual( - new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', + new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {}, diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index f38255c22..a4cad43c3 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -31,6 +31,7 @@ export class ContentDto { constructor( public readonly id: string, public readonly isPublished: boolean, + public readonly isDeleted: boolean, public readonly createdBy: string, public readonly lastModifiedBy: string, public readonly created: DateTime, @@ -44,6 +45,7 @@ export class ContentDto { return new ContentDto( this.id, true, + this.isDeleted, this.createdBy, user, this.created, now || DateTime.now(), this.data, @@ -54,6 +56,7 @@ export class ContentDto { return new ContentDto( this.id, false, + this.isDeleted, this.createdBy, user, this.created, now || DateTime.now(), this.data, @@ -64,6 +67,7 @@ export class ContentDto { return new ContentDto( this.id, this.isPublished, + this.isDeleted, this.createdBy, user, this.created, now || DateTime.now(), data, @@ -117,6 +121,7 @@ export class ContentsService { return new ContentDto( item.id, item.isPublished, + item.isDeleted === true, item.createdBy, item.lastModifiedBy, DateTime.parseISO_UTC(item.created), @@ -136,6 +141,7 @@ export class ContentsService { return new ContentDto( response.id, response.isPublished, + response.isDeleted === true, response.createdBy, response.lastModifiedBy, DateTime.parseISO_UTC(response.created), @@ -164,7 +170,7 @@ export class ContentsService { .map(response => { return new ContentDto( response.id, - response.isPublished, + response.isPublished, false, response.createdBy, response.lastModifiedBy, DateTime.parseISO_UTC(response.created), diff --git a/src/Squidex/app/shared/services/history.service.spec.ts b/src/Squidex/app/shared/services/history.service.spec.ts index 1badf7aba..5c1b651bb 100644 --- a/src/Squidex/app/shared/services/history.service.spec.ts +++ b/src/Squidex/app/shared/services/history.service.spec.ts @@ -52,20 +52,22 @@ describe('HistoryService', () => { actor: 'User1', eventId: '1', message: 'Message 1', + version: 2, created: '2016-12-12T10:10' }, { actor: 'User2', eventId: '2', message: 'Message 2', + version: 3, created: '2016-12-13T10:10' } ]); expect(events).toEqual( [ - new HistoryEventDto('1', 'User1', 'Message 1', DateTime.parseISO_UTC('2016-12-12T10:10')), - new HistoryEventDto('2', 'User2', 'Message 2', DateTime.parseISO_UTC('2016-12-13T10:10')) + new HistoryEventDto('1', 'User1', 'Message 1', 2, DateTime.parseISO_UTC('2016-12-12T10:10')), + new HistoryEventDto('2', 'User2', 'Message 2', 3, DateTime.parseISO_UTC('2016-12-13T10:10')) ]); })); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/history.service.ts b/src/Squidex/app/shared/services/history.service.ts index c27ba997e..987303cad 100644 --- a/src/Squidex/app/shared/services/history.service.ts +++ b/src/Squidex/app/shared/services/history.service.ts @@ -22,6 +22,7 @@ export class HistoryEventDto { public readonly eventId: string, public readonly actor: string, public readonly message: string, + public readonly version: number, public readonly created: DateTime ) { } @@ -47,6 +48,7 @@ export class HistoryService { item.eventId, item.actor, item.message, + item.version, DateTime.parseISO_UTC(item.created)); }); }) diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs index 525b7d862..9a04d774a 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -229,6 +229,21 @@ namespace Squidex.Domain.Apps.Write.Contents A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "delete content")).MustHaveHappened(); } + [Fact] + public async Task Restore_should_update_domain_object() + { + CreateContent(); + + content.Delete(new DeleteContent()); + + var command = CreateContextForCommand(new RestoreContent { ContentId = contentId, User = user }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(command); + }); + } + private void CreateContent() { content.Create(new CreateContent { Data = data }); diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs index be472b1fc..bd23910bb 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs @@ -303,6 +303,31 @@ namespace Squidex.Domain.Apps.Write.Contents ); } + [Fact] + public void Restore_should_throw_exception_if_not_deleted() + { + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Restore_should_update_properties_create_events() + { + CreateContent(); + DeleteContent(); + + sut.Restore(CreateContentCommand(new RestoreContent())); + + Assert.False(sut.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentRestored()) + ); + } + private void CreateContent() { sut.Create(CreateContentCommand(new CreateContent { Data = data })); From 40bb95ca3619d3d3c7299172f7da4d4d32379cc0 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 9 Sep 2017 18:55:13 +0200 Subject: [PATCH 02/10] API for loading content. --- .../Contents/ContentVersionLoader.cs | 86 +++++++++++ .../Contents/IContentVersionLoader.cs | 19 +++ .../Commands/DefaultDomainObjectRepository.cs | 4 +- src/Squidex/Config/Domain/WriteModule.cs | 4 + .../ContentApi/ContentsController.cs | 22 ++- .../Contents/ContentVersionLoaderTests.cs | 146 ++++++++++++++++++ .../DefaultDomainObjectRepositoryTests.cs | 24 +-- 7 files changed, 290 insertions(+), 15 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs create mode 100644 src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs new file mode 100644 index 000000000..95e7936af --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// ContentVersionLoader.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Write.Contents +{ + public sealed class ContentVersionLoader : IContentVersionLoader + { + private readonly IStreamNameResolver nameResolver; + private readonly IEventStore eventStore; + private readonly EventDataFormatter formatter; + + public ContentVersionLoader(IEventStore eventStore, IStreamNameResolver nameResolver, EventDataFormatter formatter) + { + Guard.NotNull(formatter, nameof(formatter)); + Guard.NotNull(eventStore, nameof(eventStore)); + Guard.NotNull(nameResolver, nameof(nameResolver)); + + this.formatter = formatter; + this.eventStore = eventStore; + this.nameResolver = nameResolver; + } + + public async Task LoadAsync(Guid appId, Guid id, long version) + { + var streamName = nameResolver.GetStreamName(typeof(ContentDomainObject), id); + + var events = await eventStore.GetEventsAsync(streamName); + + if (events.Count == 0 || events.Count < version - 1) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject)); + } + + NamedContentData contentData = null; + + foreach (var storedEvent in events.Where(x => x.EventStreamNumber <= version)) + { + var envelope = ParseKnownEvent(storedEvent); + + if (envelope != null) + { + if (envelope.Payload is ContentCreated contentCreated) + { + if (contentCreated.AppId.Id != appId) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject)); + } + + contentData = contentCreated.Data; + } + else if (envelope.Payload is ContentUpdated contentUpdated) + { + contentData = contentUpdated.Data; + } + } + } + + return contentData; + } + + private Envelope ParseKnownEvent(StoredEvent storedEvent) + { + try + { + return formatter.Parse(storedEvent.Data); + } + catch (TypeNameNotFoundException) + { + return null; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs b/src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs new file mode 100644 index 000000000..e3968fbfc --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// IContentVersionLoader.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Write.Contents +{ + public interface IContentVersionLoader + { + Task LoadAsync(Guid appId, Guid id, long version); + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs index 43d3844a5..c3a87f4c1 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs @@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.CQRS.Commands foreach (var storedEvent in events) { - var envelope = ParseKnownCommand(storedEvent); + var envelope = ParseKnownEvent(storedEvent); if (envelope != null) { @@ -79,7 +79,7 @@ namespace Squidex.Infrastructure.CQRS.Commands } } - private Envelope ParseKnownCommand(StoredEvent storedEvent) + private Envelope ParseKnownEvent(StoredEvent storedEvent) { try { diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index 41dbb272f..a77d008a5 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -55,6 +55,10 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); + builder.RegisterType() + .As() + .SingleInstance(); + builder.RegisterType() .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 5ac516a9b..fa7db3ca7 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -31,12 +31,17 @@ namespace Squidex.Controllers.ContentApi public sealed class ContentsController : ControllerBase { private readonly IContentQueryService contentQuery; + private readonly IContentVersionLoader contentVersionLoader; private readonly IGraphQLService graphQl; - public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IGraphQLService graphQl) + public ContentsController(ICommandBus commandBus, + IContentQueryService contentQuery, + IContentVersionLoader contentVersionLoader, + IGraphQLService graphQl) : base(commandBus) { this.contentQuery = contentQuery; + this.contentVersionLoader = contentVersionLoader; this.graphQl = graphQl; } @@ -124,6 +129,21 @@ namespace Squidex.Controllers.ContentApi return Ok(response); } + [MustBeAppReader] + [HttpGet] + [Route("content/{app}/{name}/{id}/{version}")] + [ApiCosts(1)] + public async Task GetContentVersion(string name, Guid id, int version) + { + var contentData = await contentVersionLoader.LoadAsync(App.Id, id, version); + + var response = contentData; + + Response.Headers["ETag"] = new StringValues(version.ToString()); + + return Ok(response); + } + [MustBeAppEditor] [HttpPost] [Route("content/{app}/{name}/")] diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs new file mode 100644 index 000000000..ef94132b6 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// ContentVersionLoaderTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Contents +{ + public class ContentVersionLoaderTests + { + private readonly IEventStore eventStore = A.Fake(); + private readonly IStreamNameResolver nameResolver = A.Fake(); + private readonly EventDataFormatter formatter = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly Guid appId = Guid.NewGuid(); + private readonly string streamName = Guid.NewGuid().ToString(); + private readonly ContentVersionLoader sut; + + public ContentVersionLoaderTests() + { + A.CallTo(() => nameResolver.GetStreamName(typeof(ContentDomainObject), id)) + .Returns(streamName); + + sut = new ContentVersionLoader(eventStore, nameResolver, formatter); + } + + [Fact] + public async Task Should_throw_exception_when_event_store_returns_no_events() + { + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(new List()); + + await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, -1)); + } + + [Fact] + public async Task Should_throw_exception_when_version_not_found() + { + var events = new List + { + new StoredEvent("0", 0, new EventData()), + new StoredEvent("1", 1, new EventData()), + new StoredEvent("2", 2, new EventData()) + }; + + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(new List()); + + await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, 3)); + } + + [Fact] + public async Task Should_throw_exception_when_content_is_from_another_event() + { + var eventData1 = new EventData(); + + var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(Guid.NewGuid(), "my-app") }; + + var events = new List + { + new StoredEvent("0", 0, eventData1) + }; + + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(events); + + A.CallTo(() => formatter.Parse(eventData1)) + .Returns(new Envelope(event1)); + + await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, 0)); + } + + [Fact] + public async Task Should_load_content_from_created_event() + { + var eventData1 = new EventData(); + var eventData2 = new EventData(); + + var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(appId, "my-app") }; + var event2 = new ContentPublished(); + + var events = new List + { + new StoredEvent("0", 0, eventData1), + new StoredEvent("1", 1, eventData2) + }; + + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(events); + + A.CallTo(() => formatter.Parse(eventData1)) + .Returns(new Envelope(event1)); + A.CallTo(() => formatter.Parse(eventData2)) + .Returns(new Envelope(event2)); + + var data = await sut.LoadAsync(appId, id, 3); + + Assert.Same(event1.Data, data); + } + + [Fact] + public async Task Should_load_content_from_correct_version() + { + var eventData1 = new EventData(); + var eventData2 = new EventData(); + var eventData3 = new EventData(); + + var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(appId, "my-app") }; + var event2 = new ContentUpdated { Data = new NamedContentData() }; + var event3 = new ContentUpdated { Data = new NamedContentData() }; + + var events = new List + { + new StoredEvent("0", 0, eventData1), + new StoredEvent("1", 1, eventData2), + new StoredEvent("2", 2, eventData3) + }; + + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(events); + + A.CallTo(() => formatter.Parse(eventData1)) + .Returns(new Envelope(event1)); + A.CallTo(() => formatter.Parse(eventData2)) + .Returns(new Envelope(event2)); + A.CallTo(() => formatter.Parse(eventData3)) + .Returns(new Envelope(event3)); + + var data = await sut.LoadAsync(appId, id, 1); + + Assert.Equal(event2.Data, data); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs index 784d00c02..44608bc6a 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs @@ -20,8 +20,8 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly IDomainObjectFactory factory = A.Fake(); private readonly IEventStore eventStore = A.Fake(); - private readonly IStreamNameResolver streamNameResolver = A.Fake(); - private readonly EventDataFormatter eventDataFormatter = A.Fake(); + private readonly IStreamNameResolver nameResolver = A.Fake(); + private readonly EventDataFormatter formatter = A.Fake(); private readonly string streamName = Guid.NewGuid().ToString(); private readonly Guid aggregateId = Guid.NewGuid(); private readonly MyDomainObject domainObject; @@ -31,13 +31,13 @@ namespace Squidex.Infrastructure.CQRS.Commands { domainObject = new MyDomainObject(aggregateId, 123); - A.CallTo(() => streamNameResolver.GetStreamName(A.Ignored, aggregateId)) + A.CallTo(() => nameResolver.GetStreamName(A.Ignored, aggregateId)) .Returns(streamName); A.CallTo(() => factory.CreateNew(aggregateId)) .Returns(domainObject); - sut = new DefaultDomainObjectRepository(eventStore, streamNameResolver, eventDataFormatter); + sut = new DefaultDomainObjectRepository(eventStore, nameResolver, formatter); } public sealed class MyEvent : IEvent @@ -96,9 +96,9 @@ namespace Squidex.Infrastructure.CQRS.Commands A.CallTo(() => eventStore.GetEventsAsync(streamName)) .Returns(events); - A.CallTo(() => eventDataFormatter.Parse(eventData1)) + A.CallTo(() => formatter.Parse(eventData1)) .Returns(new Envelope(event1)); - A.CallTo(() => eventDataFormatter.Parse(eventData2)) + A.CallTo(() => formatter.Parse(eventData2)) .Returns(new Envelope(event2)); await sut.LoadAsync(domainObject); @@ -124,9 +124,9 @@ namespace Squidex.Infrastructure.CQRS.Commands A.CallTo(() => eventStore.GetEventsAsync(streamName)) .Returns(events); - A.CallTo(() => eventDataFormatter.Parse(eventData1)) + A.CallTo(() => formatter.Parse(eventData1)) .Returns(new Envelope(event1)); - A.CallTo(() => eventDataFormatter.Parse(eventData2)) + A.CallTo(() => formatter.Parse(eventData2)) .Returns(new Envelope(event2)); await Assert.ThrowsAsync(() => sut.LoadAsync(domainObject, 200)); @@ -143,9 +143,9 @@ namespace Squidex.Infrastructure.CQRS.Commands var eventData1 = new EventData(); var eventData2 = new EventData(); - A.CallTo(() => eventDataFormatter.ToEventData(A>.That.Matches(e => e.Payload == event1), commitId)) + A.CallTo(() => formatter.ToEventData(A>.That.Matches(e => e.Payload == event1), commitId)) .Returns(eventData1); - A.CallTo(() => eventDataFormatter.ToEventData(A>.That.Matches(e => e.Payload == event2), commitId)) + A.CallTo(() => formatter.ToEventData(A>.That.Matches(e => e.Payload == event2), commitId)) .Returns(eventData2); A.CallTo(() => eventStore.AppendEventsAsync(commitId, streamName, 123, A>.That.Matches(e => e.Count == 2))) @@ -170,9 +170,9 @@ namespace Squidex.Infrastructure.CQRS.Commands var eventData1 = new EventData(); var eventData2 = new EventData(); - A.CallTo(() => eventDataFormatter.ToEventData(A>.That.Matches(e => e.Payload == event1), commitId)) + A.CallTo(() => formatter.ToEventData(A>.That.Matches(e => e.Payload == event1), commitId)) .Returns(eventData1); - A.CallTo(() => eventDataFormatter.ToEventData(A>.That.Matches(e => e.Payload == event2), commitId)) + A.CallTo(() => formatter.ToEventData(A>.That.Matches(e => e.Payload == event2), commitId)) .Returns(eventData2); A.CallTo(() => eventStore.AppendEventsAsync(commitId, streamName, 123, A>.That.Matches(e => e.Count == 2))) From ae3089152556df8c1090b525e7a0da11308b9bdf Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 9 Sep 2017 20:16:39 +0200 Subject: [PATCH 03/10] Temporary version. --- .../History/MongoHistoryEventRepository.cs | 2 +- .../content/content-history.component.html | 2 +- .../content/content-history.component.ts | 6 ++++ .../pages/content/content-page.component.html | 4 +++ .../pages/content/content-page.component.ts | 27 ++++++++++++++++- .../app/features/content/pages/messages.ts | 1 - .../shared/services/contents.service.spec.ts | 30 +++++++++++++++++++ .../app/shared/services/contents.service.ts | 20 +++++++++++++ 8 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs index f597ea9fd..15610b5c8 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History { var historyEventEntities = await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix) - .SortByDescending(x => x.Created).ThenBy(x => x.Version).Limit(count) + .SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count) .ToListAsync(); return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.html b/src/Squidex/app/features/content/pages/content/content-history.component.html index 7175d30f3..cd7db5ce1 100644 --- a/src/Squidex/app/features/content/pages/content/content-history.component.html +++ b/src/Squidex/app/features/content/pages/content/content-history.component.html @@ -21,7 +21,7 @@
{{event.created | sqxFromNow}}
- Load this Version + Load this Version diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.ts b/src/Squidex/app/features/content/pages/content/content-history.component.ts index e6df00a82..5a7547e6b 100644 --- a/src/Squidex/app/features/content/pages/content/content-history.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-history.component.ts @@ -21,6 +21,8 @@ import { UsersProviderService } from 'shared'; +import { ContentVersionSelected } from './../messages'; + const REPLACEMENT_TEMP = '$TEMP$'; @Component({ @@ -76,6 +78,10 @@ export class ContentHistoryComponent extends AppComponentBase { } } + public loadVersion(version: number) { + this.messageBus.emit(new ContentVersionSelected(version)); + } + public format(message: string): Observable { let foundUserId: string | null = null; diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index a75d7e44a..707b90122 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -38,6 +38,10 @@
+
+ You have not created the subscription. Therefore you cannot change the plan. +
+
diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 6dc4b5e49..b181af1f4 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -13,7 +13,8 @@ import { Observable, Subscription } from 'rxjs'; import { ContentCreated, ContentDeleted, - ContentUpdated + ContentUpdated, + ContentVersionSelected } from './../messages'; import { @@ -38,6 +39,7 @@ import { }) export class ContentPageComponent extends AppComponentBase implements CanComponentDeactivate, OnDestroy, OnInit { private contentDeletedSubscription: Subscription; + private contentVersionSelectedSubscription: Subscription; private version = new Version(''); private content: ContentDto; @@ -63,6 +65,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone } public ngOnDestroy() { + this.contentVersionSelectedSubscription.unsubscribe(); this.contentDeletedSubscription.unsubscribe(); } @@ -71,6 +74,12 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone this.languages = routeData['appLanguages']; + this.contentVersionSelectedSubscription = + this.messageBus.of(ContentVersionSelected) + .subscribe(message => { + this.loadVersion(message.version); + }); + this.contentDeletedSubscription = this.messageBus.of(ContentDeleted) .subscribe(message => { @@ -146,6 +155,22 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone } } + private loadVersion(version: number) { + if (!this.isNewMode && this.content) { + this.appNameOnce() + .switchMap(app => this.contentsService.getVersionData(app, this.schema.name, this.contentId!, new Version(version.toString()))) + .subscribe(dto => { + this.content = this.content.setData(dto); + + this.emitContentUpdated(this.content); + this.notifyInfo('Content version loaded successfully.'); + this.populateContentForm(); + }, error => { + this.notifyError(error); + }); + } + } + private back() { this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true }); } diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 569c201ad..13a340fcd 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -30,7 +30,6 @@ export class ContentDeleted { export class ContentVersionSelected { constructor( - public readonly id: string, public readonly version: number ) { } diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 41eaa39cc..849c9f77e 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -51,6 +51,15 @@ describe('ContentDto', () => { expect(content_2.lastModified).toEqual(now); expect(content_2.lastModifiedBy).toEqual('me'); }); + + it('should update data property when setting data', () => { + const newData = {}; + + const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); + const content_2 = content_1.setData(newData); + + expect(content_2.data).toBe(newData); + }); }); describe('ContentsService', () => { @@ -270,6 +279,27 @@ describe('ContentsService', () => { new Version('11'))); })); + it('should make get request to get versioned content data', + inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { + + const response = {}; + + let data: any | null = null; + + contentsService.getVersionData('my-app', 'my-schema', 'content1', version).subscribe(result => { + data = result; + }); + + const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/1'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBe(version.value); + + req.flush(response); + + expect(data).toBe(response); + })); + it('should make put request to update content', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index a4cad43c3..0e70d45a9 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -41,6 +41,19 @@ export class ContentDto { ) { } + public setData(data: any): ContentDto { + return new ContentDto( + this.id, + this.isPublished, + this.isDeleted, + this.createdBy, + this.lastModifiedBy, + this.created, + this.lastModified, + data, + this.version); + } + public publish(user: string, now?: DateTime): ContentDto { return new ContentDto( this.id, @@ -204,6 +217,13 @@ export class ContentsService { .pretifyError('Failed to delete content. Please reload.'); } + public getVersionData(appName: string, schemaName: string, id: string, version?: Version): Observable { + const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`); + + return HTTP.getVersioned(this.http, url, version) + .pretifyError('Failed to load data. Please reload.'); + } + public publishContent(appName: string, schemaName: string, id: string, version?: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`); From 2df3ce73af63ba36d8960f838adceee3b78d8929 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 9 Sep 2017 22:33:32 +0200 Subject: [PATCH 04/10] TS improvements --- .../pages/content/content-page.component.ts | 2 +- .../pages/contents/contents-page.component.ts | 4 +- .../content/shared/assets-editor.component.ts | 21 ++--- .../shared/references-editor.component.ts | 21 ++--- .../pages/dashboard-page.component.ts | 2 +- .../pages/schema/schema-page.component.ts | 24 ++--- .../pages/schemas/schema-form.component.ts | 3 +- .../pages/webhook-events-page.component.ts | 2 +- .../webhooks/pages/webhook.component.ts | 2 +- .../webhooks/pages/webhooks-page.component.ts | 2 +- .../angular/autocomplete.component.ts | 19 ++-- .../angular/confirm-click.directive.ts | 12 ++- .../app/framework/angular/copy.directive.ts | 3 +- .../angular/date-time-editor.component.ts | 20 ++-- .../angular/dialog-renderer.component.ts | 2 +- .../framework/angular/dropdown.component.ts | 13 ++- .../angular/geolocation-editor.component.ts | 31 ++++--- .../framework/angular/http-extensions-impl.ts | 2 +- .../angular/indeterminate-value.directive.ts | 18 ++-- .../angular/jscript-editor.component.ts | 30 +++--- .../angular/json-editor.component.ts | 35 +++---- .../angular/lowercase-input.directive.ts | 16 ++-- .../angular/markdown-editor.component.ts | 24 ++--- .../framework/angular/modal-view.directive.ts | 4 +- .../angular/panel-container.directive.ts | 2 +- .../angular/rich-editor.component.ts | 24 ++--- .../app/framework/angular/router-utils.ts | 4 +- .../app/framework/angular/slider.component.ts | 22 ++--- .../app/framework/angular/stars.component.ts | 37 ++++---- .../framework/angular/tag-editor.component.ts | 56 +++++++---- .../app/framework/angular/toggle.component.ts | 21 +++-- .../app/framework/angular/validators.ts | 39 ++++---- src/Squidex/app/framework/declarations.ts | 1 + .../framework/services/local-cache.service.ts | 2 +- src/Squidex/app/framework/utils/types.spec.ts | 92 +++++++++++++++++++ src/Squidex/app/framework/utils/types.ts | 70 ++++++++++++++ .../app/shared/components/asset.component.ts | 6 +- .../guards/resolve-app-languages.guard.ts | 4 +- .../shared/guards/resolve-content.guard.ts | 4 +- .../app/shared/guards/resolve-user.guard.ts | 4 +- .../interceptors/auth.interceptor.spec.ts | 2 +- .../shared/interceptors/auth.interceptor.ts | 2 +- .../shared/services/app-clients.service.ts | 6 +- .../services/app-contributors.service.ts | 4 +- .../shared/services/app-languages.service.ts | 6 +- .../shared/services/assets.service.spec.ts | 19 ++-- .../app/shared/services/assets.service.ts | 23 ++--- .../app/shared/services/auth.service.ts | 18 ++-- .../shared/services/contents.service.spec.ts | 16 ++-- .../app/shared/services/contents.service.ts | 14 +-- .../app/shared/services/plans.service.ts | 2 +- .../shared/services/schemas.service.spec.ts | 32 +++---- .../app/shared/services/schemas.service.ts | 38 ++++---- .../app/shared/services/ui.service.spec.ts | 3 +- .../shared/services/webhooks.service.spec.ts | 4 +- .../app/shared/services/webhooks.service.ts | 10 +- .../pages/internal/profile-menu.component.ts | 6 +- src/Squidex/tsconfig.json | 2 +- 58 files changed, 554 insertions(+), 353 deletions(-) create mode 100644 src/Squidex/app/framework/utils/types.spec.ts create mode 100644 src/Squidex/app/framework/utils/types.ts diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index b181af1f4..8645ebcfa 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -139,7 +139,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone this.appNameOnce() .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId!, requestDto, this.version)) .subscribe(dto => { - this.content = this.content.update(dto, this.authService.user.token); + this.content = this.content.update(dto, this.authService.user!.token); this.emitContentUpdated(this.content); this.notifyInfo('Content saved successfully.'); diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index 998747c61..f914ca7da 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -123,7 +123,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.appNameOnce() .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { - this.contentItems = this.contentItems.replaceBy('id', content.publish(this.authService.user.token)); + this.contentItems = this.contentItems.replaceBy('id', content.publish(this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -133,7 +133,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.appNameOnce() .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { - this.contentItems = this.contentItems.replaceBy('id', content.unpublish(this.authService.user.token)); + this.contentItems = this.contentItems.replaceBy('id', content.unpublish(this.authService.user!.token)); }, error => { this.notifyError(error); }); diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index e23fbd2ba..31e441741 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/src/Squidex/app/features/content/shared/assets-editor.component.ts @@ -19,11 +19,10 @@ import { AssetUpdated, DialogService, ImmutableArray, - MessageBus + MessageBus, + Types } from 'shared'; -const NOOP = () => { /* NOOP */ }; - export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetsEditorComponent), multi: true }; @@ -36,8 +35,8 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = { }) export class AssetsEditorComponent extends AppComponentBase implements ControlValueAccessor, OnDestroy, OnInit { private assetUpdatedSubscription: Subscription; - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; public newAssets = ImmutableArray.empty(); public oldAssets = ImmutableArray.empty(); @@ -65,10 +64,10 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa this.assetUpdatedSubscription.unsubscribe(); } - public writeValue(value: any) { + public writeValue(value: string[]) { this.oldAssets = ImmutableArray.empty(); - if (value && value.length > 0) { + if (Types.isArrayOfString(value) && value.length > 0) { const assetIds: string[] = value; this.appNameOnce() @@ -84,11 +83,11 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public addFiles(files: FileList) { @@ -143,7 +142,7 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa ids = null; } - this.touchedCallback(); - this.changeCallback(ids); + this.onTouched(); + this.onChange(ids); } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/src/Squidex/app/features/content/shared/references-editor.component.ts index 59b4e01d4..ded63f413 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.ts +++ b/src/Squidex/app/features/content/shared/references-editor.component.ts @@ -19,11 +19,10 @@ import { FieldDto, ImmutableArray, SchemaDetailsDto, - SchemasService + SchemasService, + Types } from 'shared'; -const NOOP = () => { /* NOOP */ }; - export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true }; @@ -35,8 +34,8 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class ReferencesEditorComponent extends AppComponentBase implements ControlValueAccessor, OnInit { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; @Input() public schemaId: string; @@ -73,10 +72,10 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr }); } - public writeValue(value: any) { + public writeValue(value: string[]) { this.contentItems = ImmutableArray.empty(); - if (value && value.length > 0) { + if (Types.isArrayOfString(value) && value.length > 0) { const contentIds: string[] = value; this.appNameOnce() @@ -92,11 +91,11 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public canDrop() { @@ -138,8 +137,8 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr ids = null; } - this.touchedCallback(); - this.changeCallback(ids); + this.onTouched(); + this.onChange(ids); } private loadFields() { diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts index 73d5ea2ca..c3ec3adad 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts @@ -146,7 +146,7 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit { }; }); - this.profileDisplayName = this.authService.user.displayName; + this.profileDisplayName = this.authService.user!.displayName; } public showForum() { diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index 543ddc443..f38b8cc3f 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -110,7 +110,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.publishSchema(app, this.schema.name, this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.publish(this.authService.user.token)); + this.updateSchema(this.schema.publish(this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -120,7 +120,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.unpublishSchema(app, this.schema.name, this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.unpublish(this.authService.user.token)); + this.updateSchema(this.schema.unpublish(this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -130,7 +130,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.enableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.updateField(field.enable(), this.authService.user.token)); + this.updateSchema(this.schema.updateField(field.enable(), this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -140,7 +140,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.disableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.updateField(field.disable(), this.authService.user.token)); + this.updateSchema(this.schema.updateField(field.disable(), this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -150,7 +150,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.lockField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.updateField(field.lock(), this.authService.user.token)); + this.updateSchema(this.schema.updateField(field.lock(), this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -160,7 +160,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.hideField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.updateField(field.hide(), this.authService.user.token)); + this.updateSchema(this.schema.updateField(field.hide(), this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -170,7 +170,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.deleteField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.removeField(field, this.authService.user.token)); + this.updateSchema(this.schema.removeField(field, this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -180,7 +180,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.putFieldOrdering(app, this.schema.name, fields.map(t => t.fieldId), this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.replaceFields(fields, this.authService.user.token)); + this.updateSchema(this.schema.replaceFields(fields, this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -192,7 +192,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.putField(app, this.schema.name, field.fieldId, requestDto, this.schema.version)).retry(2) .subscribe(() => { - this.updateSchema(this.schema.updateField(field, this.authService.user.token)); + this.updateSchema(this.schema.updateField(field, this.authService.user!.token)); }, error => { this.notifyError(error); }); @@ -223,7 +223,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.schemasService.postField(app, this.schema.name, requestDto, this.schema.version)) .subscribe(dto => { - this.updateSchema(this.schema.addField(dto, this.authService.user.token)); + this.updateSchema(this.schema.addField(dto, this.authService.user!.token)); this.resetFieldForm(); }, error => { this.notifyError(error); @@ -237,13 +237,13 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { } public onSchemaSaved(properties: SchemaPropertiesDto) { - this.updateSchema(this.schema.update(properties, this.authService.user.token)); + this.updateSchema(this.schema.update(properties, this.authService.user!.token)); this.editSchemaDialog.hide(); } public onSchemaScriptsSaved(scripts: UpdateSchemaScriptsDto) { - this.updateSchema(this.schema.configureScripts(scripts, this.authService.user.token)); + this.updateSchema(this.schema.configureScripts(scripts, this.authService.user!.token)); this.configureScriptsDialog.hide(); } diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts index 6f903d98f..d51a15659 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts @@ -11,6 +11,7 @@ import { FormBuilder, Validators } from '@angular/forms'; import { ApiUrlConfig, AuthService, + DateTime, fadeAnimation, SchemaDetailsDto, SchemasService, @@ -89,7 +90,7 @@ export class SchemaFormComponent { const me = this.authService.user!.token; - this.schemas.postSchema(this.appName, requestDto, me, undefined, schemaVersion) + this.schemas.postSchema(this.appName, requestDto, me, DateTime.now(), schemaVersion) .subscribe(dto => { this.emitCreated(dto); this.resetCreateForm(); diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts b/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts index a22dae47f..a5db09266 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts +++ b/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts @@ -26,7 +26,7 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn public eventsItems = ImmutableArray.empty(); public eventsPager = new Pager(0); - public selectedEventId: string; + public selectedEventId: string | null = null; constructor(dialogs: DialogService, appsStore: AppsStoreService, private readonly webhooksService: WebhooksService diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.ts b/src/Squidex/app/features/webhooks/pages/webhook.component.ts index 34c51dbbe..ae8b3ac54 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.ts +++ b/src/Squidex/app/features/webhooks/pages/webhook.component.ts @@ -91,7 +91,7 @@ export class WebhookComponent implements OnInit { } else { return null; } - }).filter(w => !!w)).sortByStringAsc(x => x.schema.name); + }).filter(w => w !== null).map(w => w!)).sortByStringAsc(x => x.schema.name); this.schemasToAdd = ImmutableArray.of( diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts index c7619bae6..83d653306 100644 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts +++ b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts @@ -91,7 +91,7 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version)) .subscribe(dto => { - this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.authService.user.token)); + this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.authService.user!.token)); this.notifyInfo('Webhook saved.'); }, error => { diff --git a/src/Squidex/app/framework/angular/autocomplete.component.ts b/src/Squidex/app/framework/angular/autocomplete.component.ts index 3742e6fac..e492a7f6b 100644 --- a/src/Squidex/app/framework/angular/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/autocomplete.component.ts @@ -17,7 +17,6 @@ const KEY_ENTER = 13; const KEY_ESCAPE = 27; const KEY_UP = 38; const KEY_DOWN = 40; -const NOOP = () => { /* NOOP */ }; export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true @@ -31,8 +30,8 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { }) export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit { private subscription: Subscription; - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; @Input() public source: AutocompleteSource; @@ -57,7 +56,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O public writeValue(value: any) { if (!value) { - this.resetValue(); + this.resetForm(); } else { const item = this.items.find(i => i === value); @@ -79,11 +78,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public ngOnDestroy() { @@ -119,7 +118,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O this.down(); return false; case KEY_ESCAPE: - this.resetValue(); + this.resetForm(); this.reset(); return false; case KEY_ENTER: @@ -135,7 +134,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O public blur() { this.reset(); - this.touchedCallback(); + this.onTouched(); } public selectItem(selection: any | null = null) { @@ -154,7 +153,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O } else { this.queryInput.setValue(selection.toString(), { emitEvent: false }); } - this.changeCallback(selection); + this.onChange(selection); } finally { this.reset(); } @@ -181,7 +180,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O this.selectIndex(this.selectedIndex + 1); } - private resetValue() { + private resetForm() { this.queryInput.setValue(''); } diff --git a/src/Squidex/app/framework/angular/confirm-click.directive.ts b/src/Squidex/app/framework/angular/confirm-click.directive.ts index 74d9980b0..168453a8c 100644 --- a/src/Squidex/app/framework/angular/confirm-click.directive.ts +++ b/src/Squidex/app/framework/angular/confirm-click.directive.ts @@ -10,11 +10,13 @@ import { Directive, EventEmitter, HostListener, Input, OnDestroy, Output } from import { DialogService } from './../services/dialog.service'; class DelayEventEmitter extends EventEmitter { - private delayedNexts: any[] = []; + private delayedNexts: any[] | null = []; public delayEmit() { - for (let callback of this.delayedNexts) { - callback(); + if (this.delayedNexts) { + for (let callback of this.delayedNexts) { + callback(); + } } } @@ -23,7 +25,9 @@ class DelayEventEmitter extends EventEmitter { } public subscribe(generatorOrNext?: any, error?: any, complete?: any): any { - this.delayedNexts.push(generatorOrNext); + if (this.delayedNexts) { + this.delayedNexts.push(generatorOrNext); + } return super.subscribe(generatorOrNext, error, complete); } diff --git a/src/Squidex/app/framework/angular/copy.directive.ts b/src/Squidex/app/framework/angular/copy.directive.ts index 356b37bd5..8cf9f5a23 100644 --- a/src/Squidex/app/framework/angular/copy.directive.ts +++ b/src/Squidex/app/framework/angular/copy.directive.ts @@ -7,6 +7,7 @@ import { Directive, HostListener, Input } from '@angular/core'; +import { Types } from './../utils/types'; import { DialogService, Notification } from './../services/dialog.service'; @Directive({ @@ -48,7 +49,7 @@ export class CopyDirective { console.log('Copy failed'); } - if (currentFocus && typeof currentFocus.focus === 'function') { + if (currentFocus && Types.isFunction(currentFocus.focus)) { currentFocus.focus(); } diff --git a/src/Squidex/app/framework/angular/date-time-editor.component.ts b/src/Squidex/app/framework/angular/date-time-editor.component.ts index 85360418d..309b3bed4 100644 --- a/src/Squidex/app/framework/angular/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/date-time-editor.component.ts @@ -12,8 +12,6 @@ import * as moment from 'moment'; let Pikaday = require('pikaday/pikaday'); -const NOOP = () => { /* NOOP */ }; - export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateTimeEditorComponent), multi: true }; @@ -31,8 +29,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, private timeValue: any | null = null; private dateValue: any | null = null; private suppressEvents = false; - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; @Input() public mode: string; @@ -116,11 +114,11 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public ngAfterViewInit() { @@ -140,7 +138,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } public touched() { - this.touchedCallback(); + this.onTouched(); } public writeNow() { @@ -159,14 +157,14 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, this.dateValue = null; - this.changeCallback(null); - this.touchedCallback(); + this.onChange(null); + this.onTouched(); return false; } private updateValue() { - let result: string | null; + let result: string | null = null; if ((this.dateValue && !this.dateValue.isValid()) || (this.timeValue && !this.timeValue.isValid())) { result = 'Invalid DateTime'; @@ -184,7 +182,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } } - this.changeCallback(result); + this.onChange(result); } private updateControls() { diff --git a/src/Squidex/app/framework/angular/dialog-renderer.component.ts b/src/Squidex/app/framework/angular/dialog-renderer.component.ts index 32315a66b..12c1d549e 100644 --- a/src/Squidex/app/framework/angular/dialog-renderer.component.ts +++ b/src/Squidex/app/framework/angular/dialog-renderer.component.ts @@ -32,7 +32,7 @@ export class DialogRendererComponent implements OnDestroy, OnInit { private notificationsSubscription: Subscription; public dialogView = new ModalView(false, true); - public dialogRequest: DialogRequest; + public dialogRequest: DialogRequest | null = null; public notifications: Notification[] = []; diff --git a/src/Squidex/app/framework/angular/dropdown.component.ts b/src/Squidex/app/framework/angular/dropdown.component.ts index a4ec504a5..f7d0fc118 100644 --- a/src/Squidex/app/framework/angular/dropdown.component.ts +++ b/src/Squidex/app/framework/angular/dropdown.component.ts @@ -12,7 +12,6 @@ const KEY_ENTER = 13; const KEY_ESCAPE = 27; const KEY_UP = 38; const KEY_DOWN = 40; -const NOOP = () => { /* NOOP */ }; import { ModalView } from './../utils/modal-view'; @@ -27,8 +26,8 @@ export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR] }) export class DropdownComponent implements AfterContentInit, ControlValueAccessor { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; @Input() public items: any[] = []; @@ -69,11 +68,11 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public onKeyDown(event: KeyboardEvent) { @@ -95,7 +94,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor public open() { this.dropdown.show(); - this.touchedCallback(); + this.onTouched(); } public selectIndexAndClose(selectedIndex: number) { @@ -132,7 +131,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor this.selectedIndex = selectedIndex; this.selectedItem = value; - this.changeCallback(value); + this.onChange(value); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.ts b/src/Squidex/app/framework/angular/geolocation-editor.component.ts index 00001813a..3cf44cbf3 100644 --- a/src/Squidex/app/framework/angular/geolocation-editor.component.ts +++ b/src/Squidex/app/framework/angular/geolocation-editor.component.ts @@ -8,17 +8,22 @@ import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Types } from './../utils/types'; + import { ResourceLoaderService } from './../services/resource-loader.service'; import { ValidatorsEx } from './validators'; -const NOOP = () => { /* NOOP */ }; - declare var L: any; export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => GeolocationEditorComponent), multi: true }; +interface Geolocation { + latitude: number; + longitude: number; +} + @Component({ selector: 'sqx-geolocation-editor', styleUrls: ['./geolocation-editor.component.scss'], @@ -26,11 +31,11 @@ export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; private marker: any; private map: any; - private value: any; + private value: Geolocation | null = null; public get hasValue() { return !!this.value; @@ -59,8 +64,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi ) { } - public writeValue(value: any) { - this.value = value; + public writeValue(value: Geolocation) { + if (Types.isObject(value) && Types.isNumber(value.latitude) && Types.isNumber(value.longitude)) { + this.value = value; + } else { + this.value = null; + } if (this.marker) { this.updateMarker(true, false); @@ -102,11 +111,11 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public updateValueByInput() { @@ -201,8 +210,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } if (fireEvent) { - this.changeCallback(this.value); - this.touchedCallback(); + this.onChange(this.value); + this.onTouched(); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/http-extensions-impl.ts b/src/Squidex/app/framework/angular/http-extensions-impl.ts index 2ff40ff46..ac444d80a 100644 --- a/src/Squidex/app/framework/angular/http-extensions-impl.ts +++ b/src/Squidex/app/framework/angular/http-extensions-impl.ts @@ -78,7 +78,7 @@ export module HTTP { } } - function handleVersion(httpRequest: Observable>, version: Version): Observable { + function handleVersion(httpRequest: Observable>, version?: Version): Observable { return httpRequest.do((response: HttpResponse) => { if (version && response.status.toString().indexOf('2') === 0 && response.headers) { const etag = response.headers.get('etag'); diff --git a/src/Squidex/app/framework/angular/indeterminate-value.directive.ts b/src/Squidex/app/framework/angular/indeterminate-value.directive.ts index 9e48bca7e..034bd5b79 100644 --- a/src/Squidex/app/framework/angular/indeterminate-value.directive.ts +++ b/src/Squidex/app/framework/angular/indeterminate-value.directive.ts @@ -8,7 +8,7 @@ import { Directive, forwardRef, ElementRef, HostListener, Renderer } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -const NOOP = () => { /* NOOP */ }; +import { Types } from './../utils/types'; export const SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IndeterminateValueDirective), multi: true @@ -19,8 +19,8 @@ export const SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR] }) export class IndeterminateValueDirective implements ControlValueAccessor { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; constructor( private readonly renderer: Renderer, @@ -30,16 +30,16 @@ export class IndeterminateValueDirective implements ControlValueAccessor { @HostListener('change', ['$event.target.value']) public onChange(value: any) { - this.changeCallback(value); + this.onChange(value); } @HostListener('blur') public onTouched() { - this.touchedCallback(); + this.onTouched(); } - public writeValue(value: any) { - if (value === undefined || value === null) { + public writeValue(value: boolean | number | undefined) { + if (!Types.isBoolean(value)) { this.renderer.setElementProperty(this.element.nativeElement, 'indeterminate', true); } else { this.renderer.setElementProperty(this.element.nativeElement, 'checked', value); @@ -51,10 +51,10 @@ export class IndeterminateValueDirective implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/jscript-editor.component.ts b/src/Squidex/app/framework/angular/jscript-editor.component.ts index be2f225b6..e2b7cc74e 100644 --- a/src/Squidex/app/framework/angular/jscript-editor.component.ts +++ b/src/Squidex/app/framework/angular/jscript-editor.component.ts @@ -9,12 +9,12 @@ import { AfterViewInit, Component, forwardRef, ElementRef, ViewChild } from '@an import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; +import { Types } from './../utils/types'; + import { ResourceLoaderService } from './../services/resource-loader.service'; declare var ace: any; -const NOOP = () => { /* NOOP */ }; - export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JscriptEditorComponent), multi: true }; @@ -26,11 +26,11 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; private valueChanged = new Subject(); private aceEditor: any; - private oldValue: string; + private value: string; private isDisabled = false; @ViewChild('editor') @@ -41,11 +41,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn ) { } - public writeValue(value: any) { - this.oldValue = value; + public writeValue(value: string) { + this.value = Types.isString(value) ? value : ''; if (this.aceEditor) { - this.setValue(value); + this.setValue(this.value); } } @@ -58,11 +58,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public ngAfterViewInit() { @@ -78,11 +78,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn this.aceEditor.setReadOnly(this.isDisabled); this.aceEditor.setFontSize(14); - this.setValue(this.oldValue); + this.setValue(this.value); this.aceEditor.on('blur', () => { this.changeValue(); - this.touchedCallback(); + this.onTouched(); }); this.aceEditor.on('change', () => { @@ -94,11 +94,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn private changeValue() { const newValue = this.aceEditor.getValue(); - if (this.oldValue !== newValue) { - this.changeCallback(newValue); + if (this.value !== newValue) { + this.onChange(newValue); } - this.oldValue = newValue; + this.value = newValue; } private setValue(value: string) { diff --git a/src/Squidex/app/framework/angular/json-editor.component.ts b/src/Squidex/app/framework/angular/json-editor.component.ts index 281d7d863..90beb4b27 100644 --- a/src/Squidex/app/framework/angular/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/json-editor.component.ts @@ -13,8 +13,6 @@ import { ResourceLoaderService } from './../services/resource-loader.service'; declare var ace: any; -const NOOP = () => { /* NOOP */ }; - export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JsonEditorComponent), multi: true }; @@ -26,12 +24,12 @@ export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; private valueChanged = new Subject(); private aceEditor: any; - private oldValue: any; - private oldValueString: string; + private value: any; + private valueString: string; private isDisabled = false; @ViewChild('editor') @@ -43,8 +41,13 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit } public writeValue(value: any) { - this.oldValue = value; - this.oldValueString = JSON.stringify(value); + this.value = value; + + try { + this.valueString = JSON.stringify(value); + } catch (e) { + this.valueString = ''; + } if (this.aceEditor) { this.setValue(value); @@ -60,11 +63,11 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public ngAfterViewInit() { @@ -80,11 +83,11 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit this.aceEditor.setReadOnly(this.isDisabled); this.aceEditor.setFontSize(14); - this.setValue(this.oldValue); + this.setValue(this.value); this.aceEditor.on('blur', () => { this.changeValue(); - this.touchedCallback(); + this.onTouched(); }); this.aceEditor.on('change', () => { @@ -108,12 +111,12 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit const newValueString = JSON.stringify(newValue); - if (this.oldValueString !== newValueString) { - this.changeCallback(newValue); + if (this.valueString !== newValueString) { + this.onChange(newValue); } - this.oldValue = newValue; - this.oldValueString = newValueString; + this.value = newValue; + this.valueString = newValueString; } private setValue(value: any) { diff --git a/src/Squidex/app/framework/angular/lowercase-input.directive.ts b/src/Squidex/app/framework/angular/lowercase-input.directive.ts index 69b40315a..66d61338d 100644 --- a/src/Squidex/app/framework/angular/lowercase-input.directive.ts +++ b/src/Squidex/app/framework/angular/lowercase-input.directive.ts @@ -8,8 +8,6 @@ import { Directive, forwardRef, ElementRef, HostListener, Renderer } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -const NOOP = () => { /* NOOP */ }; - export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LowerCaseInputDirective), multi: true }; @@ -19,8 +17,8 @@ export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = { providers: [SQX_LOWERCASE_INPUT_VALUE_ACCESSOR] }) export class LowerCaseInputDirective implements ControlValueAccessor { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; constructor( private readonly element: ElementRef, @@ -33,16 +31,16 @@ export class LowerCaseInputDirective implements ControlValueAccessor { const normalizedValue = (value == null ? '' : value.toString()).toLowerCase(); this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); - this.changeCallback(normalizedValue); + this.onChange(normalizedValue); } @HostListener('blur') public onTouched() { - this.touchedCallback(); + this.onTouched(); } public writeValue(value: any) { - const normalizedValue = (value == null ? '' : value.toString()).toLowerCase(); + const normalizedValue = value ? '' : value.toString().toLowerCase(); this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); } @@ -52,10 +50,10 @@ export class LowerCaseInputDirective implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/markdown-editor.component.ts b/src/Squidex/app/framework/angular/markdown-editor.component.ts index b06f9494b..9716ef10f 100644 --- a/src/Squidex/app/framework/angular/markdown-editor.component.ts +++ b/src/Squidex/app/framework/angular/markdown-editor.component.ts @@ -8,12 +8,12 @@ import { AfterViewInit, Component, forwardRef, ElementRef, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Types } from './../utils/types'; + import { ResourceLoaderService } from './../services/resource-loader.service'; declare var SimpleMDE: any; -const NOOP = () => { /* NOOP */ }; - export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true }; @@ -25,10 +25,10 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; private simplemde: any; - private value: any; + private value: string; private isDisabled = false; @ViewChild('editor') @@ -48,11 +48,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css'); } - public writeValue(value: any) { - this.value = value; + public writeValue(value: string) { + this.value = Types.isString(value) ? value : ''; if (this.simplemde) { - this.simplemde.value(this.value || ''); + this.simplemde.value(this.value); } } @@ -65,11 +65,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public ngAfterViewInit() { @@ -84,12 +84,12 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI if (this.value !== value) { this.value = value; - this.changeCallback(value); + this.onChange(value); } }); this.simplemde.codemirror.on('blur', () => { - this.touchedCallback(); + this.onTouched(); }); this.simplemde.codemirror.on('refresh', () => { diff --git a/src/Squidex/app/framework/angular/modal-view.directive.ts b/src/Squidex/app/framework/angular/modal-view.directive.ts index 3ff9afcdd..0082b4603 100644 --- a/src/Squidex/app/framework/angular/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modal-view.directive.ts @@ -16,8 +16,8 @@ import { RootViewService } from './../services/root-view.service'; selector: '[sqxModalView]' }) export class ModalViewDirective implements OnChanges, OnDestroy { - private subscription: Subscription | null; - private clickHandler: Function | null; + private subscription: Subscription | null = null; + private clickHandler: Function | null = null; private renderedView: EmbeddedViewRef | null = null; @Input('sqxModalView') diff --git a/src/Squidex/app/framework/angular/panel-container.directive.ts b/src/Squidex/app/framework/angular/panel-container.directive.ts index b96e2912d..1a5b983bb 100644 --- a/src/Squidex/app/framework/angular/panel-container.directive.ts +++ b/src/Squidex/app/framework/angular/panel-container.directive.ts @@ -49,7 +49,7 @@ export class PanelContainerDirective implements AfterViewInit, OnDestroy { } public invalidate(params?: { force: boolean, resize: boolean }) { - this.isInit = this.isInit || (params && params.force); + this.isInit = this.isInit || (params && params.force) === true; if (!this.isInit) { return; diff --git a/src/Squidex/app/framework/angular/rich-editor.component.ts b/src/Squidex/app/framework/angular/rich-editor.component.ts index d43668144..c690ed43a 100644 --- a/src/Squidex/app/framework/angular/rich-editor.component.ts +++ b/src/Squidex/app/framework/angular/rich-editor.component.ts @@ -8,12 +8,12 @@ import { AfterViewInit, Component, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Types } from './../utils/types'; + import { ResourceLoaderService } from './../services/resource-loader.service'; declare var tinymce: any; -const NOOP = () => { /* NOOP */ }; - export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true }; @@ -25,10 +25,10 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; private tinyEditor: any; - private value: any; + private value: string; private isDisabled = false; @ViewChild('editor') @@ -39,11 +39,11 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, ) { } - public writeValue(value: any) { - this.value = value; + public writeValue(value: string) { + this.value = Types.isString(value) ? value : ''; if (this.tinyEditor) { - this.tinyEditor.setContent(value || ''); + this.tinyEditor.setContent(this.value); } } @@ -56,11 +56,11 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public ngAfterViewInit() { @@ -78,12 +78,12 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, if (this.value !== value) { this.value = value; - self.changeCallback(value); + self.onChange(value); } }); self.tinyEditor.on('blur', () => { - self.touchedCallback(); + self.onTouched(); }); setTimeout(() => { diff --git a/src/Squidex/app/framework/angular/router-utils.ts b/src/Squidex/app/framework/angular/router-utils.ts index 82fb59638..14563787f 100644 --- a/src/Squidex/app/framework/angular/router-utils.ts +++ b/src/Squidex/app/framework/angular/router-utils.ts @@ -8,7 +8,7 @@ import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params } from '@angular/router'; export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data { - let snapshot: ActivatedRouteSnapshot = value['snapshot'] || value; + let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value; const result: { [key: string]: any } = { }; @@ -25,7 +25,7 @@ export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data { return result; } export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Params { - let snapshot: ActivatedRouteSnapshot = value['snapshot'] || value; + let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value; const result: { [key: string]: any } = { }; diff --git a/src/Squidex/app/framework/angular/slider.component.ts b/src/Squidex/app/framework/angular/slider.component.ts index f429e385f..2fe82b581 100644 --- a/src/Squidex/app/framework/angular/slider.component.ts +++ b/src/Squidex/app/framework/angular/slider.component.ts @@ -8,7 +8,7 @@ import { Component, ElementRef, forwardRef, Input, Renderer, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -const NOOP = () => { /* NOOP */ }; +import { Types } from './../utils/types'; export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SliderComponent), multi: true @@ -21,10 +21,10 @@ export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR] }) export class SliderComponent implements ControlValueAccessor { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; - private mouseMoveSubscription: Function | null; - private mouseUpSubscription: Function | null; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; + private mouseMoveSubscription: Function | null = null; + private mouseUpSubscription: Function | null = null; private centerStartOffset = 0; private startValue: number; private lastValue: number; @@ -50,8 +50,8 @@ export class SliderComponent implements ControlValueAccessor { constructor(private readonly renderer: Renderer) { } - public writeValue(value: any) { - this.lastValue = this.value = value; + public writeValue(value: number) { + this.lastValue = this.value = Types.isNumber(value) ? value : 0; this.updateThumbPosition(); } @@ -61,11 +61,11 @@ export class SliderComponent implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public onBarMouseClick(event: MouseEvent): boolean { @@ -157,14 +157,14 @@ export class SliderComponent implements ControlValueAccessor { } private updateTouched() { - this.touchedCallback(); + this.onTouched(); } private updateValue() { if (this.lastValue !== this.value) { this.lastValue = this.value; - this.changeCallback(this.value); + this.onChange(this.value); } } diff --git a/src/Squidex/app/framework/angular/stars.component.ts b/src/Squidex/app/framework/angular/stars.component.ts index 9b751b494..6afa84d37 100644 --- a/src/Squidex/app/framework/angular/stars.component.ts +++ b/src/Squidex/app/framework/angular/stars.component.ts @@ -8,7 +8,7 @@ import { Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -const NOOP = () => { /* NOOP */ }; +import { Types } from './../utils/types'; export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarsComponent), multi: true @@ -21,19 +21,15 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR] }) export class StarsComponent implements ControlValueAccessor { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; private maximumStarsValue = 5; @Input() - public set maximumStars(value: any) { - value = value || 5; + public set maximumStars(value: number) { + const maxStars: number = Types.isNumber(value) ? value : 5; - if (!(typeof value === 'number')) { - value = 5; - } - - if (this.maximumStarsValue !== value) { + if (this.maximumStarsValue !== maxStars) { this.maximumStarsValue = value; this.starsArray = []; @@ -55,8 +51,13 @@ export class StarsComponent implements ControlValueAccessor { public value: number | null = 1; - public writeValue(value: any) { - this.value = this.stars = value; + public writeValue(value: number | null | undefined) { + if (Types.isNumber(value)) { + this.value = this.stars = value!; + } else { + this.value = null; + this.stars = 0; + } } public setDisabledState(isDisabled: boolean): void { @@ -64,11 +65,11 @@ export class StarsComponent implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public setPreview(value: number) { @@ -96,8 +97,8 @@ export class StarsComponent implements ControlValueAccessor { this.value = null; this.stars = 0; - this.changeCallback(this.value); - this.touchedCallback(); + this.onChange(this.value); + this.onTouched(); } return false; @@ -111,8 +112,8 @@ export class StarsComponent implements ControlValueAccessor { if (this.value !== value) { this.value = this.stars = value; - this.changeCallback(this.value); - this.touchedCallback(); + this.onChange(this.value); + this.onTouched(); } return false; diff --git a/src/Squidex/app/framework/angular/tag-editor.component.ts b/src/Squidex/app/framework/angular/tag-editor.component.ts index 45efc61fd..e379b1947 100644 --- a/src/Squidex/app/framework/angular/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/tag-editor.component.ts @@ -8,40 +8,54 @@ import { Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Types } from './../utils/types'; + const KEY_ENTER = 13; -const NOOP = () => { /* NOOP */ }; export interface Converter { convert(input: string): any; - isValid(input: string): boolean; + isValidInput(input: string): boolean; + isValidValue(value: any): boolean; } export class IntConverter implements Converter { - public isValid(input: string): boolean { + public isValidInput(input: string): boolean { return !!parseInt(input, 10) || input === '0'; } + public isValidValue(value: any): boolean { + return Types.isNumber(value); + } + public convert(input: string): any { return parseInt(input, 10) || 0; } } export class FloatConverter implements Converter { - public isValid(input: string): boolean { + public isValidInput(input: string): boolean { return !!parseFloat(input) || input === '0'; } + public isValidValue(value: any): boolean { + return Types.isNumber(value); + } + public convert(input: string): any { return parseFloat(input) || 0; } } -export class NoopConverter implements Converter { - public isValid(input: string): boolean { +export class StringConverter implements Converter { + public isValidInput(input: string): boolean { return input.trim().length > 0; } + public isValidValue(value: any): boolean { + return Types.isString(value); + } + public convert(input: string): any { return input.trim(); } @@ -58,11 +72,11 @@ export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class TagEditorComponent implements ControlValueAccessor { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; @Input() - public converter: Converter = new NoopConverter(); + public converter: Converter = new StringConverter(); @Input() public useDefaultValue = true; @@ -74,10 +88,10 @@ export class TagEditorComponent implements ControlValueAccessor { public addInput = new FormControl(); - public writeValue(value: any) { - this.addInput.setValue(''); + public writeValue(value: any[]) { + this.resetForm(); - if (Array.isArray(value)) { + if (this.converter && Types.isArrayOf(value, v => this.converter.isValidValue(v))) { this.items = value; } else { this.items = []; @@ -93,11 +107,11 @@ export class TagEditorComponent implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public remove(index: number) { @@ -105,18 +119,22 @@ export class TagEditorComponent implements ControlValueAccessor { } public markTouched() { - this.touchedCallback(); + this.onTouched(); + } + + private resetForm() { + this.addInput.reset(); } public onKeyDown(event: KeyboardEvent) { if (event.keyCode === KEY_ENTER) { const value = this.addInput.value; - if (this.converter.isValid(value)) { + if (this.converter.isValidInput(value)) { const converted = this.converter.convert(value); this.updateItems([...this.items, converted]); - this.addInput.reset(); + this.resetForm(); return false; } } @@ -128,9 +146,9 @@ export class TagEditorComponent implements ControlValueAccessor { this.items = items; if (items.length === 0 && this.useDefaultValue) { - this.changeCallback(undefined); + this.onChange(undefined); } else { - this.changeCallback(this.items); + this.onChange(this.items); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/toggle.component.ts b/src/Squidex/app/framework/angular/toggle.component.ts index 8073ecf9d..c45705ce3 100644 --- a/src/Squidex/app/framework/angular/toggle.component.ts +++ b/src/Squidex/app/framework/angular/toggle.component.ts @@ -8,7 +8,7 @@ import { Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -const NOOP = () => { /* NOOP */ }; +import { Types } from './../utils/types'; export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToggleComponent), multi: true @@ -21,14 +21,14 @@ export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] }) export class ToggleComponent implements ControlValueAccessor { - private changeCallback: (value: any) => void = NOOP; - private touchedCallback: () => void = NOOP; + private onChange = (v: any) => { /* NOOP */ }; + private onTouched = () => { /* NOOP */ }; - public isChecked: boolean | undefined = undefined; + public isChecked: boolean | null = null; public isDisabled = false; - public writeValue(value: any) { - this.isChecked = value; + public writeValue(value: boolean | null | undefined) { + this.isChecked = Types.isBoolean(value) ? value! : null; } public setDisabledState(isDisabled: boolean): void { @@ -36,20 +36,21 @@ export class ToggleComponent implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.changeCallback = fn; + this.onChange = fn; } public registerOnTouched(fn: any) { - this.touchedCallback = fn; + this.onTouched = fn; } public changeState() { if (this.isDisabled) { return; } + this.isChecked = !(this.isChecked === true); - this.changeCallback(this.isChecked); - this.touchedCallback(); + this.onChange(this.isChecked); + this.onTouched(); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/validators.ts b/src/Squidex/app/framework/angular/validators.ts index 7ee6ee864..0e8176155 100644 --- a/src/Squidex/app/framework/angular/validators.ts +++ b/src/Squidex/app/framework/angular/validators.ts @@ -12,6 +12,7 @@ import { } from '@angular/forms'; import { DateTime } from './../utils/date-time'; +import { Types } from './../utils/types'; export module ValidatorsEx { export function pattern(regex: string | RegExp, message?: string): ValidatorFn { @@ -30,7 +31,7 @@ export module ValidatorsEx { regeExp = regex; } - return (control: AbstractControl): { [key: string]: any } => { + return (control: AbstractControl) => { const n: string = control.value; if (n == null || n.length === 0) { @@ -49,16 +50,16 @@ export module ValidatorsEx { }; } - export function match(otherControlName: string, message: string) { - let otherControl: AbstractControl = null; + export function match(otherControlName: string, message: string): ValidatorFn { + let otherControl: AbstractControl | null = null; - return (control: AbstractControl): { [key: string]: any } => { + return (control: AbstractControl) => { if (!control.parent) { return null; } if (otherControl === null) { - otherControl = control.parent.get(otherControlName) || undefined; + otherControl = control.parent.get(otherControlName); if (!otherControl) { throw new Error('matchValidator(): other control is not found in parent group'); @@ -77,8 +78,8 @@ export module ValidatorsEx { }; } - export function validDateTime() { - return (control: AbstractControl): { [key: string]: any } => { + export function validDateTime(): ValidatorFn { + return (control: AbstractControl) => { const v: string = control.value; if (v) { @@ -93,32 +94,32 @@ export module ValidatorsEx { }; } - export function between(minValue?: number, maxValue?: number) { + export function between(minValue?: number, maxValue?: number): ValidatorFn { if (!minValue || !maxValue) { return Validators.nullValidator; } - return (control: AbstractControl): { [key: string]: any } => { - const n: number = control.value; + return (control: AbstractControl) => { + const value: number = control.value; - if (typeof n !== 'number') { + if (!Types.isNumber(value)) { return { validnumber: false }; - } else if (minValue && n < minValue) { - return { minvalue: { minValue, actualValue: n } }; - } else if (maxValue && n > maxValue) { - return { maxvalue: { maxValue, actualValue: n } }; + } else if (minValue && value < minValue) { + return { minvalue: { minValue, actualValue: value } }; + } else if (maxValue && value > maxValue) { + return { maxvalue: { maxValue, actualValue: value } }; } return null; }; } - export function validValues(values: T[]) { + export function validValues(values: T[]): ValidatorFn { if (!values) { return Validators.nullValidator; } - return (control: AbstractControl): { [key: string]: any } => { + return (control: AbstractControl) => { const n: T = control.value; if (values.indexOf(n) < 0) { @@ -129,8 +130,8 @@ export module ValidatorsEx { }; } - export function noop() { - return (control: AbstractControl): { [key: string]: any } => { + export function noop(): ValidatorFn { + return (control: AbstractControl) => { return null; }; } diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index c17562594..e150b3563 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -71,4 +71,5 @@ export * from './utils/math-helper'; export * from './utils/modal-view'; export * from './utils/pager'; export * from './utils/string-helper'; +export * from './utils/types'; export * from './utils/version'; \ No newline at end of file diff --git a/src/Squidex/app/framework/services/local-cache.service.ts b/src/Squidex/app/framework/services/local-cache.service.ts index 1512fe0e8..ba81cc4f5 100644 --- a/src/Squidex/app/framework/services/local-cache.service.ts +++ b/src/Squidex/app/framework/services/local-cache.service.ts @@ -31,7 +31,7 @@ export class LocalCacheService { } } - public get(key: string, now?: number): T { + public get(key: string, now?: number): T | undefined { const entry = this.entries[key]; if (entry) { diff --git a/src/Squidex/app/framework/utils/types.spec.ts b/src/Squidex/app/framework/utils/types.spec.ts new file mode 100644 index 000000000..6b471032a --- /dev/null +++ b/src/Squidex/app/framework/utils/types.spec.ts @@ -0,0 +1,92 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Types } from './../'; + +describe('Types', () => { + it('should make string check', () => { + expect(Types.isString('')).toBeTruthy(); + expect(Types.isString('string')).toBeTruthy(); + + expect(Types.isString(false)).toBeFalsy(); + }); + + it('should make number check', () => { + expect(Types.isNumber(0)).toBeTruthy(); + expect(Types.isNumber(1)).toBeTruthy(); + + expect(Types.isNumber(NaN)).toBeFalsy(); + expect(Types.isNumber(Infinity)).toBeFalsy(); + expect(Types.isNumber(false)).toBeFalsy(); + }); + + it('should make boolean check', () => { + expect(Types.isBoolean(true)).toBeTruthy(); + expect(Types.isBoolean(false)).toBeTruthy(); + + expect(Types.isBoolean(0)).toBeFalsy(); + expect(Types.isBoolean(1)).toBeFalsy(); + }); + + it('should make number array check', () => { + expect(Types.isArrayOfNumber([])).toBeTruthy(); + expect(Types.isArrayOfNumber([0, 1])).toBeTruthy(); + + expect(Types.isArrayOfNumber(['0', 1])).toBeFalsy(); + }); + + it('should make string array check', () => { + expect(Types.isArrayOfString([])).toBeTruthy(); + expect(Types.isArrayOfString(['0', '1'])).toBeTruthy(); + + expect(Types.isArrayOfString(['0', 1])).toBeFalsy(); + }); + + it('should make array check', () => { + expect(Types.isArray([])).toBeTruthy(); + expect(Types.isArray([0])).toBeTruthy(); + + expect(Types.isArray({})).toBeFalsy(); + }); + + it('should make object check', () => { + expect(Types.isObject({})).toBeTruthy(); + expect(Types.isObject({ v: 1 })).toBeTruthy(); + + expect(Types.isObject([])).toBeFalsy(); + }); + + it('should make RegExp check', () => { + expect(Types.isRegExp(/[.*]/)).toBeTruthy(); + + expect(Types.isRegExp('/[.*]/')).toBeFalsy(); + }); + + it('should make Date check', () => { + expect(Types.isDate(new Date())).toBeTruthy(); + + expect(Types.isDate(new Date().getDate())).toBeFalsy(); + }); + + it('should make undefined check', () => { + expect(Types.isUndefined(undefined)).toBeTruthy(); + + expect(Types.isUndefined(null)).toBeFalsy(); + }); + + it('should make null check', () => { + expect(Types.isNull(null)).toBeTruthy(); + + expect(Types.isNull(undefined)).toBeFalsy(); + }); + + it('should make function check', () => { + expect(Types.isFunction(() => { /* NOOP */ })).toBeTruthy(); + + expect(Types.isFunction([])).toBeFalsy(); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/types.ts b/src/Squidex/app/framework/utils/types.ts new file mode 100644 index 000000000..d379b159f --- /dev/null +++ b/src/Squidex/app/framework/utils/types.ts @@ -0,0 +1,70 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export module Types { + export function isString(value: any): boolean { + return typeof value === 'string' || value instanceof String; + } + + export function isNumber(value: any): boolean { + return typeof value === 'number' && isFinite(value); + } + + export function isArray(value: any): boolean { + return Array.isArray(value); + } + + export function isFunction(value: any): boolean { + return typeof value === 'function'; + } + + export function isObject(value: any): boolean { + return value && typeof value === 'object' && value.constructor === Object; + } + + export function isBoolean(value: any): boolean { + return typeof value === 'boolean'; + }; + + export function isNull(value: any): boolean { + return value === null; + } + + export function isUndefined(value: any): boolean { + return typeof value === 'undefined'; + } + + export function isRegExp(value: any): boolean { + return value && typeof value === 'object' && value.constructor === RegExp; + } + + export function isDate(value: any): boolean { + return value instanceof Date; + } + + export function isArrayOfNumber(value: any): boolean { + return isArrayOf(value, v => isNumber(v)); + } + + export function isArrayOfString(value: any): boolean { + return isArrayOf(value, v => isString(v)); + } + + export function isArrayOf(value: any, validator: (v: any) => boolean): boolean { + if (!Array.isArray(value)) { + return false; + } + + for (let v of value) { + if (!validator(v)) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index e901e9e46..4c37dfa6d 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -83,7 +83,7 @@ export class AssetComponent extends AppComponentBase implements OnInit { if (initFile) { this.appNameOnce() - .switchMap(app => this.assetsService.uploadFile(app, initFile, this.authService.user.token)) + .switchMap(app => this.assetsService.uploadFile(app, initFile, this.authService.user!.token)) .subscribe(dto => { if (dto instanceof AssetDto) { this.emitLoaded(dto); @@ -104,7 +104,7 @@ export class AssetComponent extends AppComponentBase implements OnInit { .switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.assetVersion)) .subscribe(dto => { if (dto instanceof AssetReplacedDto) { - this.updateAsset(this.asset.update(dto, this.authService.user.token), true); + this.updateAsset(this.asset.update(dto, this.authService.user!.token), true); } else { this.setProgress(dto); } @@ -126,7 +126,7 @@ export class AssetComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.assetVersion)) .subscribe(() => { - this.updateAsset(this.asset.rename(requestDto.fileName, this.authService.user.token), true); + this.updateAsset(this.asset.rename(requestDto.fileName, this.authService.user!.token), true); this.resetRenameForm(); }, error => { this.notifyError(error); diff --git a/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts b/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts index 92128d956..afd763445 100644 --- a/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts @@ -14,14 +14,14 @@ import { allParams } from 'framework'; import { AppLanguageDto, AppLanguagesService } from './../services/app-languages.service'; @Injectable() -export class ResolveAppLanguagesGuard implements Resolve { +export class ResolveAppLanguagesGuard implements Resolve { constructor( private readonly appLanguagesService: AppLanguagesService, private readonly router: Router ) { } - public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const params = allParams(route); const appName = params['appName']; diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.ts b/src/Squidex/app/shared/guards/resolve-content.guard.ts index d2e9efdfd..83a0dd4e3 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-content.guard.ts @@ -14,14 +14,14 @@ import { allParams } from 'framework'; import { ContentDto, ContentsService } from './../services/contents.service'; @Injectable() -export class ResolveContentGuard implements Resolve { +export class ResolveContentGuard implements Resolve { constructor( private readonly contentsService: ContentsService, private readonly router: Router ) { } - public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const params = allParams(route); const appName = params['appName']; diff --git a/src/Squidex/app/shared/guards/resolve-user.guard.ts b/src/Squidex/app/shared/guards/resolve-user.guard.ts index a65482ff2..82871eefd 100644 --- a/src/Squidex/app/shared/guards/resolve-user.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-user.guard.ts @@ -14,14 +14,14 @@ import { allParams } from 'framework'; import { UserDto, UserManagementService } from './../services/users.service'; @Injectable() -export class ResolveUserGuard implements Resolve { +export class ResolveUserGuard implements Resolve { constructor( private readonly userManagementService: UserManagementService, private readonly router: Router ) { } - public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const params = allParams(route); const userId = params['userId']; diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts b/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts index 99515513d..d4b22c90c 100644 --- a/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts +++ b/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts @@ -18,7 +18,7 @@ import { } from './../'; describe('AuthInterceptor', () => { - let authService: IMock = null; + let authService: IMock; beforeEach(() => { authService = Mock.ofType(AuthService); diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.ts b/src/Squidex/app/shared/interceptors/auth.interceptor.ts index 6f7efeb38..860f142fb 100644 --- a/src/Squidex/app/shared/interceptors/auth.interceptor.ts +++ b/src/Squidex/app/shared/interceptors/auth.interceptor.ts @@ -32,7 +32,7 @@ export class AuthInterceptor implements HttpInterceptor { } } - private makeRequest(req: HttpRequest, next: HttpHandler, user: Profile, renew = false): Observable> { + private makeRequest(req: HttpRequest, next: HttpHandler, user: Profile | null, renew = false): Observable> { const token = user ? user.authToken : ''; const authReq = req.clone({ diff --git a/src/Squidex/app/shared/services/app-clients.service.ts b/src/Squidex/app/shared/services/app-clients.service.ts index 7e6d797a4..478581c55 100644 --- a/src/Squidex/app/shared/services/app-clients.service.ts +++ b/src/Squidex/app/shared/services/app-clients.service.ts @@ -84,7 +84,7 @@ export class AppClientsService { .pretifyError('Failed to load clients. Please reload.'); } - public postClient(appName: string, dto: CreateAppClientDto, version?: Version): Observable { + public postClient(appName: string, dto: CreateAppClientDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); return HTTP.postVersioned(this.http, url, dto, version) @@ -98,14 +98,14 @@ export class AppClientsService { .pretifyError('Failed to add client. Please reload.'); } - public updateClient(appName: string, id: string, dto: UpdateAppClientDto, version?: Version): Observable { + public updateClient(appName: string, id: string, dto: UpdateAppClientDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); return HTTP.putVersioned(this.http, url, dto, version) .pretifyError('Failed to revoke client. Please reload.'); } - public deleteClient(appName: string, id: string, version?: Version): Observable { + public deleteClient(appName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); return HTTP.deleteVersioned(this.http, url, version) diff --git a/src/Squidex/app/shared/services/app-contributors.service.ts b/src/Squidex/app/shared/services/app-contributors.service.ts index 46b2fbcdb..7ccc9f0d6 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.ts @@ -63,14 +63,14 @@ export class AppContributorsService { .pretifyError('Failed to load contributors. Please reload.'); } - public postContributor(appName: string, dto: AppContributorDto, version?: Version): Observable { + public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); return HTTP.postVersioned(this.http, url, dto, version) .pretifyError('Failed to add contributors. Please reload.'); } - public deleteContributor(appName: string, contributorId: string, version?: Version): Observable { + public deleteContributor(appName: string, contributorId: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`); return HTTP.deleteVersioned(this.http, url, version) diff --git a/src/Squidex/app/shared/services/app-languages.service.ts b/src/Squidex/app/shared/services/app-languages.service.ts index 811bb5ef1..3197562ff 100644 --- a/src/Squidex/app/shared/services/app-languages.service.ts +++ b/src/Squidex/app/shared/services/app-languages.service.ts @@ -75,7 +75,7 @@ export class AppLanguagesService { .pretifyError('Failed to load languages. Please reload.'); } - public postLanguages(appName: string, dto: AddAppLanguageDto, version?: Version): Observable { + public postLanguages(appName: string, dto: AddAppLanguageDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); return HTTP.postVersioned(this.http, url, dto, version) @@ -90,14 +90,14 @@ export class AppLanguagesService { .pretifyError('Failed to add language. Please reload.'); } - public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version?: Version): Observable { + public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); return HTTP.putVersioned(this.http, url, dto, version) .pretifyError('Failed to change language. Please reload.'); } - public deleteLanguage(appName: string, languageCode: string, version?: Version): Observable { + public deleteLanguage(appName: string, languageCode: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); return HTTP.deleteVersioned(this.http, url, version) diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index 014643cb8..8a23706b1 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/src/Squidex/app/shared/services/assets.service.spec.ts @@ -24,7 +24,7 @@ describe('AssetDto', () => { it('should update name property and user info when renaming', () => { const now = DateTime.now(); - const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, null); + const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, new Version('1')); const asset_2 = asset_1.rename('new-name.png', 'me', now); expect(asset_2.fileName).toEqual('new-name.png'); @@ -35,9 +35,9 @@ describe('AssetDto', () => { it('should update file properties when uploading', () => { const now = DateTime.now(); - const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2, null); + const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2, new Version('1')); - const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, null); + const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, new Version('1')); const asset_2 = asset_1.update(update, 'me', now); expect(asset_2.fileSize).toEqual(2); @@ -48,6 +48,7 @@ describe('AssetDto', () => { expect(asset_2.pixelHeight).toEqual(2); expect(asset_2.lastModified).toEqual(now); expect(asset_2.lastModifiedBy).toEqual('me'); + expect(asset_2.version).toEqual(update); }); }); @@ -160,7 +161,7 @@ describe('AssetsService', () => { let asset: AssetDto | null = null; - assetsService.getAsset('my-app', '123').subscribe(result => { + assetsService.getAsset('my-app', '123', version).subscribe(result => { asset = result; }); @@ -211,14 +212,14 @@ describe('AssetsService', () => { let asset: AssetDto | null = null; - assetsService.getAsset('my-app', '123').subscribe(result => { + assetsService.getAsset('my-app', '123', version).subscribe(result => { asset = result; }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123'); expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); + expect(req.request.headers.get('If-Match')).toEqual(version.value); req.flush({}, { status: 404, statusText: '404' }); @@ -246,7 +247,7 @@ describe('AssetsService', () => { const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?mimeTypes=image/png,image/png&take=17&skip=13'); expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); + expect(req.request.headers.get('If-Match')).toEqual(version.value); req.flush({ total: 10, items: [] }); })); @@ -259,7 +260,7 @@ describe('AssetsService', () => { const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?ids=12,23&take=17&skip=13'); expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); + expect(req.request.headers.get('If-Match')).toEqual(version.value); req.flush({ total: 10, items: [] }); })); @@ -276,7 +277,7 @@ describe('AssetsService', () => { const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets'); expect(req.request.method).toEqual('POST'); - expect(req.request.headers.get('If-Match')).toBeNull(); + expect(req.request.headers.get('If-Match')).toEqual(version.value); req.flush({ id: 'id1', diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index bcba00158..6e839956f 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/src/Squidex/app/shared/services/assets.service.ts @@ -163,7 +163,7 @@ export class AssetsService { return this.http.request(req) .map(event => { if (event.type === HttpEventType.UploadProgress) { - const percentDone = Math.round(100 * event.loaded / event.total); + const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0; return percentDone; } else if (event instanceof HttpResponse) { @@ -235,20 +235,21 @@ export class AssetsService { .pretifyError('Failed to load assets. Please reload.'); } - public replaceFile(appName: string, id: string, file: File, version?: Version): Observable { + public replaceFile(appName: string, id: string, file: File, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}/content`); - const req = new HttpRequest('PUT', url, getFormData(file), { - headers: new HttpHeaders({ - 'If-Match': version.value - }), - reportProgress: true - }); + const headers = new HttpHeaders(); + + if (version) { + headers.set('If-Match', version.value); + } + + const req = new HttpRequest('PUT', url, getFormData(file), { headers, reportProgress: true }); return this.http.request(req) .map(event => { if (event.type === HttpEventType.UploadProgress) { - const percentDone = Math.round(100 * event.loaded / event.total); + const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0; return percentDone; } else if (event instanceof HttpResponse) { @@ -269,7 +270,7 @@ export class AssetsService { .pretifyError('Failed to replace asset. Please reload.'); } - public deleteAsset(appName: string, id: string, version?: Version): Observable { + public deleteAsset(appName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`); return HTTP.deleteVersioned(this.http, url, version) @@ -279,7 +280,7 @@ export class AssetsService { .pretifyError('Failed to delete asset. Please reload.'); } - public putAsset(appName: string, id: string, dto: UpdateAssetDto, version?: Version): Observable { + public putAsset(appName: string, id: string, dto: UpdateAssetDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`); return HTTP.putVersioned(this.http, url, dto, version) diff --git a/src/Squidex/app/shared/services/auth.service.ts b/src/Squidex/app/shared/services/auth.service.ts index 8970884ce..efdfb970a 100644 --- a/src/Squidex/app/shared/services/auth.service.ts +++ b/src/Squidex/app/shared/services/auth.service.ts @@ -39,7 +39,7 @@ export class Profile { } public get authToken(): string { - return `${this.user.token_type} ${this.user.access_token}`; + return `${this.user!.token_type} ${this.user.access_token}`; } public get token(): string { @@ -56,7 +56,7 @@ export class Profile { export class AuthService { private readonly userManager: UserManager; private readonly user$ = new ReplaySubject(1); - private currentUser: Profile = null; + private currentUser: Profile | null = null; public get user(): Profile | null { return this.currentUser; @@ -150,7 +150,7 @@ export class AuthService { public loginSilent(): Observable { const observable: Observable = - Observable.create((observer: Observer) => { + Observable.create((observer: Observer) => { this.userManager.signinSilent() .then(x => { observer.next(this.createProfile(x)); @@ -169,13 +169,17 @@ export class AuthService { .concat(Observable.throw(new Error('Retry limit exceeded.')))); } - private createProfile(user: User) { - return user ? new Profile(user) : null; + private createProfile(user: User): Profile { + return new Profile(user); } - private checkState(promise: Promise) { + private checkState(promise: Promise) { promise.then(user => { - this.user$.next(this.createProfile(user)); + if (user) { + this.user$.next(this.createProfile(user)); + } else { + this.user$.next(null); + } return true; }, err => { diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 849c9f77e..54a0a92d1 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -22,7 +22,7 @@ describe('ContentDto', () => { it('should update data property and user info when updating', () => { const now = DateTime.now(); - const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); + const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, new Version('1')); const content_2 = content_1.update({ data: 2 }, 'me', now); expect(content_2.data).toEqual({ data: 2 }); @@ -33,7 +33,7 @@ describe('ContentDto', () => { it('should update isPublished property and user info when publishing', () => { const now = DateTime.now(); - const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); + const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, new Version('1')); const content_2 = content_1.publish('me', now); expect(content_2.isPublished).toBeTruthy(); @@ -44,7 +44,7 @@ describe('ContentDto', () => { it('should update isPublished property and user info when unpublishing', () => { const now = DateTime.now(); - const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); + const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, new Version('1')); const content_2 = content_1.unpublish('me', now); expect(content_2.isPublished).toBeFalsy(); @@ -55,7 +55,7 @@ describe('ContentDto', () => { it('should update data property when setting data', () => { const newData = {}; - const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null); + const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, new Version('1')); const content_2 = content_1.setData(newData); expect(content_2.data).toBe(newData); @@ -140,7 +140,7 @@ describe('ContentsService', () => { it('should append query to get request as search', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - let contents: ContentsDto | null; + let contents: ContentsDto | null = null; contentsService.getContents('my-app', 'my-schema', 17, 13, 'my-query').subscribe(result => { contents = result; @@ -157,9 +157,9 @@ describe('ContentsService', () => { it('should append ids to get request with ids', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - let contents: ContentsDto | null; + let contents: ContentsDto | null = null; - contentsService.getContents('my-app', 'my-schema', 17, 13, null, ['id1', 'id2']).subscribe(result => { + contentsService.getContents('my-app', 'my-schema', 17, 13, undefined, ['id1', 'id2']).subscribe(result => { contents = result; }); @@ -174,7 +174,7 @@ describe('ContentsService', () => { it('should append query to get request as plain query string', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - let contents: ContentsDto | null; + let contents: ContentsDto | null = null; contentsService.getContents('my-app', 'my-schema', 17, 13, '$filter=my-filter').subscribe(result => { contents = result; diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 0e70d45a9..ea2e47dab 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -176,7 +176,7 @@ export class ContentsService { .pretifyError('Failed to load content. Please reload.'); } - public postContent(appName: string, schemaName: string, dto: any, publish: boolean, version?: Version): Observable { + public postContent(appName: string, schemaName: string, dto: any, publish: boolean, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?publish=${publish}`); return HTTP.postVersioned(this.http, url, dto, version) @@ -197,7 +197,7 @@ export class ContentsService { .pretifyError('Failed to create content. Please reload.'); } - public putContent(appName: string, schemaName: string, id: string, dto: any, version?: Version): Observable { + public putContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); return HTTP.putVersioned(this.http, url, dto, version) @@ -207,8 +207,8 @@ export class ContentsService { .pretifyError('Failed to update content. Please reload.'); } - public deleteContent(appName: string, schemaName: string, id: string, version?: Version): Observable { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); + public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable { + const url = this.apiUrl.buildUrl(`/api/coentent/${appName}/${schemaName}/${id}`); return HTTP.deleteVersioned(this.http, url, version) .do(() => { @@ -217,21 +217,21 @@ export class ContentsService { .pretifyError('Failed to delete content. Please reload.'); } - public getVersionData(appName: string, schemaName: string, id: string, version?: Version): Observable { + public getVersionData(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`); return HTTP.getVersioned(this.http, url, version) .pretifyError('Failed to load data. Please reload.'); } - public publishContent(appName: string, schemaName: string, id: string, version?: Version): Observable { + public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`); return HTTP.putVersioned(this.http, url, {}, version) .pretifyError('Failed to publish content. Please reload.'); } - public unpublishContent(appName: string, schemaName: string, id: string, version?: Version): Observable { + public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`); return HTTP.putVersioned(this.http, url, {}, version) diff --git a/src/Squidex/app/shared/services/plans.service.ts b/src/Squidex/app/shared/services/plans.service.ts index 0a76a0ac0..afb40fded 100644 --- a/src/Squidex/app/shared/services/plans.service.ts +++ b/src/Squidex/app/shared/services/plans.service.ts @@ -85,7 +85,7 @@ export class PlansService { .pretifyError('Failed to load plans. Please reload.'); } - public putPlan(appName: string, dto: ChangePlanDto, version?: Version): Observable { + public putPlan(appName: string, dto: ChangePlanDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`); return HTTP.putVersioned(this.http, url, dto, version) diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index d2ec60e7d..7018ff6d6 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -27,12 +27,12 @@ import { } from './../'; describe('SchemaDto', () => { - const properties = new SchemaPropertiesDto('Name', null); + const properties = new SchemaPropertiesDto('Name'); it('should update isPublished property and user info when publishing', () => { const now = DateTime.now(); - const schema_1 = new SchemaDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null); + const schema_1 = new SchemaDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1')); const schema_2 = schema_1.publish('me', now); expect(schema_2.isPublished).toBeTruthy(); @@ -43,7 +43,7 @@ describe('SchemaDto', () => { it('should update isPublished property and user info when unpublishing', () => { const now = DateTime.now(); - const schema_1 = new SchemaDto('1', 'name', properties, true, 'other', 'other', DateTime.now(), DateTime.now(), null); + const schema_1 = new SchemaDto('1', 'name', properties, true, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1')); const schema_2 = schema_1.unpublish('me', now); expect(schema_2.isPublished).toBeFalsy(); @@ -52,11 +52,11 @@ describe('SchemaDto', () => { }); it('should update properties property and user info when updating', () => { - const newProperties = new SchemaPropertiesDto('New Name', null); + const newProperties = new SchemaPropertiesDto('New Name'); const now = DateTime.now(); - const schema_1 = new SchemaDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null); + const schema_1 = new SchemaDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1')); const schema_2 = schema_1.update(newProperties, 'me', now); expect(schema_2.properties).toEqual(newProperties); @@ -76,7 +76,7 @@ describe('SchemaDto', () => { const now = DateTime.now(); - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, []); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), []); const schema_2 = schema_1.configureScripts(newScripts, 'me', now); expect(schema_2.scriptQuery).toEqual(''); @@ -91,12 +91,12 @@ describe('SchemaDto', () => { }); describe('SchemaDetailsDto', () => { - const properties = new SchemaPropertiesDto('Name', null); + const properties = new SchemaPropertiesDto('Name'); it('should update isPublished property and user info when publishing', () => { const now = DateTime.now(); - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, []); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), []); const schema_2 = schema_1.publish('me', now); expect(schema_2.isPublished).toBeTruthy(); @@ -107,7 +107,7 @@ describe('SchemaDetailsDto', () => { it('should update isPublished property and user info when unpublishing', () => { const now = DateTime.now(); - const schema_1 = new SchemaDetailsDto('1', 'name', properties, true, 'other', 'other', DateTime.now(), DateTime.now(), null, []); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, true, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), []); const schema_2 = schema_1.unpublish('me', now); expect(schema_2.isPublished).toBeFalsy(); @@ -116,11 +116,11 @@ describe('SchemaDetailsDto', () => { }); it('should update properties property and user info when updating', () => { - const newProperties = new SchemaPropertiesDto('New Name', null); + const newProperties = new SchemaPropertiesDto('New Name'); const now = DateTime.now(); - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, []); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), []); const schema_2 = schema_1.update(newProperties, 'me', now); expect(schema_2.properties).toEqual(newProperties); @@ -134,7 +134,7 @@ describe('SchemaDetailsDto', () => { const now = DateTime.now(); - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, [field1]); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), [field1]); const schema_2 = schema_1.addField(field2, 'me', now); expect(schema_2.fields).toEqual([field1, field2]); @@ -148,7 +148,7 @@ describe('SchemaDetailsDto', () => { const now = DateTime.now(); - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, [field1, field2]); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), [field1, field2]); const schema_2 = schema_1.removeField(field1, 'me', now); expect(schema_2.fields).toEqual([field2]); @@ -162,7 +162,7 @@ describe('SchemaDetailsDto', () => { const now = DateTime.now(); - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, [field1, field2]); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), [field1, field2]); const schema_2 = schema_1.replaceFields([field2, field1], 'me', now); expect(schema_2.fields).toEqual([field2, field1]); @@ -177,7 +177,7 @@ describe('SchemaDetailsDto', () => { const now = DateTime.now(); - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, [field1_0, field2_1]); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), [field1_0, field2_1]); const schema_2 = schema_1.updateField(field2_2, 'me', now); expect(schema_2.fields).toEqual([field1_0, field2_2]); @@ -461,7 +461,7 @@ describe('SchemasService', () => { req.flush({ id: '1' }); expect(schema).toEqual( - new SchemaDetailsDto('1', dto.name, new SchemaPropertiesDto(null, null), false, user, user, now, now, version, [])); + new SchemaDetailsDto('1', dto.name, new SchemaPropertiesDto(), false, user, user, now, now, version, [])); })); it('should make post request to add field', diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 4d15958a1..42ca23c75 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -643,8 +643,8 @@ export class JsonFieldPropertiesDto extends FieldPropertiesDto { export class SchemaPropertiesDto { constructor( - public readonly label: string, - public readonly hints: string + public readonly label?: string, + public readonly hints?: string ) { } } @@ -781,7 +781,7 @@ export class SchemasService { .pretifyError('Failed to load schema. Please reload.'); } - public postSchema(appName: string, dto: CreateSchemaDto, user: string, now?: DateTime, version?: Version): Observable { + public postSchema(appName: string, dto: CreateSchemaDto, user: string, now: DateTime, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); return HTTP.postVersioned(this.http, url, dto, version) @@ -791,13 +791,13 @@ export class SchemasService { return new SchemaDetailsDto( response.id, dto.name, - dto.properties || new SchemaPropertiesDto(null, null), + dto.properties || new SchemaPropertiesDto(), false, user, user, now, now, - version, + version || new Version('0'), dto.fields || [], response.scriptQuery, response.scriptCreate, @@ -813,7 +813,7 @@ export class SchemasService { .pretifyError('Failed to create schema. Please reload.'); } - public postField(appName: string, schemaName: string, dto: AddFieldDto, version?: Version): Observable { + public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`); return HTTP.postVersioned(this.http, url, dto, version) @@ -830,7 +830,7 @@ export class SchemasService { .pretifyError('Failed to add field. Please reload.'); } - public deleteSchema(appName: string, schemaName: string, version?: Version): Observable { + public deleteSchema(appName: string, schemaName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); return HTTP.deleteVersioned(this.http, url, version) @@ -840,84 +840,84 @@ export class SchemasService { .pretifyError('Failed to delete schema. Please reload.'); } - public putSchemaScripts(appName: string, schemaName: string, dto: UpdateSchemaScriptsDto, version?: Version): Observable { + public putSchemaScripts(appName: string, schemaName: string, dto: UpdateSchemaScriptsDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/scripts`); return HTTP.putVersioned(this.http, url, dto, version) .pretifyError('Failed to update schema scripts. Please reload.'); } - public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version?: Version): Observable { + public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); return HTTP.putVersioned(this.http, url, dto, version) .pretifyError('Failed to update schema. Please reload.'); } - public putFieldOrdering(appName: string, schemaName: string, dto: number[], version?: Version): Observable { + public putFieldOrdering(appName: string, schemaName: string, dto: number[], version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/ordering`); return HTTP.putVersioned(this.http, url, { fieldIds: dto }, version) .pretifyError('Failed to reorder fields. Please reload.'); } - public publishSchema(appName: string, schemaName: string, version?: Version): Observable { + public publishSchema(appName: string, schemaName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`); return HTTP.putVersioned(this.http, url, {}, version) .pretifyError('Failed to publish schema. Please reload.'); } - public unpublishSchema(appName: string, schemaName: string, version?: Version): Observable { + public unpublishSchema(appName: string, schemaName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`); return HTTP.putVersioned(this.http, url, {}, version) .pretifyError('Failed to unpublish schema. Please reload.'); } - public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version?: Version): Observable { + public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); return HTTP.putVersioned(this.http, url, dto, version) .pretifyError('Failed to update field. Please reload.'); } - public enableField(appName: string, schemaName: string, fieldId: number, version?: Version): Observable { + public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`); return HTTP.putVersioned(this.http, url, {}, version) .pretifyError('Failed to enable field. Please reload.'); } - public disableField(appName: string, schemaName: string, fieldId: number, version?: Version): Observable { + public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`); return HTTP.putVersioned(this.http, url, {}, version) .pretifyError('Failed to disable field. Please reload.'); } - public lockField(appName: string, schemaName: string, fieldId: number, version?: Version): Observable { + public lockField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/lock`); return HTTP.putVersioned(this.http, url, {}, version) .pretifyError('Failed to lock field. Please reload.'); } - public showField(appName: string, schemaName: string, fieldId: number, version?: Version): Observable { + public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`); return HTTP.putVersioned(this.http, url, {}, version) .pretifyError('Failed to show field. Please reload.'); } - public hideField(appName: string, schemaName: string, fieldId: number, version?: Version): Observable { + public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`); return HTTP.putVersioned(this.http, url, {}, version) .pretifyError('Failed to hide field. Please reload.'); } - public deleteField(appName: string, schemaName: string, fieldId: number, version?: Version): Observable { + public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); return HTTP.deleteVersioned(this.http, url, version) diff --git a/src/Squidex/app/shared/services/ui.service.spec.ts b/src/Squidex/app/shared/services/ui.service.spec.ts index a1cb05b2c..129dc3598 100644 --- a/src/Squidex/app/shared/services/ui.service.spec.ts +++ b/src/Squidex/app/shared/services/ui.service.spec.ts @@ -74,6 +74,7 @@ describe('UIService', () => { req.error(new ErrorEvent('500')); - expect(settings.regexSuggestions).toEqual([]); + expect(settings).toBeDefined(); + expect(settings!.regexSuggestions).toEqual([]); })); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/webhooks.service.spec.ts b/src/Squidex/app/shared/services/webhooks.service.spec.ts index 22eedd991..52a22513f 100644 --- a/src/Squidex/app/shared/services/webhooks.service.spec.ts +++ b/src/Squidex/app/shared/services/webhooks.service.spec.ts @@ -67,14 +67,14 @@ describe('WebhooksService', () => { let webhooks: WebhookDto[] | null = null; - webhooksService.getWebhooks('my-app', version).subscribe(result => { + webhooksService.getWebhooks('my-app').subscribe(result => { webhooks = result; }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBe(version.value); + expect(req.request.headers.get('If-Match')).toBeNull(); req.flush([ { diff --git a/src/Squidex/app/shared/services/webhooks.service.ts b/src/Squidex/app/shared/services/webhooks.service.ts index ad31209bc..8e9b73903 100644 --- a/src/Squidex/app/shared/services/webhooks.service.ts +++ b/src/Squidex/app/shared/services/webhooks.service.ts @@ -111,10 +111,10 @@ export class WebhooksService { ) { } - public getWebhooks(appName: string, version?: Version): Observable { + public getWebhooks(appName: string): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); - return HTTP.getVersioned(this.http, url, version) + return HTTP.getVersioned(this.http, url) .map(response => { const items: any[] = response; @@ -147,7 +147,7 @@ export class WebhooksService { .pretifyError('Failed to load webhooks. Please reload.'); } - public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now?: DateTime, version?: Version): Observable { + public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now: DateTime, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); return HTTP.postVersioned(this.http, url, dto, version) @@ -167,14 +167,14 @@ export class WebhooksService { .pretifyError('Failed to create webhook. Please reload.'); } - public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version?: Version): Observable { + public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); return HTTP.putVersioned(this.http, url, dto, version) .pretifyError('Failed to update webhook. Please reload.'); } - public deleteWebhook(appName: string, id: string, version?: Version): Observable { + public deleteWebhook(appName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); return HTTP.deleteVersioned(this.http, url, version) diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.ts b/src/Squidex/app/shell/pages/internal/profile-menu.component.ts index 1f9ebbdc5..e5cb98ef1 100644 --- a/src/Squidex/app/shell/pages/internal/profile-menu.component.ts +++ b/src/Squidex/app/shell/pages/internal/profile-menu.component.ts @@ -49,10 +49,10 @@ export class ProfileMenuComponent implements OnDestroy, OnInit { this.authenticationSubscription = this.authService.userChanges.filter(user => !!user) .subscribe(user => { - this.profileId = user.id; - this.profileDisplayName = user.displayName; + this.profileId = user!.id; + this.profileDisplayName = user!.displayName; - this.isAdmin = user.isAdmin; + this.isAdmin = user!.isAdmin; }); } diff --git a/src/Squidex/tsconfig.json b/src/Squidex/tsconfig.json index 54692ff9c..b740cdfc7 100644 --- a/src/Squidex/tsconfig.json +++ b/src/Squidex/tsconfig.json @@ -9,7 +9,7 @@ "noUnusedParameters": false, "removeComments": false, "sourceMap": true, - "strictNullChecks": false, + "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, "target": "es5", "paths": { From 6f965686c7c0bebda4ed7c1f3eee10423a0103f2 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 9 Sep 2017 22:51:45 +0200 Subject: [PATCH 05/10] Tests simplified --- .../content/shared/assets-editor.component.ts | 12 +- .../shared/references-editor.component.ts | 12 +- .../angular/autocomplete.component.ts | 12 +- .../angular/date-time-editor.component.ts | 22 ++-- .../framework/angular/dropdown.component.ts | 12 +- .../angular/geolocation-editor.component.ts | 12 +- .../angular/indeterminate-value.directive.ts | 12 +- .../angular/jscript-editor.component.ts | 12 +- .../angular/json-editor.component.ts | 12 +- .../angular/lowercase-input.directive.ts | 18 +-- .../angular/markdown-editor.component.ts | 12 +- .../angular/rich-editor.component.ts | 12 +- .../app/framework/angular/slider.component.ts | 12 +- .../app/framework/angular/stars.component.ts | 18 +-- .../framework/angular/tag-editor.component.ts | 14 +- .../app/framework/angular/toggle.component.ts | 14 +- .../shared/services/assets.service.spec.ts | 28 ++-- .../shared/services/contents.service.spec.ts | 38 +++--- .../shared/services/schemas.service.spec.ts | 120 ++++++++---------- .../app/shared/services/schemas.service.ts | 2 +- .../shared/services/webhooks.service.spec.ts | 12 +- src/Squidex/tsconfig.json | 2 +- 22 files changed, 209 insertions(+), 211 deletions(-) diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index 31e441741..a8176fc71 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/src/Squidex/app/features/content/shared/assets-editor.component.ts @@ -35,8 +35,8 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = { }) export class AssetsEditorComponent extends AppComponentBase implements ControlValueAccessor, OnDestroy, OnInit { private assetUpdatedSubscription: Subscription; - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; public newAssets = ImmutableArray.empty(); public oldAssets = ImmutableArray.empty(); @@ -83,11 +83,11 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public addFiles(files: FileList) { @@ -142,7 +142,7 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa ids = null; } - this.onTouched(); - this.onChange(ids); + this.callTouched(); + this.callChange(ids); } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/src/Squidex/app/features/content/shared/references-editor.component.ts index ded63f413..924250d0a 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.ts +++ b/src/Squidex/app/features/content/shared/references-editor.component.ts @@ -34,8 +34,8 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class ReferencesEditorComponent extends AppComponentBase implements ControlValueAccessor, OnInit { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; @Input() public schemaId: string; @@ -91,11 +91,11 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public canDrop() { @@ -137,8 +137,8 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr ids = null; } - this.onTouched(); - this.onChange(ids); + this.callTouched(); + this.callChange(ids); } private loadFields() { diff --git a/src/Squidex/app/framework/angular/autocomplete.component.ts b/src/Squidex/app/framework/angular/autocomplete.component.ts index e492a7f6b..5f37f12f0 100644 --- a/src/Squidex/app/framework/angular/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/autocomplete.component.ts @@ -30,8 +30,8 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { }) export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit { private subscription: Subscription; - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; @Input() public source: AutocompleteSource; @@ -78,11 +78,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public ngOnDestroy() { @@ -134,7 +134,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O public blur() { this.reset(); - this.onTouched(); + this.callTouched(); } public selectItem(selection: any | null = null) { @@ -153,7 +153,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O } else { this.queryInput.setValue(selection.toString(), { emitEvent: false }); } - this.onChange(selection); + this.callChange(selection); } finally { this.reset(); } diff --git a/src/Squidex/app/framework/angular/date-time-editor.component.ts b/src/Squidex/app/framework/angular/date-time-editor.component.ts index 309b3bed4..c766be4c6 100644 --- a/src/Squidex/app/framework/angular/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/date-time-editor.component.ts @@ -10,6 +10,8 @@ import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/f import { Subscription } from 'rxjs'; import * as moment from 'moment'; +import { Types } from './../utils/types'; + let Pikaday = require('pikaday/pikaday'); export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { @@ -29,8 +31,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, private timeValue: any | null = null; private dateValue: any | null = null; private suppressEvents = false; - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; @Input() public mode: string; @@ -84,8 +86,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, }); } - public writeValue(value: any) { - if (!value || value.length === 0) { + public writeValue(value: string) { + if (!Types.isString(value) || value.length === 0) { this.timeValue = null; this.dateValue = null; } else { @@ -114,11 +116,11 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public ngAfterViewInit() { @@ -138,7 +140,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } public touched() { - this.onTouched(); + this.callTouched(); } public writeNow() { @@ -157,8 +159,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, this.dateValue = null; - this.onChange(null); - this.onTouched(); + this.callChange(null); + this.callTouched(); return false; } @@ -182,7 +184,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } } - this.onChange(result); + this.callChange(result); } private updateControls() { diff --git a/src/Squidex/app/framework/angular/dropdown.component.ts b/src/Squidex/app/framework/angular/dropdown.component.ts index f7d0fc118..858742e66 100644 --- a/src/Squidex/app/framework/angular/dropdown.component.ts +++ b/src/Squidex/app/framework/angular/dropdown.component.ts @@ -26,8 +26,8 @@ export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR] }) export class DropdownComponent implements AfterContentInit, ControlValueAccessor { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; @Input() public items: any[] = []; @@ -68,11 +68,11 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public onKeyDown(event: KeyboardEvent) { @@ -94,7 +94,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor public open() { this.dropdown.show(); - this.onTouched(); + this.callTouched(); } public selectIndexAndClose(selectedIndex: number) { @@ -131,7 +131,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor this.selectedIndex = selectedIndex; this.selectedItem = value; - this.onChange(value); + this.callChange(value); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.ts b/src/Squidex/app/framework/angular/geolocation-editor.component.ts index 3cf44cbf3..6226b8ac3 100644 --- a/src/Squidex/app/framework/angular/geolocation-editor.component.ts +++ b/src/Squidex/app/framework/angular/geolocation-editor.component.ts @@ -31,8 +31,8 @@ interface Geolocation { providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; private marker: any; private map: any; private value: Geolocation | null = null; @@ -111,11 +111,11 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public updateValueByInput() { @@ -210,8 +210,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } if (fireEvent) { - this.onChange(this.value); - this.onTouched(); + this.callChange(this.value); + this.callTouched(); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/indeterminate-value.directive.ts b/src/Squidex/app/framework/angular/indeterminate-value.directive.ts index 034bd5b79..8cec0c565 100644 --- a/src/Squidex/app/framework/angular/indeterminate-value.directive.ts +++ b/src/Squidex/app/framework/angular/indeterminate-value.directive.ts @@ -19,8 +19,8 @@ export const SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR] }) export class IndeterminateValueDirective implements ControlValueAccessor { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; constructor( private readonly renderer: Renderer, @@ -30,12 +30,12 @@ export class IndeterminateValueDirective implements ControlValueAccessor { @HostListener('change', ['$event.target.value']) public onChange(value: any) { - this.onChange(value); + this.callChange(value); } @HostListener('blur') public onTouched() { - this.onTouched(); + this.callTouched(); } public writeValue(value: boolean | number | undefined) { @@ -51,10 +51,10 @@ export class IndeterminateValueDirective implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/jscript-editor.component.ts b/src/Squidex/app/framework/angular/jscript-editor.component.ts index e2b7cc74e..d3fc0df0d 100644 --- a/src/Squidex/app/framework/angular/jscript-editor.component.ts +++ b/src/Squidex/app/framework/angular/jscript-editor.component.ts @@ -26,8 +26,8 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; private valueChanged = new Subject(); private aceEditor: any; private value: string; @@ -58,11 +58,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public ngAfterViewInit() { @@ -82,7 +82,7 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn this.aceEditor.on('blur', () => { this.changeValue(); - this.onTouched(); + this.callTouched(); }); this.aceEditor.on('change', () => { @@ -95,7 +95,7 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn const newValue = this.aceEditor.getValue(); if (this.value !== newValue) { - this.onChange(newValue); + this.callChange(newValue); } this.value = newValue; diff --git a/src/Squidex/app/framework/angular/json-editor.component.ts b/src/Squidex/app/framework/angular/json-editor.component.ts index 90beb4b27..263a1be2f 100644 --- a/src/Squidex/app/framework/angular/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/json-editor.component.ts @@ -24,8 +24,8 @@ export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; private valueChanged = new Subject(); private aceEditor: any; private value: any; @@ -63,11 +63,11 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public ngAfterViewInit() { @@ -87,7 +87,7 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit this.aceEditor.on('blur', () => { this.changeValue(); - this.onTouched(); + this.callTouched(); }); this.aceEditor.on('change', () => { @@ -112,7 +112,7 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit const newValueString = JSON.stringify(newValue); if (this.valueString !== newValueString) { - this.onChange(newValue); + this.callChange(newValue); } this.value = newValue; diff --git a/src/Squidex/app/framework/angular/lowercase-input.directive.ts b/src/Squidex/app/framework/angular/lowercase-input.directive.ts index 66d61338d..6645d1697 100644 --- a/src/Squidex/app/framework/angular/lowercase-input.directive.ts +++ b/src/Squidex/app/framework/angular/lowercase-input.directive.ts @@ -8,6 +8,8 @@ import { Directive, forwardRef, ElementRef, HostListener, Renderer } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Types } from './../utils/types'; + export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LowerCaseInputDirective), multi: true }; @@ -17,8 +19,8 @@ export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = { providers: [SQX_LOWERCASE_INPUT_VALUE_ACCESSOR] }) export class LowerCaseInputDirective implements ControlValueAccessor { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; constructor( private readonly element: ElementRef, @@ -31,16 +33,16 @@ export class LowerCaseInputDirective implements ControlValueAccessor { const normalizedValue = (value == null ? '' : value.toString()).toLowerCase(); this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); - this.onChange(normalizedValue); + this.callChange(normalizedValue); } @HostListener('blur') public onTouched() { - this.onTouched(); + this.callTouched(); } - public writeValue(value: any) { - const normalizedValue = value ? '' : value.toString().toLowerCase(); + public writeValue(value: string) { + const normalizedValue = Types.isString(value) ? value.toLowerCase() : ''; this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); } @@ -50,10 +52,10 @@ export class LowerCaseInputDirective implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/markdown-editor.component.ts b/src/Squidex/app/framework/angular/markdown-editor.component.ts index 9716ef10f..41d4cefc8 100644 --- a/src/Squidex/app/framework/angular/markdown-editor.component.ts +++ b/src/Squidex/app/framework/angular/markdown-editor.component.ts @@ -25,8 +25,8 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; private simplemde: any; private value: string; private isDisabled = false; @@ -65,11 +65,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public ngAfterViewInit() { @@ -84,12 +84,12 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI if (this.value !== value) { this.value = value; - this.onChange(value); + this.callChange(value); } }); this.simplemde.codemirror.on('blur', () => { - this.onTouched(); + this.callTouched(); }); this.simplemde.codemirror.on('refresh', () => { diff --git a/src/Squidex/app/framework/angular/rich-editor.component.ts b/src/Squidex/app/framework/angular/rich-editor.component.ts index c690ed43a..6c5806cc5 100644 --- a/src/Squidex/app/framework/angular/rich-editor.component.ts +++ b/src/Squidex/app/framework/angular/rich-editor.component.ts @@ -25,8 +25,8 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; private tinyEditor: any; private value: string; private isDisabled = false; @@ -56,11 +56,11 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public ngAfterViewInit() { @@ -78,12 +78,12 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, if (this.value !== value) { this.value = value; - self.onChange(value); + self.callChange(value); } }); self.tinyEditor.on('blur', () => { - self.onTouched(); + self.callTouched(); }); setTimeout(() => { diff --git a/src/Squidex/app/framework/angular/slider.component.ts b/src/Squidex/app/framework/angular/slider.component.ts index 2fe82b581..bab84dbdb 100644 --- a/src/Squidex/app/framework/angular/slider.component.ts +++ b/src/Squidex/app/framework/angular/slider.component.ts @@ -21,8 +21,8 @@ export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR] }) export class SliderComponent implements ControlValueAccessor { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; private mouseMoveSubscription: Function | null = null; private mouseUpSubscription: Function | null = null; private centerStartOffset = 0; @@ -61,11 +61,11 @@ export class SliderComponent implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public onBarMouseClick(event: MouseEvent): boolean { @@ -157,14 +157,14 @@ export class SliderComponent implements ControlValueAccessor { } private updateTouched() { - this.onTouched(); + this.callTouched(); } private updateValue() { if (this.lastValue !== this.value) { this.lastValue = this.value; - this.onChange(this.value); + this.callChange(this.value); } } diff --git a/src/Squidex/app/framework/angular/stars.component.ts b/src/Squidex/app/framework/angular/stars.component.ts index 6afa84d37..c73b0184e 100644 --- a/src/Squidex/app/framework/angular/stars.component.ts +++ b/src/Squidex/app/framework/angular/stars.component.ts @@ -21,8 +21,8 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR] }) export class StarsComponent implements ControlValueAccessor { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; private maximumStarsValue = 5; @Input() @@ -53,7 +53,7 @@ export class StarsComponent implements ControlValueAccessor { public writeValue(value: number | null | undefined) { if (Types.isNumber(value)) { - this.value = this.stars = value!; + this.value = this.stars = value || 0; } else { this.value = null; this.stars = 0; @@ -65,11 +65,11 @@ export class StarsComponent implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public setPreview(value: number) { @@ -97,8 +97,8 @@ export class StarsComponent implements ControlValueAccessor { this.value = null; this.stars = 0; - this.onChange(this.value); - this.onTouched(); + this.callChange(this.value); + this.callTouched(); } return false; @@ -112,8 +112,8 @@ export class StarsComponent implements ControlValueAccessor { if (this.value !== value) { this.value = this.stars = value; - this.onChange(this.value); - this.onTouched(); + this.callChange(this.value); + this.callTouched(); } return false; diff --git a/src/Squidex/app/framework/angular/tag-editor.component.ts b/src/Squidex/app/framework/angular/tag-editor.component.ts index e379b1947..87a6e6383 100644 --- a/src/Squidex/app/framework/angular/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/tag-editor.component.ts @@ -72,8 +72,8 @@ export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR] }) export class TagEditorComponent implements ControlValueAccessor { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; @Input() public converter: Converter = new StringConverter(); @@ -107,11 +107,11 @@ export class TagEditorComponent implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public remove(index: number) { @@ -119,7 +119,7 @@ export class TagEditorComponent implements ControlValueAccessor { } public markTouched() { - this.onTouched(); + this.callTouched(); } private resetForm() { @@ -146,9 +146,9 @@ export class TagEditorComponent implements ControlValueAccessor { this.items = items; if (items.length === 0 && this.useDefaultValue) { - this.onChange(undefined); + this.callChange(undefined); } else { - this.onChange(this.items); + this.callChange(this.items); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/toggle.component.ts b/src/Squidex/app/framework/angular/toggle.component.ts index c45705ce3..bac7739ba 100644 --- a/src/Squidex/app/framework/angular/toggle.component.ts +++ b/src/Squidex/app/framework/angular/toggle.component.ts @@ -21,14 +21,14 @@ export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] }) export class ToggleComponent implements ControlValueAccessor { - private onChange = (v: any) => { /* NOOP */ }; - private onTouched = () => { /* NOOP */ }; + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; public isChecked: boolean | null = null; public isDisabled = false; public writeValue(value: boolean | null | undefined) { - this.isChecked = Types.isBoolean(value) ? value! : null; + this.isChecked = Types.isBoolean(value) ? value || null : null; } public setDisabledState(isDisabled: boolean): void { @@ -36,11 +36,11 @@ export class ToggleComponent implements ControlValueAccessor { } public registerOnChange(fn: any) { - this.onChange = fn; + this.callChange = fn; } public registerOnTouched(fn: any) { - this.onTouched = fn; + this.callTouched = fn; } public changeState() { @@ -50,7 +50,7 @@ export class ToggleComponent implements ControlValueAccessor { this.isChecked = !(this.isChecked === true); - this.onChange(this.isChecked); - this.onTouched(); + this.callChange(this.isChecked); + this.callTouched(); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index 8a23706b1..f55da60ec 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/src/Squidex/app/shared/services/assets.service.spec.ts @@ -21,24 +21,26 @@ import { } from './../'; describe('AssetDto', () => { - it('should update name property and user info when renaming', () => { - const now = DateTime.now(); + const creation = DateTime.today(); + const creator = 'not-me'; + const modified = DateTime.now(); + const modifier = 'me'; + const version = new Version('2'); - const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, new Version('1')); - const asset_2 = asset_1.rename('new-name.png', 'me', now); + it('should update name property and user info when renaming', () => { + const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, version); + const asset_2 = asset_1.rename('new-name.png', modifier, modified); expect(asset_2.fileName).toEqual('new-name.png'); - expect(asset_2.lastModified).toEqual(now); - expect(asset_2.lastModifiedBy).toEqual('me'); + expect(asset_2.lastModified).toEqual(modified); + expect(asset_2.lastModifiedBy).toEqual(modifier); }); it('should update file properties when uploading', () => { - const now = DateTime.now(); - - const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2, new Version('1')); + const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2, new Version('2')); - const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, new Version('1')); - const asset_2 = asset_1.update(update, 'me', now); + const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, version); + const asset_2 = asset_1.update(update, modifier, modified); expect(asset_2.fileSize).toEqual(2); expect(asset_2.fileVersion).toEqual(2); @@ -46,8 +48,8 @@ describe('AssetDto', () => { expect(asset_2.isImage).toBeTruthy(); expect(asset_2.pixelWidth).toEqual(2); expect(asset_2.pixelHeight).toEqual(2); - expect(asset_2.lastModified).toEqual(now); - expect(asset_2.lastModifiedBy).toEqual('me'); + expect(asset_2.lastModified).toEqual(modified); + expect(asset_2.lastModifiedBy).toEqual(modifier); expect(asset_2.version).toEqual(update); }); }); diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 54a0a92d1..6385871fd 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -19,43 +19,43 @@ import { } from './../'; describe('ContentDto', () => { - it('should update data property and user info when updating', () => { - const now = DateTime.now(); + const creation = DateTime.today(); + const creator = 'not-me'; + const modified = DateTime.now(); + const modifier = 'me'; + const version = new Version('1'); - const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, new Version('1')); - const content_2 = content_1.update({ data: 2 }, 'me', now); + it('should update data property and user info when updating', () => { + const content_1 = new ContentDto('1', false, false, creator, creator, creation, creation, { data: 1 }, version); + const content_2 = content_1.update({ data: 2 }, modifier, modified); expect(content_2.data).toEqual({ data: 2 }); - expect(content_2.lastModified).toEqual(now); - expect(content_2.lastModifiedBy).toEqual('me'); + expect(content_2.lastModified).toEqual(modified); + expect(content_2.lastModifiedBy).toEqual(modifier); }); it('should update isPublished property and user info when publishing', () => { - const now = DateTime.now(); - - const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, new Version('1')); - const content_2 = content_1.publish('me', now); + const content_1 = new ContentDto('1', false, false, creator, creator, creation, creation, { data: 1 }, version); + const content_2 = content_1.publish(modifier, modified); expect(content_2.isPublished).toBeTruthy(); - expect(content_2.lastModified).toEqual(now); - expect(content_2.lastModifiedBy).toEqual('me'); + expect(content_2.lastModified).toEqual(modified); + expect(content_2.lastModifiedBy).toEqual(modifier); }); it('should update isPublished property and user info when unpublishing', () => { - const now = DateTime.now(); - - const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, new Version('1')); - const content_2 = content_1.unpublish('me', now); + const content_1 = new ContentDto('1', true, false, creator, creator, creation, creation, { data: 1 }, version); + const content_2 = content_1.unpublish(modifier, modified); expect(content_2.isPublished).toBeFalsy(); - expect(content_2.lastModified).toEqual(now); - expect(content_2.lastModifiedBy).toEqual('me'); + expect(content_2.lastModified).toEqual(modified); + expect(content_2.lastModifiedBy).toEqual(modifier); }); it('should update data property when setting data', () => { const newData = {}; - const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, new Version('1')); + const content_1 = new ContentDto('1', true, false, creator, creator, creation, creation, { data: 1 }, version); const content_2 = content_1.setData(newData); expect(content_2.data).toBe(newData); diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index 7018ff6d6..b80b9be17 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -28,40 +28,39 @@ import { describe('SchemaDto', () => { const properties = new SchemaPropertiesDto('Name'); + const creation = DateTime.today(); + const creator = 'not-me'; + const modified = DateTime.now(); + const modifier = 'me'; + const version = new Version('1'); it('should update isPublished property and user info when publishing', () => { - const now = DateTime.now(); - - const schema_1 = new SchemaDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1')); - const schema_2 = schema_1.publish('me', now); + const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version); + const schema_2 = schema_1.publish(modifier, modified); expect(schema_2.isPublished).toBeTruthy(); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update isPublished property and user info when unpublishing', () => { - const now = DateTime.now(); - - const schema_1 = new SchemaDto('1', 'name', properties, true, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1')); - const schema_2 = schema_1.unpublish('me', now); + const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version); + const schema_2 = schema_1.unpublish(modifier, modified); expect(schema_2.isPublished).toBeFalsy(); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update properties property and user info when updating', () => { const newProperties = new SchemaPropertiesDto('New Name'); - const now = DateTime.now(); - - const schema_1 = new SchemaDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1')); - const schema_2 = schema_1.update(newProperties, 'me', now); + const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version); + const schema_2 = schema_1.update(newProperties, modifier, modified); expect(schema_2.properties).toEqual(newProperties); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update scripts properties and user info when configure scripts', () => { @@ -74,10 +73,8 @@ describe('SchemaDto', () => { '', ''); - const now = DateTime.now(); - - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), []); - const schema_2 = schema_1.configureScripts(newScripts, 'me', now); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []); + const schema_2 = schema_1.configureScripts(newScripts, modifier, modified); expect(schema_2.scriptQuery).toEqual(''); expect(schema_2.scriptCreate).toEqual(''); @@ -85,89 +82,82 @@ describe('SchemaDto', () => { expect(schema_2.scriptDelete).toEqual(''); expect(schema_2.scriptPublish).toEqual(''); expect(schema_2.scriptUnpublish).toEqual(''); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); }); describe('SchemaDetailsDto', () => { const properties = new SchemaPropertiesDto('Name'); + const creation = DateTime.today(); + const creator = 'not-me'; + const modified = DateTime.now(); + const modifier = 'me'; + const version = new Version('1'); it('should update isPublished property and user info when publishing', () => { - const now = DateTime.now(); - - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), []); - const schema_2 = schema_1.publish('me', now); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []); + const schema_2 = schema_1.publish(modifier, modified); expect(schema_2.isPublished).toBeTruthy(); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update isPublished property and user info when unpublishing', () => { - const now = DateTime.now(); - - const schema_1 = new SchemaDetailsDto('1', 'name', properties, true, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), []); - const schema_2 = schema_1.unpublish('me', now); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, true, creator, creator, creation, creation, version, []); + const schema_2 = schema_1.unpublish(modifier, modified); expect(schema_2.isPublished).toBeFalsy(); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update properties property and user info when updating', () => { const newProperties = new SchemaPropertiesDto('New Name'); - const now = DateTime.now(); - - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), []); - const schema_2 = schema_1.update(newProperties, 'me', now); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []); + const schema_2 = schema_1.update(newProperties, modifier, modified); expect(schema_2.properties).toEqual(newProperties); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update fields property and user info when adding field', () => { const field1 = new FieldDto(1, '1', false, false, false, 'l', createProperties('String')); const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number')); - const now = DateTime.now(); - - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), [field1]); - const schema_2 = schema_1.addField(field2, 'me', now); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1]); + const schema_2 = schema_1.addField(field2, modifier, modified); expect(schema_2.fields).toEqual([field1, field2]); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update fields property and user info when removing field', () => { const field1 = new FieldDto(1, '1', false, false, false, 'l', createProperties('String')); const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number')); - const now = DateTime.now(); - - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), [field1, field2]); - const schema_2 = schema_1.removeField(field1, 'me', now); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1, field2]); + const schema_2 = schema_1.removeField(field1, modifier, modified); expect(schema_2.fields).toEqual([field2]); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update fields property and user info when replacing fields', () => { const field1 = new FieldDto(1, '1', false, false, false, 'l', createProperties('String')); const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number')); - const now = DateTime.now(); - - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), [field1, field2]); - const schema_2 = schema_1.replaceFields([field2, field1], 'me', now); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1, field2]); + const schema_2 = schema_1.replaceFields([field2, field1], modifier, modified); expect(schema_2.fields).toEqual([field2, field1]); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); it('should update fields property and user info when updating field', () => { @@ -175,14 +165,12 @@ describe('SchemaDetailsDto', () => { const field2_1 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number')); const field2_2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Boolean')); - const now = DateTime.now(); - - const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), new Version('1'), [field1_0, field2_1]); - const schema_2 = schema_1.updateField(field2_2, 'me', now); + const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1_0, field2_1]); + const schema_2 = schema_1.updateField(field2_2, modifier, modified); expect(schema_2.fields).toEqual([field1_0, field2_2]); - expect(schema_2.lastModified).toEqual(now); - expect(schema_2.lastModifiedBy).toEqual('me'); + expect(schema_2.lastModified).toEqual(modifier); + expect(schema_2.lastModifiedBy).toEqual(modified); }); }); diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 42ca23c75..6f054326e 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -797,7 +797,7 @@ export class SchemasService { user, now, now, - version || new Version('0'), + version, dto.fields || [], response.scriptQuery, response.scriptCreate, diff --git a/src/Squidex/app/shared/services/webhooks.service.spec.ts b/src/Squidex/app/shared/services/webhooks.service.spec.ts index 52a22513f..023266f8c 100644 --- a/src/Squidex/app/shared/services/webhooks.service.spec.ts +++ b/src/Squidex/app/shared/services/webhooks.service.spec.ts @@ -23,21 +23,25 @@ import { } from './../'; describe('WebhookDto', () => { - const now = DateTime.now(); - const user = 'me'; + const creation = DateTime.today(); + const creator = 'not-me'; + const modified = DateTime.now(); + const modifier = 'me'; const version = new Version('1'); it('should update url and schemas', () => { - const webhook_1 = new WebhookDto('id1', 'token1', user, user, now, now, version, [], 'http://squidex.io/hook', 1, 2, 3, 4); + const webhook_1 = new WebhookDto('id1', 'token1', creator, creator, creation, creation, version, [], 'http://squidex.io/hook', 1, 2, 3, 4); const webhook_2 = webhook_1.update(new UpdateWebhookDto('http://squidex.io/hook2', [ new WebhookSchemaDto('1', true, true, true, true, true), new WebhookSchemaDto('2', true, true, true, true, true) - ]), user, now); + ]), modifier, modified); expect(webhook_2.url).toEqual('http://squidex.io/hook2'); expect(webhook_2.schemas.length).toEqual(2); + expect(webhook_2.lastModified).toEqual(modified); + expect(webhook_2.lastModifiedBy).toEqual(modifier); }); }); diff --git a/src/Squidex/tsconfig.json b/src/Squidex/tsconfig.json index b740cdfc7..54692ff9c 100644 --- a/src/Squidex/tsconfig.json +++ b/src/Squidex/tsconfig.json @@ -9,7 +9,7 @@ "noUnusedParameters": false, "removeComments": false, "sourceMap": true, - "strictNullChecks": true, + "strictNullChecks": false, "suppressImplicitAnyIndexErrors": true, "target": "es5", "paths": { From 8fde549a3dfb74d32022f2258ce1cf84ceae1ba9 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Sep 2017 16:44:56 +0200 Subject: [PATCH 06/10] Archive implemented. --- .../Scripting/JintScriptEngine.cs | 15 ++++ .../Scripting/ScriptContext.cs | 2 + .../Schemas/ScriptsConfigured.cs | 4 +- .../Assets/MongoAssetEntity.cs | 4 - .../Assets/MongoAssetRepository.cs | 9 ++- .../MongoAssetRepository_EventHandling.cs | 6 +- .../Contents/MongoContentEntity.cs | 2 +- .../Contents/MongoContentRepository.cs | 8 +- .../MongoContentRepository_EventHandling.cs | 34 +++++--- .../Contents/Visitors/FindExtensions.cs | 8 +- .../Schemas/MongoSchemaEntity.cs | 4 +- .../Assets/IAssetEntity.cs | 2 - .../Contents/ContentQueryService.cs | 8 +- .../Contents/GraphQL/QueryContext.cs | 4 +- .../Contents/IContentEntity.cs | 2 +- .../Contents/IContentQueryService.cs | 2 +- .../Repositories/IContentRepository.cs | 4 +- .../Schemas/ISchemaEntity.cs | 4 +- .../Contents/ContentCommandMiddleware.cs | 45 ++++++++--- .../Contents/ContentDomainObject.cs | 30 ++++++- .../Schemas/Commands/ConfigureScripts.cs | 4 +- .../Api/Assets/AssetsController.cs | 2 +- .../Api/Schemas/Models/ConfigureScriptsDto.cs | 9 +-- .../Api/Schemas/Models/SchemaDetailsDto.cs | 9 +-- .../ContentApi/ContentsController.cs | 19 ++++- .../Generator/SchemaSwaggerGenerator.cs | 25 ++++-- .../ContentApi/Models/ContentDto.cs | 21 ++--- .../Scripting/JintScriptEngineTests.cs | 23 ++++++ .../Contents/ContentQueryServiceTests.cs | 6 +- .../Contents/TestData/FakeContentEntity.cs | 2 + .../Contents/ContentCommandMiddlewareTests.cs | 51 ++++++++---- .../Contents/ContentDomainObjectTests.cs | 79 ++++++++++++++++--- .../Schemas/SchemaDomainObjectTests.cs | 6 +- 33 files changed, 311 insertions(+), 142 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs index e9377a600..0657c3ce2 100644 --- a/src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs +++ b/src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs @@ -49,6 +49,16 @@ namespace Squidex.Domain.Apps.Core.Scripting EnableDisallow(engine); EnableReject(engine, operationName); + engine.SetValue("operation", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + engine.SetValue("replace", new Action(() => { var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); @@ -135,6 +145,11 @@ namespace Squidex.Domain.Apps.Core.Scripting contextInstance.FastAddProperty("user", new JintUser(engine, context.User), false, true, false); } + if (!string.IsNullOrWhiteSpace(context.Operation)) + { + contextInstance.FastAddProperty("operation", context.Operation, false, true, false); + } + engine.SetValue("ctx", contextInstance); return engine; diff --git a/src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs b/src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs index ae8628a20..fcc5a0733 100644 --- a/src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs +++ b/src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs @@ -21,5 +21,7 @@ namespace Squidex.Domain.Apps.Core.Scripting public NamedContentData Data { get; set; } public NamedContentData OldData { get; set; } + + public string Operation { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs b/src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs index 093197d28..36764d064 100644 --- a/src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs +++ b/src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs @@ -21,8 +21,6 @@ namespace Squidex.Domain.Apps.Events.Schemas public string ScriptDelete { get; set; } - public string ScriptPublish { get; set; } - - public string ScriptUnpublish { get; set; } + public string ScriptChange { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs index 6fdd057fa..11c54cc36 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs @@ -36,10 +36,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets [BsonElement] public bool IsImage { get; set; } - [BsonRequired] - [BsonElement] - public bool IsDeleted { get; set; } - [BsonRequired] [BsonElement] public long Version { get; set; } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs index 808f4137c..87b055d15 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs @@ -33,7 +33,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets protected override Task SetupCollectionAsync(IMongoCollection collection) { - return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.IsDeleted).Descending(x => x.LastModified).Ascending(x => x.FileName).Ascending(x => x.MimeType)); + return collection.Indexes.CreateOneAsync( + Index.Ascending(x => x.AppId) + .Ascending(x => x.FileName) + .Ascending(x => x.MimeType) + .Descending(x => x.LastModified)); } public async Task> QueryNotFoundAsync(Guid appId, IList assetIds) @@ -80,8 +84,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets { var filters = new List> { - Filter.Eq(x => x.AppId, appId), - Filter.Eq(x => x.IsDeleted, false) + Filter.Eq(x => x.AppId, appId) }; if (ids != null && ids.Count > 0) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs index 5b56f8285..1915a40bd 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs @@ -7,6 +7,7 @@ // ========================================================================== using System.Threading.Tasks; +using MongoDB.Driver; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; @@ -58,10 +59,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets protected Task On(AssetDeleted @event, EnvelopeHeaders headers) { - return Collection.UpdateAsync(@event, headers, a => - { - a.IsDeleted = true; - }); + return Collection.DeleteOneAsync(x => x.Id == @event.AssetId); } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs index 74acd25db..d14127ab8 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents [BsonRequired] [BsonElement("dl")] - public bool IsDeleted { get; set; } + public bool IsArchived { get; set; } [BsonRequired] [BsonElement("dt")] diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs index 29d4df72a..ca032d5c5 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents this.database = database; } - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, HashSet ids, ODataUriParser odataQuery) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet ids, ODataUriParser odataQuery) { var collection = GetCollection(app.Id); @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents { cursor = collection - .Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished) + .Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived) .Take(odataQuery) .Skip(odataQuery) .Sort(odataQuery, schema.SchemaDef); @@ -104,14 +104,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents return entities; } - public Task CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, HashSet ids, ODataUriParser odataQuery) + public Task CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet ids, ODataUriParser odataQuery) { var collection = GetCollection(app.Id); IFindFluent cursor; try { - cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished); + cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived); } catch (NotSupportedException) { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 918593ea8..ef5cdb889 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified)); await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsPublished)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsArchived)); await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); }); } @@ -115,30 +115,24 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents }); } - protected Task On(ContentRestored @event, EnvelopeHeaders headers) + protected Task On(ContentArchived @event, EnvelopeHeaders headers) { return ForAppIdAsync(@event.AppId.Id, collection => { return collection.UpdateAsync(@event, headers, x => { - x.IsDeleted = false; + x.IsArchived = true; }); }); } - protected Task On(ContentDeleted @event, EnvelopeHeaders headers) + protected Task On(ContentRestored @event, EnvelopeHeaders headers) { - return ForAppIdAsync(@event.AppId.Id, async collection => + return ForAppIdAsync(@event.AppId.Id, collection => { - await collection.UpdateManyAsync( - Filter.And( - Filter.AnyEq(x => x.ReferencedIds, @event.ContentId), - Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), - Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); - - await collection.UpdateAsync(@event, headers, x => + return collection.UpdateAsync(@event, headers, x => { - x.IsDeleted = true; + x.IsArchived = false; }); }); } @@ -155,6 +149,20 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents }); } + protected Task On(ContentDeleted @event, EnvelopeHeaders headers) + { + return ForAppIdAsync(@event.AppId.Id, async collection => + { + await collection.UpdateManyAsync( + Filter.And( + Filter.AnyEq(x => x.ReferencedIds, @event.ContentId), + Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), + Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); + + await collection.DeleteOneAsync(x => x.Id == @event.ContentId); + }); + } + private Task ForAppIdAsync(Guid appId, Func, Task> action) { var collection = GetCollection(appId); diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs index 47b6a56ba..ebf2d9fd0 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs @@ -56,19 +56,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors return cursor; } - public static IFindFluent Find(this IMongoCollection cursor, ODataUriParser query, HashSet ids, Guid schemaId, Schema schema, bool nonPublished) + public static IFindFluent Find(this IMongoCollection cursor, ODataUriParser query, HashSet ids, Guid schemaId, Schema schema, bool nonPublished, bool archived) { - var filter = BuildQuery(query, ids, schemaId, schema, nonPublished); + var filter = BuildQuery(query, ids, schemaId, schema, nonPublished, archived); return cursor.Find(filter); } - public static FilterDefinition BuildQuery(ODataUriParser query, HashSet ids, Guid schemaId, Schema schema, bool nonPublished) + public static FilterDefinition BuildQuery(ODataUriParser query, HashSet ids, Guid schemaId, Schema schema, bool nonPublished, bool archived) { var filters = new List> { Filter.Eq(x => x.SchemaId, schemaId), - Filter.Eq(x => x.IsDeleted, false) + Filter.Eq(x => x.IsArchived, archived) }; if (!nonPublished) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs index 0f4d7d1ae..56179470a 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs @@ -25,8 +25,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas [BsonElement] public string Name { get; set; } - public string ScriptUnpublish { get; set; } - [BsonRequired] [BsonElement] public string Schema { get; set; } @@ -73,7 +71,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas [BsonIgnoreIfNull] [BsonElement] - public string ScriptPublish { get; set; } + public string ScriptChange { get; set; } Schema ISchemaEntity.SchemaDef { diff --git a/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs b/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs index ac23e8fd4..46b0e4ea9 100644 --- a/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs @@ -20,8 +20,6 @@ namespace Squidex.Domain.Apps.Read.Assets bool IsImage { get; } - bool IsDeleted { get; } - int? PixelWidth { get; } int? PixelHeight { get; } diff --git a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs index 1177c3620..564adcdd9 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Read.Contents return (schema, content); } - public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, HashSet ids, string query) + public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet ids, string query) { Guard.NotNull(app, nameof(app)); Guard.NotNull(user, nameof(user)); @@ -85,8 +85,8 @@ namespace Squidex.Domain.Apps.Read.Contents var isFrontendClient = user.IsInClient("squidex-frontend"); - var taskForItems = contentRepository.QueryAsync(app, schema, isFrontendClient, ids, parsedQuery); - var taskForCount = contentRepository.CountAsync(app, schema, isFrontendClient, ids, parsedQuery); + var taskForItems = contentRepository.QueryAsync(app, schema, isFrontendClient, archived, ids, parsedQuery); + var taskForCount = contentRepository.CountAsync(app, schema, isFrontendClient, archived, ids, parsedQuery); await Task.WhenAll(taskForItems, taskForCount); @@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Read.Contents public Guid Id { get; set; } public Guid AppId { get; set; } public long Version { get; set; } - public bool IsDeleted { get; set; } + public bool IsArchived { get; set; } public bool IsPublished { get; set; } public Instant Created { get; set; } public Instant LastModified { get; set; } diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs index b6d39409e..6d5795755 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs @@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL public async Task> QueryContentsAsync(Guid schemaId, string query) { - var contents = (await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, null, query).ConfigureAwait(false)).Items; + var contents = (await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, false, null, query).ConfigureAwait(false)).Items; foreach (var content in contents) { @@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL if (notLoadedContents.Count > 0) { - var contents = (await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, notLoadedContents, null).ConfigureAwait(false)).Items; + var contents = (await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, false, notLoadedContents, null).ConfigureAwait(false)).Items; foreach (var content in contents) { diff --git a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs index 64f2b3970..9f0e7265c 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Read.Contents { bool IsPublished { get; } - bool IsDeleted { get; } + bool IsArchived { get; } NamedContentData Data { get; } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs index 6a8d7e4ee..1ad7c8f5b 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Read.Contents { public interface IContentQueryService { - Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, HashSet ids, string query); + Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet ids, string query); Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id); diff --git a/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs index cdede669f..fea6ff459 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs @@ -17,11 +17,11 @@ namespace Squidex.Domain.Apps.Read.Contents.Repositories { public interface IContentRepository { - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, HashSet ids, ODataUriParser odataQuery); + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet ids, ODataUriParser odataQuery); Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds); - Task CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, HashSet ids, ODataUriParser odataQuery); + Task CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet ids, ODataUriParser odataQuery); Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); } diff --git a/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs b/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs index 7de83ab0b..17348a1b4 100644 --- a/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs @@ -26,9 +26,7 @@ namespace Squidex.Domain.Apps.Read.Schemas string ScriptDelete { get; } - string ScriptPublish { get; } - - string ScriptUnpublish { get; } + string ScriptChange { get; } Schema SchemaDef { get; } } diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs index 4f055b2e9..5de4c6f6d 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs @@ -116,9 +116,9 @@ namespace Squidex.Domain.Apps.Write.Contents return handler.UpdateAsync(context, async content => { var schemaAndApp = await ResolveSchemaAndAppAsync(command); - var scriptContext = CreateScriptContext(content, command); + var scriptContext = CreateScriptContext(content, command, "Publish"); - scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptPublish, "publish content"); + scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "publish content"); content.Publish(command); }); @@ -129,35 +129,53 @@ namespace Squidex.Domain.Apps.Write.Contents return handler.UpdateAsync(context, async content => { var schemaAndApp = await ResolveSchemaAndAppAsync(command); - var scriptContext = CreateScriptContext(content, command); + var scriptContext = CreateScriptContext(content, command, "Unpublish"); - scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptUnpublish, "unpublish content"); + scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "unpublish content"); content.Unpublish(command); }); } - protected Task On(DeleteContent command, CommandContext context) + protected Task On(ArchiveContent command, CommandContext context) { return handler.UpdateAsync(context, async content => { var schemaAndApp = await ResolveSchemaAndAppAsync(command); - var scriptContext = CreateScriptContext(content, command); + var scriptContext = CreateScriptContext(content, command, "Archive"); - scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptDelete, "delete content"); + scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "archive content"); - content.Delete(command); + content.Archive(command); }); } protected Task On(RestoreContent command, CommandContext context) { - return handler.UpdateAsync(context, content => + return handler.UpdateAsync(context, async content => { + var schemaAndApp = await ResolveSchemaAndAppAsync(command); + var scriptContext = CreateScriptContext(content, command, "Restore"); + + scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "restore content"); + content.Restore(command); }); } + protected Task On(DeleteContent command, CommandContext context) + { + return handler.UpdateAsync(context, async content => + { + var schemaAndApp = await ResolveSchemaAndAppAsync(command); + var scriptContext = CreateScriptContext(content, command, "Delete"); + + scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptDelete, "delete content"); + + content.Delete(command); + }); + } + public async Task HandleAsync(CommandContext context, Func next) { if (!await this.DispatchActionAsync(context.Command, context)) @@ -200,9 +218,14 @@ namespace Squidex.Domain.Apps.Write.Contents } } - private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, NamedContentData data = null) + private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, string operation) + { + return new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Operation = operation }; + } + + private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, NamedContentData data) { - return new ScriptContext { ContentId = content.Id, Data = data, OldData = content.Data, User = command.User }; + return new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Data = data }; } private async Task<(ISchemaEntity SchemaEntity, IAppEntity AppEntity)> ResolveSchemaAndAppAsync(SchemaCommand command) diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs index d4812c129..5573f9294 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs @@ -23,6 +23,7 @@ namespace Squidex.Domain.Apps.Write.Contents private bool isDeleted; private bool isCreated; private bool isPublished; + private bool isArchived; private NamedContentData data; public bool IsDeleted @@ -30,6 +31,11 @@ namespace Squidex.Domain.Apps.Write.Contents get { return isDeleted; } } + public bool IsArchived + { + get { return isArchived; } + } + public bool IsPublished { get { return isPublished; } @@ -67,14 +73,19 @@ namespace Squidex.Domain.Apps.Write.Contents isPublished = false; } - protected void On(ContentDeleted @event) + protected void On(ContentArchived @event) { - isDeleted = true; + isArchived = true; } protected void On(ContentRestored @event) { - isDeleted = false; + isArchived = false; + } + + protected void On(ContentDeleted @event) + { + isDeleted = true; } public ContentDomainObject Create(CreateContent command) @@ -108,13 +119,24 @@ namespace Squidex.Domain.Apps.Write.Contents { Guard.NotNull(command, nameof(command)); - VerifyDeleted(); + VerifyCreatedAndNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new ContentRestored())); return this; } + public ContentDomainObject Archive(ArchiveContent command) + { + Guard.NotNull(command, nameof(command)); + + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentArchived())); + + return this; + } + public ContentDomainObject Publish(PublishContent command) { Guard.NotNull(command, nameof(command)); diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs index 9d605487b..08453ecb7 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs @@ -18,8 +18,6 @@ namespace Squidex.Domain.Apps.Write.Schemas.Commands public string ScriptDelete { get; set; } - public string ScriptPublish { get; set; } - - public string ScriptUnpublish { get; set; } + public string ScriptChange { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Assets/AssetsController.cs b/src/Squidex/Controllers/Api/Assets/AssetsController.cs index 68e8bb85e..8d538f3ca 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetsController.cs +++ b/src/Squidex/Controllers/Api/Assets/AssetsController.cs @@ -133,7 +133,7 @@ namespace Squidex.Controllers.Api.Assets { var entity = await assetRepository.FindAssetAsync(id); - if (entity == null || entity.IsDeleted) + if (entity == null) { return NotFound(); } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs index 8471d70f2..0fe895a3e 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs @@ -31,13 +31,8 @@ namespace Squidex.Controllers.Api.Schemas.Models public string ScriptDelete { get; set; } /// - /// The script that is executed when publishing a content. + /// The script that is executed when change a content status. /// - public string ScriptPublish { get; set; } - - /// - /// The script that is executed when unpublishing a content. - /// - public string ScriptUnpublish { get; set; } + public string ScriptChange { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs index 11b5beef7..34e06a360 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs @@ -54,14 +54,9 @@ namespace Squidex.Controllers.Api.Schemas.Models public string ScriptDelete { get; set; } /// - /// The script that is executed when publishing a content. + /// The script that is executed when changing a content status. /// - public string ScriptPublish { get; set; } - - /// - /// The script that is executed when unpublishing a content. - /// - public string ScriptUnpublish { get; set; } + public string ScriptChange { get; set; } /// /// The list of fields. diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index fa7db3ca7..a02fc529a 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -69,7 +69,7 @@ namespace Squidex.Controllers.ContentApi [HttpGet] [Route("content/{app}/{name}")] [ApiCosts(2)] - public async Task GetContents(string name, [FromQuery] string ids = null) + public async Task GetContents(string name, [FromQuery] bool archived = false, [FromQuery] string ids = null) { var idsList = new HashSet(); @@ -86,7 +86,7 @@ namespace Squidex.Controllers.ContentApi var isFrontendClient = User.IsFrontendClient(); - var contents = await contentQuery.QueryWithCountAsync(App, name, User, idsList, Request.QueryString.ToString()); + var contents = await contentQuery.QueryWithCountAsync(App, name, User, archived, idsList, Request.QueryString.ToString()); var response = new AssetsDto { @@ -228,6 +228,21 @@ namespace Squidex.Controllers.ContentApi return NoContent(); } + [MustBeAppEditor] + [HttpPut] + [Route("content/{app}/{name}/{id}/archive")] + [ApiCosts(1)] + public async Task ArchiveContent(string name, Guid id) + { + await contentQuery.FindSchemaAsync(App, name); + + var command = new ArchiveContent { ContentId = id, User = User }; + + await CommandBus.PublishAsync(command); + + return NoContent(); + } + [MustBeAppEditor] [HttpPut] [Route("content/{app}/{name}/{id}/restore")] diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs index 53e649ea1..e8ab4715f 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs @@ -92,9 +92,10 @@ namespace Squidex.Controllers.ContentApi.Generator GenerateSchemaGetOperation(), GenerateSchemaUpdateOperation(), GenerateSchemaPatchOperation(), - GenerateSchemaRestoreOperation(), GenerateSchemaPublishOperation(), GenerateSchemaUnpublishOperation(), + GenerateSchemaArchiveOperation(), + GenerateSchemaRestoreOperation(), GenerateSchemaDeleteOperation() }; @@ -161,7 +162,7 @@ namespace Squidex.Controllers.ContentApi.Generator operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); - operation.AddResponse("201", $"{schemaName} element updated.", dataSchema); + operation.AddResponse("201", $"{schemaName} item updated.", dataSchema); }); } @@ -175,7 +176,7 @@ namespace Squidex.Controllers.ContentApi.Generator operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription); - operation.AddResponse("201", $"{schemaName} element patched.", dataSchema); + operation.AddResponse("201", $"{schemaName} item patched.", dataSchema); }); } @@ -187,7 +188,7 @@ namespace Squidex.Controllers.ContentApi.Generator operation.Summary = $"Publish a {schemaName} content."; operation.Security = EditorSecurity; - operation.AddResponse("204", $"{schemaName} element published."); + operation.AddResponse("204", $"{schemaName} item published."); }); } @@ -199,7 +200,19 @@ namespace Squidex.Controllers.ContentApi.Generator operation.Summary = $"Unpublish a {schemaName} content."; operation.Security = EditorSecurity; - operation.AddResponse("204", $"{schemaName} element unpublished."); + operation.AddResponse("204", $"{schemaName} item unpublished."); + }); + } + + private SwaggerOperations GenerateSchemaArchiveOperation() + { + return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/archive", operation => + { + operation.OperationId = $"Archive{schemaKey}Content"; + operation.Summary = $"Archive a {schemaName} content."; + operation.Security = EditorSecurity; + + operation.AddResponse("204", $"{schemaName} item restored."); }); } @@ -211,7 +224,7 @@ namespace Squidex.Controllers.ContentApi.Generator operation.Summary = $"Restore a {schemaName} content."; operation.Security = EditorSecurity; - operation.AddResponse("204", $"{schemaName} element restored."); + operation.AddResponse("204", $"{schemaName} item restored."); }); } diff --git a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs index 37469c6c3..bfaca41c2 100644 --- a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs +++ b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs @@ -19,18 +19,18 @@ namespace Squidex.Controllers.ContentApi.Models public sealed class ContentDto { /// - /// The if of the content element. + /// The if of the content item. /// public Guid Id { get; set; } /// - /// The user that has created the content element. + /// The user that has created the content item. /// [Required] public RefToken CreatedBy { get; set; } /// - /// The user that has updated the content element. + /// The user that has updated the content item. /// [Required] public RefToken LastModifiedBy { get; set; } @@ -42,35 +42,30 @@ namespace Squidex.Controllers.ContentApi.Models public object Data { get; set; } /// - /// The date and time when the content element has been created. + /// The date and time when the content item has been created. /// public Instant Created { get; set; } /// - /// The date and time when the content element has been modified last. + /// The date and time when the content item has been modified last. /// public Instant LastModified { get; set; } /// - /// Indicates if the content element is published. + /// Indicates if the content item is published. /// public bool? IsPublished { get; set; } /// - /// Indicates if the content element is deleted. + /// Indicates if the content item is archived. /// - public bool IsDeleted { get; set; } + public bool IsArchived { get; set; } /// /// The version of the content. /// public long Version { get; set; } - public bool ShouldSerializeIsDeleted() - { - return IsDeleted; - } - public static ContentDto Create(CreateContent command, EntityCreatedResult result) { var now = SystemClock.Instance.GetCurrentInstant(); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs index 84e09f2ac..5aea17fd4 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs @@ -80,6 +80,29 @@ namespace Squidex.Domain.Apps.Core.Scripting Assert.Same(content, result); } + [Fact] + public void Should_fetch_operation_name() + { + var content = new NamedContentData(); + + var expected = + new NamedContentData() + .AddField("operation", + new ContentFieldData() + .AddValue("iv", "MyOperation")); + + var context = new ScriptContext { Data = content, Operation = "MyOperation" }; + + var result = scriptEngine.ExecuteAndTransform(context, @" + var data = ctx.data; + + data.operation = { iv: ctx.operation }; + + replace(data)", "update"); + + Assert.Equal(expected, result); + } + [Fact] public void Should_transform_content_and_return_with_transform() { diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs index 19631b7e6..143e690f7 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs @@ -122,9 +122,9 @@ namespace Squidex.Domain.Apps.Read.Contents A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)) .Returns(schema); - A.CallTo(() => contentRepository.QueryAsync(app, schema, false, ids, A.Ignored)) + A.CallTo(() => contentRepository.QueryAsync(app, schema, false, true, ids, A.Ignored)) .Returns(new List { content }); - A.CallTo(() => contentRepository.CountAsync(app, schema, false, ids, A.Ignored)) + A.CallTo(() => contentRepository.CountAsync(app, schema, false, true, ids, A.Ignored)) .Returns(123); A.CallTo(() => schema.ScriptQuery) @@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Read.Contents A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "")) .Returns(transformedData); - var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, ids, null); + var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, true, ids, null); Assert.Equal(123, result.Total); Assert.Equal(schema, result.Schema); diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs index 49a32afb2..f61a3b658 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs @@ -31,6 +31,8 @@ namespace Squidex.Domain.Apps.Read.Contents.TestData public bool IsPublished { get; set; } + public bool IsArchived { get; set; } + public NamedContentData Data { get; set; } } } diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs index 9a04d774a..f108842f0 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -178,8 +178,8 @@ namespace Squidex.Domain.Apps.Write.Contents [Fact] public async Task Publish_should_publish_domain_object() { - A.CallTo(() => schema.ScriptPublish) - .Returns(""); + A.CallTo(() => schema.ScriptChange) + .Returns(""); CreateContent(); @@ -190,14 +190,14 @@ namespace Squidex.Domain.Apps.Write.Contents await sut.HandleAsync(context); }); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "publish content")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "publish content")).MustHaveHappened(); } [Fact] public async Task Unpublish_should_unpublish_domain_object() { - A.CallTo(() => schema.ScriptUnpublish) - .Returns(""); + A.CallTo(() => schema.ScriptChange) + .Returns(""); CreateContent(); @@ -208,40 +208,61 @@ namespace Squidex.Domain.Apps.Write.Contents await sut.HandleAsync(context); }); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "unpublish content")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "unpublish content")).MustHaveHappened(); } [Fact] - public async Task Delete_should_update_domain_object() + public async Task Archive_should_archive_domain_object() { - A.CallTo(() => schema.ScriptDelete) - .Returns(""); + A.CallTo(() => schema.ScriptChange) + .Returns(""); CreateContent(); - var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user }); + var context = CreateContextForCommand(new ArchiveContent { ContentId = contentId, User = user }); await TestUpdate(content, async _ => { - await sut.HandleAsync(command); + await sut.HandleAsync(context); }); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "delete content")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "archive content")).MustHaveHappened(); } [Fact] - public async Task Restore_should_update_domain_object() + public async Task Restore_should_restore_domain_object() { + A.CallTo(() => schema.ScriptChange) + .Returns(""); + CreateContent(); - content.Delete(new DeleteContent()); + var context = CreateContextForCommand(new RestoreContent { ContentId = contentId, User = user }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "restore content")).MustHaveHappened(); + } - var command = CreateContextForCommand(new RestoreContent { ContentId = contentId, User = user }); + [Fact] + public async Task Delete_should_update_domain_object() + { + A.CallTo(() => schema.ScriptDelete) + .Returns(""); + + CreateContent(); + + var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user }); await TestUpdate(content, async _ => { await sut.HandleAsync(command); }); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "", "delete content")).MustHaveHappened(); } private void CreateContent() diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs index bd23910bb..674f8f898 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs @@ -268,59 +268,71 @@ namespace Squidex.Domain.Apps.Write.Contents } [Fact] - public void Delete_should_throw_exception_if_not_created() + public void Archive_should_throw_exception_if_not_created() { Assert.Throws(() => { - sut.Delete(CreateContentCommand(new DeleteContent())); + sut.Archive(CreateContentCommand(new ArchiveContent())); }); } [Fact] - public void Delete_should_throw_exception_if_already_deleted() + public void Archive_should_throw_exception_if_content_is_deleted() { CreateContent(); DeleteContent(); Assert.Throws(() => { - sut.Delete(CreateContentCommand(new DeleteContent())); + sut.Archive(CreateContentCommand(new ArchiveContent())); }); } [Fact] - public void Delete_should_update_properties_create_events() + public void Archive_should_refresh_properties_and_create_events() { CreateContent(); - sut.Delete(CreateContentCommand(new DeleteContent())); + sut.Archive(CreateContentCommand(new ArchiveContent())); - Assert.True(sut.IsDeleted); + Assert.True(sut.IsArchived); sut.GetUncomittedEvents() .ShouldHaveSameEvents( - CreateContentEvent(new ContentDeleted()) + CreateContentEvent(new ContentArchived()) ); } [Fact] - public void Restore_should_throw_exception_if_not_deleted() + public void Restore_should_throw_exception_if_not_created() { Assert.Throws(() => { - sut.Delete(CreateContentCommand(new DeleteContent())); + sut.Restore(CreateContentCommand(new RestoreContent())); }); } [Fact] - public void Restore_should_update_properties_create_events() + public void Restore_should_throw_exception_if_content_is_deleted() { CreateContent(); DeleteContent(); + Assert.Throws(() => + { + sut.Restore(CreateContentCommand(new RestoreContent())); + }); + } + + [Fact] + public void Restore_should_refresh_properties_and_create_events() + { + CreateContent(); + ArchiveContent(); + sut.Restore(CreateContentCommand(new RestoreContent())); - Assert.False(sut.IsDeleted); + Assert.False(sut.IsArchived); sut.GetUncomittedEvents() .ShouldHaveSameEvents( @@ -328,6 +340,42 @@ namespace Squidex.Domain.Apps.Write.Contents ); } + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Delete_should_update_properties_create_events() + { + CreateContent(); + + sut.Delete(CreateContentCommand(new DeleteContent())); + + Assert.True(sut.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDeleted()) + ); + } + private void CreateContent() { sut.Create(CreateContentCommand(new CreateContent { Data = data })); @@ -349,6 +397,13 @@ namespace Squidex.Domain.Apps.Write.Contents ((IAggregate)sut).ClearUncommittedEvents(); } + private void ArchiveContent() + { + sut.Archive(CreateContentCommand(new ArchiveContent())); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + private void DeleteContent() { sut.Delete(CreateContentCommand(new DeleteContent())); diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs index d95ce26a2..e7932c539 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs @@ -189,8 +189,7 @@ namespace Squidex.Domain.Apps.Write.Schemas ScriptCreate = "", ScriptUpdate = "", ScriptDelete = "", - ScriptPublish = "", - ScriptUnpublish = "" + ScriptChange = "" })); sut.GetUncomittedEvents() @@ -201,8 +200,7 @@ namespace Squidex.Domain.Apps.Write.Schemas ScriptCreate = "", ScriptUpdate = "", ScriptDelete = "", - ScriptPublish = "", - ScriptUnpublish = "" + ScriptChange = "" }) ); } From 302efedf1aaa7cb7d6bdb37472443b8c4524ac63 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Sep 2017 16:48:31 +0200 Subject: [PATCH 07/10] Script fixes in UI --- .../Contents/ContentArchived.cs | 17 +++++++++ .../Contents/Commands/ArchiveContent.cs | 14 ++++++++ .../schema/schema-scripts-form.component.ts | 9 ++--- .../shared/services/schemas.service.spec.ts | 12 +++---- .../app/shared/services/schemas.service.ts | 36 +++++++------------ 5 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Events/Contents/ContentArchived.cs create mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/ArchiveContent.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentArchived.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentArchived.cs new file mode 100644 index 000000000..4c5029b0d --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentArchived.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ContentArchived.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Contents +{ + [EventType(nameof(ContentArchived))] + public sealed class ContentArchived : ContentEvent + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/ArchiveContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/ArchiveContent.cs new file mode 100644 index 000000000..805bd0bc2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/ArchiveContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// ArchiveContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Contents.Commands +{ + public sealed class ArchiveContent : ContentCommand + { + } +} diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts index b616edca5..5d5c602f8 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts @@ -41,8 +41,7 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit 'Create', 'Update', 'Delete', - 'Publish', - 'Unpublish' + 'Change' ]; public editFormSubmitted = false; @@ -52,8 +51,7 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit scriptCreate: '', scriptUpdate: '', scriptDelete: '', - scriptPublish: '', - scriptUnpublish: '' + scriptChange: '' }); constructor(dialogs: DialogService, @@ -84,8 +82,7 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit this.editForm.controls['scriptCreate'].value, this.editForm.controls['scriptUpdate'].value, this.editForm.controls['scriptDelete'].value, - this.editForm.controls['scriptPublish'].value, - this.editForm.controls['scriptUnpublish'].value); + this.editForm.controls['scriptChange'].value); this.schemas.putSchemaScripts(this.appName, this.schema.name, requestDto, this.schema.version) .subscribe(dto => { diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index b80b9be17..1a7437dd6 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -70,8 +70,7 @@ describe('SchemaDto', () => { '', '', '', - '', - ''); + ''); const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []); const schema_2 = schema_1.configureScripts(newScripts, modifier, modified); @@ -80,8 +79,7 @@ describe('SchemaDto', () => { expect(schema_2.scriptCreate).toEqual(''); expect(schema_2.scriptUpdate).toEqual(''); expect(schema_2.scriptDelete).toEqual(''); - expect(schema_2.scriptPublish).toEqual(''); - expect(schema_2.scriptUnpublish).toEqual(''); + expect(schema_2.scriptChange).toEqual(''); expect(schema_2.lastModified).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modified); }); @@ -380,8 +378,7 @@ describe('SchemasService', () => { scriptCreate: '', scriptUpdate: '', scriptDelete: '', - scriptPublish: '', - scriptUnpublish: '' + scriptChange: '' }); expect(schema).toEqual( @@ -403,8 +400,7 @@ describe('SchemasService', () => { '', '', '', - '', - '')); + '')); })); it('should provide entry from cache if not found', diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 6f054326e..ec98da47d 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -126,8 +126,7 @@ export class SchemaDetailsDto extends SchemaDto { public readonly scriptCreate?: string, public readonly scriptUpdate?: string, public readonly scriptDelete?: string, - public readonly scriptPublish?: string, - public readonly scriptUnpublish?: string + public readonly scriptChange?: string ) { super(id, name, properties, isPublished, createdBy, lastModifiedBy, created, lastModified, version); } @@ -146,8 +145,7 @@ export class SchemaDetailsDto extends SchemaDto { this.scriptCreate, this.scriptUpdate, this.scriptDelete, - this.scriptPublish, - this.scriptUnpublish); + this.scriptChange); } public unpublish(user: string, now?: DateTime): SchemaDetailsDto { @@ -164,8 +162,7 @@ export class SchemaDetailsDto extends SchemaDto { this.scriptCreate, this.scriptUpdate, this.scriptDelete, - this.scriptPublish, - this.scriptUnpublish); + this.scriptChange); } public configureScripts(scripts: UpdateSchemaScriptsDto, user: string, now?: DateTime): SchemaDetailsDto { @@ -182,8 +179,7 @@ export class SchemaDetailsDto extends SchemaDto { scripts.scriptCreate, scripts.scriptUpdate, scripts.scriptDelete, - scripts.scriptPublish, - scripts.scriptUnpublish); + scripts.scriptChange); } public update(properties: SchemaPropertiesDto, user: string, now?: DateTime): SchemaDetailsDto { @@ -200,8 +196,7 @@ export class SchemaDetailsDto extends SchemaDto { this.scriptCreate, this.scriptUpdate, this.scriptDelete, - this.scriptPublish, - this.scriptUnpublish); + this.scriptChange); } public addField(field: FieldDto, user: string, now?: DateTime): SchemaDetailsDto { @@ -218,8 +213,7 @@ export class SchemaDetailsDto extends SchemaDto { this.scriptCreate, this.scriptUpdate, this.scriptDelete, - this.scriptPublish, - this.scriptUnpublish); + this.scriptChange); } public updateField(field: FieldDto, user: string, now?: DateTime): SchemaDetailsDto { @@ -236,8 +230,7 @@ export class SchemaDetailsDto extends SchemaDto { this.scriptCreate, this.scriptUpdate, this.scriptDelete, - this.scriptPublish, - this.scriptUnpublish); + this.scriptChange); } public replaceFields(fields: FieldDto[], user: string, now?: DateTime): SchemaDetailsDto { @@ -254,8 +247,7 @@ export class SchemaDetailsDto extends SchemaDto { this.scriptCreate, this.scriptUpdate, this.scriptDelete, - this.scriptPublish, - this.scriptUnpublish); + this.scriptChange); } public removeField(field: FieldDto, user: string, now?: DateTime): SchemaDetailsDto { @@ -272,8 +264,7 @@ export class SchemaDetailsDto extends SchemaDto { this.scriptCreate, this.scriptUpdate, this.scriptDelete, - this.scriptPublish, - this.scriptUnpublish); + this.scriptChange); } } @@ -688,8 +679,7 @@ export class UpdateSchemaScriptsDto { public readonly scriptCreate?: string, public readonly scriptUpdate?: string, public readonly scriptDelete?: string, - public readonly scriptPublish?: string, - public readonly scriptUnpublish?: string + public readonly scriptChange?: string ) { } } @@ -764,8 +754,7 @@ export class SchemasService { response.scriptCreate, response.scriptUpdate, response.scriptDelete, - response.scriptPublish, - response.scriptUnpublish); + response.scriptChange); }) .catch(error => { if (error instanceof HttpErrorResponse && error.status === 404) { @@ -803,8 +792,7 @@ export class SchemasService { response.scriptCreate, response.scriptUpdate, response.scriptDelete, - response.scriptPublish, - response.scriptUnpublish); + response.scriptChange); }) .do(schema => { this.localCache.set(`schema.${appName}.${schema.id}`, schema, 5000); From 2640b6eed0668f55d8852311c4e935ae095d7873 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Sep 2017 18:46:30 +0200 Subject: [PATCH 08/10] Bugfixes --- .../pages/users/user-page.component.ts | 6 +- .../pages/content/content-page.component.html | 4 -- .../pages/content/content-page.component.ts | 41 +++++-------- .../pages/contents/search-form.component.html | 40 ++++++++----- .../pages/contents/search-form.component.scss | 7 +++ .../pages/contents/search-form.component.ts | 8 ++- .../app/shared/components/asset.component.ts | 3 +- .../shared/services/assets.service.spec.ts | 9 ++- .../app/shared/services/assets.service.ts | 15 +++-- .../shared/services/contents.service.spec.ts | 52 ++++++++++++++-- .../app/shared/services/contents.service.ts | 60 +++++++++++++++---- .../shared/services/schemas.service.spec.ts | 44 +++++++------- 12 files changed, 188 insertions(+), 101 deletions(-) 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 53400a0ef..eb8824a30 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 @@ -32,7 +32,6 @@ export class UserPageComponent extends ComponentBase implements OnInit { public currentUserId: string; public userFormSubmitted = false; public userForm: FormGroup; - public userId: string; public userFormError? = ''; public isCurrentUser = false; @@ -86,7 +85,7 @@ export class UserPageComponent extends ComponentBase implements OnInit { this.resetUserForm(error.displayMessage); }); } else { - this.userManagementService.putUser(this.userId, requestDto) + this.userManagementService.putUser(this.user.id, requestDto) .subscribe(() => { this.user = this.user.update( @@ -119,7 +118,6 @@ export class UserPageComponent extends ComponentBase implements OnInit { const input = this.user || {}; this.isNewMode = !this.user; - this.userId = input['id']; this.userForm = this.formBuilder.group({ email: [input['email'], @@ -143,7 +141,7 @@ export class UserPageComponent extends ComponentBase implements OnInit { ]] }); - this.isCurrentUser = this.userId === this.currentUserId; + this.isCurrentUser = this.user && this.user.id === this.currentUserId; this.resetUserForm(); } diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 707b90122..a75d7e44a 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -38,10 +38,6 @@
-
- You have not created the subscription. Therefore you cannot change the plan. -
-
diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 8645ebcfa..f0ffc00c6 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -40,15 +40,12 @@ import { export class ContentPageComponent extends AppComponentBase implements CanComponentDeactivate, OnDestroy, OnInit { private contentDeletedSubscription: Subscription; private contentVersionSelectedSubscription: Subscription; - private version = new Version(''); private content: ContentDto; public schema: SchemaDetailsDto; public contentFormSubmitted = false; public contentForm: FormGroup; - public contentData: any = null; - public contentId: string | null = null; public isNewMode = true; @@ -83,7 +80,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone this.contentDeletedSubscription = this.messageBus.of(ContentDeleted) .subscribe(message => { - if (message.content.id === this.contentId) { + if (this.content && message.content.id === this.content.id) { this.router.navigate(['../'], { relativeTo: this.route }); } }); @@ -124,7 +121,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone if (this.isNewMode) { this.appNameOnce() - .switchMap(app => this.contentsService.postContent(app, this.schema.name, requestDto, publish, this.version)) + .switchMap(app => this.contentsService.postContent(app, this.schema.name, requestDto, publish)) .subscribe(dto => { this.content = dto; @@ -137,7 +134,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone }); } else { this.appNameOnce() - .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId!, requestDto, this.version)) + .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.content.id, requestDto, this.content.version)) .subscribe(dto => { this.content = this.content.update(dto, this.authService.user!.token); @@ -158,7 +155,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone private loadVersion(version: number) { if (!this.isNewMode && this.content) { this.appNameOnce() - .switchMap(app => this.contentsService.getVersionData(app, this.schema.name, this.contentId!, new Version(version.toString()))) + .switchMap(app => this.contentsService.getVersionData(app, this.schema.name, this.content.id, new Version(version.toString()))) .subscribe(dto => { this.content = this.content.setData(dto); @@ -220,28 +217,20 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone private populateContentForm() { this.contentForm.markAsPristine(); - if (!this.content) { - this.contentData = null; - this.contentId = null; - this.isNewMode = true; - return; - } - - this.contentData = this.content.data; - this.contentId = this.content.id; - this.version = this.content.version; - this.isNewMode = false; + this.isNewMode = !this.content; - for (const field of this.schema.fields) { - const fieldValue = this.content.data[field.name] || {}; - const fieldForm = this.contentForm.get(field.name); + if (!this.isNewMode) { + for (const field of this.schema.fields) { + const fieldValue = this.content.data[field.name] || {}; + const fieldForm = this.contentForm.get(field.name); - if (field.partitioning === 'language') { - for (let language of this.languages) { - fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]); + if (field.partitioning === 'language') { + for (let language of this.languages) { + fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]); + } + } else { + fieldForm.controls['iv'].setValue(fieldValue['iv']); } - } else { - fieldForm.controls['iv'].setValue(fieldValue['iv']); } } } diff --git a/src/Squidex/app/features/content/pages/contents/search-form.component.html b/src/Squidex/app/features/content/pages/contents/search-form.component.html index 8430ffbb9..1f92fda29 100644 --- a/src/Squidex/app/features/content/pages/contents/search-form.component.html +++ b/src/Squidex/app/features/content/pages/contents/search-form.component.html @@ -1,27 +1,35 @@ -
-
- +
+
+
+ -
- +
+ +
-
-
- +
+ -
- +
+ +
-
-
- - -
- +
+ + +
+ +
+ +
+ +
@@ -82,6 +92,8 @@ [schema]="schema" (unpublishing)="unpublishContent(content)" (publishing)="publishContent(content)" + (archiving)="archiveContent(content)" + (restoring)="restoreContent(content)" (deleting)="deleteContent(content)"> diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index f914ca7da..617eb42b8 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -12,7 +12,7 @@ import { Subscription } from 'rxjs'; import { ContentCreated, - ContentDeleted, + ContentRemoved, ContentUpdated } from './../messages'; @@ -57,6 +57,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public languageParameter: string; public isReadOnly = false; + public isArchive = false; public columnWidth: number; @@ -112,13 +113,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy return { content, schemaId: this.schema.id }; } - public search() { - this.contentsQuery = this.contentsFilter.value; - this.contentsPager = new Pager(0); - - this.load(); - } - public publishContent(content: ContentDto) { this.appNameOnce() .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version)) @@ -139,14 +133,35 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy }); } + public archiveContent(content: ContentDto) { + this.appNameOnce() + .switchMap(app => this.contentsService.archiveContent(app, this.schema.name, content.id, content.version)) + .subscribe(() => { + content = content.archive(this.authService.user!.token); + + this.removeContent(content); + }, error => { + this.notifyError(error); + }); + } + + public restoreContent(content: ContentDto) { + this.appNameOnce() + .switchMap(app => this.contentsService.restoreContent(app, this.schema.name, content.id, content.version)) + .subscribe(() => { + content = content.restore(this.authService.user!.token); + + this.removeContent(content); + }, error => { + this.notifyError(error); + }); + } + public deleteContent(content: ContentDto) { this.appNameOnce() .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { - this.contentItems = this.contentItems.removeAll(x => x.id === content.id); - this.contentsPager = this.contentsPager.decrementCount(); - - this.emitContentDeleted(content); + this.removeContent(content); }, error => { this.notifyError(error); }); @@ -154,7 +169,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public load(showInfo = false) { this.appNameOnce() - .switchMap(app => this.contentsService.getContents(app, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery)) + .switchMap(app => this.contentsService.getContents(app, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, null, this.isArchive)) .subscribe(dtos => { this.contentItems = ImmutableArray.of(dtos.items); this.contentsPager = this.contentsPager.setCount(dtos.total); @@ -167,8 +182,22 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy }); } - public selectLanguage(language: AppLanguageDto) { - this.languageSelected = language; + public updateArchive(isArchive: boolean) { + this.contentsQuery = this.contentsFilter.value; + this.contentsPager = new Pager(0); + + this.isArchive = isArchive; + + this.searchModal.hide(); + + this.load(); + } + + public search() { + this.contentsQuery = this.contentsFilter.value; + this.contentsPager = new Pager(0); + + this.load(); } public goNext() { @@ -183,8 +212,12 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.load(); } - private emitContentDeleted(content: ContentDto) { - this.messageBus.emit(new ContentDeleted(content)); + public selectLanguage(language: AppLanguageDto) { + this.languageSelected = language; + } + + private emitContentRemoved(content: ContentDto) { + this.messageBus.emit(new ContentRemoved(content)); } private resetContents() { @@ -196,6 +229,13 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.loadFields(); } + private removeContent(content: ContentDto) { + this.contentItems = this.contentItems.removeAll(x => x.id === content.id); + this.contentsPager = this.contentsPager.decrementCount(); + + this.emitContentRemoved(content); + } + private loadFields() { this.contentFields = this.schema.fields.filter(x => x.properties.isListField); @@ -203,6 +243,10 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.contentFields = [this.schema.fields[0]]; } + if (this.contentFields.length === 0) { + this.contentFields = [{}]; + } + if (this.contentFields.length > 0) { this.columnWidth = 100 / this.contentFields.length; } else { diff --git a/src/Squidex/app/features/content/pages/contents/search-form.component.html b/src/Squidex/app/features/content/pages/contents/search-form.component.html index 1f92fda29..ee3ad5c71 100644 --- a/src/Squidex/app/features/content/pages/contents/search-form.component.html +++ b/src/Squidex/app/features/content/pages/contents/search-form.component.html @@ -25,7 +25,7 @@
-
+
diff --git a/src/Squidex/app/features/content/pages/contents/search-form.component.ts b/src/Squidex/app/features/content/pages/contents/search-form.component.ts index 2ba10aee8..8fcc6a0eb 100644 --- a/src/Squidex/app/features/content/pages/contents/search-form.component.ts +++ b/src/Squidex/app/features/content/pages/contents/search-form.component.ts @@ -28,6 +28,9 @@ export class SearchFormComponent implements OnChanges { @Output() public archivedChanged = new EventEmitter(); + @Input() + public canArchive = true; + public searchForm = this.formBuilder.group({ odataOrderBy: '', diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 13a340fcd..9834b95b2 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -21,7 +21,7 @@ export class ContentUpdated { } } -export class ContentDeleted { +export class ContentRemoved { constructor( public readonly content: ContentDto ) { diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index 35d158a1f..75d92db75 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -17,13 +17,19 @@