Add version selector on document
Fix #166 Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
committed by
Sacha Al Himdani
parent
a4606f95f5
commit
f114f3bc4a
@@ -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"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user