feat: refine content calendar experience
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
ARG VITE_API_URL=/api
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
|
||||
596
frontend/src/api/schema.d.ts
vendored
596
frontend/src/api/schema.d.ts
vendored
@@ -116,6 +116,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/organizations/{organizationId}/logo": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesOrganizationsHandlersChangeOrganizationLogoHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/organizations/{organizationId}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -708,6 +724,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/content-items/{id}/activity": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["SocializeApiModulesContentItemsHandlersGetContentItemActivityHandler"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/content-items/{id}/status": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -740,22 +772,6 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/comments/{id}/resolve": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesCommentsHandlersResolveCommentHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/clients/{id}/portrait": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -868,6 +884,118 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/calendar-integrations/catalog": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["SocializeApiModulesCalendarIntegrationsHandlersListCalendarCatalogHandler"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/calendar-integrations/events": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["SocializeApiModulesCalendarIntegrationsHandlersListCalendarEventsHandler"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/calendar-integrations/sources/{sourceId}/refresh": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesCalendarIntegrationsHandlersRefreshCalendarSourceHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/calendar-integrations/export-feed": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["SocializeApiModulesCalendarIntegrationsHandlersGetUserCalendarExportFeedHandler"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete: operations["SocializeApiModulesCalendarIntegrationsHandlersRevokeUserCalendarExportFeedHandler"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/calendar-integrations/export-feed/enable": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesCalendarIntegrationsHandlersEnableUserCalendarExportFeedHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/calendar-integrations/export-feed/regenerate": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesCalendarIntegrationsHandlersRegenerateUserCalendarExportFeedHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/calendar-integrations/export-feed/{token}.ics": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["SocializeApiModulesCalendarIntegrationsHandlersGetUserCalendarExportFeedIcsHandler"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/assets/{id}/revisions": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1067,18 +1195,38 @@ export interface components {
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersChangeOrganizationLogoResponse: {
|
||||
blobUrl?: string;
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersChangeOrganizationLogoRequest: {
|
||||
/** Format: binary */
|
||||
file: string;
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersOrganizationDto: {
|
||||
/** Format: guid */
|
||||
id?: string;
|
||||
name?: string;
|
||||
logoUrl?: string | null;
|
||||
/** Format: guid */
|
||||
ownerUserId?: string;
|
||||
currentUserPermissions?: string[];
|
||||
members?: components["schemas"]["SocializeApiModulesOrganizationsHandlersOrganizationMemberDto"][];
|
||||
workspaces?: components["schemas"]["SocializeApiModulesWorkspacesHandlersWorkspaceDto"][];
|
||||
usage?: components["schemas"]["SocializeApiModulesOrganizationsHandlersOrganizationUsageDto"] | null;
|
||||
/** Format: date-time */
|
||||
createdAt?: string;
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersOrganizationUsageDto: {
|
||||
planName?: string;
|
||||
items?: components["schemas"]["SocializeApiModulesOrganizationsHandlersOrganizationUsageItemDto"][];
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersOrganizationUsageItemDto: {
|
||||
key?: string;
|
||||
/** Format: int32 */
|
||||
used?: number;
|
||||
/** Format: int32 */
|
||||
limit?: number | null;
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersUpdateOrganizationRequest: {
|
||||
name: string;
|
||||
};
|
||||
@@ -1386,6 +1534,8 @@ export interface components {
|
||||
publicationTargets: string;
|
||||
hashtags?: string | null;
|
||||
changeSummary?: string | null;
|
||||
/** Format: date-time */
|
||||
dueDate?: string | null;
|
||||
};
|
||||
SocializeApiModulesContentItemsHandlersContentItemDetailDto: {
|
||||
/** Format: guid */
|
||||
@@ -1409,6 +1559,25 @@ export interface components {
|
||||
/** Format: date-time */
|
||||
createdAt?: string;
|
||||
};
|
||||
SocializeApiModulesContentItemsHandlersContentItemActivityEntryDto: {
|
||||
/** Format: guid */
|
||||
id?: string;
|
||||
/** Format: guid */
|
||||
workspaceId?: string;
|
||||
/** Format: guid */
|
||||
contentItemId?: string;
|
||||
eventType?: string;
|
||||
entityType?: string;
|
||||
/** Format: guid */
|
||||
entityId?: string;
|
||||
summary?: string;
|
||||
/** Format: guid */
|
||||
actorUserId?: string | null;
|
||||
actorEmail?: string | null;
|
||||
metadataJson?: string | null;
|
||||
/** Format: date-time */
|
||||
createdAt?: string;
|
||||
};
|
||||
SocializeApiModulesContentItemsHandlersGetContentItemsRequest: Record<string, never>;
|
||||
SocializeApiModulesContentItemsHandlersUpdateContentItemStatusRequest: {
|
||||
status: string;
|
||||
@@ -1428,11 +1597,13 @@ export interface components {
|
||||
authorEmail?: string;
|
||||
authorPortraitUrl?: string | null;
|
||||
body?: string;
|
||||
isResolved?: boolean;
|
||||
attachmentFileName?: string | null;
|
||||
attachmentContentType?: string | null;
|
||||
/** Format: int64 */
|
||||
attachmentSizeBytes?: number | null;
|
||||
attachmentBlobUrl?: string | null;
|
||||
/** Format: date-time */
|
||||
createdAt?: string;
|
||||
/** Format: date-time */
|
||||
resolvedAt?: string | null;
|
||||
};
|
||||
SocializeApiModulesCommentsHandlersCreateCommentRequest: {
|
||||
/** Format: guid */
|
||||
@@ -1441,7 +1612,9 @@ export interface components {
|
||||
contentItemId: string;
|
||||
/** Format: guid */
|
||||
parentCommentId?: string | null;
|
||||
body: string;
|
||||
body?: string;
|
||||
/** Format: binary */
|
||||
attachment?: string | null;
|
||||
};
|
||||
SocializeApiModulesCommentsHandlersGetCommentsRequest: Record<string, never>;
|
||||
SocializeApiModulesClientsHandlersChangeClientPortraitResponse: {
|
||||
@@ -1576,7 +1749,65 @@ export interface components {
|
||||
isEnabled?: boolean;
|
||||
inheritanceMode?: string | null;
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersCalendarCatalogEntryDto: {
|
||||
/** Format: guid */
|
||||
id?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
country?: string | null;
|
||||
region?: string | null;
|
||||
language?: string;
|
||||
category?: string;
|
||||
cultureOrReligion?: string | null;
|
||||
providerName?: string;
|
||||
sourceUrl?: string;
|
||||
trustLevel?: string;
|
||||
defaultColor?: string;
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersListCalendarCatalogRequest: Record<string, never>;
|
||||
SocializeApiModulesCalendarIntegrationsHandlersCalendarEventDto: {
|
||||
/** Format: guid */
|
||||
id?: string;
|
||||
/** Format: guid */
|
||||
calendarSourceId?: string;
|
||||
sourceEventUid?: string;
|
||||
title?: string;
|
||||
description?: string | null;
|
||||
isAllDay?: boolean;
|
||||
isFloatingTime?: boolean;
|
||||
/** Format: date */
|
||||
startDate?: string;
|
||||
/** Format: date */
|
||||
endDate?: string;
|
||||
/** Format: date-time */
|
||||
startLocalDateTime?: string | null;
|
||||
/** Format: date-time */
|
||||
endLocalDateTime?: string | null;
|
||||
/** Format: date-time */
|
||||
startUtc?: string | null;
|
||||
/** Format: date-time */
|
||||
endUtc?: string | null;
|
||||
timeZoneId?: string | null;
|
||||
recurrenceId?: string | null;
|
||||
location?: string | null;
|
||||
sourceUrl?: string | null;
|
||||
/** Format: date-time */
|
||||
sourceLastModifiedAt?: string | null;
|
||||
/** Format: date-time */
|
||||
importedAt?: string;
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersListCalendarEventsRequest: Record<string, never>;
|
||||
SocializeApiModulesCalendarIntegrationsHandlersListCalendarSourcesRequest: Record<string, never>;
|
||||
SocializeApiModulesCalendarIntegrationsHandlersUserCalendarExportFeedDto: {
|
||||
isEnabled?: boolean;
|
||||
feedUrl?: string | null;
|
||||
/** Format: date-time */
|
||||
createdAt?: string | null;
|
||||
/** Format: date-time */
|
||||
updatedAt?: string | null;
|
||||
/** Format: date-time */
|
||||
revokedAt?: string | null;
|
||||
};
|
||||
SocializeApiModulesAssetsHandlersAssetRevisionDto: {
|
||||
/** Format: guid */
|
||||
id?: string;
|
||||
@@ -2001,6 +2232,48 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersChangeOrganizationLogoHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
organizationId: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"multipart/form-data": components["schemas"]["SocializeApiModulesOrganizationsHandlersChangeOrganizationLogoRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesOrganizationsHandlersChangeOrganizationLogoResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesOrganizationsHandlersGetOrganizationHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -3353,6 +3626,35 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesContentItemsHandlersGetContentItemActivityHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesContentItemsHandlersContentItemActivityEntryDto"][];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesContentItemsHandlersUpdateContentItemStatusHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -3433,7 +3735,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCommentsHandlersCreateCommentRequest"];
|
||||
"multipart/form-data": components["schemas"]["SocializeApiModulesCommentsHandlersCreateCommentRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@@ -3464,35 +3766,6 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCommentsHandlersResolveCommentHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCommentsHandlersCommentDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesClientsHandlersChangeClientPortraitHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -3923,6 +4196,229 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersListCalendarCatalogHandler: {
|
||||
parameters: {
|
||||
query?: {
|
||||
search?: string | null;
|
||||
country?: string | null;
|
||||
region?: string | null;
|
||||
language?: string | null;
|
||||
category?: string | null;
|
||||
cultureOrReligion?: string | null;
|
||||
provider?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCalendarIntegrationsHandlersCalendarCatalogEntryDto"][];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersListCalendarEventsHandler: {
|
||||
parameters: {
|
||||
query?: {
|
||||
workspaceId?: string | null;
|
||||
startDate?: string | null;
|
||||
endDate?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCalendarIntegrationsHandlersCalendarEventDto"][];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersRefreshCalendarSourceHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
sourceId: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCalendarIntegrationsHandlersCalendarSourceDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersGetUserCalendarExportFeedHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCalendarIntegrationsHandlersUserCalendarExportFeedDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersRevokeUserCalendarExportFeedHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCalendarIntegrationsHandlersUserCalendarExportFeedDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersEnableUserCalendarExportFeedHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCalendarIntegrationsHandlersUserCalendarExportFeedDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersRegenerateUserCalendarExportFeedHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesCalendarIntegrationsHandlersUserCalendarExportFeedDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCalendarIntegrationsHandlersGetUserCalendarExportFeedIcsHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
token: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description No Content */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesAssetsHandlersCreateAssetRevisionHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
:root {
|
||||
--socialize-primary: #172033;
|
||||
--socialize-accent: #ff8a3d;
|
||||
--socialize-accent-strong: #ef4444;
|
||||
--socialize-brand-gradient: linear-gradient(135deg, var(--socialize-accent) 0%, var(--socialize-accent-strong) 100%);
|
||||
--socialize-accent-shadow: rgba(255, 138, 61, 0.28);
|
||||
--socialize-accent-strong-shadow: rgba(239, 68, 68, 0.28);
|
||||
--socialize-highlight: #2fa58d;
|
||||
--h-background: #f4f6f3;
|
||||
--h-on-background: #172033;
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
|
||||
.login-brand-mark {
|
||||
@apply flex h-11 w-11 items-center justify-center rounded-2xl text-lg font-black;
|
||||
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
|
||||
background: var(--socialize-brand-gradient);
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
|
||||
69
frontend/src/features/content/components/ColorPalette.vue
Normal file
69
frontend/src/features/content/components/ColorPalette.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '#2F80ED',
|
||||
},
|
||||
colors: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
'#2F80ED',
|
||||
'#0F766E',
|
||||
'#16A34A',
|
||||
'#F59E0B',
|
||||
'#EF4444',
|
||||
'#EC4899',
|
||||
'#8B5CF6',
|
||||
'#475569',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="color-palette">
|
||||
<button
|
||||
v-for="color in props.colors"
|
||||
:key="color"
|
||||
class="color-option"
|
||||
:class="{ active: color.toLowerCase() === props.modelValue.toLowerCase() }"
|
||||
type="button"
|
||||
:style="{ background: color }"
|
||||
:aria-label="color"
|
||||
@click="emit('update:modelValue', color)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.color-palette {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1.75rem);
|
||||
gap: 0.5rem;
|
||||
width: max-content;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid rgba(23, 32, 51, 0.1);
|
||||
border-radius: 0.75rem;
|
||||
background: #ffffff;
|
||||
border-color: rgba(23, 32, 51, 0.1);
|
||||
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.14);
|
||||
}
|
||||
|
||||
.color-option {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
border-radius: 9999px;
|
||||
border-color: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 0 0 1px rgba(23, 32, 51, 0.12);
|
||||
transition: box-shadow 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.color-option:hover,
|
||||
.color-option.active {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 0 0 2px #172033;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { mdiAt, mdiClose, mdiImagePlusOutline, mdiLockOutline, mdiSend } from '@mdi/js';
|
||||
import AppAvatar from '@/components/AppAvatar.vue';
|
||||
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
|
||||
|
||||
const props = defineProps({
|
||||
members: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isPosting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
replyTarget: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit-comment', 'cancel-reply']);
|
||||
const userProfileStore = useUserProfileStore();
|
||||
const mediaFileInput = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
body: '',
|
||||
isInternal: false,
|
||||
mediaFile: null,
|
||||
showMentionPicker: false,
|
||||
});
|
||||
|
||||
const currentUserEmail = computed(() => userProfileStore.user?.email ?? '');
|
||||
const currentUserName = computed(() => userProfileStore.alias);
|
||||
const isReplyVariant = computed(() => props.variant === 'reply');
|
||||
const canSubmit = computed(() =>
|
||||
Boolean(form.body.trim() || form.mediaFile) &&
|
||||
!props.isPosting
|
||||
);
|
||||
|
||||
function submitComment() {
|
||||
if (!canSubmit.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bodyParts = [];
|
||||
const body = form.body.trim();
|
||||
|
||||
if (form.isInternal) {
|
||||
bodyParts.push('[Internal]');
|
||||
}
|
||||
|
||||
if (body) {
|
||||
bodyParts.push(body);
|
||||
}
|
||||
|
||||
emit('submit-comment', {
|
||||
body: bodyParts.join('\n\n'),
|
||||
isInternal: form.isInternal,
|
||||
mediaReference: form.mediaFile?.name ?? null,
|
||||
mediaFile: form.mediaFile,
|
||||
mediaFileName: form.mediaFile?.name ?? null,
|
||||
mediaFileSize: form.mediaFile?.size ?? null,
|
||||
mediaFileType: form.mediaFile?.type || null,
|
||||
parentCommentId: props.replyTarget?.id ?? null,
|
||||
});
|
||||
|
||||
form.body = '';
|
||||
form.isInternal = false;
|
||||
form.mediaFile = null;
|
||||
form.showMentionPicker = false;
|
||||
if (mediaFileInput.value) {
|
||||
mediaFileInput.value.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function openMediaPicker() {
|
||||
mediaFileInput.value?.click();
|
||||
form.showMentionPicker = false;
|
||||
}
|
||||
|
||||
function selectMediaFile(event) {
|
||||
form.mediaFile = event.target.files?.[0] ?? null;
|
||||
}
|
||||
|
||||
function clearMediaFile() {
|
||||
form.mediaFile = null;
|
||||
if (mediaFileInput.value) {
|
||||
mediaFileInput.value.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMentionPicker() {
|
||||
form.showMentionPicker = !form.showMentionPicker;
|
||||
}
|
||||
|
||||
function insertMention(member) {
|
||||
const label = member.displayName || member.email;
|
||||
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mention = `@${label.replace(/\s+/g, '')}`;
|
||||
const separator = form.body && !/\s$/.test(form.body) ? ' ' : '';
|
||||
form.body = `${form.body}${separator}${mention} `;
|
||||
form.showMentionPicker = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="comment-composer"
|
||||
:class="variant"
|
||||
>
|
||||
<div
|
||||
v-if="replyTarget && !isReplyVariant"
|
||||
class="reply-context"
|
||||
>
|
||||
<div>
|
||||
<span>Replying to</span>
|
||||
<strong>{{ replyTarget.authorDisplayName }}</strong>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
title="Cancel reply"
|
||||
@click="emit('cancel-reply')"
|
||||
>
|
||||
<v-icon :icon="mdiClose" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="comment-composer-main">
|
||||
<AppAvatar
|
||||
v-if="!isReplyVariant"
|
||||
:name="currentUserName"
|
||||
:email="currentUserEmail"
|
||||
:src="userProfileStore.portraitUrl"
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
v-model="form.body"
|
||||
class="comment-textarea"
|
||||
:placeholder="replyTarget ? 'Write a reply...' : 'Write a comment...'"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.mediaFile"
|
||||
class="selected-media-file"
|
||||
>
|
||||
<span>{{ form.mediaFile.name }}</span>
|
||||
<button
|
||||
type="button"
|
||||
title="Remove selected media"
|
||||
@click="clearMediaFile"
|
||||
>
|
||||
<v-icon :icon="mdiClose" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.showMentionPicker"
|
||||
class="mention-picker"
|
||||
>
|
||||
<button
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
class="mention-option"
|
||||
type="button"
|
||||
@click="insertMention(member)"
|
||||
>
|
||||
<AppAvatar
|
||||
:name="member.displayName"
|
||||
:email="member.email"
|
||||
size="sm"
|
||||
/>
|
||||
<span>{{ member.displayName }}</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="!members.length"
|
||||
class="empty-note"
|
||||
>
|
||||
No workspace members are available to mention.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment-composer-toolbar">
|
||||
<div class="comment-tool-actions">
|
||||
<label
|
||||
class="icon-tool-button internal-toggle"
|
||||
:class="{ active: form.isInternal }"
|
||||
title="Internal comment"
|
||||
>
|
||||
<input
|
||||
v-model="form.isInternal"
|
||||
type="checkbox"
|
||||
/>
|
||||
<v-icon :icon="mdiLockOutline" />
|
||||
</label>
|
||||
<button
|
||||
class="icon-tool-button"
|
||||
type="button"
|
||||
title="Upload media from computer"
|
||||
:class="{ active: form.mediaFile }"
|
||||
@click="openMediaPicker"
|
||||
>
|
||||
<v-icon :icon="mdiImagePlusOutline" />
|
||||
</button>
|
||||
<input
|
||||
ref="mediaFileInput"
|
||||
class="sr-only"
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
@change="selectMediaFile"
|
||||
/>
|
||||
<button
|
||||
class="icon-tool-button"
|
||||
type="button"
|
||||
title="Mention a member"
|
||||
:class="{ active: form.showMentionPicker }"
|
||||
@click="toggleMentionPicker"
|
||||
>
|
||||
<v-icon :icon="mdiAt" />
|
||||
</button>
|
||||
<button
|
||||
class="post-button"
|
||||
type="button"
|
||||
:disabled="!canSubmit"
|
||||
@click="submitComment"
|
||||
>
|
||||
<v-icon :icon="mdiSend" />
|
||||
{{ isPosting ? 'Posting...' : 'Post' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.comment-composer {
|
||||
@apply flex flex-col gap-3 rounded-[1.25rem] border p-4;
|
||||
background: #fffdf8;
|
||||
border-color: rgba(23, 32, 51, 0.12);
|
||||
}
|
||||
|
||||
.comment-composer.reply {
|
||||
@apply rounded-[1rem] p-3;
|
||||
background: rgba(255, 253, 248, 0.84);
|
||||
}
|
||||
|
||||
.comment-composer-main {
|
||||
@apply flex items-start gap-3;
|
||||
}
|
||||
|
||||
.reply-context {
|
||||
@apply flex items-center justify-between gap-3 rounded-[1rem] border px-3 py-2;
|
||||
background: rgba(15, 118, 110, 0.06);
|
||||
border-color: rgba(15, 118, 110, 0.14);
|
||||
}
|
||||
|
||||
.reply-context div {
|
||||
@apply flex min-w-0 items-center gap-2 text-sm;
|
||||
}
|
||||
|
||||
.reply-context span {
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.reply-context strong {
|
||||
@apply truncate;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.reply-context button {
|
||||
@apply inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.reply-context button:hover,
|
||||
.reply-context button:focus-visible {
|
||||
background: rgba(15, 118, 110, 0.12);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.comment-textarea {
|
||||
@apply min-h-24 flex-1 resize-y border-0 bg-transparent text-sm leading-6;
|
||||
color: #172033;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.comment-textarea::placeholder {
|
||||
color: #7c8798;
|
||||
}
|
||||
|
||||
.selected-media-file {
|
||||
@apply flex items-center justify-between gap-3 rounded-[1rem] border px-3 py-2 text-sm;
|
||||
background: rgba(23, 32, 51, 0.03);
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.selected-media-file span {
|
||||
@apply min-w-0 truncate font-semibold;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.selected-media-file button {
|
||||
@apply inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.selected-media-file button:hover,
|
||||
.selected-media-file button:focus-visible {
|
||||
background: rgba(185, 28, 28, 0.1);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.mention-picker {
|
||||
@apply grid max-h-52 gap-2 overflow-y-auto rounded-[1rem] border p-2;
|
||||
background: rgba(23, 32, 51, 0.03);
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.mention-option {
|
||||
@apply flex items-center gap-3 rounded-[0.875rem] px-2 py-2 text-left text-sm font-semibold transition;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.mention-option:hover {
|
||||
background: rgba(15, 118, 110, 0.1);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.empty-note {
|
||||
@apply text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.comment-composer-toolbar {
|
||||
@apply flex items-center justify-end gap-2 border-t pt-3;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.internal-toggle {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.internal-toggle input {
|
||||
@apply sr-only;
|
||||
}
|
||||
|
||||
.comment-tool-actions {
|
||||
@apply flex min-w-0 items-center justify-end gap-2;
|
||||
}
|
||||
|
||||
.icon-tool-button,
|
||||
.post-button {
|
||||
@apply inline-flex min-h-10 items-center justify-center gap-2 rounded-full px-3 text-sm font-bold transition;
|
||||
}
|
||||
|
||||
.icon-tool-button {
|
||||
@apply w-10;
|
||||
background: rgba(23, 32, 51, 0.06);
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.icon-tool-button:hover,
|
||||
.icon-tool-button.active {
|
||||
background: rgba(15, 118, 110, 0.12);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.post-button {
|
||||
@apply px-4;
|
||||
background: #172033;
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.post-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
</style>
|
||||
386
frontend/src/features/content/components/ContentCommentFeed.vue
Normal file
386
frontend/src/features/content/components/ContentCommentFeed.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import {
|
||||
mdiCheckCircleOutline,
|
||||
mdiDeleteOutline,
|
||||
mdiDotsVertical,
|
||||
mdiEmoticonPlusOutline,
|
||||
mdiPencilOutline,
|
||||
mdiReplyOutline,
|
||||
} from '@mdi/js';
|
||||
import AppAvatar from '@/components/AppAvatar.vue';
|
||||
import ContentCommentComposer from '@/features/content/components/ContentCommentComposer.vue';
|
||||
|
||||
const props = defineProps({
|
||||
comments: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isPosting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit-comment']);
|
||||
const activeReplyCommentId = ref(null);
|
||||
const commentThreads = computed(() => {
|
||||
const repliesByParentId = new Map();
|
||||
const roots = [];
|
||||
|
||||
for (const comment of props.comments) {
|
||||
if (comment.parentCommentId) {
|
||||
const existing = repliesByParentId.get(comment.parentCommentId) ?? [];
|
||||
existing.push(comment);
|
||||
repliesByParentId.set(comment.parentCommentId, existing);
|
||||
} else {
|
||||
roots.push(comment);
|
||||
}
|
||||
}
|
||||
|
||||
return roots.map(comment => ({
|
||||
comment,
|
||||
replies: repliesByParentId.get(comment.id) ?? [],
|
||||
}));
|
||||
});
|
||||
|
||||
function formatDateTime(value) {
|
||||
return value ? new Date(value).toLocaleString() : '';
|
||||
}
|
||||
|
||||
function hasImageAttachment(comment) {
|
||||
return Boolean(comment.attachmentBlobUrl && comment.attachmentContentType?.startsWith('image/'));
|
||||
}
|
||||
|
||||
function submitReply(payload) {
|
||||
emit('submit-comment', payload);
|
||||
activeReplyCommentId.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="timeline-list">
|
||||
<article
|
||||
v-for="thread in commentThreads"
|
||||
:key="thread.comment.id"
|
||||
class="comment-row"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="comment-row-header">
|
||||
<div class="comment-author">
|
||||
<AppAvatar
|
||||
:name="thread.comment.authorDisplayName"
|
||||
:email="thread.comment.authorEmail"
|
||||
:src="thread.comment.authorPortraitUrl"
|
||||
size="sm"
|
||||
/>
|
||||
<div class="comment-author-meta">
|
||||
<strong>{{ thread.comment.authorDisplayName }}</strong>
|
||||
<small>{{ formatDateTime(thread.comment.createdAt) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment-actions">
|
||||
<button
|
||||
class="comment-action-button"
|
||||
type="button"
|
||||
title="Add reaction"
|
||||
>
|
||||
<v-icon :icon="mdiEmoticonPlusOutline" />
|
||||
</button>
|
||||
<button
|
||||
class="comment-action-button"
|
||||
type="button"
|
||||
title="Resolve"
|
||||
>
|
||||
<v-icon :icon="mdiCheckCircleOutline" />
|
||||
</button>
|
||||
<button
|
||||
class="comment-action-button"
|
||||
type="button"
|
||||
title="Reply"
|
||||
@click="activeReplyCommentId = thread.comment.id"
|
||||
>
|
||||
<v-icon :icon="mdiReplyOutline" />
|
||||
</button>
|
||||
<details class="comment-more-menu">
|
||||
<summary
|
||||
class="comment-action-button"
|
||||
title="More comment actions"
|
||||
>
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
</summary>
|
||||
<div class="comment-action-menu">
|
||||
<button
|
||||
class="comment-menu-item"
|
||||
type="button"
|
||||
>
|
||||
<v-icon :icon="mdiPencilOutline" />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="comment-menu-item danger"
|
||||
type="button"
|
||||
>
|
||||
<v-icon :icon="mdiDeleteOutline" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="thread.comment.body"
|
||||
class="comment-body"
|
||||
>
|
||||
{{ thread.comment.body }}
|
||||
</p>
|
||||
|
||||
<a
|
||||
v-if="hasImageAttachment(thread.comment)"
|
||||
class="comment-attachment"
|
||||
:href="thread.comment.attachmentBlobUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="thread.comment.attachmentBlobUrl"
|
||||
:alt="thread.comment.attachmentFileName || 'Comment attachment'"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-if="thread.replies.length"
|
||||
class="reply-list"
|
||||
>
|
||||
<article
|
||||
v-for="reply in thread.replies"
|
||||
:key="reply.id"
|
||||
class="reply-row"
|
||||
>
|
||||
<AppAvatar
|
||||
:name="reply.authorDisplayName"
|
||||
:email="reply.authorEmail"
|
||||
:src="reply.authorPortraitUrl"
|
||||
size="sm"
|
||||
/>
|
||||
<div>
|
||||
<div class="reply-meta">
|
||||
<strong>{{ reply.authorDisplayName }}</strong>
|
||||
<small>{{ formatDateTime(reply.createdAt) }}</small>
|
||||
</div>
|
||||
<p v-if="reply.body">{{ reply.body }}</p>
|
||||
<a
|
||||
v-if="hasImageAttachment(reply)"
|
||||
class="comment-attachment reply-attachment"
|
||||
:href="reply.attachmentBlobUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="reply.attachmentBlobUrl"
|
||||
:alt="reply.attachmentFileName || 'Reply attachment'"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<ContentCommentComposer
|
||||
v-if="activeReplyCommentId === thread.comment.id"
|
||||
variant="reply"
|
||||
:members="members"
|
||||
:is-posting="isPosting"
|
||||
:reply-target="thread.comment"
|
||||
@submit-comment="submitReply"
|
||||
@cancel-reply="activeReplyCommentId = null"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<div
|
||||
v-if="!commentThreads.length"
|
||||
class="empty-note"
|
||||
>
|
||||
No comments yet.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.timeline-list {
|
||||
@apply flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.empty-note {
|
||||
@apply text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.comment-row {
|
||||
@apply relative flex flex-col gap-3 rounded-[1rem] border p-4 transition;
|
||||
background: #f8fafc;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.comment-row:hover,
|
||||
.comment-row:focus-within,
|
||||
.comment-row:focus {
|
||||
background: #fffdf8;
|
||||
border-color: rgba(15, 118, 110, 0.24);
|
||||
box-shadow: 0 16px 34px rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.comment-row-header {
|
||||
@apply flex min-h-9 w-full items-center;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
@apply flex w-full min-w-0 items-center gap-3;
|
||||
}
|
||||
|
||||
.comment-author-meta {
|
||||
@apply flex w-full min-w-0 flex-col;
|
||||
}
|
||||
|
||||
.comment-author strong {
|
||||
@apply truncate text-sm;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.comment-author small {
|
||||
@apply text-xs leading-5;
|
||||
color: #7c8798;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
@apply absolute right-3 top-3 z-20 flex items-center gap-1 rounded-full border px-1 py-1 opacity-0 shadow-lg transition;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.comment-row:hover .comment-actions,
|
||||
.comment-row:focus-within .comment-actions,
|
||||
.comment-row:focus .comment-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.comment-action-button {
|
||||
@apply inline-flex h-8 w-8 items-center justify-center rounded-full transition;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.comment-action-button:hover,
|
||||
.comment-action-button:focus-visible {
|
||||
background: rgba(15, 118, 110, 0.12);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.comment-more-menu {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.comment-more-menu summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.comment-more-menu summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-more-menu[open] .comment-action-button {
|
||||
background: rgba(15, 118, 110, 0.12);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.comment-more-menu[open] .comment-action-menu,
|
||||
.comment-more-menu:hover .comment-action-menu,
|
||||
.comment-more-menu:focus-within .comment-action-menu {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.comment-action-menu {
|
||||
@apply absolute right-0 top-[calc(100%+0.375rem)] z-20 hidden min-w-36 flex-col gap-1 rounded-[0.875rem] border p-1 shadow-xl;
|
||||
background: #ffffff;
|
||||
border-color: rgba(23, 32, 51, 0.1);
|
||||
}
|
||||
|
||||
.comment-menu-item {
|
||||
@apply flex items-center gap-2 rounded-[0.625rem] px-3 py-2 text-sm font-semibold transition;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.comment-menu-item:hover,
|
||||
.comment-menu-item:focus-visible {
|
||||
background: rgba(15, 118, 110, 0.1);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.comment-menu-item.danger {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.comment-menu-item.danger:hover,
|
||||
.comment-menu-item.danger:focus-visible {
|
||||
background: rgba(185, 28, 28, 0.1);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
@apply whitespace-pre-line text-sm leading-6;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.comment-attachment {
|
||||
@apply block w-fit max-w-full overflow-hidden rounded-[0.875rem] border;
|
||||
border-color: rgba(23, 32, 51, 0.1);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.comment-attachment img {
|
||||
@apply block max-h-72 max-w-full object-contain;
|
||||
}
|
||||
|
||||
.reply-attachment img {
|
||||
@apply max-h-56;
|
||||
}
|
||||
|
||||
.reply-list {
|
||||
@apply ml-2 mt-2 flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.reply-row {
|
||||
@apply flex items-start gap-3;
|
||||
}
|
||||
|
||||
.reply-row > div {
|
||||
@apply flex min-w-0 flex-col gap-1;
|
||||
}
|
||||
|
||||
.reply-meta {
|
||||
@apply flex min-w-0 flex-col;
|
||||
}
|
||||
|
||||
.reply-meta strong {
|
||||
@apply truncate text-sm;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.reply-meta small {
|
||||
@apply text-xs leading-5;
|
||||
color: #7c8798;
|
||||
}
|
||||
|
||||
.reply-row p {
|
||||
@apply whitespace-pre-line text-sm leading-6;
|
||||
color: #172033;
|
||||
}
|
||||
</style>
|
||||
@@ -128,6 +128,29 @@ export const useCalendarIntegrationsStore = defineStore('calendar-integrations',
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSource(sourceId, payload) {
|
||||
if (!sourceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.put(`/api/calendar-integrations/sources/${sourceId}`, payload);
|
||||
const updatedSource = response.data;
|
||||
if (updatedSource) {
|
||||
sources.value = sources.value.map(source =>
|
||||
source.id === updatedSource.id ? updatedSource : source
|
||||
);
|
||||
}
|
||||
return updatedSource;
|
||||
} catch (updateError) {
|
||||
console.error('Failed to update calendar source:', updateError);
|
||||
error.value = 'Failed to update calendar source.';
|
||||
throw updateError;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSource(sourceId) {
|
||||
if (!sourceId) {
|
||||
return null;
|
||||
@@ -176,6 +199,7 @@ export const useCalendarIntegrationsStore = defineStore('calendar-integrations',
|
||||
fetchEvents,
|
||||
searchCatalog,
|
||||
createSource,
|
||||
updateSource,
|
||||
refreshSource,
|
||||
toggleSourceVisibility,
|
||||
};
|
||||
|
||||
@@ -129,11 +129,8 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
||||
actions.comment = true;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/comments', {
|
||||
...payload,
|
||||
contentItemId,
|
||||
workspaceId: currentItemWorkspaceId(),
|
||||
});
|
||||
const requestPayload = buildCommentPayload(contentItemId, payload);
|
||||
const response = await client.post('/api/comments', requestPayload);
|
||||
if (response.data) {
|
||||
comments.value = [...comments.value, response.data];
|
||||
await fetchActivity(contentItemId);
|
||||
@@ -144,6 +141,22 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
||||
}
|
||||
}
|
||||
|
||||
function buildCommentPayload(contentItemId, payload) {
|
||||
const workspaceId = currentItemWorkspaceId();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('workspaceId', workspaceId);
|
||||
formData.append('contentItemId', contentItemId);
|
||||
formData.append('body', payload.body ?? '');
|
||||
if (payload.parentCommentId) {
|
||||
formData.append('parentCommentId', payload.parentCommentId);
|
||||
}
|
||||
if (payload.mediaFile) {
|
||||
formData.append('attachment', payload.mediaFile, payload.mediaFile.name || 'comment-attachment');
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
async function submitDecision(contentItemId, approvalId, payload) {
|
||||
actions.decision = true;
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import { mdiArrowLeft } from '@mdi/js';
|
||||
import AppAvatar from '@/components/AppAvatar.vue';
|
||||
import { useChannelsStore } from '@/features/channels/stores/channelsStore.js';
|
||||
import { useClientsStore } from '@/features/clients/stores/clientsStore.js';
|
||||
import ContentApprovalPanel from '@/features/content/components/ContentApprovalPanel.vue';
|
||||
import ContentCommentComposer from '@/features/content/components/ContentCommentComposer.vue';
|
||||
import ContentCommentFeed from '@/features/content/components/ContentCommentFeed.vue';
|
||||
import { useCalendarIntegrationsStore } from '@/features/content/stores/calendarIntegrationsStore.js';
|
||||
import { useContentItemDetailStore } from '@/features/content/stores/contentItemDetailStore.js';
|
||||
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
||||
@@ -39,10 +40,6 @@
|
||||
placements: [],
|
||||
});
|
||||
|
||||
const commentForm = reactive({
|
||||
body: '',
|
||||
});
|
||||
|
||||
const assetForm = reactive({
|
||||
assetType: 'Image',
|
||||
displayName: '',
|
||||
@@ -102,6 +99,11 @@
|
||||
{ key: 'assets', label: 'Assets', count: detailStore.assets.length },
|
||||
{ key: 'activity', label: 'Activity', count: detailStore.activity.length },
|
||||
]);
|
||||
const workspaceMembers = computed(() =>
|
||||
contentWorkspaceId.value
|
||||
? workspaceStore.membersByWorkspace[contentWorkspaceId.value] ?? []
|
||||
: []
|
||||
);
|
||||
const selectedDateKey = computed(() => /^\d{4}-\d{2}-\d{2}$/.test(form.dueDate) ? form.dueDate : '');
|
||||
const contextAnchorDate = computed(() => selectedDateKey.value ? parseDateKey(selectedDateKey.value) : startOfDay(new Date()));
|
||||
const calendarFetchRange = computed(() => {
|
||||
@@ -450,13 +452,12 @@
|
||||
await detailStore.submitDecision(contentItemId.value, approvalId, payload);
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
if (!contentItemId.value || !commentForm.body.trim()) {
|
||||
async function submitComment(payload) {
|
||||
if (!contentItemId.value || !payload?.body?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await detailStore.addComment(contentItemId.value, { body: commentForm.body.trim() });
|
||||
commentForm.body = '';
|
||||
await detailStore.addComment(contentItemId.value, payload);
|
||||
}
|
||||
|
||||
function inferGoogleDriveFileId(value) {
|
||||
@@ -674,6 +675,7 @@
|
||||
await Promise.all([
|
||||
calendarStore.fetchSources(workspaceId),
|
||||
calendarStore.fetchEvents({ workspaceId, startDate, endDate }),
|
||||
workspaceStore.fetchMembers(workspaceId),
|
||||
]);
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -1127,50 +1129,18 @@
|
||||
</div>
|
||||
|
||||
<template v-if="activeProductionTab === 'comments'">
|
||||
<div class="panel-stack">
|
||||
<label class="field field-wide">
|
||||
<span>New comment</span>
|
||||
<textarea v-model="commentForm.body"></textarea>
|
||||
</label>
|
||||
<button
|
||||
class="primary-button"
|
||||
:disabled="detailStore.actions.comment"
|
||||
@click="submitComment"
|
||||
>
|
||||
{{ detailStore.actions.comment ? 'Posting...' : 'Post comment' }}
|
||||
</button>
|
||||
</div>
|
||||
<ContentCommentComposer
|
||||
:members="workspaceMembers"
|
||||
:is-posting="detailStore.actions.comment"
|
||||
@submit-comment="submitComment"
|
||||
/>
|
||||
|
||||
<div class="timeline-list">
|
||||
<article
|
||||
v-for="comment in detailStore.comments"
|
||||
:key="comment.id"
|
||||
class="timeline-row"
|
||||
>
|
||||
<div class="identity-row align-start">
|
||||
<AppAvatar
|
||||
:name="comment.authorDisplayName"
|
||||
:email="comment.authorEmail"
|
||||
:src="comment.authorPortraitUrl"
|
||||
size="sm"
|
||||
/>
|
||||
<div>
|
||||
<strong>{{ comment.authorDisplayName }}</strong>
|
||||
<span>{{ comment.body }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-actions">
|
||||
<small>{{ formatDateTime(comment.createdAt) }}</small>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div
|
||||
v-if="!detailStore.comments.length"
|
||||
class="empty-note"
|
||||
>
|
||||
No comments yet.
|
||||
</div>
|
||||
</div>
|
||||
<ContentCommentFeed
|
||||
:comments="detailStore.comments"
|
||||
:members="workspaceMembers"
|
||||
:is-posting="detailStore.actions.comment"
|
||||
@submit-comment="submitComment"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeProductionTab === 'revisions'">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,13 +35,20 @@
|
||||
|
||||
.feedback-entry-button {
|
||||
@apply flex h-12 items-center gap-2 rounded-full border px-4 text-sm font-bold shadow-lg transition-colors;
|
||||
background: #172033;
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
color: #fffaf2;
|
||||
background: var(--socialize-accent-strong);
|
||||
border-color: rgba(255, 255, 255, 0.55);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 16px 34px var(--socialize-accent-strong-shadow);
|
||||
}
|
||||
|
||||
.feedback-entry-button:hover {
|
||||
background: #0f766e;
|
||||
background: color-mix(in srgb, var(--socialize-accent-strong) 82%, var(--socialize-primary));
|
||||
box-shadow: 0 18px 38px var(--socialize-accent-strong-shadow);
|
||||
}
|
||||
|
||||
.feedback-entry-button:focus-visible {
|
||||
outline: 3px solid color-mix(in srgb, var(--socialize-accent) 35%, transparent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.feedback-entry-button span {
|
||||
|
||||
@@ -1,18 +1,78 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||
import WorkspaceSelector from './WorkspaceSelector.vue';
|
||||
import {
|
||||
mdiCalendar,
|
||||
mdiChevronDown,
|
||||
mdiCogOutline,
|
||||
mdiFormatListBulleted,
|
||||
mdiLogin,
|
||||
mdiPlus,
|
||||
mdiTable,
|
||||
} from '@mdi/js';
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
const isContentViewMenuOpen = ref(false);
|
||||
|
||||
const contentViewActions = computed(() => {
|
||||
if (route.name !== 'content-items') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = route.query;
|
||||
const activeView = ['upcoming', 'table'].includes(query.view) ? query.view : 'calendar';
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'calendar',
|
||||
label: t('contentItems.views.calendar'),
|
||||
icon: mdiCalendar,
|
||||
active: activeView === 'calendar',
|
||||
route: {
|
||||
name: 'content-items',
|
||||
query: {
|
||||
...query,
|
||||
view: query.view === 'week' ? 'week' : 'month',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'upcoming',
|
||||
label: t('contentItems.upcoming'),
|
||||
icon: mdiFormatListBulleted,
|
||||
active: activeView === 'upcoming',
|
||||
route: {
|
||||
name: 'content-items',
|
||||
query: {
|
||||
...query,
|
||||
view: 'upcoming',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
label: t('contentItems.views.table'),
|
||||
icon: mdiTable,
|
||||
active: activeView === 'table',
|
||||
route: {
|
||||
name: 'content-items',
|
||||
query: {
|
||||
...query,
|
||||
view: 'table',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const activeContentViewAction = computed(() =>
|
||||
contentViewActions.value.find(action => action.active) ?? contentViewActions.value[0]
|
||||
);
|
||||
|
||||
const appBarActions = computed(() => {
|
||||
if (!authStore.isAuthenticated) {
|
||||
@@ -79,6 +139,46 @@
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="contentViewActions.length"
|
||||
class="view-selector"
|
||||
>
|
||||
<button
|
||||
class="menu-item-action view-selector-button"
|
||||
type="button"
|
||||
@click="isContentViewMenuOpen = !isContentViewMenuOpen"
|
||||
>
|
||||
<v-icon :icon="activeContentViewAction.icon" />
|
||||
<span class="label">{{ activeContentViewAction.label }}</span>
|
||||
<v-icon
|
||||
class="selector-chevron"
|
||||
:icon="mdiChevronDown"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="isContentViewMenuOpen"
|
||||
class="view-selector-menu"
|
||||
>
|
||||
<router-link
|
||||
v-for="action in contentViewActions"
|
||||
:key="action.key"
|
||||
:to="action.route"
|
||||
class="menu-action-link"
|
||||
@click="isContentViewMenuOpen = false"
|
||||
>
|
||||
<button
|
||||
class="view-selector-option"
|
||||
:class="{ 'view-selector-option-active': action.active }"
|
||||
type="button"
|
||||
>
|
||||
<v-icon :icon="action.icon" />
|
||||
<span>{{ action.label }}</span>
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
v-for="action in appBarActions"
|
||||
:key="action.key"
|
||||
@@ -121,6 +221,24 @@
|
||||
@apply justify-end;
|
||||
}
|
||||
|
||||
.view-selector {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.view-selector-button {
|
||||
@apply min-w-11 justify-between;
|
||||
}
|
||||
|
||||
.selector-chevron {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
.view-selector-menu {
|
||||
@apply absolute right-0 top-[calc(100%+0.5rem)] z-30 flex min-w-52 flex-col gap-1 rounded-[1rem] border p-2 shadow-xl;
|
||||
background: #ffffff;
|
||||
border-color: rgba(23, 32, 51, 0.1);
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply hidden text-nowrap md:inline;
|
||||
}
|
||||
@@ -137,6 +255,17 @@
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.view-selector-option {
|
||||
@apply flex min-h-11 w-full items-center gap-3 rounded-[0.75rem] px-3 text-left text-sm font-semibold transition;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.view-selector-option:hover,
|
||||
.view-selector-option-active {
|
||||
background: #172033;
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.menu-item-action i {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
@@ -665,7 +665,7 @@
|
||||
|
||||
.brand-mark {
|
||||
@apply flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[1.1rem] text-xl font-black;
|
||||
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
|
||||
background: var(--socialize-brand-gradient);
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
|
||||
@@ -859,6 +859,17 @@
|
||||
"newItem": "New content item",
|
||||
"createTitle": "Create content item",
|
||||
"upcoming": "Upcoming",
|
||||
"views": {
|
||||
"calendar": "Calendar view",
|
||||
"table": "Table"
|
||||
},
|
||||
"table": {
|
||||
"content": "Content",
|
||||
"status": "Status",
|
||||
"channels": "Channels",
|
||||
"revision": "Revision",
|
||||
"dueDate": "Due date"
|
||||
},
|
||||
"loading": "Loading content items...",
|
||||
"empty": "No content items are available for the active workspace.",
|
||||
"noDueDate": "No due date",
|
||||
@@ -879,9 +890,14 @@
|
||||
"category": "Category",
|
||||
"calendarName": "Calendar name",
|
||||
"icsUrl": "ICS URL",
|
||||
"editColor": "Edit calendar color",
|
||||
"allDay": "All day",
|
||||
"context": "Calendar context",
|
||||
"importedEvent": "Imported calendar",
|
||||
"previousWeek": "Previous week",
|
||||
"nextWeek": "Next week",
|
||||
"previousMonth": "Previous month",
|
||||
"nextMonth": "Next month",
|
||||
"errors": {
|
||||
"required": "Calendar name and URL are required.",
|
||||
"duplicate": "This calendar has already been added.",
|
||||
|
||||
@@ -859,6 +859,17 @@
|
||||
"newItem": "Nouvel élément de contenu",
|
||||
"createTitle": "Créer un élément de contenu",
|
||||
"upcoming": "À venir",
|
||||
"views": {
|
||||
"calendar": "Vue calendrier",
|
||||
"table": "Tableau"
|
||||
},
|
||||
"table": {
|
||||
"content": "Contenu",
|
||||
"status": "Statut",
|
||||
"channels": "Canaux",
|
||||
"revision": "Révision",
|
||||
"dueDate": "Échéance"
|
||||
},
|
||||
"loading": "Chargement des éléments de contenu...",
|
||||
"empty": "Aucun élément de contenu n'est disponible pour l'espace actif.",
|
||||
"noDueDate": "Aucune échéance",
|
||||
@@ -879,9 +890,14 @@
|
||||
"category": "Catégorie",
|
||||
"calendarName": "Nom du calendrier",
|
||||
"icsUrl": "URL ICS",
|
||||
"editColor": "Modifier la couleur du calendrier",
|
||||
"allDay": "Toute la journée",
|
||||
"context": "Contexte calendrier",
|
||||
"importedEvent": "Calendrier importé",
|
||||
"previousWeek": "Semaine précédente",
|
||||
"nextWeek": "Semaine suivante",
|
||||
"previousMonth": "Mois précédent",
|
||||
"nextMonth": "Mois suivant",
|
||||
"errors": {
|
||||
"required": "Le nom et l'URL du calendrier sont requis.",
|
||||
"duplicate": "Ce calendrier a déjà été ajouté.",
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
|
||||
.site-brand-mark {
|
||||
@apply flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-2xl text-base font-black;
|
||||
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
|
||||
background: var(--socialize-brand-gradient);
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user