Add version selector on document

Fix #166

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Jonathan
2025-07-08 14:59:26 +02:00
committed by Sacha Al Himdani
parent a4606f95f5
commit f114f3bc4a
14 changed files with 412 additions and 722 deletions

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5c3e7dba407e37cfc0203138d5db8318>>
* @generated SignedSource<<85c6bcb5e086b05009ccfaf8ac21a23e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -387,6 +387,19 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": [
(v4/*: any*/),
(v3/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v11/*: any*/),
@@ -475,26 +488,6 @@ return {
"kind": "LinkedHandle",
"name": "signatures"
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "changelog",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": [
(v4/*: any*/),
(v3/*: any*/)
],
"storageKey": null
},
(v2/*: any*/)
],
"storageKey": null
@@ -527,12 +520,12 @@ return {
]
},
"params": {
"cacheID": "ad8c2b55a545807ab1fe72c21ed8f9c1",
"cacheID": "fbfe2cc393b8fc5349a92f3443f12e0e",
"id": null,
"metadata": {},
"name": "DocumentGraphNodeQuery",
"operationKind": "query",
"text": "query DocumentGraphNodeQuery(\n $documentId: ID!\n) {\n node(id: $documentId) {\n __typename\n ... on Document {\n ...DocumentDetailPageDocumentFragment\n }\n id\n }\n}\n\nfragment DocumentControlsTabFragment on Document {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment DocumentDetailPageDocumentFragment on Document {\n id\n title\n documentType\n owner {\n id\n fullName\n }\n ...DocumentControlsTabFragment\n controlsInfo: controls(first: 0) {\n totalCount\n }\n versions(first: 20) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n signatures(first: 100) {\n edges {\n node {\n id\n state\n signedBy {\n id\n }\n ...DocumentSignaturesDialog_signature\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n ...DocumentVersionHistoryDialogFragment\n ...DocumentSignaturesDialog_version\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment DocumentSignaturesDialog_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n\nfragment DocumentSignaturesDialog_version on DocumentVersion {\n version\n status\n publishedAt\n updatedAt\n}\n\nfragment DocumentVersionHistoryDialogFragment on DocumentVersion {\n id\n version\n status\n content\n changelog\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n"
"text": "query DocumentGraphNodeQuery(\n $documentId: ID!\n) {\n node(id: $documentId) {\n __typename\n ... on Document {\n ...DocumentDetailPageDocumentFragment\n }\n id\n }\n}\n\nfragment DocumentControlsTabFragment on Document {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment DocumentDetailPageDocumentFragment on Document {\n id\n title\n documentType\n owner {\n id\n fullName\n }\n ...DocumentControlsTabFragment\n controlsInfo: controls(first: 0) {\n totalCount\n }\n versions(first: 20) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n publishedBy {\n fullName\n id\n }\n signatures(first: 100) {\n edges {\n node {\n id\n state\n signedBy {\n id\n }\n ...DocumentSignaturesTab_signature\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment DocumentSignaturesTab_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n"
}
};
})();

View File

