feat: refactor page-verification

This commit is contained in:
Philipinho
2026-04-11 23:54:36 +01:00
parent 77f0aa6483
commit 1bd63101d6
20 changed files with 504 additions and 118 deletions
@@ -761,6 +761,9 @@
"Set up verification": "Set up verification",
"Verify page": "Verify page",
"Page verification": "Page verification",
"Add verification": "Add verification",
"Edit verification": "Edit verification",
"Search by title": "Search by title",
"Choose how this page should stay accurate.": "Choose how this page should stay accurate.",
"Recurring verification": "Recurring verification",
"Verifiers re-confirm this page on a schedule.": "Verifiers re-confirm this page on a schedule.",
@@ -97,6 +97,12 @@ const groupedData: DataGroup[] = [
{ label: "Groups", icon: IconUsersGroup, path: "/settings/groups" },
{ label: "Spaces", icon: IconSpaces, path: "/settings/spaces" },
{ label: "Public sharing", icon: IconWorld, path: "/settings/sharing" },
{
label: "Verified pages",
icon: IconShieldCheck,
path: "/settings/verifications",
feature: Feature.PAGE_VERIFICATION,
},
{
label: "API management",
icon: IconKey,
@@ -118,13 +124,6 @@ const groupedData: DataGroup[] = [
role: "owner",
env: "selfhosted",
},
{
label: "Verified pages",
icon: IconShieldCheck,
path: "/settings/verifications",
feature: Feature.PAGE_VERIFICATION,
role: "admin",
},
],
},
{
@@ -521,6 +521,7 @@ function QmsManageContent({ pageId, info, onClose }: ManageContentProps) {
placeholder={t("Reason for returning this document...")}
minRows={2}
variant="filled"
maxLength={500}
/>
<Group justify="flex-end" mt="sm" gap="xs">
<Button
@@ -1,11 +1,4 @@
import {
ActionIcon,
Group,
Menu,
Modal,
Text,
Tooltip,
} from "@mantine/core";
import { ActionIcon, Group, Menu, Modal, Text, Tooltip } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import {
IconRosetteDiscountCheckFilled,
@@ -17,6 +10,7 @@ import { extractPageSlugId } from "@/lib";
import { usePageQuery } from "@/features/page/queries/page-query";
import { usePageVerificationInfoQuery } from "@/ee/page-verification/queries/page-verification-query";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
import { Feature } from "@/ee/features";
import { SetupVerificationForm } from "./setup-verification-form";
import { ManageVerificationForm } from "./manage-verification-form";
@@ -83,17 +77,32 @@ export function PageVerificationBadge({
const { t } = useTranslation();
const { pageSlug } = useParams();
const pageSlugId = extractPageSlugId(pageSlug);
const isCloudEE = useHasFeature(Feature.PAGE_VERIFICATION);
const hasVerificationFeature = useHasFeature(Feature.PAGE_VERIFICATION);
const [opened, { open, close }] = useDisclosure(false);
const { data: page } = usePageQuery({ pageId: pageSlugId });
const pageId = page?.id;
const { data: verificationInfo, isLoading } = usePageVerificationInfoQuery(
isCloudEE ? pageId : undefined,
hasVerificationFeature ? pageId : undefined,
);
const upgradeLabel = useUpgradeLabel();
if (!isCloudEE || !pageId) return null;
if (!pageId) return null;
if (!hasVerificationFeature) {
if (readOnly) return null;
return (
<Tooltip
label={`${t("Page verification")}${upgradeLabel}`}
withArrow
openDelay={250}
>
<ActionIcon variant="subtle" color="gray">
<IconShieldCheck size={20} stroke={1.5} />
</ActionIcon>
</Tooltip>
);
}
if (isLoading) return null;
const status = verificationInfo?.status ?? "none";
@@ -127,30 +136,51 @@ export function PageVerificationBadge({
</Tooltip>
) : null}
<PageVerificationModal
pageId={pageId}
opened={opened}
onClose={close}
/>
<PageVerificationModal pageId={pageId} opened={opened} onClose={close} />
</>
);
}
type PageVerificationMenuItemProps = {
pageId?: string;
onClick: () => void;
};
export function PageVerificationMenuItem({
pageId,
onClick,
}: PageVerificationMenuItemProps) {
const { t } = useTranslation();
const isCloudEE = useHasFeature(Feature.PAGE_VERIFICATION);
const hasVerificationFeature = useHasFeature(Feature.PAGE_VERIFICATION);
const upgradeLabel = useUpgradeLabel();
if (!isCloudEE) return null;
const { data: verificationInfo } = usePageVerificationInfoQuery(
hasVerificationFeature ? pageId : undefined,
);
return (
<Menu.Item leftSection={<IconShieldCheck size={16} />} onClick={onClick}>
{t("Page verification")}
const hasVerification =
!!verificationInfo && verificationInfo.status !== "none";
const label = hasVerification
? t("Edit verification")
: t("Add verification");
const menuItem = (
<Menu.Item
disabled={!hasVerificationFeature}
leftSection={<IconShieldCheck size={16} />}
onClick={hasVerificationFeature ? onClick : undefined}
>
{label}
</Menu.Item>
);
if (!hasVerificationFeature) {
return (
<Tooltip label={upgradeLabel} position="left" withinPortal={false}>
{menuItem}
</Tooltip>
);
}
return menuItem;
}
@@ -133,6 +133,7 @@ type SetupVerificationFormProps = {
export function SetupVerificationForm({
pageId,
onClose,
}: SetupVerificationFormProps) {
const { t } = useTranslation();
const setupMutation = useSetupVerificationMutation();
@@ -182,22 +183,31 @@ export function SetupVerificationForm({
const handleSetup = () => {
if (selectedVerifiers.length === 0) return;
setupMutation.mutate({
pageId,
type,
...(!isQms && {
mode,
...(mode === "period" && {
periodAmount,
periodUnit,
}),
...(mode === "fixed" &&
fixedDate && {
fixedExpiresAt: new Date(fixedDate).toISOString(),
setupMutation.mutate(
{
pageId,
type,
...(!isQms && {
mode,
...(mode === "period" && {
periodAmount,
periodUnit,
}),
}),
verifierIds: selectedVerifiers.map((v) => v.value),
});
...(mode === "fixed" &&
fixedDate && {
fixedExpiresAt: new Date(fixedDate).toISOString(),
}),
}),
verifierIds: selectedVerifiers.map((v) => v.value),
},
{
onSuccess: () => {
if (!isQms) {
onClose();
}
},
},
);
};
const periodValid =
@@ -1,4 +1,13 @@
import { Table, Text, Group, Skeleton, Anchor, Badge } from "@mantine/core";
import {
Table,
Text,
Group,
Skeleton,
Anchor,
Badge,
Avatar,
Tooltip,
} from "@mantine/core";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
@@ -10,6 +19,8 @@ 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;
@@ -20,7 +31,7 @@ function statusBadge(status: VerificationStatus | null, t: (s: string) => string
case "verified":
return <Badge color="green" variant="light" size="sm">{t("Verified")}</Badge>;
case "expiring":
return <Badge color="yellow" variant="light" size="sm">{t("Expiring")}</Badge>;
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":
@@ -63,9 +74,10 @@ function TableSkeleton() {
</div>
</Table.Td>
<Table.Td>
<Group gap="sm" wrap="nowrap">
<Skeleton circle height={28} />
<Skeleton height={14} width={80} />
<Group gap={8} wrap="nowrap">
<Skeleton circle height={24} />
<Skeleton circle height={24} />
<Skeleton circle height={24} />
</Group>
</Table.Td>
<Table.Td>
@@ -92,7 +104,7 @@ export default function VerificationListTable({
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Page")}</Table.Th>
<Table.Th>{t("Owner")}</Table.Th>
<Table.Th>{t("Verifiers")}</Table.Th>
<Table.Th>{t("Verified until")}</Table.Th>
<Table.Th>{t("Status")}</Table.Th>
</Table.Tr>
@@ -103,7 +115,7 @@ export default function VerificationListTable({
<TableSkeleton />
) : items && items.length > 0 ? (
items.map((item) => {
const primaryVerifier = item.verifiers[0];
const verifiers = item.verifiers ?? [];
const pageUrl = buildPageUrl(
item.spaceSlug,
@@ -132,19 +144,55 @@ export default function VerificationListTable({
</Table.Td>
<Table.Td>
{primaryVerifier ? (
<Group gap="sm" wrap="nowrap">
{verifiers.length === 1 ? (
<Group gap={8} wrap="nowrap">
<CustomAvatar
avatarUrl={primaryVerifier.avatarUrl}
name={primaryVerifier.name}
size={28}
size="sm"
avatarUrl={verifiers[0].avatarUrl}
name={verifiers[0].name}
/>
<Text fz="sm" lineClamp={1}>
{primaryVerifier.name}
{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>
<Text fz="sm" c="dimmed">
</Text>
)}
</Table.Td>
@@ -12,11 +12,9 @@ import { useVerificationListQuery } from "@/ee/page-verification/queries/page-ve
import { IVerificationListParams } from "@/ee/page-verification/types/page-verification.types";
import VerificationListTable from "@/ee/page-verification/components/verification-list-table";
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
import useUserRole from "@/hooks/use-user-role";
export default function VerifiedPages() {
const { t } = useTranslation();
const { isAdmin } = useUserRole();
const { cursor, goNext, goPrev, resetCursor } = useCursorPaginate();
const [searchValue, setSearchValue] = useState("");
@@ -53,10 +51,6 @@ export default function VerifiedPages() {
const { data, isLoading } = useVerificationListQuery(params);
if (!isAdmin) {
return null;
}
const handleSpaceChange = (value: string[]) => {
setSpaceFilter(value);
resetCursor();
@@ -84,7 +78,7 @@ export default function VerifiedPages() {
<Group mb="md" gap="sm">
<TextInput
placeholder={t("Search pages...")}
placeholder={t("Search by title")}
leftSection={<IconSearch size={16} />}
value={searchValue}
onChange={handleSearchChange}
@@ -92,6 +86,7 @@ export default function VerifiedPages() {
w={220}
/>
{/*
<MultiSelect
placeholder={t("Filter by space")}
data={spaceOptions}
@@ -112,6 +107,7 @@ export default function VerifiedPages() {
w={160}
size="sm"
/>
*/}
</Group>
<VerificationListTable items={data?.items} isLoading={isLoading} />
@@ -12,7 +12,7 @@ export async function getVerificationInfo(
pageId: string,
): Promise<IPageVerificationInfo> {
const req = await api.post<IPageVerificationInfo>(
"/page-verification/info",
"/pages/verification-info",
{ pageId },
);
return req.data;
@@ -21,41 +21,41 @@ export async function getVerificationInfo(
export async function setupVerification(
data: ISetupVerification,
): Promise<void> {
await api.post("/page-verification/setup", data);
await api.post("/pages/create-verification", data);
}
export async function updateVerification(
data: IUpdateVerification,
): Promise<void> {
await api.post("/page-verification/update", data);
await api.post("/pages/update-verification", data);
}
export async function removeVerification(pageId: string): Promise<void> {
await api.post("/page-verification/remove", { pageId });
await api.post("/pages/delete-verification", { pageId });
}
export async function verifyPage(pageId: string): Promise<void> {
await api.post("/page-verification/verify", { pageId });
await api.post("/pages/verify", { pageId });
}
export async function submitForApproval(pageId: string): Promise<void> {
await api.post("/page-verification/submit-for-approval", { pageId });
await api.post("/pages/submit-for-approval", { pageId });
}
export async function rejectApproval(data: {
pageId: string;
comment?: string;
}): Promise<void> {
await api.post("/page-verification/reject", data);
await api.post("/pages/reject-approval", data);
}
export async function markObsolete(pageId: string): Promise<void> {
await api.post("/page-verification/obsolete", { pageId });
await api.post("/pages/mark-obsolete", { pageId });
}
export async function getVerificationList(
params?: IVerificationListParams,
): Promise<IPagination<IVerificationListItem>> {
const req = await api.post("/page-verification/list", { ...params });
const req = await api.post("/pages/verifications", { ...params });
return req.data;
}
@@ -236,7 +236,10 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
</Menu.Item>
{!readOnly && (
<PageVerificationMenuItem onClick={openVerificationModal} />
<PageVerificationMenuItem
pageId={page?.id}
onClick={openVerificationModal}
/>
)}
<Menu.Divider />