Browse Source

Temporary commit

pull/1/head
Sebastian 9 years ago
parent
commit
ddd171974c
  1. 9
      src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs
  2. 143
      src/Squidex.Read/Apps/AppHistoryEventsCreator.cs
  3. 51
      src/Squidex.Read/History/HistoryEventToStore.cs
  4. 22
      src/Squidex.Read/History/IHistoryEventsCreator.cs
  5. 2
      src/Squidex.Read/Schemas/Repositories/ISchemaEntity.cs
  6. 113
      src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs
  7. 2
      src/Squidex.Read/Schemas/Services/ISchemaProvider.cs
  8. 63
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  9. 54
      src/Squidex.Store.MongoDb/History/MessagesEN.cs
  10. 128
      src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs
  11. 6
      src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs
  12. 3
      src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs
  13. 9
      src/Squidex.Store.MongoDb/Utils/EntityMapper.cs
  14. 6
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs
  15. 1
      src/Squidex/Properties/launchSettings.json
  16. 25
      src/Squidex/app/features/apps/pages/apps-page.component.html
  17. 6
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  18. 14
      src/Squidex/app/features/schemas/pages/messages.ts
  19. 12
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  20. 11
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  21. 9
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss
  22. 42
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  23. 2
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  24. 23
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  25. 18
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss
  26. 41
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  27. 21
      src/Squidex/app/features/settings/pages/clients/client.component.html
  28. 37
      src/Squidex/app/shared/services/schemas.service.ts
  29. 23
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  30. 9
      src/Squidex/app/theme/_bootstrap.scss
  31. 6
      tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs

9
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<long, FieldModel> 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;

143
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<string, string> TextsEN =
new Dictionary<string, string>
{
{
TypeNameRegistry.GetName<AppContributorAssigned>(),
"assigned {user:[Contributor]} as [Permission]"
},
{
TypeNameRegistry.GetName<AppContributorRemoved>(),
"removed {user:[Contributor]} from app"
},
{
TypeNameRegistry.GetName<AppClientAttached>(),
"added client {[Id]} to app"
},
{
TypeNameRegistry.GetName<AppClientRevoked>(),
"revoked client {[Id]}"
},
{
TypeNameRegistry.GetName<AppClientRenamed>(),
"named client {[Id]} as {[Name]}"
},
{
TypeNameRegistry.GetName<AppLanguageAdded>(),
"added language {[Language]}"
},
{
TypeNameRegistry.GetName<AppLanguageRemoved>(),
"removed language {[Language]}"
},
{
TypeNameRegistry.GetName<AppMasterLanguageSet>(),
"changed master language to {[Language]}"
}
};
public IReadOnlyDictionary<string, string> Texts
{
get { return TextsEN; }
}
protected Task<HistoryEventToStore> 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<HistoryEventToStore> On(AppContributorRemoved @event, EnvelopeHeaders headers)
{
const string channel = "settings.contributors";
return Task.FromResult(
HistoryEventToStore.Create(@event, channel)
.AddParameter("Contributor", @event.ContributorId));
}
protected Task<HistoryEventToStore> 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<HistoryEventToStore> On(AppClientAttached @event, EnvelopeHeaders headers)
{
const string channel = "settings.clients";
return Task.FromResult(
HistoryEventToStore.Create(@event, channel)
.AddParameter("Id", @event.Id));
}
protected Task<HistoryEventToStore> On(AppClientRevoked @event, EnvelopeHeaders headers)
{
const string channel = "settings.clients";
return Task.FromResult(
HistoryEventToStore.Create(@event, channel)
.AddParameter("Id", @event.Id));
}
protected Task<HistoryEventToStore> On(AppLanguageAdded @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";
return Task.FromResult(
HistoryEventToStore.Create(@event, channel)
.AddParameter("Language", @event.Language.EnglishName));
}
protected Task<HistoryEventToStore> On(AppLanguageRemoved @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";
return Task.FromResult(
HistoryEventToStore.Create(@event, channel)
.AddParameter("Language", @event.Language.EnglishName));
}
protected Task<HistoryEventToStore> On(AppMasterLanguageSet @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";
return Task.FromResult(
HistoryEventToStore.Create(@event, channel)
.AddParameter("Language", @event.Language.EnglishName));
}
public Task<HistoryEventToStore> CreateEventAsync(Envelope<IEvent> @event)
{
return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null);
}
}
}

51
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<string, string> parameters = new Dictionary<string, string>();
public string Channel { get; }
public string Message { get; }
public IReadOnlyDictionary<string, string> 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;
}
}
}

22
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<string, string> Texts { get; }
Task<HistoryEventToStore> CreateEventAsync(Envelope<IEvent> @event);
}
}

2
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; }
}
}

