Browse Source

Merge pull request #107 from Squidex/feature-history

Feature history
pull/111/head
Sebastian Stehle 9 years ago
committed by GitHub
parent
commit
555e168967
  1. 15
      src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs
  2. 2
      src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs
  3. 17
      src/Squidex.Domain.Apps.Events/Contents/ContentArchived.cs
  4. 17
      src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs
  5. 4
      src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs
  6. 4
      src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs
  7. 9
      src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs
  8. 6
      src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs
  9. 4
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs
  10. 8
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs
  11. 39
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  12. 9
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs
  13. 2
      src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs
  14. 8
      src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs
  15. 5
      src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs
  16. 4
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs
  17. 2
      src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs
  18. 13
      src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs
  19. 7
      src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs
  20. 4
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs
  21. 2
      src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs
  22. 2
      src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs
  23. 4
      src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs
  24. 2
      src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs
  25. 4
      src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs
  26. 14
      src/Squidex.Domain.Apps.Write/Contents/Commands/ArchiveContent.cs
  27. 14
      src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs
  28. 45
      src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs
  29. 46
      src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs
  30. 86
      src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs
  31. 19
      src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs
  32. 4
      src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs
  33. 4
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  34. 4
      src/Squidex/Config/Domain/WriteModule.cs
  35. 2
      src/Squidex/Controllers/Api/Assets/AssetsController.cs
  36. 5
      src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs
  37. 9
      src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs
  38. 9
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs
  39. 2
      src/Squidex/Controllers/Api/Schemas/SchemasController.cs
  40. 56
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  41. 54
      src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs
  42. 17
      src/Squidex/Controllers/ContentApi/Models/ContentDto.cs
  43. 6
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  44. 1
      src/Squidex/app/features/content/declarations.ts
  45. 5
      src/Squidex/app/features/content/module.ts
  46. 29
      src/Squidex/app/features/content/pages/content/content-history.component.html
  47. 39
      src/Squidex/app/features/content/pages/content/content-history.component.scss
  48. 107
      src/Squidex/app/features/content/pages/content/content-history.component.ts
  49. 11
      src/Squidex/app/features/content/pages/content/content-page.component.html
  50. 78
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  51. 18
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  52. 82
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  53. 40
      src/Squidex/app/features/content/pages/contents/search-form.component.html
  54. 7
      src/Squidex/app/features/content/pages/contents/search-form.component.scss
  55. 11
      src/Squidex/app/features/content/pages/contents/search-form.component.ts
  56. 9
      src/Squidex/app/features/content/pages/messages.ts
  57. 21
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  58. 12
      src/Squidex/app/features/content/shared/content-item.component.html
  59. 6
      src/Squidex/app/features/content/shared/content-item.component.ts
  60. 25
      src/Squidex/app/features/content/shared/references-editor.component.ts
  61. 2
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  62. 24
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  63. 9
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts
  64. 3
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  65. 2
      src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts
  66. 2
      src/Squidex/app/features/webhooks/pages/webhook.component.ts
  67. 2
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts
  68. 19
      src/Squidex/app/framework/angular/autocomplete.component.ts
  69. 12
      src/Squidex/app/framework/angular/confirm-click.directive.ts
  70. 3
      src/Squidex/app/framework/angular/copy.directive.ts
  71. 26
      src/Squidex/app/framework/angular/date-time-editor.component.ts
  72. 2
      src/Squidex/app/framework/angular/dialog-renderer.component.ts
  73. 13
      src/Squidex/app/framework/angular/dropdown.component.ts
  74. 31
      src/Squidex/app/framework/angular/geolocation-editor.component.ts
  75. 2
      src/Squidex/app/framework/angular/http-extensions-impl.ts
  76. 18
      src/Squidex/app/framework/angular/indeterminate-value.directive.ts
  77. 30
      src/Squidex/app/framework/angular/jscript-editor.component.ts
  78. 35
      src/Squidex/app/framework/angular/json-editor.component.ts
  79. 18
      src/Squidex/app/framework/angular/lowercase-input.directive.ts
  80. 24
      src/Squidex/app/framework/angular/markdown-editor.component.ts
  81. 4
      src/Squidex/app/framework/angular/modal-view.directive.ts
  82. 2
      src/Squidex/app/framework/angular/panel-container.directive.ts
  83. 24
      src/Squidex/app/framework/angular/rich-editor.component.ts
  84. 4
      src/Squidex/app/framework/angular/router-utils.ts
  85. 22
      src/Squidex/app/framework/angular/slider.component.ts
  86. 37
      src/Squidex/app/framework/angular/stars.component.ts
  87. 56
      src/Squidex/app/framework/angular/tag-editor.component.ts
  88. 21
      src/Squidex/app/framework/angular/toggle.component.ts
  89. 39
      src/Squidex/app/framework/angular/validators.ts
  90. 1
      src/Squidex/app/framework/declarations.ts
  91. 2
      src/Squidex/app/framework/services/local-cache.service.ts
  92. 92
      src/Squidex/app/framework/utils/types.spec.ts
  93. 70
      src/Squidex/app/framework/utils/types.ts
  94. 7
      src/Squidex/app/shared/components/asset.component.ts
  95. 4
      src/Squidex/app/shared/components/history.component.scss
  96. 4
      src/Squidex/app/shared/guards/resolve-app-languages.guard.ts
  97. 4
      src/Squidex/app/shared/guards/resolve-content.guard.ts
  98. 4
      src/Squidex/app/shared/guards/resolve-user.guard.ts
  99. 2
      src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts
  100. 2
      src/Squidex/app/shared/interceptors/auth.interceptor.ts

15
src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs

@ -49,6 +49,16 @@ namespace Squidex.Domain.Apps.Core.Scripting
EnableDisallow(engine);
EnableReject(engine, operationName);
engine.SetValue("operation", new Action(() =>
{
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
data.TryUpdate(out result);
}
}));
engine.SetValue("replace", new Action(() =>
{
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
@ -135,6 +145,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
contextInstance.FastAddProperty("user", new JintUser(engine, context.User), false, true, false);
}
if (!string.IsNullOrWhiteSpace(context.Operation))
{
contextInstance.FastAddProperty("operation", context.Operation, false, true, false);
}
engine.SetValue("ctx", contextInstance);
return engine;

2
src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs

@ -21,5 +21,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
public NamedContentData Data { get; set; }
public NamedContentData OldData { get; set; }
public string Operation { get; set; }
}
}

17
src/Squidex.Domain.Apps.Events/Contents/ContentArchived.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ContentArchived.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentArchived))]
public sealed class ContentArchived : ContentEvent
{
}
}

17
src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ContentRestored.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentRestored))]
public sealed class ContentRestored : ContentEvent
{
}
}

4
src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs

@ -21,8 +21,6 @@ namespace Squidex.Domain.Apps.Events.Schemas
public string ScriptDelete { get; set; }
public string ScriptPublish { get; set; }
public string ScriptUnpublish { get; set; }
public string ScriptChange { get; set; }
}
}

4
src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs

@ -36,10 +36,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets
[BsonElement]
public bool IsImage { get; set; }
[BsonRequired]
[BsonElement]
public bool IsDeleted { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }

9
src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs

@ -33,7 +33,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetEntity> collection)
{
return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.IsDeleted).Descending(x => x.LastModified).Ascending(x => x.FileName).Ascending(x => x.MimeType));
return collection.Indexes.CreateOneAsync(
Index.Ascending(x => x.AppId)
.Ascending(x => x.FileName)
.Ascending(x => x.MimeType)
.Descending(x => x.LastModified));
}
public async Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, IList<Guid> assetIds)
@ -80,8 +84,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets
{
var filters = new List<FilterDefinition<MongoAssetEntity>>
{
Filter.Eq(x => x.AppId, appId),
Filter.Eq(x => x.IsDeleted, false)
Filter.Eq(x => x.AppId, appId)
};
if (ids != null && ids.Count > 0)

6
src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs

@ -7,6 +7,7 @@
// ==========================================================================
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Read.MongoDb.Utils;
using Squidex.Infrastructure.CQRS.Events;
@ -58,10 +59,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets
protected Task On(AssetDeleted @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(@event, headers, a =>
{
a.IsDeleted = true;
});
return Collection.DeleteOneAsync(x => x.Id == @event.AssetId);
}
}
}

4
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs

@ -47,6 +47,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
[BsonElement("pu")]
public bool IsPublished { get; set; }
[BsonRequired]
[BsonElement("dl")]
public bool IsArchived { get; set; }
[BsonRequired]
[BsonElement("dt")]
public string DataText { get; set; }

8
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs

@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
this.database = database;
}
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, HashSet<Guid> ids, ODataUriParser odataQuery)
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery)
{
var collection = GetCollection(app.Id);
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{
cursor =
collection
.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished)
.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived)
.Take(odataQuery)
.Skip(odataQuery)
.Sort(odataQuery, schema.SchemaDef);
@ -104,14 +104,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
return entities;
}
public Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, HashSet<Guid> ids, ODataUriParser odataQuery)
public Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery)
{
var collection = GetCollection(app.Id);
IFindFluent<MongoContentEntity, MongoContentEntity> cursor;
try
{
cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished);
cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived);
}
catch (NotSupportedException)
{

39
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -62,6 +62,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsPublished));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsArchived));
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText));
});
}
@ -114,17 +115,25 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
});
}
protected Task On(ContentDeleted @event, EnvelopeHeaders headers)
protected Task On(ContentArchived @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, async collection =>
return ForAppIdAsync(@event.AppId.Id, collection =>
{
await collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, @event.ContentId),
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId));
return collection.UpdateAsync(@event, headers, x =>
{
x.IsArchived = true;
});
});
}
await collection.DeleteOneAsync(x => x.Id == headers.AggregateId());
protected Task On(ContentRestored @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, collection =>
{
return collection.UpdateAsync(@event, headers, x =>
{
x.IsArchived = false;
});
});
}
@ -140,6 +149,20 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
});
}
protected Task On(ContentDeleted @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, async collection =>
{
await collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, @event.ContentId),
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId));
await collection.DeleteOneAsync(x => x.Id == @event.ContentId);
});
}
private Task ForAppIdAsync(Guid appId, Func<IMongoCollection<MongoContentEntity>, Task> action)
{
var collection = GetCollection(appId);

9
src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs

@ -56,18 +56,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
return cursor;
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> Find(this IMongoCollection<MongoContentEntity> cursor, ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, bool nonPublished)
public static IFindFluent<MongoContentEntity, MongoContentEntity> Find(this IMongoCollection<MongoContentEntity> cursor, ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, bool nonPublished, bool archived)
{
var filter = BuildQuery(query, ids, schemaId, schema, nonPublished);
var filter = BuildQuery(query, ids, schemaId, schema, nonPublished, archived);
return cursor.Find(filter);
}
public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, bool nonPublished)
public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, bool nonPublished, bool archived)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.SchemaId, schemaId)
Filter.Eq(x => x.SchemaId, schemaId),
Filter.Eq(x => x.IsArchived, archived)
};
if (!nonPublished)

2
src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs

@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
[BsonRequired]
[BsonElement]
public int SessionEventIndex { get; set; }
public long Version { get; set; }
[BsonRequired]
[BsonElement]

8
src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs

