mirror of
https://github.com/docmost/docmost.git
synced 2026-07-26 03:04:51 +10:00
feat: import page permissions
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
"Can view": "Can view",
|
"Can view": "Can view",
|
||||||
"Can view pages in space but not edit.": "Can view pages in space but not edit.",
|
"Can view pages in space but not edit.": "Can view pages in space but not edit.",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
|
"Cancelled": "Cancelled",
|
||||||
"Change email": "Change email",
|
"Change email": "Change email",
|
||||||
"Change password": "Change password",
|
"Change password": "Change password",
|
||||||
"Change photo": "Change photo",
|
"Change photo": "Change photo",
|
||||||
@@ -29,7 +30,9 @@
|
|||||||
"Choose your preferred color scheme.": "Choose your preferred color scheme.",
|
"Choose your preferred color scheme.": "Choose your preferred color scheme.",
|
||||||
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
||||||
"Choose your preferred page width.": "Choose your preferred page width.",
|
"Choose your preferred page width.": "Choose your preferred page width.",
|
||||||
|
"Completed": "Completed",
|
||||||
"Confirm": "Confirm",
|
"Confirm": "Confirm",
|
||||||
|
"Confluence site": "Confluence site",
|
||||||
"Copy as Markdown": "Copy as Markdown",
|
"Copy as Markdown": "Copy as Markdown",
|
||||||
"Copy link": "Copy link",
|
"Copy link": "Copy link",
|
||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
@@ -55,6 +58,10 @@
|
|||||||
"e.g Space for product team": "e.g Space for product team",
|
"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",
|
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
|
"Everyone with access to this space": "Everyone with access to this space",
|
||||||
|
"Failed": "Failed",
|
||||||
|
"Import details": "Import details",
|
||||||
|
"Permissions": "Permissions",
|
||||||
"Read": "Read",
|
"Read": "Read",
|
||||||
"Edit group": "Edit group",
|
"Edit group": "Edit group",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
@@ -130,6 +137,9 @@
|
|||||||
"page": "page",
|
"page": "page",
|
||||||
"Page deleted successfully": "Page deleted successfully",
|
"Page deleted successfully": "Page deleted successfully",
|
||||||
"Page history": "Page history",
|
"Page history": "Page history",
|
||||||
|
"Restricted pages": "Restricted pages",
|
||||||
|
"Restrictions": "Restrictions",
|
||||||
|
"Running": "Running",
|
||||||
"Select version": "Select version",
|
"Select version": "Select version",
|
||||||
"Highlight changes": "Highlight changes",
|
"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.",
|
"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",
|
"Setup workspace": "Setup workspace",
|
||||||
"Sign In": "Sign In",
|
"Sign In": "Sign In",
|
||||||
"Sign Up": "Sign Up",
|
"Sign Up": "Sign Up",
|
||||||
|
"Site": "Site",
|
||||||
"Slug": "Slug",
|
"Slug": "Slug",
|
||||||
"Space": "Space",
|
"Space": "Space",
|
||||||
"Space description": "Space description",
|
"Space description": "Space description",
|
||||||
@@ -174,10 +185,13 @@
|
|||||||
"Space settings": "Space settings",
|
"Space settings": "Space settings",
|
||||||
"Space slug": "Space slug",
|
"Space slug": "Space slug",
|
||||||
"Spaces": "Spaces",
|
"Spaces": "Spaces",
|
||||||
|
"spaces": "spaces",
|
||||||
"Spaces you belong to": "Spaces you belong to",
|
"Spaces you belong to": "Spaces you belong to",
|
||||||
"No space found": "No space found",
|
"No space found": "No space found",
|
||||||
"Search for spaces": "Search for spaces",
|
"Search for spaces": "Search for spaces",
|
||||||
"Start typing to search...": "Start typing to search...",
|
"Start typing to search...": "Start typing to search...",
|
||||||
|
"Started at": "Started at",
|
||||||
|
"Started by": "Started by",
|
||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
"Successfully imported": "Successfully imported",
|
"Successfully imported": "Successfully imported",
|
||||||
"Successfully restored": "Successfully restored",
|
"Successfully restored": "Successfully restored",
|
||||||
@@ -191,6 +205,8 @@
|
|||||||
"Untitled": "Untitled",
|
"Untitled": "Untitled",
|
||||||
"Updated successfully": "Updated successfully",
|
"Updated successfully": "Updated successfully",
|
||||||
"User": "User",
|
"User": "User",
|
||||||
|
"Users": "Users",
|
||||||
|
"users": "users",
|
||||||
"Workspace": "Workspace",
|
"Workspace": "Workspace",
|
||||||
"Workspace Name": "Workspace Name",
|
"Workspace Name": "Workspace Name",
|
||||||
"Workspace settings": "Workspace settings",
|
"Workspace settings": "Workspace settings",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
|
Modal,
|
||||||
Progress,
|
Progress,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -26,7 +27,11 @@ const BADGE_STYLES = {
|
|||||||
label: { overflow: "visible" as const },
|
label: { overflow: "visible" as const },
|
||||||
};
|
};
|
||||||
|
|
||||||
function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) {
|
function statusBadge(
|
||||||
|
status: ConfluenceImportStatus,
|
||||||
|
cancelled: boolean,
|
||||||
|
t: (key: string) => string,
|
||||||
|
) {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -35,7 +40,7 @@ function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) {
|
|||||||
leftSection={<IconX size={12} />}
|
leftSection={<IconX size={12} />}
|
||||||
styles={BADGE_STYLES}
|
styles={BADGE_STYLES}
|
||||||
>
|
>
|
||||||
Cancelled
|
{t("Cancelled")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -47,7 +52,7 @@ function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) {
|
|||||||
leftSection={<Loader size={10} />}
|
leftSection={<Loader size={10} />}
|
||||||
styles={BADGE_STYLES}
|
styles={BADGE_STYLES}
|
||||||
>
|
>
|
||||||
Running
|
{t("Running")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,7 +64,7 @@ function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) {
|
|||||||
leftSection={<IconCheck size={12} />}
|
leftSection={<IconCheck size={12} />}
|
||||||
styles={BADGE_STYLES}
|
styles={BADGE_STYLES}
|
||||||
>
|
>
|
||||||
Completed
|
{t("Completed")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,14 +75,14 @@ function statusBadge(status: ConfluenceImportStatus, cancelled: boolean) {
|
|||||||
leftSection={<IconAlertCircle size={12} />}
|
leftSection={<IconAlertCircle size={12} />}
|
||||||
styles={BADGE_STYLES}
|
styles={BADGE_STYLES}
|
||||||
>
|
>
|
||||||
Failed
|
{t("Failed")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function phaseLabel(phase: string | null): string {
|
function phaseLabel(phase: string | null, t: (key: string) => string): string {
|
||||||
if (!phase) return "—";
|
if (!phase) return "—";
|
||||||
return phase.charAt(0).toUpperCase() + phase.slice(1);
|
return t(phase.charAt(0).toUpperCase() + phase.slice(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
function progressValue(item: ConfluenceImportHistoryItem) {
|
function progressValue(item: ConfluenceImportHistoryItem) {
|
||||||
@@ -92,6 +97,7 @@ function progressValue(item: ConfluenceImportHistoryItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ProgressCell({ item }: { item: ConfluenceImportHistoryItem }) {
|
function ProgressCell({ item }: { item: ConfluenceImportHistoryItem }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const value = progressValue(item);
|
const value = progressValue(item);
|
||||||
const color =
|
const color =
|
||||||
item.status === "failed"
|
item.status === "failed"
|
||||||
@@ -105,19 +111,113 @@ function ProgressCell({ item }: { item: ConfluenceImportHistoryItem }) {
|
|||||||
<Progress value={value} color={color} size="xs" animated={item.status === "processing"} />
|
<Progress value={value} color={color} size="xs" animated={item.status === "processing"} />
|
||||||
<Group gap="xs" wrap="nowrap">
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Text fz="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
<Text fz="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
||||||
{item.importedPages}/{item.totalPages || "?"} pages
|
{item.importedPages}/{item.totalPages || "?"} {t("pages")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
<Text fz="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
||||||
· {item.importedSpaces}/{item.totalSpaces || "?"} spaces
|
· {item.importedSpaces}/{item.totalSpaces || "?"} {t("spaces")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
<Text fz="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
||||||
· {item.importedUsers}/{item.totalUsers || "?"} users
|
· {item.importedUsers}/{item.totalUsers || "?"} {t("users")}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Modal
|
||||||
|
opened={!!item}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t("Import details")}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
{item && (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<div>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{t("Confluence site")}
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
|
{item.siteUrl}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{t("Started at")}
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm">{formattedDate(new Date(item.createdAt))}</Text>
|
||||||
|
</div>
|
||||||
|
<Table verticalSpacing="xs" fz="sm">
|
||||||
|
<Table.Tbody>
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<Table.Tr key={stat.label}>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fz="sm">{stat.label}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fz="sm" ta="right">
|
||||||
|
{stat.imported} / {stat.total}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TableSkeleton() {
|
function TableSkeleton() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -150,6 +250,8 @@ function TableSkeleton() {
|
|||||||
export default function ConfluenceImportHistory() {
|
export default function ConfluenceImportHistory() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, isLoading } = useConfluenceImportsQuery();
|
const { data, isLoading } = useConfluenceImportsQuery();
|
||||||
|
const [selectedItem, setSelectedItem] =
|
||||||
|
useState<ConfluenceImportHistoryItem | null>(null);
|
||||||
|
|
||||||
const items = useMemo(() => data?.items ?? [], [data]);
|
const items = useMemo(() => data?.items ?? [], [data]);
|
||||||
|
|
||||||
@@ -172,9 +274,13 @@ export default function ConfluenceImportHistory() {
|
|||||||
<TableSkeleton />
|
<TableSkeleton />
|
||||||
) : items.length > 0 ? (
|
) : items.length > 0 ? (
|
||||||
items.map((item) => (
|
items.map((item) => (
|
||||||
<Table.Tr key={item.fileTaskId}>
|
<Table.Tr
|
||||||
|
key={item.fileTaskId}
|
||||||
|
onClick={() => setSelectedItem(item)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{statusBadge(item.status, item.cancelled)}
|
{statusBadge(item.status, item.cancelled, t)}
|
||||||
{item.status === "failed" && item.errorMessage && (
|
{item.status === "failed" && item.errorMessage && (
|
||||||
<Tooltip label={item.errorMessage} multiline w={320}>
|
<Tooltip label={item.errorMessage} multiline w={320}>
|
||||||
<Text fz="xs" c="red" lineClamp={1} maw={180}>
|
<Text fz="xs" c="red" lineClamp={1} maw={180}>
|
||||||
@@ -189,7 +295,7 @@ export default function ConfluenceImportHistory() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text fz="sm">{phaseLabel(item.currentPhase)}</Text>
|
<Text fz="sm">{phaseLabel(item.currentPhase, t)}</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<ProgressCell item={item} />
|
<ProgressCell item={item} />
|
||||||
@@ -224,6 +330,11 @@ export default function ConfluenceImportHistory() {
|
|||||||
)}
|
)}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<ImportStatsModal
|
||||||
|
item={selectedItem}
|
||||||
|
onClose={() => setSelectedItem(null)}
|
||||||
|
/>
|
||||||
</Table.ScrollContainer>
|
</Table.ScrollContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,9 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) {
|
|||||||
setImportAll(true);
|
setImportAll(true);
|
||||||
setActive(1);
|
setActive(1);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.response?.data?.message || err?.message || t("Unexpected error"));
|
setError(
|
||||||
|
err?.response?.data?.message || err?.message || t("Unexpected error"),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -161,7 +163,9 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) {
|
|||||||
|
|
||||||
const toggleSpace = (key: string, checked: boolean) => {
|
const toggleSpace = (key: string, checked: boolean) => {
|
||||||
setSelectedKeys((prev) =>
|
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) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button variant="default" onClick={handleCancelFlow} disabled={loading}>
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleCancelFlow}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
{t("Cancel")}
|
{t("Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={handleNextFromCredentials} loading={loading}>
|
||||||
onClick={handleNextFromCredentials}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
{t("Test & continue")}
|
{t("Test & continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -366,7 +371,7 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{t(
|
{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.",
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -435,7 +440,6 @@ export default function ConfluenceImportModal({ opened, onClose }: Props) {
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export type ImportStatusResponse = {
|
|||||||
importedPages?: number;
|
importedPages?: number;
|
||||||
totalUsers?: number;
|
totalUsers?: number;
|
||||||
importedUsers?: number;
|
importedUsers?: number;
|
||||||
|
totalGroups?: number;
|
||||||
|
importedGroups?: number;
|
||||||
|
totalRestrictedPages?: number;
|
||||||
|
importedRestrictedPages?: number;
|
||||||
warnings?: string[];
|
warnings?: string[];
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
@@ -67,6 +71,14 @@ export type ConfluenceImportHistoryItem = {
|
|||||||
importedPages: number;
|
importedPages: number;
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
importedUsers: number;
|
importedUsers: number;
|
||||||
|
totalGroups: number;
|
||||||
|
importedGroups: number;
|
||||||
|
totalAttachments: number;
|
||||||
|
importedAttachments: number;
|
||||||
|
totalLabels: number;
|
||||||
|
importedLabels: number;
|
||||||
|
totalRestrictedPages: number;
|
||||||
|
importedRestrictedPages: number;
|
||||||
cancelled: boolean;
|
cancelled: boolean;
|
||||||
spaceKeys: string[];
|
spaceKeys: string[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
|
|||||||
@@ -53,7 +53,14 @@ export function PagePermissionItem({
|
|||||||
{isCurrentUser && <Text span c="dimmed"> ({t("You")})</Text>}
|
{isCurrentUser && <Text span c="dimmed"> ({t("You")})</Text>}
|
||||||
</AutoTooltipText>
|
</AutoTooltipText>
|
||||||
<AutoTooltipText fz="xs" c="dimmed">
|
<AutoTooltipText fz="xs" c="dimmed">
|
||||||
{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)}
|
||||||
</AutoTooltipText>
|
</AutoTooltipText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ const formSchema = z.object({
|
|||||||
.min(2)
|
.min(2)
|
||||||
.max(100)
|
.max(100)
|
||||||
.regex(
|
.regex(
|
||||||
/^[a-zA-Z0-9]+$/,
|
/^[a-zA-Z0-9-]+$/,
|
||||||
"Space slug must be alphanumeric. No special characters",
|
"Space slug can only contain letters, numbers, and hyphens",
|
||||||
),
|
),
|
||||||
description: z.string().max(500),
|
description: z.string().max(500),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const formSchema = z.object({
|
|||||||
.min(2)
|
.min(2)
|
||||||
.max(100)
|
.max(100)
|
||||||
.regex(
|
.regex(
|
||||||
/^[a-zA-Z0-9]+$/,
|
/^[a-zA-Z0-9-]+$/,
|
||||||
"Space slug must be alphanumeric. No special characters",
|
"Space slug can only contain letters, numbers, and hyphens",
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
IsAlphanumeric,
|
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
|
Matches,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
MinLength,
|
MinLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@@ -20,6 +20,8 @@ export class CreateSpaceDto {
|
|||||||
|
|
||||||
@MinLength(2)
|
@MinLength(2)
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
@IsAlphanumeric()
|
@Matches(/^[a-zA-Z0-9-]+$/, {
|
||||||
|
message: 'slug can only contain letters, numbers, and hyphens',
|
||||||
|
})
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export interface ConfluenceApiImports {
|
|||||||
importedAttachments: Generated<number>;
|
importedAttachments: Generated<number>;
|
||||||
totalLabels: Generated<number>;
|
totalLabels: Generated<number>;
|
||||||
importedLabels: Generated<number>;
|
importedLabels: Generated<number>;
|
||||||
|
totalGroups: Generated<number>;
|
||||||
|
importedGroups: Generated<number>;
|
||||||
|
totalRestrictedPages: Generated<number>;
|
||||||
|
importedRestrictedPages: Generated<number>;
|
||||||
idMapping: Generated<Json>;
|
idMapping: Generated<Json>;
|
||||||
warnings: Generated<Json>;
|
warnings: Generated<Json>;
|
||||||
currentPhase: string | null;
|
currentPhase: string | null;
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: c2d2c373c6...72a0342452
Reference in New Issue
Block a user