113
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<string, string> TextsEN =
new Dictionary<string, string>
{
{
TypeNameRegistry.GetName<SchemaCreated>(),
"created schema {[Name]}"
},
{
TypeNameRegistry.GetName<SchemaUpdated>(),
"updated schema {[Name]}"
},
{
TypeNameRegistry.GetName<SchemaPublished>(),
"published schema {[Name]}"
},
{
TypeNameRegistry.GetName<SchemaUnpublished>(),
"unpublished schema {[Name]}"
}
};
public SchemaHistoryEventsCreator(ISchemaProvider schemaProvider)
{
this.schemaProvider = schemaProvider;
}
public IReadOnlyDictionary<string, string> Texts
{
get { return TextsEN; }
}
protected Task<HistoryEventToStore> 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<HistoryEventToStore> 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<HistoryEventToStore> 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<HistoryEventToStore> On(SchemaUnpublished @event, EnvelopeHeaders headers)
{
var name = await FindSchemaNameAsync(headers);
string channel = $"schemas.{name}";
return
HistoryEventToStore.Create(@event, channel)
.AddParameter("Name", name);
}
public Task<HistoryEventToStore> CreateEventAsync(Envelope<IEvent> @event)
{
return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null);
}
private Task<string> FindSchemaNameAsync(EnvelopeHeaders headers)
{
var name = schemaProvider.FindSchemaNameByIdAsync(headers.AppId(), headers.AggregateId());
return name;
}
}
}

2
src/Squidex.Read/Schemas/Services/ISchemaProvider.cs

@ -13,6 +13,8 @@ namespace Squidex.Read.Schemas.Services
{
public interface ISchemaProvider
{
Task<string> FindSchemaNameByIdAsync(Guid schemaId);
Task<Guid?> FindSchemaIdByNameAsync(Guid appId, string name);
}
}

63
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<string> FindSchemaNameByIdAsync(Guid schemaId)
{
var cacheKey = BuildIdCacheKey(schemaId);
var cacheItem = Cache.Get<CacheItem>(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<Guid?> FindSchemaIdByNameAsync(Guid appId, string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
var cacheKey = BuildModelCacheKey(appId, name);
var cacheKey = BuildNameCacheKey(appId, name);
var cacheItem = Cache.Get<CacheItem>(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<IEvent> @event)
{
if (@event.Payload is SchemaUpdated ||
@event.Payload is SchemaDeleted)
if (@event.Payload is SchemaDeleted ||
@event.Payload is SchemaCreated)
{
var oldName = Cache.Get<string>(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<CacheItem>(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}";
}

54
src/Squidex.Store.MongoDb/History/MessagesEN.cs

@ -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<string, string> Texts =
new Dictionary<string, string>
{
{
TypeNameRegistry.GetName<AppContributorAssigned>(),
"assigned {user:[Contributor]} as [Permission]"
},
{
TypeNameRegistry.GetName<AppContributorRemoved>(),
"removed {user:[Contributor]} from app"
},
{
TypeNameRegistry.GetName<AppClientAttached>(),
"added client {[Id]} to app"
},
{
TypeNameRegistry.GetName<AppClientRevoked>(),
"revoked client {[Id]}"
},
{
TypeNameRegistry.GetName<AppClientRenamed>(),
"named client {[Id]} as {[Name]}"
},
{
TypeNameRegistry.GetName<AppLanguageAdded>(),
"added language {[Language]}"
},
{
TypeNameRegistry.GetName<AppLanguageRemoved>(),
"removed language {[Language]}"
},
{
TypeNameRegistry.GetName<AppMasterLanguageSet>(),
"changed master language to {[Language]}"
}
};
}
}

128
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<MongoHistoryEventEntity>, IHistoryEventRepository, ICatchEventConsumer
{
public MongoHistoryEventRepository(IMongoDatabase database)
private readonly List<IHistoryEventsCreator> creators;
private readonly Dictionary<string, string> texts = new Dictionary<string, string>();
public MongoHistoryEventRepository(IMongoDatabase database, IEnumerable<IHistoryEventsCreator> 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<AppContributorAssigned>(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<AppContributorRemoved>(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<AppClientRenamed>(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<AppClientAttached>(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<AppClientRevoked>(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<IEvent> @event)
{
return Collection.CreateAsync(headers, x =>
foreach (var creator in creators)
{
const string channel = "settings.languages";
x.Setup<AppLanguageAdded>(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<AppLanguageRemoved>(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<AppMasterLanguageSet>(headers, channel)
.AddParameter("Language", @event.Language.EnglishName);
}, false);
}
public Task On(Envelope<IEvent> @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);
}
}
}
}
}

6
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; }

3
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;
}

9
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<T>(this IMongoCollection<T> collection, EnvelopeHeaders headers, Func<T, Task> updater, bool useAggregateId = true) where T : MongoEntity, new()
{
var entity = Create<T>(headers, useAggregateId);
await updater(entity);
await collection.InsertOneIfNotExistsAsync(entity);
}
public static async Task UpdateAsync<T>(this IMongoCollection<T> collection, EnvelopeHeaders headers, Action<T> updater) where T : MongoEntity
{
var entity = await collection.Find(t => t.Id == headers.AggregateId()).FirstOrDefaultAsync();

6
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; }
/// <summary>
/// Optional label for the editor.
/// </summary>
[StringLength(100)]
public string Label { get; set; }
/// <summary>
/// Indicates if the schema is published.
/// </summary>

1
src/Squidex/Properties/launchSettings.json

@ -16,7 +16,6 @@
},
"Squidex": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"

25
src/Squidex/app/features/apps/pages/apps-page.component.html

@ -18,22 +18,23 @@
</div>
</content>
<div class="modal" *sqxModalView="modalDialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<div class="modal" *sqxModalView="modalDialog" [@fade]>
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Create App</h4>
</div>
<h4 class="modal-title">Create App</h4>
</div>
<div class="modal-body">
<sqx-app-form
<div class="modal-body">
<sqx-app-form
(created)="modalDialog.hide()"
(cancelled)="modalDialog.hide()"></sqx-app-form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

6
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;

14
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
) {
}
}

12
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -70,6 +70,18 @@
</div>
<div class="field-details-tab" [class.hidden]="selectedTab !== 0">
<div class="form-group row">
<label for="field-label" class="col-xs-3 col-form-label">Name</label>
<div class="col-xs-6">
<input type="text" class="form-control" id="field-label" readonly [ngModel]="field.name" [ngModelOptions]="{standalone: true}" />
<span class="form-hint">
The name of the field in the API response.
</span>
</div>
</div>
<div class="form-group row">
<label for="field-label" class="col-xs-3 col-form-label">Label</label>

11
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -3,6 +3,17 @@
<div class="panel panel-light">
<div class="panel-header">
<div>
<div class="float-xs-right">
<div class="btn-group btn-group-sm" data-toggle="buttons">
<button type="button" class="btn btn-publishing btn-default" [class.btn-success]="isPublished" [disabled]="isPublished" (click)="publish()">
Published
</button>
<button type="button" class="btn btn-publishing btn-default" [class.btn-danger]="!isPublished" [disabled]="!isPublished" (click)="unpublish()">
Unpublished
</button>
</div>
</div>
<h3 class="panel-title">{{schemaName}}</h3>
<a class="panel-close" routerLink="../">

9
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);
}
}

