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/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.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.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 97112ef82..d14127ab8 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 IsArchived { get; set; } + [BsonRequired] [BsonElement("dt")] public string DataText { get; set; } 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 2c3ef026e..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,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.IsArchived)); await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); }); } @@ -114,17 +115,25 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents }); } - protected Task On(ContentDeleted @event, EnvelopeHeaders headers) + protected Task On(ContentArchived @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)); + return collection.UpdateAsync(@event, headers, x => + { + x.IsArchived = true; + }); + }); + } - await collection.DeleteOneAsync(x => x.Id == headers.AggregateId()); + protected Task On(ContentRestored @event, EnvelopeHeaders headers) + { + return ForAppIdAsync(@event.AppId.Id, collection => + { + return collection.UpdateAsync(@event, headers, x => + { + x.IsArchived = false; + }); }); } @@ -140,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 c6fa3880a..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,18 +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.SchemaId, schemaId), + Filter.Eq(x => x.IsArchived, archived) }; 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..15610b5c8 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).ThenByDescending(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.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/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..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,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 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 4e1f355a5..9f0e7265c 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 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/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.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/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.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..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,20 +129,46 @@ 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(ArchiveContent command, CommandContext context) + { + return handler.UpdateAsync(context, async content => + { + var schemaAndApp = await ResolveSchemaAndAppAsync(command); + var scriptContext = CreateScriptContext(content, command, "Archive"); + + scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "archive content"); + + content.Archive(command); + }); + } + + protected Task On(RestoreContent command, CommandContext context) + { + 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); + var scriptContext = CreateScriptContext(content, command, "Delete"); scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptDelete, "delete content"); @@ -192,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 df6ae3a99..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,6 +73,16 @@ namespace Squidex.Domain.Apps.Write.Contents isPublished = false; } + protected void On(ContentArchived @event) + { + isArchived = true; + } + + protected void On(ContentRestored @event) + { + isArchived = false; + } + protected void On(ContentDeleted @event) { isDeleted = true; @@ -99,6 +115,28 @@ namespace Squidex.Domain.Apps.Write.Contents return this; } + public ContentDomainObject Restore(RestoreContent command) + { + Guard.NotNull(command, nameof(command)); + + 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)); @@ -159,6 +197,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.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.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.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/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/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/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/Api/Schemas/SchemasController.cs b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs index 916706ffd..d3806c93f 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs @@ -125,7 +125,7 @@ namespace Squidex.Controllers.Api.Schemas var context = await CommandBus.PublishAsync(command); var result = context.Result>(); - var response = new EntityCreatedDto { Id = command.Name, Version = result.Version }; + var response = new EntityCreatedDto { Id = command.SchemaId.ToString(), Version = result.Version }; return CreatedAtAction(nameof(GetSchema), new { name = request.Name }, response); } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 2484c7028..a02fc529a 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; } @@ -64,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(); @@ -81,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 { @@ -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}/")] @@ -208,6 +228,36 @@ 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")] + [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..e8ab4715f 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs @@ -94,6 +94,8 @@ namespace Squidex.Controllers.ContentApi.Generator GenerateSchemaPatchOperation(), GenerateSchemaPublishOperation(), GenerateSchemaUnpublishOperation(), + GenerateSchemaArchiveOperation(), + GenerateSchemaRestoreOperation(), GenerateSchemaDeleteOperation() }; @@ -109,6 +111,7 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Query{schemaKey}Contents"; operation.Summary = $"Queries {schemaName} contents."; + operation.Security = ReaderSecurity; operation.Description = SchemaQueryDescription; @@ -119,8 +122,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 +131,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 +143,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 +158,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; + operation.AddResponse("201", $"{schemaName} item updated.", dataSchema); }); } @@ -174,12 +172,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; + operation.AddResponse("201", $"{schemaName} item patched.", dataSchema); }); } @@ -189,10 +186,9 @@ namespace Squidex.Controllers.ContentApi.Generator { operation.OperationId = $"Publish{schemaKey}Content"; operation.Summary = $"Publish a {schemaName} content."; - - operation.AddResponse("204", $"{schemaName} element published."); - operation.Security = EditorSecurity; + + operation.AddResponse("204", $"{schemaName} item published."); }); } @@ -202,10 +198,33 @@ 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."); + 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."); + }); + } + + 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} item restored."); }); } @@ -215,10 +234,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..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,20 +42,25 @@ 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 publihed. + /// Indicates if the content item is published. /// public bool? IsPublished { get; set; } + /// + /// Indicates if the content item is archived. + /// + public bool IsArchived { get; set; } + /// /// The version of the content. /// 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/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..cd7db5ce1 --- /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..5a7547e6b --- /dev/null +++ b/src/Squidex/app/features/content/pages/content/content-history.component.ts @@ -0,0 +1,107 @@ +/* + * 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'; + +import { ContentVersionSelected } from './../messages'; + +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 loadVersion(version: number) { + this.messageBus.emit(new ContentVersionSelected(version)); + } + + 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/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index a75d7e44a..a6cf09326 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 @@ -4,7 +4,7 @@
-
+
-
- + +

New Content

-

+

Edit Content

+

+ Show Content +

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..0d1fff1b8 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 @@ -12,8 +12,9 @@ import { Observable, Subscription } from 'rxjs'; import { ContentCreated, - ContentDeleted, - ContentUpdated + ContentRemoved, + ContentUpdated, + ContentVersionSelected } from './../messages'; import { @@ -38,15 +39,13 @@ import { }) export class ContentPageComponent extends AppComponentBase implements CanComponentDeactivate, OnDestroy, OnInit { private contentDeletedSubscription: Subscription; - private version = new Version(''); - private content: ContentDto; + private contentVersionSelectedSubscription: Subscription; public schema: SchemaDetailsDto; + public content: ContentDto; public contentFormSubmitted = false; public contentForm: FormGroup; - public contentData: any = null; - public contentId: string | null = null; public isNewMode = true; @@ -63,6 +62,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone } public ngOnDestroy() { + this.contentVersionSelectedSubscription.unsubscribe(); this.contentDeletedSubscription.unsubscribe(); } @@ -71,10 +71,16 @@ 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) + this.messageBus.of(ContentRemoved) .subscribe(message => { - if (message.content.id === this.contentId) { + if (this.content && message.content.id === this.content.id) { this.router.navigate(['../'], { relativeTo: this.route }); } }); @@ -115,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; @@ -128,9 +134,9 @@ 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); + this.content = this.content.update(dto, this.authService.user!.token); this.emitContentUpdated(this.content); this.notifyInfo('Content saved successfully.'); @@ -146,6 +152,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.content.id, 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 }); } @@ -195,28 +217,24 @@ 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']); + } + + if (this.content.isArchived) { + this.contentForm.disable(); } } } diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index 404cdd0b3..0658e4ad9 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -21,7 +21,13 @@ @@ -33,12 +39,16 @@
-

+

Contents

+ +

+ Archive +

- References + Refs

@@ -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 998747c61..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,18 +113,11 @@ 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)) .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 +127,31 @@ 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); + }); + } + + 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); }); @@ -143,10 +161,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy 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 8430ffbb9..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 @@ -1,27 +1,35 @@ -
-
- +
+
+
+ -
- +
+ +
-
-
- +
+ -
- +
+ +
-
-
- - -
- +
+ + +
+ +
+ +
+ +