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); EnableDisallow(engine);
EnableReject(engine, operationName); 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(() => engine.SetValue("replace", new Action(() =>
{ {
var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); 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); 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); engine.SetValue("ctx", contextInstance);
return engine; 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 Data { get; set; }
public NamedContentData OldData { 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 ScriptDelete { get; set; }
public string ScriptPublish { get; set; } public string ScriptChange { get; set; }
public string ScriptUnpublish { get; set; }
} }
} }

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

@ -36,10 +36,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets
[BsonElement] [BsonElement]
public bool IsImage { get; set; } public bool IsImage { get; set; }
[BsonRequired]
[BsonElement]
public bool IsDeleted { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public long Version { get; set; } 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) 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) 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>> var filters = new List<FilterDefinition<MongoAssetEntity>>
{ {
Filter.Eq(x => x.AppId, appId), Filter.Eq(x => x.AppId, appId)
Filter.Eq(x => x.IsDeleted, false)
}; };
if (ids != null && ids.Count > 0) 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 System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Domain.Apps.Read.MongoDb.Utils;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
@ -58,10 +59,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets
protected Task On(AssetDeleted @event, EnvelopeHeaders headers) protected Task On(AssetDeleted @event, EnvelopeHeaders headers)
{ {
return Collection.UpdateAsync(@event, headers, a => return Collection.DeleteOneAsync(x => x.Id == @event.AssetId);
{
a.IsDeleted = true;
});
} }
} }
} }

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

@ -47,6 +47,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
[BsonElement("pu")] [BsonElement("pu")]
public bool IsPublished { get; set; } public bool IsPublished { get; set; }
[BsonRequired]
[BsonElement("dl")]
public bool IsArchived { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement("dt")] [BsonElement("dt")]
public string DataText { get; set; } 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; 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); var collection = GetCollection(app.Id);
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{ {
cursor = cursor =
collection collection
.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished) .Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived)
.Take(odataQuery) .Take(odataQuery)
.Skip(odataQuery) .Skip(odataQuery)
.Sort(odataQuery, schema.SchemaDef); .Sort(odataQuery, schema.SchemaDef);
@ -104,14 +104,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
return entities; 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); var collection = GetCollection(app.Id);
IFindFluent<MongoContentEntity, MongoContentEntity> cursor; IFindFluent<MongoContentEntity, MongoContentEntity> cursor;
try try
{ {
cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished); cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived);
} }
catch (NotSupportedException) 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.SchemaId).Descending(x => x.LastModified));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); 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.IsPublished));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsArchived));
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); 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( return collection.UpdateAsync(@event, headers, x =>
Filter.And( {
Filter.AnyEq(x => x.ReferencedIds, @event.ContentId), x.IsArchived = true;
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), });
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); });
}
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) private Task ForAppIdAsync(Guid appId, Func<IMongoCollection<MongoContentEntity>, Task> action)
{ {
var collection = GetCollection(appId); 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; 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); 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>> 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) if (!nonPublished)

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

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

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

