mirror of
https://github.com/docmost/docmost.git
synced 2026-05-17 23:14:07 +08:00
bd68e47e03
* feat: page verification workflow * feat: refactor page-verification * sync * fix type * fix * fix * notification icon * use full word * accept .license file * - update templates - update migration and notification * fix copy * update audit labels * sync * add space name
219 lines
7.1 KiB
TypeScript
219 lines
7.1 KiB
TypeScript
import {
|
|
Table,
|
|
Text,
|
|
Group,
|
|
Skeleton,
|
|
Anchor,
|
|
Badge,
|
|
Avatar,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import { Link } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
IVerificationListItem,
|
|
VerificationStatus,
|
|
} from "@/ee/page-verification/types/page-verification.types";
|
|
import { CustomAvatar } from "@/components/ui/custom-avatar";
|
|
import { buildPageUrl } from "@/features/page/page.utils";
|
|
import { format } from "date-fns";
|
|
import NoTableResults from "@/components/common/no-table-results";
|
|
|
|
const MAX_VISIBLE_VERIFIERS = 5;
|
|
|
|
type VerificationListTableProps = {
|
|
items?: IVerificationListItem[];
|
|
isLoading: boolean;
|
|
};
|
|
|
|
function statusBadge(status: VerificationStatus | null, t: (s: string) => string) {
|
|
switch (status) {
|
|
case "verified":
|
|
return <Badge color="green" variant="light" size="sm">{t("Verified")}</Badge>;
|
|
case "expiring":
|
|
return <Badge color="orange" variant="light" size="sm">{t("Expiring")}</Badge>;
|
|
case "expired":
|
|
return <Badge color="red" variant="light" size="sm">{t("Expired")}</Badge>;
|
|
case "approved":
|
|
return <Badge color="green" variant="light" size="sm">{t("Approved")}</Badge>;
|
|
case "draft":
|
|
return <Badge color="gray" variant="light" size="sm">{t("Draft")}</Badge>;
|
|
case "in_approval":
|
|
return <Badge color="blue" variant="light" size="sm">{t("In approval")}</Badge>;
|
|
case "obsolete":
|
|
return <Badge color="red" variant="light" size="sm">{t("Obsolete")}</Badge>;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function verifiedUntilText(item: IVerificationListItem, t: (s: string) => string): string {
|
|
if (item.type === "qms") {
|
|
if (item.status === "approved") return t("Indefinitely");
|
|
return "—";
|
|
}
|
|
|
|
if (!item.expiresAt) return t("Indefinitely");
|
|
|
|
const expires = new Date(item.expiresAt);
|
|
const now = new Date();
|
|
|
|
if (expires <= now) return t("Expired");
|
|
return format(expires, "MMM d, yyyy");
|
|
}
|
|
|
|
function TableSkeleton() {
|
|
return (
|
|
<>
|
|
{Array.from({ length: 8 }).map((_, i) => (
|
|
<Table.Tr key={i}>
|
|
<Table.Td>
|
|
<div>
|
|
<Skeleton height={14} width={160} mb={4} />
|
|
<Skeleton height={10} width={100} />
|
|
</div>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Group gap={8} wrap="nowrap">
|
|
<Skeleton circle height={24} />
|
|
<Skeleton circle height={24} />
|
|
<Skeleton circle height={24} />
|
|
</Group>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Skeleton height={14} width={100} />
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Skeleton height={20} width={60} />
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function VerificationListTable({
|
|
items,
|
|
isLoading,
|
|
}: VerificationListTableProps) {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<Table.ScrollContainer minWidth={600}>
|
|
<Table highlightOnHover verticalSpacing="xs">
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>{t("Page")}</Table.Th>
|
|
<Table.Th>{t("Verifiers")}</Table.Th>
|
|
<Table.Th>{t("Verified until")}</Table.Th>
|
|
<Table.Th>{t("Status")}</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
|
|
<Table.Tbody>
|
|
{isLoading ? (
|
|
<TableSkeleton />
|
|
) : items && items.length > 0 ? (
|
|
items.map((item) => {
|
|
const verifiers = item.verifiers ?? [];
|
|
|
|
const pageUrl = buildPageUrl(
|
|
item.spaceSlug,
|
|
item.pageSlugId,
|
|
item.pageTitle ?? undefined,
|
|
);
|
|
|
|
return (
|
|
<Table.Tr key={item.id}>
|
|
<Table.Td>
|
|
<Anchor
|
|
size="sm"
|
|
underline="never"
|
|
style={{ color: "var(--mantine-color-text)" }}
|
|
component={Link}
|
|
to={pageUrl}
|
|
>
|
|
<Text fz="sm" fw={500} lineClamp={1}>
|
|
{item.pageIcon ? `${item.pageIcon} ` : ""}
|
|
{item.pageTitle || t("Untitled")}
|
|
</Text>
|
|
</Anchor>
|
|
<Text fz="xs" c="dimmed" lineClamp={1}>
|
|
{item.spaceName}
|
|
</Text>
|
|
</Table.Td>
|
|
|
|
<Table.Td>
|
|
{verifiers.length === 1 ? (
|
|
<Group gap={8} wrap="nowrap">
|
|
<CustomAvatar
|
|
size="sm"
|
|
avatarUrl={verifiers[0].avatarUrl}
|
|
name={verifiers[0].name}
|
|
/>
|
|
<Text fz="sm" lineClamp={1}>
|
|
{verifiers[0].name}
|
|
</Text>
|
|
</Group>
|
|
) : verifiers.length > 1 ? (
|
|
<Tooltip.Group openDelay={300} closeDelay={100}>
|
|
<Avatar.Group spacing={8}>
|
|
{verifiers
|
|
.slice(0, MAX_VISIBLE_VERIFIERS)
|
|
.map((verifier) => (
|
|
<Tooltip
|
|
key={verifier.id}
|
|
label={verifier.name}
|
|
withArrow
|
|
>
|
|
<CustomAvatar
|
|
size="sm"
|
|
avatarUrl={verifier.avatarUrl}
|
|
name={verifier.name}
|
|
/>
|
|
</Tooltip>
|
|
))}
|
|
{verifiers.length > MAX_VISIBLE_VERIFIERS && (
|
|
<Tooltip
|
|
withArrow
|
|
label={verifiers
|
|
.slice(MAX_VISIBLE_VERIFIERS)
|
|
.map((v) => (
|
|
<div key={v.id}>{v.name}</div>
|
|
))}
|
|
>
|
|
<Avatar size="sm" color="gray">
|
|
+{verifiers.length - MAX_VISIBLE_VERIFIERS}
|
|
</Avatar>
|
|
</Tooltip>
|
|
)}
|
|
</Avatar.Group>
|
|
</Tooltip.Group>
|
|
) : (
|
|
<Text fz="sm" c="dimmed">
|
|
—
|
|
</Text>
|
|
)}
|
|
</Table.Td>
|
|
|
|
<Table.Td>
|
|
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
|
|
{verifiedUntilText(item, t)}
|
|
</Text>
|
|
</Table.Td>
|
|
|
|
<Table.Td>
|
|
{statusBadge(item.status as VerificationStatus, t)}
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
);
|
|
})
|
|
) : (
|
|
<NoTableResults colSpan={4} />
|
|
)}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Table.ScrollContainer>
|
|
);
|
|
}
|