chore: add missing multi-level editor for approval workflow, rename projects to campaings.
This commit is contained in:
@@ -8,7 +8,7 @@ using Socialize.Api.Modules.ContentItems.Data;
|
|||||||
using Socialize.Api.Modules.Feedback.Data;
|
using Socialize.Api.Modules.Feedback.Data;
|
||||||
using Socialize.Api.Modules.Identity.Data;
|
using Socialize.Api.Modules.Identity.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Data;
|
using Socialize.Api.Modules.Notifications.Data;
|
||||||
using Socialize.Api.Modules.Projects.Data;
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Data;
|
namespace Socialize.Api.Data;
|
||||||
@@ -20,14 +20,16 @@ public class AppDbContext(
|
|||||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||||
public DbSet<Client> Clients => Set<Client>();
|
public DbSet<Client> Clients => Set<Client>();
|
||||||
public DbSet<Project> Projects => Set<Project>();
|
public DbSet<Campaign> Campaigns => Set<Campaign>();
|
||||||
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
|
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
|
||||||
public DbSet<ContentItemRevision> ContentItemRevisions => Set<ContentItemRevision>();
|
public DbSet<ContentItemRevision> ContentItemRevisions => Set<ContentItemRevision>();
|
||||||
public DbSet<Asset> Assets => Set<Asset>();
|
public DbSet<Asset> Assets => Set<Asset>();
|
||||||
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
||||||
public DbSet<Comment> Comments => Set<Comment>();
|
public DbSet<Comment> Comments => Set<Comment>();
|
||||||
|
public DbSet<ApprovalWorkflowInstance> ApprovalWorkflowInstances => Set<ApprovalWorkflowInstance>();
|
||||||
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
|
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
|
||||||
public DbSet<ApprovalDecision> ApprovalDecisions => Set<ApprovalDecision>();
|
public DbSet<ApprovalDecision> ApprovalDecisions => Set<ApprovalDecision>();
|
||||||
|
public DbSet<WorkspaceApprovalStepConfiguration> WorkspaceApprovalStepConfigurations => Set<WorkspaceApprovalStepConfiguration>();
|
||||||
public DbSet<NotificationEvent> NotificationEvents => Set<NotificationEvent>();
|
public DbSet<NotificationEvent> NotificationEvents => Set<NotificationEvent>();
|
||||||
public DbSet<FeedbackReport> FeedbackReports => Set<FeedbackReport>();
|
public DbSet<FeedbackReport> FeedbackReports => Set<FeedbackReport>();
|
||||||
public DbSet<FeedbackTag> FeedbackTags => Set<FeedbackTag>();
|
public DbSet<FeedbackTag> FeedbackTags => Set<FeedbackTag>();
|
||||||
@@ -41,7 +43,7 @@ public class AppDbContext(
|
|||||||
|
|
||||||
builder.ConfigureWorkspacesModule();
|
builder.ConfigureWorkspacesModule();
|
||||||
builder.ConfigureClientsModule();
|
builder.ConfigureClientsModule();
|
||||||
builder.ConfigureProjectsModule();
|
builder.ConfigureCampaignsModule();
|
||||||
builder.ConfigureContentItemsModule();
|
builder.ConfigureContentItemsModule();
|
||||||
builder.ConfigureAssetsModule();
|
builder.ConfigureAssetsModule();
|
||||||
builder.ConfigureCommentsModule();
|
builder.ConfigureCommentsModule();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ using Socialize.Api.Modules.Comments.Data;
|
|||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Clients.Data;
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Data;
|
using Socialize.Api.Modules.Notifications.Data;
|
||||||
using Socialize.Api.Modules.Projects.Data;
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -22,8 +22,8 @@ public static class DevelopmentSeedExtensions
|
|||||||
private static readonly Guid WorkspaceId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
private static readonly Guid WorkspaceId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||||
private static readonly Guid ScopedClientId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
private static readonly Guid ScopedClientId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||||
private static readonly Guid HiddenClientId = Guid.Parse("22222222-2222-2222-2222-333333333333");
|
private static readonly Guid HiddenClientId = Guid.Parse("22222222-2222-2222-2222-333333333333");
|
||||||
private static readonly Guid ScopedProjectId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
private static readonly Guid ScopedCampaignId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
||||||
private static readonly Guid HiddenProjectId = Guid.Parse("33333333-3333-3333-3333-444444444444");
|
private static readonly Guid HiddenCampaignId = Guid.Parse("33333333-3333-3333-3333-444444444444");
|
||||||
private static readonly Guid ScopedContentItemId = Guid.Parse("44444444-4444-4444-4444-444444444444");
|
private static readonly Guid ScopedContentItemId = Guid.Parse("44444444-4444-4444-4444-444444444444");
|
||||||
private static readonly Guid HiddenContentItemId = Guid.Parse("44444444-4444-4444-4444-555555555555");
|
private static readonly Guid HiddenContentItemId = Guid.Parse("44444444-4444-4444-4444-555555555555");
|
||||||
private static readonly Guid ScopedAssetId = Guid.Parse("55555555-5555-5555-5555-555555555555");
|
private static readonly Guid ScopedAssetId = Guid.Parse("55555555-5555-5555-5555-555555555555");
|
||||||
@@ -99,7 +99,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
[
|
[
|
||||||
new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()),
|
new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()),
|
||||||
new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()),
|
new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()),
|
||||||
new Claim(KnownClaims.ProjectScope, ScopedProjectId.ToString()),
|
new Claim(KnownClaims.CampaignScope, ScopedCampaignId.ToString()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
User dev = await EnsureUserAsync(
|
User dev = await EnsureUserAsync(
|
||||||
@@ -200,7 +200,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
|
|
||||||
IList<Claim> existingClaims = await userManager.GetClaimsAsync(user);
|
IList<Claim> existingClaims = await userManager.GetClaimsAsync(user);
|
||||||
List<Claim> managedClaims = existingClaims
|
List<Claim> managedClaims = existingClaims
|
||||||
.Where(claim => claim.Type is KnownClaims.WorkspaceScope or KnownClaims.ClientScope or KnownClaims.ProjectScope or KnownClaims.Persona)
|
.Where(claim => claim.Type is KnownClaims.WorkspaceScope or KnownClaims.ClientScope or KnownClaims.CampaignScope or KnownClaims.Persona)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (Claim claim in managedClaims)
|
foreach (Claim claim in managedClaims)
|
||||||
@@ -273,9 +273,9 @@ public static class DevelopmentSeedExtensions
|
|||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
await UpsertProjectAsync(
|
await UpsertCampaignAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
ScopedProjectId,
|
ScopedCampaignId,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
ScopedClientId,
|
ScopedClientId,
|
||||||
"Spring Launch",
|
"Spring Launch",
|
||||||
@@ -285,9 +285,9 @@ public static class DevelopmentSeedExtensions
|
|||||||
"Cross-channel launch campaign for the spring offer.",
|
"Cross-channel launch campaign for the spring offer.",
|
||||||
"Coordinate creative approvals before the final week.",
|
"Coordinate creative approvals before the final week.",
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
await UpsertProjectAsync(
|
await UpsertCampaignAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
HiddenProjectId,
|
HiddenCampaignId,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
HiddenClientId,
|
HiddenClientId,
|
||||||
"Summer Retention",
|
"Summer Retention",
|
||||||
@@ -303,11 +303,11 @@ public static class DevelopmentSeedExtensions
|
|||||||
ScopedContentItemId,
|
ScopedContentItemId,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
ScopedClientId,
|
ScopedClientId,
|
||||||
ScopedProjectId,
|
ScopedCampaignId,
|
||||||
"Spring launch hero video",
|
"Spring launch hero video",
|
||||||
"Fresh seasonal menu launch across Instagram and TikTok.",
|
"Fresh seasonal menu launch across Instagram and TikTok.",
|
||||||
"Instagram Reel, TikTok",
|
"Instagram Reel, TikTok",
|
||||||
"In client review",
|
"In approval",
|
||||||
DateTimeOffset.UtcNow.AddDays(3),
|
DateTimeOffset.UtcNow.AddDays(3),
|
||||||
"v3",
|
"v3",
|
||||||
3,
|
3,
|
||||||
@@ -317,7 +317,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
HiddenContentItemId,
|
HiddenContentItemId,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
HiddenClientId,
|
HiddenClientId,
|
||||||
HiddenProjectId,
|
HiddenCampaignId,
|
||||||
"Bakery loyalty carousel",
|
"Bakery loyalty carousel",
|
||||||
"Reward regular customers with a four-card retention carousel.",
|
"Reward regular customers with a four-card retention carousel.",
|
||||||
"Instagram Carousel",
|
"Instagram Carousel",
|
||||||
@@ -491,7 +491,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task UpsertProjectAsync(
|
private static async Task UpsertCampaignAsync(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
Guid id,
|
Guid id,
|
||||||
Guid workspaceId,
|
Guid workspaceId,
|
||||||
@@ -504,26 +504,26 @@ public static class DevelopmentSeedExtensions
|
|||||||
string? notes,
|
string? notes,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Project? project = await dbContext.Projects.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
Campaign? campaign = await dbContext.Campaigns.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
||||||
if (project is null)
|
if (campaign is null)
|
||||||
{
|
{
|
||||||
project = new Project
|
campaign = new Campaign
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Name = string.Empty,
|
Name = string.Empty,
|
||||||
Status = string.Empty,
|
Status = string.Empty,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
dbContext.Projects.Add(project);
|
dbContext.Campaigns.Add(campaign);
|
||||||
}
|
}
|
||||||
project.WorkspaceId = workspaceId;
|
campaign.WorkspaceId = workspaceId;
|
||||||
project.ClientId = clientId;
|
campaign.ClientId = clientId;
|
||||||
project.Name = name;
|
campaign.Name = name;
|
||||||
project.Description = description;
|
campaign.Description = description;
|
||||||
project.Notes = notes;
|
campaign.Notes = notes;
|
||||||
project.Status = status;
|
campaign.Status = status;
|
||||||
project.StartDate = startDate;
|
campaign.StartDate = startDate;
|
||||||
project.EndDate = endDate;
|
campaign.EndDate = endDate;
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +532,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
Guid id,
|
Guid id,
|
||||||
Guid workspaceId,
|
Guid workspaceId,
|
||||||
Guid clientId,
|
Guid clientId,
|
||||||
Guid projectId,
|
Guid campaignId,
|
||||||
string title,
|
string title,
|
||||||
string publicationMessage,
|
string publicationMessage,
|
||||||
string publicationTargets,
|
string publicationTargets,
|
||||||
@@ -559,7 +559,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
}
|
}
|
||||||
item.WorkspaceId = workspaceId;
|
item.WorkspaceId = workspaceId;
|
||||||
item.ClientId = clientId;
|
item.ClientId = clientId;
|
||||||
item.ProjectId = projectId;
|
item.CampaignId = campaignId;
|
||||||
item.Title = title;
|
item.Title = title;
|
||||||
item.PublicationMessage = publicationMessage;
|
item.PublicationMessage = publicationMessage;
|
||||||
item.PublicationTargets = publicationTargets;
|
item.PublicationTargets = publicationTargets;
|
||||||
|
|||||||
@@ -36,21 +36,21 @@ public sealed class AccessScopeService
|
|||||||
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId));
|
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanAccessProject(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
public bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user)
|
return IsManager(user)
|
||||||
|| (CanAccessClient(user, workspaceId, clientId) && user.GetProjectScopeIds().Contains(projectId));
|
|| (CanAccessClient(user, workspaceId, clientId) && user.GetCampaignScopeIds().Contains(campaignId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanContributeToProject(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
public bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user) || (IsProvider(user) && CanAccessProject(user, workspaceId, clientId, projectId));
|
return IsManager(user) || (IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
public bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user)
|
return IsManager(user)
|
||||||
|| IsProvider(user) && CanAccessProject(user, workspaceId, clientId, projectId)
|
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|
||||||
|| IsClient(user) && CanAccessClient(user, workspaceId, clientId);
|
|| IsClient(user) && CanAccessClient(user, workspaceId, clientId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ public static class ClaimsPrincipalExtensions
|
|||||||
return claims.GetScopeIds(KnownClaims.ClientScope);
|
return claims.GetScopeIds(KnownClaims.ClientScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyCollection<Guid> GetProjectScopeIds(this ClaimsPrincipal claims)
|
public static IReadOnlyCollection<Guid> GetCampaignScopeIds(this ClaimsPrincipal claims)
|
||||||
{
|
{
|
||||||
return claims.GetScopeIds(KnownClaims.ProjectScope);
|
return claims.GetScopeIds(KnownClaims.CampaignScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string? GetPersona(this ClaimsPrincipal claims)
|
public static string? GetPersona(this ClaimsPrincipal claims)
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ public static class KnownClaims
|
|||||||
public const string PortraitUrl = "portraitUrl";
|
public const string PortraitUrl = "portraitUrl";
|
||||||
public const string WorkspaceScope = "workspace";
|
public const string WorkspaceScope = "workspace";
|
||||||
public const string ClientScope = "client";
|
public const string ClientScope = "client";
|
||||||
public const string ProjectScope = "project";
|
public const string CampaignScope = "campaign";
|
||||||
public const string Persona = "persona";
|
public const string Persona = "persona";
|
||||||
}
|
}
|
||||||
|
|||||||
1314
backend/src/Socialize.Api/Migrations/20260501170646_AddWorkspaceApprovalConfiguration.Designer.cs
generated
Normal file
1314
backend/src/Socialize.Api/Migrations/20260501170646_AddWorkspaceApprovalConfiguration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddWorkspaceApprovalConfiguration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ApprovalMode",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "Required");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "LockContentAfterApproval",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "SchedulePostsAutomaticallyOnApproval",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "SendAutomaticApprovalReminders",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ApprovalMode",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LockContentAfterApproval",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SchedulePostsAutomaticallyOnApproval",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SendAutomaticApprovalReminders",
|
||||||
|
table: "Workspaces");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1361
backend/src/Socialize.Api/Migrations/20260501173710_AddWorkspaceApprovalStepConfiguration.Designer.cs
generated
Normal file
1361
backend/src/Socialize.Api/Migrations/20260501173710_AddWorkspaceApprovalStepConfiguration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddWorkspaceApprovalStepConfiguration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "WorkspaceApprovalStepConfigurations",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TargetType = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
TargetValue = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
RequiredApproverCount = table.Column<int>(type: "integer", nullable: false, defaultValue: 1),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_WorkspaceApprovalStepConfigurations", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_WorkspaceApprovalStepConfigurations_WorkspaceId",
|
||||||
|
table: "WorkspaceApprovalStepConfigurations",
|
||||||
|
column: "WorkspaceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_WorkspaceApprovalStepConfigurations_WorkspaceId_SortOrder",
|
||||||
|
table: "WorkspaceApprovalStepConfigurations",
|
||||||
|
columns: new[] { "WorkspaceId", "SortOrder" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "WorkspaceApprovalStepConfigurations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1423
backend/src/Socialize.Api/Migrations/20260501175648_AddApprovalWorkflowRuntime.Designer.cs
generated
Normal file
1423
backend/src/Socialize.Api/Migrations/20260501175648_AddApprovalWorkflowRuntime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddApprovalWorkflowRuntime : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "WorkflowInstanceId",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "WorkflowStepRequiredApproverCount",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "WorkflowStepSortOrder",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "WorkflowStepTargetType",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "WorkflowStepTargetValue",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApprovalWorkflowInstances",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ContentItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
State = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
ApprovalMode = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
StartedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
CompletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApprovalWorkflowInstances", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalRequests_WorkflowInstanceId",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
column: "WorkflowInstanceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowInstances_ContentItemId",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
column: "ContentItemId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowInstances_ContentItemId_State",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
columns: new[] { "ContentItemId", "State" },
|
||||||
|
unique: true,
|
||||||
|
filter: "\"State\" = 'Pending'");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowInstances_WorkspaceId",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
column: "WorkspaceId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApprovalWorkflowInstances");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_ApprovalRequests_WorkflowInstanceId",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowInstanceId",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowStepRequiredApproverCount",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowStepSortOrder",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowStepTargetType",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowStepTargetValue",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1423
backend/src/Socialize.Api/Migrations/20260501191447_RenameProjectsToCampaigns.Designer.cs
generated
Normal file
1423
backend/src/Socialize.Api/Migrations/20260501191447_RenameProjectsToCampaigns.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RenameProjectsToCampaigns : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Projects",
|
||||||
|
newName: "Campaigns");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Projects",
|
||||||
|
table: "Campaigns");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Campaigns",
|
||||||
|
table: "Campaigns",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Projects_WorkspaceId",
|
||||||
|
table: "Campaigns",
|
||||||
|
newName: "IX_Campaigns_WorkspaceId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Projects_ClientId_Name",
|
||||||
|
table: "Campaigns",
|
||||||
|
newName: "IX_Campaigns_ClientId_Name");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Projects_ClientId",
|
||||||
|
table: "Campaigns",
|
||||||
|
newName: "IX_Campaigns_ClientId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "ProjectName",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
newName: "CampaignName");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "ProjectId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
newName: "CampaignId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "ProjectId",
|
||||||
|
table: "ContentItems",
|
||||||
|
newName: "CampaignId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_ContentItems_ProjectId",
|
||||||
|
table: "ContentItems",
|
||||||
|
newName: "IX_ContentItems_CampaignId");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Campaigns",
|
||||||
|
table: "Campaigns");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Projects",
|
||||||
|
table: "Campaigns",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Campaigns_WorkspaceId",
|
||||||
|
table: "Campaigns",
|
||||||
|
newName: "IX_Projects_WorkspaceId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Campaigns_ClientId_Name",
|
||||||
|
table: "Campaigns",
|
||||||
|
newName: "IX_Projects_ClientId_Name");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Campaigns_ClientId",
|
||||||
|
table: "Campaigns",
|
||||||
|
newName: "IX_Projects_ClientId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "Campaigns",
|
||||||
|
newName: "Projects");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "CampaignName",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
newName: "ProjectName");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "CampaignId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
newName: "ProjectId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "CampaignId",
|
||||||
|
table: "ContentItems",
|
||||||
|
newName: "ProjectId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_ContentItems_CampaignId",
|
||||||
|
table: "ContentItems",
|
||||||
|
newName: "IX_ContentItems_ProjectId");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -216,6 +216,23 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("WorkflowInstanceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int?>("WorkflowStepRequiredApproverCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("WorkflowStepSortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("WorkflowStepTargetType")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("WorkflowStepTargetValue")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
b.Property<Guid>("WorkspaceId")
|
b.Property<Guid>("WorkspaceId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -225,11 +242,103 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ReviewerEmail");
|
b.HasIndex("ReviewerEmail");
|
||||||
|
|
||||||
|
b.HasIndex("WorkflowInstanceId");
|
||||||
|
|
||||||
b.HasIndex("WorkspaceId");
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
b.ToTable("ApprovalRequests", (string)null);
|
b.ToTable("ApprovalRequests", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ApprovalMode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("CompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("ContentItemId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid>("WorkspaceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ContentItemId");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
|
b.HasIndex("ContentItemId", "State")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("\"State\" = 'Pending'");
|
||||||
|
|
||||||
|
b.ToTable("ApprovalWorkflowInstances", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.WorkspaceApprovalStepConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<int>("RequiredApproverCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1);
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TargetType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("TargetValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<Guid>("WorkspaceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId", "SortOrder")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("WorkspaceApprovalStepConfigurations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -329,6 +438,59 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("AssetRevisions", (string)null);
|
b.ToTable("AssetRevisions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ClientId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("character varying(4000)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("EndDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("character varying(4000)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid>("WorkspaceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId", "Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Campaigns", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -440,6 +602,9 @@ namespace Socialize.Api.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("CampaignId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<Guid>("ClientId")
|
b.Property<Guid>("ClientId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -463,9 +628,6 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(1024)
|
.HasMaxLength(1024)
|
||||||
.HasColumnType("character varying(1024)");
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
b.Property<Guid>("ProjectId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("PublicationMessage")
|
b.Property<string>("PublicationMessage")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(4000)
|
.HasMaxLength(4000)
|
||||||
@@ -491,9 +653,9 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ClientId");
|
b.HasIndex("CampaignId");
|
||||||
|
|
||||||
b.HasIndex("ProjectId");
|
b.HasIndex("ClientId");
|
||||||
|
|
||||||
b.HasIndex("WorkspaceId");
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
@@ -675,6 +837,13 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(1024)
|
.HasMaxLength(1024)
|
||||||
.HasColumnType("character varying(1024)");
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CampaignId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CampaignName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("CancellationReason")
|
b.Property<string>("CancellationReason")
|
||||||
.HasMaxLength(2000)
|
.HasMaxLength(2000)
|
||||||
.HasColumnType("character varying(2000)");
|
.HasColumnType("character varying(2000)");
|
||||||
@@ -712,13 +881,6 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<DateTimeOffset>("LastActivityAt")
|
b.Property<DateTimeOffset>("LastActivityAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<Guid?>("ProjectId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("ProjectName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ReporterDisplayName")
|
b.Property<string>("ReporterDisplayName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
@@ -1041,70 +1203,29 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("NotificationEvents", (string)null);
|
b.ToTable("NotificationEvents", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Projects.Data.Project", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("ClientId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(4000)
|
|
||||||
.HasColumnType("character varying(4000)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("EndDate")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("Notes")
|
|
||||||
.HasMaxLength(4000)
|
|
||||||
.HasColumnType("character varying(4000)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("StartDate")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<Guid>("WorkspaceId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ClientId");
|
|
||||||
|
|
||||||
b.HasIndex("WorkspaceId");
|
|
||||||
|
|
||||||
b.HasIndex("ClientId", "Name")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Projects", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ApprovalMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasDefaultValue("Required");
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<bool>("LockContentAfterApproval")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("LogoUrl")
|
b.Property<string>("LogoUrl")
|
||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
@@ -1117,6 +1238,16 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<Guid>("OwnerUserId")
|
b.Property<Guid>("OwnerUserId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("SchedulePostsAutomaticallyOnApproval")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("SendAutomaticApprovalReminders")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
b.Property<string>("Slug")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
|
|||||||
@@ -6,10 +6,28 @@ public static class ApprovalModelConfiguration
|
|||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
modelBuilder.Entity<ApprovalWorkflowInstance>(workflowInstance =>
|
||||||
|
{
|
||||||
|
workflowInstance.ToTable("ApprovalWorkflowInstances");
|
||||||
|
workflowInstance.HasKey(x => x.Id);
|
||||||
|
workflowInstance.Property(x => x.State).HasMaxLength(64).IsRequired();
|
||||||
|
workflowInstance.Property(x => x.ApprovalMode).HasMaxLength(64).IsRequired();
|
||||||
|
workflowInstance.Property(x => x.StartedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
workflowInstance.HasIndex(x => x.WorkspaceId);
|
||||||
|
workflowInstance.HasIndex(x => x.ContentItemId);
|
||||||
|
workflowInstance.HasIndex(x => new { x.ContentItemId, x.State })
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("\"State\" = 'Pending'");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
||||||
{
|
{
|
||||||
approvalRequest.ToTable("ApprovalRequests");
|
approvalRequest.ToTable("ApprovalRequests");
|
||||||
approvalRequest.HasKey(x => x.Id);
|
approvalRequest.HasKey(x => x.Id);
|
||||||
|
approvalRequest.Property(x => x.WorkflowStepTargetType).HasMaxLength(32);
|
||||||
|
approvalRequest.Property(x => x.WorkflowStepTargetValue).HasMaxLength(128);
|
||||||
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
|
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
|
||||||
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
|
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
|
||||||
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
|
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
|
||||||
@@ -20,6 +38,7 @@ public static class ApprovalModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
approvalRequest.HasIndex(x => x.WorkspaceId);
|
approvalRequest.HasIndex(x => x.WorkspaceId);
|
||||||
approvalRequest.HasIndex(x => x.ContentItemId);
|
approvalRequest.HasIndex(x => x.ContentItemId);
|
||||||
|
approvalRequest.HasIndex(x => x.WorkflowInstanceId);
|
||||||
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,6 +56,21 @@ public static class ApprovalModelConfiguration
|
|||||||
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<WorkspaceApprovalStepConfiguration>(approvalStep =>
|
||||||
|
{
|
||||||
|
approvalStep.ToTable("WorkspaceApprovalStepConfigurations");
|
||||||
|
approvalStep.HasKey(x => x.Id);
|
||||||
|
approvalStep.Property(x => x.Name).HasMaxLength(128).IsRequired();
|
||||||
|
approvalStep.Property(x => x.TargetType).HasMaxLength(32).IsRequired();
|
||||||
|
approvalStep.Property(x => x.TargetValue).HasMaxLength(128).IsRequired();
|
||||||
|
approvalStep.Property(x => x.RequiredApproverCount).HasDefaultValue(1);
|
||||||
|
approvalStep.Property(x => x.CreatedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
approvalStep.HasIndex(x => x.WorkspaceId);
|
||||||
|
approvalStep.HasIndex(x => new { x.WorkspaceId, x.SortOrder }).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ public class ApprovalRequest
|
|||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
public Guid ContentItemId { get; set; }
|
public Guid ContentItemId { get; set; }
|
||||||
|
public Guid? WorkflowInstanceId { get; set; }
|
||||||
|
public int? WorkflowStepSortOrder { get; set; }
|
||||||
|
public string? WorkflowStepTargetType { get; set; }
|
||||||
|
public string? WorkflowStepTargetValue { get; set; }
|
||||||
|
public int? WorkflowStepRequiredApproverCount { get; set; }
|
||||||
public required string Stage { get; set; }
|
public required string Stage { get; set; }
|
||||||
public required string ReviewerName { get; set; }
|
public required string ReviewerName { get; set; }
|
||||||
public required string ReviewerEmail { get; set; }
|
public required string ReviewerEmail { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
|
public class ApprovalWorkflowInstance
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid WorkspaceId { get; set; }
|
||||||
|
public Guid ContentItemId { get; set; }
|
||||||
|
public required string State { get; set; }
|
||||||
|
public required string ApprovalMode { get; set; }
|
||||||
|
public DateTimeOffset StartedAt { get; init; }
|
||||||
|
public DateTimeOffset? CompletedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
|
public class WorkspaceApprovalStepConfiguration
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid WorkspaceId { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public required string TargetType { get; set; }
|
||||||
|
public required string TargetValue { get; set; }
|
||||||
|
public int RequiredApproverCount { get; set; } = 1;
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Socialize.Api.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals;
|
namespace Socialize.Api.Modules.Approvals;
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@ public static class DependencyInjection
|
|||||||
public static WebApplicationBuilder AddApprovalsModule(
|
public static WebApplicationBuilder AddApprovalsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
|
builder.Services.AddScoped<ApprovalWorkflowRuntimeService>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ using System.Security.Cryptography;
|
|||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Socialize.Api.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
@@ -62,6 +64,22 @@ public class CreateApprovalRequestHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ApprovalWorkflowRules.CanCreateSingleStepApprovalRequest(workspace.ApprovalMode))
|
||||||
|
{
|
||||||
|
AddError(request => request.WorkspaceId, workspace.ApprovalMode == ApprovalModes.None
|
||||||
|
? "Approval workflow is disabled for this workspace."
|
||||||
|
: "Move content to In approval to start the configured multi-level approval workflow.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var approval = new ApprovalRequest()
|
var approval = new ApprovalRequest()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
@@ -79,14 +97,7 @@ public class CreateApprovalRequestHandler(
|
|||||||
|
|
||||||
dbContext.ApprovalRequests.Add(approval);
|
dbContext.ApprovalRequests.Add(approval);
|
||||||
|
|
||||||
if (approval.Stage == "Internal")
|
contentItem.Status = "In approval";
|
||||||
{
|
|
||||||
contentItem.Status = "In internal review";
|
|
||||||
}
|
|
||||||
else if (approval.Stage == "Client")
|
|
||||||
{
|
|
||||||
contentItem.Status = "In client review";
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
@@ -107,6 +118,11 @@ public class CreateApprovalRequestHandler(
|
|||||||
approval.Id,
|
approval.Id,
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
approval.ContentItemId,
|
approval.ContentItemId,
|
||||||
|
approval.WorkflowInstanceId,
|
||||||
|
approval.WorkflowStepSortOrder,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue,
|
||||||
|
approval.WorkflowStepRequiredApproverCount,
|
||||||
approval.Stage,
|
approval.Stage,
|
||||||
approval.ReviewerName,
|
approval.ReviewerName,
|
||||||
approval.ReviewerEmail,
|
approval.ReviewerEmail,
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ public record ApprovalRequestDto(
|
|||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
|
Guid? WorkflowInstanceId,
|
||||||
|
int? WorkflowStepSortOrder,
|
||||||
|
string? WorkflowStepTargetType,
|
||||||
|
string? WorkflowStepTargetValue,
|
||||||
|
int? WorkflowStepRequiredApproverCount,
|
||||||
string Stage,
|
string Stage,
|
||||||
string ReviewerName,
|
string ReviewerName,
|
||||||
string ReviewerEmail,
|
string ReviewerEmail,
|
||||||
@@ -56,7 +61,7 @@ public class GetApprovalsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -65,6 +70,7 @@ public class GetApprovalsHandler(
|
|||||||
List<ApprovalRequest> approvals = await dbContext.ApprovalRequests
|
List<ApprovalRequest> approvals = await dbContext.ApprovalRequests
|
||||||
.Where(approval => approval.ContentItemId == request.ContentItemId)
|
.Where(approval => approval.ContentItemId == request.ContentItemId)
|
||||||
.OrderByDescending(approval => approval.SentAt)
|
.OrderByDescending(approval => approval.SentAt)
|
||||||
|
.ThenBy(approval => approval.WorkflowStepSortOrder)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
List<Guid> approvalIds = approvals
|
List<Guid> approvalIds = approvals
|
||||||
@@ -91,6 +97,11 @@ public class GetApprovalsHandler(
|
|||||||
approval.Id,
|
approval.Id,
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
approval.ContentItemId,
|
approval.ContentItemId,
|
||||||
|
approval.WorkflowInstanceId,
|
||||||
|
approval.WorkflowStepSortOrder,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue,
|
||||||
|
approval.WorkflowStepRequiredApproverCount,
|
||||||
approval.Stage,
|
approval.Stage,
|
||||||
approval.ReviewerName,
|
approval.ReviewerName,
|
||||||
approval.ReviewerEmail,
|
approval.ReviewerEmail,
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ using Socialize.Api.Data;
|
|||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
@@ -19,7 +21,10 @@ public class SubmitApprovalDecisionRequestValidator
|
|||||||
{
|
{
|
||||||
public SubmitApprovalDecisionRequestValidator()
|
public SubmitApprovalDecisionRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Decision).NotEmpty().MaximumLength(64);
|
RuleFor(x => x.Decision)
|
||||||
|
.NotEmpty()
|
||||||
|
.Equal("Approved")
|
||||||
|
.WithMessage("Only approved decisions are supported.");
|
||||||
RuleFor(x => x.Comment).MaximumLength(2048);
|
RuleFor(x => x.Comment).MaximumLength(2048);
|
||||||
RuleFor(x => x.ReviewerName).MaximumLength(256);
|
RuleFor(x => x.ReviewerName).MaximumLength(256);
|
||||||
RuleFor(x => x.ReviewerEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ReviewerEmail));
|
RuleFor(x => x.ReviewerEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ReviewerEmail));
|
||||||
@@ -29,6 +34,7 @@ public class SubmitApprovalDecisionRequestValidator
|
|||||||
public class SubmitApprovalDecisionHandler(
|
public class SubmitApprovalDecisionHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
||||||
{
|
{
|
||||||
@@ -58,12 +64,19 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (User?.Identity?.IsAuthenticated == true &&
|
if (User?.Identity?.IsAuthenticated == true &&
|
||||||
!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
|
!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string normalizedDecision = request.Decision.Trim();
|
string normalizedDecision = request.Decision.Trim();
|
||||||
string decidedByName = User?.Identity?.IsAuthenticated == true
|
string decidedByName = User?.Identity?.IsAuthenticated == true
|
||||||
? User.GetAlias() ?? User.GetName()
|
? User.GetAlias() ?? User.GetName()
|
||||||
@@ -84,45 +97,44 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
approval.State = normalizedDecision;
|
ApprovalWorkflowDecisionResult workflowDecisionResult = await approvalWorkflowRuntimeService
|
||||||
approval.CompletedAt = DateTimeOffset.UtcNow;
|
.ApplyWorkflowStepDecisionAsync(approval, contentItem, workspace, User!, decision, ct);
|
||||||
|
|
||||||
if (approval.Stage == "Internal")
|
if (!workflowDecisionResult.Succeeded)
|
||||||
{
|
{
|
||||||
contentItem.Status = normalizedDecision switch
|
AddError(request => request.Decision, workflowDecisionResult.ErrorMessage ?? "The approval decision could not be recorded.");
|
||||||
{
|
await SendErrorsAsync(workflowDecisionResult.StatusCode, ct);
|
||||||
"Approved" => "Ready for client review",
|
return;
|
||||||
"Changes requested" => "Changes requested internally",
|
|
||||||
"Rejected" => "Rejected",
|
|
||||||
_ => contentItem.Status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (approval.Stage == "Client")
|
|
||||||
{
|
|
||||||
contentItem.Status = normalizedDecision switch
|
|
||||||
{
|
|
||||||
"Approved" => "Approved",
|
|
||||||
"Changes requested" => "Changes requested by client",
|
|
||||||
"Rejected" => "Rejected",
|
|
||||||
_ => contentItem.Status,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.ApprovalDecisions.Add(decision);
|
if (!workflowDecisionResult.IsWorkflowStep)
|
||||||
await dbContext.SaveChangesAsync(ct);
|
{
|
||||||
|
approval.State = normalizedDecision;
|
||||||
|
approval.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
await notificationEventWriter.WriteAsync(
|
if (normalizedDecision == "Approved")
|
||||||
new NotificationEventWriteModel(
|
{
|
||||||
approval.WorkspaceId,
|
contentItem.Status = ApprovalWorkflowRules.GetFinalApprovalStatus(
|
||||||
approval.ContentItemId,
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
"approval.decision.recorded",
|
contentItem.DueDate);
|
||||||
"ApprovalDecision",
|
}
|
||||||
decision.Id,
|
|
||||||
$"{decidedByName} recorded {normalizedDecision} for {contentItem.Title}.",
|
dbContext.ApprovalDecisions.Add(decision);
|
||||||
null,
|
await dbContext.SaveChangesAsync(ct);
|
||||||
decidedByEmail,
|
|
||||||
$$"""{"stage":"{{approval.Stage}}","status":"{{contentItem.Status}}"}"""),
|
await notificationEventWriter.WriteAsync(
|
||||||
ct);
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.decision.recorded",
|
||||||
|
"ApprovalDecision",
|
||||||
|
decision.Id,
|
||||||
|
$"{decidedByName} recorded {normalizedDecision} for {contentItem.Title}.",
|
||||||
|
null,
|
||||||
|
decidedByEmail,
|
||||||
|
$$"""{"stage":"{{approval.Stage}}","status":"{{contentItem.Status}}"}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
List<ApprovalDecision> decisions = await dbContext.ApprovalDecisions
|
List<ApprovalDecision> decisions = await dbContext.ApprovalDecisions
|
||||||
.Where(candidate => candidate.ApprovalRequestId == approval.Id)
|
.Where(candidate => candidate.ApprovalRequestId == approval.Id)
|
||||||
@@ -158,6 +170,11 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
approval.Id,
|
approval.Id,
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
approval.ContentItemId,
|
approval.ContentItemId,
|
||||||
|
approval.WorkflowInstanceId,
|
||||||
|
approval.WorkflowStepSortOrder,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue,
|
||||||
|
approval.WorkflowStepRequiredApproverCount,
|
||||||
approval.Stage,
|
approval.Stage,
|
||||||
approval.ReviewerName,
|
approval.ReviewerName,
|
||||||
approval.ReviewerEmail,
|
approval.ReviewerEmail,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
public static class ApprovalStepTargetTypes
|
||||||
|
{
|
||||||
|
public const string Role = "Role";
|
||||||
|
public const string Membership = "Membership";
|
||||||
|
public const string Member = "Member";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ApprovalMembershipTargets
|
||||||
|
{
|
||||||
|
public const string Team = "Team";
|
||||||
|
public const string Client = "Client";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ApprovalStepConfigurationRules
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlySet<string> AllowedTargetTypes = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role,
|
||||||
|
ApprovalStepTargetTypes.Membership,
|
||||||
|
ApprovalStepTargetTypes.Member,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly IReadOnlySet<string> AllowedRoleTargets = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
KnownRoles.Administrator,
|
||||||
|
KnownRoles.Manager,
|
||||||
|
KnownRoles.WorkspaceMember,
|
||||||
|
KnownRoles.Client,
|
||||||
|
KnownRoles.Provider,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly IReadOnlySet<string> AllowedMembershipTargets = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Team,
|
||||||
|
ApprovalMembershipTargets.Client,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool IsValidTargetType(string? targetType)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetType) && AllowedTargetTypes.Contains(targetType.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidRoleTarget(string? targetValue)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetValue) && AllowedRoleTargets.Contains(targetValue.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidMembershipTarget(string? targetValue)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetValue) && AllowedMembershipTargets.Contains(targetValue.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
public static class ApprovalModes
|
||||||
|
{
|
||||||
|
public const string None = "None";
|
||||||
|
public const string Optional = "Optional";
|
||||||
|
public const string Required = "Required";
|
||||||
|
public const string MultiLevel = "Multi-level";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ApprovalWorkflowRules
|
||||||
|
{
|
||||||
|
public static bool CanCreateSingleStepApprovalRequest(string approvalMode)
|
||||||
|
{
|
||||||
|
return approvalMode is ApprovalModes.Optional or ApprovalModes.Required;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool BlocksManualApprovedOrScheduledStatus(string approvalMode)
|
||||||
|
{
|
||||||
|
return approvalMode is ApprovalModes.Required or ApprovalModes.MultiLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsApprovalCompletionStatus(string status)
|
||||||
|
{
|
||||||
|
return status is "Approved" or "Scheduled";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetFinalApprovalStatus(bool schedulePostsAutomaticallyOnApproval, DateTimeOffset? plannedPublishDate)
|
||||||
|
{
|
||||||
|
return schedulePostsAutomaticallyOnApproval && plannedPublishDate.HasValue
|
||||||
|
? "Scheduled"
|
||||||
|
: "Approved";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasRequiredStepApprovals(int approvedDecisionCount, int requiredApproverCount)
|
||||||
|
{
|
||||||
|
return approvedDecisionCount >= Math.Max(1, requiredApproverCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanApproveWorkflowStep(
|
||||||
|
bool isAdministrator,
|
||||||
|
bool hasWorkspaceAccess,
|
||||||
|
IReadOnlyCollection<string> userRoles,
|
||||||
|
Guid userId,
|
||||||
|
string? targetType,
|
||||||
|
string? targetValue)
|
||||||
|
{
|
||||||
|
if (isAdministrator)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasWorkspaceAccess ||
|
||||||
|
string.IsNullOrWhiteSpace(targetType) ||
|
||||||
|
string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role => userRoles.Contains(targetValue),
|
||||||
|
ApprovalStepTargetTypes.Membership => MatchesMembershipTarget(userRoles, targetValue),
|
||||||
|
ApprovalStepTargetTypes.Member => ParseMemberTargetIds(targetValue).Contains(userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyCollection<Guid> ParseMemberTargetIds(string? targetValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetValue
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(value => Guid.TryParse(value, out Guid memberUserId) ? memberUserId : Guid.Empty)
|
||||||
|
.Where(memberUserId => memberUserId != Guid.Empty)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatMemberTargetValue(IEnumerable<Guid> memberUserIds)
|
||||||
|
{
|
||||||
|
return string.Join(",", memberUserIds.Distinct().OrderBy(memberUserId => memberUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesMembershipTarget(
|
||||||
|
IReadOnlyCollection<string> userRoles,
|
||||||
|
string targetValue)
|
||||||
|
{
|
||||||
|
return targetValue switch
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Client => userRoles.Contains(KnownRoles.Client),
|
||||||
|
ApprovalMembershipTargets.Team => !userRoles.Contains(KnownRoles.Client),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
public record ApprovalWorkflowStartResult(bool Succeeded, string? ErrorMessage);
|
||||||
|
|
||||||
|
public record ApprovalWorkflowDecisionResult(
|
||||||
|
bool Succeeded,
|
||||||
|
string? ErrorMessage,
|
||||||
|
int StatusCode,
|
||||||
|
bool IsWorkflowStep);
|
||||||
|
|
||||||
|
public class ApprovalWorkflowRuntimeService(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
INotificationEventWriter notificationEventWriter)
|
||||||
|
{
|
||||||
|
private const string PendingState = "Pending";
|
||||||
|
private const string ApprovedState = "Approved";
|
||||||
|
|
||||||
|
public async Task<ApprovalWorkflowStartResult> StartMultiLevelWorkflowAsync(
|
||||||
|
ContentItem contentItem,
|
||||||
|
Workspace workspace,
|
||||||
|
Guid requestedByUserId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (workspace.ApprovalMode != ApprovalModes.MultiLevel)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowStartResult(false, "The workspace is not configured for multi-level approval.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ApprovalWorkflowInstance? activeWorkflow = await dbContext.ApprovalWorkflowInstances
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
workflow => workflow.ContentItemId == contentItem.Id && workflow.State == PendingState,
|
||||||
|
ct);
|
||||||
|
if (activeWorkflow is not null)
|
||||||
|
{
|
||||||
|
contentItem.Status = "In approval";
|
||||||
|
return new ApprovalWorkflowStartResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WorkspaceApprovalStepConfiguration> configuredSteps = await dbContext.WorkspaceApprovalStepConfigurations
|
||||||
|
.Where(step => step.WorkspaceId == workspace.Id)
|
||||||
|
.OrderBy(step => step.SortOrder)
|
||||||
|
.ThenBy(step => step.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
if (configuredSteps.Count == 0)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowStartResult(false, "Multi-level approval requires at least one configured approval step.");
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
var workflowInstance = new ApprovalWorkflowInstance
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
WorkspaceId = workspace.Id,
|
||||||
|
ContentItemId = contentItem.Id,
|
||||||
|
State = PendingState,
|
||||||
|
ApprovalMode = workspace.ApprovalMode,
|
||||||
|
StartedAt = now,
|
||||||
|
};
|
||||||
|
|
||||||
|
List<ApprovalRequest> workflowSteps = configuredSteps
|
||||||
|
.Select((step, index) => new ApprovalRequest
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
WorkspaceId = workspace.Id,
|
||||||
|
ContentItemId = contentItem.Id,
|
||||||
|
WorkflowInstanceId = workflowInstance.Id,
|
||||||
|
WorkflowStepSortOrder = index,
|
||||||
|
WorkflowStepTargetType = step.TargetType,
|
||||||
|
WorkflowStepTargetValue = step.TargetValue,
|
||||||
|
WorkflowStepRequiredApproverCount = step.RequiredApproverCount,
|
||||||
|
Stage = step.Name,
|
||||||
|
ReviewerName = FormatStepTarget(step),
|
||||||
|
ReviewerEmail = string.Empty,
|
||||||
|
RequestedByUserId = requestedByUserId,
|
||||||
|
DueAt = contentItem.DueDate,
|
||||||
|
State = PendingState,
|
||||||
|
AccessToken = CreateAccessToken(),
|
||||||
|
SentAt = now,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
dbContext.ApprovalWorkflowInstances.Add(workflowInstance);
|
||||||
|
dbContext.ApprovalRequests.AddRange(workflowSteps);
|
||||||
|
contentItem.Status = "In approval";
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
await NotifyCurrentStepApproversAsync(workflowSteps[0], contentItem, ct);
|
||||||
|
|
||||||
|
return new ApprovalWorkflowStartResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApprovalWorkflowDecisionResult> ApplyWorkflowStepDecisionAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
Workspace workspace,
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
ApprovalDecision decision,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!approval.WorkflowInstanceId.HasValue)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Identity?.IsAuthenticated != true)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "Multi-level approval steps require an authenticated approver.", StatusCodes.Status401Unauthorized, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await CanApproveStepAsync(user, approval, workspace.Id, ct))
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "You cannot approve the current workflow step.", StatusCodes.Status403Forbidden, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApprovalRequest? currentStep = await GetCurrentPendingStepAsync(approval.WorkflowInstanceId.Value, ct);
|
||||||
|
if (currentStep?.Id != approval.Id)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "Only the current pending approval step can be approved.", StatusCodes.Status409Conflict, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid currentUserId = user.GetUserId();
|
||||||
|
bool alreadyApproved = await dbContext.ApprovalDecisions.AnyAsync(
|
||||||
|
candidate => candidate.ApprovalRequestId == approval.Id &&
|
||||||
|
candidate.DecidedByUserId == currentUserId &&
|
||||||
|
candidate.Decision == ApprovedState,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (alreadyApproved)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "You have already approved this workflow step.", StatusCodes.Status409Conflict, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.ApprovalDecisions.Add(decision);
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
int approvedCount = await dbContext.ApprovalDecisions
|
||||||
|
.Where(candidate => candidate.ApprovalRequestId == approval.Id && candidate.Decision == ApprovedState)
|
||||||
|
.Select(candidate => candidate.DecidedByUserId.HasValue
|
||||||
|
? candidate.DecidedByUserId.Value.ToString()
|
||||||
|
: candidate.DecidedByEmail.ToLower())
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync(ct);
|
||||||
|
|
||||||
|
int requiredApproverCount = approval.WorkflowStepRequiredApproverCount ?? 1;
|
||||||
|
if (!ApprovalWorkflowRules.HasRequiredStepApprovals(approvedCount, requiredApproverCount))
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
approval.State = ApprovedState;
|
||||||
|
approval.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
ApprovalRequest? nextStep = await dbContext.ApprovalRequests
|
||||||
|
.Where(candidate => candidate.WorkflowInstanceId == approval.WorkflowInstanceId &&
|
||||||
|
candidate.State == PendingState &&
|
||||||
|
candidate.Id != approval.Id)
|
||||||
|
.OrderBy(candidate => candidate.WorkflowStepSortOrder)
|
||||||
|
.ThenBy(candidate => candidate.SentAt)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
if (nextStep is null)
|
||||||
|
{
|
||||||
|
ApprovalWorkflowInstance? workflowInstance = await dbContext.ApprovalWorkflowInstances
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == approval.WorkflowInstanceId.Value, ct);
|
||||||
|
if (workflowInstance is null)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "The approval workflow instance could not be found.", StatusCodes.Status404NotFound, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowInstance.State = ApprovedState;
|
||||||
|
workflowInstance.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
contentItem.Status = ApprovalWorkflowRules.GetFinalApprovalStatus(
|
||||||
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
|
contentItem.DueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
if (nextStep is null)
|
||||||
|
{
|
||||||
|
await NotifyPublishUsersAsync(approval, contentItem, ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await NotifyCurrentStepApproversAsync(nextStep, contentItem, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasCompletedMultiLevelWorkflowAsync(Guid contentItemId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await dbContext.ApprovalWorkflowInstances.AnyAsync(
|
||||||
|
workflow => workflow.ContentItemId == contentItemId && workflow.State == ApprovedState,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ApprovalRequest?> GetCurrentPendingStepAsync(Guid workflowInstanceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await dbContext.ApprovalRequests
|
||||||
|
.Where(candidate => candidate.WorkflowInstanceId == workflowInstanceId && candidate.State == PendingState)
|
||||||
|
.OrderBy(candidate => candidate.WorkflowStepSortOrder)
|
||||||
|
.ThenBy(candidate => candidate.SentAt)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CanApproveStepAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
ApprovalRequest approval,
|
||||||
|
Guid workspaceId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
Guid userId = user.GetUserId();
|
||||||
|
bool hasWorkspaceAccess = await UserHasWorkspaceAccessAsync(userId, workspaceId, ct);
|
||||||
|
string[] userRoles = ApprovalStepConfigurationRules.AllowedRoleTargets
|
||||||
|
.Where(user.IsInRole)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return ApprovalWorkflowRules.CanApproveWorkflowStep(
|
||||||
|
user.IsInRole(KnownRoles.Administrator),
|
||||||
|
hasWorkspaceAccess,
|
||||||
|
userRoles,
|
||||||
|
userId,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> UserHasWorkspaceAccessAsync(Guid userId, Guid workspaceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
string workspaceClaimValue = workspaceId.ToString();
|
||||||
|
return await dbContext.UserClaims.AnyAsync(
|
||||||
|
claim => claim.UserId == userId &&
|
||||||
|
claim.ClaimType == KnownClaims.WorkspaceScope &&
|
||||||
|
claim.ClaimValue == workspaceClaimValue,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyCurrentStepApproversAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
List<ApprovalNotificationRecipient> recipients = await GetStepApproverRecipientsAsync(approval, ct);
|
||||||
|
|
||||||
|
foreach (ApprovalNotificationRecipient recipient in recipients)
|
||||||
|
{
|
||||||
|
await notificationEventWriter.WriteAsync(
|
||||||
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.step.current",
|
||||||
|
"ApprovalRequest",
|
||||||
|
approval.Id,
|
||||||
|
$"{approval.Stage} approval is ready for {contentItem.Title}.",
|
||||||
|
recipient.UserId,
|
||||||
|
recipient.Email,
|
||||||
|
$$"""{"stage":"{{approval.Stage}}","requiredApproverCount":{{approval.WorkflowStepRequiredApproverCount ?? 1}}}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyPublishUsersAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
List<ApprovalNotificationRecipient> recipients = await GetPublishRecipientUsersAsync(approval.WorkspaceId, ct);
|
||||||
|
|
||||||
|
foreach (ApprovalNotificationRecipient recipient in recipients)
|
||||||
|
{
|
||||||
|
await notificationEventWriter.WriteAsync(
|
||||||
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.workflow.completed",
|
||||||
|
"ApprovalWorkflowInstance",
|
||||||
|
approval.WorkflowInstanceId!.Value,
|
||||||
|
$"Final approval completed for {contentItem.Title}.",
|
||||||
|
recipient.UserId,
|
||||||
|
recipient.Email,
|
||||||
|
$$"""{"status":"{{contentItem.Status}}"}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetStepApproverRecipientsAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string? targetType = approval.WorkflowStepTargetType;
|
||||||
|
string? targetValue = approval.WorkflowStepTargetValue;
|
||||||
|
if (string.IsNullOrWhiteSpace(targetType) || string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Member => await GetMemberRecipientsAsync(targetValue, ct),
|
||||||
|
ApprovalStepTargetTypes.Role => await GetRoleRecipientsAsync(approval.WorkspaceId, [targetValue], ct),
|
||||||
|
ApprovalStepTargetTypes.Membership => await GetMembershipRecipientsAsync(approval.WorkspaceId, targetValue, ct),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetMemberRecipientsAsync(string targetValue, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<Guid> userIds = ApprovalWorkflowRules.ParseMemberTargetIds(targetValue);
|
||||||
|
if (userIds.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dbContext.Users
|
||||||
|
.Where(user => userIds.Contains(user.Id))
|
||||||
|
.Select(user => new ApprovalNotificationRecipient(user.Id, user.Email))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetMembershipRecipientsAsync(
|
||||||
|
Guid workspaceId,
|
||||||
|
string targetValue,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string[] roles = targetValue switch
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Client => [KnownRoles.Client],
|
||||||
|
ApprovalMembershipTargets.Team => [KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember, KnownRoles.Provider],
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return roles.Length == 0
|
||||||
|
? []
|
||||||
|
: await GetRoleRecipientsAsync(workspaceId, roles, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetPublishRecipientUsersAsync(Guid workspaceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await GetRoleRecipientsAsync(workspaceId, [KnownRoles.Administrator, KnownRoles.Manager], ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetRoleRecipientsAsync(
|
||||||
|
Guid workspaceId,
|
||||||
|
IReadOnlyCollection<string> roles,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string workspaceClaimValue = workspaceId.ToString();
|
||||||
|
|
||||||
|
return await dbContext.UserRoles
|
||||||
|
.Join(
|
||||||
|
dbContext.Roles,
|
||||||
|
userRole => userRole.RoleId,
|
||||||
|
role => role.Id,
|
||||||
|
(userRole, role) => new { userRole.UserId, RoleName = role.Name })
|
||||||
|
.Where(candidate => candidate.RoleName != null && roles.Contains(candidate.RoleName))
|
||||||
|
.Join(
|
||||||
|
dbContext.UserClaims.Where(claim =>
|
||||||
|
claim.ClaimType == KnownClaims.WorkspaceScope &&
|
||||||
|
claim.ClaimValue == workspaceClaimValue),
|
||||||
|
candidate => candidate.UserId,
|
||||||
|
claim => claim.UserId,
|
||||||
|
(candidate, _) => candidate.UserId)
|
||||||
|
.Distinct()
|
||||||
|
.Join(
|
||||||
|
dbContext.Users,
|
||||||
|
userId => userId,
|
||||||
|
user => user.Id,
|
||||||
|
(_, user) => new ApprovalNotificationRecipient(user.Id, user.Email))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatStepTarget(WorkspaceApprovalStepConfiguration step)
|
||||||
|
{
|
||||||
|
return step.TargetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role => $"Role: {step.TargetValue}",
|
||||||
|
ApprovalStepTargetTypes.Membership => $"Membership: {step.TargetValue}",
|
||||||
|
ApprovalStepTargetTypes.Member => "Assigned members",
|
||||||
|
_ => step.TargetValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateAccessToken()
|
||||||
|
{
|
||||||
|
return Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record ApprovalNotificationRecipient(Guid UserId, string? Email);
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ public class CreateAssetRevisionHandler(
|
|||||||
.SingleOrDefaultAsync(candidate => candidate.Id == asset.ContentItemId, ct);
|
.SingleOrDefaultAsync(candidate => candidate.Id == asset.ContentItemId, ct);
|
||||||
|
|
||||||
if (contentItem is not null &&
|
if (contentItem is not null &&
|
||||||
!accessScopeService.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
|
!accessScopeService.CanContributeToCampaign(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class CreateGoogleDriveAssetHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
|
if (!accessScopeService.CanContributeToCampaign(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class GetAssetsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Projects.Data;
|
namespace Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
|
||||||
public class Project
|
public class Campaign
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
|
||||||
|
public static class CampaignModelConfiguration
|
||||||
|
{
|
||||||
|
public static ModelBuilder ConfigureCampaignsModule(this ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<Campaign>(campaign =>
|
||||||
|
{
|
||||||
|
campaign.ToTable("Campaigns");
|
||||||
|
campaign.HasKey(x => x.Id);
|
||||||
|
campaign.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||||
|
campaign.Property(x => x.Description).HasMaxLength(4000);
|
||||||
|
campaign.Property(x => x.Notes).HasMaxLength(4000);
|
||||||
|
campaign.Property(x => x.Status).HasMaxLength(64).IsRequired();
|
||||||
|
campaign.Property(x => x.CreatedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
campaign.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
|
||||||
|
campaign.HasIndex(x => x.WorkspaceId);
|
||||||
|
campaign.HasIndex(x => x.ClientId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return modelBuilder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Campaigns;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddCampaignsModule(
|
||||||
|
this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,11 @@ using FastEndpoints;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Socialize.Api.Modules.Projects.Data;
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Projects.Handlers;
|
namespace Socialize.Api.Modules.Campaigns.Handlers;
|
||||||
|
|
||||||
public record CreateProjectRequest(
|
public record CreateCampaignRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ClientId,
|
Guid ClientId,
|
||||||
string Name,
|
string Name,
|
||||||
@@ -15,10 +15,10 @@ public record CreateProjectRequest(
|
|||||||
string? Description,
|
string? Description,
|
||||||
string? Notes);
|
string? Notes);
|
||||||
|
|
||||||
public class CreateProjectRequestValidator
|
public class CreateCampaignRequestValidator
|
||||||
: Validator<CreateProjectRequest>
|
: Validator<CreateCampaignRequest>
|
||||||
{
|
{
|
||||||
public CreateProjectRequestValidator()
|
public CreateCampaignRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.WorkspaceId).NotEmpty();
|
RuleFor(x => x.WorkspaceId).NotEmpty();
|
||||||
RuleFor(x => x.ClientId).NotEmpty();
|
RuleFor(x => x.ClientId).NotEmpty();
|
||||||
@@ -32,18 +32,18 @@ public class CreateProjectRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateProjectHandler(
|
public class CreateCampaignHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<CreateProjectRequest, ProjectDto>
|
: Endpoint<CreateCampaignRequest, CampaignDto>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
Post("/api/projects");
|
Post("/api/campaigns");
|
||||||
Options(o => o.WithTags("Projects"));
|
Options(o => o.WithTags("Campaigns"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task HandleAsync(CreateProjectRequest request, CancellationToken ct)
|
public override async Task HandleAsync(CreateCampaignRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!accessScopeService.CanManageWorkspace(User, request.WorkspaceId))
|
if (!accessScopeService.CanManageWorkspace(User, request.WorkspaceId))
|
||||||
{
|
{
|
||||||
@@ -75,19 +75,19 @@ public class CreateProjectHandler(
|
|||||||
|
|
||||||
string normalizedName = request.Name.Trim();
|
string normalizedName = request.Name.Trim();
|
||||||
|
|
||||||
bool duplicateProject = await dbContext.Projects
|
bool duplicateCampaign = await dbContext.Campaigns
|
||||||
.AnyAsync(
|
.AnyAsync(
|
||||||
project => project.ClientId == request.ClientId && project.Name == normalizedName,
|
campaign => campaign.ClientId == request.ClientId && campaign.Name == normalizedName,
|
||||||
ct);
|
ct);
|
||||||
|
|
||||||
if (duplicateProject)
|
if (duplicateCampaign)
|
||||||
{
|
{
|
||||||
AddError(request => request.Name, "A project with this name already exists for the selected client.");
|
AddError(request => request.Name, "A campaign with this name already exists for the selected client.");
|
||||||
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Project project = new()
|
Campaign campaign = new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
WorkspaceId = request.WorkspaceId,
|
WorkspaceId = request.WorkspaceId,
|
||||||
@@ -101,19 +101,19 @@ public class CreateProjectHandler(
|
|||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
dbContext.Projects.Add(project);
|
dbContext.Campaigns.Add(campaign);
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
ProjectDto dto = new(
|
CampaignDto dto = new(
|
||||||
project.Id,
|
campaign.Id,
|
||||||
project.WorkspaceId,
|
campaign.WorkspaceId,
|
||||||
project.ClientId,
|
campaign.ClientId,
|
||||||
project.Name,
|
campaign.Name,
|
||||||
project.Description,
|
campaign.Description,
|
||||||
project.Notes,
|
campaign.Notes,
|
||||||
project.Status,
|
campaign.Status,
|
||||||
project.StartDate,
|
campaign.StartDate,
|
||||||
project.EndDate);
|
campaign.EndDate);
|
||||||
|
|
||||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Campaigns.Handlers;
|
||||||
|
|
||||||
|
public record GetCampaignsRequest(Guid? WorkspaceId, Guid? ClientId);
|
||||||
|
|
||||||
|
public record CampaignDto(
|
||||||
|
Guid Id,
|
||||||
|
Guid WorkspaceId,
|
||||||
|
Guid ClientId,
|
||||||
|
string Name,
|
||||||
|
string? Description,
|
||||||
|
string? Notes,
|
||||||
|
string Status,
|
||||||
|
DateTimeOffset StartDate,
|
||||||
|
DateTimeOffset EndDate);
|
||||||
|
|
||||||
|
public class GetCampaignsHandler(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
AccessScopeService accessScopeService)
|
||||||
|
: Endpoint<GetCampaignsRequest, IReadOnlyCollection<CampaignDto>>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Get("/api/campaigns");
|
||||||
|
Options(o => o.WithTags("Campaigns"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(GetCampaignsRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IQueryable<Campaign> query = dbContext.Campaigns.AsQueryable();
|
||||||
|
|
||||||
|
if (accessScopeService.IsManager(User))
|
||||||
|
{
|
||||||
|
if (request.WorkspaceId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(campaign => campaign.WorkspaceId == request.WorkspaceId.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
|
||||||
|
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
||||||
|
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
|
||||||
|
|
||||||
|
query = query.Where(campaign => workspaceScopeIds.Contains(campaign.WorkspaceId));
|
||||||
|
|
||||||
|
if (clientScopeIds.Count > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(campaign => clientScopeIds.Contains(campaign.ClientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaignScopeIds.Count > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(campaign => campaignScopeIds.Contains(campaign.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ClientId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(campaign => campaign.ClientId == request.ClientId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.WorkspaceId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(campaign => campaign.WorkspaceId == request.WorkspaceId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CampaignDto> campaigns = await query
|
||||||
|
.OrderBy(campaign => campaign.Name)
|
||||||
|
.Select(campaign => new CampaignDto(
|
||||||
|
campaign.Id,
|
||||||
|
campaign.WorkspaceId,
|
||||||
|
campaign.ClientId,
|
||||||
|
campaign.Name,
|
||||||
|
campaign.Description,
|
||||||
|
campaign.Notes,
|
||||||
|
campaign.Status,
|
||||||
|
campaign.StartDate,
|
||||||
|
campaign.EndDate))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
await SendOkAsync(campaigns, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ public class CreateCommentHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
|
if (!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public class GetCommentsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class ResolveCommentHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool canResolve = accessScopeService.CanManageWorkspace(User, comment.WorkspaceId)
|
bool canResolve = accessScopeService.CanManageWorkspace(User, comment.WorkspaceId)
|
||||||
|| accessScopeService.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId);
|
|| accessScopeService.CanContributeToCampaign(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId);
|
||||||
|
|
||||||
if (!canResolve)
|
if (!canResolve)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ public class ContentItem
|
|||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
public Guid ClientId { get; set; }
|
public Guid ClientId { get; set; }
|
||||||
public Guid ProjectId { get; set; }
|
public Guid CampaignId { get; set; }
|
||||||
public required string Title { get; set; }
|
public required string Title { get; set; }
|
||||||
public required string PublicationMessage { get; set; }
|
public required string PublicationMessage { get; set; }
|
||||||
public required string PublicationTargets { get; set; }
|
public required string PublicationTargets { get; set; }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public static class ContentItemModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
contentItem.HasIndex(x => x.WorkspaceId);
|
contentItem.HasIndex(x => x.WorkspaceId);
|
||||||
contentItem.HasIndex(x => x.ClientId);
|
contentItem.HasIndex(x => x.ClientId);
|
||||||
contentItem.HasIndex(x => x.ProjectId);
|
contentItem.HasIndex(x => x.CampaignId);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ContentItemRevision>(revision =>
|
modelBuilder.Entity<ContentItemRevision>(revision =>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Socialize.Api.Modules.ContentItems.Handlers;
|
|||||||
public record CreateContentItemRequest(
|
public record CreateContentItemRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ClientId,
|
Guid ClientId,
|
||||||
Guid ProjectId,
|
Guid CampaignId,
|
||||||
string Title,
|
string Title,
|
||||||
string PublicationMessage,
|
string PublicationMessage,
|
||||||
string PublicationTargets,
|
string PublicationTargets,
|
||||||
@@ -25,7 +25,7 @@ public class CreateContentItemRequestValidator
|
|||||||
{
|
{
|
||||||
RuleFor(x => x.WorkspaceId).NotEmpty();
|
RuleFor(x => x.WorkspaceId).NotEmpty();
|
||||||
RuleFor(x => x.ClientId).NotEmpty();
|
RuleFor(x => x.ClientId).NotEmpty();
|
||||||
RuleFor(x => x.ProjectId).NotEmpty();
|
RuleFor(x => x.CampaignId).NotEmpty();
|
||||||
RuleFor(x => x.Title).NotEmpty().MaximumLength(256);
|
RuleFor(x => x.Title).NotEmpty().MaximumLength(256);
|
||||||
RuleFor(x => x.PublicationMessage).NotEmpty().MaximumLength(4000);
|
RuleFor(x => x.PublicationMessage).NotEmpty().MaximumLength(4000);
|
||||||
RuleFor(x => x.PublicationTargets).NotEmpty().MaximumLength(512);
|
RuleFor(x => x.PublicationTargets).NotEmpty().MaximumLength(512);
|
||||||
@@ -47,7 +47,7 @@ public class CreateContentItemHandler(
|
|||||||
|
|
||||||
public override async Task HandleAsync(CreateContentItemRequest request, CancellationToken ct)
|
public override async Task HandleAsync(CreateContentItemRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!accessScopeService.CanContributeToProject(User, request.WorkspaceId, request.ClientId, request.ProjectId))
|
if (!accessScopeService.CanContributeToCampaign(User, request.WorkspaceId, request.ClientId, request.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -75,16 +75,16 @@ public class CreateContentItemHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool projectExists = await dbContext.Projects
|
bool campaignExists = await dbContext.Campaigns
|
||||||
.AnyAsync(
|
.AnyAsync(
|
||||||
project => project.Id == request.ProjectId &&
|
campaign => campaign.Id == request.CampaignId &&
|
||||||
project.WorkspaceId == request.WorkspaceId &&
|
campaign.WorkspaceId == request.WorkspaceId &&
|
||||||
project.ClientId == request.ClientId,
|
campaign.ClientId == request.ClientId,
|
||||||
ct);
|
ct);
|
||||||
|
|
||||||
if (!projectExists)
|
if (!campaignExists)
|
||||||
{
|
{
|
||||||
AddError(request => request.ProjectId, "The selected project does not belong to the selected client.");
|
AddError(request => request.CampaignId, "The selected campaign does not belong to the selected client.");
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ public class CreateContentItemHandler(
|
|||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
WorkspaceId = request.WorkspaceId,
|
WorkspaceId = request.WorkspaceId,
|
||||||
ClientId = request.ClientId,
|
ClientId = request.ClientId,
|
||||||
ProjectId = request.ProjectId,
|
CampaignId = request.CampaignId,
|
||||||
Title = request.Title.Trim(),
|
Title = request.Title.Trim(),
|
||||||
PublicationMessage = request.PublicationMessage.Trim(),
|
PublicationMessage = request.PublicationMessage.Trim(),
|
||||||
PublicationTargets = request.PublicationTargets.Trim(),
|
PublicationTargets = request.PublicationTargets.Trim(),
|
||||||
@@ -138,7 +138,7 @@ public class CreateContentItemHandler(
|
|||||||
item.Id,
|
item.Id,
|
||||||
item.WorkspaceId,
|
item.WorkspaceId,
|
||||||
item.ClientId,
|
item.ClientId,
|
||||||
item.ProjectId,
|
item.CampaignId,
|
||||||
item.Title,
|
item.Title,
|
||||||
item.PublicationMessage,
|
item.PublicationMessage,
|
||||||
item.PublicationTargets,
|
item.PublicationTargets,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class CreateContentItemRevisionHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanContributeToProject(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!accessScopeService.CanContributeToCampaign(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -66,15 +66,6 @@ public class CreateContentItemRevisionHandler(
|
|||||||
item.CurrentRevisionNumber = revisionNumber;
|
item.CurrentRevisionNumber = revisionNumber;
|
||||||
item.CurrentRevisionLabel = revisionLabel;
|
item.CurrentRevisionLabel = revisionLabel;
|
||||||
|
|
||||||
if (item.Status == "Changes requested internally")
|
|
||||||
{
|
|
||||||
item.Status = "Internal changes in progress";
|
|
||||||
}
|
|
||||||
else if (item.Status == "Changes requested by client")
|
|
||||||
{
|
|
||||||
item.Status = "Client changes in progress";
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentItemRevision revision = new()
|
ContentItemRevision revision = new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public record ContentItemDetailDto(
|
|||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ClientId,
|
Guid ClientId,
|
||||||
Guid ProjectId,
|
Guid CampaignId,
|
||||||
string Title,
|
string Title,
|
||||||
string PublicationMessage,
|
string PublicationMessage,
|
||||||
string PublicationTargets,
|
string PublicationTargets,
|
||||||
@@ -42,7 +42,7 @@ public class GetContentItemHandler(
|
|||||||
candidate.Id,
|
candidate.Id,
|
||||||
candidate.WorkspaceId,
|
candidate.WorkspaceId,
|
||||||
candidate.ClientId,
|
candidate.ClientId,
|
||||||
candidate.ProjectId,
|
candidate.CampaignId,
|
||||||
candidate.Title,
|
candidate.Title,
|
||||||
candidate.PublicationMessage,
|
candidate.PublicationMessage,
|
||||||
candidate.PublicationTargets,
|
candidate.PublicationTargets,
|
||||||
@@ -60,7 +60,7 @@ public class GetContentItemHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class GetContentItemRevisionsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ using Socialize.Api.Modules.ContentItems.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
public record GetContentItemsRequest(Guid? WorkspaceId, Guid? ClientId, Guid? ProjectId);
|
public record GetContentItemsRequest(Guid? WorkspaceId, Guid? ClientId, Guid? CampaignId);
|
||||||
|
|
||||||
public record ContentItemDto(
|
public record ContentItemDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ClientId,
|
Guid ClientId,
|
||||||
Guid ProjectId,
|
Guid CampaignId,
|
||||||
string Title,
|
string Title,
|
||||||
string PublicationMessage,
|
string PublicationMessage,
|
||||||
string PublicationTargets,
|
string PublicationTargets,
|
||||||
@@ -41,7 +41,7 @@ public class GetContentItemsHandler(
|
|||||||
{
|
{
|
||||||
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
|
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
|
||||||
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
||||||
IReadOnlyCollection<Guid> projectScopeIds = User.GetProjectScopeIds();
|
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
|
||||||
|
|
||||||
query = query.Where(item => workspaceScopeIds.Contains(item.WorkspaceId));
|
query = query.Where(item => workspaceScopeIds.Contains(item.WorkspaceId));
|
||||||
|
|
||||||
@@ -50,9 +50,9 @@ public class GetContentItemsHandler(
|
|||||||
query = query.Where(item => clientScopeIds.Contains(item.ClientId));
|
query = query.Where(item => clientScopeIds.Contains(item.ClientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectScopeIds.Count > 0)
|
if (campaignScopeIds.Count > 0)
|
||||||
{
|
{
|
||||||
query = query.Where(item => projectScopeIds.Contains(item.ProjectId));
|
query = query.Where(item => campaignScopeIds.Contains(item.CampaignId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +61,9 @@ public class GetContentItemsHandler(
|
|||||||
query = query.Where(item => item.WorkspaceId == request.WorkspaceId.Value);
|
query = query.Where(item => item.WorkspaceId == request.WorkspaceId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.ProjectId.HasValue)
|
if (request.CampaignId.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(item => item.ProjectId == request.ProjectId.Value);
|
query = query.Where(item => item.CampaignId == request.CampaignId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.ClientId.HasValue)
|
if (request.ClientId.HasValue)
|
||||||
@@ -78,7 +78,7 @@ public class GetContentItemsHandler(
|
|||||||
item.Id,
|
item.Id,
|
||||||
item.WorkspaceId,
|
item.WorkspaceId,
|
||||||
item.ClientId,
|
item.ClientId,
|
||||||
item.ProjectId,
|
item.CampaignId,
|
||||||
item.Title,
|
item.Title,
|
||||||
item.PublicationMessage,
|
item.PublicationMessage,
|
||||||
item.PublicationTargets,
|
item.PublicationTargets,
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ using FastEndpoints;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
@@ -21,24 +23,18 @@ public class UpdateContentItemStatusRequestValidator
|
|||||||
public class UpdateContentItemStatusHandler(
|
public class UpdateContentItemStatusHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<UpdateContentItemStatusRequest, ContentItemDetailDto>
|
: Endpoint<UpdateContentItemStatusRequest, ContentItemDetailDto>
|
||||||
{
|
{
|
||||||
private static readonly HashSet<string> AllowedStatuses =
|
private static readonly HashSet<string> AllowedStatuses =
|
||||||
[
|
[
|
||||||
"Draft",
|
"Draft",
|
||||||
"In internal review",
|
"In production",
|
||||||
"Changes requested internally",
|
"In approval",
|
||||||
"Internal changes in progress",
|
|
||||||
"Ready for client review",
|
|
||||||
"In client review",
|
|
||||||
"Changes requested by client",
|
|
||||||
"Client changes in progress",
|
|
||||||
"Approved",
|
"Approved",
|
||||||
"Rejected",
|
"Scheduled",
|
||||||
"Ready to publish",
|
|
||||||
"Published",
|
"Published",
|
||||||
"Archived",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -72,7 +68,64 @@ public class UpdateContentItemStatusHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Status = normalizedStatus;
|
Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == item.WorkspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedStatus == "In approval" && workspace.ApprovalMode == ApprovalModes.MultiLevel)
|
||||||
|
{
|
||||||
|
ApprovalWorkflowStartResult startResult = await approvalWorkflowRuntimeService.StartMultiLevelWorkflowAsync(
|
||||||
|
item,
|
||||||
|
workspace,
|
||||||
|
User.GetUserId(),
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (!startResult.Succeeded)
|
||||||
|
{
|
||||||
|
AddError(request => request.Status, startResult.ErrorMessage ?? "The approval workflow could not be started.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ApprovalWorkflowRules.IsApprovalCompletionStatus(normalizedStatus) &&
|
||||||
|
ApprovalWorkflowRules.BlocksManualApprovedOrScheduledStatus(workspace.ApprovalMode))
|
||||||
|
{
|
||||||
|
if (workspace.ApprovalMode == ApprovalModes.MultiLevel)
|
||||||
|
{
|
||||||
|
bool hasCompletedWorkflow = await approvalWorkflowRuntimeService.HasCompletedMultiLevelWorkflowAsync(item.Id, ct);
|
||||||
|
|
||||||
|
if (!hasCompletedWorkflow)
|
||||||
|
{
|
||||||
|
AddError(request => request.Status, "This workspace requires the multi-level approval workflow to complete before content can be approved or scheduled.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool hasApprovedDecision = await dbContext.ApprovalRequests.AnyAsync(
|
||||||
|
approval => approval.ContentItemId == item.Id &&
|
||||||
|
approval.WorkspaceId == item.WorkspaceId &&
|
||||||
|
approval.State == "Approved" &&
|
||||||
|
approval.CompletedAt.HasValue,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (!hasApprovedDecision)
|
||||||
|
{
|
||||||
|
AddError(request => request.Status, "This workspace requires approval before content can be approved or scheduled.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Status != "In approval" || normalizedStatus != "In approval")
|
||||||
|
{
|
||||||
|
item.Status = normalizedStatus;
|
||||||
|
}
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
await notificationEventWriter.WriteAsync(
|
await notificationEventWriter.WriteAsync(
|
||||||
@@ -92,7 +145,7 @@ public class UpdateContentItemStatusHandler(
|
|||||||
item.Id,
|
item.Id,
|
||||||
item.WorkspaceId,
|
item.WorkspaceId,
|
||||||
item.ClientId,
|
item.ClientId,
|
||||||
item.ProjectId,
|
item.CampaignId,
|
||||||
item.Title,
|
item.Title,
|
||||||
item.PublicationMessage,
|
item.PublicationMessage,
|
||||||
item.PublicationTargets,
|
item.PublicationTargets,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ public record FeedbackContextDto(
|
|||||||
string? WorkspaceName,
|
string? WorkspaceName,
|
||||||
Guid? ClientId,
|
Guid? ClientId,
|
||||||
string? ClientName,
|
string? ClientName,
|
||||||
Guid? ProjectId,
|
Guid? CampaignId,
|
||||||
string? ProjectName,
|
string? CampaignName,
|
||||||
Guid? ContentItemId,
|
Guid? ContentItemId,
|
||||||
string? ContentItemTitle);
|
string? ContentItemTitle);
|
||||||
|
|
||||||
@@ -82,8 +82,8 @@ public static class FeedbackDtoMapper
|
|||||||
report.WorkspaceName,
|
report.WorkspaceName,
|
||||||
report.ClientId,
|
report.ClientId,
|
||||||
report.ClientName,
|
report.ClientName,
|
||||||
report.ProjectId,
|
report.CampaignId,
|
||||||
report.ProjectName,
|
report.CampaignName,
|
||||||
report.ContentItemId,
|
report.ContentItemId,
|
||||||
report.ContentItemTitle),
|
report.ContentItemTitle),
|
||||||
report.Screenshot is null
|
report.Screenshot is null
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public static class FeedbackModelConfiguration
|
|||||||
feedback.Property(x => x.AppVersion).HasMaxLength(128);
|
feedback.Property(x => x.AppVersion).HasMaxLength(128);
|
||||||
feedback.Property(x => x.WorkspaceName).HasMaxLength(256);
|
feedback.Property(x => x.WorkspaceName).HasMaxLength(256);
|
||||||
feedback.Property(x => x.ClientName).HasMaxLength(256);
|
feedback.Property(x => x.ClientName).HasMaxLength(256);
|
||||||
feedback.Property(x => x.ProjectName).HasMaxLength(256);
|
feedback.Property(x => x.CampaignName).HasMaxLength(256);
|
||||||
feedback.Property(x => x.ContentItemTitle).HasMaxLength(256);
|
feedback.Property(x => x.ContentItemTitle).HasMaxLength(256);
|
||||||
feedback.Property(x => x.CancellationReason).HasMaxLength(2000);
|
feedback.Property(x => x.CancellationReason).HasMaxLength(2000);
|
||||||
feedback.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
feedback.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ public class FeedbackReport
|
|||||||
public string? WorkspaceName { get; set; }
|
public string? WorkspaceName { get; set; }
|
||||||
public Guid? ClientId { get; set; }
|
public Guid? ClientId { get; set; }
|
||||||
public string? ClientName { get; set; }
|
public string? ClientName { get; set; }
|
||||||
public Guid? ProjectId { get; set; }
|
public Guid? CampaignId { get; set; }
|
||||||
public string? ProjectName { get; set; }
|
public string? CampaignName { get; set; }
|
||||||
public Guid? ContentItemId { get; set; }
|
public Guid? ContentItemId { get; set; }
|
||||||
public string? ContentItemTitle { get; set; }
|
public string? ContentItemTitle { get; set; }
|
||||||
public DateTimeOffset CreatedAt { get; set; }
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ public record SubmitFeedbackRequest(
|
|||||||
string? WorkspaceName,
|
string? WorkspaceName,
|
||||||
Guid? ClientId,
|
Guid? ClientId,
|
||||||
string? ClientName,
|
string? ClientName,
|
||||||
Guid? ProjectId,
|
Guid? CampaignId,
|
||||||
string? ProjectName,
|
string? CampaignName,
|
||||||
Guid? ContentItemId,
|
Guid? ContentItemId,
|
||||||
string? ContentItemTitle);
|
string? ContentItemTitle);
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ public class SubmitFeedbackRequestValidator
|
|||||||
RuleFor(x => x.AppVersion).MaximumLength(128);
|
RuleFor(x => x.AppVersion).MaximumLength(128);
|
||||||
RuleFor(x => x.WorkspaceName).MaximumLength(256);
|
RuleFor(x => x.WorkspaceName).MaximumLength(256);
|
||||||
RuleFor(x => x.ClientName).MaximumLength(256);
|
RuleFor(x => x.ClientName).MaximumLength(256);
|
||||||
RuleFor(x => x.ProjectName).MaximumLength(256);
|
RuleFor(x => x.CampaignName).MaximumLength(256);
|
||||||
RuleFor(x => x.ContentItemTitle).MaximumLength(256);
|
RuleFor(x => x.ContentItemTitle).MaximumLength(256);
|
||||||
RuleFor(x => x.ViewportWidth).GreaterThan(0).When(x => x.ViewportWidth.HasValue);
|
RuleFor(x => x.ViewportWidth).GreaterThan(0).When(x => x.ViewportWidth.HasValue);
|
||||||
RuleFor(x => x.ViewportHeight).GreaterThan(0).When(x => x.ViewportHeight.HasValue);
|
RuleFor(x => x.ViewportHeight).GreaterThan(0).When(x => x.ViewportHeight.HasValue);
|
||||||
@@ -82,8 +82,8 @@ public class SubmitFeedbackHandler(
|
|||||||
WorkspaceName = NormalizeOptional(request.WorkspaceName),
|
WorkspaceName = NormalizeOptional(request.WorkspaceName),
|
||||||
ClientId = request.ClientId,
|
ClientId = request.ClientId,
|
||||||
ClientName = NormalizeOptional(request.ClientName),
|
ClientName = NormalizeOptional(request.ClientName),
|
||||||
ProjectId = request.ProjectId,
|
CampaignId = request.CampaignId,
|
||||||
ProjectName = NormalizeOptional(request.ProjectName),
|
CampaignName = NormalizeOptional(request.CampaignName),
|
||||||
ContentItemId = request.ContentItemId,
|
ContentItemId = request.ContentItemId,
|
||||||
ContentItemTitle = NormalizeOptional(request.ContentItemTitle),
|
ContentItemTitle = NormalizeOptional(request.ContentItemTitle),
|
||||||
CreatedAt = now,
|
CreatedAt = now,
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ public class GetCurrentUserQueryHandler(
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
List<Guid> projectIds = claims
|
List<Guid> campaignIds = claims
|
||||||
.Where(claim => claim.Type == KnownClaims.ProjectScope)
|
.Where(claim => claim.Type == KnownClaims.CampaignScope)
|
||||||
.Select(claim => Guid.TryParse(claim.Value, out Guid id) ? id : Guid.Empty)
|
.Select(claim => Guid.TryParse(claim.Value, out Guid id) ? id : Guid.Empty)
|
||||||
.Where(id => id != Guid.Empty)
|
.Where(id => id != Guid.Empty)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
@@ -64,7 +64,7 @@ public class GetCurrentUserQueryHandler(
|
|||||||
Persona = persona,
|
Persona = persona,
|
||||||
AuthorizedWorkspaceIds = workspaceIds,
|
AuthorizedWorkspaceIds = workspaceIds,
|
||||||
AuthorizedClientIds = clientIds,
|
AuthorizedClientIds = clientIds,
|
||||||
AuthorizedProjectIds = projectIds,
|
AuthorizedCampaignIds = campaignIds,
|
||||||
Alias = userModel.Alias,
|
Alias = userModel.Alias,
|
||||||
PortraitUrl = userModel.PortraitUrl,
|
PortraitUrl = userModel.PortraitUrl,
|
||||||
Firstname = userModel.Firstname,
|
Firstname = userModel.Firstname,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class UserDto
|
|||||||
public string? Persona { get; init; }
|
public string? Persona { get; init; }
|
||||||
public IList<Guid> AuthorizedWorkspaceIds { get; init; } = [];
|
public IList<Guid> AuthorizedWorkspaceIds { get; init; } = [];
|
||||||
public IList<Guid> AuthorizedClientIds { get; init; } = [];
|
public IList<Guid> AuthorizedClientIds { get; init; } = [];
|
||||||
public IList<Guid> AuthorizedProjectIds { get; init; } = [];
|
public IList<Guid> AuthorizedCampaignIds { get; init; } = [];
|
||||||
public string Username { get; init; } = null!;
|
public string Username { get; init; } = null!;
|
||||||
public string? Alias { get; init; }
|
public string? Alias { get; init; }
|
||||||
public string? PortraitUrl { get; init; }
|
public string? PortraitUrl { get; init; }
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class GetNotificationsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Projects.Data;
|
|
||||||
|
|
||||||
public static class ProjectModelConfiguration
|
|
||||||
{
|
|
||||||
public static ModelBuilder ConfigureProjectsModule(this ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Project>(project =>
|
|
||||||
{
|
|
||||||
project.ToTable("Projects");
|
|
||||||
project.HasKey(x => x.Id);
|
|
||||||
project.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
|
||||||
project.Property(x => x.Description).HasMaxLength(4000);
|
|
||||||
project.Property(x => x.Notes).HasMaxLength(4000);
|
|
||||||
project.Property(x => x.Status).HasMaxLength(64).IsRequired();
|
|
||||||
project.Property(x => x.CreatedAt)
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
|
||||||
project.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
|
|
||||||
project.HasIndex(x => x.WorkspaceId);
|
|
||||||
project.HasIndex(x => x.ClientId);
|
|
||||||
});
|
|
||||||
|
|
||||||
return modelBuilder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Socialize.Api.Modules.Projects.Data;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Projects;
|
|
||||||
|
|
||||||
public static class DependencyInjection
|
|
||||||
{
|
|
||||||
public static WebApplicationBuilder AddProjectsModule(
|
|
||||||
this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using FastEndpoints;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Socialize.Api.Data;
|
|
||||||
using Socialize.Api.Infrastructure.Security;
|
|
||||||
using Socialize.Api.Modules.Projects.Data;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Projects.Handlers;
|
|
||||||
|
|
||||||
public record GetProjectsRequest(Guid? WorkspaceId, Guid? ClientId);
|
|
||||||
|
|
||||||
public record ProjectDto(
|
|
||||||
Guid Id,
|
|
||||||
Guid WorkspaceId,
|
|
||||||
Guid ClientId,
|
|
||||||
string Name,
|
|
||||||
string? Description,
|
|
||||||
string? Notes,
|
|
||||||
string Status,
|
|
||||||
DateTimeOffset StartDate,
|
|
||||||
DateTimeOffset EndDate);
|
|
||||||
|
|
||||||
public class GetProjectsHandler(
|
|
||||||
AppDbContext dbContext,
|
|
||||||
AccessScopeService accessScopeService)
|
|
||||||
: Endpoint<GetProjectsRequest, IReadOnlyCollection<ProjectDto>>
|
|
||||||
{
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Get("/api/projects");
|
|
||||||
Options(o => o.WithTags("Projects"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(GetProjectsRequest request, CancellationToken ct)
|
|
||||||
{
|
|
||||||
IQueryable<Project> query = dbContext.Projects.AsQueryable();
|
|
||||||
|
|
||||||
if (accessScopeService.IsManager(User))
|
|
||||||
{
|
|
||||||
if (request.WorkspaceId.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(project => project.WorkspaceId == request.WorkspaceId.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
|
|
||||||
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
|
||||||
IReadOnlyCollection<Guid> projectScopeIds = User.GetProjectScopeIds();
|
|
||||||
|
|
||||||
query = query.Where(project => workspaceScopeIds.Contains(project.WorkspaceId));
|
|
||||||
|
|
||||||
if (clientScopeIds.Count > 0)
|
|
||||||
{
|
|
||||||
query = query.Where(project => clientScopeIds.Contains(project.ClientId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectScopeIds.Count > 0)
|
|
||||||
{
|
|
||||||
query = query.Where(project => projectScopeIds.Contains(project.Id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.ClientId.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(project => project.ClientId == request.ClientId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.WorkspaceId.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(project => project.WorkspaceId == request.WorkspaceId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ProjectDto> projects = await query
|
|
||||||
.OrderBy(project => project.Name)
|
|
||||||
.Select(project => new ProjectDto(
|
|
||||||
project.Id,
|
|
||||||
project.WorkspaceId,
|
|
||||||
project.ClientId,
|
|
||||||
project.Name,
|
|
||||||
project.Description,
|
|
||||||
project.Notes,
|
|
||||||
project.Status,
|
|
||||||
project.StartDate,
|
|
||||||
project.EndDate))
|
|
||||||
.ToListAsync(ct);
|
|
||||||
|
|
||||||
await SendOkAsync(projects, ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,5 +8,9 @@ public class Workspace
|
|||||||
public string? LogoUrl { get; set; }
|
public string? LogoUrl { get; set; }
|
||||||
public Guid OwnerUserId { get; set; }
|
public Guid OwnerUserId { get; set; }
|
||||||
public required string TimeZone { get; set; }
|
public required string TimeZone { get; set; }
|
||||||
|
public string ApprovalMode { get; set; } = "Required";
|
||||||
|
public bool SchedulePostsAutomaticallyOnApproval { get; set; }
|
||||||
|
public bool LockContentAfterApproval { get; set; }
|
||||||
|
public bool SendAutomaticApprovalReminders { get; set; }
|
||||||
public DateTimeOffset CreatedAt { get; init; }
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ public static class WorkspaceModelConfiguration
|
|||||||
workspace.Property(x => x.Slug).HasMaxLength(128).IsRequired();
|
workspace.Property(x => x.Slug).HasMaxLength(128).IsRequired();
|
||||||
workspace.Property(x => x.LogoUrl).HasMaxLength(2048);
|
workspace.Property(x => x.LogoUrl).HasMaxLength(2048);
|
||||||
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
|
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
|
||||||
|
workspace.Property(x => x.ApprovalMode).HasMaxLength(32).IsRequired().HasDefaultValue("Required");
|
||||||
|
workspace.Property(x => x.SchedulePostsAutomaticallyOnApproval).HasDefaultValue(false);
|
||||||
|
workspace.Property(x => x.LockContentAfterApproval).HasDefaultValue(false);
|
||||||
|
workspace.Property(x => x.SendAutomaticApprovalReminders).HasDefaultValue(false);
|
||||||
workspace.Property(x => x.CreatedAt)
|
workspace.Property(x => x.CreatedAt)
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ public class CreateWorkspaceHandler(
|
|||||||
workspace.Slug,
|
workspace.Slug,
|
||||||
workspace.LogoUrl,
|
workspace.LogoUrl,
|
||||||
workspace.TimeZone,
|
workspace.TimeZone,
|
||||||
|
workspace.ApprovalMode,
|
||||||
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
|
workspace.LockContentAfterApproval,
|
||||||
|
workspace.SendAutomaticApprovalReminders,
|
||||||
|
[],
|
||||||
workspace.CreatedAt);
|
workspace.CreatedAt);
|
||||||
|
|
||||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
||||||
|
|||||||
@@ -2,16 +2,32 @@ using FastEndpoints;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
||||||
|
|
||||||
|
public record ApprovalStepConfigurationDto(
|
||||||
|
Guid Id,
|
||||||
|
Guid WorkspaceId,
|
||||||
|
string Name,
|
||||||
|
int SortOrder,
|
||||||
|
string TargetType,
|
||||||
|
string TargetValue,
|
||||||
|
int RequiredApproverCount,
|
||||||
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
public record WorkspaceDto(
|
public record WorkspaceDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
string Name,
|
string Name,
|
||||||
string Slug,
|
string Slug,
|
||||||
string? LogoUrl,
|
string? LogoUrl,
|
||||||
string TimeZone,
|
string TimeZone,
|
||||||
|
string ApprovalMode,
|
||||||
|
bool SchedulePostsAutomaticallyOnApproval,
|
||||||
|
bool LockContentAfterApproval,
|
||||||
|
bool SendAutomaticApprovalReminders,
|
||||||
|
IReadOnlyCollection<ApprovalStepConfigurationDto> ApprovalSteps,
|
||||||
DateTimeOffset CreatedAt);
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
internal class GetWorkspacesHandler(
|
internal class GetWorkspacesHandler(
|
||||||
@@ -35,17 +51,53 @@ internal class GetWorkspacesHandler(
|
|||||||
query = query.Where(workspace => workspaceScopeIds.Contains(workspace.Id));
|
query = query.Where(workspace => workspaceScopeIds.Contains(workspace.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
var workspaces = await query
|
var workspaceRows = await query
|
||||||
.OrderBy(workspace => workspace.Name)
|
.OrderBy(workspace => workspace.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var workspaceIds = workspaceRows.Select(workspace => workspace.Id).ToList();
|
||||||
|
List<WorkspaceApprovalStepConfiguration> approvalStepRows = await dbContext.WorkspaceApprovalStepConfigurations
|
||||||
|
.Where(step => workspaceIds.Contains(step.WorkspaceId))
|
||||||
|
.OrderBy(step => step.SortOrder)
|
||||||
|
.ThenBy(step => step.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var approvalStepsByWorkspaceId = approvalStepRows
|
||||||
|
.GroupBy(step => step.WorkspaceId)
|
||||||
|
.ToDictionary(
|
||||||
|
group => group.Key,
|
||||||
|
group => (IReadOnlyCollection<ApprovalStepConfigurationDto>)group
|
||||||
|
.Select(ToApprovalStepConfigurationDto)
|
||||||
|
.ToArray());
|
||||||
|
|
||||||
|
var workspaces = workspaceRows
|
||||||
.Select(workspace => new WorkspaceDto(
|
.Select(workspace => new WorkspaceDto(
|
||||||
workspace.Id,
|
workspace.Id,
|
||||||
workspace.Name,
|
workspace.Name,
|
||||||
workspace.Slug,
|
workspace.Slug,
|
||||||
workspace.LogoUrl,
|
workspace.LogoUrl,
|
||||||
workspace.TimeZone,
|
workspace.TimeZone,
|
||||||
|
workspace.ApprovalMode,
|
||||||
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
|
workspace.LockContentAfterApproval,
|
||||||
|
workspace.SendAutomaticApprovalReminders,
|
||||||
|
approvalStepsByWorkspaceId.GetValueOrDefault(workspace.Id) ?? Array.Empty<ApprovalStepConfigurationDto>(),
|
||||||
workspace.CreatedAt))
|
workspace.CreatedAt))
|
||||||
.ToListAsync(ct);
|
.ToList();
|
||||||
|
|
||||||
await SendOkAsync(workspaces, ct);
|
await SendOkAsync(workspaces, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ApprovalStepConfigurationDto ToApprovalStepConfigurationDto(WorkspaceApprovalStepConfiguration step)
|
||||||
|
{
|
||||||
|
return new ApprovalStepConfigurationDto(
|
||||||
|
step.Id,
|
||||||
|
step.WorkspaceId,
|
||||||
|
step.Name,
|
||||||
|
step.SortOrder,
|
||||||
|
step.TargetType,
|
||||||
|
step.TargetValue,
|
||||||
|
step.RequiredApproverCount,
|
||||||
|
step.CreatedAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,52 @@ using FastEndpoints;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
||||||
|
|
||||||
|
public record UpdateApprovalStepConfigurationRequest(
|
||||||
|
string Name,
|
||||||
|
int SortOrder,
|
||||||
|
string TargetType,
|
||||||
|
string TargetValue,
|
||||||
|
int RequiredApproverCount);
|
||||||
|
|
||||||
public record UpdateWorkspaceRequest(
|
public record UpdateWorkspaceRequest(
|
||||||
string Name,
|
string Name,
|
||||||
string TimeZone);
|
string TimeZone,
|
||||||
|
string? ApprovalMode,
|
||||||
|
bool? SchedulePostsAutomaticallyOnApproval,
|
||||||
|
bool? LockContentAfterApproval,
|
||||||
|
bool? SendAutomaticApprovalReminders,
|
||||||
|
IReadOnlyCollection<UpdateApprovalStepConfigurationRequest>? ApprovalSteps);
|
||||||
|
|
||||||
public class UpdateWorkspaceRequestValidator
|
public class UpdateWorkspaceRequestValidator
|
||||||
: Validator<UpdateWorkspaceRequest>
|
: Validator<UpdateWorkspaceRequest>
|
||||||
{
|
{
|
||||||
|
private static readonly string[] AllowedApprovalModes = ["None", "Optional", "Required", "Multi-level"];
|
||||||
|
|
||||||
public UpdateWorkspaceRequestValidator()
|
public UpdateWorkspaceRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||||
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
||||||
|
RuleFor(x => x.ApprovalMode)
|
||||||
|
.Must(mode => string.IsNullOrWhiteSpace(mode) || AllowedApprovalModes.Contains(mode.Trim()))
|
||||||
|
.WithMessage("A valid approval mode should be specified.");
|
||||||
|
RuleFor(x => x.ApprovalSteps)
|
||||||
|
.Must(steps => steps is null || steps.Select(step => step.SortOrder).Distinct().Count() == steps.Count)
|
||||||
|
.WithMessage("Approval step sort orders must be unique.");
|
||||||
|
RuleForEach(x => x.ApprovalSteps).ChildRules(step =>
|
||||||
|
{
|
||||||
|
step.RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||||
|
step.RuleFor(x => x.TargetType)
|
||||||
|
.Must(ApprovalStepConfigurationRules.IsValidTargetType)
|
||||||
|
.WithMessage("A valid approval step target type should be specified.");
|
||||||
|
step.RuleFor(x => x.TargetValue).NotEmpty().MaximumLength(128);
|
||||||
|
step.RuleFor(x => x.RequiredApproverCount).GreaterThanOrEqualTo(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,19 +79,162 @@ public class UpdateWorkspaceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string nextApprovalMode = string.IsNullOrWhiteSpace(request.ApprovalMode)
|
||||||
|
? workspace.ApprovalMode
|
||||||
|
: request.ApprovalMode.Trim();
|
||||||
|
List<UpdateApprovalStepConfigurationRequest>? requestedApprovalSteps = request.ApprovalSteps?.ToList();
|
||||||
|
|
||||||
|
if (nextApprovalMode == ApprovalModes.MultiLevel)
|
||||||
|
{
|
||||||
|
bool hasConfiguredSteps = requestedApprovalSteps is null
|
||||||
|
? await dbContext.WorkspaceApprovalStepConfigurations.AnyAsync(step => step.WorkspaceId == workspace.Id, ct)
|
||||||
|
: requestedApprovalSteps.Count > 0;
|
||||||
|
|
||||||
|
if (!hasConfiguredSteps)
|
||||||
|
{
|
||||||
|
AddError(request => request.ApprovalSteps, "Multi-level approval requires at least one approval step.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedApprovalSteps is not null &&
|
||||||
|
!await ValidateApprovalStepsAsync(workspace.Id, requestedApprovalSteps, ct))
|
||||||
|
{
|
||||||
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
workspace.Name = request.Name.Trim();
|
workspace.Name = request.Name.Trim();
|
||||||
workspace.TimeZone = request.TimeZone.Trim();
|
workspace.TimeZone = request.TimeZone.Trim();
|
||||||
|
workspace.ApprovalMode = nextApprovalMode;
|
||||||
|
workspace.SchedulePostsAutomaticallyOnApproval = request.SchedulePostsAutomaticallyOnApproval ?? workspace.SchedulePostsAutomaticallyOnApproval;
|
||||||
|
workspace.LockContentAfterApproval = request.LockContentAfterApproval ?? workspace.LockContentAfterApproval;
|
||||||
|
workspace.SendAutomaticApprovalReminders = request.SendAutomaticApprovalReminders ?? workspace.SendAutomaticApprovalReminders;
|
||||||
|
|
||||||
|
if (requestedApprovalSteps is not null)
|
||||||
|
{
|
||||||
|
List<WorkspaceApprovalStepConfiguration> existingSteps = await dbContext.WorkspaceApprovalStepConfigurations
|
||||||
|
.Where(step => step.WorkspaceId == workspace.Id)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
dbContext.WorkspaceApprovalStepConfigurations.RemoveRange(existingSteps);
|
||||||
|
|
||||||
|
List<WorkspaceApprovalStepConfiguration> replacementSteps = requestedApprovalSteps
|
||||||
|
.OrderBy(step => step.SortOrder)
|
||||||
|
.Select(step => new WorkspaceApprovalStepConfiguration
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
WorkspaceId = workspace.Id,
|
||||||
|
Name = step.Name.Trim(),
|
||||||
|
SortOrder = step.SortOrder,
|
||||||
|
TargetType = step.TargetType.Trim(),
|
||||||
|
TargetValue = NormalizeTargetValue(step),
|
||||||
|
RequiredApproverCount = step.RequiredApproverCount,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
dbContext.WorkspaceApprovalStepConfigurations.AddRange(replacementSteps);
|
||||||
|
}
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
List<ApprovalStepConfigurationDto> approvalSteps = await dbContext.WorkspaceApprovalStepConfigurations
|
||||||
|
.Where(step => step.WorkspaceId == workspace.Id)
|
||||||
|
.OrderBy(step => step.SortOrder)
|
||||||
|
.ThenBy(step => step.Name)
|
||||||
|
.Select(step => new ApprovalStepConfigurationDto(
|
||||||
|
step.Id,
|
||||||
|
step.WorkspaceId,
|
||||||
|
step.Name,
|
||||||
|
step.SortOrder,
|
||||||
|
step.TargetType,
|
||||||
|
step.TargetValue,
|
||||||
|
step.RequiredApproverCount,
|
||||||
|
step.CreatedAt))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
WorkspaceDto dto = new(
|
WorkspaceDto dto = new(
|
||||||
workspace.Id,
|
workspace.Id,
|
||||||
workspace.Name,
|
workspace.Name,
|
||||||
workspace.Slug,
|
workspace.Slug,
|
||||||
workspace.LogoUrl,
|
workspace.LogoUrl,
|
||||||
workspace.TimeZone,
|
workspace.TimeZone,
|
||||||
|
workspace.ApprovalMode,
|
||||||
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
|
workspace.LockContentAfterApproval,
|
||||||
|
workspace.SendAutomaticApprovalReminders,
|
||||||
|
approvalSteps,
|
||||||
workspace.CreatedAt);
|
workspace.CreatedAt);
|
||||||
|
|
||||||
await SendOkAsync(dto, ct);
|
await SendOkAsync(dto, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ValidateApprovalStepsAsync(
|
||||||
|
Guid workspaceId,
|
||||||
|
IReadOnlyCollection<UpdateApprovalStepConfigurationRequest> steps,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
foreach (UpdateApprovalStepConfigurationRequest step in steps)
|
||||||
|
{
|
||||||
|
string targetType = step.TargetType.Trim();
|
||||||
|
string targetValue = step.TargetValue.Trim();
|
||||||
|
|
||||||
|
if (targetType == ApprovalStepTargetTypes.Role &&
|
||||||
|
!ApprovalStepConfigurationRules.IsValidRoleTarget(targetValue))
|
||||||
|
{
|
||||||
|
AddError(request => request.ApprovalSteps, $"'{targetValue}' is not a supported approval role target.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == ApprovalStepTargetTypes.Membership &&
|
||||||
|
!ApprovalStepConfigurationRules.IsValidMembershipTarget(targetValue))
|
||||||
|
{
|
||||||
|
AddError(request => request.ApprovalSteps, $"'{targetValue}' is not a supported approval membership target.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == ApprovalStepTargetTypes.Member)
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<Guid> memberUserIds = ApprovalWorkflowRules.ParseMemberTargetIds(targetValue);
|
||||||
|
|
||||||
|
if (memberUserIds.Count == 0)
|
||||||
|
{
|
||||||
|
AddError(request => request.ApprovalSteps, "Member approval step targets must reference at least one valid user id.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberUserIds.Count < step.RequiredApproverCount)
|
||||||
|
{
|
||||||
|
AddError(request => request.ApprovalSteps, "Member approval step targets must include at least as many members as required approvers.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string workspaceClaimValue = workspaceId.ToString();
|
||||||
|
int workspaceMemberCount = await dbContext.UserClaims
|
||||||
|
.Where(claim => memberUserIds.Contains(claim.UserId) &&
|
||||||
|
claim.ClaimType == KnownClaims.WorkspaceScope &&
|
||||||
|
claim.ClaimValue == workspaceClaimValue)
|
||||||
|
.Select(claim => claim.UserId)
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync(ct);
|
||||||
|
|
||||||
|
if (workspaceMemberCount != memberUserIds.Count)
|
||||||
|
{
|
||||||
|
AddError(request => request.ApprovalSteps, "Member approval step targets must reference users with access to the workspace.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeTargetValue(UpdateApprovalStepConfigurationRequest step)
|
||||||
|
{
|
||||||
|
string targetValue = step.TargetValue.Trim();
|
||||||
|
return step.TargetType.Trim() == ApprovalStepTargetTypes.Member
|
||||||
|
? ApprovalWorkflowRules.FormatMemberTargetValue(ApprovalWorkflowRules.ParseMemberTargetIds(targetValue))
|
||||||
|
: targetValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ using Socialize.Api.Modules.ContentItems;
|
|||||||
using Socialize.Api.Modules.Feedback;
|
using Socialize.Api.Modules.Feedback;
|
||||||
using Socialize.Api.Modules.Identity;
|
using Socialize.Api.Modules.Identity;
|
||||||
using Socialize.Api.Modules.Notifications;
|
using Socialize.Api.Modules.Notifications;
|
||||||
using Socialize.Api.Modules.Projects;
|
using Socialize.Api.Modules.Campaigns;
|
||||||
using Socialize.Api.Modules.Workspaces;
|
using Socialize.Api.Modules.Workspaces;
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ builder.AddInfrastructureModule();
|
|||||||
builder.AddIdentityModule();
|
builder.AddIdentityModule();
|
||||||
builder.AddWorkspaceModule();
|
builder.AddWorkspaceModule();
|
||||||
builder.AddClientsModule();
|
builder.AddClientsModule();
|
||||||
builder.AddProjectsModule();
|
builder.AddCampaignsModule();
|
||||||
builder.AddContentItemsModule();
|
builder.AddContentItemsModule();
|
||||||
builder.AddAssetsModule();
|
builder.AddAssetsModule();
|
||||||
builder.AddCommentsModule();
|
builder.AddCommentsModule();
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
||||||
|
</startup>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Build" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Build.Framework" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Build.Utilities.Core" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Build.Tasks.Core" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.IO.Redist" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.1" newVersion="6.0.0.1"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
||||||
Binary file not shown.
BIN
backend/src/Socialize.Api/bin\Debug/net10.0/BuildHost-net472/Newtonsoft.Json.dll
Executable file
BIN
backend/src/Socialize.Api/bin\Debug/net10.0/BuildHost-net472/Newtonsoft.Json.dll
Executable file
Binary file not shown.
BIN
backend/src/Socialize.Api/bin\Debug/net10.0/BuildHost-net472/System.Buffers.dll
Executable file
BIN
backend/src/Socialize.Api/bin\Debug/net10.0/BuildHost-net472/System.Buffers.dll
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/src/Socialize.Api/bin\Debug/net10.0/BuildHost-net472/System.Memory.dll
Executable file
BIN
backend/src/Socialize.Api/bin\Debug/net10.0/BuildHost-net472/System.Memory.dll
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,260 @@
|
|||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v6.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v6.0": {
|
||||||
|
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/4.14.0-3.25262.10": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Build.Locator": "1.6.10",
|
||||||
|
"Microsoft.CodeAnalysis.NetAnalyzers": "8.0.0-preview.23468.1",
|
||||||
|
"Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers": "3.3.4-beta1.22504.1",
|
||||||
|
"Microsoft.DotNet.XliffTasks": "9.0.0-beta.25255.5",
|
||||||
|
"Microsoft.VisualStudio.Threading.Analyzers": "17.13.2",
|
||||||
|
"Newtonsoft.Json": "13.0.3",
|
||||||
|
"Roslyn.Diagnostics.Analyzers": "3.11.0-beta1.24081.1",
|
||||||
|
"System.Collections.Immutable": "9.0.0",
|
||||||
|
"System.CommandLine": "2.0.0-beta4.24528.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll": {}
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"cs/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "cs"
|
||||||
|
},
|
||||||
|
"de/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "de"
|
||||||
|
},
|
||||||
|
"es/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "es"
|
||||||
|
},
|
||||||
|
"fr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "fr"
|
||||||
|
},
|
||||||
|
"it/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "it"
|
||||||
|
},
|
||||||
|
"ja/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "ja"
|
||||||
|
},
|
||||||
|
"ko/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "ko"
|
||||||
|
},
|
||||||
|
"pl/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "pl"
|
||||||
|
},
|
||||||
|
"pt-BR/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "pt-BR"
|
||||||
|
},
|
||||||
|
"ru/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "ru"
|
||||||
|
},
|
||||||
|
"tr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "tr"
|
||||||
|
},
|
||||||
|
"zh-Hans/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "zh-Hans"
|
||||||
|
},
|
||||||
|
"zh-Hant/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
|
||||||
|
"locale": "zh-Hant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Build.Locator/1.6.10": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/Microsoft.Build.Locator.dll": {
|
||||||
|
"assemblyVersion": "1.0.0.0",
|
||||||
|
"fileVersion": "1.6.10.57384"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.BannedApiAnalyzers/3.11.0-beta1.24081.1": {},
|
||||||
|
"Microsoft.CodeAnalysis.NetAnalyzers/8.0.0-preview.23468.1": {},
|
||||||
|
"Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers/3.3.4-beta1.22504.1": {},
|
||||||
|
"Microsoft.CodeAnalysis.PublicApiAnalyzers/3.11.0-beta1.24081.1": {},
|
||||||
|
"Microsoft.DotNet.XliffTasks/9.0.0-beta.25255.5": {},
|
||||||
|
"Microsoft.VisualStudio.Threading.Analyzers/17.13.2": {},
|
||||||
|
"Newtonsoft.Json/13.0.3": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/Newtonsoft.Json.dll": {
|
||||||
|
"assemblyVersion": "13.0.0.0",
|
||||||
|
"fileVersion": "13.0.3.27908"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Roslyn.Diagnostics.Analyzers/3.11.0-beta1.24081.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.CodeAnalysis.BannedApiAnalyzers": "3.11.0-beta1.24081.1",
|
||||||
|
"Microsoft.CodeAnalysis.PublicApiAnalyzers": "3.11.0-beta1.24081.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Collections.Immutable/9.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.Memory": "4.5.5",
|
||||||
|
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard2.0/System.Collections.Immutable.dll": {
|
||||||
|
"assemblyVersion": "9.0.0.0",
|
||||||
|
"fileVersion": "9.0.24.52809"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.CommandLine/2.0.0-beta4.24528.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.Memory": "4.5.5"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard2.0/System.CommandLine.dll": {
|
||||||
|
"assemblyVersion": "2.0.0.0",
|
||||||
|
"fileVersion": "2.0.24.52801"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"lib/netstandard2.0/cs/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "cs"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/de/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "de"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/es/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "es"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/fr/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "fr"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/it/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "it"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/ja/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "ja"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/ko/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "ko"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/pl/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "pl"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/pt-BR/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "pt-BR"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/ru/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "ru"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/tr/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "tr"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/zh-Hans/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "zh-Hans"
|
||||||
|
},
|
||||||
|
"lib/netstandard2.0/zh-Hant/System.CommandLine.resources.dll": {
|
||||||
|
"locale": "zh-Hant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Memory/4.5.5": {},
|
||||||
|
"System.Runtime.CompilerServices.Unsafe/6.0.0": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/4.14.0-3.25262.10": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"Microsoft.Build.Locator/1.6.10": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-DJhCkTGqy1LMJzEmG/2qxRTMHwdPc3WdVoGQI5o5mKHVo4dsHrCMLIyruwU/NSvPNSdvONlaf7jdFXnAMuxAuA==",
|
||||||
|
"path": "microsoft.build.locator/1.6.10",
|
||||||
|
"hashPath": "microsoft.build.locator.1.6.10.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.BannedApiAnalyzers/3.11.0-beta1.24081.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-DH6L3rsbjppLrHM2l2/NKbnMaYd0NFHx2pjZaFdrVcRkONrV3i9FHv6Id8Dp6/TmjhXQsJVJJFbhhjkpuP1xxg==",
|
||||||
|
"path": "microsoft.codeanalysis.bannedapianalyzers/3.11.0-beta1.24081.1",
|
||||||
|
"hashPath": "microsoft.codeanalysis.bannedapianalyzers.3.11.0-beta1.24081.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.NetAnalyzers/8.0.0-preview.23468.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-ZhIvyxmUCqb8OiU/VQfxfuAmIB4lQsjqhMVYKeoyxzSI+d7uR5Pzx3ZKoaIhPizQ15wa4lnyD6wg3TnSJ6P4LA==",
|
||||||
|
"path": "microsoft.codeanalysis.netanalyzers/8.0.0-preview.23468.1",
|
||||||
|
"hashPath": "microsoft.codeanalysis.netanalyzers.8.0.0-preview.23468.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers/3.3.4-beta1.22504.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-2XRlqPAzVke7Sb80+UqaC7o57OwfK+tIr+aIOxrx41RWDMeR2SBUW7kL4sd6hfLFfBNsLo3W5PT+UwfvwPaOzA==",
|
||||||
|
"path": "microsoft.codeanalysis.performancesensitiveanalyzers/3.3.4-beta1.22504.1",
|
||||||
|
"hashPath": "microsoft.codeanalysis.performancesensitiveanalyzers.3.3.4-beta1.22504.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.PublicApiAnalyzers/3.11.0-beta1.24081.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-3bYGBihvoNO0rhCOG1U9O50/4Q8suZ+glHqQLIAcKvnodSnSW+dYWYzTNb1UbS8pUS8nAUfxSFMwuMup/G5DtQ==",
|
||||||
|
"path": "microsoft.codeanalysis.publicapianalyzers/3.11.0-beta1.24081.1",
|
||||||
|
"hashPath": "microsoft.codeanalysis.publicapianalyzers.3.11.0-beta1.24081.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.DotNet.XliffTasks/9.0.0-beta.25255.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-bb0fZB5ViPscdfYeWlmtyXJMzNkgcpkV5RWmXktfV9lwIUZgNZmFotUXrdcTyZzrN7v1tQK/Y6BGnbkP9gEsXg==",
|
||||||
|
"path": "microsoft.dotnet.xlifftasks/9.0.0-beta.25255.5",
|
||||||
|
"hashPath": "microsoft.dotnet.xlifftasks.9.0.0-beta.25255.5.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.VisualStudio.Threading.Analyzers/17.13.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-Qcd8IlaTXZVq3wolBnzby1P7kWihdWaExtD8riumiKuG1sHa8EgjV/o70TMjTaeUMhomBbhfdC9OPwAHoZfnjQ==",
|
||||||
|
"path": "microsoft.visualstudio.threading.analyzers/17.13.2",
|
||||||
|
"hashPath": "microsoft.visualstudio.threading.analyzers.17.13.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Newtonsoft.Json/13.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
|
||||||
|
"path": "newtonsoft.json/13.0.3",
|
||||||
|
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Roslyn.Diagnostics.Analyzers/3.11.0-beta1.24081.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-reHqZCDKifA+DURcL8jUfYkMGL4FpgNt5LI0uWTS6IpM8kKVbu/kO8byZsqfhBu4wUzT3MBDcoMfzhZPdENIpg==",
|
||||||
|
"path": "roslyn.diagnostics.analyzers/3.11.0-beta1.24081.1",
|
||||||
|
"hashPath": "roslyn.diagnostics.analyzers.3.11.0-beta1.24081.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Collections.Immutable/9.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==",
|
||||||
|
"path": "system.collections.immutable/9.0.0",
|
||||||
|
"hashPath": "system.collections.immutable.9.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.CommandLine/2.0.0-beta4.24528.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-Xt8tsSU8yd0ZpbT9gl5DAwkMYWLo8PV1fq2R/belrUbHVVOIKqhLfbWksbdknUDpmzMHZenBtD6AGAp9uJTa2w==",
|
||||||
|
"path": "system.commandline/2.0.0-beta4.24528.1",
|
||||||
|
"hashPath": "system.commandline.2.0.0-beta4.24528.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Memory/4.5.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
|
||||||
|
"path": "system.memory/4.5.5",
|
||||||
|
"hashPath": "system.memory.4.5.5.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Runtime.CompilerServices.Unsafe/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
|
||||||
|
"path": "system.runtime.compilerservices.unsafe/6.0.0",
|
||||||
|
"hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,659 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Build" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Build.Framework" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Build.Utilities.Core" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Build.Tasks.Core" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.VisualBasic.Core" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Win32.Primitives" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Win32.Registry" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Collections.Concurrent" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Collections.NonGeneric" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Collections.Specialized" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Collections" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.ComponentModel.Annotations" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.ComponentModel.EventBasedAsync" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.ComponentModel.Primitives" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.ComponentModel.TypeConverter" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.ComponentModel" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Console" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Data.Common" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.Contracts" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.FileVersionInfo" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.Process" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.StackTrace" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.TextWriterTraceListener" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.TraceSource" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Drawing.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.Compression.ZipFile" publicKeyToken="b77a5c561934e089"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.Compression" publicKeyToken="b77a5c561934e089" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.FileSystem.AccessControl" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.FileSystem.DriveInfo" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.FileSystem.Watcher" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.IsolatedStorage" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.MemoryMappedFiles" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.Pipes.AccessControl" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.IO.Pipes" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Linq.Expressions" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Linq.Parallel" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Linq.Queryable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Linq" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.HttpListener" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.Mail" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.NameResolution" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.NetworkInformation" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.Ping" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.Requests" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.Security" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.ServicePoint" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.Sockets" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.WebClient" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.WebHeaderCollection" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.WebProxy" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.WebSockets.Client" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Net.WebSockets" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.ObjectModel" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Reflection.Emit.ILGeneration" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Reflection.Emit.Lightweight" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Reflection.Emit" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Reflection.Primitives" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Resources.Writer" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.CompilerServices.VisualC" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.InteropServices.RuntimeInformation"
|
||||||
|
publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.Numerics" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.Serialization.Formatters" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.Serialization.Json" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.Serialization.Primitives" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.Serialization.Xml" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.AccessControl" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Claims" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Cryptography.Algorithms" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Cryptography.Cng" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Cryptography.Csp" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Cryptography.Encoding" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Cryptography.Primitives" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Cryptography.X509Certificates" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Principal.Windows" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Text.Encoding.Extensions" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Text.RegularExpressions" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Threading.Overlapped" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Threading.Tasks.Parallel" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Threading.Thread" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Threading.ThreadPool" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Threading" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Transactions.Local" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Web.HttpUtility" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Xml.ReaderWriter" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Xml.XDocument" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Xml.XPath.XDocument" publicKeyToken="b03f5f7f11d50a3a"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Xml.XPath" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Xml.XmlSerializer" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="netstandard" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Configuration.ConfigurationManager" publicKeyToken="cc7b13ffcd2ddd51"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Security.Cryptography.Xml" publicKeyToken="cc7b13ffcd2ddd51"
|
||||||
|
culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.CodeDom" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net6.0",
|
||||||
|
"framework": {
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "6.0.0"
|
||||||
|
},
|
||||||
|
"rollForward": "Major",
|
||||||
|
"configProperties": {
|
||||||
|
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user