@ -9,7 +9,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
@ -25,7 +24,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
{ {
private readonly List<IHistoryEventsCreator> creators; private readonly List<IHistoryEventsCreator> creators;
private readonly Dictionary<string, string> texts = new Dictionary<string, string>(); private readonly Dictionary<string, string> texts = new Dictionary<string, string>();
private int sessionEventCount;
public string Name public string Name
{ {
@ -64,7 +62,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
.Ascending(x => x.AppId) .Ascending(x => x.AppId)
.Ascending(x => x.Channel) .Ascending(x => x.Channel)
.Descending(x => x.Created) .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) })); 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 = var historyEventEntities =
await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix) 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(); .ToListAsync();
return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); 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.Id = Guid.NewGuid();
entity.SessionEventIndex = Interlocked.Increment(ref sessionEventCount); entity.Version = @event.Headers.EventStreamNumber();
entity.Channel = message.Channel; entity.Channel = message.Channel;
entity.Message = message.Message; 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; } get { return inner.LastModified; }
} }
public long Version
{
get { return inner.Version; }
}
public string Channel public string Channel
{ {
get { return inner.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] [BsonElement]
public string Name { get; set; } public string Name { get; set; }
public string ScriptUnpublish { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public string Schema { get; set; } public string Schema { get; set; }
@ -73,7 +71,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
[BsonIgnoreIfNull] [BsonIgnoreIfNull]
[BsonElement] [BsonElement]
public string ScriptPublish { get; set; } public string ScriptChange { get; set; }
Schema ISchemaEntity.SchemaDef 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 IsImage { get; }
bool IsDeleted { get; }
int? PixelWidth { get; } int? PixelWidth { get; }
int? PixelHeight { 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) : base(typeNameRegistry)
{ {
AddEventMessage<ContentCreated>( AddEventMessage<ContentCreated>(
"created content element."); "created content item.");
AddEventMessage<ContentUpdated>( AddEventMessage<ContentUpdated>(
"updated content element."); "updated content item.");
AddEventMessage<ContentDeleted>( AddEventMessage<ContentDeleted>(
"deleted content element."); "deleted content item.");
AddEventMessage<ContentRestored>(
"restored content item.");
AddEventMessage<ContentPublished>( AddEventMessage<ContentPublished>(
"published content element."); "published content item.");
AddEventMessage<ContentUnpublished>( AddEventMessage<ContentUnpublished>(
"unpublished content element."); "unpublished content item.");
} }
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event) 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); 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(app, nameof(app));
Guard.NotNull(user, nameof(user)); Guard.NotNull(user, nameof(user));
@ -85,8 +85,8 @@ namespace Squidex.Domain.Apps.Read.Contents
var isFrontendClient = user.IsInClient("squidex-frontend"); var isFrontendClient = user.IsInClient("squidex-frontend");
var taskForItems = contentRepository.QueryAsync(app, schema, isFrontendClient, ids, parsedQuery); var taskForItems = contentRepository.QueryAsync(app, schema, isFrontendClient, archived, ids, parsedQuery);
var taskForCount = contentRepository.CountAsync(app, schema, isFrontendClient, ids, parsedQuery); var taskForCount = contentRepository.CountAsync(app, schema, isFrontendClient, archived, ids, parsedQuery);
await Task.WhenAll(taskForItems, taskForCount); await Task.WhenAll(taskForItems, taskForCount);
@ -156,6 +156,7 @@ namespace Squidex.Domain.Apps.Read.Contents
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid AppId { get; set; } public Guid AppId { get; set; }
public long Version { get; set; } public long Version { get; set; }
public bool IsArchived { get; set; }
public bool IsPublished { get; set; } public bool IsPublished { get; set; }
public Instant Created { get; set; } public Instant Created { get; set; }
public Instant LastModified { 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) 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) foreach (var content in contents)
{ {
@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
if (notLoadedContents.Count > 0) 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) 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 IsPublished { get; }
bool IsArchived { get; }
NamedContentData Data { 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 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); 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 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<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); 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; } string Message { get; }
long Version { get; }
RefToken Actor { 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 ScriptDelete { get; }
string ScriptPublish { get; } string ScriptChange { get; }
string ScriptUnpublish { get; }
Schema SchemaDef { 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 => return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{ {
var schemaAndApp = await ResolveSchemaAndAppAsync(command); 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); content.Publish(command);
}); });
@ -129,20 +129,46 @@ namespace Squidex.Domain.Apps.Write.Contents
return handler.UpdateAsync<ContentDomainObject>(context, async content => return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{ {
var schemaAndApp = await ResolveSchemaAndAppAsync(command); 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); 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) protected Task On(DeleteContent command, CommandContext context)
{ {
return handler.UpdateAsync<ContentDomainObject>(context, async content => return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{ {
var schemaAndApp = await ResolveSchemaAndAppAsync(command); var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command); var scriptContext = CreateScriptContext(content, command, "Delete");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptDelete, "delete content"); 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) 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 isDeleted;
private bool isCreated; private bool isCreated;
private bool isPublished; private bool isPublished;
private bool isArchived;
private NamedContentData data; private NamedContentData data;
public bool IsDeleted public bool IsDeleted
@ -30,6 +31,11 @@ namespace Squidex.Domain.Apps.Write.Contents
get { return isDeleted; } get { return isDeleted; }
} }
public bool IsArchived
{
get { return isArchived; }
}
public bool IsPublished public bool IsPublished
{ {
get { return isPublished; } get { return isPublished; }
@ -67,6 +73,16 @@ namespace Squidex.Domain.Apps.Write.Contents
isPublished = false; isPublished = false;
} }
protected void On(ContentArchived @event)
{
isArchived = true;
}
protected void On(ContentRestored @event)
{
isArchived = false;
}
protected void On(ContentDeleted @event) protected void On(ContentDeleted @event)
{ {
isDeleted = true; isDeleted = true;
@ -99,6 +115,28 @@ namespace Squidex.Domain.Apps.Write.Contents
return this; 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) public ContentDomainObject Publish(PublishContent command)
{ {
Guard.NotNull(command, nameof(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() private void VerifyCreatedAndNotDeleted()
{ {
if (isDeleted || !isCreated) 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 ScriptDelete { get; set; }
public string ScriptPublish { get; set; } public string ScriptChange { get; set; }
public string ScriptUnpublish { get; set; }
} }
} }

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

@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
foreach (var storedEvent in events) foreach (var storedEvent in events)
{ {
var envelope = ParseKnownCommand(storedEvent); var envelope = ParseKnownEvent(storedEvent);
if (envelope != null) 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 try
{ {

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

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

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

@ -133,7 +133,7 @@ namespace Squidex.Controllers.Api.Assets
{ {
var entity = await assetRepository.FindAssetAsync(id); var entity = await assetRepository.FindAssetAsync(id);
if (entity == null || entity.IsDeleted) if (entity == null)
{ {
return NotFound(); 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. /// The time when the event happened.
/// </summary> /// </summary>
public Instant Created { get; set; } 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; } public string ScriptDelete { get; set; }
/// <summary> /// <summary>
/// The script that is executed when publishing a content. /// The script that is executed when change a content status.
/// </summary> /// </summary>
public string ScriptPublish { get; set; } public string ScriptChange { get; set; }
/// <summary>
/// The script that is executed when unpublishing a content.
/// </summary>
public string ScriptUnpublish { 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; } public string ScriptDelete { get; set; }
/// <summary> /// <summary>
/// The script that is executed when publishing a content. /// The script that is executed when changing a content status.
/// </summary> /// </summary>
public string ScriptPublish { get; set; } public string ScriptChange { get; set; }
/// <summary>
/// The script that is executed when unpublishing a content.
/// </summary>
public string ScriptUnpublish { get; set; }
/// <summary> /// <summary>
/// The list of fields. /// 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 context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>(); 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); 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 public sealed class ContentsController : ControllerBase
{ {
private readonly IContentQueryService contentQuery; private readonly IContentQueryService contentQuery;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IGraphQLService graphQl; private readonly IGraphQLService graphQl;
public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IGraphQLService graphQl) public ContentsController(ICommandBus commandBus,
IContentQueryService contentQuery,
IContentVersionLoader contentVersionLoader,
IGraphQLService graphQl)
: base(commandBus) : base(commandBus)
{ {
this.contentQuery = contentQuery; this.contentQuery = contentQuery;
this.contentVersionLoader = contentVersionLoader;
this.graphQl = graphQl; this.graphQl = graphQl;
} }
@ -64,7 +69,7 @@ namespace Squidex.Controllers.ContentApi
[HttpGet] [HttpGet]
[Route("content/{app}/{name}")] [Route("content/{app}/{name}")]
[ApiCosts(2)] [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>(); var idsList = new HashSet<Guid>();
@ -81,7 +86,7 @@ namespace Squidex.Controllers.ContentApi
var isFrontendClient = User.IsFrontendClient(); 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 var response = new AssetsDto
{ {
@ -124,6 +129,21 @@ namespace Squidex.Controllers.ContentApi
return Ok(response); 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] [MustBeAppEditor]
[HttpPost] [HttpPost]
[Route("content/{app}/{name}/")] [Route("content/{app}/{name}/")]
@ -208,6 +228,36 @@ namespace Squidex.Controllers.ContentApi
return NoContent(); 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] [MustBeAppEditor]
[HttpDelete] [HttpDelete]
[Route("content/{app}/{name}/{id}")] [Route("content/{app}/{name}/{id}")]

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

@ -94,6 +94,8 @@ namespace Squidex.Controllers.ContentApi.Generator
GenerateSchemaPatchOperation(), GenerateSchemaPatchOperation(),
GenerateSchemaPublishOperation(), GenerateSchemaPublishOperation(),
GenerateSchemaUnpublishOperation(), GenerateSchemaUnpublishOperation(),
GenerateSchemaArchiveOperation(),
GenerateSchemaRestoreOperation(),
GenerateSchemaDeleteOperation() GenerateSchemaDeleteOperation()
}; };
@ -109,6 +111,7 @@ namespace Squidex.Controllers.ContentApi.Generator
{ {
operation.OperationId = $"Query{schemaKey}Contents"; operation.OperationId = $"Query{schemaKey}Contents";
operation.Summary = $"Queries {schemaName} contents."; operation.Summary = $"Queries {schemaName} contents.";
operation.Security = ReaderSecurity;
operation.Description = SchemaQueryDescription; operation.Description = SchemaQueryDescription;
@ -119,8 +122,6 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition."); operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema)); 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.OperationId = $"Get{schemaKey}Content";
operation.Summary = $"Get a {schemaName} content."; operation.Summary = $"Get a {schemaName} content.";
operation.Security = ReaderSecurity;
operation.AddResponse("200", $"{schemaName} content found.", contentSchema); 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.OperationId = $"Create{schemaKey}Content";
operation.Summary = $"Create a {schemaName} content."; operation.Summary = $"Create a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content."); operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content.");
operation.AddResponse("201", $"{schemaName} created.", contentSchema); operation.AddResponse("201", $"{schemaName} created.", contentSchema);
operation.Security = EditorSecurity;
}); });
} }
@ -159,12 +158,11 @@ namespace Squidex.Controllers.ContentApi.Generator
{ {
operation.OperationId = $"Update{schemaKey}Content"; operation.OperationId = $"Update{schemaKey}Content";
operation.Summary = $"Update a {schemaName} content."; operation.Summary = $"Update a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("201", $"{schemaName} element updated.", dataSchema); operation.AddResponse("201", $"{schemaName} item updated.", dataSchema);
operation.Security = EditorSecurity;
}); });
} }
@ -174,12 +172,11 @@ namespace Squidex.Controllers.ContentApi.Generator
{ {
operation.OperationId = $"Path{schemaKey}Content"; operation.OperationId = $"Path{schemaKey}Content";
operation.Summary = $"Patchs a {schemaName} content."; operation.Summary = $"Patchs a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription); operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription);
operation.AddResponse("201", $"{schemaName} element patched.", dataSchema); operation.AddResponse("201", $"{schemaName} item patched.", dataSchema);
operation.Security = EditorSecurity;
}); });
} }
@ -189,10 +186,9 @@ namespace Squidex.Controllers.ContentApi.Generator
{ {
operation.OperationId = $"Publish{schemaKey}Content"; operation.OperationId = $"Publish{schemaKey}Content";
operation.Summary = $"Publish a {schemaName} content."; operation.Summary = $"Publish a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} element published.");
operation.Security = EditorSecurity; operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} item published.");
}); });
} }
@ -202,10 +198,33 @@ namespace Squidex.Controllers.ContentApi.Generator
{ {
operation.OperationId = $"Unpublish{schemaKey}Content"; operation.OperationId = $"Unpublish{schemaKey}Content";
operation.Summary = $"Unpublish a {schemaName} 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.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.OperationId = $"Delete{schemaKey}Content";
operation.Summary = $"Delete a {schemaName} content."; operation.Summary = $"Delete a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} content deleted."); 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 public sealed class ContentDto
{ {
/// <summary> /// <summary>
/// The if of the content element. /// The if of the content item.
/// </summary> /// </summary>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary> /// <summary>
/// The user that has created the content element. /// The user that has created the content item.
/// </summary> /// </summary>
[Required] [Required]
public RefToken CreatedBy { get; set; } public RefToken CreatedBy { get; set; }
/// <summary> /// <summary>
/// The user that has updated the content element. /// The user that has updated the content item.
/// </summary> /// </summary>
[Required] [Required]
public RefToken LastModifiedBy { get; set; } public RefToken LastModifiedBy { get; set; }
@ -42,20 +42,25 @@ namespace Squidex.Controllers.ContentApi.Models
public object Data { get; set; } public object Data { get; set; }
/// <summary> /// <summary>
/// The date and time when the content element has been created. /// The date and time when the content item has been created.
/// </summary> /// </summary>
public Instant Created { get; set; } public Instant Created { get; set; }
/// <summary> /// <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> /// </summary>
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
/// <summary> /// <summary>
/// Indicates if the content element is publihed. /// Indicates if the content item is published.
/// </summary> /// </summary>
public bool? IsPublished { get; set; } public bool? IsPublished { get; set; }
/// <summary>
/// Indicates if the content item is archived.
/// </summary>
public bool IsArchived { get; set; }
/// <summary> /// <summary>
/// The version of the content. /// The version of the content.
/// </summary> /// </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 currentUserId: string;
public userFormSubmitted = false; public userFormSubmitted = false;
public userForm: FormGroup; public userForm: FormGroup;
public userId: string;
public userFormError? = ''; public userFormError? = '';
public isCurrentUser = false; public isCurrentUser = false;
@ -86,7 +85,7 @@ export class UserPageComponent extends ComponentBase implements OnInit {
this.resetUserForm(error.displayMessage); this.resetUserForm(error.displayMessage);
}); });
} else { } else {
this.userManagementService.putUser(this.userId, requestDto) this.userManagementService.putUser(this.user.id, requestDto)
.subscribe(() => { .subscribe(() => {
this.user = this.user =
this.user.update( this.user.update(
@ -119,7 +118,6 @@ export class UserPageComponent extends ComponentBase implements OnInit {
const input = this.user || {}; const input = this.user || {};
this.isNewMode = !this.user; this.isNewMode = !this.user;
this.userId = input['id'];
this.userForm = this.userForm =
this.formBuilder.group({ this.formBuilder.group({
email: [input['email'], 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(); 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-field.component';
export * from './pages/content/content-history.component';
export * from './pages/content/content-page.component'; export * from './pages/content/content-page.component';
export * from './pages/contents/contents-page.component'; export * from './pages/contents/contents-page.component';
export * from './pages/contents/search-form.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 { import {
CanDeactivateGuard, CanDeactivateGuard,
HistoryComponent,
ResolveAppLanguagesGuard, ResolveAppLanguagesGuard,
ResolveContentGuard, ResolveContentGuard,
ResolvePublishedSchemaGuard, ResolvePublishedSchemaGuard,
@ -22,6 +21,7 @@ import {
import { import {
AssetsEditorComponent, AssetsEditorComponent,
ContentFieldComponent, ContentFieldComponent,
ContentHistoryComponent,
ContentPageComponent, ContentPageComponent,
ContentItemComponent, ContentItemComponent,
ContentsPageComponent, ContentsPageComponent,
@ -76,7 +76,7 @@ const routes: Routes = [
children: [ children: [
{ {
path: 'history', path: 'history',
component: HistoryComponent, component: ContentHistoryComponent,
data: { data: {
channel: 'contents.{contentId}' channel: 'contents.{contentId}'
} }
@ -112,6 +112,7 @@ const routes: Routes = [
declarations: [ declarations: [
AssetsEditorComponent, AssetsEditorComponent,
ContentFieldComponent, ContentFieldComponent,
ContentHistoryComponent,
ContentItemComponent, ContentItemComponent,
ContentPageComponent, ContentPageComponent,
ContentsPageComponent, 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"> <sqx-panel panelWidth="53rem">
<div class="panel-header"> <div class="panel-header">
<div class="panel-title-row"> <div class="panel-title-row">
<div class="float-right"> <div class="float-right" *ngIf="!content || !content.isArchived">
<span *ngIf="isNewMode"> <span *ngIf="isNewMode">
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S"> <button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S">
Save as Draft Save as Draft
@ -19,16 +19,19 @@
Save Save
</button> </button>
</span> </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"> <h3 class="panel-title" *ngIf="isNewMode">
New Content New Content
</h3> </h3>
<h3 class="panel-title" *ngIf="!isNewMode"> <h3 class="panel-title" *ngIf="!isNewMode && !content.isArchived">
Edit Content Edit Content
</h3> </h3>
<h3 class="panel-title" *ngIf="!isNewMode && content.isArchived">
Show Content
</h3>
</div> </div>
<a class="panel-close" sqxParentLink> <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 { import {
ContentCreated, ContentCreated,
ContentDeleted, ContentRemoved,
ContentUpdated ContentUpdated,
ContentVersionSelected
} from './../messages'; } from './../messages';
import { import {
@ -38,15 +39,13 @@ import {
}) })
export class ContentPageComponent extends AppComponentBase implements CanComponentDeactivate, OnDestroy, OnInit { export class ContentPageComponent extends AppComponentBase implements CanComponentDeactivate, OnDestroy, OnInit {
private contentDeletedSubscription: Subscription; private contentDeletedSubscription: Subscription;
private version = new Version(''); private contentVersionSelectedSubscription: Subscription;
private content: ContentDto;
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public content: ContentDto;
public contentFormSubmitted = false; public contentFormSubmitted = false;
public contentForm: FormGroup; public contentForm: FormGroup;
public contentData: any = null;
public contentId: string | null = null;
public isNewMode = true; public isNewMode = true;
@ -63,6 +62,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
} }
public ngOnDestroy() { public ngOnDestroy() {
this.contentVersionSelectedSubscription.unsubscribe();
this.contentDeletedSubscription.unsubscribe(); this.contentDeletedSubscription.unsubscribe();
} }
@ -71,10 +71,16 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
this.languages = routeData['appLanguages']; this.languages = routeData['appLanguages'];
this.contentVersionSelectedSubscription =
this.messageBus.of(ContentVersionSelected)
.subscribe(message => {
this.loadVersion(message.version);
});
this.contentDeletedSubscription = this.contentDeletedSubscription =
this.messageBus.of(ContentDeleted) this.messageBus.of(ContentRemoved)
.subscribe(message => { .subscribe(message => {
if (message.content.id === this.contentId) { if (this.content && message.content.id === this.content.id) {
this.router.navigate(['../'], { relativeTo: this.route }); this.router.navigate(['../'], { relativeTo: this.route });
} }
}); });
@ -115,7 +121,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
if (this.isNewMode) { if (this.isNewMode) {
this.appNameOnce() 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 => { .subscribe(dto => {
this.content = dto; this.content = dto;
@ -128,9 +134,9 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
}); });
} else { } else {
this.appNameOnce() 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 => { .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.emitContentUpdated(this.content);
this.notifyInfo('Content saved successfully.'); 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() { private back() {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true }); this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
} }
@ -195,28 +217,24 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
private populateContentForm() { private populateContentForm() {
this.contentForm.markAsPristine(); this.contentForm.markAsPristine();
if (!this.content) { this.isNewMode = !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;
for (const field of this.schema.fields) { if (!this.isNewMode) {
const fieldValue = this.content.data[field.name] || {}; for (const field of this.schema.fields) {
const fieldForm = <FormGroup>this.contentForm.get(field.name); const fieldValue = this.content.data[field.name] || {};
const fieldForm = <FormGroup>this.contentForm.get(field.name);
if (field.partitioning === 'language') { if (field.partitioning === 'language') {
for (let language of this.languages) { for (let language of this.languages) {
fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]); 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> </form>
<div class="dropdown-menu" *sqxModalView="searchModal" closeAlways="true" [sqxModalTarget]="searchInput" position="right"> <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> </div>
<span *ngIf="!isReadOnly && languages.length > 1"> <span *ngIf="!isReadOnly && languages.length > 1">
@ -33,12 +39,16 @@
</button> </button>
</div> </div>
<h3 class="panel-title" *ngIf="!isReadOnly"> <h3 class="panel-title" *ngIf="!isReadOnly && !isArchive">
Contents Contents
</h3> </h3>
<h3 class="panel-title" *ngIf="isArchive">
Archive
</h3>
<h3 class="panel-title" *ngIf="isReadOnly"> <h3 class="panel-title" *ngIf="isReadOnly">
References Refs
</h3> </h3>
</div> </div>
@ -82,6 +92,8 @@
[schema]="schema" [schema]="schema"
(unpublishing)="unpublishContent(content)" (unpublishing)="unpublishContent(content)"
(publishing)="publishContent(content)" (publishing)="publishContent(content)"
(archiving)="archiveContent(content)"
(restoring)="restoreContent(content)"
(deleting)="deleteContent(content)"></tr> (deleting)="deleteContent(content)"></tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</ng-template> </ng-template>

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

@ -12,7 +12,7 @@ import { Subscription } from 'rxjs';
import { import {
ContentCreated, ContentCreated,
ContentDeleted, ContentRemoved,
ContentUpdated ContentUpdated
} from './../messages'; } from './../messages';
@ -57,6 +57,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public languageParameter: string; public languageParameter: string;
public isReadOnly = false; public isReadOnly = false;
public isArchive = false;
public columnWidth: number; public columnWidth: number;
@ -112,18 +113,11 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
return { content, schemaId: this.schema.id }; return { content, schemaId: this.schema.id };
} }
public search() {
this.contentsQuery = this.contentsFilter.value;
this.contentsPager = new Pager(0);
this.load();
}
public publishContent(content: ContentDto) { public publishContent(content: ContentDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version)) .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .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 => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -133,7 +127,31 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version)) .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .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 => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -143,10 +161,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version)) .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .subscribe(() => {
this.contentItems = this.contentItems.removeAll(x => x.id === content.id); this.removeContent(content);
this.contentsPager = this.contentsPager.decrementCount();
this.emitContentDeleted(content);
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -154,7 +169,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public load(showInfo = false) { public load(showInfo = false) {
this.appNameOnce() 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 => { .subscribe(dtos => {
this.contentItems = ImmutableArray.of(dtos.items); this.contentItems = ImmutableArray.of(dtos.items);
this.contentsPager = this.contentsPager.setCount(dtos.total); this.contentsPager = this.contentsPager.setCount(dtos.total);
@ -167,8 +182,22 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
}); });
} }
public selectLanguage(language: AppLanguageDto) { public updateArchive(isArchive: boolean) {
this.languageSelected = language; 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() { public goNext() {
@ -183,8 +212,12 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.load(); this.load();
} }
private emitContentDeleted(content: ContentDto) { public selectLanguage(language: AppLanguageDto) {
this.messageBus.emit(new ContentDeleted(content)); this.languageSelected = language;
}
private emitContentRemoved(content: ContentDto) {
this.messageBus.emit(new ContentRemoved(content));
} }
private resetContents() { private resetContents() {
@ -196,6 +229,13 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.loadFields(); 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() { private loadFields() {
this.contentFields = this.schema.fields.filter(x => x.properties.isListField); 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]]; this.contentFields = [this.schema.fields[0]];
} }
if (this.contentFields.length === 0) {
this.contentFields = [<any>{}];
}
if (this.contentFields.length > 0) { if (this.contentFields.length > 0) {
this.columnWidth = 100 / this.contentFields.length; this.columnWidth = 100 / this.contentFields.length;
} else { } 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-horizontal">
<div class="form-group row"> <div [formGroup]="searchForm">
<label class="col col-2 col-form-label" for="odataSearch">Text</label> <div class="form-group row">
<label class="col col-2 col-form-label" for="odataSearch">Text</label>
<div class="col col-10"> <div class="col col-10">
<input type="input" class="form-control" id="search" (blur)="updateQuery()" formControlName="odataSearch" placeholder="Fulltext search" /> <input type="input" class="form-control" id="search" (blur)="updateQuery()" formControlName="odataSearch" placeholder="Fulltext search" />
</div>
</div> </div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col col-2 col-form-label" for="filter">Filter</label> <label class="col col-2 col-form-label" for="filter">Filter</label>
<div class="col col-10"> <div class="col col-10">
<input type="input" class="form-control" id="filter" (blur)="updateQuery()" formControlName="odataFilter" placeholder="data/[MY_FIELD]/iv eq 100" /> <input type="input" class="form-control" id="filter" (blur)="updateQuery()" formControlName="odataFilter" placeholder="data/[MY_FIELD]/iv eq 100" />
</div>
</div> </div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col col-2 col-form-label" for="orderBy">Order</label> <label class="col col-2 col-form-label" for="orderBy">Order</label>
<div class="col col-10"> <div class="col col-10">
<input type="input" class="form-control" id="orderBy" (blur)="updateQuery()" formControlName="odataOrderBy" placeholder="data/[MY_FIELD]/iv desc" /> <input type="input" class="form-control" id="orderBy" (blur)="updateQuery()" formControlName="odataOrderBy" placeholder="data/[MY_FIELD]/iv desc" />
</div>
</div> </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"> <div class="link">
Read more about filtering in the <a href="https://docs.squidex.io/04-guides/02-api.html" target="_blank">Documentation</a>. 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; max-width: 25rem;
} }
.form-check {
margin-top: 1rem;
font-size: 1rem;
font-weight: normal;
text-align: right;
}
.link { .link {
margin-top: 1.5rem; margin-top: 1.5rem;
font-size: .8rem; 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 = ''; public query = '';
@Output() @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 = public searchForm =
this.formBuilder.group({ 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( constructor(
public readonly content: ContentDto 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, AssetUpdated,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
MessageBus MessageBus,
Types
} from 'shared'; } from 'shared';
const NOOP = () => { /* NOOP */ };
export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetsEditorComponent), multi: true 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 { export class AssetsEditorComponent extends AppComponentBase implements ControlValueAccessor, OnDestroy, OnInit {
private assetUpdatedSubscription: Subscription; private assetUpdatedSubscription: Subscription;
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
public newAssets = ImmutableArray.empty<File>(); public newAssets = ImmutableArray.empty<File>();
public oldAssets = ImmutableArray.empty<AssetDto>(); public oldAssets = ImmutableArray.empty<AssetDto>();
@ -65,10 +64,10 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa
this.assetUpdatedSubscription.unsubscribe(); this.assetUpdatedSubscription.unsubscribe();
} }
public writeValue(value: any) { public writeValue(value: string[]) {
this.oldAssets = ImmutableArray.empty<AssetDto>(); this.oldAssets = ImmutableArray.empty<AssetDto>();
if (value && value.length > 0) { if (Types.isArrayOfString(value) && value.length > 0) {
const assetIds: string[] = value; const assetIds: string[] = value;
this.appNameOnce() this.appNameOnce()
@ -84,11 +83,11 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public addFiles(files: FileList) { public addFiles(files: FileList) {
@ -143,7 +142,7 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa
ids = null; ids = null;
} }
this.touchedCallback(); this.callTouched();
this.changeCallback(ids); this.callChange(ids);
} }
} }

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

@ -17,13 +17,19 @@
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]> <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 Publish
</a> </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 Unpublish
</a> </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()" (sqxConfirmClick)="deleting.emit()"
confirmTitle="Delete content" confirmTitle="Delete content"
confirmText="Do you really want to delete the 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() @Output()
public unpublishing = new EventEmitter<ContentDto>(); public unpublishing = new EventEmitter<ContentDto>();
@Output()
public archiving = new EventEmitter<ContentDto>();
@Output()
public restoring = new EventEmitter<ContentDto>();
@Output() @Output()
public deleting = new EventEmitter<ContentDto>(); public deleting = new EventEmitter<ContentDto>();

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

@ -19,11 +19,10 @@ import {
FieldDto, FieldDto,
ImmutableArray, ImmutableArray,
SchemaDetailsDto, SchemaDetailsDto,
SchemasService SchemasService,
Types
} from 'shared'; } from 'shared';
const NOOP = () => { /* NOOP */ };
export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true 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] providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR]
}) })
export class ReferencesEditorComponent extends AppComponentBase implements ControlValueAccessor, OnInit { export class ReferencesEditorComponent extends AppComponentBase implements ControlValueAccessor, OnInit {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
@Input() @Input()
public schemaId: string; 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>(); this.contentItems = ImmutableArray.empty<ContentDto>();
if (value && value.length > 0) { if (Types.isArrayOfString(value) && value.length > 0) {
const contentIds: string[] = value; const contentIds: string[] = value;
this.appNameOnce() this.appNameOnce()
@ -92,11 +91,11 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public canDrop() { public canDrop() {
@ -138,8 +137,8 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
ids = null; ids = null;
} }
this.touchedCallback(); this.callTouched();
this.changeCallback(ids); this.callChange(ids);
} }
private loadFields() { private loadFields() {
@ -149,6 +148,10 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
this.contentFields = [this.schema.fields[0]]; this.contentFields = [this.schema.fields[0]];
} }
if (this.contentFields.length === 0) {
this.contentFields = [<any>{}];
}
if (this.contentFields.length > 0) { if (this.contentFields.length > 0) {
this.columnWidth = 100 / this.contentFields.length; this.columnWidth = 100 / this.contentFields.length;
} else { } 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() { 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() this.appNameOnce()
.switchMap(app => this.schemasService.publishSchema(app, this.schema.name, this.schema.version)).retry(2) .switchMap(app => this.schemasService.publishSchema(app, this.schema.name, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.publish(this.authService.user.token)); this.updateSchema(this.schema.publish(this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -120,7 +120,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.unpublishSchema(app, this.schema.name, this.schema.version)).retry(2) .switchMap(app => this.schemasService.unpublishSchema(app, this.schema.name, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.unpublish(this.authService.user.token)); this.updateSchema(this.schema.unpublish(this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -130,7 +130,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.enableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.enableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.updateField(field.enable(), this.authService.user.token)); this.updateSchema(this.schema.updateField(field.enable(), this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -140,7 +140,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.disableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.disableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.updateField(field.disable(), this.authService.user.token)); this.updateSchema(this.schema.updateField(field.disable(), this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -150,7 +150,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.lockField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.lockField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.updateField(field.lock(), this.authService.user.token)); this.updateSchema(this.schema.updateField(field.lock(), this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -160,7 +160,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.hideField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.hideField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.updateField(field.hide(), this.authService.user.token)); this.updateSchema(this.schema.updateField(field.hide(), this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -170,7 +170,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.deleteField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.deleteField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.removeField(field, this.authService.user.token)); this.updateSchema(this.schema.removeField(field, this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -180,7 +180,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.putFieldOrdering(app, this.schema.name, fields.map(t => t.fieldId), this.schema.version)).retry(2) .switchMap(app => this.schemasService.putFieldOrdering(app, this.schema.name, fields.map(t => t.fieldId), this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.replaceFields(fields, this.authService.user.token)); this.updateSchema(this.schema.replaceFields(fields, this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -192,7 +192,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.putField(app, this.schema.name, field.fieldId, requestDto, this.schema.version)).retry(2) .switchMap(app => this.schemasService.putField(app, this.schema.name, field.fieldId, requestDto, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateSchema(this.schema.updateField(field, this.authService.user.token)); this.updateSchema(this.schema.updateField(field, this.authService.user!.token));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -223,7 +223,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.postField(app, this.schema.name, requestDto, this.schema.version)) .switchMap(app => this.schemasService.postField(app, this.schema.name, requestDto, this.schema.version))
.subscribe(dto => { .subscribe(dto => {
this.updateSchema(this.schema.addField(dto, this.authService.user.token)); this.updateSchema(this.schema.addField(dto, this.authService.user!.token));
this.resetFieldForm(); this.resetFieldForm();
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
@ -237,13 +237,13 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
} }
public onSchemaSaved(properties: SchemaPropertiesDto) { 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(); this.editSchemaDialog.hide();
} }
public onSchemaScriptsSaved(scripts: UpdateSchemaScriptsDto) { 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(); 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', 'Create',
'Update', 'Update',
'Delete', 'Delete',
'Publish', 'Change'
'Unpublish'
]; ];
public editFormSubmitted = false; public editFormSubmitted = false;
@ -52,8 +51,7 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit
scriptCreate: '', scriptCreate: '',
scriptUpdate: '', scriptUpdate: '',
scriptDelete: '', scriptDelete: '',
scriptPublish: '', scriptChange: ''
scriptUnpublish: ''
}); });
constructor(dialogs: DialogService, constructor(dialogs: DialogService,
@ -84,8 +82,7 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit
this.editForm.controls['scriptCreate'].value, this.editForm.controls['scriptCreate'].value,
this.editForm.controls['scriptUpdate'].value, this.editForm.controls['scriptUpdate'].value,
this.editForm.controls['scriptDelete'].value, this.editForm.controls['scriptDelete'].value,
this.editForm.controls['scriptPublish'].value, this.editForm.controls['scriptChange'].value);
this.editForm.controls['scriptUnpublish'].value);
this.schemas.putSchemaScripts(this.appName, this.schema.name, requestDto, this.schema.version) this.schemas.putSchemaScripts(this.appName, this.schema.name, requestDto, this.schema.version)
.subscribe(dto => { .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 { import {
ApiUrlConfig, ApiUrlConfig,
AuthService, AuthService,
DateTime,
fadeAnimation, fadeAnimation,
SchemaDetailsDto, SchemaDetailsDto,
SchemasService, SchemasService,
@ -89,7 +90,7 @@ export class SchemaFormComponent {
const me = this.authService.user!.token; 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 => { .subscribe(dto => {
this.emitCreated(dto); this.emitCreated(dto);
this.resetCreateForm(); 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 eventsItems = ImmutableArray.empty<WebhookEventDto>();
public eventsPager = new Pager(0); public eventsPager = new Pager(0);
public selectedEventId: string; public selectedEventId: string | null = null;
constructor(dialogs: DialogService, appsStore: AppsStoreService, constructor(dialogs: DialogService, appsStore: AppsStoreService,
private readonly webhooksService: WebhooksService private readonly webhooksService: WebhooksService

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

@ -91,7 +91,7 @@ export class WebhookComponent implements OnInit {
} else { } else {
return null; return null;
} }
}).filter(w => !!w)).sortByStringAsc(x => x.schema.name); }).filter(w => w !== null).map(w => w!)).sortByStringAsc(x => x.schema.name);
this.schemasToAdd = this.schemasToAdd =
ImmutableArray.of( 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() this.appNameOnce()
.switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version)) .switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version))
.subscribe(dto => { .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.'); this.notifyInfo('Webhook saved.');
}, error => { }, error => {

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

@ -17,7 +17,6 @@ const KEY_ENTER = 13;
const KEY_ESCAPE = 27; const KEY_ESCAPE = 27;
const KEY_UP = 38; const KEY_UP = 38;
const KEY_DOWN = 40; const KEY_DOWN = 40;
const NOOP = () => { /* NOOP */ };
export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true 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 { export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit {
private subscription: Subscription; private subscription: Subscription;
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
@Input() @Input()
public source: AutocompleteSource; public source: AutocompleteSource;
@ -57,7 +56,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public writeValue(value: any) { public writeValue(value: any) {
if (!value) { if (!value) {
this.resetValue(); this.resetForm();
} else { } else {
const item = this.items.find(i => i === value); const item = this.items.find(i => i === value);
@ -79,11 +78,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -119,7 +118,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
this.down(); this.down();
return false; return false;
case KEY_ESCAPE: case KEY_ESCAPE:
this.resetValue(); this.resetForm();
this.reset(); this.reset();
return false; return false;
case KEY_ENTER: case KEY_ENTER:
@ -135,7 +134,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public blur() { public blur() {
this.reset(); this.reset();
this.touchedCallback(); this.callTouched();
} }
public selectItem(selection: any | null = null) { public selectItem(selection: any | null = null) {
@ -154,7 +153,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
} else { } else {
this.queryInput.setValue(selection.toString(), { emitEvent: false }); this.queryInput.setValue(selection.toString(), { emitEvent: false });
} }
this.changeCallback(selection); this.callChange(selection);
} finally { } finally {
this.reset(); this.reset();
} }
@ -181,7 +180,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
this.selectIndex(this.selectedIndex + 1); this.selectIndex(this.selectedIndex + 1);
} }
private resetValue() { private resetForm() {
this.queryInput.setValue(''); 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'; import { DialogService } from './../services/dialog.service';
class DelayEventEmitter<T> extends EventEmitter<T> { class DelayEventEmitter<T> extends EventEmitter<T> {
private delayedNexts: any[] = []; private delayedNexts: any[] | null = [];
public delayEmit() { public delayEmit() {
for (let callback of this.delayedNexts) { if (this.delayedNexts) {
callback(); 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 { 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); 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 { Directive, HostListener, Input } from '@angular/core';
import { Types } from './../utils/types';
import { DialogService, Notification } from './../services/dialog.service'; import { DialogService, Notification } from './../services/dialog.service';
@Directive({ @Directive({
@ -48,7 +49,7 @@ export class CopyDirective {
console.log('Copy failed'); console.log('Copy failed');
} }
if (currentFocus && typeof currentFocus.focus === 'function') { if (currentFocus && Types.isFunction(currentFocus.focus)) {
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 { Subscription } from 'rxjs';
import * as moment from 'moment'; 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 = { export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateTimeEditorComponent), multi: true 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 timeValue: any | null = null;
private dateValue: any | null = null; private dateValue: any | null = null;
private suppressEvents = false; private suppressEvents = false;
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
@Input() @Input()
public mode: string; public mode: string;
@ -86,8 +86,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
}); });
} }
public writeValue(value: any) { public writeValue(value: string) {
if (!value || value.length === 0) { if (!Types.isString(value) || value.length === 0) {
this.timeValue = null; this.timeValue = null;
this.dateValue = null; this.dateValue = null;
} else { } else {
@ -116,11 +116,11 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -140,7 +140,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
} }
public touched() { public touched() {
this.touchedCallback(); this.callTouched();
} }
public writeNow() { public writeNow() {
@ -159,14 +159,14 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
this.dateValue = null; this.dateValue = null;
this.changeCallback(null); this.callChange(null);
this.touchedCallback(); this.callTouched();
return false; return false;
} }
private updateValue() { private updateValue() {
let result: string | null; let result: string | null = null;
if ((this.dateValue && !this.dateValue.isValid()) || (this.timeValue && !this.timeValue.isValid())) { if ((this.dateValue && !this.dateValue.isValid()) || (this.timeValue && !this.timeValue.isValid())) {
result = 'Invalid DateTime'; result = 'Invalid DateTime';
@ -184,7 +184,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
} }
} }
this.changeCallback(result); this.callChange(result);
} }
private updateControls() { 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; private notificationsSubscription: Subscription;
public dialogView = new ModalView(false, true); public dialogView = new ModalView(false, true);
public dialogRequest: DialogRequest; public dialogRequest: DialogRequest | null = null;
public notifications: Notification[] = []; 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_ESCAPE = 27;
const KEY_UP = 38; const KEY_UP = 38;
const KEY_DOWN = 40; const KEY_DOWN = 40;
const NOOP = () => { /* NOOP */ };
import { ModalView } from './../utils/modal-view'; import { ModalView } from './../utils/modal-view';
@ -27,8 +26,8 @@ export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR] providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR]
}) })
export class DropdownComponent implements AfterContentInit, ControlValueAccessor { export class DropdownComponent implements AfterContentInit, ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
@Input() @Input()
public items: any[] = []; public items: any[] = [];
@ -69,11 +68,11 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public onKeyDown(event: KeyboardEvent) { public onKeyDown(event: KeyboardEvent) {
@ -95,7 +94,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
public open() { public open() {
this.dropdown.show(); this.dropdown.show();
this.touchedCallback(); this.callTouched();
} }
public selectIndexAndClose(selectedIndex: number) { public selectIndexAndClose(selectedIndex: number) {
@ -132,7 +131,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
this.selectedIndex = selectedIndex; this.selectedIndex = selectedIndex;
this.selectedItem = value; 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 { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from './../utils/types';
import { ResourceLoaderService } from './../services/resource-loader.service'; import { ResourceLoaderService } from './../services/resource-loader.service';
import { ValidatorsEx } from './validators'; import { ValidatorsEx } from './validators';
const NOOP = () => { /* NOOP */ };
declare var L: any; declare var L: any;
export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => GeolocationEditorComponent), multi: true provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => GeolocationEditorComponent), multi: true
}; };
interface Geolocation {
latitude: number;
longitude: number;
}
@Component({ @Component({
selector: 'sqx-geolocation-editor', selector: 'sqx-geolocation-editor',
styleUrls: ['./geolocation-editor.component.scss'], 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] providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR]
}) })
export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
private marker: any; private marker: any;
private map: any; private map: any;
private value: any; private value: Geolocation | null = null;
public get hasValue() { public get hasValue() {
return !!this.value; return !!this.value;
@ -59,8 +64,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
) { ) {
} }
public writeValue(value: any) { public writeValue(value: Geolocation) {
this.value = value; if (Types.isObject(value) && Types.isNumber(value.latitude) && Types.isNumber(value.longitude)) {
this.value = value;
} else {
this.value = null;
}
if (this.marker) { if (this.marker) {
this.updateMarker(true, false); this.updateMarker(true, false);
@ -102,11 +111,11 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public updateValueByInput() { public updateValueByInput() {
@ -201,8 +210,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
if (fireEvent) { if (fireEvent) {
this.changeCallback(this.value); this.callChange(this.value);
this.touchedCallback(); 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>) => { return httpRequest.do((response: HttpResponse<any>) => {
if (version && response.status.toString().indexOf('2') === 0 && response.headers) { if (version && response.status.toString().indexOf('2') === 0 && response.headers) {
const etag = response.headers.get('etag'); 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 { Directive, forwardRef, ElementRef, HostListener, Renderer } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 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 = { export const SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IndeterminateValueDirective), multi: true 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] providers: [SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR]
}) })
export class IndeterminateValueDirective implements ControlValueAccessor { export class IndeterminateValueDirective implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
constructor( constructor(
private readonly renderer: Renderer, private readonly renderer: Renderer,
@ -30,16 +30,16 @@ export class IndeterminateValueDirective implements ControlValueAccessor {
@HostListener('change', ['$event.target.value']) @HostListener('change', ['$event.target.value'])
public onChange(value: any) { public onChange(value: any) {
this.changeCallback(value); this.callChange(value);
} }
@HostListener('blur') @HostListener('blur')
public onTouched() { public onTouched() {
this.touchedCallback(); this.callTouched();
} }
public writeValue(value: any) { public writeValue(value: boolean | number | undefined) {
if (value === undefined || value === null) { if (!Types.isBoolean(value)) {
this.renderer.setElementProperty(this.element.nativeElement, 'indeterminate', true); this.renderer.setElementProperty(this.element.nativeElement, 'indeterminate', true);
} else { } else {
this.renderer.setElementProperty(this.element.nativeElement, 'checked', value); this.renderer.setElementProperty(this.element.nativeElement, 'checked', value);
@ -51,10 +51,10 @@ export class IndeterminateValueDirective implements ControlValueAccessor {
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { 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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { Types } from './../utils/types';
import { ResourceLoaderService } from './../services/resource-loader.service'; import { ResourceLoaderService } from './../services/resource-loader.service';
declare var ace: any; declare var ace: any;
const NOOP = () => { /* NOOP */ };
export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JscriptEditorComponent), multi: true 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] providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR]
}) })
export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit { export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
private valueChanged = new Subject(); private valueChanged = new Subject();
private aceEditor: any; private aceEditor: any;
private oldValue: string; private value: string;
private isDisabled = false; private isDisabled = false;
@ViewChild('editor') @ViewChild('editor')
@ -41,11 +41,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn
) { ) {
} }
public writeValue(value: any) { public writeValue(value: string) {
this.oldValue = value; this.value = Types.isString(value) ? value : '';
if (this.aceEditor) { if (this.aceEditor) {
this.setValue(value); this.setValue(this.value);
} }
} }
@ -58,11 +58,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -78,11 +78,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn
this.aceEditor.setReadOnly(this.isDisabled); this.aceEditor.setReadOnly(this.isDisabled);
this.aceEditor.setFontSize(14); this.aceEditor.setFontSize(14);
this.setValue(this.oldValue); this.setValue(this.value);
this.aceEditor.on('blur', () => { this.aceEditor.on('blur', () => {
this.changeValue(); this.changeValue();
this.touchedCallback(); this.callTouched();
}); });
this.aceEditor.on('change', () => { this.aceEditor.on('change', () => {
@ -94,11 +94,11 @@ export class JscriptEditorComponent implements ControlValueAccessor, AfterViewIn
private changeValue() { private changeValue() {
const newValue = this.aceEditor.getValue(); const newValue = this.aceEditor.getValue();
if (this.oldValue !== newValue) { if (this.value !== newValue) {
this.changeCallback(newValue); this.callChange(newValue);
} }
this.oldValue = newValue; this.value = newValue;
} }
private setValue(value: string) { 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; declare var ace: any;
const NOOP = () => { /* NOOP */ };
export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JsonEditorComponent), multi: true 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] providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR]
}) })
export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit { export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
private valueChanged = new Subject(); private valueChanged = new Subject();
private aceEditor: any; private aceEditor: any;
private oldValue: any; private value: any;
private oldValueString: string; private valueString: string;
private isDisabled = false; private isDisabled = false;
@ViewChild('editor') @ViewChild('editor')
@ -43,8 +41,13 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit
} }
public writeValue(value: any) { public writeValue(value: any) {
this.oldValue = value; this.value = value;
this.oldValueString = JSON.stringify(value);
try {
this.valueString = JSON.stringify(value);
} catch (e) {
this.valueString = '';
}
if (this.aceEditor) { if (this.aceEditor) {
this.setValue(value); this.setValue(value);
@ -60,11 +63,11 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -80,11 +83,11 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit
this.aceEditor.setReadOnly(this.isDisabled); this.aceEditor.setReadOnly(this.isDisabled);
this.aceEditor.setFontSize(14); this.aceEditor.setFontSize(14);
this.setValue(this.oldValue); this.setValue(this.value);
this.aceEditor.on('blur', () => { this.aceEditor.on('blur', () => {
this.changeValue(); this.changeValue();
this.touchedCallback(); this.callTouched();
}); });
this.aceEditor.on('change', () => { this.aceEditor.on('change', () => {
@ -108,12 +111,12 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit
const newValueString = JSON.stringify(newValue); const newValueString = JSON.stringify(newValue);
if (this.oldValueString !== newValueString) { if (this.valueString !== newValueString) {
this.changeCallback(newValue); this.callChange(newValue);
} }
this.oldValue = newValue; this.value = newValue;
this.oldValueString = newValueString; this.valueString = newValueString;
} }
private setValue(value: any) { 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 { Directive, forwardRef, ElementRef, HostListener, Renderer } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const NOOP = () => { /* NOOP */ }; import { Types } from './../utils/types';
export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = { export const SQX_LOWERCASE_INPUT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LowerCaseInputDirective), multi: true 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] providers: [SQX_LOWERCASE_INPUT_VALUE_ACCESSOR]
}) })
export class LowerCaseInputDirective implements ControlValueAccessor { export class LowerCaseInputDirective implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
constructor( constructor(
private readonly element: ElementRef, private readonly element: ElementRef,
@ -33,16 +33,16 @@ export class LowerCaseInputDirective implements ControlValueAccessor {
const normalizedValue = (value == null ? '' : value.toString()).toLowerCase(); const normalizedValue = (value == null ? '' : value.toString()).toLowerCase();
this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue);
this.changeCallback(normalizedValue); this.callChange(normalizedValue);
} }
@HostListener('blur') @HostListener('blur')
public onTouched() { public onTouched() {
this.touchedCallback(); this.callTouched();
} }
public writeValue(value: any) { public writeValue(value: string) {
const normalizedValue = (value == null ? '' : value.toString()).toLowerCase(); const normalizedValue = Types.isString(value) ? value.toLowerCase() : '';
this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue); this.renderer.setElementProperty(this.element.nativeElement, 'value', normalizedValue);
} }
@ -52,10 +52,10 @@ export class LowerCaseInputDirective implements ControlValueAccessor {
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { 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 { AfterViewInit, Component, forwardRef, ElementRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from './../utils/types';
import { ResourceLoaderService } from './../services/resource-loader.service'; import { ResourceLoaderService } from './../services/resource-loader.service';
declare var SimpleMDE: any; declare var SimpleMDE: any;
const NOOP = () => { /* NOOP */ };
export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true 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] providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR]
}) })
export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit { export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
private simplemde: any; private simplemde: any;
private value: any; private value: string;
private isDisabled = false; private isDisabled = false;
@ViewChild('editor') @ViewChild('editor')
@ -48,11 +48,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css'); this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css');
} }
public writeValue(value: any) { public writeValue(value: string) {
this.value = value; this.value = Types.isString(value) ? value : '';
if (this.simplemde) { 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) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -84,12 +84,12 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
if (this.value !== value) { if (this.value !== value) {
this.value = value; this.value = value;
this.changeCallback(value); this.callChange(value);
} }
}); });
this.simplemde.codemirror.on('blur', () => { this.simplemde.codemirror.on('blur', () => {
this.touchedCallback(); this.callTouched();
}); });
this.simplemde.codemirror.on('refresh', () => { 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]' selector: '[sqxModalView]'
}) })
export class ModalViewDirective implements OnChanges, OnDestroy { export class ModalViewDirective implements OnChanges, OnDestroy {
private subscription: Subscription | null; private subscription: Subscription | null = null;
private clickHandler: Function | null; private clickHandler: Function | null = null;
private renderedView: EmbeddedViewRef<any> | null = null; private renderedView: EmbeddedViewRef<any> | null = null;
@Input('sqxModalView') @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 }) { 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) { if (!this.isInit) {
return; 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 { AfterViewInit, Component, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from './../utils/types';
import { ResourceLoaderService } from './../services/resource-loader.service'; import { ResourceLoaderService } from './../services/resource-loader.service';
declare var tinymce: any; declare var tinymce: any;
const NOOP = () => { /* NOOP */ };
export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true 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] providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR]
}) })
export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
private tinyEditor: any; private tinyEditor: any;
private value: any; private value: string;
private isDisabled = false; private isDisabled = false;
@ViewChild('editor') @ViewChild('editor')
@ -39,11 +39,11 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
) { ) {
} }
public writeValue(value: any) { public writeValue(value: string) {
this.value = value; this.value = Types.isString(value) ? value : '';
if (this.tinyEditor) { 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) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -78,12 +78,12 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
if (this.value !== value) { if (this.value !== value) {
this.value = value; this.value = value;
self.changeCallback(value); self.callChange(value);
} }
}); });
self.tinyEditor.on('blur', () => { self.tinyEditor.on('blur', () => {
self.touchedCallback(); self.callTouched();
}); });
setTimeout(() => { setTimeout(() => {

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

@ -8,7 +8,7 @@
import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params } from '@angular/router';
export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data { 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 } = { }; const result: { [key: string]: any } = { };
@ -25,7 +25,7 @@ export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data {
return result; return result;
} }
export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Params { 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 } = { }; 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 { Component, ElementRef, forwardRef, Input, Renderer, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const NOOP = () => { /* NOOP */ }; import { Types } from './../utils/types';
export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = { export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SliderComponent), multi: true 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] providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR]
}) })
export class SliderComponent implements ControlValueAccessor { export class SliderComponent implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
private mouseMoveSubscription: Function | null; private mouseMoveSubscription: Function | null = null;
private mouseUpSubscription: Function | null; private mouseUpSubscription: Function | null = null;
private centerStartOffset = 0; private centerStartOffset = 0;
private startValue: number; private startValue: number;
private lastValue: number; private lastValue: number;
@ -50,8 +50,8 @@ export class SliderComponent implements ControlValueAccessor {
constructor(private readonly renderer: Renderer) { } constructor(private readonly renderer: Renderer) { }
public writeValue(value: any) { public writeValue(value: number) {
this.lastValue = this.value = value; this.lastValue = this.value = Types.isNumber(value) ? value : 0;
this.updateThumbPosition(); this.updateThumbPosition();
} }
@ -61,11 +61,11 @@ export class SliderComponent implements ControlValueAccessor {
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public onBarMouseClick(event: MouseEvent): boolean { public onBarMouseClick(event: MouseEvent): boolean {
@ -157,14 +157,14 @@ export class SliderComponent implements ControlValueAccessor {
} }
private updateTouched() { private updateTouched() {
this.touchedCallback(); this.callTouched();
} }
private updateValue() { private updateValue() {
if (this.lastValue !== this.value) { if (this.lastValue !== this.value) {
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 { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const NOOP = () => { /* NOOP */ }; import { Types } from './../utils/types';
export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarsComponent), multi: true 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] providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR]
}) })
export class StarsComponent implements ControlValueAccessor { export class StarsComponent implements ControlValueAccessor {
private changeCallback: (value: any) => void = NOOP; private callChange = (v: any) => { /* NOOP */ };
private touchedCallback: () => void = NOOP; private callTouched = () => { /* NOOP */ };
private maximumStarsValue = 5; private maximumStarsValue = 5;
@Input() @Input()
public set maximumStars(value: any) { public set maximumStars(value: number) {
value = value || 5; const maxStars: number = Types.isNumber(value) ? value : 5;
if (!(typeof value === 'number')) { if (this.maximumStarsValue !== maxStars) {
value = 5;
}
if (this.maximumStarsValue !== value) {
this.maximumStarsValue = value; this.maximumStarsValue = value;
this.starsArray = []; this.starsArray = [];
@ -55,8 +51,13 @@ export class StarsComponent implements ControlValueAccessor {
public value: number | null = 1; public value: number | null = 1;
public writeValue(value: any) { public writeValue(value: number | null | undefined) {
this.value = this.stars = value; if (Types.isNumber(value)) {
this.value = this.stars = value || 0;
} else {
this.value = null;
this.stars = 0;
}
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
@ -64,11 +65,11 @@ export class StarsComponent implements ControlValueAccessor {
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
this.changeCallback = fn; this.callChange = fn;
} }
public registerOnTouched(fn: any) { public registerOnTouched(fn: any) {
this.touchedCallback = fn; this.callTouched = fn;
} }
public setPreview(value: number) { public setPreview(value: number) {
@ -96,8 +97,8 @@ export class StarsComponent implements ControlValueAccessor {
this.value = null; this.value = null;
this.stars = 0; this.stars = 0;
this.changeCallback(this.value); this.callChange(this.value);
this.touchedCallback(); this.callTouched();
} }
return false; return false;
@ -111,8 +112,8 @@ export class StarsComponent implements ControlValueAccessor {
if (this.value !== value) { if (this.value !== value) {
this.value = this.stars = value; this.value = this.stars = value;
this.changeCallback(this.value); this.callChange(this.value);
this.touchedCallback(); this.callTouched();
} }
return false; return false;

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

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

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

@ -12,6 +12,7 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { DateTime } from './../utils/date-time'; import { DateTime } from './../utils/date-time';
import { Types } from './../utils/types';
export module ValidatorsEx { export module ValidatorsEx {
export function pattern(regex: string | RegExp, message?: string): ValidatorFn { export function pattern(regex: string | RegExp, message?: string): ValidatorFn {
@ -30,7 +31,7 @@ export module ValidatorsEx {
regeExp = regex; regeExp = regex;
} }
return (control: AbstractControl): { [key: string]: any } => { return (control: AbstractControl) => {
const n: string = control.value; const n: string = control.value;
if (n == null || n.length === 0) { if (n == null || n.length === 0) {
@ -49,16 +50,16 @@ export module ValidatorsEx {
}; };
} }
export function match(otherControlName: string, message: string) { export function match(otherControlName: string, message: string): ValidatorFn {
let otherControl: AbstractControl = null; let otherControl: AbstractControl | null = null;
return (control: AbstractControl): { [key: string]: any } => { return (control: AbstractControl) => {
if (!control.parent) { if (!control.parent) {
return null; return null;
} }
if (otherControl === null) { if (otherControl === null) {
otherControl = control.parent.get(otherControlName) || undefined; otherControl = control.parent.get(otherControlName);
if (!otherControl) { if (!otherControl) {
throw new Error('matchValidator(): other control is not found in parent group'); throw new Error('matchValidator(): other control is not found in parent group');
@ -77,8 +78,8 @@ export module ValidatorsEx {
}; };
} }
export function validDateTime() { export function validDateTime(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => { return (control: AbstractControl) => {
const v: string = control.value; const v: string = control.value;
if (v) { 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) { if (!minValue || !maxValue) {
return Validators.nullValidator; return Validators.nullValidator;
} }
return (control: AbstractControl): { [key: string]: any } => { return (control: AbstractControl) => {
const n: number = control.value; const value: number = control.value;
if (typeof n !== 'number') { if (!Types.isNumber(value)) {
return { validnumber: false }; return { validnumber: false };
} else if (minValue && n < minValue) { } else if (minValue && value < minValue) {
return { minvalue: { minValue, actualValue: n } }; return { minvalue: { minValue, actualValue: value } };
} else if (maxValue && n > maxValue) { } else if (maxValue && value > maxValue) {
return { maxvalue: { maxValue, actualValue: n } }; return { maxvalue: { maxValue, actualValue: value } };
} }
return null; return null;
}; };
} }
export function validValues<T>(values: T[]) { export function validValues<T>(values: T[]): ValidatorFn {
if (!values) { if (!values) {
return Validators.nullValidator; return Validators.nullValidator;
} }
return (control: AbstractControl): { [key: string]: any } => { return (control: AbstractControl) => {
const n: T = control.value; const n: T = control.value;
if (values.indexOf(n) < 0) { if (values.indexOf(n) < 0) {
@ -129,8 +130,8 @@ export module ValidatorsEx {
}; };
} }
export function noop() { export function noop(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => { return (control: AbstractControl) => {
return null; 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/modal-view';
export * from './utils/pager'; export * from './utils/pager';
export * from './utils/string-helper'; export * from './utils/string-helper';
export * from './utils/types';
export * from './utils/version'; 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]; const entry = this.entries[key];
if (entry) { 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, AssetReplacedDto,
AssetsService, AssetsService,
AuthService, AuthService,
DateTime,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
ModalView, ModalView,
@ -83,7 +84,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
if (initFile) { if (initFile) {
this.appNameOnce() 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 => { .subscribe(dto => {
if (dto instanceof AssetDto) { if (dto instanceof AssetDto) {
this.emitLoaded(dto); 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)) .switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.assetVersion))
.subscribe(dto => { .subscribe(dto => {
if (dto instanceof AssetReplacedDto) { 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 { } else {
this.setProgress(dto); this.setProgress(dto);
} }
@ -126,7 +127,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.assetVersion)) .switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.assetVersion))
.subscribe(() => { .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(); this.resetRenameForm();
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);

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

@ -7,10 +7,6 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
&-message {
font-size: .9rem;
}
&-main { &-main {
@include flex-grow(1); @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'; import { AppLanguageDto, AppLanguagesService } from './../services/app-languages.service';
@Injectable() @Injectable()
export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[]> { export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[] | null> {
constructor( constructor(
private readonly appLanguagesService: AppLanguagesService, private readonly appLanguagesService: AppLanguagesService,
private readonly router: Router 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 params = allParams(route);
const appName = params['appName']; 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'; import { ContentDto, ContentsService } from './../services/contents.service';
@Injectable() @Injectable()
export class ResolveContentGuard implements Resolve<ContentDto> { export class ResolveContentGuard implements Resolve<ContentDto | null> {
constructor( constructor(
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly router: Router 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 params = allParams(route);
const appName = params['appName']; 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'; import { UserDto, UserManagementService } from './../services/users.service';
@Injectable() @Injectable()
export class ResolveUserGuard implements Resolve<UserDto> { export class ResolveUserGuard implements Resolve<UserDto | null> {
constructor( constructor(
private readonly userManagementService: UserManagementService, private readonly userManagementService: UserManagementService,
private readonly router: Router 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 params = allParams(route);
const userId = params['userId']; const userId = params['userId'];

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

@ -18,7 +18,7 @@ import {
} from './../'; } from './../';
describe('AuthInterceptor', () => { describe('AuthInterceptor', () => {
let authService: IMock<AuthService> = null; let authService: IMock<AuthService>;
beforeEach(() => { beforeEach(() => {
authService = Mock.ofType(AuthService); 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 token = user ? user.authToken : '';
const authReq = req.clone({ const authReq = req.clone({

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

Loading…
Cancel
Save