Add calendar integrations and collaboration updates
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-05-05 15:25:53 -04:00
parent c49f03ec06
commit b66c10b681
82 changed files with 8420 additions and 2048 deletions

View File

@@ -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,

View File

@@ -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,
});
}
}

View File

@@ -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);
}
}

View File

@@ -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,