Add calendar integrations and collaboration updates
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
namespace Socialize.Api.Modules.ContentItems.Contracts;
|
||||
|
||||
public record ContentItemActivityWriteModel(
|
||||
Guid WorkspaceId,
|
||||
Guid ContentItemId,
|
||||
string EventType,
|
||||
string EntityType,
|
||||
Guid EntityId,
|
||||
string Summary,
|
||||
Guid? ActorUserId,
|
||||
string? ActorEmail,
|
||||
string? MetadataJson);
|
||||
|
||||
public interface IContentItemActivityWriter
|
||||
{
|
||||
Task WriteAsync(ContentItemActivityWriteModel model, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
public class ContentItemActivityEntry
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid WorkspaceId { get; set; }
|
||||
public Guid ContentItemId { get; set; }
|
||||
public required string EventType { get; set; }
|
||||
public required string EntityType { get; set; }
|
||||
public Guid EntityId { get; set; }
|
||||
public required string Summary { get; set; }
|
||||
public Guid? ActorUserId { get; set; }
|
||||
public string? ActorEmail { get; set; }
|
||||
public string? MetadataJson { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
@@ -41,6 +41,23 @@ public static class ContentItemModelConfiguration
|
||||
revision.HasIndex(x => new { x.ContentItemId, x.RevisionNumber }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ContentItemActivityEntry>(entry =>
|
||||
{
|
||||
entry.ToTable("ContentItemActivityEntries");
|
||||
entry.HasKey(x => x.Id);
|
||||
entry.Property(x => x.EventType).HasMaxLength(128).IsRequired();
|
||||
entry.Property(x => x.EntityType).HasMaxLength(128).IsRequired();
|
||||
entry.Property(x => x.Summary).HasMaxLength(1024).IsRequired();
|
||||
entry.Property(x => x.ActorEmail).HasMaxLength(256);
|
||||
entry.Property(x => x.MetadataJson).HasColumnType("jsonb");
|
||||
entry.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
entry.HasIndex(x => x.WorkspaceId);
|
||||
entry.HasIndex(x => x.ContentItemId);
|
||||
entry.HasIndex(x => new { x.ContentItemId, x.CreatedAt });
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems;
|
||||
|
||||
@@ -7,6 +8,8 @@ public static class DependencyInjection
|
||||
public static WebApplicationBuilder AddContentItemsModule(
|
||||
this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddScoped<IContentItemActivityWriter, ContentItemActivityWriter>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
@@ -36,6 +38,7 @@ public class CreateContentItemRequestValidator
|
||||
public class CreateContentItemHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<CreateContentItemRequest, ContentItemDto>
|
||||
{
|
||||
@@ -121,6 +124,26 @@ public class CreateContentItemHandler(
|
||||
});
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
item.WorkspaceId,
|
||||
item.Id,
|
||||
"content-item.created",
|
||||
"ContentItem",
|
||||
item.Id,
|
||||
$"Content item {item.Title} was created.",
|
||||
User.GetUserId(),
|
||||
User.GetEmail(),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
status = item.Status,
|
||||
revisionLabel = item.CurrentRevisionLabel,
|
||||
dueDate = item.DueDate,
|
||||
publicationTargets = item.PublicationTargets,
|
||||
hashtags = item.Hashtags,
|
||||
})),
|
||||
ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
item.WorkspaceId,
|
||||
|
||||
@@ -2,8 +2,10 @@ using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
@@ -12,7 +14,8 @@ public record CreateContentItemRevisionRequest(
|
||||
string PublicationMessage,
|
||||
string PublicationTargets,
|
||||
string? Hashtags,
|
||||
string? ChangeSummary);
|
||||
string? ChangeSummary,
|
||||
DateTimeOffset? DueDate);
|
||||
|
||||
public class CreateContentItemRevisionRequestValidator
|
||||
: Validator<CreateContentItemRevisionRequest>
|
||||
@@ -30,6 +33,7 @@ public class CreateContentItemRevisionRequestValidator
|
||||
public class CreateContentItemRevisionHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<CreateContentItemRevisionRequest, ContentItemRevisionDto>
|
||||
{
|
||||
@@ -58,11 +62,21 @@ public class CreateContentItemRevisionHandler(
|
||||
|
||||
int revisionNumber = item.CurrentRevisionNumber + 1;
|
||||
string revisionLabel = $"v{revisionNumber}";
|
||||
string previousTitle = item.Title;
|
||||
string previousPublicationMessage = item.PublicationMessage;
|
||||
string previousPublicationTargets = item.PublicationTargets;
|
||||
string? previousHashtags = item.Hashtags;
|
||||
DateTimeOffset? previousDueDate = item.DueDate;
|
||||
string newTitle = request.Title.Trim();
|
||||
string newPublicationMessage = request.PublicationMessage.Trim();
|
||||
string newPublicationTargets = request.PublicationTargets.Trim();
|
||||
string? newHashtags = string.IsNullOrWhiteSpace(request.Hashtags) ? null : request.Hashtags.Trim();
|
||||
|
||||
item.Title = request.Title.Trim();
|
||||
item.PublicationMessage = request.PublicationMessage.Trim();
|
||||
item.PublicationTargets = request.PublicationTargets.Trim();
|
||||
item.Hashtags = string.IsNullOrWhiteSpace(request.Hashtags) ? null : request.Hashtags.Trim();
|
||||
item.Title = newTitle;
|
||||
item.PublicationMessage = newPublicationMessage;
|
||||
item.PublicationTargets = newPublicationTargets;
|
||||
item.Hashtags = newHashtags;
|
||||
item.DueDate = request.DueDate;
|
||||
item.CurrentRevisionNumber = revisionNumber;
|
||||
item.CurrentRevisionLabel = revisionLabel;
|
||||
|
||||
@@ -84,6 +98,32 @@ public class CreateContentItemRevisionHandler(
|
||||
dbContext.ContentItemRevisions.Add(revision);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
List<object> changedFields = [];
|
||||
AddChangedField(changedFields, "title", previousTitle, item.Title);
|
||||
AddChangedField(changedFields, "publicationMessage", previousPublicationMessage, item.PublicationMessage);
|
||||
AddChangedField(changedFields, "publicationTargets", previousPublicationTargets, item.PublicationTargets);
|
||||
AddChangedField(changedFields, "hashtags", previousHashtags, item.Hashtags);
|
||||
AddChangedField(changedFields, "dueDate", previousDueDate, item.DueDate);
|
||||
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
item.WorkspaceId,
|
||||
item.Id,
|
||||
"content-item.revision.created",
|
||||
"ContentItemRevision",
|
||||
revision.Id,
|
||||
$"Revision {revisionLabel} was created for {item.Title}.",
|
||||
User.GetUserId(),
|
||||
User.GetEmail(),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
revisionLabel,
|
||||
revisionNumber,
|
||||
changeSummary = revision.ChangeSummary,
|
||||
changedFields,
|
||||
})),
|
||||
ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
item.WorkspaceId,
|
||||
@@ -112,4 +152,19 @@ public class CreateContentItemRevisionHandler(
|
||||
|
||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
||||
}
|
||||
|
||||
private static void AddChangedField<T>(List<object> changedFields, string field, T oldValue, T newValue)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
changedFields.Add(new
|
||||
{
|
||||
field,
|
||||
oldValue,
|
||||
newValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
public record ContentItemActivityEntryDto(
|
||||
Guid Id,
|
||||
Guid WorkspaceId,
|
||||
Guid ContentItemId,
|
||||
string EventType,
|
||||
string EntityType,
|
||||
Guid EntityId,
|
||||
string Summary,
|
||||
Guid? ActorUserId,
|
||||
string? ActorEmail,
|
||||
string? MetadataJson,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
public class GetContentItemActivityHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: EndpointWithoutRequest<IReadOnlyCollection<ContentItemActivityEntryDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/content-items/{id}/activity");
|
||||
Options(o => o.WithTags("Content Items"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
Guid id = Route<Guid>("id");
|
||||
|
||||
ContentItem? item = await dbContext.ContentItems
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == id, ct);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
List<ContentItemActivityEntryDto> entries = await dbContext.ContentItemActivityEntries
|
||||
.Where(entry => entry.ContentItemId == item.Id)
|
||||
.OrderByDescending(entry => entry.CreatedAt)
|
||||
.Take(200)
|
||||
.Select(entry => new ContentItemActivityEntryDto(
|
||||
entry.Id,
|
||||
entry.WorkspaceId,
|
||||
entry.ContentItemId,
|
||||
entry.EventType,
|
||||
entry.EntityType,
|
||||
entry.EntityId,
|
||||
entry.Summary,
|
||||
entry.ActorUserId,
|
||||
entry.ActorEmail,
|
||||
entry.MetadataJson,
|
||||
entry.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(entries, ct);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Approvals.Services;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
@@ -24,6 +26,7 @@ public class UpdateContentItemStatusHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<UpdateContentItemStatusRequest, ContentItemDetailDto>
|
||||
{
|
||||
@@ -122,12 +125,33 @@ public class UpdateContentItemStatusHandler(
|
||||
}
|
||||
}
|
||||
|
||||
string previousStatus = item.Status;
|
||||
if (item.Status != "In approval" || normalizedStatus != "In approval")
|
||||
{
|
||||
item.Status = normalizedStatus;
|
||||
}
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
if (previousStatus != item.Status)
|
||||
{
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
item.WorkspaceId,
|
||||
item.Id,
|
||||
"content-item.status.updated",
|
||||
"ContentItem",
|
||||
item.Id,
|
||||
$"Status changed from {previousStatus} to {item.Status} for {item.Title}.",
|
||||
User.GetUserId(),
|
||||
User.GetEmail(),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
oldValue = previousStatus,
|
||||
newValue = item.Status,
|
||||
})),
|
||||
ct);
|
||||
}
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
item.WorkspaceId,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Services;
|
||||
|
||||
public class ContentItemActivityWriter(
|
||||
AppDbContext dbContext)
|
||||
: IContentItemActivityWriter
|
||||
{
|
||||
public async Task WriteAsync(ContentItemActivityWriteModel model, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ContentItemActivityEntry entry = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
WorkspaceId = model.WorkspaceId,
|
||||
ContentItemId = model.ContentItemId,
|
||||
EventType = model.EventType,
|
||||
EntityType = model.EntityType,
|
||||
EntityId = model.EntityId,
|
||||
Summary = model.Summary,
|
||||
ActorUserId = model.ActorUserId,
|
||||
ActorEmail = model.ActorEmail,
|
||||
MetadataJson = model.MetadataJson,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
dbContext.ContentItemActivityEntries.Add(entry);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user