Merge branch 'approval-workflow-docs' into HEAD
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

# Conflicts:
#	frontend/src/api/schema.d.ts
#	frontend/src/features/content/views/ContentItemDetailView.vue
#	frontend/src/features/workspaces/views/DashboardView.vue
#	shared/openapi/openapi.json
This commit is contained in:
2026-05-01 15:58:04 -04:00
118 changed files with 3783 additions and 1205 deletions

View File

@@ -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,7 +20,7 @@ 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>();
@@ -43,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();

View File

@@ -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,7 +303,7 @@ 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",
@@ -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;

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -438,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")
@@ -549,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");
@@ -572,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)
@@ -600,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");
@@ -784,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)");
@@ -821,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)
@@ -1150,59 +1203,6 @@ 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")

View File

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

View File

@@ -64,7 +64,7 @@ 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
}
}
}

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ Composition registers:
- web services and auth in `DependencyInjection.cs` - web services and auth in `DependencyInjection.cs`
- infrastructure in `Infrastructure/DependencyInjection.cs` - infrastructure in `Infrastructure/DependencyInjection.cs`
- domain modules for Identity, Workspaces, Clients, Projects, ContentItems, Assets, Comments, Approvals, Notifications, and Feedback - domain modules for Identity, Workspaces, Clients, Campaigns, ContentItems, Assets, Comments, Approvals, Notifications, and Feedback
## Data Ownership ## Data Ownership

View File

@@ -100,7 +100,7 @@ Each report should capture useful debugging context automatically when available
- current app URL/path - current app URL/path
- active workspace id/name - active workspace id/name
- active client id/name - active client id/name
- active project id/name - active campaign id/name
- active content item id/title - active content item id/title
- browser user agent - browser user agent
- viewport size - viewport size

View File

@@ -0,0 +1,25 @@
# Rename Projects To Campaigns
## Goal
Align the codebase terminology with the product language by replacing the `Project` domain surface with `Campaign`.
## Relevant Specs
- `docs/product/glossary.md`
- `docs/ARCHITECTURE.md`
## Scope
- Rename backend module, entity, DTOs, handlers, EF configuration, and route/tag names from projects to campaigns.
- Update content item and access-scope references that point at the renamed campaign concept.
- Update frontend feature naming and API calls where they still refer to projects.
- Update OpenAPI snapshots if backend contracts change and the backend can run.
## Validation
```bash
dotnet build backend/Socialize.slnx
dotnet test backend/Socialize.slnx
cd frontend && npm run build
```

View File

@@ -17,7 +17,7 @@ Add the backend foundation for product feedback reports.
- type: `Bug`, `Suggestion`, `Request` - type: `Bug`, `Suggestion`, `Request`
- status: `New`, `Planned`, `Resolved`, `Won't Do`, `Cancelled` - status: `New`, `Planned`, `Resolved`, `Won't Do`, `Cancelled`
- Add `DbSet` entries and module configuration to `AppDbContext`. - Add `DbSet` entries and module configuration to `AppDbContext`.
- Capture reporter id, reporter display fields, submitted route, browser metadata, viewport size, app version if available, and optional workspace/client/project/content context. - Capture reporter id, reporter display fields, submitted route, browser metadata, viewport size, app version if available, and optional workspace/client/campaign/content context.
- Add API endpoints for: - Add API endpoints for:
- submit feedback - submit feedback
- list current user's feedback - list current user's feedback

View File

@@ -29,6 +29,10 @@ An agency is above the workspace level in the business model.
Business, brand, or organization represented by a workspace and participating in review and approval flows. Business, brand, or organization represented by a workspace and participating in review and approval flows.
## Campaign
Client-owned body of content work that groups related content items, notes, and timelines.
## Content Item ## Content Item
Primary reviewable unit in the system. Contains metadata, copy, due dates, networks, channels, and linked assets. Primary reviewable unit in the system. Contains metadata, copy, due dates, networks, channels, and linked assets.

View File

@@ -1,4 +1,4 @@
VITE_API_URL=http://192.168.1.2:5080 VITE_API_URL=http://192.168.1.17:5080
VITE_STRIPE_API_KEY=pk_test_51OoveVDrRyqXtNdB2st1NgA8WQA9rhgGaf3q7bCpAOoQyyRS30HMCzGeHba7meVGCSPfb1BVWmOTmFOcr9MkKf5H00bLu5MqsS VITE_STRIPE_API_KEY=pk_test_51OoveVDrRyqXtNdB2st1NgA8WQA9rhgGaf3q7bCpAOoQyyRS30HMCzGeHba7meVGCSPfb1BVWmOTmFOcr9MkKf5H00bLu5MqsS
VITE_GOOGLE_CLIENT_ID=213344094492-9dbaet2gaschju3hj1sgv1umk0qpd833.apps.googleusercontent.com VITE_GOOGLE_CLIENT_ID=213344094492-9dbaet2gaschju3hj1sgv1umk0qpd833.apps.googleusercontent.com
VITE_FACEBOOK_APP_ID=1076433907621883 VITE_FACEBOOK_APP_ID=1076433907621883

View File

@@ -0,0 +1,16 @@
# Claims and Roles Guidelines
To ensure consistency across the application, all claim and role values MUST be in lowercase.
## Roles
The following roles are currently used in the system:
- `administrator`
- `manager`
- `client`
- `provider`
- `developer`
## Implementation Notes
- **Processing**: The `authStore.js` automatically converts all roles extracted from JWT tokens to lowercase.
- **Comparisons**: All checks (e.g., `authStore.hasAnyRole(['role-name'])` or `meta: { roles: ['role-name'] }`) should use lowercase strings.
- **Routing**: Route guards in `router.js` expect lowercase role names in the `meta.roles` field.

View File

