Browse Source

Optimisic concurrency

pull/1/head
Sebastian 9 years ago
parent
commit
ee3857bd81
  1. 9
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs
  2. 3
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  3. 2
      src/Squidex.Infrastructure/CQRS/DomainObject.cs
  4. 10
      src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs
  5. 12
      src/Squidex.Write/Apps/AppCommandHandler.cs
  6. 11
      src/Squidex.Write/Contents/ContentCommandHandler.cs
  7. 6
      src/Squidex/Config/Domain/WriteModule.cs
  8. 2
      src/Squidex/Controllers/Api/Languages/LanguagesController.cs
  9. 5
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs
  10. 5
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs
  11. 11
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  12. 5
      src/Squidex/Controllers/ContentApi/Models/ContentDto.cs
  13. 2
      src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs
  14. 6
      src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs
  15. 8
      src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs
  16. 2
      src/Squidex/Views/Shared/Docs.cshtml
  17. 12
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  18. 31
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  19. 6
      src/Squidex/app/features/content/pages/messages.ts
  20. 3
      src/Squidex/app/features/schemas/pages/messages.ts
  21. 8
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  22. 1
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  23. 27
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  24. 16
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  25. 6
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  26. 2
      src/Squidex/app/features/settings/pages/clients/client.component.html
  27. 13
      src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
  28. 13
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  29. 17
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  30. 9
      src/Squidex/app/framework/angular/http-utils.ts
  31. 1
      src/Squidex/app/framework/declarations.ts
  32. 21
      src/Squidex/app/framework/utils/version.ts
  33. 6
      src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts
  34. 2
      src/Squidex/app/shared/guards/resolve-app-languages.guard.ts
  35. 6
      src/Squidex/app/shared/guards/resolve-content.guard.spec.ts
  36. 2
      src/Squidex/app/shared/guards/resolve-content.guard.ts
  37. 8
      src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts
  38. 2
      src/Squidex/app/shared/guards/resolve-published-schema.guard.ts
  39. 6
      src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts
  40. 2
      src/Squidex/app/shared/guards/resolve-schema.guard.ts
  41. 20
      src/Squidex/app/shared/services/app-clients.service.spec.ts
  42. 18
      src/Squidex/app/shared/services/app-clients.service.ts
  43. 16
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  44. 14
      src/Squidex/app/shared/services/app-contributors.service.ts
  45. 20
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  46. 18
      src/Squidex/app/shared/services/app-languages.service.ts
  47. 67
      src/Squidex/app/shared/services/auth.service.ts
  48. 49
      src/Squidex/app/shared/services/contents.service.spec.ts
  49. 36
      src/Squidex/app/shared/services/contents.service.ts
  50. 91
      src/Squidex/app/shared/services/schemas.service.spec.ts
  51. 57
      src/Squidex/app/shared/services/schemas.service.ts
  52. 4
      src/Squidex/app/theme/_bootstrap.scss

9
src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs

@ -28,7 +28,14 @@ namespace Squidex.Infrastructure.CQRS.Commands
var factoryFunctionType = typeof(DomainObjectFactoryFunction<>).MakeGenericType(type); var factoryFunctionType = typeof(DomainObjectFactoryFunction<>).MakeGenericType(type);
var factoryFunction = (Delegate)serviceProvider.GetService(factoryFunctionType); var factoryFunction = (Delegate)serviceProvider.GetService(factoryFunctionType);
return (IAggregate)factoryFunction.DynamicInvoke(id); var aggregate = (IAggregate)factoryFunction.DynamicInvoke(id);
if (aggregate.Version != -1)
{
throw new InvalidOperationException("Must have a version of -1");
}
return aggregate;
} }
} }
} }

3
src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs

@ -74,8 +74,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var streamName = nameResolver.GetStreamName(domainObject.GetType(), domainObject.Id); var streamName = nameResolver.GetStreamName(domainObject.GetType(), domainObject.Id);
var versionCurrent = domainObject.Version; var versionCurrent = domainObject.Version;
var versionBefore = versionCurrent - events.Count; var versionExpected = versionCurrent - events.Count;
var versionExpected = versionBefore == 0 ? -1 : versionBefore - 1;
var eventsToSave = events.Select(x => formatter.ToEventData(x, commitId)).ToList(); var eventsToSave = events.Select(x => formatter.ToEventData(x, commitId)).ToList();

2
src/Squidex.Infrastructure/CQRS/DomainObject.cs

@ -31,7 +31,7 @@ namespace Squidex.Infrastructure.CQRS
protected DomainObject(Guid id, int version) protected DomainObject(Guid id, int version)
{ {
Guard.NotEmpty(id, nameof(id)); Guard.NotEmpty(id, nameof(id));
Guard.GreaterEquals(version, 0, nameof(version)); Guard.GreaterEquals(version, -1, nameof(version));
this.id = id; this.id = id;

10
src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs

@ -27,7 +27,7 @@ namespace Squidex.Read.MongoDb.Contents
{ {
private const string Prefix = "Projections_Content_"; private const string Prefix = "Projections_Content_";
private readonly IMongoDatabase database; private readonly IMongoDatabase database;
private readonly ISchemaProvider schemaProvider; private readonly ISchemaProvider schemas;
private readonly EdmModelBuilder modelBuilder; private readonly EdmModelBuilder modelBuilder;
protected static IndexKeysDefinitionBuilder<MongoContentEntity> IndexKeys protected static IndexKeysDefinitionBuilder<MongoContentEntity> IndexKeys
@ -38,15 +38,15 @@ namespace Squidex.Read.MongoDb.Contents
} }
} }
public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemaProvider, EdmModelBuilder modelBuilder) public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemas, EdmModelBuilder modelBuilder)
{ {
Guard.NotNull(database, nameof(database)); Guard.NotNull(database, nameof(database));
Guard.NotNull(modelBuilder, nameof(modelBuilder)); Guard.NotNull(modelBuilder, nameof(modelBuilder));
Guard.NotNull(schemaProvider, nameof(schemaProvider)); Guard.NotNull(schemas, nameof(schemas));
this.schemas = schemas;
this.database = database; this.database = database;
this.modelBuilder = modelBuilder; this.modelBuilder = modelBuilder;
this.schemaProvider = schemaProvider;
} }
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages) public async Task<IReadOnlyList<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages)
@ -142,7 +142,7 @@ namespace Squidex.Read.MongoDb.Contents
{ {
var collection = GetCollection(schemaId); var collection = GetCollection(schemaId);
var schemaEntity = await schemaProvider.FindSchemaByIdAsync(schemaId); var schemaEntity = await schemas.FindSchemaByIdAsync(schemaId);
if (schemaEntity == null) if (schemaEntity == null)
{ {

12
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -52,10 +52,7 @@ namespace Squidex.Write.Apps
throw new ValidationException("Cannot create a new app", error); throw new ValidationException("Cannot create a new app", error);
} }
await handler.CreateAsync<AppDomainObject>(context, x => await handler.CreateAsync<AppDomainObject>(context, x => x.Create(command));
{
context.Succeed(command.AggregateId);
});
} }
protected async Task On(AssignContributor command, CommandContext context) protected async Task On(AssignContributor command, CommandContext context)
@ -69,10 +66,7 @@ namespace Squidex.Write.Apps
throw new ValidationException("Cannot assign contributor to app", error); throw new ValidationException("Cannot assign contributor to app", error);
} }
await handler.UpdateAsync<AppDomainObject>(context, x => await handler.UpdateAsync<AppDomainObject>(context, x => x.AssignContributor(command));
{
context.Succeed(new EntitySavedResult(x.Version));
});
} }
protected Task On(AttachClient command, CommandContext context) protected Task On(AttachClient command, CommandContext context)
@ -81,7 +75,7 @@ namespace Squidex.Write.Apps
{ {
x.AttachClient(command, keyGenerator.GenerateKey()); x.AttachClient(command, keyGenerator.GenerateKey());
context.Succeed(x.Clients[command.Id]); context.Succeed(new EntityCreatedResult<AppClient>(x.Clients[command.Id], x.Version));
}); });
} }

11
src/Squidex.Write/Contents/ContentCommandHandler.cs

@ -23,20 +23,21 @@ namespace Squidex.Write.Contents
{ {
private readonly IAggregateHandler handler; private readonly IAggregateHandler handler;
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly ISchemaProvider schemaProvider; private readonly ISchemaProvider schemas;
public ContentCommandHandler( public ContentCommandHandler(
IAggregateHandler handler, IAggregateHandler handler,
IAppProvider appProvider, IAppProvider appProvider,
ISchemaProvider schemaProvider) ISchemaProvider schemas)
{ {
Guard.NotNull(handler, nameof(handler)); Guard.NotNull(handler, nameof(handler));
Guard.NotNull(schemas, nameof(schemas));
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(schemaProvider, nameof(schemaProvider));
this.handler = handler; this.handler = handler;
this.schemas = schemas;
this.appProvider = appProvider; this.appProvider = appProvider;
this.schemaProvider = schemaProvider;
} }
protected async Task On(CreateContent command, CommandContext context) protected async Task On(CreateContent command, CommandContext context)
@ -88,7 +89,7 @@ namespace Squidex.Write.Contents
appProvider.FindAppByIdAsync(command.AppId.Id); appProvider.FindAppByIdAsync(command.AppId.Id);
var taskForSchema = var taskForSchema =
schemaProvider.FindSchemaByIdAsync(command.SchemaId.Id); schemas.FindSchemaByIdAsync(command.SchemaId.Id);
await Task.WhenAll(taskForApp, taskForSchema); await Task.WhenAll(taskForApp, taskForSchema);

6
src/Squidex/Config/Domain/WriteModule.cs

@ -72,11 +72,11 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
builder.Register<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, 0))) builder.Register<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, -1)))
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
builder.Register<DomainObjectFactoryFunction<ContentDomainObject>>(c => (id => new ContentDomainObject(id, 0))) builder.Register<DomainObjectFactoryFunction<ContentDomainObject>>(c => (id => new ContentDomainObject(id, -1)))
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
@ -84,7 +84,7 @@ namespace Squidex.Config.Domain
{ {
var fieldRegistry = c.Resolve<FieldRegistry>(); var fieldRegistry = c.Resolve<FieldRegistry>();
return (id => new SchemaDomainObject(id, 0, fieldRegistry)); return (id => new SchemaDomainObject(id, -1, fieldRegistry));
}) })
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();

2
src/Squidex/Controllers/Api/Languages/LanguagesController.cs

@ -38,7 +38,7 @@ namespace Squidex.Controllers.Api.Languages
[ProducesResponseType(typeof(string[]), 200)] [ProducesResponseType(typeof(string[]), 200)]
public IActionResult GetLanguages() public IActionResult GetLanguages()
{ {
var response = Language.AllLanguages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList(); var response = Language.AllLanguages.Where(x => x != Language.Invariant).Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList();
return Ok(response); return Ok(response);
} }

5
src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs

@ -72,5 +72,10 @@ namespace Squidex.Controllers.Api.Schemas.Models
/// The date and time when the schema has been modified last. /// The date and time when the schema has been modified last.
/// </summary> /// </summary>
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
/// <summary>
/// The version of the schema.
/// </summary>
public int Version { get; set; }
} }
} }

5
src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs

@ -59,5 +59,10 @@ namespace Squidex.Controllers.Api.Schemas.Models
/// The date and time when the schema has been modified last. /// The date and time when the schema has been modified last.
/// </summary> /// </summary>
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
/// <summary>
/// The version of the schema.
/// </summary>
public int Version { get; set; }
} }
} }

11
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -32,13 +32,14 @@ namespace Squidex.Controllers.ContentApi
[ServiceFilter(typeof(AppFilterAttribute))] [ServiceFilter(typeof(AppFilterAttribute))]
public class ContentsController : ControllerBase public class ContentsController : ControllerBase
{ {
private readonly ISchemaProvider schemaProvider; private readonly ISchemaProvider schemas;
private readonly IContentRepository contentRepository; private readonly IContentRepository contentRepository;
public ContentsController(ICommandBus commandBus, ISchemaProvider schemaProvider, IContentRepository contentRepository) public ContentsController(ICommandBus commandBus, ISchemaProvider schemas, IContentRepository contentRepository)
: base(commandBus) : base(commandBus)
{ {
this.schemaProvider = schemaProvider; this.schemas = schemas;
this.contentRepository = contentRepository; this.contentRepository = contentRepository;
} }
@ -46,7 +47,7 @@ namespace Squidex.Controllers.ContentApi
[Route("content/{app}/{name}")] [Route("content/{app}/{name}")]
public async Task<IActionResult> GetContents(string name, [FromQuery] bool nonPublished = false, [FromQuery] bool hidden = false) public async Task<IActionResult> GetContents(string name, [FromQuery] bool nonPublished = false, [FromQuery] bool hidden = false)
{ {
var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name); var schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name);
if (schemaEntity == null) if (schemaEntity == null)
{ {
@ -85,7 +86,7 @@ namespace Squidex.Controllers.ContentApi
[Route("content/{app}/{name}/{id}")] [Route("content/{app}/{name}/{id}")]
public async Task<IActionResult> GetContent(string name, Guid id, bool hidden = false) public async Task<IActionResult> GetContent(string name, Guid id, bool hidden = false)
{ {
var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name); var schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name);
if (schemaEntity == null) if (schemaEntity == null)
{ {

5
src/Squidex/Controllers/ContentApi/Models/ContentDto.cs

@ -52,5 +52,10 @@ namespace Squidex.Controllers.ContentApi.Models
/// Indicates if the content element is publihed. /// Indicates if the content element is publihed.
/// </summary> /// </summary>
public bool IsPublished { get; set; } public bool IsPublished { get; set; }
/// <summary>
/// The version of the content.
/// </summary>
public int Version { get; set; }
} }
} }

2
src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs

@ -47,7 +47,7 @@ namespace Squidex.Pipeline
private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex)
{ {
return new ObjectResult(new ErrorDto { Message = ex.Message }) { StatusCode = 409 }; return new ObjectResult(new ErrorDto { Message = ex.Message }) { StatusCode = 412 };
} }
private static IActionResult OnDomainException(DomainException ex) private static IActionResult OnDomainException(DomainException ex)

6
src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs

@ -26,10 +26,10 @@ namespace Squidex.Pipeline.CommandHandlers
public Task<bool> HandleAsync(CommandContext context) public Task<bool> HandleAsync(CommandContext context)
{ {
var headers = httpContextAccessor.HttpContext.Request.GetTypedHeaders(); var headers = httpContextAccessor.HttpContext.Request.Headers;
var headerMatch = headers.IfMatch?.FirstOrDefault(); var headerMatch = headers["If-Match"].ToString();
if (!string.IsNullOrWhiteSpace(headerMatch?.Tag) && long.TryParse(headerMatch.Tag, NumberStyles.Any, CultureInfo.InvariantCulture, out long expectedVersion)) if (!string.IsNullOrWhiteSpace(headerMatch) && long.TryParse(headerMatch, NumberStyles.Any, CultureInfo.InvariantCulture, out long expectedVersion))
{ {
context.Command.ExpectedVersion = expectedVersion; context.Command.ExpectedVersion = expectedVersion;
} }

8
src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs

@ -21,12 +21,12 @@ namespace Squidex.Pipeline.CommandHandlers
{ {
public sealed class EnrichWithSchemaIdHandler : ICommandHandler public sealed class EnrichWithSchemaIdHandler : ICommandHandler
{ {
private readonly ISchemaProvider schemaProvider; private readonly ISchemaProvider schemas;
private readonly IActionContextAccessor actionContextAccessor; private readonly IActionContextAccessor actionContextAccessor;
public EnrichWithSchemaIdHandler(ISchemaProvider schemaProvider, IActionContextAccessor actionContextAccessor) public EnrichWithSchemaIdHandler(ISchemaProvider schemas, IActionContextAccessor actionContextAccessor)
{ {
this.schemaProvider = schemaProvider; this.schemas = schemas;
this.actionContextAccessor = actionContextAccessor; this.actionContextAccessor = actionContextAccessor;
} }
@ -43,7 +43,7 @@ namespace Squidex.Pipeline.CommandHandlers
{ {
var schemaName = routeValues["name"].ToString(); var schemaName = routeValues["name"].ToString();
var schema = await schemaProvider.FindSchemaByNameAsync(schemaCommand.AppId.Id, schemaName); var schema = await schemas.FindSchemaByNameAsync(schemaCommand.AppId.Id, schemaName);
if (schema == null) if (schema == null)
{ {

2
src/Squidex/Views/Shared/Docs.cshtml

@ -28,6 +28,6 @@
<body> <body>
<redoc lazy-rendering="true" spec-url="@Url.Content(ViewBag.Specification)"></redoc> <redoc lazy-rendering="true" spec-url="@Url.Content(ViewBag.Specification)"></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"></script> <script src="https://rebilly.github.io/ReDoc/releases/v1.10.1/redoc.min.js"></script>
</body> </body>
</html> </html>

12
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -28,7 +28,8 @@ import {
SchemaDetailsDto, SchemaDetailsDto,
StringFieldPropertiesDto, StringFieldPropertiesDto,
UsersProviderService, UsersProviderService,
ValidatorsEx ValidatorsEx,
Version
} from 'shared'; } from 'shared';
@Component({ @Component({
@ -38,6 +39,7 @@ import {
}) })
export class ContentPageComponent extends AppComponentBase implements OnDestroy, OnInit { export class ContentPageComponent extends AppComponentBase implements OnDestroy, OnInit {
private messageSubscription: Subscription; private messageSubscription: Subscription;
private version: Version;
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
@ -94,9 +96,9 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy,
if (this.isNewMode) { if (this.isNewMode) {
this.appName() this.appName()
.switchMap(app => this.contentsService.postContent(app, this.schema.name, data)) .switchMap(app => this.contentsService.postContent(app, this.schema.name, data, this.version))
.subscribe(created => { .subscribe(created => {
this.messageBus.publish(new ContentCreated(created.id, data)); this.messageBus.publish(new ContentCreated(created.id, data, this.version.value));
this.router.navigate(['../'], { relativeTo: this.route }); this.router.navigate(['../'], { relativeTo: this.route });
}, error => { }, error => {
@ -105,9 +107,9 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy,
}); });
} else { } else {
this.appName() this.appName()
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId, data)) .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId, data, this.version))
.subscribe(() => { .subscribe(() => {
this.messageBus.publish(new ContentUpdated(this.contentId, data)); this.messageBus.publish(new ContentUpdated(this.contentId, data, this.version.value));
this.router.navigate(['../'], { relativeTo: this.route }); this.router.navigate(['../'], { relativeTo: this.route });
}, error => { }, error => {

31
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -29,7 +29,8 @@ import {
MessageBus, MessageBus,
NotificationService, NotificationService,
SchemaDetailsDto, SchemaDetailsDto,
UsersProviderService UsersProviderService,
Version
} from 'shared'; } from 'shared';
@Component({ @Component({
@ -86,12 +87,12 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.messageBus.of(ContentCreated).subscribe(message => { this.messageBus.of(ContentCreated).subscribe(message => {
this.itemLast++; this.itemLast++;
this.contentTotal++; this.contentTotal++;
this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data)); this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data, message.version));
}); });
this.messageUpdatedSubscription = this.messageUpdatedSubscription =
this.messageBus.of(ContentUpdated).subscribe(message => { this.messageBus.of(ContentUpdated).subscribe(message => {
this.updateContents(message.id, undefined, message.data); this.updateContents(message.id, undefined, message.data, message.version);
}); });
this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => { this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => {
@ -115,9 +116,9 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public publishContent(content: ContentDto) { public publishContent(content: ContentDto) {
this.appName() this.appName()
.switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id)) .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .subscribe(() => {
this.updateContents(content.id, true, content.data); this.updateContents(content.id, true, content.data, content.version.value);
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -125,9 +126,9 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public unpublishContent(content: ContentDto) { public unpublishContent(content: ContentDto) {
this.appName() this.appName()
.switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id)) .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .subscribe(() => {
this.updateContents(content.id, false, content.data); this.updateContents(content.id, false, content.data, content.version.value);
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -135,7 +136,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public deleteContent(content: ContentDto) { public deleteContent(content: ContentDto) {
this.appName() this.appName()
.switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id)) .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .subscribe(() => {
this.contentItems = this.contentItems.removeAll(x => x.id === content.id); this.contentItems = this.contentItems.removeAll(x => x.id === content.id);
@ -204,11 +205,11 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.canGoPrev = this.currentPage > 0; this.canGoPrev = this.currentPage > 0;
} }
private updateContents(id: string, p: boolean | undefined, data: any) { private updateContents(id: string, p: boolean | undefined, data: any, version: string) {
this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data)); this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data, version));
} }
private createContent(id: string, data: any): ContentDto { private createContent(id: string, data: any, version: string): ContentDto {
const me = `subject:${this.authService.user!.id}`; const me = `subject:${this.authService.user!.id}`;
const newContent = const newContent =
@ -217,12 +218,13 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
me, me, me, me,
DateTime.now(), DateTime.now(),
DateTime.now(), DateTime.now(),
data); data,
new Version(version));
return newContent; return newContent;
} }
private updateContent(content: ContentDto, isPublished: boolean, data: any): ContentDto { private updateContent(content: ContentDto, isPublished: boolean, data: any, version: string): ContentDto {
const me = `subject:${this.authService.user!.id}`; const me = `subject:${this.authService.user!.id}`;
const newContent = const newContent =
@ -230,7 +232,8 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
content.id, isPublished, content.id, isPublished,
content.createdBy, me, content.createdBy, me,
content.created, DateTime.now(), content.created, DateTime.now(),
data); data,
new Version(version));
return newContent; return newContent;
} }

6
src/Squidex/app/features/content/pages/messages.ts

@ -8,7 +8,8 @@
export class ContentCreated { export class ContentCreated {
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly data: any public readonly data: any,
public readonly version: string
) { ) {
} }
} }
@ -16,7 +17,8 @@ export class ContentCreated {
export class ContentUpdated { export class ContentUpdated {
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly data: any public readonly data: any,
public readonly version: string
) { ) {
} }
} }

3
src/Squidex/app/features/schemas/pages/messages.ts

@ -9,7 +9,8 @@ export class SchemaUpdated {
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly label: string, public readonly label: string,
public readonly isPublished: boolean public readonly isPublished: boolean,
public readonly version: string
) { ) {
} }
} }

8
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts

@ -11,7 +11,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { import {
Notification, Notification,
NotificationService, NotificationService,
SchemasService SchemasService,
Version
} from 'shared'; } from 'shared';
import { SchemaPropertiesDto } from './schema-properties'; import { SchemaPropertiesDto } from './schema-properties';
@ -31,6 +32,9 @@ export class SchemaEditFormComponent implements OnInit {
@Input() @Input()
public schema: SchemaPropertiesDto; public schema: SchemaPropertiesDto;
@Input()
public version: Version;
@Input() @Input()
public appName: string; public appName: string;
@ -72,7 +76,7 @@ export class SchemaEditFormComponent implements OnInit {
const requestDto = this.editForm.value; const requestDto = this.editForm.value;
this.schemas.putSchema(this.appName, this.schema.name, requestDto) this.schemas.putSchema(this.appName, this.schema.name, requestDto, this.version)
.subscribe(dto => { .subscribe(dto => {
this.reset(); this.reset();
this.saved.emit(new SchemaPropertiesDto(this.schema.name, requestDto.label, requestDto.hints)); this.saved.emit(new SchemaPropertiesDto(this.schema.name, requestDto.label, requestDto.hints));

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

@ -82,6 +82,7 @@
<sqx-schema-edit-form <sqx-schema-edit-form
[appName]="appName() | async" [appName]="appName() | async"
[schema]="schemaProperties" [schema]="schemaProperties"
[version]="version"
(saved)="onSchemaSaved($event)" (saved)="onSchemaSaved($event)"
(cancelled)="editSchemaDialog.hide()"></sqx-schema-edit-form> (cancelled)="editSchemaDialog.hide()"></sqx-schema-edit-form>
</div> </div>

27
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -25,7 +25,8 @@ import {
SchemasService, SchemasService,
UpdateFieldDto, UpdateFieldDto,
UsersProviderService, UsersProviderService,
ValidatorsEx ValidatorsEx,
Version
} from 'shared'; } from 'shared';
import { SchemaPropertiesDto } from './schema-properties'; import { SchemaPropertiesDto } from './schema-properties';
@ -52,6 +53,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public schemaFields = ImmutableArray.empty<FieldDto>(); public schemaFields = ImmutableArray.empty<FieldDto>();
public schemaProperties: SchemaPropertiesDto; public schemaProperties: SchemaPropertiesDto;
public version = new Version('');
public editSchemaDialog = new ModalView(); public editSchemaDialog = new ModalView();
public isPublished: boolean; public isPublished: boolean;
@ -86,13 +89,15 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.schemaFields = ImmutableArray.of(schema.fields); this.schemaFields = ImmutableArray.of(schema.fields);
this.schemaProperties = new SchemaPropertiesDto(schema.name, schema.label, schema.hints); this.schemaProperties = new SchemaPropertiesDto(schema.name, schema.label, schema.hints);
this.version = schema.version;
this.isPublished = schema.isPublished; this.isPublished = schema.isPublished;
}); });
} }
public publish() { public publish() {
this.appName() this.appName()
.switchMap(app => this.schemasService.publishSchema(app, this.schemaName)).retry(2) .switchMap(app => this.schemasService.publishSchema(app, this.schemaName, this.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.isPublished = true; this.isPublished = true;
this.notify(); this.notify();
@ -103,7 +108,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public unpublish() { public unpublish() {
this.appName() this.appName()
.switchMap(app => this.schemasService.unpublishSchema(app, this.schemaName)).retry(2) .switchMap(app => this.schemasService.unpublishSchema(app, this.schemaName, this.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.isPublished = false; this.isPublished = false;
this.notify(); this.notify();
@ -114,7 +119,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public enableField(field: FieldDto) { public enableField(field: FieldDto) {
this.appName() this.appName()
.switchMap(app => this.schemasService.enableField(app, this.schemaName, field.fieldId)).retry(2) .switchMap(app => this.schemasService.enableField(app, this.schemaName, field.fieldId, this.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateField(field, new FieldDto(field.fieldId, field.name, field.isHidden, false, field.properties)); this.updateField(field, new FieldDto(field.fieldId, field.name, field.isHidden, false, field.properties));
}, error => { }, error => {
@ -124,7 +129,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public disableField(field: FieldDto) { public disableField(field: FieldDto) {
this.appName() this.appName()
.switchMap(app => this.schemasService.disableField(app, this.schemaName, field.fieldId)).retry(2) .switchMap(app => this.schemasService.disableField(app, this.schemaName, field.fieldId, this.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateField(field, new FieldDto(field.fieldId, field.name, field.isHidden, true, field.properties)); this.updateField(field, new FieldDto(field.fieldId, field.name, field.isHidden, true, field.properties));
}, error => { }, error => {
@ -134,7 +139,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public showField(field: FieldDto) { public showField(field: FieldDto) {
this.appName() this.appName()
.switchMap(app => this.schemasService.showField(app, this.schemaName, field.fieldId)).retry(2) .switchMap(app => this.schemasService.showField(app, this.schemaName, field.fieldId, this.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateField(field, new FieldDto(field.fieldId, field.name, false, field.isDisabled, field.properties)); this.updateField(field, new FieldDto(field.fieldId, field.name, false, field.isDisabled, field.properties));
}, error => { }, error => {
@ -144,7 +149,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public hideField(field: FieldDto) { public hideField(field: FieldDto) {
this.appName() this.appName()
.switchMap(app => this.schemasService.hideField(app, this.schemaName, field.fieldId)).retry(2) .switchMap(app => this.schemasService.hideField(app, this.schemaName, field.fieldId, this.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateField(field, new FieldDto(field.fieldId, field.name, true, field.isDisabled, field.properties)); this.updateField(field, new FieldDto(field.fieldId, field.name, true, field.isDisabled, field.properties));
}, error => { }, error => {
@ -154,7 +159,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public deleteField(field: FieldDto) { public deleteField(field: FieldDto) {
this.appName() this.appName()
.switchMap(app => this.schemasService.deleteField(app, this.schemaName, field.fieldId)).retry(2) .switchMap(app => this.schemasService.deleteField(app, this.schemaName, field.fieldId, this.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateFields(this.schemaFields.remove(field)); this.updateFields(this.schemaFields.remove(field));
}, error => { }, error => {
@ -166,7 +171,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
const request = new UpdateFieldDto(newField.properties); const request = new UpdateFieldDto(newField.properties);
this.appName() this.appName()
.switchMap(app => this.schemasService.putField(app, this.schemaName, field.fieldId, request)).retry(2) .switchMap(app => this.schemasService.putField(app, this.schemaName, field.fieldId, request, this.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateField(field, new FieldDto(field.fieldId, field.name, newField.isHidden, field.isDisabled, newField.properties)); this.updateField(field, new FieldDto(field.fieldId, field.name, newField.isHidden, field.isDisabled, newField.properties));
}, error => { }, error => {
@ -191,7 +196,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
}; };
this.appName() this.appName()
.switchMap(app => this.schemasService.postField(app, this.schemaName, requestDto)) .switchMap(app => this.schemasService.postField(app, this.schemaName, requestDto, this.version))
.subscribe(dto => { .subscribe(dto => {
const newField = const newField =
new FieldDto(parseInt(dto.id, 10), new FieldDto(parseInt(dto.id, 10),
@ -240,7 +245,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
private notify() { private notify() {
this.messageBus.publish(new HistoryChannelUpdated()); this.messageBus.publish(new HistoryChannelUpdated());
this.messageBus.publish(new SchemaUpdated(this.schemaName, this.schemaProperties.label, this.isPublished)); this.messageBus.publish(new SchemaUpdated(this.schemaName, this.schemaProperties.label, this.isPublished, this.version.value));
} }
} }

16
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts

@ -17,7 +17,8 @@ import {
fadeAnimation, fadeAnimation,
SchemaDto, SchemaDto,
SchemasService, SchemasService,
ValidatorsEx ValidatorsEx,
Version
} from 'shared'; } from 'shared';
const FALLBACK_NAME = 'my-schema'; const FALLBACK_NAME = 'my-schema';
@ -75,14 +76,15 @@ export class SchemaFormComponent {
if (this.createForm.valid) { if (this.createForm.valid) {
this.createForm.disable(); this.createForm.disable();
const name = this.createForm.get('name').value; const schemaVersion = new Version();
const schemaName = this.createForm.get('name').value;
const requestDto = new CreateSchemaDto(name); const requestDto = new CreateSchemaDto(schemaName);
this.schemas.postSchema(this.appName, requestDto) this.schemas.postSchema(this.appName, requestDto, schemaVersion)
.subscribe(dto => { .subscribe(dto => {
this.reset(); this.reset();
this.created.emit(this.createSchemaDto(dto.id, name)); this.created.emit(this.createSchemaDto(dto.id, schemaName, schemaVersion));
}, error => { }, error => {
this.createForm.enable(); this.createForm.enable();
this.creationError = error.displayMessage; this.creationError = error.displayMessage;
@ -96,10 +98,10 @@ export class SchemaFormComponent {
this.createFormSubmitted = false; this.createFormSubmitted = false;
} }
private createSchemaDto(id: string, name: string) { private createSchemaDto(id: string, name: string, version: Version) {
const user = this.authService.user!.token; const user = this.authService.user!.token;
const now = DateTime.now(); const now = DateTime.now();
return new SchemaDto(id, name, undefined, false, user, user, now, now); return new SchemaDto(id, name, undefined, false, user, user, now, now, version);
} }
} }

6
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -22,7 +22,8 @@ import {
NotificationService, NotificationService,
SchemaDto, SchemaDto,
SchemasService, SchemasService,
UsersProviderService UsersProviderService,
Version
} from 'shared'; } from 'shared';
import { SchemaUpdated } from './../messages'; import { SchemaUpdated } from './../messages';
@ -130,6 +131,7 @@ function updateSchema(schema: SchemaDto, authService: AuthService, message: Sche
message.label, message.label,
message.isPublished, message.isPublished,
schema.createdBy, me, schema.createdBy, me,
schema.created, DateTime.now()); schema.created, DateTime.now(),
new Version(message.version));
} }

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

@ -22,7 +22,7 @@
<button type="submit" class="btn btn-primary" [disabled]="!renameForm.valid">Save</button> <button type="submit" class="btn btn-primary" [disabled]="!renameForm.valid">Save</button>
<button class="btn btn-default btn-cancel" (click)="cancelRename()"> <button class="btn btn-simple btn-cancel" (click)="cancelRename()">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</form> </form>

13
src/Squidex/app/features/settings/pages/clients/clients-page.component.ts

@ -20,7 +20,8 @@ import {
NotificationService, NotificationService,
UpdateAppClientDto, UpdateAppClientDto,
UsersProviderService, UsersProviderService,
ValidatorsEx ValidatorsEx,
Version
} from 'shared'; } from 'shared';
@Component({ @Component({
@ -29,6 +30,8 @@ import {
templateUrl: './clients-page.component.html' templateUrl: './clients-page.component.html'
}) })
export class ClientsPageComponent extends AppComponentBase implements OnInit { export class ClientsPageComponent extends AppComponentBase implements OnInit {
private version = new Version();
public appClients: ImmutableArray<AppClientDto>; public appClients: ImmutableArray<AppClientDto>;
public addClientForm: FormGroup = public addClientForm: FormGroup =
@ -55,7 +58,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
public load() { public load() {
this.appName() this.appName()
.switchMap(app => this.appClientsService.getClients(app).retry(2)) .switchMap(app => this.appClientsService.getClients(app, this.version).retry(2))
.subscribe(dtos => { .subscribe(dtos => {
this.updateClients(ImmutableArray.of(dtos)); this.updateClients(ImmutableArray.of(dtos));
}, error => { }, error => {
@ -65,7 +68,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
public revokeClient(client: AppClientDto) { public revokeClient(client: AppClientDto) {
this.appName() this.appName()
.switchMap(app => this.appClientsService.deleteClient(app, client.id)) .switchMap(app => this.appClientsService.deleteClient(app, client.id, this.version))
.subscribe(() => { .subscribe(() => {
this.updateClients(this.appClients.remove(client)); this.updateClients(this.appClients.remove(client));
}, error => { }, error => {
@ -77,7 +80,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
const request = new UpdateAppClientDto(name); const request = new UpdateAppClientDto(name);
this.appName() this.appName()
.switchMap(app => this.appClientsService.updateClient(app, client.id, request)) .switchMap(app => this.appClientsService.updateClient(app, client.id, request, this.version))
.subscribe(() => { .subscribe(() => {
this.updateClients(this.appClients.replace(client, rename(client, name))); this.updateClients(this.appClients.replace(client, rename(client, name)));
}, error => { }, error => {
@ -103,7 +106,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
}; };
this.appName() this.appName()
.switchMap(app => this.appClientsService.postClient(app, requestDto)) .switchMap(app => this.appClientsService.postClient(app, requestDto, this.version))
.subscribe(dto => { .subscribe(dto => {
this.updateClients(this.appClients.push(dto)); this.updateClients(this.appClients.push(dto));
reset(); reset();

13
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -22,7 +22,8 @@ import {
MessageBus, MessageBus,
NotificationService, NotificationService,
UsersProviderService, UsersProviderService,
UsersService UsersService,
Version
} from 'shared'; } from 'shared';
export class UsersDataSource implements AutocompleteSource { export class UsersDataSource implements AutocompleteSource {
@ -58,6 +59,8 @@ export class UsersDataSource implements AutocompleteSource {
templateUrl: './contributors-page.component.html' templateUrl: './contributors-page.component.html'
}) })
export class ContributorsPageComponent extends AppComponentBase implements OnInit { export class ContributorsPageComponent extends AppComponentBase implements OnInit {
private version = new Version();
public appContributors = ImmutableArray.empty<AppContributorDto>(); public appContributors = ImmutableArray.empty<AppContributorDto>();
public currentUserId: string; public currentUserId: string;
@ -96,7 +99,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
public load() { public load() {
this.appName() this.appName()
.switchMap(app => this.appContributorsService.getContributors(app).retry(2)) .switchMap(app => this.appContributorsService.getContributors(app, this.version).retry(2))
.subscribe(dtos => { .subscribe(dtos => {
this.updateContributors(ImmutableArray.of(dtos)); this.updateContributors(ImmutableArray.of(dtos));
}, error => { }, error => {
@ -106,7 +109,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
public removeContributor(contributor: AppContributorDto) { public removeContributor(contributor: AppContributorDto) {
this.appName() this.appName()
.switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId)) .switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId, this.version))
.subscribe(() => { .subscribe(() => {
this.updateContributors(this.appContributors.remove(contributor)); this.updateContributors(this.appContributors.remove(contributor));
}, error => { }, error => {
@ -118,7 +121,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
const newContributor = new AppContributorDto(this.addContributorForm.get('user').value.model.id, 'Editor'); const newContributor = new AppContributorDto(this.addContributorForm.get('user').value.model.id, 'Editor');
this.appName() this.appName()
.switchMap(app => this.appContributorsService.postContributor(app, newContributor)) .switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version))
.subscribe(() => { .subscribe(() => {
this.updateContributors(this.appContributors.push(newContributor)); this.updateContributors(this.appContributors.push(newContributor));
}, error => { }, error => {
@ -132,7 +135,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
const newContributor = changePermission(contributor, permission); const newContributor = changePermission(contributor, permission);
this.appName() this.appName()
.switchMap(app => this.appContributorsService.postContributor(app, newContributor)) .switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version))
.subscribe(() => { .subscribe(() => {
this.updateContributors(this.appContributors.replace(contributor, newContributor)); this.updateContributors(this.appContributors.replace(contributor, newContributor));
}, error => { }, error => {

17
src/Squidex/app/features/settings/pages/languages/languages-page.component.ts

@ -21,7 +21,8 @@ import {
LanguageService, LanguageService,
NotificationService, NotificationService,
UpdateAppLanguageDto, UpdateAppLanguageDto,
UsersProviderService UsersProviderService,
Version
} from 'shared'; } from 'shared';
@Component({ @Component({
@ -30,6 +31,8 @@ import {
templateUrl: './languages-page.component.html' templateUrl: './languages-page.component.html'
}) })
export class LanguagesPageComponent extends AppComponentBase implements OnInit { export class LanguagesPageComponent extends AppComponentBase implements OnInit {
private version = new Version();
public allLanguages: LanguageDto[] = []; public allLanguages: LanguageDto[] = [];
public appLanguages = ImmutableArray.empty<AppLanguageDto>(); public appLanguages = ImmutableArray.empty<AppLanguageDto>();
@ -66,7 +69,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
public load() { public load() {
this.appName() this.appName()
.switchMap(app => this.appLanguagesService.getLanguages(app).retry(2)) .switchMap(app => this.appLanguagesService.getLanguages(app, this.version).retry(2))
.subscribe(dtos => { .subscribe(dtos => {
this.updateLanguages(ImmutableArray.of(dtos)); this.updateLanguages(ImmutableArray.of(dtos));
}, error => { }, error => {
@ -76,9 +79,9 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
public removeLanguage(language: AppLanguageDto) { public removeLanguage(language: AppLanguageDto) {
this.appName() this.appName()
.switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code)) .switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code, this.version))
.subscribe(dto => { .subscribe(dto => {
this.updateLanguages(this.appLanguages.remove(dto)); this.updateLanguages(this.appLanguages.remove(language));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -88,7 +91,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
const request = new AddAppLanguageDto(this.addLanguageForm.get('language').value.iso2Code); const request = new AddAppLanguageDto(this.addLanguageForm.get('language').value.iso2Code);
this.appName() this.appName()
.switchMap(app => this.appLanguagesService.postLanguages(app, request)) .switchMap(app => this.appLanguagesService.postLanguages(app, request, this.version))
.subscribe(dto => { .subscribe(dto => {
this.updateLanguages(this.appLanguages.push(dto)); this.updateLanguages(this.appLanguages.push(dto));
}, error => { }, error => {
@ -100,7 +103,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
const request = new UpdateAppLanguageDto(true); const request = new UpdateAppLanguageDto(true);
this.appName() this.appName()
.switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, request)) .switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, request, this.version))
.subscribe(() => { .subscribe(() => {
this.updateLanguages(this.appLanguages.map(l => { this.updateLanguages(this.appLanguages.map(l => {
const isMasterLanguage = l === language; const isMasterLanguage = l === language;
@ -114,6 +117,8 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
return false;
} }
private updateLanguages(languages: ImmutableArray<AppLanguageDto>) { private updateLanguages(languages: ImmutableArray<AppLanguageDto>) {

9
src/Squidex/app/framework/angular/http-utils.ts

@ -53,10 +53,15 @@ export function catchError(message: string): Observable<any> {
return this.catch((error: any | Response) => { return this.catch((error: any | Response) => {
let result = new ErrorDto(500, message); let result = new ErrorDto(500, message);
if (error instanceof Response && error.status !== 500) { if (error instanceof Response) {
const body = error.json(); const body = error.json();
result = new ErrorDto(error.status, body.message, body.details); if (error.status === 412) {
result = new ErrorDto(error.status, 'Failed to make the update. Another user has made a change. Please reload.');
} else if (error.status !== 500) {
result = new ErrorDto(error.status, body.message, body.details);
}
} }
return Observable.throw(result); return Observable.throw(result);

1
src/Squidex/app/framework/declarations.ts

@ -50,3 +50,4 @@ export * from './utils/immutable-array';
export * from './utils/math-helper'; export * from './utils/math-helper';
export * from './utils/modal-view'; export * from './utils/modal-view';
export * from './utils/string-helper'; export * from './utils/string-helper';
export * from './utils/version';

21
src/Squidex/app/framework/utils/version.ts

@ -0,0 +1,21 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export class Version {
public get value() {
return this.currentValue;
}
constructor(
private currentValue: string = ''
) {
}
public update(newValue: string) {
this.currentValue = newValue;
}
}

6
src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts

@ -33,7 +33,7 @@ describe('ResolveAppLanguagesGuard', () => {
}); });
it('should navigate to 404 page if languages are not found', (done) => { it('should navigate to 404 page if languages are not found', (done) => {
appLanguagesService.setup(x => x.getLanguages('my-app')) appLanguagesService.setup(x => x.getLanguages('my-app', null))
.returns(() => Observable.of(null!)); .returns(() => Observable.of(null!));
const router = new RouterMockup(); const router = new RouterMockup();
@ -49,7 +49,7 @@ describe('ResolveAppLanguagesGuard', () => {
}); });
it('should navigate to 404 page if languages loading fails', (done) => { it('should navigate to 404 page if languages loading fails', (done) => {
appLanguagesService.setup(x => x.getLanguages('my-app')) appLanguagesService.setup(x => x.getLanguages('my-app', null))
.returns(() => Observable.throw(null!)); .returns(() => Observable.throw(null!));
const router = new RouterMockup(); const router = new RouterMockup();
@ -67,7 +67,7 @@ describe('ResolveAppLanguagesGuard', () => {
it('should return schema if loading succeeded', (done) => { it('should return schema if loading succeeded', (done) => {
const languages: AppLanguageDto[] = []; const languages: AppLanguageDto[] = [];
appLanguagesService.setup(x => x.getLanguages('my-app')) appLanguagesService.setup(x => x.getLanguages('my-app', null))
.returns(() => Observable.of(languages)); .returns(() => Observable.of(languages));
const router = new RouterMockup(); const router = new RouterMockup();

2
src/Squidex/app/shared/guards/resolve-app-languages.guard.ts

@ -26,7 +26,7 @@ export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[]> {
} }
const result = const result =
this.appLanguagesService.getLanguages(appName).toPromise() this.appLanguagesService.getLanguages(appName, null).toPromise()
.then(dto => { .then(dto => {
if (!dto) { if (!dto) {
this.router.navigate(['/404']); this.router.navigate(['/404']);

6
src/Squidex/app/shared/guards/resolve-content.guard.spec.ts

@ -43,7 +43,7 @@ describe('ResolveContentGuard', () => {
}); });
it('should navigate to 404 page if schema is not found', (done) => { it('should navigate to 404 page if schema is not found', (done) => {
appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null))
.returns(() => Observable.of(null!)); .returns(() => Observable.of(null!));
const router = new RouterMockup(); const router = new RouterMockup();
@ -59,7 +59,7 @@ describe('ResolveContentGuard', () => {
}); });
it('should navigate to 404 page if schema loading fails', (done) => { it('should navigate to 404 page if schema loading fails', (done) => {
appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null))
.returns(() => Observable.throw(null!)); .returns(() => Observable.throw(null!));
const router = new RouterMockup(); const router = new RouterMockup();
@ -77,7 +77,7 @@ describe('ResolveContentGuard', () => {
it('should return schema if loading succeeded', (done) => { it('should return schema if loading succeeded', (done) => {
const schema = {}; const schema = {};
appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null))
.returns(() => Observable.of(schema)); .returns(() => Observable.of(schema));
const router = new RouterMockup(); const router = new RouterMockup();

2
src/Squidex/app/shared/guards/resolve-content.guard.ts

@ -28,7 +28,7 @@ export class ResolveContentGuard implements Resolve<ContentDto> {
} }
const result = const result =
this.contentsService.getContent(appName, schemaName, contentId).toPromise() this.contentsService.getContent(appName, schemaName, contentId, null).toPromise()
.then(dto => { .then(dto => {
if (!dto) { if (!dto) {
this.router.navigate(['/404']); this.router.navigate(['/404']);

8
src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts

@ -38,7 +38,7 @@ describe('ResolvePublishedSchemaGuard', () => {
}); });
it('should navigate to 404 page if schema is not found', (done) => { it('should navigate to 404 page if schema is not found', (done) => {
schemasService.setup(x => x.getSchema('my-app', 'my-schema')) schemasService.setup(x => x.getSchema('my-app', 'my-schema', null))
.returns(() => Observable.of(null!)); .returns(() => Observable.of(null!));
const router = new RouterMockup(); const router = new RouterMockup();
@ -54,7 +54,7 @@ describe('ResolvePublishedSchemaGuard', () => {
}); });
it('should navigate to 404 page if schema loading fails', (done) => { it('should navigate to 404 page if schema loading fails', (done) => {
schemasService.setup(x => x.getSchema('my-app', 'my-schema')) schemasService.setup(x => x.getSchema('my-app', 'my-schema', null))
.returns(() => Observable.throw(null)); .returns(() => Observable.throw(null));
const router = new RouterMockup(); const router = new RouterMockup();
@ -72,7 +72,7 @@ describe('ResolvePublishedSchemaGuard', () => {
it('should navigate to 404 page if schema not published', (done) => { it('should navigate to 404 page if schema not published', (done) => {
const schema = { isPublished: false }; const schema = { isPublished: false };
schemasService.setup(x => x.getSchema('my-app', 'my-schema')) schemasService.setup(x => x.getSchema('my-app', 'my-schema', null))
.returns(() => Observable.of(schema)); .returns(() => Observable.of(schema));
const router = new RouterMockup(); const router = new RouterMockup();
@ -90,7 +90,7 @@ describe('ResolvePublishedSchemaGuard', () => {
it('should return schema if loading succeeded', (done) => { it('should return schema if loading succeeded', (done) => {
const schema = { isPublished: true }; const schema = { isPublished: true };
schemasService.setup(x => x.getSchema('my-app', 'my-schema')) schemasService.setup(x => x.getSchema('my-app', 'my-schema', null))
.returns(() => Observable.of(schema)); .returns(() => Observable.of(schema));
const router = new RouterMockup(); const router = new RouterMockup();

2
src/Squidex/app/shared/guards/resolve-published-schema.guard.ts

@ -27,7 +27,7 @@ export class ResolvePublishedSchemaGuard implements Resolve<SchemaDetailsDto> {
} }
const result = const result =
this.schemasService.getSchema(appName, schemaName).toPromise() this.schemasService.getSchema(appName, schemaName, null).toPromise()
.then(dto => { .then(dto => {
if (!dto || !dto.isPublished) { if (!dto || !dto.isPublished) {
this.router.navigate(['/404']); this.router.navigate(['/404']);

6
src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts

@ -38,7 +38,7 @@ describe('ResolveSchemaGuard', () => {
}); });
it('should navigate to 404 page if schema is not found', (done) => { it('should navigate to 404 page if schema is not found', (done) => {
schemasService.setup(x => x.getSchema('my-app', 'my-schema')) schemasService.setup(x => x.getSchema('my-app', 'my-schema', null))
.returns(() => Observable.of(null!)); .returns(() => Observable.of(null!));
const router = new RouterMockup(); const router = new RouterMockup();
@ -54,7 +54,7 @@ describe('ResolveSchemaGuard', () => {
}); });
it('should navigate to 404 page if schema loading fails', (done) => { it('should navigate to 404 page if schema loading fails', (done) => {
schemasService.setup(x => x.getSchema('my-app', 'my-schema')) schemasService.setup(x => x.getSchema('my-app', 'my-schema', null))
.returns(() => Observable.throw(null!)); .returns(() => Observable.throw(null!));
const router = new RouterMockup(); const router = new RouterMockup();
@ -72,7 +72,7 @@ describe('ResolveSchemaGuard', () => {
it('should return schema if loading succeeded', (done) => { it('should return schema if loading succeeded', (done) => {
const schema = {}; const schema = {};
schemasService.setup(x => x.getSchema('my-app', 'my-schema')) schemasService.setup(x => x.getSchema('my-app', 'my-schema', null))
.returns(() => Observable.of(schema)); .returns(() => Observable.of(schema));
const router = new RouterMockup(); const router = new RouterMockup();

2
src/Squidex/app/shared/guards/resolve-schema.guard.ts

@ -27,7 +27,7 @@ export class ResolveSchemaGuard implements Resolve<SchemaDetailsDto> {
} }
const result = const result =
this.schemasService.getSchema(appName, schemaName).toPromise() this.schemasService.getSchema(appName, schemaName, null).toPromise()
.then(dto => { .then(dto => {
if (!dto) { if (!dto) {
this.router.navigate(['/404']); this.router.navigate(['/404']);

20
src/Squidex/app/shared/services/app-clients.service.spec.ts

@ -16,12 +16,14 @@ import {
AppClientsService, AppClientsService,
AuthService, AuthService,
CreateAppClientDto, CreateAppClientDto,
UpdateAppClientDto UpdateAppClientDto,
Version
} from './../'; } from './../';
describe('AppClientsService', () => { describe('AppClientsService', () => {
let authService: IMock<AuthService>; let authService: IMock<AuthService>;
let appClientsService: AppClientsService; let appClientsService: AppClientsService;
let version = new Version('1');
let http: IMock<Http>; let http: IMock<Http>;
beforeEach(() => { beforeEach(() => {
@ -32,7 +34,7 @@ describe('AppClientsService', () => {
}); });
it('should make get request to get app clients', () => { it('should make get request to get app clients', () => {
authService.setup(x => x.authGet('http://service/p/api/apps/my-app/clients')) authService.setup(x => x.authGet('http://service/p/api/apps/my-app/clients', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -52,7 +54,7 @@ describe('AppClientsService', () => {
let clients: AppClientDto[] | null = null; let clients: AppClientDto[] | null = null;
appClientsService.getClients('my-app').subscribe(result => { appClientsService.getClients('my-app', version).subscribe(result => {
clients = result; clients = result;
}).unsubscribe(); }).unsubscribe();
@ -68,7 +70,7 @@ describe('AppClientsService', () => {
it('should make post request to create client', () => { it('should make post request to create client', () => {
const dto = new CreateAppClientDto('client1'); const dto = new CreateAppClientDto('client1');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', dto)) authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -84,7 +86,7 @@ describe('AppClientsService', () => {
let client: AppClientDto | null = null; let client: AppClientDto | null = null;
appClientsService.postClient('my-app', dto).subscribe(result => { appClientsService.postClient('my-app', dto, version).subscribe(result => {
client = result; client = result;
}); });
@ -97,7 +99,7 @@ describe('AppClientsService', () => {
it('should make put request to rename client', () => { it('should make put request to rename client', () => {
const dto = new UpdateAppClientDto('Client 1 New'); const dto = new UpdateAppClientDto('Client 1 New');
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', dto)) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -105,13 +107,13 @@ describe('AppClientsService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
appClientsService.updateClient('my-app', 'client1', dto); appClientsService.updateClient('my-app', 'client1', dto, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make delete request to remove client', () => { it('should make delete request to remove client', () => {
authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/clients/client1')) authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/clients/client1', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -119,7 +121,7 @@ describe('AppClientsService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
appClientsService.deleteClient('my-app', 'client1'); appClientsService.deleteClient('my-app', 'client1', version);
authService.verifyAll(); authService.verifyAll();
}); });

18
src/Squidex/app/shared/services/app-clients.service.ts

@ -11,7 +11,7 @@ import { Observable } from 'rxjs';
import 'framework/angular/http-extensions'; import 'framework/angular/http-extensions';
import { ApiUrlConfig } from 'framework'; import { ApiUrlConfig, Version } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
export class AppClientDto { export class AppClientDto {
@ -54,10 +54,10 @@ export class AppClientsService {
) { ) {
} }
public getClients(appName: string): Observable<AppClientDto[]> { public getClients(appName: string, version: Version): Observable<AppClientDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return this.authService.authGet(url) return this.authService.authGet(url, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
const items: any[] = response; const items: any[] = response;
@ -72,10 +72,10 @@ export class AppClientsService {
.catchError('Failed to load clients. Please reload.'); .catchError('Failed to load clients. Please reload.');
} }
public postClient(appName: string, dto: CreateAppClientDto): Observable<AppClientDto> { public postClient(appName: string, dto: CreateAppClientDto, version: Version): Observable<AppClientDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return this.authService.authPost(url, dto) return this.authService.authPost(url, dto, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
return new AppClientDto( return new AppClientDto(
@ -86,17 +86,17 @@ export class AppClientsService {
.catchError('Failed to add client. Please reload.'); .catchError('Failed to add client. Please reload.');
} }
public updateClient(appName: string, id: string, dto: UpdateAppClientDto): Observable<any> { public updateClient(appName: string, id: string, dto: UpdateAppClientDto, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`);
return this.authService.authPut(url, dto) return this.authService.authPut(url, dto, version)
.catchError('Failed to revoke client. Please reload.'); .catchError('Failed to revoke client. Please reload.');
} }
public deleteClient(appName: string, id: string): Observable<any> { public deleteClient(appName: string, id: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`);
return this.authService.authDelete(url) return this.authService.authDelete(url, version)
.catchError('Failed to revoke client. Please reload.'); .catchError('Failed to revoke client. Please reload.');
} }

16
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -13,12 +13,14 @@ import {
ApiUrlConfig, ApiUrlConfig,
AppContributorDto, AppContributorDto,
AppContributorsService, AppContributorsService,
AuthService AuthService,
Version
} from './../'; } from './../';
describe('AppContributorsService', () => { describe('AppContributorsService', () => {
let authService: IMock<AuthService>; let authService: IMock<AuthService>;
let appContributorsService: AppContributorsService; let appContributorsService: AppContributorsService;
let version = new Version('1');
beforeEach(() => { beforeEach(() => {
authService = Mock.ofType(AuthService); authService = Mock.ofType(AuthService);
@ -26,7 +28,7 @@ describe('AppContributorsService', () => {
}); });
it('should make get request to get app contributors', () => { it('should make get request to get app contributors', () => {
authService.setup(x => x.authGet('http://service/p/api/apps/my-app/contributors')) authService.setup(x => x.authGet('http://service/p/api/apps/my-app/contributors', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -44,7 +46,7 @@ describe('AppContributorsService', () => {
let contributors: AppContributorDto[] | null = null; let contributors: AppContributorDto[] | null = null;
appContributorsService.getContributors('my-app').subscribe(result => { appContributorsService.getContributors('my-app', version).subscribe(result => {
contributors = result; contributors = result;
}).unsubscribe(); }).unsubscribe();
@ -60,7 +62,7 @@ describe('AppContributorsService', () => {
it('should make post request to assign contributor', () => { it('should make post request to assign contributor', () => {
const dto = new AppContributorDto('123', 'Owner'); const dto = new AppContributorDto('123', 'Owner');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', dto)) authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -68,13 +70,13 @@ describe('AppContributorsService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
appContributorsService.postContributor('my-app', dto); appContributorsService.postContributor('my-app', dto, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make delete request to remove contributor', () => { it('should make delete request to remove contributor', () => {
authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/contributors/123')) authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/contributors/123', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -82,7 +84,7 @@ describe('AppContributorsService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
appContributorsService.deleteContributor('my-app', '123'); appContributorsService.deleteContributor('my-app', '123', version);
authService.verifyAll(); authService.verifyAll();
}); });

14
src/Squidex/app/shared/services/app-contributors.service.ts

@ -10,7 +10,7 @@ import { Observable } from 'rxjs';
import 'framework/angular/http-extensions'; import 'framework/angular/http-extensions';
import { ApiUrlConfig } from 'framework'; import { ApiUrlConfig, Version } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
export class AppContributorDto { export class AppContributorDto {
@ -29,10 +29,10 @@ export class AppContributorsService {
) { ) {
} }
public getContributors(appName: string): Observable<AppContributorDto[]> { public getContributors(appName: string, version: Version): Observable<AppContributorDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return this.authService.authGet(url) return this.authService.authGet(url, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
const items: any[] = response; const items: any[] = response;
@ -46,17 +46,17 @@ export class AppContributorsService {
.catchError('Failed to load contributors. Please reload.'); .catchError('Failed to load contributors. Please reload.');
} }
public postContributor(appName: string, dto: AppContributorDto): Observable<any> { public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return this.authService.authPost(url, dto) return this.authService.authPost(url, dto, version)
.catchError('Failed to add contributors. Please reload.'); .catchError('Failed to add contributors. Please reload.');
} }
public deleteContributor(appName: string, contributorId: string): Observable<any> { public deleteContributor(appName: string, contributorId: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`);
return this.authService.authDelete(url) return this.authService.authDelete(url, version)
.catchError('Failed to delete contributors. Please reload.'); .catchError('Failed to delete contributors. Please reload.');
} }
} }

20
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -15,12 +15,14 @@ import {
AppLanguageDto, AppLanguageDto,
AppLanguagesService, AppLanguagesService,
AuthService, AuthService,
UpdateAppLanguageDto UpdateAppLanguageDto,
Version
} from './../'; } from './../';
describe('AppLanguagesService', () => { describe('AppLanguagesService', () => {
let authService: IMock<AuthService>; let authService: IMock<AuthService>;
let appLanguagesService: AppLanguagesService; let appLanguagesService: AppLanguagesService;
let version = new Version('1');
beforeEach(() => { beforeEach(() => {
authService = Mock.ofType(AuthService); authService = Mock.ofType(AuthService);
@ -28,7 +30,7 @@ describe('AppLanguagesService', () => {
}); });
it('should make get request to get app languages', () => { it('should make get request to get app languages', () => {
authService.setup(x => x.authGet('http://service/p/api/apps/my-app/languages')) authService.setup(x => x.authGet('http://service/p/api/apps/my-app/languages', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -47,7 +49,7 @@ describe('AppLanguagesService', () => {
let languages: AppLanguageDto[] | null = null; let languages: AppLanguageDto[] | null = null;
appLanguagesService.getLanguages('my-app').subscribe(result => { appLanguagesService.getLanguages('my-app', version).subscribe(result => {
languages = result; languages = result;
}).unsubscribe(); }).unsubscribe();
@ -63,7 +65,7 @@ describe('AppLanguagesService', () => {
it('should make post request to add language', () => { it('should make post request to add language', () => {
const dto = new AddAppLanguageDto('de'); const dto = new AddAppLanguageDto('de');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', dto)) authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -78,7 +80,7 @@ describe('AppLanguagesService', () => {
let language: AppLanguageDto | null = null; let language: AppLanguageDto | null = null;
appLanguagesService.postLanguages('my-app', dto).subscribe(result => { appLanguagesService.postLanguages('my-app', dto, version).subscribe(result => {
language = result; language = result;
}); });
@ -91,7 +93,7 @@ describe('AppLanguagesService', () => {
it('should make put request to make master language', () => { it('should make put request to make master language', () => {
const dto = new UpdateAppLanguageDto(true); const dto = new UpdateAppLanguageDto(true);
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', dto)) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -99,13 +101,13 @@ describe('AppLanguagesService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
appLanguagesService.updateLanguage('my-app', 'de', dto); appLanguagesService.updateLanguage('my-app', 'de', dto, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make delete request to remove language', () => { it('should make delete request to remove language', () => {
authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/languages/de')) authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/languages/de', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -113,7 +115,7 @@ describe('AppLanguagesService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
appLanguagesService.deleteLanguage('my-app', 'de'); appLanguagesService.deleteLanguage('my-app', 'de', version);
authService.verifyAll(); authService.verifyAll();
}); });

18
src/Squidex/app/shared/services/app-languages.service.ts

@ -10,7 +10,7 @@ import { Observable } from 'rxjs';
import 'framework/angular/http-extensions'; import 'framework/angular/http-extensions';
import { ApiUrlConfig } from 'framework'; import { ApiUrlConfig, Version } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
export class AppLanguageDto { export class AppLanguageDto {
@ -44,10 +44,10 @@ export class AppLanguagesService {
) { ) {
} }
public getLanguages(appName: string): Observable<AppLanguageDto[]> { public getLanguages(appName: string, version: Version): Observable<AppLanguageDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return this.authService.authGet(url) return this.authService.authGet(url, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
const items: any[] = response; const items: any[] = response;
@ -62,10 +62,10 @@ export class AppLanguagesService {
.catchError('Failed to load languages. Please reload.'); .catchError('Failed to load languages. Please reload.');
} }
public postLanguages(appName: string, dto: AddAppLanguageDto): Observable<AppLanguageDto> { public postLanguages(appName: string, dto: AddAppLanguageDto, version: Version): Observable<AppLanguageDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return this.authService.authPost(url, dto) return this.authService.authPost(url, dto, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
return new AppLanguageDto( return new AppLanguageDto(
@ -76,17 +76,17 @@ export class AppLanguagesService {
.catchError('Failed to add language. Please reload.'); .catchError('Failed to add language. Please reload.');
} }
public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto): Observable<any> { public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return this.authService.authPut(url, dto) return this.authService.authPut(url, dto, version)
.catchError('Failed to change language. Please reload.'); .catchError('Failed to change language. Please reload.');
} }
public deleteLanguage(appName: string, languageCode: string): Observable<any> { public deleteLanguage(appName: string, languageCode: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return this.authService.authDelete(url) return this.authService.authDelete(url, version)
.catchError('Failed to add language. Please reload.'); .catchError('Failed to add language. Please reload.');
} }
} }

67
src/Squidex/app/shared/services/auth.service.ts

@ -16,7 +16,7 @@ import {
UserManager UserManager
} from 'oidc-client'; } from 'oidc-client';
import { ApiUrlConfig } from 'framework'; import { ApiUrlConfig, Version } from 'framework';
export class Profile { export class Profile {
public get id(): string { public get id(): string {
@ -169,46 +169,56 @@ export class AuthService {
return resultPromise; return resultPromise;
} }
public authGet(url: string, options?: RequestOptions): Observable<Response> { public authGet(url: string, version?: Version, options?: RequestOptions): Observable<Response> {
options = this.setRequestOptions(options); options = this.setRequestOptions(options, version);
return this.checkResponse(this.http.get(url, options)); return this.checkResponse(this.http.get(url, options), version);
} }
public authPut(url: string, data: any, options?: RequestOptions): Observable<Response> { public authPut(url: string, data: any, version?: Version, options?: RequestOptions): Observable<Response> {
options = this.setRequestOptions(options); options = this.setRequestOptions(options, version);
return this.checkResponse(this.http.put(url, data, options)); return this.checkResponse(this.http.put(url, data, options), version);
} }
public authDelete(url: string, options?: RequestOptions): Observable<Response> { public authDelete(url: string, version?: Version, options?: RequestOptions): Observable<Response> {
options = this.setRequestOptions(options); options = this.setRequestOptions(options, version);
return this.checkResponse(this.http.delete(url, options)); return this.checkResponse(this.http.delete(url, options), version);
} }
public authPost(url: string, data: any, options?: RequestOptions): Observable<Response> { public authPost(url: string, data: any, version?: Version, options?: RequestOptions): Observable<Response> {
options = this.setRequestOptions(options); options = this.setRequestOptions(options, version);
return this.checkResponse(this.http.post(url, data, options)); return this.checkResponse(this.http.post(url, data, options), version);
} }
private checkResponse(response: Observable<Response>) { private checkResponse(responseStream: Observable<Response>, version?: Version) {
return response.catch((error: Response) => { return responseStream
if (error.status === 401 || error.status === 404) { .do((response: Response) => {
this.logoutRedirect(); if (version && response.status.toString().indexOf('2') === 0) {
const etag = response.headers.get('etag');
return Observable.empty<Response>(); if (etag) {
} else if (error.status === 403) { version.update(etag);
this.router.navigate(['/404']); }
}
})
.catch((error: Response) => {
if (error.status === 401 || error.status === 404) {
this.logoutRedirect();
return Observable.empty<Response>(); return Observable.empty<Response>();
} } else if (error.status === 403) {
return Observable.throw(error); this.router.navigate(['/404']);
});
return Observable.empty<Response>();
}
return Observable.throw(error);
});
} }
private setRequestOptions(options?: RequestOptions) { private setRequestOptions(options?: RequestOptions, version?: Version) {
if (!options) { if (!options) {
options = new RequestOptions(); options = new RequestOptions();
} }
@ -219,12 +229,17 @@ export class AuthService {
} }
options.headers.append('Accept-Language', '*'); options.headers.append('Accept-Language', '*');
options.headers.append('Pragma', 'no-cache');
if (version && version.value.length > 0) {
options.headers.append('If-Match', version.value);
}
if (this.currentUser && this.currentUser.user) { if (this.currentUser && this.currentUser.user) {
options.headers.append('Authorization', `${this.currentUser.user.token_type} ${this.currentUser.user.access_token}`); options.headers.append('Authorization', `${this.currentUser.user.token_type} ${this.currentUser.user.access_token}`);
} }
options.headers.append('Pragma', 'no-cache');
return options; return options;
} }
} }

49
src/Squidex/app/shared/services/contents.service.spec.ts

@ -16,12 +16,14 @@ import {
ContentDto, ContentDto,
ContentsDto, ContentsDto,
ContentsService, ContentsService,
DateTime DateTime,
Version
} from './../'; } from './../';
describe('ContentsService', () => { describe('ContentsService', () => {
let authService: IMock<AuthService>; let authService: IMock<AuthService>;
let contentsService: ContentsService; let contentsService: ContentsService;
let version = new Version('1');
beforeEach(() => { beforeEach(() => {
authService = Mock.ofType(AuthService); authService = Mock.ofType(AuthService);
@ -42,6 +44,7 @@ describe('ContentsService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
version: 11,
data: {} data: {}
}, { }, {
id: 'id2', id: 'id2',
@ -50,6 +53,7 @@ describe('ContentsService', () => {
createdBy: 'Created2', createdBy: 'Created2',
lastModified: '2017-10-12T10:10', lastModified: '2017-10-12T10:10',
lastModifiedBy: 'LastModifiedBy2', lastModifiedBy: 'LastModifiedBy2',
version: 22,
data: {} data: {}
}] }]
} }
@ -66,8 +70,16 @@ describe('ContentsService', () => {
expect(contents).toEqual( expect(contents).toEqual(
new ContentsDto(10, [ new ContentsDto(10, [
new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {}), new ContentDto('id1', true, 'Created1', 'LastModifiedBy1',
new ContentDto('id2', true, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), {}) DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
{},
new Version('11')),
new ContentDto('id2', true, 'Created2', 'LastModifiedBy2',
DateTime.parseISO_UTC('2016-10-12T10:10'),
DateTime.parseISO_UTC('2017-10-12T10:10'),
{},
new Version('22'))
])); ]));
authService.verifyAll(); authService.verifyAll();
@ -118,7 +130,7 @@ describe('ContentsService', () => {
}); });
it('should make get request to get content', () => { it('should make get request to get content', () => {
authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema/content1?hidden=true')) authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema/content1?hidden=true', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -129,6 +141,7 @@ describe('ContentsService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
version: 11,
data: {} data: {}
} }
}) })
@ -138,12 +151,16 @@ describe('ContentsService', () => {
let content: ContentDto | null = null; let content: ContentDto | null = null;
contentsService.getContent('my-app', 'my-schema', 'content1').subscribe(result => { contentsService.getContent('my-app', 'my-schema', 'content1', version).subscribe(result => {
content = result; content = result;
}).unsubscribe(); }).unsubscribe();
expect(content).toEqual( expect(content).toEqual(
new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {})); new ContentDto('id1', true, 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
{},
new Version('11')));
authService.verifyAll(); authService.verifyAll();
}); });
@ -151,7 +168,7 @@ describe('ContentsService', () => {
it('should make post request to create content', () => { it('should make post request to create content', () => {
const dto = {}; const dto = {};
authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema', dto)) authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -165,7 +182,7 @@ describe('ContentsService', () => {
let created: EntityCreatedDto | null = null; let created: EntityCreatedDto | null = null;
contentsService.postContent('my-app', 'my-schema', dto).subscribe(result => { contentsService.postContent('my-app', 'my-schema', dto, version).subscribe(result => {
created = result; created = result;
}); });
@ -178,7 +195,7 @@ describe('ContentsService', () => {
it('should make put request to update content', () => { it('should make put request to update content', () => {
const dto = {}; const dto = {};
authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1', dto)) authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -186,13 +203,13 @@ describe('ContentsService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
contentsService.putContent('my-app', 'my-schema', 'content1', dto); contentsService.putContent('my-app', 'my-schema', 'content1', dto, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to publish content', () => { it('should make put request to publish content', () => {
authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/publish', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/publish', It.isAny(), version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -200,13 +217,13 @@ describe('ContentsService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
contentsService.publishContent('my-app', 'my-schema', 'content1'); contentsService.publishContent('my-app', 'my-schema', 'content1', version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to unpublish content', () => { it('should make put request to unpublish content', () => {
authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/unpublish', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/unpublish', It.isAny(), version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -214,13 +231,13 @@ describe('ContentsService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
contentsService.unpublishContent('my-app', 'my-schema', 'content1'); contentsService.unpublishContent('my-app', 'my-schema', 'content1', version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make delete request to delete content', () => { it('should make delete request to delete content', () => {
authService.setup(x => x.authDelete('http://service/p/api/content/my-app/my-schema/content1')) authService.setup(x => x.authDelete('http://service/p/api/content/my-app/my-schema/content1', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -228,7 +245,7 @@ describe('ContentsService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
contentsService.deleteContent('my-app', 'my-schema', 'content1'); contentsService.deleteContent('my-app', 'my-schema', 'content1', version);
authService.verifyAll(); authService.verifyAll();
}); });

36
src/Squidex/app/shared/services/contents.service.ts

@ -13,7 +13,8 @@ import 'framework/angular/http-extensions';
import { import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
EntityCreatedDto EntityCreatedDto,
Version
} from 'framework'; } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@ -34,7 +35,8 @@ export class ContentDto {
public readonly lastModifiedBy: string, public readonly lastModifiedBy: string,
public readonly created: DateTime, public readonly created: DateTime,
public readonly lastModified: DateTime, public readonly lastModified: DateTime,
public readonly data: any public readonly data: any,
public readonly version: Version
) { ) {
} }
} }
@ -83,16 +85,17 @@ export class ContentsService {
item.lastModifiedBy, item.lastModifiedBy,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
DateTime.parseISO_UTC(item.lastModified), DateTime.parseISO_UTC(item.lastModified),
item.data); item.data,
new Version(item.version.toString()));
})); }));
}) })
.catchError('Failed to load contents. Please reload.'); .catchError('Failed to load contents. Please reload.');
} }
public getContent(appName: string, schemaName: string, id: string): Observable<ContentDto> { public getContent(appName: string, schemaName: string, id: string, version: Version): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}?hidden=true`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}?hidden=true`);
return this.authService.authGet(url) return this.authService.authGet(url, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
return new ContentDto( return new ContentDto(
@ -102,15 +105,16 @@ export class ContentsService {
response.lastModifiedBy, response.lastModifiedBy,
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.created),
DateTime.parseISO_UTC(response.lastModified), DateTime.parseISO_UTC(response.lastModified),
response.data); response.data,
new Version(response.version.toString()));
}) })
.catchError('Failed to load content. Please reload.'); .catchError('Failed to load content. Please reload.');
} }
public postContent(appName: string, schemaName: string, dto: any): Observable<EntityCreatedDto> { public postContent(appName: string, schemaName: string, dto: any, version: Version): Observable<EntityCreatedDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}`);
return this.authService.authPost(url, dto) return this.authService.authPost(url, dto, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
return new EntityCreatedDto(response.id); return new EntityCreatedDto(response.id);
@ -118,31 +122,31 @@ export class ContentsService {
.catchError('Failed to create content. Please reload.'); .catchError('Failed to create content. Please reload.');
} }
public putContent(appName: string, schemaName: string, id: string, dto: any): Observable<any> { public putContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return this.authService.authPut(url, dto) return this.authService.authPut(url, dto, version)
.catchError('Failed to update content. Please reload.'); .catchError('Failed to update content. Please reload.');
} }
public publishContent(appName: string, schemaName: string, id: string): Observable<any> { public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`);
return this.authService.authPut(url, {}) return this.authService.authPut(url, {}, version)
.catchError('Failed to publish content. Please reload.'); .catchError('Failed to publish content. Please reload.');
} }
public unpublishContent(appName: string, schemaName: string, id: string): Observable<any> { public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`);
return this.authService.authPut(url, {}) return this.authService.authPut(url, {}, version)
.catchError('Failed to unpublish content. Please reload.'); .catchError('Failed to unpublish content. Please reload.');
} }
public deleteContent(appName: string, schemaName: string, id: string): Observable<any> { public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return this.authService.authDelete(url) return this.authService.authDelete(url, version)
.catchError('Failed to delete content. Please reload.'); .catchError('Failed to delete content. Please reload.');
} }
} }

91
src/Squidex/app/shared/services/schemas.service.spec.ts

@ -22,12 +22,14 @@ import {
SchemaDto, SchemaDto,
SchemasService, SchemasService,
UpdateFieldDto, UpdateFieldDto,
UpdateSchemaDto UpdateSchemaDto,
Version
} from './../'; } from './../';
describe('SchemasService', () => { describe('SchemasService', () => {
let authService: IMock<AuthService>; let authService: IMock<AuthService>;
let schemasService: SchemasService; let schemasService: SchemasService;
let version = new Version('1');
beforeEach(() => { beforeEach(() => {
authService = Mock.ofType(AuthService); authService = Mock.ofType(AuthService);
@ -52,6 +54,7 @@ describe('SchemasService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
version: 11,
data: {} data: {}
}, { }, {
id: 'id2', id: 'id2',
@ -62,6 +65,7 @@ describe('SchemasService', () => {
createdBy: 'Created2', createdBy: 'Created2',
lastModified: '2017-10-12T10:10', lastModified: '2017-10-12T10:10',
lastModifiedBy: 'LastModifiedBy2', lastModifiedBy: 'LastModifiedBy2',
version: 22,
data: {} data: {}
}] }]
}) })
@ -76,8 +80,14 @@ describe('SchemasService', () => {
}).unsubscribe(); }).unsubscribe();
expect(schemas).toEqual([ expect(schemas).toEqual([
new SchemaDto('id1', 'name1', 'label1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10')), new SchemaDto('id1', 'name1', 'label1', true, 'Created1', 'LastModifiedBy1',
new SchemaDto('id2', 'name2', 'label2', true, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10')) DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
new Version('11')),
new SchemaDto('id2', 'name2', 'label2', true, 'Created2', 'LastModifiedBy2',
DateTime.parseISO_UTC('2016-10-12T10:10'),
DateTime.parseISO_UTC('2017-10-12T10:10'),
new Version('22'))
]); ]);
authService.verifyAll(); authService.verifyAll();
@ -98,13 +108,14 @@ describe('SchemasService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
version: 11,
fields: [{ fields: [{
fieldId: 1, fieldId: 1,
name: 'field1', name: 'field1',
isHidden: true, isHidden: true,
isDisabled: true, isDisabled: true,
properties: { properties: {
fieldType: 'number' fieldType: 'Number'
} }
}, { }, {
fieldId: 2, fieldId: 2,
@ -112,7 +123,7 @@ describe('SchemasService', () => {
isHidden: true, isHidden: true,
isDisabled: true, isDisabled: true,
properties: { properties: {
fieldType: 'string' fieldType: 'String'
} }
}, { }, {
fieldId: 3, fieldId: 3,
@ -120,7 +131,7 @@ describe('SchemasService', () => {
isHidden: true, isHidden: true,
isDisabled: true, isDisabled: true,
properties: { properties: {
fieldType: 'boolean' fieldType: 'Boolean'
} }
}, { }, {
fieldId: 4, fieldId: 4,
@ -128,7 +139,7 @@ describe('SchemasService', () => {
isHidden: true, isHidden: true,
isDisabled: true, isDisabled: true,
properties: { properties: {
fieldType: 'dateTime' fieldType: 'DateTime'
} }
}, { }, {
fieldId: 5, fieldId: 5,
@ -136,7 +147,7 @@ describe('SchemasService', () => {
isHidden: true, isHidden: true,
isDisabled: true, isDisabled: true,
properties: { properties: {
fieldType: 'json' fieldType: 'Json'
} }
}] }]
} }
@ -147,19 +158,21 @@ describe('SchemasService', () => {
let schema: SchemaDetailsDto | null = null; let schema: SchemaDetailsDto | null = null;
schemasService.getSchema('my-app', 'my-schema').subscribe(result => { schemasService.getSchema('my-app', 'my-schema', version).subscribe(result => {
schema = result; schema = result;
}).unsubscribe(); }).unsubscribe();
expect(schema).toEqual( expect(schema).toEqual(
new SchemaDetailsDto('id1', 'name1', 'label1', 'hints1', true, 'Created1', 'LastModifiedBy1', new SchemaDetailsDto('id1', 'name1', 'label1', 'hints1', true, 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), [ DateTime.parseISO_UTC('2017-12-12T10:10'),
new FieldDto(1, 'field1', true, true, createProperties('number')), new Version('11'),
new FieldDto(2, 'field2', true, true, createProperties('string')), [
new FieldDto(3, 'field3', true, true, createProperties('boolean')), new FieldDto(1, 'field1', true, true, createProperties('Number')),
new FieldDto(4, 'field4', true, true, createProperties('dateTime')), new FieldDto(2, 'field2', true, true, createProperties('String')),
new FieldDto(5, 'field5', true, true, createProperties('json')) new FieldDto(3, 'field3', true, true, createProperties('Boolean')),
new FieldDto(4, 'field4', true, true, createProperties('DateTime')),
new FieldDto(5, 'field5', true, true, createProperties('Json'))
])); ]));
authService.verifyAll(); authService.verifyAll();
@ -168,7 +181,7 @@ describe('SchemasService', () => {
it('should make post request to create schema', () => { it('should make post request to create schema', () => {
const dto = new CreateSchemaDto('name'); const dto = new CreateSchemaDto('name');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas', dto)) authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -182,7 +195,7 @@ describe('SchemasService', () => {
let created: EntityCreatedDto | null = null; let created: EntityCreatedDto | null = null;
schemasService.postSchema('my-app', dto).subscribe(result => { schemasService.postSchema('my-app', dto, version).subscribe(result => {
created = result; created = result;
}); });
@ -193,9 +206,9 @@ describe('SchemasService', () => {
}); });
it('should make post request to add field', () => { it('should make post request to add field', () => {
const dto = new AddFieldDto('name', createProperties('number')); const dto = new AddFieldDto('name', createProperties('Number'));
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/fields', dto)) authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/fields', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
@ -209,7 +222,7 @@ describe('SchemasService', () => {
let created: EntityCreatedDto | null = null; let created: EntityCreatedDto | null = null;
schemasService.postField('my-app', 'my-schema', dto).subscribe(result => { schemasService.postField('my-app', 'my-schema', dto, version).subscribe(result => {
created = result; created = result;
}); });
@ -222,7 +235,7 @@ describe('SchemasService', () => {
it('should make put request to update schema', () => { it('should make put request to update schema', () => {
const dto = new UpdateSchemaDto('label', 'hints'); const dto = new UpdateSchemaDto('label', 'hints');
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema', dto)) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -230,15 +243,15 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.putSchema('my-app', 'my-schema', dto); schemasService.putSchema('my-app', 'my-schema', dto, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to update field', () => { it('should make put request to update field', () => {
const dto = new UpdateFieldDto(createProperties('number')); const dto = new UpdateFieldDto(createProperties('Number'));
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', dto)) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', dto, version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -246,13 +259,13 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.putField('my-app', 'my-schema', 1, dto); schemasService.putField('my-app', 'my-schema', 1, dto, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to publish schema', () => { it('should make put request to publish schema', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/publish', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/publish', It.isAny(), version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -260,13 +273,13 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.publishSchema('my-app', 'my-schema'); schemasService.publishSchema('my-app', 'my-schema', version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to unpublish schema', () => { it('should make put request to unpublish schema', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/unpublish', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/unpublish', It.isAny(), version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -274,13 +287,13 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.unpublishSchema('my-app', 'my-schema'); schemasService.unpublishSchema('my-app', 'my-schema', version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to enable field', () => { it('should make put request to enable field', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/enable', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/enable', It.isAny(), version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -288,13 +301,13 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.enableField('my-app', 'my-schema', 1); schemasService.enableField('my-app', 'my-schema', 1, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to disable field', () => { it('should make put request to disable field', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/disable', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/disable', It.isAny(), version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -302,13 +315,13 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.disableField('my-app', 'my-schema', 1); schemasService.disableField('my-app', 'my-schema', 1, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to show field', () => { it('should make put request to show field', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/show', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/show', It.isAny(), version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -316,13 +329,13 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.showField('my-app', 'my-schema', 1); schemasService.showField('my-app', 'my-schema', 1, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make put request to hide field', () => { it('should make put request to hide field', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/hide', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/hide', It.isAny(), version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -330,13 +343,13 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.hideField('my-app', 'my-schema', 1); schemasService.hideField('my-app', 'my-schema', 1, version);
authService.verifyAll(); authService.verifyAll();
}); });
it('should make delete request to delete field', () => { it('should make delete request to delete field', () => {
authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema/fields/1')) authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', version))
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions() new ResponseOptions()
@ -344,7 +357,7 @@ describe('SchemasService', () => {
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
schemasService.deleteField('my-app', 'my-schema', 1); schemasService.deleteField('my-app', 'my-schema', 1, version);
authService.verifyAll(); authService.verifyAll();
}); });

57
src/Squidex/app/shared/services/schemas.service.ts

@ -13,7 +13,8 @@ import 'framework/angular/http-extensions';
import { import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
EntityCreatedDto EntityCreatedDto,
Version
} from 'framework'; } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@ -69,7 +70,8 @@ export class SchemaDto {
public readonly createdBy: string, public readonly createdBy: string,
public readonly lastModifiedBy: string, public readonly lastModifiedBy: string,
public readonly created: DateTime, public readonly created: DateTime,
public readonly lastModified: DateTime public readonly lastModified: DateTime,
public readonly version: Version
) { ) {
} }
} }
@ -85,6 +87,7 @@ export class SchemaDetailsDto {
public readonly lastModifiedBy: string, public readonly lastModifiedBy: string,
public readonly created: DateTime, public readonly created: DateTime,
public readonly lastModified: DateTime, public readonly lastModified: DateTime,
public readonly version: Version,
public readonly fields: FieldDto[] public readonly fields: FieldDto[]
) { ) {
} }
@ -246,13 +249,14 @@ export class SchemasService {
item.createdBy, item.createdBy,
item.lastModifiedBy, item.lastModifiedBy,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
DateTime.parseISO_UTC(item.lastModified)); DateTime.parseISO_UTC(item.lastModified),
new Version(item.version.toString()));
}); });
}) })
.catchError('Failed to load schemas. Please reload.'); .catchError('Failed to load schemas. Please reload.');
} }
public getSchema(appName: string, id: string): Observable<SchemaDetailsDto> { public getSchema(appName: string, id: string, version: Version): Observable<SchemaDetailsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`);
return this.authService.authGet(url) return this.authService.authGet(url)
@ -282,15 +286,16 @@ export class SchemasService {
response.lastModifiedBy, response.lastModifiedBy,
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.created),
DateTime.parseISO_UTC(response.lastModified), DateTime.parseISO_UTC(response.lastModified),
new Version(response.version.toString()),
fields); fields);
}) })
.catchError('Failed to load schema. Please reload.'); .catchError('Failed to load schema. Please reload.');
} }
public postSchema(appName: string, dto: CreateSchemaDto): Observable<EntityCreatedDto> { public postSchema(appName: string, dto: CreateSchemaDto, version: Version): Observable<EntityCreatedDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`);
return this.authService.authPost(url, dto) return this.authService.authPost(url, dto, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
return new EntityCreatedDto(response.id); return new EntityCreatedDto(response.id);
@ -298,10 +303,10 @@ export class SchemasService {
.catchError('Failed to create schema. Please reload.'); .catchError('Failed to create schema. Please reload.');
} }
public postField(appName: string, schemaName: string, dto: AddFieldDto): Observable<EntityCreatedDto> { public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable<EntityCreatedDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`);
return this.authService.authPost(url, dto) return this.authService.authPost(url, dto, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
return new EntityCreatedDto(response.id); return new EntityCreatedDto(response.id);
@ -309,66 +314,66 @@ export class SchemasService {
.catchError('Failed to add field. Please reload.'); .catchError('Failed to add field. Please reload.');
} }
public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto): Observable<any> { public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`);
return this.authService.authPut(url, dto) return this.authService.authPut(url, dto, version)
.catchError('Failed to update schema. Please reload.'); .catchError('Failed to update schema. Please reload.');
} }
public publishSchema(appName: string, schemaName: string): Observable<any> { public publishSchema(appName: string, schemaName: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`);
return this.authService.authPut(url, {}) return this.authService.authPut(url, {}, version)
.catchError('Failed to publish schema. Please reload.'); .catchError('Failed to publish schema. Please reload.');
} }
public unpublishSchema(appName: string, schemaName: string): Observable<any> { public unpublishSchema(appName: string, schemaName: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`);
return this.authService.authPut(url, {}) return this.authService.authPut(url, {}, version)
.catchError('Failed to unpublish schema. Please reload.'); .catchError('Failed to unpublish schema. Please reload.');
} }
public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto): Observable<any> { public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`);
return this.authService.authPut(url, dto) return this.authService.authPut(url, dto, version)
.catchError('Failed to update field. Please reload.'); .catchError('Failed to update field. Please reload.');
} }
public enableField(appName: string, schemaName: string, fieldId: number): Observable<any> { public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`);
return this.authService.authPut(url, {}) return this.authService.authPut(url, {}, version)
.catchError('Failed to enable field. Please reload.'); .catchError('Failed to enable field. Please reload.');
} }
public disableField(appName: string, schemaName: string, fieldId: number): Observable<any> { public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`);
return this.authService.authPut(url, {}) return this.authService.authPut(url, {}, version)
.catchError('Failed to disable field. Please reload.'); .catchError('Failed to disable field. Please reload.');
} }
public showField(appName: string, schemaName: string, fieldId: number): Observable<any> { public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`);
return this.authService.authPut(url, {}) return this.authService.authPut(url, {}, version)
.catchError('Failed to show field. Please reload.'); .catchError('Failed to show field. Please reload.');
} }
public hideField(appName: string, schemaName: string, fieldId: number): Observable<any> { public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`);
return this.authService.authPut(url, {}) return this.authService.authPut(url, {}, version)
.catchError('Failed to hide field. Please reload.'); .catchError('Failed to hide field. Please reload.');
} }
public deleteField(appName: string, schemaName: string, fieldId: number): Observable<any> { public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`);
return this.authService.authDelete(url) return this.authService.authDelete(url, version)
.catchError('Failed to delete field. Please reload.'); .catchError('Failed to delete field. Please reload.');
} }
} }

4
src/Squidex/app/theme/_bootstrap.scss

@ -151,8 +151,8 @@ body {
} }
&-cancel { &-cancel {
padding: .2rem; padding: .4rem;
font-size: 1.5rem; font-size: 1.1rem;
font-weight: normal; font-weight: normal;
} }

Loading…
Cancel
Save