@ -9,7 +9,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Events;
@ -25,7 +24,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
{
private readonly List<IHistoryEventsCreator> creators;
private readonly Dictionary<string, string> texts = new Dictionary<string, string>();
private int sessionEventCount;
public string Name
{
@ -64,7 +62,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
.Ascending(x => x.AppId)
.Ascending(x => x.Channel)
.Descending(x => x.Created)
.Descending(x => x.SessionEventIndex)),
.Descending(x => x.Version)),
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) }));
}
@ -72,7 +70,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
{
var historyEventEntities =
await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix)
.SortByDescending(x => x.Created).ThenByDescending(x => x.SessionEventIndex).Limit(count)
.SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count)
.ToListAsync();
return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList();
@ -90,7 +88,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
{
entity.Id = Guid.NewGuid();
entity.SessionEventIndex = Interlocked.Increment(ref sessionEventCount);
entity.Version = @event.Headers.EventStreamNumber();
entity.Channel = message.Channel;
entity.Message = message.Message;

5
src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs

@ -44,6 +44,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
get { return inner.LastModified; }
}
public long Version
{
get { return inner.Version; }
}
public string Channel
{
get { return inner.Channel; }

4
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs

@ -25,8 +25,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
[BsonElement]
public string Name { get; set; }
public string ScriptUnpublish { get; set; }
[BsonRequired]
[BsonElement]
public string Schema { get; set; }
@ -73,7 +71,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
[BsonIgnoreIfNull]
[BsonElement]
public string ScriptPublish { get; set; }
public string ScriptChange { get; set; }
Schema ISchemaEntity.SchemaDef
{

2
src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs

@ -20,8 +20,6 @@ namespace Squidex.Domain.Apps.Read.Assets
bool IsImage { get; }
bool IsDeleted { get; }
int? PixelWidth { get; }
int? PixelHeight { get; }

13
src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs

@ -20,19 +20,22 @@ namespace Squidex.Domain.Apps.Read.Contents
: base(typeNameRegistry)
{
AddEventMessage<ContentCreated>(
"created content element.");
"created content item.");
AddEventMessage<ContentUpdated>(
"updated content element.");
"updated content item.");
AddEventMessage<ContentDeleted>(
"deleted content element.");
"deleted content item.");
AddEventMessage<ContentRestored>(
"restored content item.");
AddEventMessage<ContentPublished>(
"published content element.");
"published content item.");
AddEventMessage<ContentUnpublished>(
"unpublished content element.");
"unpublished content item.");
}
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)

7
src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs

@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Read.Contents
return (schema, content);
}
public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, HashSet<Guid> ids, string query)
public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids, string query)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(user, nameof(user));
@ -85,8 +85,8 @@ namespace Squidex.Domain.Apps.Read.Contents
var isFrontendClient = user.IsInClient("squidex-frontend");
var taskForItems = contentRepository.QueryAsync(app, schema, isFrontendClient, ids, parsedQuery);
var taskForCount = contentRepository.CountAsync(app, schema, isFrontendClient, ids, parsedQuery);
var taskForItems = contentRepository.QueryAsync(app, schema, isFrontendClient, archived, ids, parsedQuery);
var taskForCount = contentRepository.CountAsync(app, schema, isFrontendClient, archived, ids, parsedQuery);
await Task.WhenAll(taskForItems, taskForCount);
@ -156,6 +156,7 @@ namespace Squidex.Domain.Apps.Read.Contents
public Guid Id { get; set; }
public Guid AppId { get; set; }
public long Version { get; set; }
public bool IsArchived { get; set; }
public bool IsPublished { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }

4
src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs

@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
public async Task<IReadOnlyList<IContentEntity>> QueryContentsAsync(Guid schemaId, string query)
{
var contents = (await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, null, query).ConfigureAwait(false)).Items;
var contents = (await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, false, null, query).ConfigureAwait(false)).Items;
foreach (var content in contents)
{
@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
if (notLoadedContents.Count > 0)
{
var contents = (await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, notLoadedContents, null).ConfigureAwait(false)).Items;
var contents = (await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, false, notLoadedContents, null).ConfigureAwait(false)).Items;
foreach (var content in contents)
{

2
src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs

@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Read.Contents
{
bool IsPublished { get; }
bool IsArchived { get; }
NamedContentData Data { get; }
}
}

2
src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Read.Contents
{
public interface IContentQueryService
{
Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, HashSet<Guid> ids, string query);
Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids, string query);
Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id);

4
src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs

@ -17,11 +17,11 @@ namespace Squidex.Domain.Apps.Read.Contents.Repositories
{
public interface IContentRepository
{
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds);
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id);
}

2
src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Read.History
string Message { get; }
long Version { get; }
RefToken Actor { get; }
}
}

4
src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs

@ -26,9 +26,7 @@ namespace Squidex.Domain.Apps.Read.Schemas
string ScriptDelete { get; }
string ScriptPublish { get; }
string ScriptUnpublish { get; }
string ScriptChange { get; }
Schema SchemaDef { get; }
}

14
src/Squidex.Domain.Apps.Write/Contents/Commands/ArchiveContent.cs

@ -0,0 +1,14 @@
// ==========================================================================
// ArchiveContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public sealed class ArchiveContent : ContentCommand
{
}
}

14
src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs

@ -0,0 +1,14 @@
// ==========================================================================
// RestoreContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public sealed class RestoreContent : ContentCommand
{
}
}

45
src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs

@ -116,9 +116,9 @@ namespace Squidex.Domain.Apps.Write.Contents
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command);
var scriptContext = CreateScriptContext(content, command, "Publish");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptPublish, "publish content");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "publish content");
content.Publish(command);
});
@ -129,20 +129,46 @@ namespace Squidex.Domain.Apps.Write.Contents
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command);
var scriptContext = CreateScriptContext(content, command, "Unpublish");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptUnpublish, "unpublish content");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "unpublish content");
content.Unpublish(command);
});
}
protected Task On(ArchiveContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Archive");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "archive content");
content.Archive(command);
});
}
protected Task On(RestoreContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Restore");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "restore content");
content.Restore(command);
});
}
protected Task On(DeleteContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command);
var scriptContext = CreateScriptContext(content, command, "Delete");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptDelete, "delete content");
@ -192,9 +218,14 @@ namespace Squidex.Domain.Apps.Write.Contents
}
}
private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, NamedContentData data = null)
private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, string operation)
{
return new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Operation = operation };
}
private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, NamedContentData data)
{
return new ScriptContext { ContentId = content.Id, Data = data, OldData = content.Data, User = command.User };
return new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Data = data };
}
private async Task<(ISchemaEntity SchemaEntity, IAppEntity AppEntity)> ResolveSchemaAndAppAsync(SchemaCommand command)

46
src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs

@ -23,6 +23,7 @@ namespace Squidex.Domain.Apps.Write.Contents
private bool isDeleted;
private bool isCreated;
private bool isPublished;
private bool isArchived;
private NamedContentData data;
public bool IsDeleted
@ -30,6 +31,11 @@ namespace Squidex.Domain.Apps.Write.Contents
get { return isDeleted; }
}
public bool IsArchived
{
get { return isArchived; }
}
public bool IsPublished
{
get { return isPublished; }
@ -67,6 +73,16 @@ namespace Squidex.Domain.Apps.Write.Contents
isPublished = false;
}
protected void On(ContentArchived @event)
{
isArchived = true;
}
protected void On(ContentRestored @event)
{
isArchived = false;
}
protected void On(ContentDeleted @event)
{
isDeleted = true;
@ -99,6 +115,28 @@ namespace Squidex.Domain.Apps.Write.Contents
return this;
}
public ContentDomainObject Restore(RestoreContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentRestored()));
return this;
}
public ContentDomainObject Archive(ArchiveContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentArchived()));
return this;
}
public ContentDomainObject Publish(PublishContent command)
{
Guard.NotNull(command, nameof(command));
@ -159,6 +197,14 @@ namespace Squidex.Domain.Apps.Write.Contents
}
}
private void VerifyDeleted()
{
if (!isDeleted)
{
throw new DomainException("Content has not been deleted.");
}
}
private void VerifyCreatedAndNotDeleted()
{
if (isDeleted || !isCreated)

86
src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs

@ -0,0 +1,86 @@
// ==========================================================================
// ContentVersionLoader.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Write.Contents
{
public sealed class ContentVersionLoader : IContentVersionLoader
{
private readonly IStreamNameResolver nameResolver;
private readonly IEventStore eventStore;
private readonly EventDataFormatter formatter;
public ContentVersionLoader(IEventStore eventStore, IStreamNameResolver nameResolver, EventDataFormatter formatter)
{
Guard.NotNull(formatter, nameof(formatter));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(nameResolver, nameof(nameResolver));
this.formatter = formatter;
this.eventStore = eventStore;
this.nameResolver = nameResolver;
}
public async Task<NamedContentData> LoadAsync(Guid appId, Guid id, long version)
{
var streamName = nameResolver.GetStreamName(typeof(ContentDomainObject), id);
var events = await eventStore.GetEventsAsync(streamName);
if (events.Count == 0 || events.Count < version - 1)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject));
}
NamedContentData contentData = null;
foreach (var storedEvent in events.Where(x => x.EventStreamNumber <= version))
{
var envelope = ParseKnownEvent(storedEvent);
if (envelope != null)
{
if (envelope.Payload is ContentCreated contentCreated)
{
if (contentCreated.AppId.Id != appId)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject));
}
contentData = contentCreated.Data;
}
else if (envelope.Payload is ContentUpdated contentUpdated)
{
contentData = contentUpdated.Data;
}
}
}
return contentData;
}
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)
{
try
{
return formatter.Parse(storedEvent.Data);
}
catch (TypeNameNotFoundException)
{
return null;
}
}
}
}

19
src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs

@ -0,0 +1,19 @@
// ==========================================================================
// IContentVersionLoader.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Write.Contents
{
public interface IContentVersionLoader
{
Task<NamedContentData> LoadAsync(Guid appId, Guid id, long version);
}
}

4
src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs

@ -18,8 +18,6 @@ namespace Squidex.Domain.Apps.Write.Schemas.Commands
public string ScriptDelete { get; set; }
public string ScriptPublish { get; set; }
public string ScriptUnpublish { get; set; }
public string ScriptChange { get; set; }
}
}

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

@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
foreach (var storedEvent in events)
{
var envelope = ParseKnownCommand(storedEvent);
var envelope = ParseKnownEvent(storedEvent);
if (envelope != null)
{
@ -79,7 +79,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
}
}
private Envelope<IEvent> ParseKnownCommand(StoredEvent storedEvent)
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)
{
try
{

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

@ -55,6 +55,10 @@ namespace Squidex.Config.Domain
.As<IScriptEngine>()
.SingleInstance();
builder.RegisterType<ContentVersionLoader>()
.As<IContentVersionLoader>()
.SingleInstance();
builder.RegisterType<FieldRegistry>()
.AsSelf()
.SingleInstance();

2
src/Squidex/Controllers/Api/Assets/AssetsController.cs

@ -133,7 +133,7 @@ namespace Squidex.Controllers.Api.Assets
{
var entity = await assetRepository.FindAssetAsync(id);
if (entity == null || entity.IsDeleted)
if (entity == null)
{
return NotFound();
}

5
src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs

@ -35,5 +35,10 @@ namespace Squidex.Controllers.Api.History.Models
/// The time when the event happened.
/// </summary>
public Instant Created { get; set; }
/// <summary>
/// The version identifier.
/// </summary>
public long Version { get; set; }
}
}

9
src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs

@ -31,13 +31,8 @@ namespace Squidex.Controllers.Api.Schemas.Models
public string ScriptDelete { get; set; }
/// <summary>
/// The script that is executed when publishing a content.
/// The script that is executed when change a content status.
/// </summary>
public string ScriptPublish { get; set; }
/// <summary>
/// The script that is executed when unpublishing a content.
/// </summary>
public string ScriptUnpublish { get; set; }
public string ScriptChange { get; set; }
}
}

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