@@ -100,22 +100,6 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/api/projects": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["SocializeApiModulesProjectsHandlersGetProjectsHandler"];
put?: never;
post: operations["SocializeApiModulesProjectsHandlersCreateProjectHandler"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/notifications": { "/api/notifications": {
parameters: { parameters: {
query?: never; query?: never;
@@ -772,6 +756,22 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/api/campaigns": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["SocializeApiModulesCampaignsHandlersGetCampaignsHandler"];
put?: never;
post: operations["SocializeApiModulesCampaignsHandlersCreateCampaignHandler"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/assets/{id}/revisions": { "/api/assets/{id}/revisions": {
parameters: { parameters: {
query?: never; query?: never;
@@ -952,36 +952,6 @@ export interface components {
/** Format: int32 */ /** Format: int32 */
requiredApproverCount?: number; requiredApproverCount?: number;
}; };
SocializeApiModulesProjectsHandlersProjectDto: {
/** Format: guid */
id?: string;
/** Format: guid */
workspaceId?: string;
/** Format: guid */
clientId?: string;
name?: string;
description?: string | null;
notes?: string | null;
status?: string;
/** Format: date-time */
startDate?: string;
/** Format: date-time */
endDate?: string;
};
SocializeApiModulesProjectsHandlersCreateProjectRequest: {
/** Format: guid */
workspaceId: string;
/** Format: guid */
clientId: string;
name: string;
/** Format: date-time */
startDate: string;
/** Format: date-time */
endDate: string;
description?: string | null;
notes?: string | null;
};
SocializeApiModulesProjectsHandlersGetProjectsRequest: Record<string, never>;
SocializeApiModulesNotificationsHandlersNotificationEventDto: { SocializeApiModulesNotificationsHandlersNotificationEventDto: {
/** Format: guid */ /** Format: guid */
id?: string; id?: string;
@@ -1041,7 +1011,7 @@ export interface components {
persona?: string | null; persona?: string | null;
authorizedWorkspaceIds?: string[]; authorizedWorkspaceIds?: string[];
authorizedClientIds?: string[]; authorizedClientIds?: string[];
authorizedProjectIds?: string[]; authorizedCampaignIds?: string[];
username?: string; username?: string;
alias?: string | null; alias?: string | null;
portraitUrl?: string | null; portraitUrl?: string | null;
@@ -1176,8 +1146,8 @@ export interface components {
clientId?: string | null; clientId?: string | null;
clientName?: string | null; clientName?: string | null;
/** Format: guid */ /** Format: guid */
projectId?: string | null; campaignId?: string | null;
projectName?: string | null; campaignName?: string | null;
/** Format: guid */ /** Format: guid */
contentItemId?: string | null; contentItemId?: string | null;
contentItemTitle?: string | null; contentItemTitle?: string | null;
@@ -1217,8 +1187,8 @@ export interface components {
clientId?: string | null; clientId?: string | null;
clientName?: string | null; clientName?: string | null;
/** Format: guid */ /** Format: guid */
projectId?: string | null; campaignId?: string | null;
projectName?: string | null; campaignName?: string | null;
/** Format: guid */ /** Format: guid */
contentItemId?: string | null; contentItemId?: string | null;
contentItemTitle?: string | null; contentItemTitle?: string | null;
@@ -1236,7 +1206,7 @@ export interface components {
/** Format: guid */ /** Format: guid */
clientId?: string; clientId?: string;
/** Format: guid */ /** Format: guid */
projectId?: string; campaignId?: string;
title?: string; title?: string;
publicationMessage?: string; publicationMessage?: string;
publicationTargets?: string; publicationTargets?: string;
@@ -1254,7 +1224,7 @@ export interface components {
/** Format: guid */ /** Format: guid */
clientId: string; clientId: string;
/** Format: guid */ /** Format: guid */
projectId: string; campaignId: string;
title: string; title: string;
publicationMessage: string; publicationMessage: string;
publicationTargets: string; publicationTargets: string;
@@ -1295,7 +1265,7 @@ export interface components {
/** Format: guid */ /** Format: guid */
clientId?: string; clientId?: string;
/** Format: guid */ /** Format: guid */
projectId?: string; campaignId?: string;
title?: string; title?: string;
publicationMessage?: string; publicationMessage?: string;
publicationTargets?: string; publicationTargets?: string;
@@ -1383,6 +1353,36 @@ export interface components {
primaryContactEmail?: string | null; primaryContactEmail?: string | null;
primaryContactPortraitUrl?: string | null; primaryContactPortraitUrl?: string | null;
}; };
SocializeApiModulesCampaignsHandlersCampaignDto: {
/** Format: guid */
id?: string;
/** Format: guid */
workspaceId?: string;
/** Format: guid */
clientId?: string;
name?: string;
description?: string | null;
notes?: string | null;
status?: string;
/** Format: date-time */
startDate?: string;
/** Format: date-time */
endDate?: string;
};
SocializeApiModulesCampaignsHandlersCreateCampaignRequest: {
/** Format: guid */
workspaceId: string;
/** Format: guid */
clientId: string;
name: string;
/** Format: date-time */
startDate: string;
/** Format: date-time */
endDate: string;
description?: string | null;
notes?: string | null;
};
SocializeApiModulesCampaignsHandlersGetCampaignsRequest: Record<string, never>;
SocializeApiModulesAssetsHandlersAssetRevisionDto: { SocializeApiModulesAssetsHandlersAssetRevisionDto: {
/** Format: guid */ /** Format: guid */
id?: string; id?: string;
@@ -1778,76 +1778,6 @@ export interface operations {
}; };
}; };
}; };
SocializeApiModulesProjectsHandlersGetProjectsHandler: {
parameters: {
query?: {
workspaceId?: string | null;
clientId?: string | null;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SocializeApiModulesProjectsHandlersProjectDto"][];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
SocializeApiModulesProjectsHandlersCreateProjectHandler: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SocializeApiModulesProjectsHandlersCreateProjectRequest"];
};
};
responses: {
/** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SocializeApiModulesProjectsHandlersProjectDto"];
};
};
/** @description Bad Request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
SocializeApiModulesNotificationsHandlersGetNotificationsHandler: { SocializeApiModulesNotificationsHandlersGetNotificationsHandler: {
parameters: { parameters: {
query?: { query?: {
@@ -2936,7 +2866,7 @@ export interface operations {
query?: { query?: {
workspaceId?: string | null; workspaceId?: string | null;
clientId?: string | null; clientId?: string | null;
projectId?: string | null; campaignId?: string | null;
}; };
header?: never; header?: never;
path?: never; path?: never;
@@ -3395,6 +3325,76 @@ export interface operations {
}; };
}; };
}; };
SocializeApiModulesCampaignsHandlersGetCampaignsHandler: {
parameters: {
query?: {
workspaceId?: string | null;
clientId?: string | null;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SocializeApiModulesCampaignsHandlersCampaignDto"][];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
SocializeApiModulesCampaignsHandlersCreateCampaignHandler: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SocializeApiModulesCampaignsHandlersCreateCampaignRequest"];
};
};
responses: {
/** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SocializeApiModulesCampaignsHandlersCampaignDto"];
};
};
/** @description Bad Request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
SocializeApiModulesAssetsHandlersCreateAssetRevisionHandler: { SocializeApiModulesAssetsHandlersCreateAssetRevisionHandler: {
parameters: { parameters: {
query?: never; query?: never;

View File

@@ -7,294 +7,295 @@ import { jwtDecode } from 'jwt-decode';
import { formatDuration } from '@/internal_time_ago.js'; import { formatDuration } from '@/internal_time_ago.js';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const clientApi = useClient(); const clientApi = useClient();
const router = useRouter(); const router = useRouter();
const isRefreshing = ref(false); const isRefreshing = ref(false);
let refreshPromise = null; let refreshPromise = null;
const accessToken = useSessionStorage('auth-accessToken', undefined); const accessToken = useSessionStorage('auth-accessToken', undefined);
const refreshToken = useSessionStorage('auth-refreshToken', undefined); const refreshToken = useSessionStorage('auth-refreshToken', undefined);
const tokenClaims = useSessionStorage('auth-tokenClaims', null, { const tokenClaims = useSessionStorage('auth-tokenClaims', null, {
serializer: { serializer: {
read: v => (v ? JSON.parse(v) : null), read: v => (v ? JSON.parse(v) : null),
write: v => (v ? JSON.stringify(v) : null), write: v => (v ? JSON.stringify(v) : null),
}, },
}); });
const isAuthenticated = computed(() => !!accessToken.value); const isAuthenticated = computed(() => !!accessToken.value);
const userId = computed(() => tokenClaims.value?.sub); const userId = computed(() => tokenClaims.value?.sub);
const userRoles = computed(() => { const userRoles = computed(() => {
const claims = tokenClaims.value ?? {}; const claims = tokenClaims.value ?? {};
const candidates = [ const candidates = [
claims.role, claims.role,
claims.roles, claims.roles,
claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'], claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'],
].flatMap(value => Array.isArray(value) ? value : value ? [value] : []); ].flatMap(value => Array.isArray(value) ? value : value ? [value] : [])
.map(v => v.toLowerCase());
return [...new Set(candidates)]; return [...new Set(candidates)];
}); });
const persona = computed(() => tokenClaims.value?.persona ?? null); const persona = computed(() => tokenClaims.value?.persona ?? null);
const isManager = computed(() => userRoles.value.includes('Administrator') || userRoles.value.includes('Manager')); const isManager = computed(() => userRoles.value.includes('administrator') || userRoles.value.includes('manager'));
const isClient = computed(() => userRoles.value.includes('Client')); const isClient = computed(() => userRoles.value.includes('client'));
const isProvider = computed(() => userRoles.value.includes('Provider')); const isProvider = computed(() => userRoles.value.includes('provider'));
function updateTokens(data) { function updateTokens(data) {
if (!data?.accessToken || !data?.refreshToken) { if (!data?.accessToken || !data?.refreshToken) {
throw new Error('Invalid token data'); throw new Error('Invalid token data');
} }
accessToken.value = data.accessToken; accessToken.value = data.accessToken;
refreshToken.value = data.refreshToken; refreshToken.value = data.refreshToken;
const claims = getClaimsFromToken(data.accessToken); const claims = getClaimsFromToken(data.accessToken);
tokenClaims.value = claims; tokenClaims.value = claims;
console.log('Tokens updated, user ID:', claims?.sub); console.log('Tokens updated, user ID:', claims?.sub);
}
function cleanTokens() {
console.log('cleanTokens called - clearing stored tokens');
accessToken.value = undefined;
refreshToken.value = undefined;
tokenClaims.value = null;
}
async function logout() {
cleanTokens();
await router.push('/');
}
async function login(email, password) {
console.log('login called with email:', email);
if (!email || !password) {
throw new Error('Email and password are required');
} }
function cleanTokens() { try {
console.log('cleanTokens called - clearing stored tokens'); const response = await clientApi.post('api/users/login', {
accessToken.value = undefined; email: email.trim(),
refreshToken.value = undefined; password: password,
tokenClaims.value = null; });
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid login response');
}
updateTokens(response.data);
console.log('login successful');
return true;
} catch (error) {
console.error('Login failed:', error);
cleanTokens();
throw error;
}
}
async function loginWithGoogle(accessTokenParam) {
console.log('loginWithGoogle called');
if (!accessTokenParam) {
throw new Error('Google access token is required');
} }
async function logout() { try {
cleanTokens(); const response = await clientApi.post('api/users/login-with-google', {
await router.push('/'); token: accessTokenParam,
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid Google login response');
}
updateTokens(response.data);
console.log('Google login successful');
return true;
} catch (error) {
console.error('Google login failed:', error);
cleanTokens();
throw error;
}
}
async function loginWithFacebook(authResponse) {
console.log('loginWithFacebook called');
if (!authResponse?.accessToken) {
throw new Error('Facebook access token is required');
} }
async function login(email, password) { try {
console.log('login called with email:', email); const response = await clientApi.post('api/users/login-with-facebook', {
if (!email || !password) { token: authResponse.accessToken,
throw new Error('Email and password are required'); });
}
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid Facebook login response');
}
updateTokens(response.data);
console.log('Facebook login successful');
return true;
} catch (error) {
console.error('Facebook login failed:', error);
cleanTokens();
throw error;
}
}
async function refresh() {
console.log('refresh called');
if (!refreshToken.value) {
cleanTokens(); // Clear tokens first
throw new Error('No refresh token available');
}
if (isRefreshing.value && refreshPromise) {
console.log('Already refreshing, returning existing refreshPromise');
return refreshPromise;
}
try {
isRefreshing.value = true;
refreshPromise = (async () => {
try { try {
const response = await clientApi.post('api/users/login', { console.log('Sending refresh request...');
email: email.trim(),
password: password, const response = await clientApi.post('api/users/refresh', {
refreshToken: refreshToken.value,
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid refresh response');
}
updateTokens({
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken,
});
console.log('Token refresh successful');
return true;
} catch (error) {
console.error('Token refresh failed:', error);
cleanTokens();
const currentRoute = router.currentRoute.value;
const returnUrl = currentRoute.fullPath;
// Handle navigation
router
.push({
name: 'login',
query: { returnUrl },
})
.catch(navError => {
console.error('Navigation error after token refresh failure:', navError);
}); });
if (!response.data?.accessToken || !response.data?.refreshToken) { throw error; // Re-throw to notify callers
throw new Error('Invalid login response');
}
updateTokens(response.data);
console.log('login successful');
return true;
} catch (error) {
console.error('Login failed:', error);
cleanTokens();
throw error;
} }
})();
return await refreshPromise;
} catch (error) {
throw error;
} finally {
// Ensure these are always reset, even if an error is thrown
isRefreshing.value = false;
refreshPromise = null;
}
}
function getClaimsFromToken(token) {
if (!token) return null;
try {
return jwtDecode(token);
} catch (error) {
console.error('Failed to decode token:', error);
return null;
}
}
function isTokenExpiringSoon(token) {
if (!token) {
console.log('No token provided, considered expiring soon');
return true;
} }
async function loginWithGoogle(accessTokenParam) { const claims = getClaimsFromToken(token);
console.log('loginWithGoogle called'); if (!claims || !claims.exp) {
if (!accessTokenParam) { console.log('No valid claims found, considered expiring soon');
throw new Error('Google access token is required'); return true;
}
try {
const response = await clientApi.post('api/users/login-with-google', {
token: accessTokenParam,
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid Google login response');
}
updateTokens(response.data);
console.log('Google login successful');
return true;
} catch (error) {
console.error('Google login failed:', error);
cleanTokens();
throw error;
}
} }
async function loginWithFacebook(authResponse) { const expirationTime = claims.exp * 1000; // Convert to milliseconds
console.log('loginWithFacebook called'); const currentTime = Date.now();
if (!authResponse?.accessToken) { const fiveMinutesInMs = 2 * 60 * 1000; // 2 minutes for demonstration
throw new Error('Facebook access token is required');
}
try { // Calculate time remaining (can be negative if already expired)
const response = await clientApi.post('api/users/login-with-facebook', { const timeRemainingMs = expirationTime - currentTime;
token: authResponse.accessToken,
});
if (!response.data?.accessToken || !response.data?.refreshToken) { // Token is expiring soon if less than 2 minutes remaining or already expired
throw new Error('Invalid Facebook login response'); const isExpiring = timeRemainingMs < fiveMinutesInMs;
}
updateTokens(response.data); // Determine the sign for display purposes
console.log('Facebook login successful'); const formattedTimeRemaining =
return true; timeRemainingMs < 0 ? `-${formatDuration(Math.abs(timeRemainingMs))}` : formatDuration(timeRemainingMs);
} catch (error) {
console.error('Facebook login failed:', error); if (isExpiring) {
cleanTokens(); console.log(`Token expiration check; is token expired: ${isExpiring}`, {
throw error; expirationTime: new Date(expirationTime).toLocaleString(),
} currentTime: new Date(currentTime).toLocaleString(),
timeRemaining: formattedTimeRemaining,
});
} }
async function refresh() { return isExpiring;
console.log('refresh called'); }
if (!refreshToken.value) { async function changePassword(newPassword) {
cleanTokens(); // Clear tokens first console.log('changePassword called');
throw new Error('No refresh token available'); if (!isAuthenticated.value) {
} throw new Error('User must be authenticated to change password');
if (isRefreshing.value && refreshPromise) {
console.log('Already refreshing, returning existing refreshPromise');
return refreshPromise;
}
try {
isRefreshing.value = true;
refreshPromise = (async () => {
try {
console.log('Sending refresh request...');
const response = await clientApi.post('api/users/refresh', {
refreshToken: refreshToken.value,
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid refresh response');
}
updateTokens({
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken,
});
console.log('Token refresh successful');
return true;
} catch (error) {
console.error('Token refresh failed:', error);
cleanTokens();
const currentRoute = router.currentRoute.value;
const returnUrl = currentRoute.fullPath;
// Handle navigation
router
.push({
name: 'login',
query: { returnUrl },
})
.catch(navError => {
console.error('Navigation error after token refresh failure:', navError);
});
throw error; // Re-throw to notify callers
}
})();
return await refreshPromise;
} catch (error) {
throw error;
} finally {
// Ensure these are always reset, even if an error is thrown
isRefreshing.value = false;
refreshPromise = null;
}
} }
function getClaimsFromToken(token) { if (!newPassword) {
if (!token) return null; throw new Error('New password is required');
try {
return jwtDecode(token);
} catch (error) {
console.error('Failed to decode token:', error);
return null;
}
} }
function isTokenExpiringSoon(token) { try {
if (!token) { const response = await clientApi.post('api/users/set-password', {
console.log('No token provided, considered expiring soon'); newPassword,
return true; });
}
const claims = getClaimsFromToken(token); console.log('Password changed successfully');
if (!claims || !claims.exp) { return true;
console.log('No valid claims found, considered expiring soon'); } catch (error) {
return true; console.error('Password change failed:', error);
} throw error;
const expirationTime = claims.exp * 1000; // Convert to milliseconds
const currentTime = Date.now();
const fiveMinutesInMs = 2 * 60 * 1000; // 2 minutes for demonstration
// Calculate time remaining (can be negative if already expired)
const timeRemainingMs = expirationTime - currentTime;
// Token is expiring soon if less than 2 minutes remaining or already expired
const isExpiring = timeRemainingMs < fiveMinutesInMs;
// Determine the sign for display purposes
const formattedTimeRemaining =
timeRemainingMs < 0 ? `-${formatDuration(Math.abs(timeRemainingMs))}` : formatDuration(timeRemainingMs);
if (isExpiring) {
console.log(`Token expiration check; is token expired: ${isExpiring}`, {
expirationTime: new Date(expirationTime).toLocaleString(),
currentTime: new Date(currentTime).toLocaleString(),
timeRemaining: formattedTimeRemaining,
});
}
return isExpiring;
} }
}
async function changePassword(newPassword) { function hasAnyRole(roles) {
console.log('changePassword called'); return roles.some(role => userRoles.value.includes(role));
if (!isAuthenticated.value) { }
throw new Error('User must be authenticated to change password');
}
if (!newPassword) { return {
throw new Error('New password is required'); accessToken,
} refreshToken,
isAuthenticated,
try { userId,
const response = await clientApi.post('api/users/set-password', { userRoles,
newPassword, persona,
}); hasAnyRole,
isManager,
console.log('Password changed successfully'); isClient,
return true; isProvider,
} catch (error) { isRefreshing,
console.error('Password change failed:', error); login,
throw error; loginWithGoogle,
} loginWithFacebook,
} logout,
refresh,
function hasAnyRole(roles) { isTokenExpiringSoon,
return roles.some(role => userRoles.value.includes(role)); changePassword,
} };
return {
accessToken,
refreshToken,
isAuthenticated,
userId,
userRoles,
persona,
hasAnyRole,
isManager,
isClient,
isProvider,
isRefreshing,
login,
loginWithGoogle,
loginWithFacebook,
logout,
refresh,
isTokenExpiringSoon,
changePassword,
};
}); });

View File

@@ -4,19 +4,19 @@ import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js'; import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
import { useClient } from '@/plugins/api.js'; import { useClient } from '@/plugins/api.js';
export const useProjectsStore = defineStore('projects', () => { export const useCampaignsStore = defineStore('campaigns', () => {
const authStore = useAuthStore(); const authStore = useAuthStore();
const workspaceStore = useWorkspaceStore(); const workspaceStore = useWorkspaceStore();
const client = useClient(); const client = useClient();
const projects = ref([]); const campaigns = ref([]);
const isLoading = ref(false); const isLoading = ref(false);
const isCreating = ref(false); const isCreating = ref(false);
const error = ref(null); const error = ref(null);
async function fetchProjects() { async function fetchCampaigns() {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) { if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
projects.value = []; campaigns.value = [];
error.value = null; error.value = null;
return; return;
} }
@@ -25,49 +25,49 @@ export const useProjectsStore = defineStore('projects', () => {
error.value = null; error.value = null;
try { try {
const response = await client.get('/api/projects', { const response = await client.get('/api/campaigns', {
params: { params: {
workspaceId: workspaceStore.activeWorkspaceId, workspaceId: workspaceStore.activeWorkspaceId,
}, },
}); });
projects.value = response.data ?? []; campaigns.value = response.data ?? [];
} catch (fetchError) { } catch (fetchError) {
console.error('Failed to fetch projects:', fetchError); console.error('Failed to fetch campaigns:', fetchError);
projects.value = []; campaigns.value = [];
error.value = 'Failed to load projects.'; error.value = 'Failed to load campaigns.';
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
async function createProject(payload) { async function createCampaign(payload) {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) { if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
throw new Error('You must be authenticated to create a project.'); throw new Error('You must be authenticated to create a campaign.');
} }
if (isCreating.value) { if (isCreating.value) {
throw new Error('A project creation request is already in progress.'); throw new Error('A campaign creation request is already in progress.');
} }
isCreating.value = true; isCreating.value = true;
error.value = null; error.value = null;
try { try {
const response = await client.post('/api/projects', { const response = await client.post('/api/campaigns', {
...payload, ...payload,
workspaceId: workspaceStore.activeWorkspaceId, workspaceId: workspaceStore.activeWorkspaceId,
}); });
if (response.data) { if (response.data) {
projects.value = [...projects.value, response.data] campaigns.value = [...campaigns.value, response.data]
.sort((left, right) => left.name.localeCompare(right.name)); .sort((left, right) => left.name.localeCompare(right.name));
} }
return response.data; return response.data;
} catch (createError) { } catch (createError) {
console.error('Failed to create project:', createError); console.error('Failed to create campaign:', createError);
error.value = 'Failed to create project.'; error.value = 'Failed to create campaign.';
throw createError; throw createError;
} finally { } finally {
isCreating.value = false; isCreating.value = false;
@@ -78,22 +78,22 @@ export const useProjectsStore = defineStore('projects', () => {
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId], () => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
async ([isAuthenticated, workspaceId]) => { async ([isAuthenticated, workspaceId]) => {
if (!isAuthenticated || !workspaceId) { if (!isAuthenticated || !workspaceId) {
projects.value = []; campaigns.value = [];
error.value = null; error.value = null;
return; return;
} }
await fetchProjects(); await fetchCampaigns();
}, },
{ immediate: true } { immediate: true }
); );
return { return {
projects, campaigns,
isLoading, isLoading,
isCreating, isCreating,
error, error,
fetchProjects, fetchCampaigns,
createProject, createCampaign,
}; };
}); });

View File

@@ -3,22 +3,22 @@
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAuthStore } from '@/features/auth/stores/authStore.js'; import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js'; import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
import { useProjectsStore } from '@/features/projects/stores/projectsStore.js'; import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js'; import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
const authStore = useAuthStore(); const authStore = useAuthStore();
const route = useRoute(); const route = useRoute();
const workspaceStore = useWorkspaceStore(); const workspaceStore = useWorkspaceStore();
const projectsStore = useProjectsStore(); const campaignsStore = useCampaignsStore();
const contentItemsStore = useContentItemsStore(); const contentItemsStore = useContentItemsStore();
const project = computed(() => const campaign = computed(() =>
projectsStore.projects.find(candidate => candidate.id === route.params.projectId) ?? null campaignsStore.campaigns.find(candidate => candidate.id === route.params.campaignId) ?? null
); );
const scopedItems = computed(() => const scopedItems = computed(() =>
contentItemsStore.items contentItemsStore.items
.filter(item => item.projectId === route.params.projectId) .filter(item => item.campaignId === route.params.campaignId)
.sort((left, right) => { .sort((left, right) => {
const leftDue = left.dueDate ? new Date(left.dueDate).getTime() : Number.MAX_SAFE_INTEGER; const leftDue = left.dueDate ? new Date(left.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
const rightDue = right.dueDate ? new Date(right.dueDate).getTime() : Number.MAX_SAFE_INTEGER; const rightDue = right.dueDate ? new Date(right.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
@@ -26,8 +26,8 @@
}) })
); );
function formatProjectDateRange(projectValue) { function formatCampaignDateRange(campaignValue) {
if (!projectValue?.startDate || !projectValue?.endDate) { if (!campaignValue?.startDate || !campaignValue?.endDate) {
return 'No date range'; return 'No date range';
} }
@@ -35,14 +35,14 @@
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
year: 'numeric', year: 'numeric',
}).formatRange(new Date(projectValue.startDate), new Date(projectValue.endDate)); }).formatRange(new Date(campaignValue.startDate), new Date(campaignValue.endDate));
} }
</script> </script>
<template> <template>
<section class="page-shell"> <section class="page-shell">
<div <div
v-if="!project" v-if="!campaign"
class="page-message error" class="page-message error"
> >
The selected campaign could not be found in the active workspace. The selected campaign could not be found in the active workspace.
@@ -66,21 +66,21 @@
Campaigns Campaigns
</router-link> </router-link>
</div> </div>
<h1>{{ project.name }}</h1> <h1>{{ campaign.name }}</h1>
<p>{{ project.description || `${workspaceStore.activeWorkspace?.name} delivery stream with only the content scheduled in this campaign.` }}</p> <p>{{ campaign.description || `${workspaceStore.activeWorkspace?.name} delivery stream with only the content scheduled in this campaign.` }}</p>
</div> </div>
<div class="hero-meta"> <div class="hero-meta">
<div class="meta-chip">{{ project.status }}</div> <div class="meta-chip">{{ campaign.status }}</div>
<div class="meta-copy">{{ formatProjectDateRange(project) }}</div> <div class="meta-copy">{{ formatCampaignDateRange(campaign) }}</div>
</div> </div>
</div> </div>
<div <div
v-if="project.notes" v-if="campaign.notes"
class="page-message" class="page-message"
> >
{{ project.notes }} {{ campaign.notes }}
</div> </div>
<div class="section-header"> <div class="section-header">
@@ -91,10 +91,10 @@
<div class="scope-actions"> <div class="scope-actions">
<router-link <router-link
v-if="authStore.isManager || authStore.isProvider" v-if="authStore.isManager || authStore.isProvider"
:to="{ name: 'content-item-create', query: { projectId: project.id } }" :to="{ name: 'content-item-create', query: { campaignId: campaign.id } }"
class="scope-button" class="scope-button"
> >
New content in {{ project.name }} New content in {{ campaign.name }}
</router-link> </router-link>
</div> </div>

View File

@@ -5,13 +5,13 @@
import { useAuthStore } from '@/features/auth/stores/authStore.js'; import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useClientsStore } from '@/features/clients/stores/clientsStore.js'; import { useClientsStore } from '@/features/clients/stores/clientsStore.js';
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js'; import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
import { useProjectsStore } from '@/features/projects/stores/projectsStore.js'; import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
const route = useRoute(); const route = useRoute();
const authStore = useAuthStore(); const authStore = useAuthStore();
const workspaceStore = useWorkspaceStore(); const workspaceStore = useWorkspaceStore();
const clientsStore = useClientsStore(); const clientsStore = useClientsStore();
const projectsStore = useProjectsStore(); const campaignsStore = useCampaignsStore();
const { t } = useI18n(); const { t } = useI18n();
const isCreateFormVisible = ref(false); const isCreateFormVisible = ref(false);
const formError = ref(null); const formError = ref(null);
@@ -41,29 +41,29 @@
} }
async function submitForm() { async function submitForm() {
if (projectsStore.isCreating) { if (campaignsStore.isCreating) {
return; return;
} }
formError.value = null; formError.value = null;
if (!form.name || !form.startDate || !form.endDate) { if (!form.name || !form.startDate || !form.endDate) {
formError.value = t('projects.errors.required'); formError.value = t('campaigns.errors.required');
return; return;
} }
if (new Date(form.endDate) < new Date(form.startDate)) { if (new Date(form.endDate) < new Date(form.startDate)) {
formError.value = t('projects.errors.invalidDateRange'); formError.value = t('campaigns.errors.invalidDateRange');
return; return;
} }
if (!operationalClient.value?.id) { if (!operationalClient.value?.id) {
formError.value = t('projects.errors.workspaceAccountRequired'); formError.value = t('campaigns.errors.workspaceAccountRequired');
return; return;
} }
try { try {
await projectsStore.createProject({ await campaignsStore.createCampaign({
clientId: operationalClient.value.id, clientId: operationalClient.value.id,
name: form.name, name: form.name,
startDate: new Date(form.startDate).toISOString(), startDate: new Date(form.startDate).toISOString(),
@@ -75,7 +75,7 @@
isCreateFormVisible.value = false; isCreateFormVisible.value = false;
resetForm(); resetForm();
} catch (error) { } catch (error) {
formError.value = t('projects.errors.createFailed'); formError.value = t('campaigns.errors.createFailed');
} }
} }
@@ -89,13 +89,13 @@
{ immediate: true } { immediate: true }
); );
function formatProjectDateRange(project) { function formatCampaignDateRange(campaign) {
if (!project?.startDate || !project?.endDate) { if (!campaign?.startDate || !campaign?.endDate) {
return t('projects.noDateRange'); return t('campaigns.noDateRange');
} }
const start = new Date(project.startDate); const start = new Date(campaign.startDate);
const end = new Date(project.endDate); const end = new Date(campaign.endDate);
return new Intl.DateTimeFormat(undefined, { return new Intl.DateTimeFormat(undefined, {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
@@ -108,9 +108,9 @@
<section class="page-shell"> <section class="page-shell">
<div class="header"> <div class="header">
<div> <div>
<div class="eyebrow">{{ t('projects.eyebrow') }}</div> <div class="eyebrow">{{ t('campaigns.eyebrow') }}</div>
<h1>{{ t('projects.title') }}</h1> <h1>{{ t('campaigns.title') }}</h1>
<p>{{ t('projects.description') }}</p> <p>{{ t('campaigns.description') }}</p>
</div> </div>
</div> </div>
@@ -120,7 +120,7 @@
class="create-button" class="create-button"
@click="openCreateForm" @click="openCreateForm"
> >
{{ t('projects.newProject') }} {{ t('campaigns.newCampaign') }}
</button> </button>
</div> </div>
@@ -129,7 +129,7 @@
class="create-panel" class="create-panel"
> >
<div class="panel-header"> <div class="panel-header">
<strong>{{ t('projects.createTitle') }}</strong> <strong>{{ t('campaigns.createTitle') }}</strong>
<span>{{ workspaceStore.activeWorkspace?.name }}</span> <span>{{ workspaceStore.activeWorkspace?.name }}</span>
</div> </div>
@@ -142,45 +142,45 @@
<div class="form-grid"> <div class="form-grid">
<label class="field"> <label class="field">
<span>{{ t('projects.fields.startDate') }}</span> <span>{{ t('campaigns.fields.startDate') }}</span>
<input <input
v-model="form.startDate" v-model="form.startDate"
type="date" type="date"
:disabled="projectsStore.isCreating" :disabled="campaignsStore.isCreating"
/> />
</label> </label>
<label class="field"> <label class="field">
<span>{{ t('projects.fields.endDate') }}</span> <span>{{ t('campaigns.fields.endDate') }}</span>
<input <input
v-model="form.endDate" v-model="form.endDate"
type="date" type="date"
:disabled="projectsStore.isCreating" :disabled="campaignsStore.isCreating"
/> />
</label> </label>
<label class="field field-wide"> <label class="field field-wide">
<span>{{ t('projects.fields.name') }}</span> <span>{{ t('campaigns.fields.name') }}</span>
<input <input
v-model="form.name" v-model="form.name"
type="text" type="text"
:disabled="projectsStore.isCreating" :disabled="campaignsStore.isCreating"
/> />
</label> </label>
<label class="field field-wide"> <label class="field field-wide">
<span>{{ t('projects.fields.description') }}</span> <span>{{ t('campaigns.fields.description') }}</span>
<textarea <textarea
v-model="form.description" v-model="form.description"
:disabled="projectsStore.isCreating" :disabled="campaignsStore.isCreating"
></textarea> ></textarea>
</label> </label>
<label class="field field-wide"> <label class="field field-wide">
<span>{{ t('projects.fields.notes') }}</span> <span>{{ t('campaigns.fields.notes') }}</span>
<textarea <textarea
v-model="form.notes" v-model="form.notes"
:disabled="projectsStore.isCreating" :disabled="campaignsStore.isCreating"
></textarea> ></textarea>
</label> </label>
</div> </div>
@@ -188,64 +188,64 @@
<div class="panel-actions"> <div class="panel-actions">
<button <button
class="secondary" class="secondary"
:disabled="projectsStore.isCreating" :disabled="campaignsStore.isCreating"
@click="isCreateFormVisible = false" @click="isCreateFormVisible = false"
> >
{{ t('common.cancel') }} {{ t('common.cancel') }}
</button> </button>
<button <button
class="primary" class="primary"
:disabled="projectsStore.isCreating" :disabled="campaignsStore.isCreating"
@click="submitForm" @click="submitForm"
> >
<v-progress-circular <v-progress-circular
v-if="projectsStore.isCreating" v-if="campaignsStore.isCreating"
indeterminate indeterminate
:size="16" :size="16"
:width="2" :width="2"
/> />
<span>{{ projectsStore.isCreating ? t('common.creating') : t('projects.createTitle') }}</span> <span>{{ campaignsStore.isCreating ? t('common.creating') : t('campaigns.createTitle') }}</span>
</button> </button>
</div> </div>
</div> </div>
<div <div
v-if="projectsStore.isLoading" v-if="campaignsStore.isLoading"
class="page-message" class="page-message"
> >
{{ t('projects.loading') }} {{ t('campaigns.loading') }}
</div> </div>
<div <div
v-else-if="projectsStore.error" v-else-if="campaignsStore.error"
class="page-message error" class="page-message error"
> >
{{ projectsStore.error }} {{ campaignsStore.error }}
</div> </div>
<div class="project-stack"> <div class="campaign-stack">
<router-link <router-link
v-for="project in projectsStore.projects" v-for="campaign in campaignsStore.campaigns"
:key="project.id" :key="campaign.id"
:to="{ name: 'campaign-detail', params: { projectId: project.id } }" :to="{ name: 'campaign-detail', params: { campaignId: campaign.id } }"
class="project-row" class="campaign-row"
> >
<div> <div>
<strong>{{ project.name }}</strong> <strong>{{ campaign.name }}</strong>
<span>{{ project.description || project.status }}</span> <span>{{ campaign.description || campaign.status }}</span>
</div> </div>
<div class="project-meta"> <div class="campaign-meta">
<span>{{ workspaceStore.activeWorkspace?.name || t('nav.noWorkspace') }}</span> <span>{{ workspaceStore.activeWorkspace?.name || t('nav.noWorkspace') }}</span>
<em>{{ formatProjectDateRange(project) }}</em> <em>{{ formatCampaignDateRange(campaign) }}</em>
</div> </div>
</router-link> </router-link>
</div> </div>
<div <div
v-if="!projectsStore.isLoading && !projectsStore.projects.length" v-if="!campaignsStore.isLoading && !campaignsStore.campaigns.length"
class="page-message" class="page-message"
> >
{{ t('projects.empty') }} {{ t('campaigns.empty') }}
</div> </div>
</section> </section>
</template> </template>
@@ -267,9 +267,9 @@
.header p, .header p,
.panel-header span, .panel-header span,
.project-row span, .campaign-row span,
.project-meta span, .campaign-meta span,
.project-meta em { .campaign-meta em {
@apply text-sm leading-6 not-italic; @apply text-sm leading-6 not-italic;
color: #526178; color: #526178;
} }
@@ -296,7 +296,7 @@
} }
.create-panel, .create-panel,
.project-row { .campaign-row {
@apply rounded-[1.5rem] border; @apply rounded-[1.5rem] border;
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
border-color: rgba(23, 32, 51, 0.08); border-color: rgba(23, 32, 51, 0.08);
@@ -311,7 +311,7 @@
} }
.panel-header strong, .panel-header strong,
.project-row strong { .campaign-row strong {
color: #172033; color: #172033;
} }
@@ -347,19 +347,19 @@
@apply flex justify-end gap-3; @apply flex justify-end gap-3;
} }
.project-stack { .campaign-stack {
@apply flex flex-col gap-4; @apply flex flex-col gap-4;
} }
.project-row { .campaign-row {
@apply flex flex-col justify-between gap-4 p-5 no-underline lg:flex-row lg:items-center; @apply flex flex-col justify-between gap-4 p-5 no-underline lg:flex-row lg:items-center;
} }
.project-row strong { .campaign-row strong {
@apply block text-xl font-black; @apply block text-xl font-black;
} }
.project-meta { .campaign-meta {
@apply flex flex-col items-start gap-1 lg:items-end; @apply flex flex-col items-start gap-1 lg:items-end;
} }

View File

@@ -5,13 +5,13 @@
import ImageCropperDialog from '@/components/ImageCropperDialog.vue'; import ImageCropperDialog from '@/components/ImageCropperDialog.vue';
import { useAuthStore } from '@/features/auth/stores/authStore.js'; import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useClientsStore } from '@/features/clients/stores/clientsStore.js'; import { useClientsStore } from '@/features/clients/stores/clientsStore.js';
import { useProjectsStore } from '@/features/projects/stores/projectsStore.js'; import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js'; import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
const authStore = useAuthStore(); const authStore = useAuthStore();
const route = useRoute(); const route = useRoute();
const clientsStore = useClientsStore(); const clientsStore = useClientsStore();
const projectsStore = useProjectsStore(); const campaignsStore = useCampaignsStore();
const contentItemsStore = useContentItemsStore(); const contentItemsStore = useContentItemsStore();
const isEditFormVisible = ref(false); const isEditFormVisible = ref(false);
const isPortraitDialogOpen = ref(false); const isPortraitDialogOpen = ref(false);
@@ -48,9 +48,9 @@
clientsStore.clients.find(candidate => candidate.id === route.params.clientId) ?? null clientsStore.clients.find(candidate => candidate.id === route.params.clientId) ?? null
); );
const scopedProjects = computed(() => const scopedCampaigns = computed(() =>
projectsStore.projects campaignsStore.campaigns
.filter(project => project.clientId === route.params.clientId) .filter(campaign => campaign.clientId === route.params.clientId)
.sort((left, right) => { .sort((left, right) => {
const leftDue = left.endDate ? new Date(left.endDate).getTime() : Number.MAX_SAFE_INTEGER; const leftDue = left.endDate ? new Date(left.endDate).getTime() : Number.MAX_SAFE_INTEGER;
const rightDue = right.endDate ? new Date(right.endDate).getTime() : Number.MAX_SAFE_INTEGER; const rightDue = right.endDate ? new Date(right.endDate).getTime() : Number.MAX_SAFE_INTEGER;
@@ -58,26 +58,26 @@
}) })
); );
const currentProjects = computed(() => const currentCampaigns = computed(() =>
scopedProjects.value.filter(project => project.status !== 'Completed' && project.status !== 'Archived') scopedCampaigns.value.filter(campaign => campaign.status !== 'Completed' && campaign.status !== 'Archived')
); );
const pastProjects = computed(() => const pastCampaigns = computed(() =>
scopedProjects.value.filter(project => project.status === 'Completed' || project.status === 'Archived') scopedCampaigns.value.filter(campaign => campaign.status === 'Completed' || campaign.status === 'Archived')
); );
const itemCountByProjectId = computed(() => { const itemCountByCampaignId = computed(() => {
const counts = new Map(); const counts = new Map();
for (const item of contentItemsStore.items.filter(candidate => candidate.clientId === route.params.clientId)) { for (const item of contentItemsStore.items.filter(candidate => candidate.clientId === route.params.clientId)) {
counts.set(item.projectId, (counts.get(item.projectId) ?? 0) + 1); counts.set(item.campaignId, (counts.get(item.campaignId) ?? 0) + 1);
} }
return counts; return counts;
}); });
function formatProjectDateRange(project) { function formatCampaignDateRange(campaign) {
if (!project?.startDate || !project?.endDate) { if (!campaign?.startDate || !campaign?.endDate) {
return 'No date range'; return 'No date range';
} }
@@ -85,7 +85,7 @@
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
year: 'numeric', year: 'numeric',
}).formatRange(new Date(project.startDate), new Date(project.endDate)); }).formatRange(new Date(campaign.startDate), new Date(campaign.endDate));
} }
function syncForm() { function syncForm() {
@@ -188,18 +188,18 @@
<div class="hero-meta"> <div class="hero-meta">
<span class="hero-status">{{ client.status }}</span> <span class="hero-status">{{ client.status }}</span>
</div> </div>
<p>The client area scopes projects and content so review stays inside one account.</p> <p>The client area scopes campaigns and content so review stays inside one account.</p>
</div> </div>
</div> </div>
<div class="stats-grid"> <div class="stats-grid">
<article class="stat-card"> <article class="stat-card">
<span>Current campaigns</span> <span>Current campaigns</span>
<strong>{{ currentProjects.length }}</strong> <strong>{{ currentCampaigns.length }}</strong>
</article> </article>
<article class="stat-card"> <article class="stat-card">
<span>Past campaigns</span> <span>Past campaigns</span>
<strong>{{ pastProjects.length }}</strong> <strong>{{ pastCampaigns.length }}</strong>
</article> </article>
<article class="stat-card"> <article class="stat-card">
<span>Total content items</span> <span>Total content items</span>
@@ -420,26 +420,26 @@
<div class="section"> <div class="section">
<div class="section-header"> <div class="section-header">
<strong>Current campaigns</strong> <strong>Current campaigns</strong>
<span>{{ currentProjects.length }} active</span> <span>{{ currentCampaigns.length }} active</span>
</div> </div>
<div <div
v-if="currentProjects.length" v-if="currentCampaigns.length"
class="project-list" class="campaign-list"
> >
<router-link <router-link
v-for="project in currentProjects" v-for="campaign in currentCampaigns"
:key="project.id" :key="campaign.id"
:to="{ name: 'client-project-detail', params: { clientId: client.id, projectId: project.id } }" :to="{ name: 'campaign-detail', params: { campaignId: campaign.id } }"
class="project-card" class="campaign-card"
> >
<div> <div>
<strong>{{ project.name }}</strong> <strong>{{ campaign.name }}</strong>
<span>{{ project.status }}</span> <span>{{ campaign.status }}</span>
</div> </div>
<div class="project-meta"> <div class="campaign-meta">
<small>{{ itemCountByProjectId.get(project.id) ?? 0 }} content items</small> <small>{{ itemCountByCampaignId.get(campaign.id) ?? 0 }} content items</small>
<em>{{ formatProjectDateRange(project) }}</em> <em>{{ formatCampaignDateRange(campaign) }}</em>
</div> </div>
</router-link> </router-link>
</div> </div>
@@ -454,26 +454,26 @@
<div class="section"> <div class="section">
<div class="section-header"> <div class="section-header">
<strong>Past campaigns</strong> <strong>Past campaigns</strong>
<span>{{ pastProjects.length }} archived or completed</span> <span>{{ pastCampaigns.length }} archived or completed</span>
</div> </div>
<div <div
v-if="pastProjects.length" v-if="pastCampaigns.length"
class="project-list" class="campaign-list"
> >
<router-link <router-link
v-for="project in pastProjects" v-for="campaign in pastCampaigns"
:key="project.id" :key="campaign.id"
:to="{ name: 'client-project-detail', params: { clientId: client.id, projectId: project.id } }" :to="{ name: 'campaign-detail', params: { campaignId: campaign.id } }"
class="project-card muted" class="campaign-card muted"
> >
<div> <div>
<strong>{{ project.name }}</strong> <strong>{{ campaign.name }}</strong>
<span>{{ project.status }}</span> <span>{{ campaign.status }}</span>
</div> </div>
<div class="project-meta"> <div class="campaign-meta">
<small>{{ itemCountByProjectId.get(project.id) ?? 0 }} content items</small> <small>{{ itemCountByCampaignId.get(campaign.id) ?? 0 }} content items</small>
<em>{{ formatProjectDateRange(project) }}</em> <em>{{ formatCampaignDateRange(campaign) }}</em>
</div> </div>
</router-link> </router-link>
</div> </div>
@@ -489,7 +489,7 @@
.hero, .hero,
.stat-card, .stat-card,
.project-card { .campaign-card {
@apply rounded-[1.5rem] border; @apply rounded-[1.5rem] border;
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
border-color: rgba(23, 32, 51, 0.08); border-color: rgba(23, 32, 51, 0.08);
@@ -501,7 +501,7 @@
.hero-main h1, .hero-main h1,
.stat-card strong, .stat-card strong,
.project-card strong, .campaign-card strong,
.contact-card strong { .contact-card strong {
color: #172033; color: #172033;
} }
@@ -513,9 +513,9 @@
.hero-main p, .hero-main p,
.breadcrumb, .breadcrumb,
.stat-card span, .stat-card span,
.project-card span, .campaign-card span,
.project-card small, .campaign-card small,
.project-card em, .campaign-card em,
.section-header span { .section-header span {
@apply text-sm leading-6 not-italic; @apply text-sm leading-6 not-italic;
color: #526178; color: #526178;
@@ -675,27 +675,27 @@
color: #172033; color: #172033;
} }
.project-list { .campaign-list {
@apply grid gap-4 md:grid-cols-2; @apply grid gap-4 md:grid-cols-2;
} }
.project-card { .campaign-card {
@apply flex flex-col gap-4 p-5 no-underline transition; @apply flex flex-col gap-4 p-5 no-underline transition;
} }
.project-card:hover { .campaign-card:hover {
transform: translateY(-2px); transform: translateY(-2px);
} }
.project-card.muted { .campaign-card.muted {
background: rgba(255, 250, 242, 0.88); background: rgba(255, 250, 242, 0.88);
} }
.project-card span { .campaign-card span {
@apply uppercase tracking-[0.16em]; @apply uppercase tracking-[0.16em];
} }
.project-meta { .campaign-meta {
@apply flex items-center justify-between gap-3; @apply flex items-center justify-between gap-3;
} }

View File

@@ -34,7 +34,7 @@ export const useContentItemsStore = defineStore('content-items', () => {
params: { params: {
workspaceId: workspaceStore.activeWorkspaceId, workspaceId: workspaceStore.activeWorkspaceId,
clientId: filters.clientId, clientId: filters.clientId,
projectId: filters.projectId, campaignId: filters.campaignId,
}, },
}); });

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