diff --git a/src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs b/src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs index b12dd85d9..c9c7b82ac 100644 --- a/src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs +++ b/src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs @@ -35,6 +35,8 @@ namespace Squidex.Core.Schemas.Json { public string Name; + public bool IsPublished; + public SchemaProperties Properties; public Dictionary Fields; @@ -52,7 +54,7 @@ namespace Squidex.Core.Schemas.Json public JToken Serialize(Schema schema) { - var model = new SchemaModel { Name = schema.Name, Properties = schema.Properties }; + var model = new SchemaModel { Name = schema.Name, IsPublished = schema.IsPublished, Properties = schema.Properties }; model.Fields = schema.Fields @@ -76,6 +78,11 @@ namespace Squidex.Core.Schemas.Json var schema = Schema.Create(model.Name, model.Properties); + if (model.IsPublished) + { + schema = schema.Publish(); + } + foreach (var kvp in model.Fields) { var fieldModel = kvp.Value; diff --git a/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs new file mode 100644 index 000000000..a4581fd13 --- /dev/null +++ b/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs @@ -0,0 +1,143 @@ +// ========================================================================== +// AppHistoryEventsCreator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; +using Squidex.Read.History; + +namespace Squidex.Read.Apps +{ + public class AppHistoryEventsCreator : IHistoryEventsCreator + { + private static readonly IReadOnlyDictionary TextsEN = + new Dictionary + { + { + TypeNameRegistry.GetName(), + "assigned {user:[Contributor]} as [Permission]" + }, + { + TypeNameRegistry.GetName(), + "removed {user:[Contributor]} from app" + }, + { + TypeNameRegistry.GetName(), + "added client {[Id]} to app" + }, + { + TypeNameRegistry.GetName(), + "revoked client {[Id]}" + }, + { + TypeNameRegistry.GetName(), + "named client {[Id]} as {[Name]}" + }, + { + TypeNameRegistry.GetName(), + "added language {[Language]}" + }, + { + TypeNameRegistry.GetName(), + "removed language {[Language]}" + }, + { + TypeNameRegistry.GetName(), + "changed master language to {[Language]}" + } + }; + + public IReadOnlyDictionary Texts + { + get { return TextsEN; } + } + + protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers) + { + const string channel = "settings.contributors"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Contributor", @event.ContributorId) + .AddParameter("Permission", @event.Permission.ToString())); + } + + protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers) + { + const string channel = "settings.contributors"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Contributor", @event.ContributorId)); + } + + protected Task On(AppClientRenamed @event, EnvelopeHeaders headers) + { + const string channel = "settings.clients"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Id", @event.Id) + .AddParameter("Name", !string.IsNullOrWhiteSpace(@event.Name) ? @event.Name : @event.Id)); + } + + protected Task On(AppClientAttached @event, EnvelopeHeaders headers) + { + const string channel = "settings.clients"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Id", @event.Id)); + } + + protected Task On(AppClientRevoked @event, EnvelopeHeaders headers) + { + const string channel = "settings.clients"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Id", @event.Id)); + } + + protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers) + { + const string channel = "settings.languages"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Language", @event.Language.EnglishName)); + } + + protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers) + { + const string channel = "settings.languages"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Language", @event.Language.EnglishName)); + } + + protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers) + { + const string channel = "settings.languages"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Language", @event.Language.EnglishName)); + } + + public Task CreateEventAsync(Envelope @event) + { + return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Read/History/HistoryEventToStore.cs b/src/Squidex.Read/History/HistoryEventToStore.cs new file mode 100644 index 000000000..be5e03613 --- /dev/null +++ b/src/Squidex.Read/History/HistoryEventToStore.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// HistoryEventToStore.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Read.History +{ + public class HistoryEventToStore + { + private readonly Dictionary parameters = new Dictionary(); + + public string Channel { get; } + + public string Message { get; } + + public IReadOnlyDictionary Parameters + { + get { return parameters; } + } + + public static HistoryEventToStore Create(IEvent @event, string channel) + { + Guard.NotNull(@event, nameof(@event)); + + return new HistoryEventToStore(channel, TypeNameRegistry.GetName(@event.GetType())); + } + + public HistoryEventToStore(string channel, string message) + { + Guard.NotNullOrEmpty(channel, nameof(channel)); + Guard.NotNullOrEmpty(message, nameof(message)); + + Channel = channel; + Message = message; + } + + public HistoryEventToStore AddParameter(string key, string value) + { + parameters[key] = value; + + return this; + } + } +} diff --git a/src/Squidex.Read/History/IHistoryEventsCreator.cs b/src/Squidex.Read/History/IHistoryEventsCreator.cs new file mode 100644 index 000000000..bae86c240 --- /dev/null +++ b/src/Squidex.Read/History/IHistoryEventsCreator.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// IHistoryEventCreator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Read.History +{ + public interface IHistoryEventsCreator + { + IReadOnlyDictionary Texts { get; } + + Task CreateEventAsync(Envelope @event); + } +} diff --git a/src/Squidex.Read/Schemas/Repositories/ISchemaEntity.cs b/src/Squidex.Read/Schemas/Repositories/ISchemaEntity.cs index 55efd3565..bf3c1a43f 100644 --- a/src/Squidex.Read/Schemas/Repositories/ISchemaEntity.cs +++ b/src/Squidex.Read/Schemas/Repositories/ISchemaEntity.cs @@ -11,6 +11,8 @@ namespace Squidex.Read.Schemas.Repositories { string Name { get; } + string Label { get; } + bool IsPublished { get; } } } diff --git a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs new file mode 100644 index 000000000..906fb8e7e --- /dev/null +++ b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// AppHistoryEventsCreator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Events; +using Squidex.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; +using Squidex.Read.History; +using Squidex.Read.Schemas.Services; + +namespace Squidex.Read.Schemas +{ + public class SchemaHistoryEventsCreator : IHistoryEventsCreator + { + private readonly ISchemaProvider schemaProvider; + + private static readonly IReadOnlyDictionary TextsEN = + new Dictionary + { + { + TypeNameRegistry.GetName(), + "created schema {[Name]}" + }, + { + TypeNameRegistry.GetName(), + "updated schema {[Name]}" + }, + { + TypeNameRegistry.GetName(), + "published schema {[Name]}" + }, + { + TypeNameRegistry.GetName(), + "unpublished schema {[Name]}" + } + }; + + public SchemaHistoryEventsCreator(ISchemaProvider schemaProvider) + { + this.schemaProvider = schemaProvider; + } + + public IReadOnlyDictionary Texts + { + get { return TextsEN; } + } + + protected Task On(SchemaCreated @event, EnvelopeHeaders headers) + { + var name = @event.Name; + + string channel = $"schemas.{name}"; + + return Task.FromResult( + HistoryEventToStore.Create(@event, channel) + .AddParameter("Name", name)); + } + + protected async Task On(SchemaUpdated @event, EnvelopeHeaders headers) + { + var name = await FindSchemaNameAsync(headers); + + string channel = $"schemas.{name}"; + + return + HistoryEventToStore.Create(@event, channel) + .AddParameter("Name", name); + } + + protected async Task On(SchemaPublished @event, EnvelopeHeaders headers) + { + var name = await FindSchemaNameAsync(headers); + + string channel = $"schemas.{name}"; + + return + HistoryEventToStore.Create(@event, channel) + .AddParameter("Name", name); + } + + protected async Task On(SchemaUnpublished @event, EnvelopeHeaders headers) + { + var name = await FindSchemaNameAsync(headers); + + string channel = $"schemas.{name}"; + + return + HistoryEventToStore.Create(@event, channel) + .AddParameter("Name", name); + } + + public Task CreateEventAsync(Envelope @event) + { + return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null); + } + + private Task FindSchemaNameAsync(EnvelopeHeaders headers) + { + var name = schemaProvider.FindSchemaNameByIdAsync(headers.AppId(), headers.AggregateId()); + + return name; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs b/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs index 2a90dd3fc..b0896ed60 100644 --- a/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs @@ -13,6 +13,8 @@ namespace Squidex.Read.Schemas.Services { public interface ISchemaProvider { + Task FindSchemaNameByIdAsync(Guid schemaId); + Task FindSchemaIdByNameAsync(Guid appId, string name); } } diff --git a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs index d9a875fff..45a25642a 100644 --- a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs @@ -28,7 +28,9 @@ namespace Squidex.Read.Schemas.Services.Implementations private sealed class CacheItem { - public ISchemaEntityWithSchema Entity; + public Guid? Id; + + public string Name; } public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository) @@ -39,61 +41,78 @@ namespace Squidex.Read.Schemas.Services.Implementations this.repository = repository; } + public async Task FindSchemaNameByIdAsync(Guid schemaId) + { + var cacheKey = BuildIdCacheKey(schemaId); + var cacheItem = Cache.Get(cacheKey); + + if (cacheItem == null) + { + var schema = await repository.FindSchemaAsync(schemaId); + + cacheItem = new CacheItem { Id = schema?.Id, Name = schema?.Name }; + + Cache.Set(cacheKey, cacheItem, CacheDuration); + + if (cacheItem.Id != null) + { + Cache.Set(BuildIdCacheKey(cacheItem.Id.Value), cacheItem, CacheDuration); + } + } + + return cacheItem.Name; + } + public async Task FindSchemaIdByNameAsync(Guid appId, string name) { Guard.NotNullOrEmpty(name, nameof(name)); - var cacheKey = BuildModelCacheKey(appId, name); + var cacheKey = BuildNameCacheKey(appId, name); var cacheItem = Cache.Get(cacheKey); if (cacheItem == null) { var schema = await repository.FindSchemaAsync(appId, name); - cacheItem = new CacheItem { Entity = schema }; + cacheItem = new CacheItem { Id = schema?.Id, Name = schema?.Name }; Cache.Set(cacheKey, cacheItem, CacheDuration); - if (cacheItem.Entity != null) + if (cacheItem.Id != null) { - Cache.Set(BuildNamesCacheKey(cacheItem.Entity.Id), cacheItem.Entity.Name, CacheDuration); + Cache.Set(BuildIdCacheKey(cacheItem.Id.Value), cacheItem, CacheDuration); } } - return cacheItem.Entity?.Id; + return cacheItem.Id; } public Task On(Envelope @event) { - if (@event.Payload is SchemaUpdated || - @event.Payload is SchemaDeleted) + if (@event.Payload is SchemaDeleted || + @event.Payload is SchemaCreated) { - var oldName = Cache.Get(BuildNamesCacheKey(@event.Headers.AggregateId())); + var cacheKey = BuildIdCacheKey(@event.Headers.AggregateId()); - if (oldName != null) - { - Cache.Remove(BuildModelCacheKey(@event.Headers.AppId(), oldName)); - } - } - else - { - var schemaCreated = @event.Payload as SchemaCreated; + var cacheItem = Cache.Get(cacheKey); - if (schemaCreated != null) + if (cacheItem.Name != null) { - Cache.Remove(BuildModelCacheKey(@event.Headers.AppId(), schemaCreated.Name)); + Cache.Remove(BuildNameCacheKey(@event.Headers.AppId(), cacheItem.Name)); } + + Cache.Remove(cacheKey); } return Task.FromResult(true); } - private static string BuildModelCacheKey(Guid appId, string name) + private static string BuildNameCacheKey(Guid appId, string name) { - return $"Schema_{appId}_{name}"; + return $"Schema_Ids_{appId}_{name}"; } - private static string BuildNamesCacheKey(Guid schemaId) + private static string BuildIdCacheKey(Guid schemaId) { return $"Schema_Names_{schemaId}"; } diff --git a/src/Squidex.Store.MongoDb/History/MessagesEN.cs b/src/Squidex.Store.MongoDb/History/MessagesEN.cs deleted file mode 100644 index 54d8ffd02..000000000 --- a/src/Squidex.Store.MongoDb/History/MessagesEN.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// MessagesEN.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Events.Apps; -using Squidex.Infrastructure; - -namespace Squidex.Store.MongoDb.History -{ - public static class MessagesEN - { - public static readonly IReadOnlyDictionary Texts = - new Dictionary - { - { - TypeNameRegistry.GetName(), - "assigned {user:[Contributor]} as [Permission]" - }, - { - TypeNameRegistry.GetName(), - "removed {user:[Contributor]} from app" - }, - { - TypeNameRegistry.GetName(), - "added client {[Id]} to app" - }, - { - TypeNameRegistry.GetName(), - "revoked client {[Id]}" - }, - { - TypeNameRegistry.GetName(), - "named client {[Id]} as {[Name]}" - }, - { - TypeNameRegistry.GetName(), - "added language {[Language]}" - }, - { - TypeNameRegistry.GetName(), - "removed language {[Language]}" - }, - { - TypeNameRegistry.GetName(), - "changed master language to {[Language]}" - } - }; - } -} diff --git a/src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs index 39b4cf991..3f7d650a2 100644 --- a/src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs @@ -8,25 +8,35 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using MongoDB.Driver; -using Squidex.Events.Apps; using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.MongoDb; using Squidex.Read.History; using Squidex.Read.History.Repositories; using Squidex.Store.MongoDb.Utils; -using System.Linq; -using Squidex.Infrastructure.MongoDb; namespace Squidex.Store.MongoDb.History { public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository, ICatchEventConsumer { - public MongoHistoryEventRepository(IMongoDatabase database) + private readonly List creators; + private readonly Dictionary texts = new Dictionary(); + + public MongoHistoryEventRepository(IMongoDatabase database, IEnumerable creators) : base(database) { + this.creators = creators.ToList(); + + foreach (var creator in this.creators) + { + foreach (var text in creator.Texts) + { + texts[text.Key] = text.Value; + } + } } protected override string CollectionName() @@ -47,102 +57,26 @@ namespace Squidex.Store.MongoDb.History var entities = await Collection.Find(x => x.AppId == appId && x.Channel.StartsWith(channelPrefix)).SortByDescending(x => x.Created).Limit(count).ToListAsync(); - return entities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, MessagesEN.Texts)).ToList(); - } - - protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(headers, x => - { - const string channel = "settings.contributors"; - - x.Setup(headers, channel) - .AddParameter("Contributor", @event.ContributorId) - .AddParameter("Permission", @event.Permission.ToString()); - }, false); - } - - protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(headers, x => - { - const string channel = "settings.contributors"; - - x.Setup(headers, channel) - .AddParameter("Contributor", @event.ContributorId); - }, false); - } - - protected Task On(AppClientRenamed @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(headers, x => - { - const string channel = "settings.clients"; - - x.Setup(headers, channel) - .AddParameter("Id", @event.Id) - .AddParameter("Name", !string.IsNullOrWhiteSpace(@event.Name) ? @event.Name : @event.Id); - }, false); - } - - protected Task On(AppClientAttached @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(headers, x => - { - const string channel = "settings.clients"; - - x.Setup(headers, channel) - .AddParameter("Id", @event.Id); - }, false); - } - - protected Task On(AppClientRevoked @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(headers, x => - { - const string channel = "settings.clients"; - - x.Setup(headers, channel) - .AddParameter("Id", @event.Id); - }, false); + return entities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); } - protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers) + public async Task On(Envelope @event) { - return Collection.CreateAsync(headers, x => + foreach (var creator in creators) { - const string channel = "settings.languages"; - - x.Setup(headers, channel) - .AddParameter("Language", @event.Language.EnglishName); - }, false); - } - - protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(headers, x => - { - const string channel = "settings.languages"; - - x.Setup(headers, channel) - .AddParameter("Language", @event.Language.EnglishName); - }, false); - } - - protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(headers, x => - { - const string channel = "settings.languages"; - - x.Setup(headers, channel) - .AddParameter("Language", @event.Language.EnglishName); - }, false); - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); + var message = await creator.CreateEventAsync(@event); + + if (message != null) + { + await Collection.CreateAsync(@event.Headers, x => + { + x.Channel = message.Channel; + x.Message = message.Message; + + x.Parameters = message.Parameters.ToDictionary(p => p.Key, p => p.Value); + }, false); + } + } } } } diff --git a/src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs index 30886a40c..e7c489c6c 100644 --- a/src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs @@ -26,6 +26,10 @@ namespace Squidex.Store.MongoDb.Schemas [BsonElement] public string Name { get; set; } + [BsonRequired] + [BsonElement] + public string Label { get; set; } + [BsonRequired] [BsonElement] public Guid AppId { get; set; } @@ -46,7 +50,7 @@ namespace Squidex.Store.MongoDb.Schemas [BsonElement] public BsonDocument Schema { get; set; } - [BsonIgnoreIfDefault] + [BsonRequired] [BsonElement] public bool IsPublished { get; set; } diff --git a/src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs index 0a1b5b80b..50a2b7dda 100644 --- a/src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs @@ -136,7 +136,7 @@ namespace Squidex.Store.MongoDb.Schemas protected Task On(SchemaUnpublished @event, EnvelopeHeaders headers) { - return UpdateSchema(headers, s => s.Publish()); + return UpdateSchema(headers, s => s.Unpublish()); } protected Task On(FieldAdded @event, EnvelopeHeaders headers) @@ -171,6 +171,7 @@ namespace Squidex.Store.MongoDb.Schemas Serialize(entity, currentSchema); + entity.Label = currentSchema.Properties.Label; entity.IsPublished = currentSchema.IsPublished; } diff --git a/src/Squidex.Store.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Store.MongoDb/Utils/EntityMapper.cs index 5cb1bc027..33b2264ce 100644 --- a/src/Squidex.Store.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Store.MongoDb/Utils/EntityMapper.cs @@ -117,6 +117,15 @@ namespace Squidex.Store.MongoDb.Utils return collection.InsertOneIfNotExistsAsync(entity); } + public static async Task CreateAsync(this IMongoCollection collection, EnvelopeHeaders headers, Func updater, bool useAggregateId = true) where T : MongoEntity, new() + { + var entity = Create(headers, useAggregateId); + + await updater(entity); + + await collection.InsertOneIfNotExistsAsync(entity); + } + public static async Task UpdateAsync(this IMongoCollection collection, EnvelopeHeaders headers, Action updater) where T : MongoEntity { var entity = await collection.Find(t => t.Id == headers.AggregateId()).FirstOrDefaultAsync(); diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs index 4204b31ec..978f79fb1 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs @@ -26,6 +26,12 @@ namespace Squidex.Controllers.Api.Schemas.Models [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] public string Name { get; set; } + /// + /// Optional label for the editor. + /// + [StringLength(100)] + public string Label { get; set; } + /// /// Indicates if the schema is published. /// diff --git a/src/Squidex/Properties/launchSettings.json b/src/Squidex/Properties/launchSettings.json index ba7b5eacf..1aa5504bd 100644 --- a/src/Squidex/Properties/launchSettings.json +++ b/src/Squidex/Properties/launchSettings.json @@ -16,7 +16,6 @@ }, "Squidex": { "commandName": "Project", - "launchBrowser": true, "launchUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index 0e79b9491..626ae5181 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -18,22 +18,23 @@ - + + + \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/src/Squidex/app/features/apps/pages/apps-page.component.ts index d5dc29e61..bf4cfe374 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.ts +++ b/src/Squidex/app/features/apps/pages/apps-page.component.ts @@ -11,13 +11,17 @@ import { Subscription } from 'rxjs'; import { AppDto, AppsStoreService, + fadeAnimation, ModalView } from 'shared'; @Component({ selector: 'sqx-apps-page', styleUrls: ['./apps-page.component.scss'], - templateUrl: './apps-page.component.html' + templateUrl: './apps-page.component.html', + animations: [ + fadeAnimation + ] }) export class AppsPageComponent implements OnInit, OnDestroy { private appsSubscription: Subscription; diff --git a/src/Squidex/app/features/schemas/pages/messages.ts b/src/Squidex/app/features/schemas/pages/messages.ts new file mode 100644 index 000000000..fd0aa058c --- /dev/null +++ b/src/Squidex/app/features/schemas/pages/messages.ts @@ -0,0 +1,14 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export class SchemaUpdated { + constructor( + public readonly name: string, + public readonly isPublished: boolean + ) { + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.html b/src/Squidex/app/features/schemas/pages/schema/field.component.html index b5269fa8d..199288676 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.html @@ -70,6 +70,18 @@
+
+ + +
+ + + + The name of the field in the API response. + +
+
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 7cad0e4b3..a83df5ca4 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -3,6 +3,17 @@
+
+
+ + +
+
+

{{schemaName}}

diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss index d7893c195..d85043aa8 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss @@ -3,9 +3,16 @@ .panel { min-width: 760px; - max-width: 700px; + max-width: 760px; } .panel-content { overflow-y: scroll; +} + +.btn-publishing { + &.disabled, + &:disabled { + @include opacity(1); + } } \ No newline at end of file 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 536b0ff37..3b9ff0398 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 @@ -15,6 +15,7 @@ import { AppComponentBase, AppsStoreService, createProperties, + fadeAnimation, FieldDto, HistoryChannelUpdated, ImmutableArray, @@ -25,10 +26,15 @@ import { UsersProviderService } from 'shared'; +import { SchemaUpdated } from './../messages'; + @Component({ selector: 'sqx-schema-page', styleUrls: ['./schema-page.component.scss'], - templateUrl: './schema-page.component.html' + templateUrl: './schema-page.component.html', + animations: [ + fadeAnimation + ] }) export class SchemaPageComponent extends AppComponentBase implements OnDestroy, OnInit { private routerSubscription: Subscription; @@ -42,6 +48,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, public schemaName: string; public schemaFields = ImmutableArray.empty(); + public isPublished: boolean; + public addFieldForm: FormGroup = this.formBuilder.group({ type: ['string', @@ -84,6 +92,29 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, .switchMap(app => this.schemasService.getSchema(app, this.schemaName)).retry(2) .subscribe(dto => { this.schemaFields = ImmutableArray.of(dto.fields); + this.isPublished = dto.isPublished; + }, error => { + this.notifyError(error); + }); + } + + public publish() { + this.appName() + .switchMap(app => this.schemasService.publishSchema(app, this.schemaName)).retry(2) + .subscribe(() => { + this.isPublished = true; + this.updateAll(this.schemaFields); + }, error => { + this.notifyError(error); + }); + } + + public unpublish() { + this.appName() + .switchMap(app => this.schemasService.unpublishSchema(app, this.schemaName)).retry(2) + .subscribe(() => { + this.isPublished = false; + this.updateAll(this.schemaFields); }, error => { this.notifyError(error); }); @@ -133,7 +164,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, this.appName() .switchMap(app => this.schemasService.deleteField(app, this.schemaName, field.fieldId)).retry(2) .subscribe(() => { - this.updateFields(this.schemaFields.remove(field)); + this.updateAll(this.schemaFields.remove(field)); }, error => { this.notifyError(error); }); @@ -176,7 +207,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, false, properties); - this.updateFields(this.schemaFields.push(newField)); + this.updateAll(this.schemaFields.push(newField)); reset(); }, error => { this.notifyError(error); @@ -190,13 +221,14 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, } public updateField(field: FieldDto, newField: FieldDto) { - this.updateFields(this.schemaFields.replace(field, newField)); + this.updateAll(this.schemaFields.replace(field, newField)); } - private updateFields(fields: ImmutableArray) { + private updateAll(fields: ImmutableArray) { this.schemaFields = fields; this.messageBus.publish(new HistoryChannelUpdated()); + this.messageBus.publish(new SchemaUpdated(this.schemaName, this.isPublished)); } } 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 34560922f..2a809f150 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 @@ -82,7 +82,7 @@ export class SchemaFormComponent implements OnInit { this.schemas.postSchema(this.appName, requestDto) .subscribe(dto => { this.createForm.reset(); - this.created.emit(new SchemaDto(dto.id, name, now, now, me, me)); + this.created.emit(new SchemaDto(dto.id, name, now, now, me, me, false)); }, error => { this.reset(); this.creationError = error.displayMessage; diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html index e3de6a862..3d0c72af5 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html @@ -38,6 +38,8 @@
{{schema.lastModified | fromNow}} + +
@@ -48,24 +50,25 @@
+
+ \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss index 04b3f2673..2682cb9f3 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss @@ -2,8 +2,8 @@ @import '_mixins'; .panel { - min-width: 450px; - max-width: 450px; + min-width: 480px; + max-width: 480px; } .panel-header { @@ -141,6 +141,20 @@ font-weight: normal; } + &-published { + & { + @include circle(.5rem); + display: inline-block; + border: 0; + background: $color-theme-green; + margin-left: .4rem; + } + + &.unpublished { + background: $color-theme-error; + } + } + &-user { & { @include border-radius(1px); diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index 073793139..92433db79 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -5,15 +5,18 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Component } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { AppComponentBase, AppsStoreService, + AuthService, + DateTime, fadeAnimation, ImmutableArray, + MessageBus, ModalView, NotificationService, SchemaDto, @@ -21,6 +24,8 @@ import { UsersProviderService } from 'shared'; +import { SchemaUpdated } from './../messages'; + @Component({ selector: 'sqx-schemas-page', styleUrls: ['./schemas-page.component.scss'], @@ -29,7 +34,9 @@ import { fadeAnimation ] }) -export class SchemasPageComponent extends AppComponentBase { +export class SchemasPageComponent extends AppComponentBase implements OnDestroy, OnInit { + private messageSubscription: Subscription; + public modalDialog = new ModalView(); public schemas = new BehaviorSubject(ImmutableArray.empty()); @@ -57,13 +64,39 @@ export class SchemasPageComponent extends AppComponentBase { }); constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, - private readonly schemasService: SchemasService + private readonly schemasService: SchemasService, + private readonly messageBus: MessageBus, + private readonly authService: AuthService ) { super(apps, notifications, users); } public ngOnInit() { this.load(); + + this.messageSubscription = + this.messageBus.of(SchemaUpdated).subscribe(message => { + const schemas = this.schemas.value; + const oldSchema = schemas.find(i => i.name === message.name); + + if (oldSchema) { + const me = `subject:${this.authService.user.id}`; + + const newSchema = + new SchemaDto( + oldSchema.id, + oldSchema.name, + oldSchema.created, + DateTime.now(), + oldSchema.createdBy, me, + message.isPublished); + this.schemas.next(schemas.replace(oldSchema, newSchema)); + } + }); + } + + public ngOnDestroy() { + this.messageSubscription.unsubscribe(); } public load() { diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html index 090bf9dcd..992ec4ce2 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.html +++ b/src/Squidex/app/features/settings/pages/clients/client.component.html @@ -76,19 +76,20 @@ + + \ No newline at end of file diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 63d9be8e1..d773fd536 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -57,7 +57,8 @@ export class SchemaDto { public readonly created: DateTime, public readonly lastModified: DateTime, public readonly createdBy: string, - public readonly lastModifiedBy: string + public readonly lastModifiedBy: string, + public readonly isPublished: boolean ) { } } @@ -70,7 +71,10 @@ export class SchemaDetailsDto { public readonly lastModified: DateTime, public readonly createdBy: string, public readonly lastModifiedBy: string, - public readonly fields: FieldDto[] + public readonly fields: FieldDto[], + public readonly label: string, + public readonly hints: string, + public readonly isPublished: boolean ) { } } @@ -190,7 +194,8 @@ export class SchemasService { DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.lastModified), item.createdBy, - item.lastModifiedBy); + item.lastModifiedBy, + item.isPublished); }); }) .catch(response => handleError('Failed to load schemas. Please reload.', response)); @@ -223,7 +228,10 @@ export class SchemasService { DateTime.parseISO_UTC(response.lastModified), response.createdBy, response.lastModifiedBy, - fields); + fields, + response.label, + response.hints, + response.isPublished); }) .catch(response => handleError('Failed to load schema. Please reload.', response)); } @@ -250,6 +258,27 @@ export class SchemasService { .catch(response => handleError('Failed to add field. Please reload.', response)); } + public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/`); + + return this.authService.authPut(url, dto) + .catch(response => handleError('Failed to update schema. Please reload.', response)); + } + + public publishSchema(appName: string, schemaName: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish/`); + + return this.authService.authPut(url, {}) + .catch(response => handleError('Failed to publish schema. Please reload.', response)); + } + + public unpublishSchema(appName: string, schemaName: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish/`); + + return this.authService.authPut(url, {}) + .catch(response => handleError('Failed to unpublish schema. Please reload.', response)); + } + public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/`); diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.html b/src/Squidex/app/shell/pages/internal/apps-menu.component.html index 092f8bd09..b8ddeddf0 100644 --- a/src/Squidex/app/shell/pages/internal/apps-menu.component.html +++ b/src/Squidex/app/shell/pages/internal/apps-menu.component.html @@ -24,21 +24,22 @@ + + + \ No newline at end of file diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index 443e22218..9b1b52985 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -204,6 +204,10 @@ border: 0; } + &-backdrop { + @include opacity(.5); + } + &-header { @include border-radius-top(.3rem); background: $color-modal-header-background; @@ -231,9 +235,14 @@ &-content { @include box-shadow(0, 6px, 16px, .4); + @include border-radiusn(.4rem, .35rem, .35rem, .4rem); } &-dialog { + & { + z-index: 1100; + } + @media (min-width: 576px) { margin-top: 70px; } diff --git a/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs b/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs index 08f77c0dd..39577e06c 100644 --- a/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs @@ -34,8 +34,10 @@ namespace Squidex.Core.Schemas.Json Schema.Create("my-schema", new SchemaProperties()) .AddOrUpdateField(new StringField(1, "field1", new StringFieldProperties { Label = "Field1", Pattern = "[0-9]{3}" })) .AddOrUpdateField(new NumberField(2, "field2", new NumberFieldProperties { Hints = "Hints" })) - .DisableField(1) - .HideField(2); + .AddOrUpdateField(new BooleanField(2, "field2", new BooleanFieldProperties())) + .Publish() + .HideField(2) + .DisableField(1); var sut = new SchemaJsonSerializer(new FieldRegistry(), serializerSettings);