@@ -1,5 +1,4 @@
import type { PreloadedQuery } from "react-relay";
import type { DocumentGraphNodeQuery } from "/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql";
import {
ConnectionHandler,
graphql,
@@ -7,42 +6,53 @@ import {
useFragment,
usePreloadedQuery,
} from "react-relay";
import type { DocumentGraphNodeQuery } from "/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql";
import {
DocumentsConnectionKey,
documentNodeQuery,
DocumentsConnectionKey,
useDeleteDocumentMutation,
} from "/hooks/graph/DocumentGraph";
import { usePageTitle } from "@probo/hooks";
import type { DocumentDetailPageDocumentFragment$key } from "./__generated__/DocumentDetailPageDocumentFragment.graphql";
import type {
DocumentDetailPageDocumentFragment$data,
DocumentDetailPageDocumentFragment$key,
} from "./__generated__/DocumentDetailPageDocumentFragment.graphql";
import { useTranslate } from "@probo/i18n";
import {
PageHeader,
Breadcrumb,
IconCheckmark1,
PropertyRow,
Drawer,
Badge,
Avatar,
DropdownItem,
ActionDropdown,
IconTrashCan,
IconPencil,
Avatar,
Badge,
Breadcrumb,
Button,
Drawer,
Dropdown,
DropdownItem,
IconCheckmark1,
IconChevronDown,
IconClock,
IconSignature,
useConfirm,
Tabs,
TabLink,
IconPencil,
IconTrashCan,
PageHeader,
PropertyRow,
TabBadge,
TabLink,
Tabs,
useConfirm,
} from "@probo/ui";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { Button } from "@probo/ui";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { sprintf, getDocumentTypeLabel } from "@probo/helpers";
import { Outlet, useNavigate } from "react-router";
import { getDocumentTypeLabel, sprintf } from "@probo/helpers";
import {
Link,
Outlet,
useLocation,
useNavigate,
useParams,
} from "react-router";
import UpdateVersionDialog from "./dialogs/UpdateVersionDialog";
import { useRef } from "react";
import { DocumentVersionHistoryDialog } from "./dialogs/DocumentVersionHistoryDialog";
import { DocumentSignaturesDialog } from "./dialogs/DocumentSignaturesDialog";
import type { NodeOf } from "/types.ts";
import clsx from "clsx";
type Props = {
queryRef: PreloadedQuery<DocumentGraphNodeQuery>;
@@ -71,6 +81,9 @@ const documentFragment = graphql`
publishedAt
version
updatedAt
publishedBy {
fullName
}
signatures(first: 100)
@connection(key: "DocumentDetailPage_signatures") {
__id
@@ -81,12 +94,10 @@ const documentFragment = graphql`
signedBy {
id
}
...DocumentSignaturesDialog_signature
...DocumentSignaturesTab_signature
}
}
}
...DocumentVersionHistoryDialogFragment
...DocumentSignaturesDialog_version
}
}
}
@@ -106,22 +117,28 @@ const publishDocumentVersionMutation = graphql`
`;
export default function DocumentDetailPage(props: Props) {
const { versionId } = useParams<{ versionId?: string }>();
const node = usePreloadedQuery(documentNodeQuery, props.queryRef).node;
const document = useFragment(
documentFragment,
node as DocumentDetailPageDocumentFragment$key
node as DocumentDetailPageDocumentFragment$key,
);
const { __, dateFormat } = useTranslate();
const organizationId = useOrganizationId();
const navigate = useNavigate();
const lastVersion = document.versions.edges[0].node;
const isDraft = lastVersion.status === "DRAFT";
const versions = document.versions.edges.map((edge) => edge.node);
const currentVersion =
document.versions.edges.find((v) => v.node.id === versionId)?.node ??
document.versions.edges[0].node;
const signatures = currentVersion.signatures.edges.map((s) => s.node);
const signedSignatures = signatures.filter((s) => s.state === "SIGNED");
const isDraft = currentVersion.status === "DRAFT";
const [publishDocumentVersion, isPublishing] = useMutationWithToasts(
publishDocumentVersionMutation,
{
successMessage: __("Document published successfully."),
errorMessage: __("Failed to publish document. Please try again."),
}
},
);
const [deleteDocument, isDeleting] = useDeleteDocumentMutation();
const versionConnectionId = document.versions.__id;
@@ -139,7 +156,7 @@ export default function DocumentDetailPage(props: Props) {
props.queryRef.environment,
documentNodeQuery,
props.queryRef.variables,
{ fetchPolicy: "network-only" }
{ fetchPolicy: "network-only" },
);
},
});
@@ -153,7 +170,7 @@ export default function DocumentDetailPage(props: Props) {
new Promise<void>((resolve) => {
const connectionId = ConnectionHandler.getConnectionID(
organizationId,
DocumentsConnectionKey
DocumentsConnectionKey,
);
deleteDocument({
variables: {
@@ -169,16 +186,19 @@ export default function DocumentDetailPage(props: Props) {
{
message: sprintf(
__(
'This will permanently delete the document "%s". This action cannot be undone.'
'This will permanently delete the document "%s". This action cannot be undone.',
),
document.title
document.title,
),
}
},
);
};
const updateDialogRef = useRef<{ open: () => void }>(null);
const controlsCount = document.controlsInfo.totalCount;
const urlPrefix = versionId
? `/organizations/${organizationId}/documents/${document.id}/versions/${versionId}`
: `/organizations/${organizationId}/documents/${document.id}`;
return (
<>
@@ -210,16 +230,155 @@ export default function DocumentDetailPage(props: Props) {
{__("Publish")}
</Button>
)}
<DocumentVersionHistoryDialog document={document}>
<Button icon={IconClock} variant="secondary">
{__("Version history")}
</Button>
</DocumentVersionHistoryDialog>
<DocumentSignaturesDialog document={document}>
<Button icon={IconSignature} variant="secondary">
{__("Signatures history")}
</Button>
</DocumentSignaturesDialog>
<Dropdown
toggle={
<Button icon={IconClock} variant="secondary">
{__("Version history")}
<IconChevronDown size={12} />
</Button>
}
>
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
{versions.map((version) => (
<DropdownItem asChild key={version.id}>
<VersionItem
document={document}
version={version}
active={version.id === currentVersion.id}
onSelect={() => {}}
/>
</DropdownItem>
))}
</Dropdown>
<ActionDropdown variant="secondary">
<DropdownItem
@@ -242,20 +401,20 @@ export default function DocumentDetailPage(props: Props) {
<PageHeader title={document.title} />
<Tabs>
<TabLink
to={`/organizations/${organizationId}/documents/${document.id}/description`}
>
{__("Description")}
</TabLink>
<TabLink
to={`/organizations/${organizationId}/documents/${document.id}/controls`}
>
<TabLink to={`${urlPrefix}/description`}>{__("Description")}</TabLink>
<TabLink to={`${urlPrefix}/controls`}>
{__("Controls")}
<TabBadge>{controlsCount}</TabBadge>
</TabLink>
<TabLink to={`${urlPrefix}/signatures`}>
{__("Signatures")}
<TabBadge>
{signedSignatures.length}/{signatures.length}
</TabBadge>
</TabLink>
</Tabs>
<Outlet context={{ document, lastVersion }} />
<Outlet context={{ document, version: currentVersion }} />
</div>
<Drawer>
<div className="text-base text-txt-primary font-medium mb-4">
@@ -283,18 +442,18 @@ export default function DocumentDetailPage(props: Props) {
</PropertyRow>
<PropertyRow label={__("Version")}>
<div className="text-sm text-txt-secondary">
{lastVersion.version}
{currentVersion.version}
</div>
</PropertyRow>
<PropertyRow label={__("Last modified")}>
<div className="text-sm text-txt-secondary">
{dateFormat(lastVersion.updatedAt)}
{dateFormat(currentVersion.updatedAt)}
</div>
</PropertyRow>
{lastVersion.publishedAt && (
{currentVersion.publishedAt && (
<PropertyRow label={__("Published Date")}>
<div className="text-sm text-txt-secondary">
{dateFormat(lastVersion.publishedAt)}
{dateFormat(currentVersion.publishedAt)}
</div>
</PropertyRow>
)}
@@ -302,3 +461,46 @@ export default function DocumentDetailPage(props: Props) {
</>
);
}
type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
function VersionItem({
document,
version,
active,
onSelect,
...props
}: {
document: DocumentDetailPageDocumentFragment$data;
version: Version;
active?: boolean;
onSelect: (v: Version) => void;
}) {
const { dateTimeFormat } = useTranslate();
const organizationId = useOrganizationId();
const suffix = useLocation().pathname.split("/").at(-1);
return (
<Link
to={`/organizations/${organizationId}/documents/${document.id}/versions/${version.id}/${suffix}`}
onClick={() => onSelect(version)}
className={clsx(
"flex items-center gap-2 py-2 px-[10px] w-full hover:bg-tertiary-hover cursor-pointer rounded",
active && "bg-tertiary-pressed",
)}
{...props}
>
<Avatar
name={version.publishedBy?.fullName ?? document.owner.fullName}
size="l"
/>
<div className="text-start space-y-[2px] w-full overflow-hidden">
<div className="text-sm text-txt-primary whitespace-nowrap overflow-hidden text-ellipsis">
{version.publishedBy?.fullName ?? document.owner.fullName}
</div>
<div className="text-xs text-txt-secondary whitespace-nowrap overflow-hidden text-ellipsis">
{dateTimeFormat(version.publishedAt ?? version.updatedAt)}
</div>
</div>
</Link>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<4318aaecefe001fe6f42e6db26c421b4>>
* @generated SignedSource<<8db0c7dcd7ff31c01ec5facd5a3b598a>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -31,6 +31,9 @@ export type DocumentDetailPageDocumentFragment$data = {
readonly content: string;
readonly id: string;
readonly publishedAt: any | null | undefined;
readonly publishedBy: {
readonly fullName: string;
} | null | undefined;
readonly signatures: {
readonly __id: string;
readonly edges: ReadonlyArray<{
@@ -40,14 +43,13 @@ export type DocumentDetailPageDocumentFragment$data = {
readonly id: string;
};
readonly state: DocumentVersionSignatureState;
readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesDialog_signature">;
readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesTab_signature">;
};
}>;
};
readonly status: DocumentStatus;
readonly updatedAt: any;
readonly version: number;
readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesDialog_version" | "DocumentVersionHistoryDialogFragment">;
};
}>;
};
@@ -71,17 +73,24 @@ v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"name": "fullName",
"storageKey": null
},
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"name": "__typename",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
@@ -106,7 +115,7 @@ v3 = {
],
"storageKey": null
},
v4 = {
v5 = {
"kind": "ClientExtension",
"selections": [
{
@@ -165,13 +174,7 @@ return {
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
(v1/*: any*/)
],
"storageKey": null
},
@@ -264,6 +267,18 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": [
(v1/*: any*/)
],
"storageKey": null
},
{
"alias": "signatures",
"args": null,
@@ -311,41 +326,31 @@ return {
{
"args": null,
"kind": "FragmentSpread",
"name": "DocumentSignaturesDialog_signature"
"name": "DocumentSignaturesTab_signature"
},
(v1/*: any*/)
(v2/*: any*/)
],
"storageKey": null
},
(v2/*: any*/)
(v3/*: any*/)
],
"storageKey": null
},
(v3/*: any*/),
(v4/*: any*/)
(v4/*: any*/),
(v5/*: any*/)
],
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "DocumentVersionHistoryDialogFragment"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "DocumentSignaturesDialog_version"
},
(v1/*: any*/)
(v2/*: any*/)
],
"storageKey": null
},
(v2/*: any*/)
(v3/*: any*/)
],
"storageKey": null
},
(v3/*: any*/),
(v4/*: any*/)
(v4/*: any*/),
(v5/*: any*/)
],
"storageKey": null
}
@@ -355,6 +360,6 @@ return {
};
})();
(node as any).hash = "da1a4055d701fa9c3e0e0e50df24860f";
(node as any).hash = "1523ddad229ae2d6bf94d30de5579ae6";
export default node;

