diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index c4f7c2bcc..eb8fde5e7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -63,6 +63,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonJson] public IdContentData DataDraftByIds { get; set; } + [BsonIgnoreIfNull] + [BsonElement("sj")] + [BsonJson] + public ScheduleJob ScheduleJob { get; set; } + [BsonIgnoreIfDefault] [BsonElement("dt")] public string DataText { get; set; } @@ -75,18 +80,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonElement("si")] public NamedId SchemaId { get; set; } - [BsonIgnoreIfNull] - [BsonElement("st")] - public Status? ScheduledTo { get; set; } - [BsonIgnoreIfNull] [BsonElement("sa")] public Instant? ScheduledAt { get; set; } - [BsonIgnoreIfNull] - [BsonElement("sb")] - public RefToken ScheduledBy { get; set; } - [BsonRequired] [BsonElement("ct")] public Instant Created { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index ea1bd0903..ddb240e1c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -43,6 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents IndexedAppId = value.AppId.Id, IndexedSchemaId = value.SchemaId.Id, ReferencedIds = idData.ToReferencedIds(schema.SchemaDef), + ScheduledAt = value.ScheduleJob?.DueTime, Version = newVersion }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs index e855a9aff..d388b70e7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================= +using System; using NodaTime; using Squidex.Domain.Apps.Core.Contents; @@ -15,5 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public Status Status { get; set; } public Instant? DueTime { get; set; } + + public Guid? JobId { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 72c2158ee..a32b203db 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -30,11 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public Status Status { get; set; } - public Status? ScheduledTo { get; set; } - - public Instant? ScheduledAt { get; set; } - - public RefToken ScheduledBy { get; set; } + public ScheduleJob ScheduleJob { get; set; } public RefToken CreatedBy { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index 936cb377e..000314ccb 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -98,25 +98,39 @@ namespace Squidex.Domain.Apps.Entities.Contents case ChangeContentStatus changeContentStatus: return UpdateAsync(changeContentStatus, async c => { - GuardContent.CanChangeContentStatus(Snapshot.IsPending, Snapshot.Status, c); - - if (c.DueTime.HasValue) + try { - ScheduleStatus(c); + GuardContent.CanChangeContentStatus(Snapshot.IsPending, Snapshot.Status, c); + + if (c.DueTime.HasValue) + { + ScheduleStatus(c); + } + else + { + if (Snapshot.IsPending && Snapshot.Status == Status.Published && c.Status == Status.Published) + { + ConfirmChanges(c); + } + else + { + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to change content."); + + await ctx.ExecuteScriptAsync(x => x.ScriptChange, c.Status, c, Snapshot.Data); + + ChangeStatus(c); + } + } } - else + catch (Exception) { - if (Snapshot.IsPending && Snapshot.Status == Status.Published && c.Status == Status.Published) + if (c.JobId.HasValue && Snapshot?.ScheduleJob.Id == c.JobId) { - ConfirmChanges(c); + CancelScheduling(c); } else { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, () => "Failed to change content."); - - await ctx.ExecuteScriptAsync(x => x.ScriptChange, c.Status, c, Snapshot.Data); - - ChangeStatus(c); + throw; } } }); @@ -220,6 +234,11 @@ namespace Squidex.Domain.Apps.Entities.Contents RaiseEvent(SimpleMapper.Map(command, new ContentUpdateProposed { Data = data })); } + public void CancelScheduling(ChangeContentStatus command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentSchedulingCancelled())); + } + public void ScheduleStatus(ChangeContentStatus command) { RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs index 5778497e1..3b7260f3f 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs @@ -37,6 +37,9 @@ namespace Squidex.Domain.Apps.Entities.Contents AddEventMessage( "proposed update for {[Schema]} content."); + AddEventMessage( + "failed to schedule status change for {[Schema]} content."); + AddEventMessage( "changed status of {[Schema]} content to {[Status]}."); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs index aac0110eb..968b2d384 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs @@ -72,9 +72,14 @@ namespace Squidex.Domain.Apps.Entities.Contents { try { - var command = new ChangeContentStatus { ContentId = content.Id, Status = content.ScheduledTo.Value, Actor = content.ScheduledBy }; + var job = content.ScheduleJob; - await commandBus.Value.PublishAsync(command); + if (job != null) + { + var command = new ChangeContentStatus { ContentId = content.Id, Status = job.Status, Actor = job.ScheduledBy, JobId = job.Id }; + + await commandBus.Value.PublishAsync(command); + } } catch (Exception ex) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs index cf908dd4d..1a7e53424 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; @@ -25,11 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Status Status { get; } - Status? ScheduledTo { get; } - - Instant? ScheduledAt { get; } - - RefToken ScheduledBy { get; } + ScheduleJob ScheduleJob { get; } NamedContentData Data { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs b/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs new file mode 100644 index 000000000..d1dc4c444 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ScheduleJob + { + public Guid Id { get; } + + public Status Status { get; } + + public RefToken ScheduledBy { get; } + + public Instant DueTime { get; } + + public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant dueTime) + { + Id = id; + ScheduledBy = scheduledBy; + Status = status; + DueTime = dueTime; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index c6032ea29..f9028fcbf 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -7,7 +7,6 @@ using System; using Newtonsoft.Json; -using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; @@ -36,13 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State public Status Status { get; set; } [JsonProperty] - public Status? ScheduledTo { get; set; } - - [JsonProperty] - public Instant? ScheduledAt { get; set; } - - [JsonProperty] - public RefToken ScheduledBy { get; set; } + public ScheduleJob ScheduleJob { get; set; } [JsonProperty] public bool IsPending { get; set; } @@ -81,18 +74,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.State IsPending = false; } - protected void On(ContentStatusScheduled @event) - { - ScheduledAt = @event.DueTime; - ScheduledBy = @event.Actor; - ScheduledTo = @event.Status; - } - protected void On(ContentChangesPublished @event) { - ScheduledAt = null; - ScheduledBy = null; - ScheduledTo = null; + ScheduleJob = null; Data = DataDraft; @@ -101,9 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State protected void On(ContentStatusChanged @event) { - ScheduledAt = null; - ScheduledBy = null; - ScheduledTo = null; + ScheduleJob = null; Status = @event.Status; @@ -113,6 +95,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.State } } + protected void On(ContentSchedulingCancelled @event) + { + ScheduleJob = null; + } + + protected void On(ContentStatusScheduled @event) + { + ScheduleJob = new ScheduleJob(Guid.NewGuid(), @event.Status, @event.Actor, @event.DueTime); + } + protected void On(ContentDeleted @event) { IsDeleted = true; diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs new file mode 100644 index 000000000..e585a64e1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Contents +{ + [EventType(nameof(ContentSchedulingCancelled))] + public sealed class ContentSchedulingCancelled : ContentEvent + { + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs rename to src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs similarity index 97% rename from src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs rename to src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 3905530ea..165dbe519 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -119,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var response = new ContentsDto { Total = result.Total, - Items = result.Take(200).Select(item => SimpleMapper.Map(item, new ContentDto { Data = item.Data, DataDraft = item.DataDraft })).ToArray() + Items = result.Take(200).Select(ContentDto.FromContent).ToArray() }; Response.Headers["Surrogate-Key"] = string.Join(" ", response.Items.Select(x => x.Id)); @@ -148,7 +148,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { var content = await contentQuery.FindContentAsync(App, name, User, id); - var response = SimpleMapper.Map(content, new ContentDto { Data = content.Data, DataDraft = content.DataDraft }); + var response = ContentDto.FromContent(content); Response.Headers["ETag"] = content.Version.ToString(); Response.Headers["Surrogate-Key"] = content.Id.ToString(); @@ -179,7 +179,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { var content = await contentQuery.FindContentAsync(App, name, User, id, version); - var response = SimpleMapper.Map(content, new ContentDto { Data = content.Data, DataDraft = content.DataDraft }); + var response = ContentDto.FromContent(content); Response.Headers["ETag"] = content.Version.ToString(); Response.Headers["Surrogate-Key"] = content.Id.ToString(); diff --git a/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs rename to src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs diff --git a/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs rename to src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs diff --git a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs similarity index 83% rename from src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs rename to src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index cd5340a26..48885031d 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -9,9 +9,11 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Contents.Models { @@ -53,17 +55,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// /// The scheduled status. /// - public Status? ScheduledTo { get; set; } - - /// - /// The scheduled date. - /// - public Instant? ScheduledAt { get; set; } - - /// - /// The user that has scheduled the content. - /// - public RefToken ScheduledBy { get; set; } + public ScheduleJobDto ScheduleJob { get; set; } /// /// The date and time when the content item has been created. @@ -103,5 +95,20 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models return response; } + + public static ContentDto FromContent(IContentEntity content) + { + var response = SimpleMapper.Map(content, new ContentDto()); + + response.Data = content.Data; + response.DataDraft = content.DataDraft; + + if (content.ScheduleJob != null) + { + response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); + } + + return response; + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Content/Models/ContentsDto.cs rename to src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs new file mode 100644 index 000000000..b0e7b34cb --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class ScheduleJobDto + { + /// + /// The id of the schedule job. + /// + public Guid Id { get; set; } + + /// + /// The new status. + /// + public Status Status { get; set; } + + /// + /// The user who schedule the content. + /// + public RefToken ScheduledBy { get; set; } + + /// + /// The target date and time when the content should be scheduled. + /// + public Instant DueTime { get; set; } + } +} diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 9d5918c8d..c119b7849 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -35,8 +35,8 @@