@ -54,14 +54,9 @@ namespace Squidex.Controllers.Api.Schemas.Models
public string ScriptDelete { get; set; }
/// <summary>
/// The script that is executed when publishing a content.
/// The script that is executed when changing a content status.
/// </summary>
public string ScriptPublish { get; set; }
/// <summary>
/// The script that is executed when unpublishing a content.
/// </summary>
public string ScriptUnpublish { get; set; }
public string ScriptChange { get; set; }
/// <summary>
/// The list of fields.

2
src/Squidex/Controllers/Api/Schemas/SchemasController.cs

@ -125,7 +125,7 @@ namespace Squidex.Controllers.Api.Schemas
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>();
var response = new EntityCreatedDto { Id = command.Name, Version = result.Version };
var response = new EntityCreatedDto { Id = command.SchemaId.ToString(), Version = result.Version };
return CreatedAtAction(nameof(GetSchema), new { name = request.Name }, response);
}

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

@ -31,12 +31,17 @@ namespace Squidex.Controllers.ContentApi
public sealed class ContentsController : ControllerBase
{
private readonly IContentQueryService contentQuery;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IGraphQLService graphQl;
public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IGraphQLService graphQl)
public ContentsController(ICommandBus commandBus,
IContentQueryService contentQuery,
IContentVersionLoader contentVersionLoader,
IGraphQLService graphQl)
: base(commandBus)
{
this.contentQuery = contentQuery;
this.contentVersionLoader = contentVersionLoader;
this.graphQl = graphQl;
}
@ -64,7 +69,7 @@ namespace Squidex.Controllers.ContentApi
[HttpGet]
[Route("content/{app}/{name}")]
[ApiCosts(2)]
public async Task<IActionResult> GetContents(string name, [FromQuery] string ids = null)
public async Task<IActionResult> GetContents(string name, [FromQuery] bool archived = false, [FromQuery] string ids = null)
{
var idsList = new HashSet<Guid>();
@ -81,7 +86,7 @@ namespace Squidex.Controllers.ContentApi
var isFrontendClient = User.IsFrontendClient();
var contents = await contentQuery.QueryWithCountAsync(App, name, User, idsList, Request.QueryString.ToString());
var contents = await contentQuery.QueryWithCountAsync(App, name, User, archived, idsList, Request.QueryString.ToString());
var response = new AssetsDto
{
@ -124,6 +129,21 @@ namespace Squidex.Controllers.ContentApi
return Ok(response);
}
[MustBeAppReader]
[HttpGet]
[Route("content/{app}/{name}/{id}/{version}")]
[ApiCosts(1)]
public async Task<IActionResult> GetContentVersion(string name, Guid id, int version)
{
var contentData = await contentVersionLoader.LoadAsync(App.Id, id, version);
var response = contentData;
Response.Headers["ETag"] = new StringValues(version.ToString());
return Ok(response);
}
[MustBeAppEditor]
[HttpPost]
[Route("content/{app}/{name}/")]
@ -208,6 +228,36 @@ namespace Squidex.Controllers.ContentApi
return NoContent();
}
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/archive")]
[ApiCosts(1)]
public async Task<IActionResult> ArchiveContent(string name, Guid id)
{
await contentQuery.FindSchemaAsync(App, name);
var command = new ArchiveContent { ContentId = id, User = User };
await CommandBus.PublishAsync(command);
return NoContent();
}
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/restore")]
[ApiCosts(1)]
public async Task<IActionResult> RestoreContent(string name, Guid id)
{
await contentQuery.FindSchemaAsync(App, name);
var command = new RestoreContent { ContentId = id, User = User };
await CommandBus.PublishAsync(command);
return NoContent();
}
[MustBeAppEditor]
[HttpDelete]
[Route("content/{app}/{name}/{id}")]

54
src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs

@ -94,6 +94,8 @@ namespace Squidex.Controllers.ContentApi.Generator
GenerateSchemaPatchOperation(),
GenerateSchemaPublishOperation(),
GenerateSchemaUnpublishOperation(),
GenerateSchemaArchiveOperation(),
GenerateSchemaRestoreOperation(),
GenerateSchemaDeleteOperation()
};
@ -109,6 +111,7 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Query{schemaKey}Contents";
operation.Summary = $"Queries {schemaName} contents.";
operation.Security = ReaderSecurity;
operation.Description = SchemaQueryDescription;
@ -119,8 +122,6 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema));
operation.Security = ReaderSecurity;
});
}
@ -130,10 +131,9 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Get{schemaKey}Content";
operation.Summary = $"Get a {schemaName} content.";
operation.Security = ReaderSecurity;
operation.AddResponse("200", $"{schemaName} content found.", contentSchema);
operation.Security = ReaderSecurity;
});
}
@ -143,13 +143,12 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Create{schemaKey}Content";
operation.Summary = $"Create a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content.");
operation.AddResponse("201", $"{schemaName} created.", contentSchema);
operation.Security = EditorSecurity;
});
}
@ -159,12 +158,11 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Update{schemaKey}Content";
operation.Summary = $"Update a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("201", $"{schemaName} element updated.", dataSchema);
operation.Security = EditorSecurity;
operation.AddResponse("201", $"{schemaName} item updated.", dataSchema);
});
}
@ -174,12 +172,11 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Path{schemaKey}Content";
operation.Summary = $"Patchs a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription);
operation.AddResponse("201", $"{schemaName} element patched.", dataSchema);
operation.Security = EditorSecurity;
operation.AddResponse("201", $"{schemaName} item patched.", dataSchema);
});
}
@ -189,10 +186,9 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Publish{schemaKey}Content";
operation.Summary = $"Publish a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} element published.");
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} item published.");
});
}
@ -202,10 +198,33 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Unpublish{schemaKey}Content";
operation.Summary = $"Unpublish a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} element unpublished.");
operation.AddResponse("204", $"{schemaName} item unpublished.");
});
}
private SwaggerOperations GenerateSchemaArchiveOperation()
{
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/archive", operation =>
{
operation.OperationId = $"Archive{schemaKey}Content";
operation.Summary = $"Archive a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} item restored.");
});
}
private SwaggerOperations GenerateSchemaRestoreOperation()
{
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/restore", operation =>
{
operation.OperationId = $"Restore{schemaKey}Content";
operation.Summary = $"Restore a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} item restored.");
});
}
@ -215,10 +234,9 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Delete{schemaKey}Content";
operation.Summary = $"Delete a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} content deleted.");
operation.Security = EditorSecurity;
});
}

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

