From 79df1229cb1139b131e9c1bf91fadf243c141e1c Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:59:31 +0100 Subject: [PATCH] feat: import page permissions --- .../public/locales/en-US/translation.json | 16 ++ .../components/confluence-import-history.tsx | 139 ++++++++++++++++-- .../components/confluence-import-modal.tsx | 22 +-- .../types/confluence-import.types.ts | 12 ++ .../components/page-permission-item.tsx | 9 +- .../space/components/create-space-form.tsx | 4 +- .../space/components/edit-space-form.tsx | 4 +- .../src/core/space/dto/create-space.dto.ts | 6 +- .../server/src/database/types/custom.types.ts | 4 + apps/server/src/ee | 2 +- 10 files changed, 187 insertions(+), 31 deletions(-) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index d59aa94b9..da4c9271a 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -22,6 +22,7 @@ "Can view": "Can view", "Can view pages in space but not edit.": "Can view pages in space but not edit.", "Cancel": "Cancel", + "Cancelled": "Cancelled", "Change email": "Change email", "Change password": "Change password", "Change photo": "Change photo", @@ -29,7 +30,9 @@ "Choose your preferred color scheme.": "Choose your preferred color scheme.", "Choose your preferred interface language.": "Choose your preferred interface language.", "Choose your preferred page width.": "Choose your preferred page width.", + "Completed": "Completed", "Confirm": "Confirm", + "Confluence site": "Confluence site", "Copy as Markdown": "Copy as Markdown", "Copy link": "Copy link", "Create": "Create", @@ -55,6 +58,10 @@ "e.g Space for product team": "e.g Space for product team", "e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate", "Edit": "Edit", + "Everyone with access to this space": "Everyone with access to this space", + "Failed": "Failed", + "Import details": "Import details", + "Permissions": "Permissions", "Read": "Read", "Edit group": "Edit group", "Email": "Email", @@ -130,6 +137,9 @@ "page": "page", "Page deleted successfully": "Page deleted successfully", "Page history": "Page history", + "Restricted pages": "Restricted pages", + "Restrictions": "Restrictions", + "Running": "Running", "Select version": "Select version", "Highlight changes": "Highlight changes", "Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.", @@ -166,6 +176,7 @@ "Setup workspace": "Setup workspace", "Sign In": "Sign In", "Sign Up": "Sign Up", + "Site": "Site", "Slug": "Slug", "Space": "Space", "Space description": "Space description", @@ -174,10 +185,13 @@ "Space settings": "Space settings", "Space slug": "Space slug", "Spaces": "Spaces", + "spaces": "spaces", "Spaces you belong to": "Spaces you belong to", "No space found": "No space found", "Search for spaces": "Search for spaces", "Start typing to search...": "Start typing to search...", + "Started at": "Started at", + "Started by": "Started by", "Status": "Status", "Successfully imported": "Successfully imported", "Successfully restored": "Successfully restored", @@ -191,6 +205,8 @@ "Untitled": "Untitled", "Updated successfully": "Updated successfully", "User": "User", + "Users": "Users", + "users": "users", "Workspace": "Workspace", "Workspace Name": "Workspace Name", "Workspace settings": "Workspace settings", diff --git a/apps/client/src/ee/confluence-import/components/confluence-import-history.tsx b/apps/client/src/ee/confluence-import/components/confluence-import-history.tsx index 2fee21dcd..c207e9ced 100644 --- a/apps/client/src/ee/confluence-import/components/confluence-import-history.tsx +++ b/apps/client/src/ee/confluence-import/components/confluence-import-history.tsx @@ -1,8 +1,9 @@ -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { Badge, Group, Loader, + Modal, Progress, Skeleton, Stack, @@ -26,7 +27,11 @@ const BADGE_STYLES = { label: { overflow: "visible" as const }, }; -function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) { +function statusBadge( + status: ConfluenceImportStatus, + cancelled: boolean, + t: (key: string) => string, +) { if (cancelled) { return ( } styles={BADGE_STYLES} > - Cancelled + {t("Cancelled")} ); } @@ -47,7 +52,7 @@ function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) { leftSection={} styles={BADGE_STYLES} > - Running + {t("Running")} ); } @@ -59,7 +64,7 @@ function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) { leftSection={} styles={BADGE_STYLES} > - Completed + {t("Completed")} ); } @@ -70,14 +75,14 @@ function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) { leftSection={} styles={BADGE_STYLES} > - Failed + {t("Failed")} ); } -function phaseLabel(phase: string | null): string { +function phaseLabel(phase: string | null, t: (key: string) => string): string { if (!phase) return "—"; - return phase.charAt(0).toUpperCase() + phase.slice(1); + return t(phase.charAt(0).toUpperCase() + phase.slice(1)); } function progressValue(item: ConfluenceImportHistoryItem) { @@ -92,6 +97,7 @@ function progressValue(item: ConfluenceImportHistoryItem) { } function ProgressCell({ item }: { item: ConfluenceImportHistoryItem }) { + const { t } = useTranslation(); const value = progressValue(item); const color = item.status === "failed" @@ -105,19 +111,113 @@ function ProgressCell({ item }: { item: ConfluenceImportHistoryItem }) { - {item.importedPages}/{item.totalPages || "?"} pages + {item.importedPages}/{item.totalPages || "?"} {t("pages")} - · {item.importedSpaces}/{item.totalSpaces || "?"} spaces + · {item.importedSpaces}/{item.totalSpaces || "?"} {t("spaces")} - · {item.importedUsers}/{item.totalUsers || "?"} users + · {item.importedUsers}/{item.totalUsers || "?"} {t("users")} ); } +function ImportStatsModal({ + item, + onClose, +}: { + item: ConfluenceImportHistoryItem | null; + onClose: () => void; +}) { + const { t } = useTranslation(); + + const stats = item + ? [ + { + label: t("Spaces"), + imported: item.importedSpaces, + total: item.totalSpaces, + }, + { + label: t("Pages"), + imported: item.importedPages, + total: item.totalPages, + }, + { + label: t("Users"), + imported: item.importedUsers, + total: item.totalUsers, + }, + { + label: t("Groups"), + imported: item.importedGroups, + total: item.totalGroups, + }, + { + label: t("Attachments"), + imported: item.importedAttachments, + total: item.totalAttachments, + }, + { + label: t("Labels"), + imported: item.importedLabels, + total: item.totalLabels, + }, + { + label: t("Restricted pages"), + imported: item.importedRestrictedPages, + total: item.totalRestrictedPages, + }, + ] + : []; + + return ( + + {item && ( + + + + {t("Confluence site")} + + + {item.siteUrl} + + + + + {t("Started at")} + + {formattedDate(new Date(item.createdAt))} + + + + {stats.map((stat) => ( + + + {stat.label} + + + + {stat.imported} / {stat.total} + + + + ))} + + + + )} + + ); +} + function TableSkeleton() { return ( <> @@ -150,6 +250,8 @@ function TableSkeleton() { export default function ConfluenceImportHistory() { const { t } = useTranslation(); const { data, isLoading } = useConfluenceImportsQuery(); + const [selectedItem, setSelectedItem] = + useState(null); const items = useMemo(() => data?.items ?? [], [data]); @@ -172,9 +274,13 @@ export default function ConfluenceImportHistory() { ) : items.length > 0 ? ( items.map((item) => ( - + setSelectedItem(item)} + style={{ cursor: "pointer" }} + > - {statusBadge(item.status, item.cancelled)} + {statusBadge(item.status, item.cancelled, t)} {item.status === "failed" && item.errorMessage && ( @@ -189,7 +295,7 @@ export default function ConfluenceImportHistory() { - {phaseLabel(item.currentPhase)} + {phaseLabel(item.currentPhase, t)} @@ -224,6 +330,11 @@ export default function ConfluenceImportHistory() { )} + + setSelectedItem(null)} + /> ); } diff --git a/apps/client/src/ee/confluence-import/components/confluence-import-modal.tsx b/apps/client/src/ee/confluence-import/components/confluence-import-modal.tsx index 39b3a502a..ea2fa0048 100644 --- a/apps/client/src/ee/confluence-import/components/confluence-import-modal.tsx +++ b/apps/client/src/ee/confluence-import/components/confluence-import-modal.tsx @@ -153,7 +153,9 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) { setImportAll(true); setActive(1); } catch (err: any) { - setError(err?.response?.data?.message || err?.message || t("Unexpected error")); + setError( + err?.response?.data?.message || err?.message || t("Unexpected error"), + ); } finally { setLoading(false); } @@ -161,7 +163,9 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) { const toggleSpace = (key: string, checked: boolean) => { setSelectedKeys((prev) => - checked ? Array.from(new Set([...prev, key])) : prev.filter((k) => k !== key), + checked + ? Array.from(new Set([...prev, key])) + : prev.filter((k) => k !== key), ); }; @@ -349,13 +353,14 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) { )} - + {t("Cancel")} - + {t("Test & continue")} @@ -366,7 +371,7 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) { {t( - "Choose the spaces to import. Users, groups and permissions will be imported for the selected spaces.", + "Pages, comments, page labels, users, groups, spaces and permissions will be imported.", )} @@ -435,7 +440,6 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) { )} - ); } diff --git a/apps/client/src/ee/confluence-import/types/confluence-import.types.ts b/apps/client/src/ee/confluence-import/types/confluence-import.types.ts index d70ab5a3e..fbd3dee40 100644 --- a/apps/client/src/ee/confluence-import/types/confluence-import.types.ts +++ b/apps/client/src/ee/confluence-import/types/confluence-import.types.ts @@ -49,6 +49,10 @@ export type ImportStatusResponse = { importedPages?: number; totalUsers?: number; importedUsers?: number; + totalGroups?: number; + importedGroups?: number; + totalRestrictedPages?: number; + importedRestrictedPages?: number; warnings?: string[]; createdAt?: string; updatedAt?: string; @@ -67,6 +71,14 @@ export type ConfluenceImportHistoryItem = { importedPages: number; totalUsers: number; importedUsers: number; + totalGroups: number; + importedGroups: number; + totalAttachments: number; + importedAttachments: number; + totalLabels: number; + importedLabels: number; + totalRestrictedPages: number; + importedRestrictedPages: number; cancelled: boolean; spaceKeys: string[]; warnings: string[]; diff --git a/apps/client/src/ee/page-permission/components/page-permission-item.tsx b/apps/client/src/ee/page-permission/components/page-permission-item.tsx index b0a5c5f4f..283447012 100644 --- a/apps/client/src/ee/page-permission/components/page-permission-item.tsx +++ b/apps/client/src/ee/page-permission/components/page-permission-item.tsx @@ -53,7 +53,14 @@ export function PagePermissionItem({ {isCurrentUser && ({t("You")})} - {member.type === "user" ? member.email : formatMemberCount(member.memberCount, t)} + {member.type === "user" + ? member.email + : member.isDefault + ? // Page access still requires space membership, so the + // workspace-wide member count would overstate who can + // actually see the page. + t("Everyone with access to this space") + : formatMemberCount(member.memberCount, t)} diff --git a/apps/client/src/features/space/components/create-space-form.tsx b/apps/client/src/features/space/components/create-space-form.tsx index 896b599b3..3d4804704 100644 --- a/apps/client/src/features/space/components/create-space-form.tsx +++ b/apps/client/src/features/space/components/create-space-form.tsx @@ -17,8 +17,8 @@ const formSchema = z.object({ .min(2) .max(100) .regex( - /^[a-zA-Z0-9]+$/, - "Space slug must be alphanumeric. No special characters", + /^[a-zA-Z0-9-]+$/, + "Space slug can only contain letters, numbers, and hyphens", ), description: z.string().max(500), }); diff --git a/apps/client/src/features/space/components/edit-space-form.tsx b/apps/client/src/features/space/components/edit-space-form.tsx index fae8de11d..72aae7565 100644 --- a/apps/client/src/features/space/components/edit-space-form.tsx +++ b/apps/client/src/features/space/components/edit-space-form.tsx @@ -15,8 +15,8 @@ const formSchema = z.object({ .min(2) .max(100) .regex( - /^[a-zA-Z0-9]+$/, - "Space slug must be alphanumeric. No special characters", + /^[a-zA-Z0-9-]+$/, + "Space slug can only contain letters, numbers, and hyphens", ), }); diff --git a/apps/server/src/core/space/dto/create-space.dto.ts b/apps/server/src/core/space/dto/create-space.dto.ts index 310bdcf25..0f3b0cdd1 100644 --- a/apps/server/src/core/space/dto/create-space.dto.ts +++ b/apps/server/src/core/space/dto/create-space.dto.ts @@ -1,7 +1,7 @@ import { - IsAlphanumeric, IsOptional, IsString, + Matches, MaxLength, MinLength, } from 'class-validator'; @@ -20,6 +20,8 @@ export class CreateSpaceDto { @MinLength(2) @MaxLength(100) - @IsAlphanumeric() + @Matches(/^[a-zA-Z0-9-]+$/, { + message: 'slug can only contain letters, numbers, and hyphens', + }) slug: string; } diff --git a/apps/server/src/database/types/custom.types.ts b/apps/server/src/database/types/custom.types.ts index ae717e96a..11b809b01 100644 --- a/apps/server/src/database/types/custom.types.ts +++ b/apps/server/src/database/types/custom.types.ts @@ -18,6 +18,10 @@ export interface ConfluenceApiImports { importedAttachments: Generated; totalLabels: Generated; importedLabels: Generated; + totalGroups: Generated; + importedGroups: Generated; + totalRestrictedPages: Generated; + importedRestrictedPages: Generated; idMapping: Generated; warnings: Generated; currentPhase: string | null; diff --git a/apps/server/src/ee b/apps/server/src/ee index c2d2c373c..72a034245 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit c2d2c373c6c464cdc02e8c59f07e71a9cd8a2a56 +Subproject commit 72a03424525db60c672aed8532e1823759004f16