Compare commits
1 Commits
feat/prepr
...
feat/googl
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fbb30bb4f |
@@ -15,6 +15,7 @@ using Socialize.Api.Modules.Campaigns.Data;
|
|||||||
using Socialize.Api.Modules.Organizations.Data;
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
using Socialize.Api.Modules.Organizations.Services;
|
using Socialize.Api.Modules.Organizations.Services;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Services;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.TestData;
|
namespace Socialize.Api.Infrastructure.TestData;
|
||||||
@@ -261,6 +262,10 @@ internal static class TestDataSeedExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
organization.Name = "Northstar Agency";
|
organization.Name = "Northstar Agency";
|
||||||
|
organization.IsGoogleDriveDamEnabled = true;
|
||||||
|
organization.GoogleDriveRootFolderId = "dev-socialize-dam-root";
|
||||||
|
organization.GoogleDriveRootFolderName = "Socialize DAM";
|
||||||
|
organization.GoogleDriveRootFolderUrl = "https://drive.google.com/drive/folders/dev-socialize-dam-root";
|
||||||
organization.MembershipTierId = OrganizationMembershipTierSeed.AgencyId;
|
organization.MembershipTierId = OrganizationMembershipTierSeed.AgencyId;
|
||||||
organization.OwnerUserId = managerUserId;
|
organization.OwnerUserId = managerUserId;
|
||||||
|
|
||||||
@@ -465,6 +470,7 @@ internal static class TestDataSeedExtensions
|
|||||||
asset.DisplayName = "Spring launch cut";
|
asset.DisplayName = "Spring launch cut";
|
||||||
asset.GoogleDriveFileId = "dev-socialize-demo";
|
asset.GoogleDriveFileId = "dev-socialize-demo";
|
||||||
asset.GoogleDriveLink = "https://drive.google.com/file/d/dev-socialize-demo/view";
|
asset.GoogleDriveLink = "https://drive.google.com/file/d/dev-socialize-demo/view";
|
||||||
|
asset.GoogleDriveWorkspaceFolderPath = "Socialize DAM/luma-coffee";
|
||||||
asset.PreviewUrl = "https://drive.google.com/thumbnail?id=dev-socialize-demo";
|
asset.PreviewUrl = "https://drive.google.com/thumbnail?id=dev-socialize-demo";
|
||||||
asset.CurrentRevisionNumber = 2;
|
asset.CurrentRevisionNumber = 2;
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
@@ -587,6 +593,7 @@ internal static class TestDataSeedExtensions
|
|||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Name = string.Empty,
|
Name = string.Empty,
|
||||||
|
Slug = string.Empty,
|
||||||
TimeZone = string.Empty,
|
TimeZone = string.Empty,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
@@ -594,6 +601,12 @@ internal static class TestDataSeedExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspace.Name = name;
|
workspace.Name = name;
|
||||||
|
workspace.Slug = await WorkspaceSlugGenerator.CreateUniqueAsync(
|
||||||
|
dbContext,
|
||||||
|
organizationId,
|
||||||
|
string.IsNullOrWhiteSpace(workspace.Slug) ? name : workspace.Slug,
|
||||||
|
workspace.Id,
|
||||||
|
cancellationToken);
|
||||||
workspace.OrganizationId = organizationId;
|
workspace.OrganizationId = organizationId;
|
||||||
workspace.OwnerUserId = ownerUserId;
|
workspace.OwnerUserId = ownerUserId;
|
||||||
workspace.TimeZone = timeZone;
|
workspace.TimeZone = timeZone;
|
||||||
|
|||||||
2657
backend/src/Socialize.Api/Migrations/20260508152102_AddGoogleDriveDamFoundation.Designer.cs
generated
Normal file
2657
backend/src/Socialize.Api/Migrations/20260508152102_AddGoogleDriveDamFoundation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddGoogleDriveDamFoundation : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] WorkspaceOrganizationSlugIndexColumns =
|
||||||
|
[
|
||||||
|
"OrganizationId",
|
||||||
|
"Slug",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Slug",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "character varying(96)",
|
||||||
|
maxLength: 96,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
WITH normalized AS (
|
||||||
|
SELECT
|
||||||
|
"Id",
|
||||||
|
"OrganizationId",
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(
|
||||||
|
trim(both '-' from lower(regexp_replace(trim("Name"), '[^a-zA-Z0-9]+', '-', 'g'))),
|
||||||
|
''
|
||||||
|
),
|
||||||
|
'workspace'
|
||||||
|
) AS "BaseSlug"
|
||||||
|
FROM "Workspaces"
|
||||||
|
),
|
||||||
|
numbered AS (
|
||||||
|
SELECT
|
||||||
|
"Id",
|
||||||
|
"BaseSlug",
|
||||||
|
row_number() OVER (PARTITION BY "OrganizationId", "BaseSlug" ORDER BY "CreatedAt", "Id") AS "SlugIndex"
|
||||||
|
FROM normalized
|
||||||
|
)
|
||||||
|
UPDATE "Workspaces"
|
||||||
|
SET "Slug" = left(
|
||||||
|
CASE
|
||||||
|
WHEN numbered."SlugIndex" = 1 THEN numbered."BaseSlug"
|
||||||
|
ELSE numbered."BaseSlug" || '-' || numbered."SlugIndex"
|
||||||
|
END,
|
||||||
|
96
|
||||||
|
)
|
||||||
|
FROM numbered
|
||||||
|
WHERE "Workspaces"."Id" = numbered."Id";
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleDriveRootFolderId",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleDriveRootFolderName",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleDriveRootFolderUrl",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "character varying(2048)",
|
||||||
|
maxLength: 2048,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsGoogleDriveDamEnabled",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleDriveWorkspaceFolderPath",
|
||||||
|
table: "Assets",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Workspaces_OrganizationId_Slug",
|
||||||
|
table: "Workspaces",
|
||||||
|
columns: WorkspaceOrganizationSlugIndexColumns,
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Workspaces_OrganizationId_Slug",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Slug",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleDriveRootFolderId",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleDriveRootFolderName",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleDriveRootFolderUrl",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsGoogleDriveDamEnabled",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleDriveWorkspaceFolderPath",
|
||||||
|
table: "Assets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -374,6 +374,10 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleDriveWorkspaceFolderPath")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
b.Property<string>("PreviewUrl")
|
b.Property<string>("PreviewUrl")
|
||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
@@ -1658,6 +1662,23 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleDriveRootFolderId")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleDriveRootFolderName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleDriveRootFolderUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGoogleDriveDamEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("LogoUrl")
|
b.Property<string>("LogoUrl")
|
||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
@@ -2167,6 +2188,11 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(96)
|
||||||
|
.HasColumnType("character varying(96)");
|
||||||
|
|
||||||
b.Property<string>("TimeZone")
|
b.Property<string>("TimeZone")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
@@ -2178,6 +2204,9 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasIndex("OwnerUserId");
|
b.HasIndex("OwnerUserId");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId", "Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Workspaces", (string)null);
|
b.ToTable("Workspaces", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ internal class Asset
|
|||||||
public required string DisplayName { get; set; }
|
public required string DisplayName { get; set; }
|
||||||
public string? GoogleDriveFileId { get; set; }
|
public string? GoogleDriveFileId { get; set; }
|
||||||
public string? GoogleDriveLink { get; set; }
|
public string? GoogleDriveLink { get; set; }
|
||||||
|
public string? GoogleDriveWorkspaceFolderPath { get; set; }
|
||||||
public string? PreviewUrl { get; set; }
|
public string? PreviewUrl { get; set; }
|
||||||
public int CurrentRevisionNumber { get; set; }
|
public int CurrentRevisionNumber { get; set; }
|
||||||
public DateTimeOffset CreatedAt { get; init; }
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ internal static class AssetModelConfiguration
|
|||||||
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
|
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
|
||||||
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
|
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
|
||||||
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
|
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
|
||||||
|
asset.Property(x => x.GoogleDriveWorkspaceFolderPath).HasMaxLength(512);
|
||||||
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
||||||
asset.Property(x => x.CreatedAt)
|
asset.Property(x => x.CreatedAt)
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using Socialize.Api.Modules.Assets.Data;
|
|||||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
@@ -67,6 +69,26 @@ internal class CreateGoogleDriveAssetHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Workspace? workspace = await dbContext.Workspaces
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Organization? organization = await dbContext.Organizations
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == workspace.OrganizationId, ct);
|
||||||
|
if (organization is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? workspaceFolderPath = organization.IsGoogleDriveDamEnabled
|
||||||
|
? $"{organization.GoogleDriveRootFolderName}/{workspace.Slug}"
|
||||||
|
: null;
|
||||||
|
|
||||||
Asset asset = new()
|
Asset asset = new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
@@ -77,6 +99,7 @@ internal class CreateGoogleDriveAssetHandler(
|
|||||||
DisplayName = request.DisplayName.Trim(),
|
DisplayName = request.DisplayName.Trim(),
|
||||||
GoogleDriveFileId = request.GoogleDriveFileId.Trim(),
|
GoogleDriveFileId = request.GoogleDriveFileId.Trim(),
|
||||||
GoogleDriveLink = request.GoogleDriveLink.Trim(),
|
GoogleDriveLink = request.GoogleDriveLink.Trim(),
|
||||||
|
GoogleDriveWorkspaceFolderPath = workspaceFolderPath,
|
||||||
PreviewUrl = string.IsNullOrWhiteSpace(request.PreviewUrl) ? null : request.PreviewUrl.Trim(),
|
PreviewUrl = string.IsNullOrWhiteSpace(request.PreviewUrl) ? null : request.PreviewUrl.Trim(),
|
||||||
CurrentRevisionNumber = 1,
|
CurrentRevisionNumber = 1,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
@@ -111,6 +134,7 @@ internal class CreateGoogleDriveAssetHandler(
|
|||||||
assetType = asset.AssetType,
|
assetType = asset.AssetType,
|
||||||
sourceType = asset.SourceType,
|
sourceType = asset.SourceType,
|
||||||
googleDriveFileId = asset.GoogleDriveFileId,
|
googleDriveFileId = asset.GoogleDriveFileId,
|
||||||
|
googleDriveWorkspaceFolderPath = asset.GoogleDriveWorkspaceFolderPath,
|
||||||
currentRevisionNumber = asset.CurrentRevisionNumber,
|
currentRevisionNumber = asset.CurrentRevisionNumber,
|
||||||
})),
|
})),
|
||||||
ct);
|
ct);
|
||||||
@@ -137,6 +161,7 @@ internal class CreateGoogleDriveAssetHandler(
|
|||||||
asset.DisplayName,
|
asset.DisplayName,
|
||||||
asset.GoogleDriveFileId,
|
asset.GoogleDriveFileId,
|
||||||
asset.GoogleDriveLink,
|
asset.GoogleDriveLink,
|
||||||
|
asset.GoogleDriveWorkspaceFolderPath,
|
||||||
asset.PreviewUrl,
|
asset.PreviewUrl,
|
||||||
asset.CurrentRevisionNumber,
|
asset.CurrentRevisionNumber,
|
||||||
asset.CreatedAt,
|
asset.CreatedAt,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ internal record AssetDto(
|
|||||||
string DisplayName,
|
string DisplayName,
|
||||||
string? GoogleDriveFileId,
|
string? GoogleDriveFileId,
|
||||||
string? GoogleDriveLink,
|
string? GoogleDriveLink,
|
||||||
|
string? GoogleDriveWorkspaceFolderPath,
|
||||||
string? PreviewUrl,
|
string? PreviewUrl,
|
||||||
int CurrentRevisionNumber,
|
int CurrentRevisionNumber,
|
||||||
DateTimeOffset CreatedAt,
|
DateTimeOffset CreatedAt,
|
||||||
@@ -70,6 +71,7 @@ internal class GetAssetsHandler(
|
|||||||
asset.DisplayName,
|
asset.DisplayName,
|
||||||
asset.GoogleDriveFileId,
|
asset.GoogleDriveFileId,
|
||||||
asset.GoogleDriveLink,
|
asset.GoogleDriveLink,
|
||||||
|
asset.GoogleDriveWorkspaceFolderPath,
|
||||||
asset.PreviewUrl,
|
asset.PreviewUrl,
|
||||||
asset.CurrentRevisionNumber,
|
asset.CurrentRevisionNumber,
|
||||||
asset.CreatedAt,
|
asset.CreatedAt,
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
|
internal record WorkspaceDamBackingStoreDto(
|
||||||
|
string Type,
|
||||||
|
bool IsConfigured,
|
||||||
|
string? RootFolderId,
|
||||||
|
string? RootFolderName,
|
||||||
|
string? RootFolderUrl);
|
||||||
|
|
||||||
|
internal record WorkspaceDamFolderDto(
|
||||||
|
string Name,
|
||||||
|
string Path);
|
||||||
|
|
||||||
|
internal record WorkspaceDamDto(
|
||||||
|
Guid WorkspaceId,
|
||||||
|
Guid OrganizationId,
|
||||||
|
string WorkspaceName,
|
||||||
|
string WorkspaceSlug,
|
||||||
|
WorkspaceDamBackingStoreDto BackingStore,
|
||||||
|
WorkspaceDamFolderDto? Folder,
|
||||||
|
IReadOnlyCollection<AssetDto> Assets);
|
||||||
|
|
||||||
|
internal class GetWorkspaceDamHandler(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
AccessScopeService accessScopeService)
|
||||||
|
: EndpointWithoutRequest<WorkspaceDamDto>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Get("/api/workspaces/{workspaceId:guid}/dam");
|
||||||
|
Options(o => o.WithTags("Assets"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
Guid workspaceId = Route<Guid>("workspaceId");
|
||||||
|
|
||||||
|
Workspace? workspace = await dbContext.Workspaces
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == workspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyCollection<Guid> accessibleWorkspaceIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||||
|
if (!accessibleWorkspaceIds.Contains(workspace.Id))
|
||||||
|
{
|
||||||
|
await SendForbiddenAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Organization? organization = await dbContext.Organizations
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == workspace.OrganizationId, ct);
|
||||||
|
if (organization is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceDamBackingStoreDto backingStore = new(
|
||||||
|
organization.IsGoogleDriveDamEnabled ? "GoogleDrive" : "Unconfigured",
|
||||||
|
organization.IsGoogleDriveDamEnabled,
|
||||||
|
organization.GoogleDriveRootFolderId,
|
||||||
|
organization.GoogleDriveRootFolderName,
|
||||||
|
organization.GoogleDriveRootFolderUrl);
|
||||||
|
|
||||||
|
WorkspaceDamFolderDto? folder = organization.IsGoogleDriveDamEnabled
|
||||||
|
? new WorkspaceDamFolderDto(
|
||||||
|
workspace.Slug,
|
||||||
|
$"{organization.GoogleDriveRootFolderName}/{workspace.Slug}")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
List<AssetDto> assets = await dbContext.Assets
|
||||||
|
.Where(asset => asset.WorkspaceId == workspace.Id)
|
||||||
|
.OrderBy(asset => asset.DisplayName)
|
||||||
|
.Select(asset => new AssetDto(
|
||||||
|
asset.Id,
|
||||||
|
asset.WorkspaceId,
|
||||||
|
asset.ContentItemId,
|
||||||
|
asset.AssetType,
|
||||||
|
asset.SourceType,
|
||||||
|
asset.DisplayName,
|
||||||
|
asset.GoogleDriveFileId,
|
||||||
|
asset.GoogleDriveLink,
|
||||||
|
asset.GoogleDriveWorkspaceFolderPath,
|
||||||
|
asset.PreviewUrl,
|
||||||
|
asset.CurrentRevisionNumber,
|
||||||
|
asset.CreatedAt,
|
||||||
|
dbContext.AssetRevisions
|
||||||
|
.Where(revision => revision.AssetId == asset.Id)
|
||||||
|
.OrderByDescending(revision => revision.RevisionNumber)
|
||||||
|
.Select(revision => new AssetRevisionDto(
|
||||||
|
revision.Id,
|
||||||
|
revision.AssetId,
|
||||||
|
revision.RevisionNumber,
|
||||||
|
revision.SourceReference,
|
||||||
|
revision.PreviewUrl,
|
||||||
|
revision.Notes,
|
||||||
|
revision.CreatedByUserId,
|
||||||
|
revision.CreatedAt))
|
||||||
|
.ToList()))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
await SendOkAsync(
|
||||||
|
new WorkspaceDamDto(
|
||||||
|
workspace.Id,
|
||||||
|
workspace.OrganizationId,
|
||||||
|
workspace.Name,
|
||||||
|
workspace.Slug,
|
||||||
|
backingStore,
|
||||||
|
folder,
|
||||||
|
assets),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,10 @@ internal class Organization
|
|||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
public string? LogoUrl { get; set; }
|
public string? LogoUrl { get; set; }
|
||||||
|
public bool IsGoogleDriveDamEnabled { get; set; }
|
||||||
|
public string? GoogleDriveRootFolderId { get; set; }
|
||||||
|
public string? GoogleDriveRootFolderName { get; set; }
|
||||||
|
public string? GoogleDriveRootFolderUrl { get; set; }
|
||||||
public Guid MembershipTierId { get; set; } = OrganizationMembershipTierSeed.FreeId;
|
public Guid MembershipTierId { get; set; } = OrganizationMembershipTierSeed.FreeId;
|
||||||
public Guid OwnerUserId { get; set; }
|
public Guid OwnerUserId { get; set; }
|
||||||
public DateTimeOffset CreatedAt { get; init; }
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ internal static class OrganizationModelConfiguration
|
|||||||
organization.HasKey(x => x.Id);
|
organization.HasKey(x => x.Id);
|
||||||
organization.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
organization.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||||
organization.Property(x => x.LogoUrl).HasMaxLength(2048);
|
organization.Property(x => x.LogoUrl).HasMaxLength(2048);
|
||||||
|
organization.Property(x => x.IsGoogleDriveDamEnabled).HasDefaultValue(false);
|
||||||
|
organization.Property(x => x.GoogleDriveRootFolderId).HasMaxLength(256);
|
||||||
|
organization.Property(x => x.GoogleDriveRootFolderName).HasMaxLength(256);
|
||||||
|
organization.Property(x => x.GoogleDriveRootFolderUrl).HasMaxLength(2048);
|
||||||
organization.Property(x => x.MembershipTierId)
|
organization.Property(x => x.MembershipTierId)
|
||||||
.HasDefaultValue(OrganizationMembershipTierSeed.FreeId);
|
.HasDefaultValue(OrganizationMembershipTierSeed.FreeId);
|
||||||
organization.Property(x => x.CreatedAt)
|
organization.Property(x => x.CreatedAt)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ internal record OrganizationDto(
|
|||||||
Guid Id,
|
Guid Id,
|
||||||
string Name,
|
string Name,
|
||||||
string? LogoUrl,
|
string? LogoUrl,
|
||||||
|
OrganizationGoogleDriveDamConfigurationDto GoogleDriveDam,
|
||||||
OrganizationMembershipTierDto? MembershipTier,
|
OrganizationMembershipTierDto? MembershipTier,
|
||||||
Guid OwnerUserId,
|
Guid OwnerUserId,
|
||||||
IReadOnlyCollection<string> CurrentUserPermissions,
|
IReadOnlyCollection<string> CurrentUserPermissions,
|
||||||
@@ -38,6 +39,7 @@ internal record OrganizationDto(
|
|||||||
organization.Id,
|
organization.Id,
|
||||||
organization.Name,
|
organization.Name,
|
||||||
organization.LogoUrl,
|
organization.LogoUrl,
|
||||||
|
OrganizationGoogleDriveDamConfigurationDto.FromOrganization(organization),
|
||||||
membershipTier,
|
membershipTier,
|
||||||
organization.OwnerUserId,
|
organization.OwnerUserId,
|
||||||
currentUserPermissions,
|
currentUserPermissions,
|
||||||
@@ -49,6 +51,22 @@ internal record OrganizationDto(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal record OrganizationGoogleDriveDamConfigurationDto(
|
||||||
|
bool IsEnabled,
|
||||||
|
string? RootFolderId,
|
||||||
|
string? RootFolderName,
|
||||||
|
string? RootFolderUrl)
|
||||||
|
{
|
||||||
|
public static OrganizationGoogleDriveDamConfigurationDto FromOrganization(Organization organization)
|
||||||
|
{
|
||||||
|
return new OrganizationGoogleDriveDamConfigurationDto(
|
||||||
|
organization.IsGoogleDriveDamEnabled,
|
||||||
|
organization.GoogleDriveRootFolderId,
|
||||||
|
organization.GoogleDriveRootFolderName,
|
||||||
|
organization.GoogleDriveRootFolderUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal record OrganizationMembershipTierDto(
|
internal record OrganizationMembershipTierDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
string Key,
|
string Key,
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.Organizations.Services;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Organizations.Handlers;
|
||||||
|
|
||||||
|
internal record UpdateGoogleDriveDamConfigurationRequest(
|
||||||
|
bool IsEnabled,
|
||||||
|
string? RootFolderId,
|
||||||
|
string? RootFolderName,
|
||||||
|
string? RootFolderUrl);
|
||||||
|
|
||||||
|
internal class UpdateGoogleDriveDamConfigurationRequestValidator
|
||||||
|
: Validator<UpdateGoogleDriveDamConfigurationRequest>
|
||||||
|
{
|
||||||
|
public UpdateGoogleDriveDamConfigurationRequestValidator()
|
||||||
|
{
|
||||||
|
When(x => x.IsEnabled, () =>
|
||||||
|
{
|
||||||
|
RuleFor(x => x.RootFolderId).NotEmpty().MaximumLength(256);
|
||||||
|
RuleFor(x => x.RootFolderName).NotEmpty().MaximumLength(256);
|
||||||
|
RuleFor(x => x.RootFolderUrl).NotEmpty().MaximumLength(2048);
|
||||||
|
});
|
||||||
|
|
||||||
|
RuleFor(x => x.RootFolderId).MaximumLength(256);
|
||||||
|
RuleFor(x => x.RootFolderName).MaximumLength(256);
|
||||||
|
RuleFor(x => x.RootFolderUrl).MaximumLength(2048);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class UpdateGoogleDriveDamConfigurationHandler(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
OrganizationAccessService organizationAccessService)
|
||||||
|
: Endpoint<UpdateGoogleDriveDamConfigurationRequest, OrganizationGoogleDriveDamConfigurationDto>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Put("/api/organizations/{organizationId:guid}/google-drive-dam");
|
||||||
|
Options(o => o.WithTags("Organizations"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(UpdateGoogleDriveDamConfigurationRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
Guid organizationId = Route<Guid>("organizationId");
|
||||||
|
|
||||||
|
Organization? organization = await dbContext.Organizations
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == organizationId, ct);
|
||||||
|
if (organization is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await organizationAccessService.HasOrganizationPermissionAsync(
|
||||||
|
User,
|
||||||
|
organizationId,
|
||||||
|
OrganizationPermissions.ManageConnectors,
|
||||||
|
ct))
|
||||||
|
{
|
||||||
|
await SendForbiddenAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
organization.IsGoogleDriveDamEnabled = request.IsEnabled;
|
||||||
|
organization.GoogleDriveRootFolderId = NormalizeOptional(request.RootFolderId);
|
||||||
|
organization.GoogleDriveRootFolderName = NormalizeOptional(request.RootFolderName);
|
||||||
|
organization.GoogleDriveRootFolderUrl = NormalizeOptional(request.RootFolderUrl);
|
||||||
|
|
||||||
|
if (!organization.IsGoogleDriveDamEnabled)
|
||||||
|
{
|
||||||
|
organization.GoogleDriveRootFolderId = null;
|
||||||
|
organization.GoogleDriveRootFolderName = null;
|
||||||
|
organization.GoogleDriveRootFolderUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
await SendOkAsync(OrganizationGoogleDriveDamConfigurationDto.FromOrganization(organization), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptional(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ internal class Workspace
|
|||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
public required string Slug { get; set; }
|
||||||
public string? LogoUrl { get; set; }
|
public string? LogoUrl { get; set; }
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
public Guid OwnerUserId { get; set; }
|
public Guid OwnerUserId { get; set; }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ internal static class WorkspaceModelConfiguration
|
|||||||
workspace.ToTable("Workspaces");
|
workspace.ToTable("Workspaces");
|
||||||
workspace.HasKey(x => x.Id);
|
workspace.HasKey(x => x.Id);
|
||||||
workspace.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
workspace.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||||
|
workspace.Property(x => x.Slug).HasMaxLength(96).IsRequired();
|
||||||
workspace.Property(x => x.LogoUrl).HasMaxLength(2048);
|
workspace.Property(x => x.LogoUrl).HasMaxLength(2048);
|
||||||
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
|
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
|
||||||
workspace.Property(x => x.ApprovalMode).HasMaxLength(32).IsRequired().HasDefaultValue("Required");
|
workspace.Property(x => x.ApprovalMode).HasMaxLength(32).IsRequired().HasDefaultValue("Required");
|
||||||
@@ -22,6 +23,7 @@ internal static class WorkspaceModelConfiguration
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
workspace.HasIndex(x => x.OrganizationId);
|
workspace.HasIndex(x => x.OrganizationId);
|
||||||
|
workspace.HasIndex(x => new { x.OrganizationId, x.Slug }).IsUnique();
|
||||||
workspace.HasIndex(x => x.OwnerUserId);
|
workspace.HasIndex(x => x.OwnerUserId);
|
||||||
workspace.HasOne<Organization>()
|
workspace.HasOne<Organization>()
|
||||||
.WithMany()
|
.WithMany()
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ 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.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Services;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
||||||
|
|
||||||
internal record CreateWorkspaceRequest(
|
internal record CreateWorkspaceRequest(
|
||||||
Guid OrganizationId,
|
Guid OrganizationId,
|
||||||
string Name,
|
string Name,
|
||||||
|
string? Slug,
|
||||||
string TimeZone);
|
string TimeZone);
|
||||||
|
|
||||||
internal class CreateWorkspaceRequestValidator
|
internal class CreateWorkspaceRequestValidator
|
||||||
@@ -18,6 +20,7 @@ internal class CreateWorkspaceRequestValidator
|
|||||||
{
|
{
|
||||||
RuleFor(x => x.OrganizationId).NotEmpty();
|
RuleFor(x => x.OrganizationId).NotEmpty();
|
||||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||||
|
RuleFor(x => x.Slug).MaximumLength(96);
|
||||||
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +54,12 @@ internal class CreateWorkspaceHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
string normalizedName = request.Name.Trim();
|
string normalizedName = request.Name.Trim();
|
||||||
|
string normalizedSlug = await WorkspaceSlugGenerator.CreateUniqueAsync(
|
||||||
|
dbContext,
|
||||||
|
request.OrganizationId,
|
||||||
|
string.IsNullOrWhiteSpace(request.Slug) ? normalizedName : request.Slug,
|
||||||
|
null,
|
||||||
|
ct);
|
||||||
string normalizedTimeZone = request.TimeZone.Trim();
|
string normalizedTimeZone = request.TimeZone.Trim();
|
||||||
|
|
||||||
Workspace workspace = new()
|
Workspace workspace = new()
|
||||||
@@ -58,6 +67,7 @@ internal class CreateWorkspaceHandler(
|
|||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
OrganizationId = request.OrganizationId,
|
OrganizationId = request.OrganizationId,
|
||||||
Name = normalizedName,
|
Name = normalizedName,
|
||||||
|
Slug = normalizedSlug,
|
||||||
OwnerUserId = User.GetUserId(),
|
OwnerUserId = User.GetUserId(),
|
||||||
TimeZone = normalizedTimeZone,
|
TimeZone = normalizedTimeZone,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ internal record WorkspaceDto(
|
|||||||
Guid Id,
|
Guid Id,
|
||||||
Guid OrganizationId,
|
Guid OrganizationId,
|
||||||
string Name,
|
string Name,
|
||||||
|
string Slug,
|
||||||
string? LogoUrl,
|
string? LogoUrl,
|
||||||
string TimeZone,
|
string TimeZone,
|
||||||
string ApprovalMode,
|
string ApprovalMode,
|
||||||
@@ -38,6 +39,7 @@ internal record WorkspaceDto(
|
|||||||
workspace.Id,
|
workspace.Id,
|
||||||
workspace.OrganizationId,
|
workspace.OrganizationId,
|
||||||
workspace.Name,
|
workspace.Name,
|
||||||
|
workspace.Slug,
|
||||||
workspace.LogoUrl,
|
workspace.LogoUrl,
|
||||||
workspace.TimeZone,
|
workspace.TimeZone,
|
||||||
workspace.ApprovalMode,
|
workspace.ApprovalMode,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Socialize.Api.Infrastructure.Security;
|
|||||||
using Socialize.Api.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
using Socialize.Api.Modules.Approvals.Services;
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Services;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ internal record UpdateApprovalStepConfigurationRequest(
|
|||||||
|
|
||||||
internal record UpdateWorkspaceRequest(
|
internal record UpdateWorkspaceRequest(
|
||||||
string Name,
|
string Name,
|
||||||
|
string? Slug,
|
||||||
string TimeZone,
|
string TimeZone,
|
||||||
string? ApprovalMode,
|
string? ApprovalMode,
|
||||||
bool? SchedulePostsAutomaticallyOnApproval,
|
bool? SchedulePostsAutomaticallyOnApproval,
|
||||||
@@ -32,6 +34,7 @@ internal class UpdateWorkspaceRequestValidator
|
|||||||
public UpdateWorkspaceRequestValidator()
|
public UpdateWorkspaceRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||||
|
RuleFor(x => x.Slug).MaximumLength(96);
|
||||||
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
||||||
RuleFor(x => x.ApprovalMode)
|
RuleFor(x => x.ApprovalMode)
|
||||||
.Must(mode => string.IsNullOrWhiteSpace(mode) || AllowedApprovalModes.Contains(mode.Trim()))
|
.Must(mode => string.IsNullOrWhiteSpace(mode) || AllowedApprovalModes.Contains(mode.Trim()))
|
||||||
@@ -106,6 +109,12 @@ internal class UpdateWorkspaceHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspace.Name = request.Name.Trim();
|
workspace.Name = request.Name.Trim();
|
||||||
|
workspace.Slug = await WorkspaceSlugGenerator.CreateUniqueAsync(
|
||||||
|
dbContext,
|
||||||
|
workspace.OrganizationId,
|
||||||
|
string.IsNullOrWhiteSpace(request.Slug) ? workspace.Name : request.Slug,
|
||||||
|
workspace.Id,
|
||||||
|
ct);
|
||||||
workspace.TimeZone = request.TimeZone.Trim();
|
workspace.TimeZone = request.TimeZone.Trim();
|
||||||
workspace.ApprovalMode = nextApprovalMode;
|
workspace.ApprovalMode = nextApprovalMode;
|
||||||
workspace.SchedulePostsAutomaticallyOnApproval = request.SchedulePostsAutomaticallyOnApproval ?? workspace.SchedulePostsAutomaticallyOnApproval;
|
workspace.SchedulePostsAutomaticallyOnApproval = request.SchedulePostsAutomaticallyOnApproval ?? workspace.SchedulePostsAutomaticallyOnApproval;
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Workspaces.Services;
|
||||||
|
|
||||||
|
internal static class WorkspaceSlugGenerator
|
||||||
|
{
|
||||||
|
public static string Normalize(string value)
|
||||||
|
{
|
||||||
|
#pragma warning disable CA1308 // Workspace slugs are intentionally lowercase external folder names.
|
||||||
|
string trimmed = value.Trim().ToLowerInvariant();
|
||||||
|
#pragma warning restore CA1308
|
||||||
|
var builder = new StringBuilder(trimmed.Length);
|
||||||
|
bool previousWasSeparator = false;
|
||||||
|
|
||||||
|
foreach (char character in trimmed)
|
||||||
|
{
|
||||||
|
if (char.IsLetterOrDigit(character))
|
||||||
|
{
|
||||||
|
builder.Append(character);
|
||||||
|
previousWasSeparator = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousWasSeparator)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append('-');
|
||||||
|
previousWasSeparator = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string slug = builder.ToString().Trim('-');
|
||||||
|
return string.IsNullOrWhiteSpace(slug) ? "workspace" : slug[..Math.Min(slug.Length, 96)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> CreateUniqueAsync(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
Guid organizationId,
|
||||||
|
string source,
|
||||||
|
Guid? excludingWorkspaceId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string baseSlug = Normalize(source);
|
||||||
|
string candidate = baseSlug;
|
||||||
|
int suffix = 2;
|
||||||
|
|
||||||
|
while (await dbContext.Workspaces.AnyAsync(
|
||||||
|
workspace => workspace.OrganizationId == organizationId &&
|
||||||
|
workspace.Slug == candidate &&
|
||||||
|
(!excludingWorkspaceId.HasValue || workspace.Id != excludingWorkspaceId.Value),
|
||||||
|
ct))
|
||||||
|
{
|
||||||
|
string suffixText = $"-{suffix}";
|
||||||
|
int maxBaseLength = 96 - suffixText.Length;
|
||||||
|
candidate = $"{baseSlug[..Math.Min(baseSlug.Length, maxBaseLength)]}{suffixText}";
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
docs/FEATURES/digital-asset-management.md
Normal file
56
docs/FEATURES/digital-asset-management.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Digital Asset Management
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Draft
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Provide a workspace media library backed by the organization's configured Google Drive.
|
||||||
|
|
||||||
|
The DAM is not a standalone storage system in v1. When an organization has Google Drive configured, Google Drive is the backing store for media files and Socialize stores metadata, workflow relationships, revisions, and audit history.
|
||||||
|
|
||||||
|
## Backing Store
|
||||||
|
|
||||||
|
Google Drive configuration belongs to the organization.
|
||||||
|
|
||||||
|
An organization Google Drive configuration includes:
|
||||||
|
|
||||||
|
- enabled state
|
||||||
|
- root folder id
|
||||||
|
- root folder name
|
||||||
|
- root folder URL
|
||||||
|
|
||||||
|
Workspace media is organized inside the organization Drive root by workspace slug:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
<organization-drive-root>/<workspace-slug>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Each workspace must have a stable slug. Slugs are unique within the owning organization and should not change casually because they map to an external DAM folder.
|
||||||
|
|
||||||
|
## Workspace DAM
|
||||||
|
|
||||||
|
The workspace DAM view should expose:
|
||||||
|
|
||||||
|
- whether Google Drive is configured for the owning organization
|
||||||
|
- the workspace slug
|
||||||
|
- the resolved workspace DAM folder name and path
|
||||||
|
- linked media assets for the workspace
|
||||||
|
- asset revisions and source references
|
||||||
|
|
||||||
|
## Business Rules
|
||||||
|
|
||||||
|
- Organization connector settings are managed by users with `ManageConnectors`.
|
||||||
|
- Workspace slugs are required and unique within an organization.
|
||||||
|
- Asset metadata remains workspace-scoped.
|
||||||
|
- Content item assets can point at Google Drive files in the workspace DAM folder.
|
||||||
|
- Socialize must not claim to upload or sync files until the Google Drive API integration exists.
|
||||||
|
|
||||||
|
## Out Of Scope For First Slice
|
||||||
|
|
||||||
|
- Google OAuth consent and refresh-token storage.
|
||||||
|
- Creating folders in Google Drive through the Drive API.
|
||||||
|
- Uploading files to Google Drive.
|
||||||
|
- Background Drive synchronization.
|
||||||
|
- Moving existing files between Drive folders.
|
||||||
45
docs/TASKS/content/008-google-drive-backed-dam-foundation.md
Normal file
45
docs/TASKS/content/008-google-drive-backed-dam-foundation.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Task: Add Google Drive backed DAM foundation
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
|
||||||
|
`docs/FEATURES/digital-asset-management.md`
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the DAM model aware of organization-level Google Drive backing storage and workspace slug folders.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add organization Google Drive configuration metadata.
|
||||||
|
- Add a required workspace `Slug`, unique within the owning organization.
|
||||||
|
- Generate a slug from workspace name during workspace creation.
|
||||||
|
- Allow workspace managers to update a workspace slug.
|
||||||
|
- Add DAM metadata to workspace asset responses.
|
||||||
|
- Add a workspace DAM endpoint that returns backing-store configuration, workspace folder information, and workspace assets.
|
||||||
|
- Keep actual Google Drive API folder creation, uploads, and sync out of scope.
|
||||||
|
|
||||||
|
## Likely Files
|
||||||
|
|
||||||
|
- `backend/src/Socialize.Api/Modules/Organizations/`
|
||||||
|
- `backend/src/Socialize.Api/Modules/Workspaces/`
|
||||||
|
- `backend/src/Socialize.Api/Modules/Assets/`
|
||||||
|
- `backend/src/Socialize.Api/Data/AppDbContext.cs`
|
||||||
|
- `frontend/src/features/content/views/MediaLibraryView.vue`
|
||||||
|
- `frontend/src/features/content/stores/`
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build backend/Socialize.slnx
|
||||||
|
dotnet test backend/Socialize.slnx
|
||||||
|
cd frontend && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Organization responses include Google Drive DAM configuration metadata.
|
||||||
|
- [x] Organization connector managers can save Google Drive DAM configuration.
|
||||||
|
- [x] Workspace responses include a stable slug.
|
||||||
|
- [x] New workspaces receive a unique slug based on the workspace name.
|
||||||
|
- [x] Workspace DAM data resolves to `<drive-root>/<workspace-slug>`.
|
||||||
|
- [x] Existing manually linked Google Drive content assets remain supported.
|
||||||
41
frontend/src/features/content/stores/mediaLibraryStore.js
Normal file
41
frontend/src/features/content/stores/mediaLibraryStore.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { useClient } from '@/plugins/api.js';
|
||||||
|
|
||||||
|
export const useMediaLibraryStore = defineStore('media-library', () => {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const dam = ref(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const error = ref(null);
|
||||||
|
|
||||||
|
async function fetchWorkspaceDam(workspaceId) {
|
||||||
|
if (!workspaceId) {
|
||||||
|
dam.value = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.get(`/api/workspaces/${workspaceId}/dam`);
|
||||||
|
dam.value = response.data ?? null;
|
||||||
|
return dam.value;
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.error('Failed to load workspace DAM:', fetchError);
|
||||||
|
dam.value = null;
|
||||||
|
error.value = 'Failed to load the media library.';
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dam,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
fetchWorkspaceDam,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,25 +1,35 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, onMounted, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useMediaLibraryStore } from '@/features/content/stores/mediaLibraryStore.js';
|
||||||
|
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||||
import {
|
import {
|
||||||
mdiCheckCircleOutline,
|
mdiCheckCircleOutline,
|
||||||
mdiCloudSyncOutline,
|
mdiCloudSyncOutline,
|
||||||
mdiFolderGoogleDrive,
|
mdiFolderGoogleDrive,
|
||||||
mdiImageMultipleOutline,
|
|
||||||
mdiVideoOutline,
|
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const mediaLibraryStore = useMediaLibraryStore();
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
|
||||||
const mediaTypes = [
|
const activeWorkspaceId = computed(() => workspaceStore.activeWorkspaceId);
|
||||||
{ label: t('mediaLibrary.mediaTypes.images'), icon: mdiImageMultipleOutline },
|
const dam = computed(() => mediaLibraryStore.dam);
|
||||||
{ label: t('mediaLibrary.mediaTypes.videos'), icon: mdiVideoOutline },
|
const assets = computed(() => dam.value?.assets ?? []);
|
||||||
];
|
const folderPath = computed(() => dam.value?.folder?.path ?? t('mediaLibrary.notConfigured'));
|
||||||
|
|
||||||
const workflowSteps = [
|
const workflowSteps = [
|
||||||
t('mediaLibrary.workflow.connectDrive'),
|
t('mediaLibrary.workflow.connectDrive'),
|
||||||
t('mediaLibrary.workflow.syncAssets'),
|
t('mediaLibrary.workflow.syncAssets'),
|
||||||
t('mediaLibrary.workflow.organizeLibrary'),
|
t('mediaLibrary.workflow.organizeLibrary'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
async function loadDam() {
|
||||||
|
await mediaLibraryStore.fetchWorkspaceDam(activeWorkspaceId.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadDam);
|
||||||
|
watch(activeWorkspaceId, loadDam);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -35,26 +45,40 @@
|
|||||||
<div class="hero-card-icon">
|
<div class="hero-card-icon">
|
||||||
<v-icon :icon="mdiFolderGoogleDrive" />
|
<v-icon :icon="mdiFolderGoogleDrive" />
|
||||||
</div>
|
</div>
|
||||||
<strong>{{ t('mediaLibrary.syncCard.title') }}</strong>
|
<strong>{{ dam?.backingStore?.isConfigured ? dam.backingStore.rootFolderName : t('mediaLibrary.syncCard.title') }}</strong>
|
||||||
<span>{{ t('mediaLibrary.syncCard.description') }}</span>
|
<span>{{ dam?.backingStore?.isConfigured ? folderPath : t('mediaLibrary.syncCard.description') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="mediaLibraryStore.isLoading"
|
||||||
|
class="page-message"
|
||||||
|
>
|
||||||
|
{{ t('mediaLibrary.loading') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="mediaLibraryStore.error"
|
||||||
|
class="page-message error"
|
||||||
|
>
|
||||||
|
{{ mediaLibraryStore.error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="content-grid">
|
<div class="content-grid">
|
||||||
<article class="panel">
|
<article class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<strong>{{ t('mediaLibrary.mediaTypesTitle') }}</strong>
|
<strong>{{ t('mediaLibrary.damRootTitle') }}</strong>
|
||||||
<span>{{ t('mediaLibrary.mediaTypesDescription') }}</span>
|
<span>{{ dam?.backingStore?.isConfigured ? dam.backingStore.rootFolderUrl : t('mediaLibrary.notConfiguredDescription') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="media-type-list">
|
<div class="media-type-list">
|
||||||
<div
|
<div class="media-type-item">
|
||||||
v-for="type in mediaTypes"
|
<v-icon :icon="mdiFolderGoogleDrive" />
|
||||||
:key="type.label"
|
<span>{{ folderPath }}</span>
|
||||||
class="media-type-item"
|
</div>
|
||||||
>
|
<div class="media-type-item">
|
||||||
<v-icon :icon="type.icon" />
|
<v-icon :icon="mdiCheckCircleOutline" />
|
||||||
<span>{{ type.label }}</span>
|
<span>{{ t('mediaLibrary.workspaceSlug', { slug: dam?.workspaceSlug ?? '-' }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -87,8 +111,40 @@
|
|||||||
<v-icon :icon="mdiCloudSyncOutline" />
|
<v-icon :icon="mdiCloudSyncOutline" />
|
||||||
<span>{{ t('mediaLibrary.statusLabel') }}</span>
|
<span>{{ t('mediaLibrary.statusLabel') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<strong>{{ t('mediaLibrary.pendingTitle') }}</strong>
|
<strong>{{ dam?.backingStore?.isConfigured ? t('mediaLibrary.configuredTitle') : t('mediaLibrary.pendingTitle') }}</strong>
|
||||||
<p>{{ t('mediaLibrary.pendingDescription') }}</p>
|
<p>{{ dam?.backingStore?.isConfigured ? t('mediaLibrary.configuredDescription') : t('mediaLibrary.pendingDescription') }}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<strong>{{ t('mediaLibrary.assetsTitle') }}</strong>
|
||||||
|
<span>{{ t('mediaLibrary.assetsDescription') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!assets.length"
|
||||||
|
class="empty-state"
|
||||||
|
>
|
||||||
|
{{ t('mediaLibrary.emptyAssets') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="asset in assets"
|
||||||
|
:key="asset.id"
|
||||||
|
class="asset-row"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong>{{ asset.displayName }}</strong>
|
||||||
|
<span>{{ asset.assetType }} · {{ asset.googleDriveWorkspaceFolderPath || folderPath }}</span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-if="asset.googleDriveLink"
|
||||||
|
:href="asset.googleDriveLink"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{{ t('mediaLibrary.openDrive') }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
@@ -189,7 +245,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.media-type-list,
|
.media-type-list,
|
||||||
.workflow-list {
|
.workflow-list,
|
||||||
|
.asset-row-list {
|
||||||
@apply flex flex-col gap-3;
|
@apply flex flex-col gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +261,23 @@
|
|||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-row {
|
||||||
|
@apply flex items-center justify-between gap-4 rounded-[1.1rem] border px-4 py-3;
|
||||||
|
border-color: rgba(23, 32, 51, 0.08);
|
||||||
|
background: rgba(248, 250, 252, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-row div {
|
||||||
|
@apply flex min-w-0 flex-col gap-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-row span,
|
||||||
|
.asset-row a,
|
||||||
|
.empty-state {
|
||||||
|
@apply text-sm;
|
||||||
|
color: #526178;
|
||||||
|
}
|
||||||
|
|
||||||
.status-panel {
|
.status-panel {
|
||||||
background: linear-gradient(135deg, rgba(255, 247, 237, 0.95), rgba(255, 255, 255, 0.98));
|
background: linear-gradient(135deg, rgba(255, 247, 237, 0.95), rgba(255, 255, 255, 0.98));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const useOrganizationStore = defineStore('organization', () => {
|
|||||||
const isLoadingMembershipTiers = ref(false);
|
const isLoadingMembershipTiers = ref(false);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const isUpdatingMembershipTier = ref(false);
|
const isUpdatingMembershipTier = ref(false);
|
||||||
|
const isSavingGoogleDriveDam = ref(false);
|
||||||
const isAddingMember = ref(false);
|
const isAddingMember = ref(false);
|
||||||
const isUploadingLogo = ref(false);
|
const isUploadingLogo = ref(false);
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
@@ -260,6 +261,45 @@ export const useOrganizationStore = defineStore('organization', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateGoogleDriveDam(organizationId, payload) {
|
||||||
|
if (!authStore.isAuthenticated || !organizationId) {
|
||||||
|
throw new Error('You must be authenticated to update organization connectors.');
|
||||||
|
}
|
||||||
|
|
||||||
|
isSavingGoogleDriveDam.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.put(`/api/organizations/${organizationId}/google-drive-dam`, payload);
|
||||||
|
const googleDriveDam = response.data;
|
||||||
|
const currentDetails = detailsById.value[organizationId];
|
||||||
|
|
||||||
|
if (currentDetails) {
|
||||||
|
detailsById.value = {
|
||||||
|
...detailsById.value,
|
||||||
|
[organizationId]: {
|
||||||
|
...currentDetails,
|
||||||
|
googleDriveDam,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
organizations.value = organizations.value.map(organization =>
|
||||||
|
organization.id === organizationId
|
||||||
|
? { ...organization, googleDriveDam }
|
||||||
|
: organization
|
||||||
|
);
|
||||||
|
|
||||||
|
return googleDriveDam;
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error('Failed to update Google Drive DAM configuration:', updateError);
|
||||||
|
error.value = 'Failed to update Google Drive DAM configuration.';
|
||||||
|
throw updateError;
|
||||||
|
} finally {
|
||||||
|
isSavingGoogleDriveDam.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function addMember(organizationId, payload) {
|
async function addMember(organizationId, payload) {
|
||||||
if (!authStore.isAuthenticated || !organizationId) {
|
if (!authStore.isAuthenticated || !organizationId) {
|
||||||
throw new Error('You must be authenticated to add an organization member.');
|
throw new Error('You must be authenticated to add an organization member.');
|
||||||
@@ -371,6 +411,7 @@ export const useOrganizationStore = defineStore('organization', () => {
|
|||||||
isLoadingMembershipTiers,
|
isLoadingMembershipTiers,
|
||||||
isSaving,
|
isSaving,
|
||||||
isUpdatingMembershipTier,
|
isUpdatingMembershipTier,
|
||||||
|
isSavingGoogleDriveDam,
|
||||||
isAddingMember,
|
isAddingMember,
|
||||||
isUploadingLogo,
|
isUploadingLogo,
|
||||||
error,
|
error,
|
||||||
@@ -383,6 +424,7 @@ export const useOrganizationStore = defineStore('organization', () => {
|
|||||||
createOrganization,
|
createOrganization,
|
||||||
updateOrganization,
|
updateOrganization,
|
||||||
updateMembershipTier,
|
updateMembershipTier,
|
||||||
|
updateGoogleDriveDam,
|
||||||
addMember,
|
addMember,
|
||||||
uploadLogo,
|
uploadLogo,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,12 @@
|
|||||||
const membershipTierForm = reactive({
|
const membershipTierForm = reactive({
|
||||||
membershipTierId: null,
|
membershipTierId: null,
|
||||||
});
|
});
|
||||||
|
const googleDriveDamForm = reactive({
|
||||||
|
isEnabled: false,
|
||||||
|
rootFolderId: '',
|
||||||
|
rootFolderName: '',
|
||||||
|
rootFolderUrl: '',
|
||||||
|
});
|
||||||
const memberRoleOptions = ['Member', 'Admin', 'BillingManager', 'ConnectorManager'];
|
const memberRoleOptions = ['Member', 'Admin', 'BillingManager', 'ConnectorManager'];
|
||||||
|
|
||||||
const organizationId = computed(() => route.params.organizationId);
|
const organizationId = computed(() => route.params.organizationId);
|
||||||
@@ -195,6 +201,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitGoogleDriveDam() {
|
||||||
|
settingsError.value = null;
|
||||||
|
settingsStatus.value = null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
googleDriveDamForm.isEnabled &&
|
||||||
|
(!googleDriveDamForm.rootFolderId.trim() ||
|
||||||
|
!googleDriveDamForm.rootFolderName.trim() ||
|
||||||
|
!googleDriveDamForm.rootFolderUrl.trim())
|
||||||
|
) {
|
||||||
|
settingsError.value = t('organizationSettings.sections.connections.googleDrive.required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await organizationStore.updateGoogleDriveDam(organizationId.value, {
|
||||||
|
isEnabled: googleDriveDamForm.isEnabled,
|
||||||
|
rootFolderId: googleDriveDamForm.rootFolderId.trim() || null,
|
||||||
|
rootFolderName: googleDriveDamForm.rootFolderName.trim() || null,
|
||||||
|
rootFolderUrl: googleDriveDamForm.rootFolderUrl.trim() || null,
|
||||||
|
});
|
||||||
|
settingsStatus.value = t('organizationSettings.sections.connections.googleDrive.saved');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save Google Drive DAM configuration:', error);
|
||||||
|
settingsError.value = t('organizationSettings.sections.connections.googleDrive.saveFailed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatTierSummary(tier) {
|
function formatTierSummary(tier) {
|
||||||
const price = tier.isCustom || tier.monthlyPriceCents === null || tier.monthlyPriceCents === undefined
|
const price = tier.isCustom || tier.monthlyPriceCents === null || tier.monthlyPriceCents === undefined
|
||||||
? t('organizationSettings.tiers.customPrice')
|
? t('organizationSettings.tiers.customPrice')
|
||||||
@@ -232,6 +266,10 @@
|
|||||||
currentOrganization => {
|
currentOrganization => {
|
||||||
profileForm.name = currentOrganization?.name ?? '';
|
profileForm.name = currentOrganization?.name ?? '';
|
||||||
membershipTierForm.membershipTierId = currentOrganization?.membershipTier?.id ?? null;
|
membershipTierForm.membershipTierId = currentOrganization?.membershipTier?.id ?? null;
|
||||||
|
googleDriveDamForm.isEnabled = Boolean(currentOrganization?.googleDriveDam?.isEnabled);
|
||||||
|
googleDriveDamForm.rootFolderId = currentOrganization?.googleDriveDam?.rootFolderId ?? '';
|
||||||
|
googleDriveDamForm.rootFolderName = currentOrganization?.googleDriveDam?.rootFolderName ?? '';
|
||||||
|
googleDriveDamForm.rootFolderUrl = currentOrganization?.googleDriveDam?.rootFolderUrl ?? '';
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
@@ -457,10 +495,55 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="activeSection.key === 'connections'"
|
v-else-if="activeSection.key === 'connections'"
|
||||||
class="placeholder-panel"
|
class="settings-form"
|
||||||
>
|
>
|
||||||
<strong>{{ t('organizationSettings.sections.connections.placeholderTitle') }}</strong>
|
<div class="placeholder-panel">
|
||||||
<span>{{ t('organizationSettings.sections.connections.placeholderText') }}</span>
|
<strong>{{ t('organizationSettings.sections.connections.googleDrive.title') }}</strong>
|
||||||
|
<span>{{ t('organizationSettings.sections.connections.googleDrive.description') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-form
|
||||||
|
class="settings-form"
|
||||||
|
@submit.prevent="submitGoogleDriveDam"
|
||||||
|
>
|
||||||
|
<v-switch
|
||||||
|
v-model="googleDriveDamForm.isEnabled"
|
||||||
|
:label="t('organizationSettings.sections.connections.googleDrive.enabled')"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="googleDriveDamForm.rootFolderName"
|
||||||
|
:label="t('organizationSettings.sections.connections.googleDrive.rootFolderName')"
|
||||||
|
:disabled="!googleDriveDamForm.isEnabled || organizationStore.isSavingGoogleDriveDam"
|
||||||
|
maxlength="256"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="googleDriveDamForm.rootFolderId"
|
||||||
|
:label="t('organizationSettings.sections.connections.googleDrive.rootFolderId')"
|
||||||
|
:disabled="!googleDriveDamForm.isEnabled || organizationStore.isSavingGoogleDriveDam"
|
||||||
|
maxlength="256"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="googleDriveDamForm.rootFolderUrl"
|
||||||
|
:label="t('organizationSettings.sections.connections.googleDrive.rootFolderUrl')"
|
||||||
|
:disabled="!googleDriveDamForm.isEnabled || organizationStore.isSavingGoogleDriveDam"
|
||||||
|
maxlength="2048"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
:loading="organizationStore.isSavingGoogleDriveDam"
|
||||||
|
>
|
||||||
|
{{ t('organizationSettings.sections.connections.googleDrive.save') }}
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
const activeTab = ref('general');
|
const activeTab = ref('general');
|
||||||
const settingsForm = reactive({
|
const settingsForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
|
slug: '',
|
||||||
timeZone: '',
|
timeZone: '',
|
||||||
approvalMode: 'Required',
|
approvalMode: 'Required',
|
||||||
schedulePostsAutomaticallyOnApproval: false,
|
schedulePostsAutomaticallyOnApproval: false,
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
const workspaceApprovalSteps = normalizeApprovalSteps(workspace.approvalSteps ?? []);
|
const workspaceApprovalSteps = normalizeApprovalSteps(workspace.approvalSteps ?? []);
|
||||||
|
|
||||||
return settingsForm.name.trim() !== workspace.name ||
|
return settingsForm.name.trim() !== workspace.name ||
|
||||||
|
settingsForm.slug.trim() !== workspace.slug ||
|
||||||
settingsForm.timeZone.trim() !== workspace.timeZone ||
|
settingsForm.timeZone.trim() !== workspace.timeZone ||
|
||||||
settingsForm.approvalMode !== (workspace.approvalMode ?? 'Required') ||
|
settingsForm.approvalMode !== (workspace.approvalMode ?? 'Required') ||
|
||||||
settingsForm.schedulePostsAutomaticallyOnApproval !== Boolean(workspace.schedulePostsAutomaticallyOnApproval) ||
|
settingsForm.schedulePostsAutomaticallyOnApproval !== Boolean(workspace.schedulePostsAutomaticallyOnApproval) ||
|
||||||
@@ -196,6 +198,7 @@
|
|||||||
() => workspaceStore.activeWorkspace,
|
() => workspaceStore.activeWorkspace,
|
||||||
workspace => {
|
workspace => {
|
||||||
settingsForm.name = workspace?.name ?? '';
|
settingsForm.name = workspace?.name ?? '';
|
||||||
|
settingsForm.slug = workspace?.slug ?? '';
|
||||||
settingsForm.timeZone = workspace?.timeZone ?? '';
|
settingsForm.timeZone = workspace?.timeZone ?? '';
|
||||||
settingsForm.approvalMode = workspace?.approvalMode ?? 'Required';
|
settingsForm.approvalMode = workspace?.approvalMode ?? 'Required';
|
||||||
settingsForm.schedulePostsAutomaticallyOnApproval = Boolean(workspace?.schedulePostsAutomaticallyOnApproval);
|
settingsForm.schedulePostsAutomaticallyOnApproval = Boolean(workspace?.schedulePostsAutomaticallyOnApproval);
|
||||||
@@ -237,9 +240,10 @@
|
|||||||
settingsStatus.value = null;
|
settingsStatus.value = null;
|
||||||
|
|
||||||
const name = settingsForm.name.trim();
|
const name = settingsForm.name.trim();
|
||||||
|
const slug = settingsForm.slug.trim();
|
||||||
const timeZone = settingsForm.timeZone.trim();
|
const timeZone = settingsForm.timeZone.trim();
|
||||||
|
|
||||||
if (!name || !timeZone) {
|
if (!name || !slug || !timeZone) {
|
||||||
settingsError.value = t('workspaceSettings.errors.required');
|
settingsError.value = t('workspaceSettings.errors.required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -254,6 +258,7 @@
|
|||||||
try {
|
try {
|
||||||
await workspaceStore.updateWorkspace(workspace.id, {
|
await workspaceStore.updateWorkspace(workspace.id, {
|
||||||
name,
|
name,
|
||||||
|
slug,
|
||||||
timeZone,
|
timeZone,
|
||||||
approvalMode: settingsForm.approvalMode,
|
approvalMode: settingsForm.approvalMode,
|
||||||
schedulePostsAutomaticallyOnApproval: settingsForm.schedulePostsAutomaticallyOnApproval,
|
schedulePostsAutomaticallyOnApproval: settingsForm.schedulePostsAutomaticallyOnApproval,
|
||||||
@@ -504,6 +509,15 @@
|
|||||||
hide-details
|
hide-details
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="settingsForm.slug"
|
||||||
|
:label="t('workspaceSettings.fields.slug')"
|
||||||
|
:hint="t('workspaceSettings.fields.slugHint')"
|
||||||
|
:disabled="workspaceStore.isUpdating"
|
||||||
|
variant="outlined"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>{{ t('workspaceSettings.fields.timeZone') }}</span>
|
<span>{{ t('workspaceSettings.fields.timeZone') }}</span>
|
||||||
<TimeZoneSelect
|
<TimeZoneSelect
|
||||||
|
|||||||
@@ -504,7 +504,19 @@
|
|||||||
"title": "Connections",
|
"title": "Connections",
|
||||||
"description": "Organization-level connectors and data mappings.",
|
"description": "Organization-level connectors and data mappings.",
|
||||||
"placeholderTitle": "No organization connections configured",
|
"placeholderTitle": "No organization connections configured",
|
||||||
"placeholderText": "Connector authorization flows are intentionally out of scope for this UI shell."
|
"placeholderText": "Connector authorization flows are intentionally out of scope for this UI shell.",
|
||||||
|
"googleDrive": {
|
||||||
|
"title": "Google Drive DAM",
|
||||||
|
"description": "Use an organization Drive folder as the media library backing store. Workspace media is organized by workspace slug.",
|
||||||
|
"enabled": "Use Google Drive as the DAM backing store",
|
||||||
|
"rootFolderName": "Root folder name",
|
||||||
|
"rootFolderId": "Root folder ID",
|
||||||
|
"rootFolderUrl": "Root folder URL",
|
||||||
|
"save": "Save Google Drive DAM",
|
||||||
|
"saved": "Google Drive DAM configuration saved.",
|
||||||
|
"required": "Root folder name, ID, and URL are required when Google Drive DAM is enabled.",
|
||||||
|
"saveFailed": "Google Drive DAM configuration could not be saved."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"title": "Workspaces",
|
"title": "Workspaces",
|
||||||
@@ -1120,6 +1132,8 @@
|
|||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": "Workspace name",
|
"name": "Workspace name",
|
||||||
|
"slug": "Workspace slug",
|
||||||
|
"slugHint": "Used as the folder name under the organization DAM root.",
|
||||||
"timeZone": "Time zone",
|
"timeZone": "Time zone",
|
||||||
"memberEmail": "Member email",
|
"memberEmail": "Member email",
|
||||||
"memberRole": "Role"
|
"memberRole": "Role"
|
||||||
@@ -1295,6 +1309,17 @@
|
|||||||
"organizeLibrary": "Review, tag, and reuse media from one workspace-level place."
|
"organizeLibrary": "Review, tag, and reuse media from one workspace-level place."
|
||||||
},
|
},
|
||||||
"statusLabel": "Status",
|
"statusLabel": "Status",
|
||||||
|
"loading": "Loading media library...",
|
||||||
|
"notConfigured": "Google Drive DAM not configured",
|
||||||
|
"notConfiguredDescription": "Configure the organization Google Drive DAM connection before this workspace can resolve a backing folder.",
|
||||||
|
"damRootTitle": "DAM folder",
|
||||||
|
"workspaceSlug": "Workspace slug: {slug}",
|
||||||
|
"configuredTitle": "Google Drive backing store configured",
|
||||||
|
"configuredDescription": "Socialize is using the organization Drive root and this workspace slug to resolve media library metadata.",
|
||||||
|
"assetsTitle": "Workspace assets",
|
||||||
|
"assetsDescription": "Google Drive assets currently linked to content in this workspace.",
|
||||||
|
"emptyAssets": "No workspace assets have been linked yet.",
|
||||||
|
"openDrive": "Open in Drive",
|
||||||
"pendingTitle": "Management UI pending",
|
"pendingTitle": "Management UI pending",
|
||||||
"pendingDescription": "The navigation and page entry point are in place. Next step is wiring actual Drive sync, listing, filters, and asset actions."
|
"pendingDescription": "The navigation and page entry point are in place. Next step is wiring actual Drive sync, listing, filters, and asset actions."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -504,7 +504,19 @@
|
|||||||
"title": "Connexions",
|
"title": "Connexions",
|
||||||
"description": "Connecteurs et regles de donnees au niveau de l'organisation.",
|
"description": "Connecteurs et regles de donnees au niveau de l'organisation.",
|
||||||
"placeholderTitle": "Aucune connexion d'organisation configuree",
|
"placeholderTitle": "Aucune connexion d'organisation configuree",
|
||||||
"placeholderText": "Les flux d'autorisation des connecteurs sont volontairement hors portee de cette interface."
|
"placeholderText": "Les flux d'autorisation des connecteurs sont volontairement hors portee de cette interface.",
|
||||||
|
"googleDrive": {
|
||||||
|
"title": "DAM Google Drive",
|
||||||
|
"description": "Utilisez un dossier Drive de l'organisation comme stockage de la bibliotheque media. Les medias sont organises par slug d'espace.",
|
||||||
|
"enabled": "Utiliser Google Drive comme stockage DAM",
|
||||||
|
"rootFolderName": "Nom du dossier racine",
|
||||||
|
"rootFolderId": "ID du dossier racine",
|
||||||
|
"rootFolderUrl": "URL du dossier racine",
|
||||||
|
"save": "Enregistrer le DAM Google Drive",
|
||||||
|
"saved": "Configuration DAM Google Drive enregistree.",
|
||||||
|
"required": "Le nom, l'ID et l'URL du dossier racine sont requis quand le DAM Google Drive est active.",
|
||||||
|
"saveFailed": "La configuration DAM Google Drive n'a pas pu etre enregistree."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"title": "Espaces",
|
"title": "Espaces",
|
||||||
@@ -1120,6 +1132,8 @@
|
|||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": "Nom de l'espace",
|
"name": "Nom de l'espace",
|
||||||
|
"slug": "Slug de l'espace",
|
||||||
|
"slugHint": "Utilise comme nom de dossier sous la racine DAM de l'organisation.",
|
||||||
"timeZone": "Fuseau horaire",
|
"timeZone": "Fuseau horaire",
|
||||||
"memberEmail": "Email du membre",
|
"memberEmail": "Email du membre",
|
||||||
"memberRole": "Rôle"
|
"memberRole": "Rôle"
|
||||||
@@ -1295,6 +1309,17 @@
|
|||||||
"organizeLibrary": "Reviser, etiqueter et reutiliser les medias depuis un seul endroit au niveau de l'espace."
|
"organizeLibrary": "Reviser, etiqueter et reutiliser les medias depuis un seul endroit au niveau de l'espace."
|
||||||
},
|
},
|
||||||
"statusLabel": "Statut",
|
"statusLabel": "Statut",
|
||||||
|
"loading": "Chargement de la bibliotheque media...",
|
||||||
|
"notConfigured": "DAM Google Drive non configure",
|
||||||
|
"notConfiguredDescription": "Configurez la connexion DAM Google Drive de l'organisation avant de resoudre le dossier de cet espace.",
|
||||||
|
"damRootTitle": "Dossier DAM",
|
||||||
|
"workspaceSlug": "Slug de l'espace : {slug}",
|
||||||
|
"configuredTitle": "Stockage Google Drive configure",
|
||||||
|
"configuredDescription": "Socialize utilise la racine Drive de l'organisation et le slug de cet espace pour resoudre les metadonnees media.",
|
||||||
|
"assetsTitle": "Ressources de l'espace",
|
||||||
|
"assetsDescription": "Ressources Google Drive actuellement liees au contenu de cet espace.",
|
||||||
|
"emptyAssets": "Aucune ressource d'espace n'a encore ete liee.",
|
||||||
|
"openDrive": "Ouvrir dans Drive",
|
||||||
"pendingTitle": "Interface de gestion en attente",
|
"pendingTitle": "Interface de gestion en attente",
|
||||||
"pendingDescription": "L'entree de navigation et la page sont en place. La prochaine etape est de brancher la vraie synchro Drive, le listing, les filtres et les actions sur les ressources."
|
"pendingDescription": "L'entree de navigation et la page sont en place. La prochaine etape est de brancher la vraie synchro Drive, le listing, les filtres et les actions sur les ressources."
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user