@ -19,18 +19,18 @@ namespace Squidex.Controllers.ContentApi.Models
public sealed class ContentDto
{
/// <summary>
/// The if of the content element.
/// The if of the content item.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The user that has created the content element.
/// The user that has created the content item.
/// </summary>
[Required]
public RefToken CreatedBy { get; set; }
/// <summary>
/// The user that has updated the content element.
/// The user that has updated the content item.
/// </summary>
[Required]
public RefToken LastModifiedBy { get; set; }
@ -42,20 +42,25 @@ namespace Squidex.Controllers.ContentApi.Models
public object Data { get; set; }
/// <summary>
/// The date and time when the content element has been created.
/// The date and time when the content item has been created.
/// </summary>
public Instant Created { get; set; }
/// <summary>
/// The date and time when the content element has been modified last.
/// The date and time when the content item has been modified last.
/// </summary>
public Instant LastModified { get; set; }
/// <summary>
/// Indicates if the content element is publihed.
/// Indicates if the content item is published.
/// </summary>
public bool? IsPublished { get; set; }
/// <summary>
/// Indicates if the content item is archived.
/// </summary>
public bool IsArchived { get; set; }
/// <summary>
/// The version of the content.
/// </summary>

6
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -32,7 +32,6 @@ export class UserPageComponent extends ComponentBase implements OnInit {
public currentUserId: string;
public userFormSubmitted = false;
public userForm: FormGroup;
public userId: string;
public userFormError? = '';
public isCurrentUser = false;
@ -86,7 +85,7 @@ export class UserPageComponent extends ComponentBase implements OnInit {
this.resetUserForm(error.displayMessage);
});
} else {
this.userManagementService.putUser(this.userId, requestDto)
this.userManagementService.putUser(this.user.id, requestDto)
.subscribe(() => {
this.user =
this.user.update(
@ -119,7 +118,6 @@ export class UserPageComponent extends ComponentBase implements OnInit {
const input = this.user || {};
this.isNewMode = !this.user;
this.userId = input['id'];
this.userForm =
this.formBuilder.group({
email: [input['email'],
@ -143,7 +141,7 @@ export class UserPageComponent extends ComponentBase implements OnInit {
]]
});
this.isCurrentUser = this.userId === this.currentUserId;
this.isCurrentUser = this.user && this.user.id === this.currentUserId;
this.resetUserForm();
}

1
src/Squidex/app/features/content/declarations.ts

@ -6,6 +6,7 @@
*/
export * from './pages/content/content-field.component';
export * from './pages/content/content-history.component';
export * from './pages/content/content-page.component';
export * from './pages/contents/contents-page.component';
export * from './pages/contents/search-form.component';

5
src/Squidex/app/features/content/module.ts

@ -11,7 +11,6 @@ import { DndModule } from 'ng2-dnd';
import {
CanDeactivateGuard,
HistoryComponent,
ResolveAppLanguagesGuard,
ResolveContentGuard,
ResolvePublishedSchemaGuard,
@ -22,6 +21,7 @@ import {
import {
AssetsEditorComponent,
ContentFieldComponent,
ContentHistoryComponent,
ContentPageComponent,
ContentItemComponent,
ContentsPageComponent,
@ -76,7 +76,7 @@ const routes: Routes = [
children: [
{
path: 'history',
component: HistoryComponent,
component: ContentHistoryComponent,
data: {
channel: 'contents.{contentId}'
}
@ -112,6 +112,7 @@ const routes: Routes = [
declarations: [
AssetsEditorComponent,
ContentFieldComponent,
ContentHistoryComponent,
ContentItemComponent,
ContentPageComponent,
ContentsPageComponent,

29
src/Squidex/app/features/content/pages/content/content-history.component.html

@ -0,0 +1,29 @@
<sqx-panel panelWidth="16rem">
<div class="panel-header">
<div class="panel-title-row">
<h3 class="panel-title">Activity</h3>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-blank">
<div *ngFor="let event of events | async" class="event">
<div class="event-left">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:'I'" [attr.src]="event.actor | sqxUserPictureRef" />
</div>
<div class="event-main">
<div class="event-message">
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:'I'}}</span> <span [innerHTML]="format(event.message) | async"></span>
</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a>
</div>
</div>
</div>
</div>
</sqx-panel>

39
src/Squidex/app/features/content/pages/content/content-history.component.scss

@ -0,0 +1,39 @@
@import '_vars';
@import '_mixins';
.event {
& {
@include flex-box;
margin-bottom: 1rem;
}
&-main {
@include flex-grow(1);
}
&-load {
& {
font-size: .9rem;
font-weight: normal;
cursor: pointer;
color: $color-theme-blue !important;
}
&:focus,
&:hover {
text-decoration: underline !important;
}
}
&-left {
min-width: 2.8rem;
max-width: 2.8rem;
margin-top: .3rem;
}
&-created {
font-size: .65rem;
font-weight: normal;
color: $color-text-decent;
}
}

107
src/Squidex/app/features/content/pages/content/content-history.component.ts

@ -0,0 +1,107 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import {
allParams,
AppComponentBase,
AppsStoreService,
DialogService,
HistoryChannelUpdated,
HistoryEventDto,
HistoryService,
MessageBus,
UsersProviderService
} from 'shared';
import { ContentVersionSelected } from './../messages';
const REPLACEMENT_TEMP = '$TEMP$';
@Component({
selector: 'sqx-history',
styleUrls: ['./content-history.component.scss'],
templateUrl: './content-history.component.html'
})
export class ContentHistoryComponent extends AppComponentBase {
public get channel(): string {
let channelPath = this.route.snapshot.data['channel'];
if (channelPath) {
const params = allParams(this.route);
for (let key in params) {
if (params.hasOwnProperty(key)) {
const value = params[key];
channelPath = channelPath.replace(`{${key}}`, value);
}
}
}
return channelPath;
}
public events: Observable<HistoryEventDto[]> =
Observable.timer(0, 10000)
.merge(this.messageBus.of(HistoryChannelUpdated).delay(1000))
.switchMap(() => this.appNameOnce())
.switchMap(app => this.historyService.getHistory(app, this.channel).retry(2));
constructor(appsStore: AppsStoreService, dialogs: DialogService,
private readonly users: UsersProviderService,
private readonly historyService: HistoryService,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute
) {
super(dialogs, appsStore);
}
private userName(userId: string): Observable<string> {
const parts = userId.split(':');
if (parts[0] === 'subject') {
return this.users.getUser(parts[1], 'Me').map(u => u.displayName);
} else {
if (parts[1].endsWith('client')) {
return Observable.of(parts[1]);
} else {
return Observable.of(`${parts[1]}-client`);
}
}
}
public loadVersion(version: number) {
this.messageBus.emit(new ContentVersionSelected(version));
}
public format(message: string): Observable<string> {
let foundUserId: string | null = null;
message = message.replace(/{([^\s:]*):([^}]*)}/, (match: string, type: string, id: string) => {
if (type === 'user') {
foundUserId = id;
return REPLACEMENT_TEMP;
} else {
return id;
}
});
message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => {
return `<span class="marker-ref">${marker}</span>`;
});
if (foundUserId) {
return this.userName(foundUserId).map(t => message.replace(REPLACEMENT_TEMP, `<span class="user-ref">${t}</span>`));
}
return Observable.of(message);
}
}

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

@ -4,7 +4,7 @@
<sqx-panel panelWidth="53rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<div class="float-right" *ngIf="!content || !content.isArchived">
<span *ngIf="isNewMode">
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S">
Save as Draft
@ -19,16 +19,19 @@
Save
</button>
</span>
</div>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
</div>
<h3 class="panel-title" *ngIf="isNewMode">
New Content
</h3>
<h3 class="panel-title" *ngIf="!isNewMode">
<h3 class="panel-title" *ngIf="!isNewMode && !content.isArchived">
Edit Content
</h3>
<h3 class="panel-title" *ngIf="!isNewMode && content.isArchived">
Show Content
</h3>
</div>
<a class="panel-close" sqxParentLink>

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

@ -12,8 +12,9 @@ import { Observable, Subscription } from 'rxjs';
import {
ContentCreated,
ContentDeleted,
ContentUpdated
ContentRemoved,
ContentUpdated,
ContentVersionSelected
} from './../messages';
import {
@ -38,15 +39,13 @@ import {
})
export class ContentPageComponent extends AppComponentBase implements CanComponentDeactivate, OnDestroy, OnInit {
private contentDeletedSubscription: Subscription;
private version = new Version('');
private content: ContentDto;
private contentVersionSelectedSubscription: Subscription;
public schema: SchemaDetailsDto;
public content: ContentDto;
public contentFormSubmitted = false;
public contentForm: FormGroup;
public contentData: any = null;
public contentId: string | null = null;
public isNewMode = true;
@ -63,6 +62,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
}
public ngOnDestroy() {
this.contentVersionSelectedSubscription.unsubscribe();
this.contentDeletedSubscription.unsubscribe();
}
@ -71,10 +71,16 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
this.languages = routeData['appLanguages'];
this.contentVersionSelectedSubscription =
this.messageBus.of(ContentVersionSelected)
.subscribe(message => {
this.loadVersion(message.version);
});
this.contentDeletedSubscription =
this.messageBus.of(ContentDeleted)
this.messageBus.of(ContentRemoved)
.subscribe(message => {
if (message.content.id === this.contentId) {
if (this.content && message.content.id === this.content.id) {
this.router.navigate(['../'], { relativeTo: this.route });
}
});
@ -115,7 +121,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
if (this.isNewMode) {
this.appNameOnce()
.switchMap(app => this.contentsService.postContent(app, this.schema.name, requestDto, publish, this.version))
.switchMap(app => this.contentsService.postContent(app, this.schema.name, requestDto, publish))
.subscribe(dto => {
this.content = dto;
@ -128,9 +134,9 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
});
} else {
this.appNameOnce()
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId!, requestDto, this.version))
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.content.id, requestDto, this.content.version))
.subscribe(dto => {
this.content = this.content.update(dto, this.authService.user.token);
this.content = this.content.update(dto, this.authService.user!.token);
this.emitContentUpdated(this.content);
this.notifyInfo('Content saved successfully.');
@ -146,6 +152,22 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
}
}
private loadVersion(version: number) {
if (!this.isNewMode && this.content) {
this.appNameOnce()
.switchMap(app => this.contentsService.getVersionData(app, this.schema.name, this.content.id, new Version(version.toString())))
.subscribe(dto => {
this.content = this.content.setData(dto);
this.emitContentUpdated(this.content);
this.notifyInfo('Content version loaded successfully.');
this.populateContentForm();
}, error => {
this.notifyError(error);
});
}
}
private back() {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
}
@ -195,28 +217,24 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
private populateContentForm() {
this.contentForm.markAsPristine();
if (!this.content) {
this.contentData = null;
this.contentId = null;
this.isNewMode = true;
return;
}
this.contentData = this.content.data;
this.contentId = this.content.id;
this.version = this.content.version;
this.isNewMode = false;
this.isNewMode = !this.content;
for (const field of this.schema.fields) {
const fieldValue = this.content.data[field.name] || {};
const fieldForm = <FormGroup>this.contentForm.get(field.name);
if (!this.isNewMode) {
for (const field of this.schema.fields) {
const fieldValue = this.content.data[field.name] || {};
const fieldForm = <FormGroup>this.contentForm.get(field.name);
if (field.partitioning === 'language') {
for (let language of this.languages) {
fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]);
if (field.partitioning === 'language') {
for (let language of this.languages) {
fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]);
}
} else {
fieldForm.controls['iv'].setValue(fieldValue['iv']);
}
} else {
fieldForm.controls['iv'].setValue(fieldValue['iv']);
}
if (this.content.isArchived) {
this.contentForm.disable();
}
}
}

18
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -21,7 +21,13 @@
</form>
<div class="dropdown-menu" *sqxModalView="searchModal" closeAlways="true" [sqxModalTarget]="searchInput" position="right">
<sqx-search-form (queryChanged)="contentsFilter.setValue($event, { emitEvent: false })" [query]="contentsFilter.value"></sqx-search-form>
<sqx-search-form
[canArchive]="!isReadOnly"
(queryChanged)="contentsFilter.setValue($event, { emitEvent: false })"
[query]="contentsFilter.value"
(archivedChanged)="updateArchive($event)"
[archived]="isArchive">
</sqx-search-form>
</div>
<span *ngIf="!isReadOnly && languages.length > 1">
@ -33,12 +39,16 @@
</button>
</div>
<h3 class="panel-title" *ngIf="!isReadOnly">
<h3 class="panel-title" *ngIf="!isReadOnly && !isArchive">
Contents
</h3>
<h3 class="panel-title" *ngIf="isArchive">
Archive
</h3>
<h3 class="panel-title" *ngIf="isReadOnly">
References
Refs
</h3>
</div>
@ -82,6 +92,8 @@
[schema]="schema"
(unpublishing)="unpublishContent(content)"
(publishing)="publishContent(content)"
(archiving)="archiveContent(content)"
(restoring)="restoreContent(content)"
(deleting)="deleteContent(content)"></tr>
<tr class="spacer"></tr>
</ng-template>

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

@ -12,7 +12,7 @@ import { Subscription } from 'rxjs';
import {
ContentCreated,
ContentDeleted,
ContentRemoved,
ContentUpdated
} from './../messages';
@ -57,6 +57,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public languageParameter: string;
public isReadOnly = false;
public isArchive = false;
public columnWidth: number;
@ -112,18 +113,11 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
return { content, schemaId: this.schema.id };
}
public search() {
this.contentsQuery = this.contentsFilter.value;
this.contentsPager = new Pager(0);
this.load();
}
public publishContent(content: ContentDto) {
this.appNameOnce()
.switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version))
.subscribe(() => {
this.contentItems = this.contentItems.replaceBy('id', content.publish(this.authService.user.token));
this.contentItems = this.contentItems.replaceBy('id', content.publish(this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -133,7 +127,31 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.appNameOnce()
.switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version))
.subscribe(() => {
this.contentItems = this.contentItems.replaceBy('id', content.unpublish(this.authService.user.token));
this.contentItems = this.contentItems.replaceBy('id', content.unpublish(this.authService.user!.token));
}, error => {
this.notifyError(error);
});
}
public archiveContent(content: ContentDto) {
this.appNameOnce()
.switchMap(app => this.contentsService.archiveContent(app, this.schema.name, content.id, content.version))
.subscribe(() => {
content = content.archive(this.authService.user!.token);
this.removeContent(content);
}, error => {
this.notifyError(error);
});
}
public restoreContent(content: ContentDto) {
this.appNameOnce()
.switchMap(app => this.contentsService.restoreContent(app, this.schema.name, content.id, content.version))
.subscribe(() => {
content = content.restore(this.authService.user!.token);
this.removeContent(content);
}, error => {
this.notifyError(error);
});
@ -143,10 +161,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.appNameOnce()
.switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version))
.subscribe(() => {
this.contentItems = this.contentItems.removeAll(x => x.id === content.id);
this.contentsPager = this.contentsPager.decrementCount();
this.emitContentDeleted(content);
this.removeContent(content);
}, error => {
this.notifyError(error);
});
@ -154,7 +169,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public load(showInfo = false) {
this.appNameOnce()
.switchMap(app => this.contentsService.getContents(app, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery))
.switchMap(app => this.contentsService.getContents(app, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, null, this.isArchive))
.subscribe(dtos => {
this.contentItems = ImmutableArray.of(dtos.items);
this.contentsPager = this.contentsPager.setCount(dtos.total);
@ -167,8 +182,22 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
});
}
public selectLanguage(language: AppLanguageDto) {
this.languageSelected = language;
public updateArchive(isArchive: boolean) {
this.contentsQuery = this.contentsFilter.value;
this.contentsPager = new Pager(0);
this.isArchive = isArchive;
this.searchModal.hide();
this.load();
}
public search() {
this.contentsQuery = this.contentsFilter.value;
this.contentsPager = new Pager(0);
this.load();
}
public goNext() {
@ -183,8 +212,12 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.load();
}
private emitContentDeleted(content: ContentDto) {
this.messageBus.emit(new ContentDeleted(content));
public selectLanguage(language: AppLanguageDto) {
this.languageSelected = language;
}
private emitContentRemoved(content: ContentDto) {
this.messageBus.emit(new ContentRemoved(content));
}
private resetContents() {
@ -196,6 +229,13 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.loadFields();
}
private removeContent(content: ContentDto) {
this.contentItems = this.contentItems.removeAll(x => x.id === content.id);
this.contentsPager = this.contentsPager.decrementCount();
this.emitContentRemoved(content);
}
private loadFields() {
this.contentFields = this.schema.fields.filter(x => x.properties.isListField);
@ -203,6 +243,10 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.contentFields = [this.schema.fields[0]];
}
if (this.contentFields.length === 0) {
this.contentFields = [<any>{}];
}
if (this.contentFields.length > 0) {
this.columnWidth = 100 / this.contentFields.length;
} else {

40
src/Squidex/app/features/content/pages/contents/search-form.component.html

@ -1,27 +1,35 @@
<div [formGroup]="searchForm" class="form-horizontal">
<div class="form-group row">
<label class="col col-2 col-form-label" for="odataSearch">Text</label>
<div class="form-horizontal">
<div [formGroup]="searchForm">
<div class="form-group row">
<label class="col col-2 col-form-label" for="odataSearch">Text</label>
<div class="col col-10">
<input type="input" class="form-control" id="search" (blur)="updateQuery()" formControlName="odataSearch" placeholder="Fulltext search" />
<div class="col col-10">
<input type="input" class="form-control" id="search" (blur)="updateQuery()" formControlName="odataSearch" placeholder="Fulltext search" />
</div>
</div>
</div>
<div class="form-group row">
<label class="col col-2 col-form-label" for="filter">Filter</label>
<div class="form-group row">
<label class="col col-2 col-form-label" for="filter">Filter</label>
<div class="col col-10">
<input type="input" class="form-control" id="filter" (blur)="updateQuery()" formControlName="odataFilter" placeholder="data/[MY_FIELD]/iv eq 100" />
<div class="col col-10">
<input type="input" class="form-control" id="filter" (blur)="updateQuery()" formControlName="odataFilter" placeholder="data/[MY_FIELD]/iv eq 100" />
</div>
</div>
</div>
<div class="form-group row">
<label class="col col-2 col-form-label" for="orderBy">Order</label>
<div class="col col-10">
<input type="input" class="form-control" id="orderBy" (blur)="updateQuery()" formControlName="odataOrderBy" placeholder="data/[MY_FIELD]/iv desc" />
<div class="form-group row">
<label class="col col-2 col-form-label" for="orderBy">Order</label>
<div class="col col-10">
<input type="input" class="form-control" id="orderBy" (blur)="updateQuery()" formControlName="odataOrderBy" placeholder="data/[MY_FIELD]/iv desc" />
</div>
</div>
</div>
<div class="form-check" *ngIf="canArchive">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" [ngModel]="archived" (ngModelChange)="archivedChanged.emit($event)" /> Archived items
</label>
</div>
<div class="link">
Read more about filtering in the <a href="https://docs.squidex.io/04-guides/02-api.html" target="_blank">Documentation</a>.

7
src/Squidex/app/features/content/pages/contents/search-form.component.scss

@ -7,6 +7,13 @@
max-width: 25rem;
}
.form-check {
margin-top: 1rem;
font-size: 1rem;
font-weight: normal;
text-align: right;
}
.link {
margin-top: 1.5rem;
font-size: .8rem;

11
src/Squidex/app/features/content/pages/contents/search-form.component.ts

@ -20,7 +20,16 @@ export class SearchFormComponent implements OnChanges {
public query = '';
@Output()
public queryChanged = new EventEmitter();
public queryChanged = new EventEmitter<string>();
@Input()
public archived = false;
@Output()
public archivedChanged = new EventEmitter<boolean>();
@Input()
public canArchive = true;
public searchForm =
this.formBuilder.group({

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

@ -21,9 +21,16 @@ export class ContentUpdated {
}
}
export class ContentDeleted {
export class ContentRemoved {
constructor(
public readonly content: ContentDto
) {
}
}
export class ContentVersionSelected {
constructor(
public readonly version: number
) {
}
}

21
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -19,11 +19,10 @@ import {
AssetUpdated,
DialogService,
ImmutableArray,
MessageBus
MessageBus,
Types
} from 'shared';
const NOOP = () => { /* NOOP */ };
export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetsEditorComponent), multi: true
};
@ -36,8 +35,8 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
})
export class AssetsEditorComponent extends AppComponentBase implements ControlValueAccessor, OnDestroy, OnInit {
private assetUpdatedSubscription: Subscription;
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
public newAssets = ImmutableArray.empty<File>();
public oldAssets = ImmutableArray.empty<AssetDto>();
@ -65,10 +64,10 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa
this.assetUpdatedSubscription.unsubscribe();
}
public writeValue(value: any) {
public writeValue(value: string[]) {
this.oldAssets = ImmutableArray.empty<AssetDto>();
if (value && value.length > 0) {
if (Types.isArrayOfString(value) && value.length > 0) {
const assetIds: string[] = value;
this.appNameOnce()
@ -84,11 +83,11 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public addFiles(files: FileList) {
@ -143,7 +142,7 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa
ids = null;
}
this.touchedCallback();
this.changeCallback(ids);
this.callTouched();
this.callChange(ids);
}
}

12
src/Squidex/app/features/content/shared/content-item.component.html

@ -17,13 +17,19 @@
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]>
<a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="!content.isPublished">
<a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="!content.isPublished && !content.isArchived">
Publish
</a>
<a class="dropdown-item" (click)="unpublishing.emit(); $event.stopPropagation()" *ngIf="content.isPublished">
<a class="dropdown-item" (click)="unpublishing.emit(); $event.stopPropagation()" *ngIf="content.isPublished && !content.isArchived">
Unpublish
</a>
<a class="dropdown-item dropdown-item-delete"
<a class="dropdown-item" (click)="archiving.emit(); $event.stopPropagation()" *ngIf="!content.isArchived">
Archive
</a>
<a class="dropdown-item" (click)="restoring.emit(); $event.stopPropagation()" *ngIf="content.isArchived">
Restore
</a>
<a class="dropdown-item dropdown-item-delete" *ngIf="content.isArchived"
(sqxConfirmClick)="deleting.emit()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">

6
src/Squidex/app/features/content/shared/content-item.component.ts

@ -35,6 +35,12 @@ export class ContentItemComponent extends AppComponentBase implements OnInit, On
@Output()
public unpublishing = new EventEmitter<ContentDto>();
@Output()
public archiving = new EventEmitter<ContentDto>();
@Output()
public restoring = new EventEmitter<ContentDto>();
@Output()
public deleting = new EventEmitter<ContentDto>();

25
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -19,11 +19,10 @@ import {
FieldDto,
ImmutableArray,
SchemaDetailsDto,
SchemasService
SchemasService,
Types
} from 'shared';
const NOOP = () => { /* NOOP */ };
export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true
};
@ -35,8 +34,8 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class ReferencesEditorComponent extends AppComponentBase implements ControlValueAccessor, OnInit {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
@Input()
public schemaId: string;
@ -73,10 +72,10 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
});
}
public writeValue(value: any) {
public writeValue(value: string[]) {
this.contentItems = ImmutableArray.empty<ContentDto>();
if (value && value.length > 0) {
if (Types.isArrayOfString(value) && value.length > 0) {
const contentIds: string[] = value;
this.appNameOnce()
@ -92,11 +91,11 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public canDrop() {
@ -138,8 +137,8 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
ids = null;
}
this.touchedCallback();
this.changeCallback(ids);
this.callTouched();
this.callChange(ids);
}
private loadFields() {
@ -149,6 +148,10 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
this.contentFields = [this.schema.fields[0]];
}
if (this.contentFields.length === 0) {
this.contentFields = [<any>{}];
}
if (this.contentFields.length > 0) {
this.columnWidth = 100 / this.contentFields.length;
} else {

2
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -146,7 +146,7 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit {
};
});
this.profileDisplayName = this.authService.user.displayName;
this.profileDisplayName = this.authService.user!.displayName;
}
public showForum() {

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

@ -110,7 +110,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.publishSchema(app, this.schema.name, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.publish(this.authService.user.token));
this.updateSchema(this.schema.publish(this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -120,7 +120,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.unpublishSchema(app, this.schema.name, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.unpublish(this.authService.user.token));
this.updateSchema(this.schema.unpublish(this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -130,7 +130,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.enableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.updateField(field.enable(), this.authService.user.token));
this.updateSchema(this.schema.updateField(field.enable(), this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -140,7 +140,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.disableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.updateField(field.disable(), this.authService.user.token));
this.updateSchema(this.schema.updateField(field.disable(), this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -150,7 +150,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.lockField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.updateField(field.lock(), this.authService.user.token));
this.updateSchema(this.schema.updateField(field.lock(), this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -160,7 +160,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.hideField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.updateField(field.hide(), this.authService.user.token));
this.updateSchema(this.schema.updateField(field.hide(), this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -170,7 +170,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.deleteField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.removeField(field, this.authService.user.token));
this.updateSchema(this.schema.removeField(field, this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -180,7 +180,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.putFieldOrdering(app, this.schema.name, fields.map(t => t.fieldId), this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.replaceFields(fields, this.authService.user.token));
this.updateSchema(this.schema.replaceFields(fields, this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -192,7 +192,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.putField(app, this.schema.name, field.fieldId, requestDto, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.updateField(field, this.authService.user.token));
this.updateSchema(this.schema.updateField(field, this.authService.user!.token));
}, error => {
this.notifyError(error);
});
@ -223,7 +223,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.schemasService.postField(app, this.schema.name, requestDto, this.schema.version))
.subscribe(dto => {
this.updateSchema(this.schema.addField(dto, this.authService.user.token));
this.updateSchema(this.schema.addField(dto, this.authService.user!.token));
this.resetFieldForm();
}, error => {
this.notifyError(error);
@ -237,13 +237,13 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
}
public onSchemaSaved(properties: SchemaPropertiesDto) {
this.updateSchema(this.schema.update(properties, this.authService.user.token));
this.updateSchema(this.schema.update(properties, this.authService.user!.token));
this.editSchemaDialog.hide();
}
public onSchemaScriptsSaved(scripts: UpdateSchemaScriptsDto) {
this.updateSchema(this.schema.configureScripts(scripts, this.authService.user.token));
this.updateSchema(this.schema.configureScripts(scripts, this.authService.user!.token));
this.configureScriptsDialog.hide();
}

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

@ -41,8 +41,7 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit
'Create',
'Update',
'Delete',
'Publish',
'Unpublish'
'Change'
];
public editFormSubmitted = false;
@ -52,8 +51,7 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit
scriptCreate: '',
scriptUpdate: '',
scriptDelete: '',
scriptPublish: '',
scriptUnpublish: ''
scriptChange: ''
});
constructor(dialogs: DialogService,
@ -84,8 +82,7 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit
this.editForm.controls['scriptCreate'].value,
this.editForm.controls['scriptUpdate'].value,
this.editForm.controls['scriptDelete'].value,
this.editForm.controls['scriptPublish'].value,
this.editForm.controls['scriptUnpublish'].value);
this.editForm.controls['scriptChange'].value);
this.schemas.putSchemaScripts(this.appName, this.schema.name, requestDto, this.schema.version)
.subscribe(dto => {

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

@ -11,6 +11,7 @@ import { FormBuilder, Validators } from '@angular/forms';
import {
ApiUrlConfig,
AuthService,
DateTime,
fadeAnimation,
SchemaDetailsDto,
SchemasService,
@ -89,7 +90,7 @@ export class SchemaFormComponent {
const me = this.authService.user!.token;
this.schemas.postSchema(this.appName, requestDto, me, undefined, schemaVersion)
this.schemas.postSchema(this.appName, requestDto, me, DateTime.now(), schemaVersion)
.subscribe(dto => {
this.emitCreated(dto);
this.resetCreateForm();

2
src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts

@ -26,7 +26,7 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn
public eventsItems = ImmutableArray.empty<WebhookEventDto>();
public eventsPager = new Pager(0);
public selectedEventId: string;
public selectedEventId: string | null = null;
constructor(dialogs: DialogService, appsStore: AppsStoreService,
private readonly webhooksService: WebhooksService

2
src/Squidex/app/features/webhooks/pages/webhook.component.ts

@ -91,7 +91,7 @@ export class WebhookComponent implements OnInit {
} else {
return null;
}
}).filter(w => !!w)).sortByStringAsc(x => x.schema.name);
}).filter(w => w !== null).map(w => w!)).sortByStringAsc(x => x.schema.name);
this.schemasToAdd =
ImmutableArray.of(

2
src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts

@ -91,7 +91,7 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version))
.subscribe(dto => {
this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.authService.user.token));
this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.authService.user!.token));
this.notifyInfo('Webhook saved.');
}, error => {

19
src/Squidex/app/framework/angular/autocomplete.component.ts

@ -17,7 +17,6 @@ const KEY_ENTER = 13;
const KEY_ESCAPE = 27;
const KEY_UP = 38;
const KEY_DOWN = 40;
const NOOP = () => { /* NOOP */ };
export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true
@ -31,8 +30,8 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
})
export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit {
private subscription: Subscription;
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
@Input()
public source: AutocompleteSource;
@ -57,7 +56,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public writeValue(value: any) {
if (!value) {
this.resetValue();
this.resetForm();
} else {
const item = this.items.find(i => i === value);
@ -79,11 +78,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public ngOnDestroy() {
@ -119,7 +118,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
this.down();
return false;
case KEY_ESCAPE:
this.resetValue();
this.resetForm();
this.reset();
return false;
case KEY_ENTER:
@ -135,7 +134,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public blur() {
this.reset();
this.touchedCallback();
this.callTouched();
}
public selectItem(selection: any | null = null) {
@ -154,7 +153,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
} else {
this.queryInput.setValue(selection.toString(), { emitEvent: false });
}
this.changeCallback(selection);
this.callChange(selection);
} finally {
this.reset();
}
@ -181,7 +180,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
this.selectIndex(this.selectedIndex + 1);
}
private resetValue() {
private resetForm() {
this.queryInput.setValue('');
}

12
src/Squidex/app/framework/angular/confirm-click.directive.ts

@ -10,11 +10,13 @@ import { Directive, EventEmitter, HostListener, Input, OnDestroy, Output } from
import { DialogService } from './../services/dialog.service';
class DelayEventEmitter<T> extends EventEmitter<T> {
private delayedNexts: any[] = [];
private delayedNexts: any[] | null = [];
public delayEmit() {
for (let callback of this.delayedNexts) {
callback();
if (this.delayedNexts) {
for (let callback of this.delayedNexts) {
callback();
}
}
}
@ -23,7 +25,9 @@ class DelayEventEmitter<T> extends EventEmitter<T> {
}
public subscribe(generatorOrNext?: any, error?: any, complete?: any): any {
this.delayedNexts.push(generatorOrNext);
if (this.delayedNexts) {
this.delayedNexts.push(generatorOrNext);
}
return super.subscribe(generatorOrNext, error, complete);
}

3
src/Squidex/app/framework/angular/copy.directive.ts

@ -7,6 +7,7 @@
import { Directive, HostListener, Input } from '@angular/core';
import { Types } from './../utils/types';
import { DialogService, Notification } from './../services/dialog.service';
@Directive({
@ -48,7 +49,7 @@ export class CopyDirective {
console.log('Copy failed');
}
if (currentFocus && typeof currentFocus.focus === 'function') {
if (currentFocus && Types.isFunction(currentFocus.focus)) {
currentFocus.focus();
}

26
src/Squidex/app/framework/angular/date-time-editor.component.ts

@ -10,9 +10,9 @@ import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/f
import { Subscription } from 'rxjs';
import * as moment from 'moment';
let Pikaday = require('pikaday/pikaday');
import { Types } from './../utils/types';
const NOOP = () => { /* NOOP */ };
let Pikaday = require('pikaday/pikaday');
export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateTimeEditorComponent), multi: true
@ -31,8 +31,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
private timeValue: any | null = null;
private dateValue: any | null = null;
private suppressEvents = false;
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
@Input()
public mode: string;
@ -86,8 +86,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
});
}
public writeValue(value: any) {
if (!value || value.length === 0) {
public writeValue(value: string) {
if (!Types.isString(value) || value.length === 0) {
this.timeValue = null;
this.dateValue = null;
} else {
@ -116,11 +116,11 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public ngAfterViewInit() {
@ -140,7 +140,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
}
public touched() {
this.touchedCallback();
this.callTouched();
}
public writeNow() {
@ -159,14 +159,14 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
this.dateValue = null;
this.changeCallback(null);
this.touchedCallback();
this.callChange(null);
this.callTouched();
return false;
}
private updateValue() {
let result: string | null;
let result: string | null = null;
if ((this.dateValue && !this.dateValue.isValid()) || (this.timeValue && !this.timeValue.isValid())) {
result = 'Invalid DateTime';
@ -184,7 +184,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
}
}
this.changeCallback(result);
this.callChange(result);
}
private updateControls() {

2
src/Squidex/app/framework/angular/dialog-renderer.component.ts

@ -32,7 +32,7 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
private notificationsSubscription: Subscription;
public dialogView = new ModalView(false, true);
public dialogRequest: DialogRequest;
public dialogRequest: DialogRequest | null = null;
public notifications: Notification[] = [];

13
src/Squidex/app/framework/angular/dropdown.component.ts

@ -12,7 +12,6 @@ const KEY_ENTER = 13;
const KEY_ESCAPE = 27;
const KEY_UP = 38;
const KEY_DOWN = 40;
const NOOP = () => { /* NOOP */ };
import { ModalView } from './../utils/modal-view';
@ -27,8 +26,8 @@ export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR]
})
export class DropdownComponent implements AfterContentInit, ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
@Input()
public items: any[] = [];
@ -69,11 +68,11 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public onKeyDown(event: KeyboardEvent) {
@ -95,7 +94,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
public open() {
this.dropdown.show();
this.touchedCallback();
this.callTouched();
}
public selectIndexAndClose(selectedIndex: number) {
@ -132,7 +131,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
this.selectedIndex = selectedIndex;
this.selectedItem = value;
this.changeCallback(value);
this.callChange(value);
}
}
}

31
src/Squidex/app/framework/angular/geolocation-editor.component.ts

@ -8,17 +8,22 @@
import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from './../utils/types';
import { ResourceLoaderService } from './../services/resource-loader.service';
import { ValidatorsEx } from './validators';
const NOOP = () => { /* NOOP */ };
declare var L: any;
export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => GeolocationEditorComponent), multi: true
};
interface Geolocation {
latitude: number;
longitude: number;
}
@Component({
selector: 'sqx-geolocation-editor',
styleUrls: ['./geolocation-editor.component.scss'],
@ -26,11 +31,11 @@ export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private marker: any;
private map: any;
private value: any;
private value: Geolocation | null = null;
public get hasValue() {
return !!this.value;
@ -59,8 +64,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
) {
}
public writeValue(value: any) {
this.value = value;
public writeValue(value: Geolocation) {
if (Types.isObject(value) && Types.isNumber(value.latitude) && Types.isNumber(value.longitude)) {
this.value = value;
} else {
this.value = null;
}
if (this.marker) {
this.updateMarker(true, false);
@ -102,11 +111,11 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public updateValueByInput() {
@ -201,8 +210,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
if (fireEvent) {
this.changeCallback(this.value);
this.touchedCallback();
this.callChange(this.value);
this.callTouched();
}
}
}

2
src/Squidex/app/framework/angular/http-extensions-impl.ts

@ -78,7 +78,7 @@ export module HTTP {
}
}
function handleVersion(httpRequest: Observable<HttpResponse<any>>, version: Version): Observable<any> {
function handleVersion(httpRequest: Observable<HttpResponse<any>>, version?: Version): Observable<any> {
return httpRequest.do((response: HttpResponse<any>) => {
if (version && response.status.toString().indexOf('2') === 0 && response.headers) {
const etag = response.headers.get('etag');

18
src/Squidex/app/framework/angular/indeterminate-value.directive.ts

@ -8,7 +8,7 @@
import { Directive, forwardRef, ElementRef, HostListener, Renderer } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const NOOP = () => { /* NOOP */ };
import { Types } from './../utils/types';
export const SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IndeterminateValueDirective), multi: true
@ -19,8 +19,8 @@ export const SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR]
})
export class IndeterminateValueDirective implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
constructor(
private readonly renderer: Renderer,
@ -30,16 +30,16 @@ export class IndeterminateValueDirective implements ControlValueAccessor {
@HostListener('change', ['$event.target.value'])
public onChange(value: any) {
this.changeCallback(value);
this.callChange(value);
}
@HostListener('blur')
public onTouched() {
this.touchedCallback();
this.callTouched();
}
public writeValue(value: any) {
if (value === undefined || value === null) {
public writeValue(value: boolean | number | undefined) {
if (!Types.isBoolean(value)) {
this.renderer.setElementProperty(this.element.nativeElement, 'indeterminate', true);
} else {
this.renderer.setElementProperty(this.element.nativeElement, 'checked', value);
@ -51,10 +51,10 @@ export class IndeterminateValueDirective implements ControlValueAccessor {
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
}

30
src/Squidex/app/framework/angular/jscript-editor.component.ts

@ -9,12 +9,12 @@ import { AfterViewInit, Component, forwardRef, ElementRef, ViewChild } from '@an
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs';
import { Types } from './../utils/types';
import { ResourceLoaderService } from './../services/resource-loader.service';
declare var ace: any;
const NOOP = () => { /* NOOP */ };
export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JscriptEditorComponent), multi: true
};
@ -26,11 +26,11 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private valueChanged = new Subject();
private aceEditor: any;
private oldValue: string;
private value: string;
private isDisabled = false;
@ViewChild('editor')
@ -41,11 +41,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn
) {
}
public writeValue(value: any) {
this.oldValue = value;
public writeValue(value: string) {
this.value = Types.isString(value) ? value : '';
if (this.aceEditor) {
this.setValue(value);
this.setValue(this.value);
}
}
@ -58,11 +58,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public ngAfterViewInit() {
@ -78,11 +78,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn
this.aceEditor.setReadOnly(this.isDisabled);
this.aceEditor.setFontSize(14);
this.setValue(this.oldValue);
this.setValue(this.value);
this.aceEditor.on('blur', () => {
this.changeValue();
this.touchedCallback();
this.callTouched();
});
this.aceEditor.on('change', () => {
@ -94,11 +94,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn
private changeValue() {
const newValue = this.aceEditor.getValue();
if (this.oldValue !== newValue) {
this.changeCallback(newValue);
if (this.value !== newValue) {
this.callChange(newValue);
}
this.oldValue = newValue;
this.value = newValue;
}
private setValue(value: string) {

35
src/Squidex/app/framework/angular/json-editor.component.ts

@ -13,8 +13,6 @@ import { ResourceLoaderService } from './../services/resource-loader.service';
declare var ace: any;
const NOOP = () => { /* NOOP */ };
export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JsonEditorComponent), multi: true
};
@ -26,12 +24,12 @@ export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private valueChanged = new Subject();
private aceEditor: any;
private oldValue: any;
private oldValueString: string;
private value: any;
private valueString: string;
private isDisabled = false;
@ViewChild('editor')
@ -43,8 +41,13 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit
}
public writeValue(value: any) {
this.oldValue = value;
this.oldValueString = JSON.stringify(value);
this.value = value;
try {
this.valueString = JSON.stringify(value);
} catch (e) {
this.valueString = '';
}
if (this.aceEditor) {
this.setValue(value);
@ -60,11 +63,11 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public ngAfterViewInit() {
@ -80,11 +83,11 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit
this.aceEditor.setReadOnly(this.isDisabled);
this.aceEditor.setFontSize(14);
this.setValue(this.oldValue);
this.setValue(this.value);
this.aceEditor.on('blur', () => {
this.changeValue();
this.touchedCallback();
this.callTouched();
});
this.aceEditor.on('change', () => {
@ -108,12 +111,12 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit
const newValueString = JSON.stringify(newValue);
if (this.oldValueString !== newValueString) {
this.changeCallback(newValue);
if (this.valueString !== newValueString) {
this.callChange(newValue);
}
this.oldValue = newValue;
this.oldValueString = newValueString;
this.value = newValue;
this.valueString = newValueString;
}
private setValue(value: any) {

18
src/Squidex/app/framework/angular/lowercase-input.directive.ts

@ -8,7 +8,7 @@
import { Directive, forwardRef, ElementRef, HostListener, Renderer } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const NOOP = () => { /* NOOP */ };
import { Types } from './../utils/types';
export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LowerCaseInputDirective), multi: true
@ -19,8 +19,8 @@ export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = {
providers: [SQX_LOWERCASE_INPUT_VALUE_ACCESSOR]
})
export class LowerCaseInputDirective implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
constructor(
private readonly element: ElementRef,
@ -33,16 +33,16 @@ export class LowerCaseInputDirective implements ControlValueAccessor {
const normalizedValue = (value == null ? '' : value.toString()).toLowerCase();
this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue);
this.changeCallback(normalizedValue);
this.callChange(normalizedValue);
}
@HostListener('blur')
public onTouched() {
this.touchedCallback();
this.callTouched();
}
public writeValue(value: any) {
const normalizedValue = (value == null ? '' : value.toString()).toLowerCase();
public writeValue(value: string) {
const normalizedValue = Types.isString(value) ? value.toLowerCase() : '';
this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue);
}
@ -52,10 +52,10 @@ export class LowerCaseInputDirective implements ControlValueAccessor {
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
}

24
src/Squidex/app/framework/angular/markdown-editor.component.ts

@ -8,12 +8,12 @@
import { AfterViewInit, Component, forwardRef, ElementRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from './../utils/types';
import { ResourceLoaderService } from './../services/resource-loader.service';
declare var SimpleMDE: any;
const NOOP = () => { /* NOOP */ };
export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true
};
@ -25,10 +25,10 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private simplemde: any;
private value: any;
private value: string;
private isDisabled = false;
@ViewChild('editor')
@ -48,11 +48,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css');
}
public writeValue(value: any) {
this.value = value;
public writeValue(value: string) {
this.value = Types.isString(value) ? value : '';
if (this.simplemde) {
this.simplemde.value(this.value || '');
this.simplemde.value(this.value);
}
}
@ -65,11 +65,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public ngAfterViewInit() {
@ -84,12 +84,12 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
if (this.value !== value) {
this.value = value;
this.changeCallback(value);
this.callChange(value);
}
});
this.simplemde.codemirror.on('blur', () => {
this.touchedCallback();
this.callTouched();
});
this.simplemde.codemirror.on('refresh', () => {

4
src/Squidex/app/framework/angular/modal-view.directive.ts

@ -16,8 +16,8 @@ import { RootViewService } from './../services/root-view.service';
selector: '[sqxModalView]'
})
export class ModalViewDirective implements OnChanges, OnDestroy {
private subscription: Subscription | null;
private clickHandler: Function | null;
private subscription: Subscription | null = null;
private clickHandler: Function | null = null;
private renderedView: EmbeddedViewRef<any> | null = null;
@Input('sqxModalView')

2
src/Squidex/app/framework/angular/panel-container.directive.ts

@ -49,7 +49,7 @@ export class PanelContainerDirective implements AfterViewInit, OnDestroy {
}
public invalidate(params?: { force: boolean, resize: boolean }) {
this.isInit = this.isInit || (params && params.force);
this.isInit = this.isInit || (params && params.force) === true;
if (!this.isInit) {
return;

24
src/Squidex/app/framework/angular/rich-editor.component.ts

@ -8,12 +8,12 @@
import { AfterViewInit, Component, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from './../utils/types';
import { ResourceLoaderService } from './../services/resource-loader.service';
declare var tinymce: any;
const NOOP = () => { /* NOOP */ };
export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true
};
@ -25,10 +25,10 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private tinyEditor: any;
private value: any;
private value: string;
private isDisabled = false;
@ViewChild('editor')
@ -39,11 +39,11 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
) {
}
public writeValue(value: any) {
this.value = value;
public writeValue(value: string) {
this.value = Types.isString(value) ? value : '';
if (this.tinyEditor) {
this.tinyEditor.setContent(value || '');
this.tinyEditor.setContent(this.value);
}
}
@ -56,11 +56,11 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public ngAfterViewInit() {
@ -78,12 +78,12 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
if (this.value !== value) {
this.value = value;
self.changeCallback(value);
self.callChange(value);
}
});
self.tinyEditor.on('blur', () => {
self.touchedCallback();
self.callTouched();
});
setTimeout(() => {

4
src/Squidex/app/framework/angular/router-utils.ts

@ -8,7 +8,7 @@
import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params } from '@angular/router';
export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data {
let snapshot: ActivatedRouteSnapshot = value['snapshot'] || value;
let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value;
const result: { [key: string]: any } = { };
@ -25,7 +25,7 @@ export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data {
return result;
}
export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Params {
let snapshot: ActivatedRouteSnapshot = value['snapshot'] || value;
let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value;
const result: { [key: string]: any } = { };

22
src/Squidex/app/framework/angular/slider.component.ts

@ -8,7 +8,7 @@
import { Component, ElementRef, forwardRef, Input, Renderer, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const NOOP = () => { /* NOOP */ };
import { Types } from './../utils/types';
export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SliderComponent), multi: true
@ -21,10 +21,10 @@ export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR]
})
export class SliderComponent implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private mouseMoveSubscription: Function | null;
private mouseUpSubscription: Function | null;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private mouseMoveSubscription: Function | null = null;
private mouseUpSubscription: Function | null = null;
private centerStartOffset = 0;
private startValue: number;
private lastValue: number;
@ -50,8 +50,8 @@ export class SliderComponent implements ControlValueAccessor {
constructor(private readonly renderer: Renderer) { }
public writeValue(value: any) {
this.lastValue = this.value = value;
public writeValue(value: number) {
this.lastValue = this.value = Types.isNumber(value) ? value : 0;
this.updateThumbPosition();
}
@ -61,11 +61,11 @@ export class SliderComponent implements ControlValueAccessor {
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public onBarMouseClick(event: MouseEvent): boolean {
@ -157,14 +157,14 @@ export class SliderComponent implements ControlValueAccessor {
}
private updateTouched() {
this.touchedCallback();
this.callTouched();
}
private updateValue() {
if (this.lastValue !== this.value) {
this.lastValue = this.value;
this.changeCallback(this.value);
this.callChange(this.value);
}
}

37
src/Squidex/app/framework/angular/stars.component.ts

@ -8,7 +8,7 @@
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const NOOP = () => { /* NOOP */ };
import { Types } from './../utils/types';
export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarsComponent), multi: true
@ -21,19 +21,15 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR]
})
export class StarsComponent implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private maximumStarsValue = 5;
@Input()
public set maximumStars(value: any) {
value = value || 5;
public set maximumStars(value: number) {
const maxStars: number = Types.isNumber(value) ? value : 5;
if (!(typeof value === 'number')) {
value = 5;
}
if (this.maximumStarsValue !== value) {
if (this.maximumStarsValue !== maxStars) {
this.maximumStarsValue = value;
this.starsArray = [];
@ -55,8 +51,13 @@ export class StarsComponent implements ControlValueAccessor {
public value: number | null = 1;
public writeValue(value: any) {
this.value = this.stars = value;
public writeValue(value: number | null | undefined) {
if (Types.isNumber(value)) {
this.value = this.stars = value || 0;
} else {
this.value = null;
this.stars = 0;
}
}
public setDisabledState(isDisabled: boolean): void {
@ -64,11 +65,11 @@ export class StarsComponent implements ControlValueAccessor {
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public setPreview(value: number) {
@ -96,8 +97,8 @@ export class StarsComponent implements ControlValueAccessor {
this.value = null;
this.stars = 0;
this.changeCallback(this.value);
this.touchedCallback();
this.callChange(this.value);
this.callTouched();
}
return false;
@ -111,8 +112,8 @@ export class StarsComponent implements ControlValueAccessor {
if (this.value !== value) {
this.value = this.stars = value;
this.changeCallback(this.value);
this.touchedCallback();
this.callChange(this.value);
this.callTouched();
}
return false;

56
src/Squidex/app/framework/angular/tag-editor.component.ts

@ -8,40 +8,54 @@
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from './../utils/types';
const KEY_ENTER = 13;
const NOOP = () => { /* NOOP */ };
export interface Converter {
convert(input: string): any;
isValid(input: string): boolean;
isValidInput(input: string): boolean;
isValidValue(value: any): boolean;
}
export class IntConverter implements Converter {
public isValid(input: string): boolean {
public isValidInput(input: string): boolean {
return !!parseInt(input, 10) || input === '0';
}
public isValidValue(value: any): boolean {
return Types.isNumber(value);
}
public convert(input: string): any {
return parseInt(input, 10) || 0;
}
}
export class FloatConverter implements Converter {
public isValid(input: string): boolean {
public isValidInput(input: string): boolean {
return !!parseFloat(input) || input === '0';
}
public isValidValue(value: any): boolean {
return Types.isNumber(value);
}
public convert(input: string): any {
return parseFloat(input) || 0;
}
}
export class NoopConverter implements Converter {
public isValid(input: string): boolean {
export class StringConverter implements Converter {
public isValidInput(input: string): boolean {
return input.trim().length > 0;
}
public isValidValue(value: any): boolean {
return Types.isString(value);
}
public convert(input: string): any {
return input.trim();
}
@ -58,11 +72,11 @@ export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class TagEditorComponent implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
@Input()
public converter: Converter = new NoopConverter();
public converter: Converter = new StringConverter();
@Input()
public useDefaultValue = true;
@ -74,10 +88,10 @@ export class TagEditorComponent implements ControlValueAccessor {
public addInput = new FormControl();
public writeValue(value: any) {
this.addInput.setValue('');
public writeValue(value: any[]) {
this.resetForm();
if (Array.isArray(value)) {
if (this.converter && Types.isArrayOf(value, v => this.converter.isValidValue(v))) {
this.items = value;
} else {
this.items = [];
@ -93,11 +107,11 @@ export class TagEditorComponent implements ControlValueAccessor {
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public remove(index: number) {
@ -105,18 +119,22 @@ export class TagEditorComponent implements ControlValueAccessor {
}
public markTouched() {
this.touchedCallback();
this.callTouched();
}
private resetForm() {
this.addInput.reset();
}
public onKeyDown(event: KeyboardEvent) {
if (event.keyCode === KEY_ENTER) {
const value = <string>this.addInput.value;
if (this.converter.isValid(value)) {
if (this.converter.isValidInput(value)) {
const converted = this.converter.convert(value);
this.updateItems([...this.items, converted]);
this.addInput.reset();
this.resetForm();
return false;
}
}
@ -128,9 +146,9 @@ export class TagEditorComponent implements ControlValueAccessor {
this.items = items;
if (items.length === 0 && this.useDefaultValue) {
this.changeCallback(undefined);
this.callChange(undefined);
} else {
this.changeCallback(this.items);
this.callChange(this.items);
}
}
}

21
src/Squidex/app/framework/angular/toggle.component.ts

@ -8,7 +8,7 @@
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const NOOP = () => { /* NOOP */ };
import { Types } from './../utils/types';
export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToggleComponent), multi: true
@ -21,14 +21,14 @@ export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR]
})
export class ToggleComponent implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
public isChecked: boolean | undefined = undefined;
public isChecked: boolean | null = null;
public isDisabled = false;
public writeValue(value: any) {
this.isChecked = value;
public writeValue(value: boolean | null | undefined) {
this.isChecked = Types.isBoolean(value) ? value || null : null;
}
public setDisabledState(isDisabled: boolean): void {
@ -36,20 +36,21 @@ export class ToggleComponent implements ControlValueAccessor {
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
this.callTouched = fn;
}
public changeState() {
if (this.isDisabled) {
return;
}
this.isChecked = !(this.isChecked === true);
this.changeCallback(this.isChecked);
this.touchedCallback();
this.callChange(this.isChecked);
this.callTouched();
}
}

39
src/Squidex/app/framework/angular/validators.ts

@ -12,6 +12,7 @@ import {
} from '@angular/forms';
import { DateTime } from './../utils/date-time';
import { Types } from './../utils/types';
export module ValidatorsEx {
export function pattern(regex: string | RegExp, message?: string): ValidatorFn {
@ -30,7 +31,7 @@ export module ValidatorsEx {
regeExp = regex;
}
return (control: AbstractControl): { [key: string]: any } => {
return (control: AbstractControl) => {
const n: string = control.value;
if (n == null || n.length === 0) {
@ -49,16 +50,16 @@ export module ValidatorsEx {
};
}
export function match(otherControlName: string, message: string) {
let otherControl: AbstractControl = null;
export function match(otherControlName: string, message: string): ValidatorFn {
let otherControl: AbstractControl | null = null;
return (control: AbstractControl): { [key: string]: any } => {
return (control: AbstractControl) => {
if (!control.parent) {
return null;
}
if (otherControl === null) {
otherControl = control.parent.get(otherControlName) || undefined;
otherControl = control.parent.get(otherControlName);
if (!otherControl) {
throw new Error('matchValidator(): other control is not found in parent group');
@ -77,8 +78,8 @@ export module ValidatorsEx {
};
}
export function validDateTime() {
return (control: AbstractControl): { [key: string]: any } => {
export function validDateTime(): ValidatorFn {
return (control: AbstractControl) => {
const v: string = control.value;
if (v) {
@ -93,32 +94,32 @@ export module ValidatorsEx {
};
}
export function between(minValue?: number, maxValue?: number) {
export function between(minValue?: number, maxValue?: number): ValidatorFn {
if (!minValue || !maxValue) {
return Validators.nullValidator;
}
return (control: AbstractControl): { [key: string]: any } => {
const n: number = control.value;
return (control: AbstractControl) => {
const value: number = control.value;
if (typeof n !== 'number') {
if (!Types.isNumber(value)) {
return { validnumber: false };
} else if (minValue && n < minValue) {
return { minvalue: { minValue, actualValue: n } };
} else if (maxValue && n > maxValue) {
return { maxvalue: { maxValue, actualValue: n } };
} else if (minValue && value < minValue) {
return { minvalue: { minValue, actualValue: value } };
} else if (maxValue && value > maxValue) {
return { maxvalue: { maxValue, actualValue: value } };
}
return null;
};
}
export function validValues<T>(values: T[]) {
export function validValues<T>(values: T[]): ValidatorFn {
if (!values) {
return Validators.nullValidator;
}
return (control: AbstractControl): { [key: string]: any } => {
return (control: AbstractControl) => {
const n: T = control.value;
if (values.indexOf(n) < 0) {
@ -129,8 +130,8 @@ export module ValidatorsEx {
};
}
export function noop() {
return (control: AbstractControl): { [key: string]: any } => {
export function noop(): ValidatorFn {
return (control: AbstractControl) => {
return null;
};
}

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

@ -71,4 +71,5 @@ export * from './utils/math-helper';
export * from './utils/modal-view';
export * from './utils/pager';
export * from './utils/string-helper';
export * from './utils/types';
export * from './utils/version';

2
src/Squidex/app/framework/services/local-cache.service.ts

@ -31,7 +31,7 @@ export class LocalCacheService {
}
}
public get<T>(key: string, now?: number): T {
public get<T>(key: string, now?: number): T | undefined {
const entry = this.entries[key];
if (entry) {

92
src/Squidex/app/framework/utils/types.spec.ts

@ -0,0 +1,92 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Types } from './../';
describe('Types', () => {
it('should make string check', () => {
expect(Types.isString('')).toBeTruthy();
expect(Types.isString('string')).toBeTruthy();
expect(Types.isString(false)).toBeFalsy();
});
it('should make number check', () => {
expect(Types.isNumber(0)).toBeTruthy();
expect(Types.isNumber(1)).toBeTruthy();
expect(Types.isNumber(NaN)).toBeFalsy();
expect(Types.isNumber(Infinity)).toBeFalsy();
expect(Types.isNumber(false)).toBeFalsy();
});
it('should make boolean check', () => {
expect(Types.isBoolean(true)).toBeTruthy();
expect(Types.isBoolean(false)).toBeTruthy();
expect(Types.isBoolean(0)).toBeFalsy();
expect(Types.isBoolean(1)).toBeFalsy();
});
it('should make number array check', () => {
expect(Types.isArrayOfNumber([])).toBeTruthy();
expect(Types.isArrayOfNumber([0, 1])).toBeTruthy();
expect(Types.isArrayOfNumber(['0', 1])).toBeFalsy();
});
it('should make string array check', () => {
expect(Types.isArrayOfString([])).toBeTruthy();
expect(Types.isArrayOfString(['0', '1'])).toBeTruthy();
expect(Types.isArrayOfString(['0', 1])).toBeFalsy();
});
it('should make array check', () => {
expect(Types.isArray([])).toBeTruthy();
expect(Types.isArray([0])).toBeTruthy();
expect(Types.isArray({})).toBeFalsy();
});
it('should make object check', () => {
expect(Types.isObject({})).toBeTruthy();
expect(Types.isObject({ v: 1 })).toBeTruthy();
expect(Types.isObject([])).toBeFalsy();
});
it('should make RegExp check', () => {
expect(Types.isRegExp(/[.*]/)).toBeTruthy();
expect(Types.isRegExp('/[.*]/')).toBeFalsy();
});
it('should make Date check', () => {
expect(Types.isDate(new Date())).toBeTruthy();
expect(Types.isDate(new Date().getDate())).toBeFalsy();
});
it('should make undefined check', () => {
expect(Types.isUndefined(undefined)).toBeTruthy();
expect(Types.isUndefined(null)).toBeFalsy();
});
it('should make null check', () => {
expect(Types.isNull(null)).toBeTruthy();
expect(Types.isNull(undefined)).toBeFalsy();
});
it('should make function check', () => {
expect(Types.isFunction(() => { /* NOOP */ })).toBeTruthy();
expect(Types.isFunction([])).toBeFalsy();
});
});

70
src/Squidex/app/framework/utils/types.ts

@ -0,0 +1,70 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export module Types {
export function isString(value: any): boolean {
return typeof value === 'string' || value instanceof String;
}
export function isNumber(value: any): boolean {
return typeof value === 'number' && isFinite(value);
}
export function isArray(value: any): boolean {
return Array.isArray(value);
}
export function isFunction(value: any): boolean {
return typeof value === 'function';
}
export function isObject(value: any): boolean {
return value && typeof value === 'object' && value.constructor === Object;
}
export function isBoolean(value: any): boolean {
return typeof value === 'boolean';
};
export function isNull(value: any): boolean {
return value === null;
}
export function isUndefined(value: any): boolean {
return typeof value === 'undefined';
}
export function isRegExp(value: any): boolean {
return value && typeof value === 'object' && value.constructor === RegExp;
}
export function isDate(value: any): boolean {
return value instanceof Date;
}
export function isArrayOfNumber(value: any): boolean {
return isArrayOf(value, v => isNumber(v));
}
export function isArrayOfString(value: any): boolean {
return isArrayOf(value, v => isString(v));
}
export function isArrayOf(value: any, validator: (v: any) => boolean): boolean {
if (!Array.isArray(value)) {
return false;
}
for (let v of value) {
if (!validator(v)) {
return false;
}
}
return true;
}
}

7
src/Squidex/app/shared/components/asset.component.ts

@ -16,6 +16,7 @@ import {
AssetReplacedDto,
AssetsService,
AuthService,
DateTime,
DialogService,
fadeAnimation,
ModalView,
@ -83,7 +84,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
if (initFile) {
this.appNameOnce()
.switchMap(app => this.assetsService.uploadFile(app, initFile, this.authService.user.token))
.switchMap(app => this.assetsService.uploadFile(app, initFile, this.authService.user!.token, DateTime.now()))
.subscribe(dto => {
if (dto instanceof AssetDto) {
this.emitLoaded(dto);
@ -104,7 +105,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
.switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.assetVersion))
.subscribe(dto => {
if (dto instanceof AssetReplacedDto) {
this.updateAsset(this.asset.update(dto, this.authService.user.token), true);
this.updateAsset(this.asset.update(dto, this.authService.user!.token), true);
} else {
this.setProgress(dto);
}
@ -126,7 +127,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.assetVersion))
.subscribe(() => {
this.updateAsset(this.asset.rename(requestDto.fileName, this.authService.user.token), true);
this.updateAsset(this.asset.rename(requestDto.fileName, this.authService.user!.token), true);
this.resetRenameForm();
}, error => {
this.notifyError(error);

4
src/Squidex/app/shared/components/history.component.scss

@ -7,10 +7,6 @@
margin-bottom: 1rem;
}
&-message {
font-size: .9rem;
}
&-main {
@include flex-grow(1);
}

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

@ -14,14 +14,14 @@ import { allParams } from 'framework';
import { AppLanguageDto, AppLanguagesService } from './../services/app-languages.service';
@Injectable()
export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[]> {
export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[] | null> {
constructor(
private readonly appLanguagesService: AppLanguagesService,
private readonly router: Router
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<AppLanguageDto[]> {
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<AppLanguageDto[] | null> {
const params = allParams(route);
const appName = params['appName'];

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

@ -14,14 +14,14 @@ import { allParams } from 'framework';
import { ContentDto, ContentsService } from './../services/contents.service';
@Injectable()
export class ResolveContentGuard implements Resolve<ContentDto> {
export class ResolveContentGuard implements Resolve<ContentDto | null> {
constructor(
private readonly contentsService: ContentsService,
private readonly router: Router
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ContentDto> {
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ContentDto | null> {
const params = allParams(route);
const appName = params['appName'];

4
src/Squidex/app/shared/guards/resolve-user.guard.ts

@ -14,14 +14,14 @@ import { allParams } from 'framework';
import { UserDto, UserManagementService } from './../services/users.service';
@Injectable()
export class ResolveUserGuard implements Resolve<UserDto> {
export class ResolveUserGuard implements Resolve<UserDto | null> {
constructor(
private readonly userManagementService: UserManagementService,
private readonly router: Router
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<UserDto> {
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<UserDto | null> {
const params = allParams(route);
const userId = params['userId'];

2
src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts

@ -18,7 +18,7 @@ import {
} from './../';
describe('AuthInterceptor', () => {
let authService: IMock<AuthService> = null;
let authService: IMock<AuthService>;
beforeEach(() => {
authService = Mock.ofType(AuthService);

2
src/Squidex/app/shared/interceptors/auth.interceptor.ts

@ -32,7 +32,7 @@ export class AuthInterceptor implements HttpInterceptor {
}
}
private makeRequest(req: HttpRequest<any>, next: HttpHandler, user: Profile, renew = false): Observable<HttpEvent<any>> {
private makeRequest(req: HttpRequest<any>, next: HttpHandler, user: Profile | null, renew = false): Observable<HttpEvent<any>> {
const token = user ? user.authToken : '';
const authReq = req.clone({

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save