42
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<FieldDto>();
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<FieldDto>) {
private updateAll(fields: ImmutableArray<FieldDto>) {
this.schemaFields = fields;
this.messageBus.publish(new HistoryChannelUpdated());
this.messageBus.publish(new SchemaUpdated(this.schemaName, this.isPublished));
}
}

2
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;

23
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html

@ -38,6 +38,8 @@
</div>
<div class="col-xs-4 schema-col-right">
<span class="schema-modified">{{schema.lastModified | fromNow}}</span>
<span class="schema-published" [class.unpublished]="!schema.isPublished"></span>
</div>
</div>
</div>
@ -48,24 +50,25 @@
</div>
<div class="modal" *sqxModalView="modalDialog" [@fade]>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Create Schema</h4>
</div>
<h4 class="modal-title">Create Schema</h4>
</div>
<div class="modal-body">
<div class="modal-body">
<sqx-schema-form
[appName]="appName() | async"
(created)="onSchemaCreationCompleted($event)"
(cancelled)="modalDialog.hide()"></sqx-schema-form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<router-outlet></router-outlet>

18
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);

41
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<SchemaDto>());
@ -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() {

21
src/Squidex/app/features/settings/pages/clients/client.component.html

@ -76,19 +76,20 @@
</div>
<div class="modal" *sqxModalView="modalDialog" [@fade]>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Access Token</h4>
</div>
<h4 class="modal-title">Access Token</h4>
</div>
<div class="modal-body">
<div class="modal-body">
<textarea class="form-control access-token">{{appClientToken.tokenType}} {{appClientToken.accessToken}}</textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

37
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<any> {
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<any> {
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<any> {
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<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/`);

23
src/Squidex/app/shell/pages/internal/apps-menu.component.html

@ -24,21 +24,22 @@
</ul>
<div class="modal" *sqxModalView="modalDialog" [@fade]>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Create App</h4>
</div>
<h4 class="modal-title">Create App</h4>
</div>
<div class="modal-body">
<sqx-app-form
<div class="modal-body">
<sqx-app-form
(created)="onAppCreationCompleted($event)"
(cancelled)="onAppCreationCancelled()"></sqx-app-form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

9
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;
}

6
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);

Loading…
Cancel
Save