View File

@@ -1,167 +0,0 @@
import {
Avatar,
Button,
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
IconArrowInbox,
Markdown,
} from "@probo/ui";
import { graphql } from "relay-runtime";
import { useTranslate } from "@probo/i18n";
import { useState, type ReactNode } from "react";
import { useFragment } from "react-relay";
import type { DocumentVersionHistoryDialogFragment$key } from "./__generated__/DocumentVersionHistoryDialogFragment.graphql";
import type { DocumentVersionHistoryDialogExportPDFMutation } from "./__generated__/DocumentVersionHistoryDialogExportPDFMutation.graphql";
import clsx from "clsx";
import type { NodeOf } from "/types";
import type { DocumentDetailPageDocumentFragment$data } from "../__generated__/DocumentDetailPageDocumentFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
const historyFragment = graphql`
fragment DocumentVersionHistoryDialogFragment on DocumentVersion {
id
version
status
content
changelog
publishedAt
updatedAt
publishedBy {
fullName
}
}
`;
const exportPDFMutation = graphql`
mutation DocumentVersionHistoryDialogExportPDFMutation(
$documentVersionId: ID!
) {
exportDocumentVersionPDF(input: { documentVersionId: $documentVersionId }) {
data
}
}
`;
type Props = {
document: DocumentDetailPageDocumentFragment$data;
children?: ReactNode;
};
type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
export function DocumentVersionHistoryDialog(props: Props) {
const { __ } = useTranslate();
const versions = props.document.versions.edges.map((edge) => edge.node);
const [selectedVersion, setSelectedVersion] = useState<Version>(versions[0]);
const [exportPDF, isExporting] =
useMutationWithToasts<DocumentVersionHistoryDialogExportPDFMutation>(
exportPDFMutation,
{
errorMessage: __("Failed to export PDF"),
successMessage: __("PDF exported successfully"),
}
);
const handleDownloadPDF = () => {
exportPDF({
variables: { documentVersionId: selectedVersion.id },
onCompleted: (data) => {
if (data.exportDocumentVersionPDF.data) {
// Handle data URI format (data:application/pdf;base64,...)
const dataUri = data.exportDocumentVersionPDF.data;
// Extract base64 part from data URI
const base64Data = dataUri.split(",")[1];
// Convert base64 to blob and download
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([bytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${props.document.title}-v${selectedVersion.version}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
},
});
};
return (
<Dialog trigger={props.children}>
<DialogContent className="flex" scrollableChildren>
<aside className="p-6 overflow-y-auto w-60 flex-none space-y-2">
<DialogTitle className="text-base text-txt-primary font-medium mb-4">
{__("Version History")}
</DialogTitle>
{versions.map((version) => (
<VersionItem
key={version.id}
document={props.document}
version={version}
active={selectedVersion === version}
onSelect={setSelectedVersion}
/>
))}
</aside>
<main className="flex-1 px-12 py-8">
<div className="mb-4">
<Button
onClick={handleDownloadPDF}
icon={IconArrowInbox}
variant="secondary"
disabled={isExporting}
>
{isExporting ? __("Exporting...") : __("Download PDF")}
</Button>
</div>
<Markdown content={selectedVersion.content} />
</main>
</DialogContent>
<DialogFooter />
</Dialog>
);
}
function VersionItem(props: {
document: DocumentDetailPageDocumentFragment$data;
version: Version;
active?: boolean;
onSelect: (v: Version) => void;
}) {
const version = useFragment<DocumentVersionHistoryDialogFragment$key>(
historyFragment,
props.version
);
const { dateTimeFormat } = useTranslate();
return (
<button
onClick={() => props.onSelect(props.version)}
className={clsx(
"flex items-center gap-2 py-2 px-[10px] w-full hover:bg-tertiary-hover cursor-pointer rounded",
props.active && "bg-tertiary-pressed"
)}
>
<Avatar
name={version.publishedBy?.fullName ?? props.document.owner.fullName}
size="l"
/>
<div className="text-start space-y-[2px] w-full overflow-hidden">
<div className="text-sm text-txt-primary whitespace-nowrap overflow-hidden text-ellipsis">
{version.publishedBy?.fullName ?? props.document.owner.fullName}
</div>
<div className="text-xs text-txt-secondary whitespace-nowrap overflow-hidden text-ellipsis">
{dateTimeFormat(version.publishedAt ?? version.updatedAt)}
</div>
</div>
</button>
);
}

View File

@@ -1,67 +0,0 @@
/**
* @generated SignedSource<<dc19ab6bedc70a5b4546a2edb922b97f>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type DocumentStatus = "DRAFT" | "PUBLISHED";
import { FragmentRefs } from "relay-runtime";
export type DocumentSignaturesDialog_version$data = {
readonly publishedAt: any | null | undefined;
readonly status: DocumentStatus;
readonly updatedAt: any;
readonly version: number;
readonly " $fragmentType": "DocumentSignaturesDialog_version";
};
export type DocumentSignaturesDialog_version$key = {
readonly " $data"?: DocumentSignaturesDialog_version$data;
readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesDialog_version">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DocumentSignaturesDialog_version",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "publishedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"type": "DocumentVersion",
"abstractKey": null
};
(node as any).hash = "564746ce584a834e25a5a341155d6b2c";
export default node;

View File

@@ -1,95 +0,0 @@
/**
* @generated SignedSource<<06126b60bc37d8fbbb9a475af6d5d29d>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DocumentVersionHistoryDialogExportPDFMutation$variables = {
documentVersionId: string;
};
export type DocumentVersionHistoryDialogExportPDFMutation$data = {
readonly exportDocumentVersionPDF: {
readonly data: string;
};
};
export type DocumentVersionHistoryDialogExportPDFMutation = {
response: DocumentVersionHistoryDialogExportPDFMutation$data;
variables: DocumentVersionHistoryDialogExportPDFMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "documentVersionId"
}
],
v1 = [
{
"alias": null,
"args": [
{
"fields": [
{
"kind": "Variable",
"name": "documentVersionId",
"variableName": "documentVersionId"
}
],
"kind": "ObjectValue",
"name": "input"
}
],
"concreteType": "ExportDocumentVersionPDFPayload",
"kind": "LinkedField",
"name": "exportDocumentVersionPDF",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "data",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "DocumentVersionHistoryDialogExportPDFMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "DocumentVersionHistoryDialogExportPDFMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "45204e42c1018e00dcceaae84eb9a5f7",
"id": null,
"metadata": {},
"name": "DocumentVersionHistoryDialogExportPDFMutation",
"operationKind": "mutation",
"text": "mutation DocumentVersionHistoryDialogExportPDFMutation(\n $documentVersionId: ID!\n) {\n exportDocumentVersionPDF(input: {documentVersionId: $documentVersionId}) {\n data\n }\n}\n"
}
};
})();
(node as any).hash = "587bcc8bb6cf72e64a5eca18769ac432";
export default node;

View File

@@ -1,112 +0,0 @@
/**
* @generated SignedSource<<dd89afd13dd17b29485a3fdaf840cd42>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type DocumentStatus = "DRAFT" | "PUBLISHED";
import { FragmentRefs } from "relay-runtime";
export type DocumentVersionHistoryDialogFragment$data = {
readonly changelog: string;
readonly content: string;
readonly id: string;
readonly publishedAt: any | null | undefined;
readonly publishedBy: {
readonly fullName: string;
} | null | undefined;
readonly status: DocumentStatus;
readonly updatedAt: any;
readonly version: number;
readonly " $fragmentType": "DocumentVersionHistoryDialogFragment";
};
export type DocumentVersionHistoryDialogFragment$key = {
readonly " $data"?: DocumentVersionHistoryDialogFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DocumentVersionHistoryDialogFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DocumentVersionHistoryDialogFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "changelog",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "publishedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "DocumentVersion",
"abstractKey": null
};
(node as any).hash = "947253287039236aa413ee33879a6e4b";
export default node;

View File

@@ -4,12 +4,12 @@ import type { NodeOf } from "/types";
import { Markdown } from "@probo/ui";
export default function DocumentDescriptionTab() {
const { lastVersion } = useOutletContext<{
lastVersion: NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
const { version } = useOutletContext<{
version: NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
}>();
return (
<div>
<Markdown content={lastVersion.content} />
<Markdown content={version.content} />
</div>
);
}

View File

@@ -2,119 +2,34 @@ import {
Avatar,
Badge,
Button,
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
IconCircleCheck,
IconClock,
DocumentVersionBadge,
Spinner,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useState, type ReactNode, Suspense } from "react";
import clsx from "clsx";
import { Suspense } from "react";
import type { ItemOf, NodeOf } from "/types";
import { graphql, useFragment } from "react-relay";
import type { DocumentSignaturesDialog_version$key } from "./__generated__/DocumentSignaturesDialog_version.graphql";
import type { DocumentSignaturesDialog_signature$key } from "./__generated__/DocumentSignaturesDialog_signature.graphql";
import { usePeople } from "/hooks/graph/PeopleGraph.ts";
import { useOrganizationId } from "/hooks/useOrganizationId.ts";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts.ts";
import { sprintf } from "@probo/helpers";
import type { DocumentDetailPageDocumentFragment$data } from "../__generated__/DocumentDetailPageDocumentFragment.graphql";
type Props = {
document: DocumentDetailPageDocumentFragment$data;
children?: ReactNode;
};
import { useOutletContext } from "react-router";
import type { DocumentSignaturesTab_signature$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_signature.graphql.ts";
type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
export function DocumentSignaturesDialog(props: Props) {
const { __ } = useTranslate();
const versions = props.document.versions.edges.map((edge) => edge.node);
const [selectedVersionId, setSelectedVersionId] = useState<string>(
versions[0].id,
);
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
if (!selectedVersion) {
export default function DocumentSignaturesTab() {
const { version } = useOutletContext<{ version: Version }>();
if (!version) {
return null;
}
return (
<Dialog trigger={props.children}>
<DialogContent className="flex" scrollableChildren>
<aside className="p-6 overflow-y-auto w-60 flex-none space-y-2">
<div className="text-base text-txt-primary font-medium mb-4">
{__("Version History")}
</div>
{versions.map((version) => (
<VersionItem
key={version.id}
version={version}
active={selectedVersionId === version.id}
onSelect={setSelectedVersionId}
/>
))}
</aside>
<main className="flex-1 px-12 py-8">
<DialogTitle className="text-2xl font-bold text-txt-primary mb-4">
{__("Signatures")}
</DialogTitle>
<p className="text-sm text-txt-secondary mb-4">
{__(
"Click request to ask for a signature from people in your organization.",
)}
</p>
<Suspense fallback={<Spinner centered />}>
<SignatureList version={selectedVersion} />
</Suspense>
</main>
</DialogContent>
<DialogFooter />
</Dialog>
);
}
const versionFragment = graphql`
fragment DocumentSignaturesDialog_version on DocumentVersion {
version
status
publishedAt
updatedAt
}
`;
function VersionItem(props: {
version: Version;
active?: boolean;
onSelect: (v: string) => void;
}) {
const { dateTimeFormat, __ } = useTranslate();
const version = useFragment<DocumentSignaturesDialog_version$key>(
versionFragment,
props.version,
);
return (
<button
onClick={() => props.onSelect(props.version.id)}
className={clsx(
"text-start space-y-1 w-full overflow-hidden py-2 px-3 w-full hover:bg-tertiary-hover cursor-pointer rounded",
props.active && "bg-tertiary-pressed",
)}
>
<div className="flex gap-1 text-sm text-txt-primary">
<span>
{__("Version")} {version.version}
</span>
<DocumentVersionBadge state={version.status} />
</div>
<div className="text-xs text-txt-secondary">
{dateTimeFormat(version.publishedAt ?? version.updatedAt)}
</div>
</button>
<Suspense fallback={<Spinner centered />}>
<SignatureList version={version} />
</Suspense>
);
}
@@ -154,7 +69,7 @@ function SignatureList(props: { version: Version }) {
* Fragments
*/
const signatureFragment = graphql`
fragment DocumentSignaturesDialog_signature on DocumentVersionSignature {
fragment DocumentSignaturesTab_signature on DocumentVersionSignature {
id
state
signedAt
@@ -170,7 +85,7 @@ const signatureFragment = graphql`
* Mutations
*/
const requestSignatureMutation = graphql`
mutation DocumentSignaturesDialog_requestSignatureMutation(
mutation DocumentSignaturesTab_requestSignatureMutation(
$input: RequestSignatureInput!
$connections: [ID!]!
) {
@@ -182,14 +97,14 @@ const requestSignatureMutation = graphql`
signedBy {
id
}
...DocumentSignaturesDialog_signature
...DocumentSignaturesTab_signature
}
}
}
}
`;
const cancelSignatureMutation = graphql`
mutation DocumentSignaturesDialog_cancelSignatureMutation(
mutation DocumentSignaturesTab_cancelSignatureMutation(
$input: CancelSignatureRequestInput!
$connections: [ID!]!
) {
@@ -201,7 +116,7 @@ const cancelSignatureMutation = graphql`
function SignatureItem(props: {
versionId: string;
signature?: DocumentSignaturesDialog_signature$key;
signature?: DocumentSignaturesTab_signature$key;
people: ItemOf<ReturnType<typeof usePeople>>;
connectionId: string;
signable: boolean;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9f55e528d2b6366114a1852c9beaf1b0>>
* @generated SignedSource<<0a17c03011fe74b75a90dbeae02ab2ef>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime';
export type CancelSignatureRequestInput = {
documentVersionSignatureId: string;
};
export type DocumentSignaturesDialog_cancelSignatureMutation$variables = {
export type DocumentSignaturesTab_cancelSignatureMutation$variables = {
connections: ReadonlyArray<string>;
input: CancelSignatureRequestInput;
};
export type DocumentSignaturesDialog_cancelSignatureMutation$data = {
export type DocumentSignaturesTab_cancelSignatureMutation$data = {
readonly cancelSignatureRequest: {
readonly deletedDocumentVersionSignatureId: string;
};
};
export type DocumentSignaturesDialog_cancelSignatureMutation = {
response: DocumentSignaturesDialog_cancelSignatureMutation$data;
variables: DocumentSignaturesDialog_cancelSignatureMutation$variables;
export type DocumentSignaturesTab_cancelSignatureMutation = {
response: DocumentSignaturesTab_cancelSignatureMutation$data;
variables: DocumentSignaturesTab_cancelSignatureMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -59,7 +59,7 @@ return {
],
"kind": "Fragment",
"metadata": null,
"name": "DocumentSignaturesDialog_cancelSignatureMutation",
"name": "DocumentSignaturesTab_cancelSignatureMutation",
"selections": [
{
"alias": null,
@@ -84,7 +84,7 @@ return {
(v0/*: any*/)
],
"kind": "Operation",
"name": "DocumentSignaturesDialog_cancelSignatureMutation",
"name": "DocumentSignaturesTab_cancelSignatureMutation",
"selections": [
{
"alias": null,
@@ -117,16 +117,16 @@ return {
]
},
"params": {
"cacheID": "81d10822e563ceb7ebcd4fdb5d19e8f5",
"cacheID": "c57722e2b57faca64eea4950864e034d",
"id": null,
"metadata": {},
"name": "DocumentSignaturesDialog_cancelSignatureMutation",
"name": "DocumentSignaturesTab_cancelSignatureMutation",
"operationKind": "mutation",
"text": "mutation DocumentSignaturesDialog_cancelSignatureMutation(\n $input: CancelSignatureRequestInput!\n) {\n cancelSignatureRequest(input: $input) {\n deletedDocumentVersionSignatureId\n }\n}\n"
"text": "mutation DocumentSignaturesTab_cancelSignatureMutation(\n $input: CancelSignatureRequestInput!\n) {\n cancelSignatureRequest(input: $input) {\n deletedDocumentVersionSignatureId\n }\n}\n"
}
};
})();
(node as any).hash = "0cbec91e7ba7cc09d2bbf7169e98f30c";
(node as any).hash = "9f5d3ff869ec6f91285140285248e824";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<fa604130d8725651770072b638fc5926>>
* @generated SignedSource<<de274a51c1b339789540a969b0ebe507>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,11 +15,11 @@ export type RequestSignatureInput = {
documentVersionId: string;
signatoryId: string;
};
export type DocumentSignaturesDialog_requestSignatureMutation$variables = {
export type DocumentSignaturesTab_requestSignatureMutation$variables = {
connections: ReadonlyArray<string>;
input: RequestSignatureInput;
};
export type DocumentSignaturesDialog_requestSignatureMutation$data = {
export type DocumentSignaturesTab_requestSignatureMutation$data = {
readonly requestSignature: {
readonly documentVersionSignatureEdge: {
readonly node: {
@@ -28,14 +28,14 @@ export type DocumentSignaturesDialog_requestSignatureMutation$data = {
readonly id: string;
};
readonly state: DocumentVersionSignatureState;
readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesDialog_signature">;
readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesTab_signature">;
};
};
};
};
export type DocumentSignaturesDialog_requestSignatureMutation = {
response: DocumentSignaturesDialog_requestSignatureMutation$data;
variables: DocumentSignaturesDialog_requestSignatureMutation$variables;
export type DocumentSignaturesTab_requestSignatureMutation = {
response: DocumentSignaturesTab_requestSignatureMutation$data;
variables: DocumentSignaturesTab_requestSignatureMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -78,7 +78,7 @@ return {
],
"kind": "Fragment",
"metadata": null,
"name": "DocumentSignaturesDialog_requestSignatureMutation",
"name": "DocumentSignaturesTab_requestSignatureMutation",
"selections": [
{
"alias": null,
@@ -121,7 +121,7 @@ return {
{
"args": null,
"kind": "FragmentSpread",
"name": "DocumentSignaturesDialog_signature"
"name": "DocumentSignaturesTab_signature"
}
],
"storageKey": null
@@ -143,7 +143,7 @@ return {
(v0/*: any*/)
],
"kind": "Operation",
"name": "DocumentSignaturesDialog_requestSignatureMutation",
"name": "DocumentSignaturesTab_requestSignatureMutation",
"selections": [
{
"alias": null,
@@ -239,16 +239,16 @@ return {
]
},
"params": {
"cacheID": "6a5413db69a07e0eef99245e949cf4b2",
"cacheID": "e582b90bdad831d64741b45de17f45b5",
"id": null,
"metadata": {},
"name": "DocumentSignaturesDialog_requestSignatureMutation",
"name": "DocumentSignaturesTab_requestSignatureMutation",
"operationKind": "mutation",
"text": "mutation DocumentSignaturesDialog_requestSignatureMutation(\n $input: RequestSignatureInput!\n) {\n requestSignature(input: $input) {\n documentVersionSignatureEdge {\n node {\n id\n state\n signedBy {\n id\n }\n ...DocumentSignaturesDialog_signature\n }\n }\n }\n}\n\nfragment DocumentSignaturesDialog_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n"
"text": "mutation DocumentSignaturesTab_requestSignatureMutation(\n $input: RequestSignatureInput!\n) {\n requestSignature(input: $input) {\n documentVersionSignatureEdge {\n node {\n id\n state\n signedBy {\n id\n }\n ...DocumentSignaturesTab_signature\n }\n }\n }\n}\n\nfragment DocumentSignaturesTab_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n"
}
};
})();
(node as any).hash = "6c51f681411cd3de162c78a3db20ec30";
(node as any).hash = "f0b9fc6ff9db455e9079bf7e45fcd603";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<71e44f5802c3eaba88dde399af20ed89>>
* @generated SignedSource<<e3ddbde4beb849447cb819f8991245a7>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,7 +11,7 @@
import { ReaderFragment } from 'relay-runtime';
export type DocumentVersionSignatureState = "REQUESTED" | "SIGNED";
import { FragmentRefs } from "relay-runtime";
export type DocumentSignaturesDialog_signature$data = {
export type DocumentSignaturesTab_signature$data = {
readonly id: string;
readonly requestedAt: any;
readonly signedAt: any | null | undefined;
@@ -20,18 +20,18 @@ export type DocumentSignaturesDialog_signature$data = {
readonly primaryEmailAddress: string;
};
readonly state: DocumentVersionSignatureState;
readonly " $fragmentType": "DocumentSignaturesDialog_signature";
readonly " $fragmentType": "DocumentSignaturesTab_signature";
};
export type DocumentSignaturesDialog_signature$key = {
readonly " $data"?: DocumentSignaturesDialog_signature$data;
readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesDialog_signature">;
export type DocumentSignaturesTab_signature$key = {
readonly " $data"?: DocumentSignaturesTab_signature$data;
readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesTab_signature">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DocumentSignaturesDialog_signature",
"name": "DocumentSignaturesTab_signature",
"selections": [
{
"alias": null,
@@ -91,6 +91,6 @@ const node: ReaderFragment = {
"abstractKey": null
};
(node as any).hash = "0db843d4ebd21f44a3d126e85da8cf26";
(node as any).hash = "b07437b34548744fef4ed78d4f1f0a62";
export default node;

View File

@@ -9,6 +9,46 @@ import { redirect } from "react-router";
import { lazy } from "@probo/react-lazy";
import { LinkCardSkeleton } from "/components/skeletons/LinkCardSkeleton";
const documentTabs = (prefix: string) => {
return [
{
path: `${prefix}`,
queryLoader: ({ organizationId, documentId }) => {
throw redirect(
`/organizations/${organizationId}/documents/${documentId}/description`,
);
},
Component: Fragment,
},
{
path: `${prefix}description`,
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import(
"../pages/organizations/documents/tabs/DocumentDescriptionTab"
),
),
},
{
path: `${prefix}controls`,
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import("../pages/organizations/documents/tabs/DocumentControlsTab"),
),
},
{
path: `${prefix}signatures`,
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import("../pages/organizations/documents/tabs/DocumentSignaturesTab"),
),
},
] satisfies AppRoute[];
};
export const documentsRoutes = [
{
path: "documents",
@@ -16,7 +56,7 @@ export const documentsRoutes = [
queryLoader: ({ organizationId }) =>
loadQuery(relayEnvironment, documentsQuery, { organizationId }),
Component: lazy(
() => import("/pages/organizations/documents/DocumentsPage")
() => import("/pages/organizations/documents/DocumentsPage"),
),
},
{
@@ -25,36 +65,8 @@ export const documentsRoutes = [
queryLoader: ({ documentId }) =>
loadQuery(relayEnvironment, documentNodeQuery, { documentId }),
Component: lazy(
() => import("../pages/organizations/documents/DocumentDetailPage")
() => import("../pages/organizations/documents/DocumentDetailPage"),
),
children: [
{
path: "",
queryLoader: ({ organizationId, documentId }) => {
throw redirect(
`/organizations/${organizationId}/documents/${documentId}/description`
);
},
Component: Fragment,
},
{
path: "description",
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import(
"../pages/organizations/documents/tabs/DocumentDescriptionTab"
)
),
},
{
path: "controls",
fallback: LinkCardSkeleton,
Component: lazy(
() =>
import("../pages/organizations/documents/tabs/DocumentControlsTab")
),
},
],
children: [...documentTabs(""), ...documentTabs("versions/:versionId/")],
},
] satisfies AppRoute[];