Fix document signature loading

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-29 13:51:35 +01:00
parent ecb716066c
commit 766aba5da1
18 changed files with 842 additions and 1563 deletions

View File

@@ -1,237 +0,0 @@
/**
* @generated SignedSource<<8bbf9386bee93021b7e06e5b3bd31c99>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type domainSettingsPermissionsQuery$variables = {
organizationId: string;
};
export type domainSettingsPermissionsQuery$data = {
readonly organization: {
readonly __typename: "Organization";
readonly viewerMembership: {
readonly role: MembershipRole;
};
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
};
readonly viewer: {
readonly canCreateCustomDomain: boolean;
readonly canDeleteCustomDomain: boolean;
};
};
export type domainSettingsPermissionsQuery = {
response: domainSettingsPermissionsQuery$data;
variables: domainSettingsPermissionsQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = {
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
},
v2 = {
"alias": "canCreateCustomDomain",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:custom-domain:create"
},
(v1/*: any*/)
],
"kind": "ScalarField",
"name": "permission",
"storageKey": null
},
v3 = {
"alias": "canDeleteCustomDomain",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:custom-domain:delete"
},
(v1/*: any*/)
],
"kind": "ScalarField",
"name": "permission",
"storageKey": null
},
v4 = [
(v1/*: any*/)
],
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "domainSettingsPermissionsQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/)
],
"storageKey": null
},
"action": "THROW"
},
{
"kind": "RequiredField",
"field": {
"alias": "organization",
"args": (v4/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v5/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "viewerMembership",
"plural": false,
"selections": [
(v6/*: any*/)
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "domainSettingsPermissionsQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v7/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v4/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v5/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "viewerMembership",
"plural": false,
"selections": [
(v6/*: any*/),
(v7/*: any*/)
],
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
},
(v7/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "de4faa8385c652071c57b4ddcc58e793",
"id": null,
"metadata": {},
"name": "domainSettingsPermissionsQuery",
"operationKind": "query",
"text": "query domainSettingsPermissionsQuery(\n $organizationId: ID!\n) {\n viewer {\n canCreateCustomDomain: permission(action: \"core:custom-domain:create\", id: $organizationId)\n canDeleteCustomDomain: permission(action: \"core:custom-domain:delete\", id: $organizationId)\n id\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n viewerMembership {\n role\n id\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "75397f9db7cb6f2320ff91f30e773e22";
export default node;

View File

@@ -1,38 +1,30 @@
import { useTranslate } from "@probo/i18n";
import {
Button,
Card,
Spinner,
IconCircleCheck,
IconRadioUnchecked,
} from "@probo/ui";
import clsx from "clsx";
import { Card, Spinner } from "@probo/ui";
import {
usePreloadedQuery,
useFragment,
useMutation,
type PreloadedQuery,
} from "react-relay";
import { graphql } from "relay-runtime";
import { graphql } from "react-relay";
import type { EmployeeDocumentSignaturePageQuery } from "./__generated__/EmployeeDocumentSignaturePageQuery.graphql";
import { usePageTitle } from "@probo/hooks";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { EmployeeDocumentSignaturePageSignMutation } from "./__generated__/EmployeeDocumentSignaturePageSignMutation.graphql";
import type { EmployeeDocumentSignaturePageExportSignablePDFMutation } from "./__generated__/EmployeeDocumentSignaturePageExportSignablePDFMutation.graphql";
import { useNavigate } from "react-router";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { PDFPreview } from "/components/documents/PDFPreview";
import { useWindowSize } from "usehooks-ts";
import { useState, useEffect, useRef, useMemo } from "react";
import { useState, useEffect, useRef } from "react";
import type { EmployeeDocumentSignaturePageDocumentFragment$key } from "./__generated__/EmployeeDocumentSignaturePageDocumentFragment.graphql";
import type { EmployeeDocumentSignaturePageVersionFragment$key } from "./__generated__/EmployeeDocumentSignaturePageVersionFragment.graphql";
import { useToast } from "@probo/ui";
import { formatError, type GraphQLError } from "@probo/helpers";
import { VersionActions } from "./_components/VersionActions";
import { VersionRow } from "./_components/VersionRow";
export const employeeDocumentSignatureQuery = graphql`
export const employeeDocumentSignaturePageQuery = graphql`
query EmployeeDocumentSignaturePageQuery($documentId: ID!) {
viewer {
id
viewer @required(action: THROW) {
signableDocument(id: $documentId) {
id
...EmployeeDocumentSignaturePageDocumentFragment
@@ -46,28 +38,23 @@ const documentFragment = graphql`
id
title
signed
versions(first: 100, orderBy: { field: CREATED_AT, direction: DESC }) {
edges {
node {
versions(first: 100, orderBy: { field: CREATED_AT, direction: DESC })
@required(action: THROW) {
edges @required(action: THROW) {
node @required(action: THROW) {
id
...EmployeeDocumentSignaturePageVersionFragment
...VersionActionsFragment
...VersionRowFragment
}
}
}
}
`;
const versionFragment = graphql`
fragment EmployeeDocumentSignaturePageVersionFragment on DocumentVersion {
id
version
signed
publishedAt
}
`;
const signDocumentMutation = graphql`
mutation EmployeeDocumentSignaturePageSignMutation($input: SignDocumentInput!) {
mutation EmployeeDocumentSignaturePageSignMutation(
$input: SignDocumentInput!
) {
signDocument(input: $input) {
documentVersionSignature {
id
@@ -87,13 +74,15 @@ const exportSignableVersionDocumentPDFMutation = graphql`
}
`;
type Props = {
export function EmployeeDocumentSignaturePage(props: {
queryRef: PreloadedQuery<EmployeeDocumentSignaturePageQuery>;
};
export default function EmployeeDocumentSignaturePage(props: Props) {
const data = usePreloadedQuery(employeeDocumentSignatureQuery, props.queryRef);
const document = data.viewer.signableDocument;
}) {
const { queryRef } = props;
const { viewer } = usePreloadedQuery<EmployeeDocumentSignaturePageQuery>(
employeeDocumentSignaturePageQuery,
queryRef
);
const document = viewer.signableDocument;
if (!document) {
return (
@@ -103,13 +92,13 @@ export default function EmployeeDocumentSignaturePage(props: Props) {
);
}
return <DocumentSignatureContent document={document} />;
return <DocumentSignatureContent fKey={document} />;
}
function DocumentSignatureContent({
document,
fKey,
}: {
document: EmployeeDocumentSignaturePageDocumentFragment$key;
fKey: EmployeeDocumentSignaturePageDocumentFragment$key;
}) {
const { __ } = useTranslate();
const navigate = useNavigate();
@@ -118,45 +107,38 @@ function DocumentSignatureContent({
const isDesktop = !isMobile;
const organizationId = useOrganizationId();
const documentData = useFragment<EmployeeDocumentSignaturePageDocumentFragment$key>(
documentFragment,
document
);
const documentData =
useFragment<EmployeeDocumentSignaturePageDocumentFragment$key>(
documentFragment,
fKey
);
const versions = useMemo(() => {
return documentData.versions?.edges
?.map((edge) => edge?.node)
.filter(Boolean) || [];
}, [documentData.versions?.edges]);
const versions = documentData.versions.edges.map(({ node }) => node);
const [selectedVersionId, setSelectedVersionId] = useState<string | undefined>(
() => versions[0]?.id
);
const [selectedVersionId, setSelectedVersionId] = useState<
string | undefined
>(() => versions[0]?.id);
const selectedVersion = useMemo(() => {
return versions.find((v) => v?.id === selectedVersionId);
}, [versions, selectedVersionId]);
const selectedVersion = versions.find((v) => v?.id === selectedVersionId);
usePageTitle(__("Sign Document"));
const { toast } = useToast();
const [signDocument, isSigning] = useMutationWithToasts<EmployeeDocumentSignaturePageSignMutation>(
signDocumentMutation,
{
successMessage: __("Document signed successfully"),
errorMessage: __("Failed to sign document"),
}
);
const [signDocument, isSigning] =
useMutation<EmployeeDocumentSignaturePageSignMutation>(
signDocumentMutation
);
const [exportSignableVersionDocumentPDF] = useMutation<EmployeeDocumentSignaturePageExportSignablePDFMutation>(
exportSignableVersionDocumentPDFMutation
);
const [exportSignableVersionDocumentPDF] =
useMutation<EmployeeDocumentSignaturePageExportSignablePDFMutation>(
exportSignableVersionDocumentPDFMutation
);
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const pdfUrlRef = useRef<string | null>(null);
const handleSign = async (versionId: string) => {
await signDocument({
signDocument({
variables: {
input: {
documentVersionId: versionId,
@@ -170,10 +152,22 @@ function DocumentSignatureContent({
store.invalidateStore();
},
onCompleted: () => {
toast({
title: __("Success"),
description: __("Document signed successfully"),
variant: "success",
});
navigate(`/organizations/${organizationId}/employee`);
},
onError: (error) => {
console.error("Error signing document:", error);
toast({
title: __("Error"),
description: formatError(
__("Failed to sign document"),
error as GraphQLError
),
variant: "error",
});
},
});
};
@@ -191,7 +185,10 @@ function DocumentSignatureContent({
if (errors) {
toast({
title: __("Error"),
description: formatError(__("Failed to load PDF"), errors as GraphQLError[]),
description: formatError(
__("Failed to load PDF"),
errors as GraphQLError[]
),
variant: "error",
});
return;
@@ -205,7 +202,10 @@ function DocumentSignatureContent({
onError: (error) => {
toast({
title: __("Error"),
description: formatError(__("Failed to load PDF"), error as GraphQLError),
description: formatError(
__("Failed to load PDF"),
error as GraphQLError
),
variant: "error",
});
},
@@ -217,7 +217,10 @@ function DocumentSignatureContent({
}, [selectedVersion?.id, exportSignableVersionDocumentPDF, toast, __]);
return (
<div className="fixed bg-level-2 flex flex-col" style={{ top: '3rem', left: 0, right: 0, bottom: 0 }}>
<div
className="fixed bg-level-2 flex flex-col"
style={{ top: "3rem", left: 0, right: 0, bottom: 0 }}
>
<div className="grid lg:grid-cols-2 min-h-0 h-full">
<div className="w-full lg:w-[440px] mx-auto py-20 overflow-y-auto scrollbar-hide">
<h1 className="text-2xl font-semibold mb-6">
@@ -230,7 +233,7 @@ function DocumentSignatureContent({
return (
<VersionRow
key={version.id}
version={version}
fKey={version}
isSelected={version.id === selectedVersionId}
onSelect={() => setSelectedVersionId(version.id)}
/>
@@ -246,10 +249,12 @@ function DocumentSignatureContent({
<div className="min-h-[60px]">
{selectedVersion ? (
<VersionActions
version={selectedVersion}
fKey={selectedVersion}
isSigning={isSigning}
onSign={handleSign}
onBack={() => navigate(`/organizations/${organizationId}/employee`)}
onBack={() =>
navigate(`/organizations/${organizationId}/employee`)
}
/>
) : null}
</div>
@@ -257,143 +262,12 @@ function DocumentSignatureContent({
{isDesktop && (
<div className="bg-subtle h-full border-l border-border-solid min-h-0">
{pdfUrl && <PDFPreview src={pdfUrl} name={documentData.title || ""} />}
{pdfUrl && (
<PDFPreview src={pdfUrl} name={documentData.title || ""} />
)}
</div>
)}
</div>
</div>
);
}
function VersionActions({
version,
isSigning,
onSign,
onBack,
}: {
version: EmployeeDocumentSignaturePageVersionFragment$key;
isSigning: boolean;
onSign: (versionId: string) => void;
onBack: () => void;
}) {
const { __ } = useTranslate();
const versionData = useFragment<EmployeeDocumentSignaturePageVersionFragment$key>(
versionFragment,
version
);
const isSigned = versionData.signed;
if (isSigned) {
return (
<>
<Button
onClick={onBack}
className="h-10 w-full"
variant="secondary"
>
{__("Back to Documents")}
</Button>
<p className="text-xs text-txt-tertiary mt-2 h-5" />
</>
);
}
return (
<>
<Button
onClick={() => onSign(versionData.id)}
className="h-10 w-full"
disabled={isSigning}
icon={isSigning ? Spinner : undefined}
>
{__("I acknowledge and agree")}
</Button>
<p className="text-xs text-txt-tertiary mt-2 h-5">
{__(
"By clicking 'I acknowledge and agree', your digital signature will be recorded."
)}
</p>
</>
);
}
function VersionRow({
version,
isSelected,
onSelect,
}: {
version: EmployeeDocumentSignaturePageVersionFragment$key;
isSelected: boolean;
onSelect: () => void;
}) {
const { __ } = useTranslate();
const versionData = useFragment<EmployeeDocumentSignaturePageVersionFragment$key>(
versionFragment,
version
);
const isVersionSigned = versionData.signed;
return (
<div
onClick={onSelect}
className={clsx(
"flex items-center gap-3 py-3 px-4 transition-colors cursor-pointer",
isSelected
? "bg-blue-50 border-l-4 border-blue-500"
: "bg-transparent hover:bg-level-1"
)}
>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-level-2 flex-shrink-0">
{isVersionSigned ? (
<IconCircleCheck
size={20}
className="text-txt-success"
/>
) : (
<IconRadioUnchecked
size={20}
className="text-txt-tertiary"
/>
)}
</div>
<div className="flex-1 min-w-0">
<p
className={clsx(
"text-sm font-medium truncate",
isVersionSigned
? "text-txt-tertiary"
: "text-txt-primary"
)}
>
{versionData.publishedAt
? `v${versionData.version} - ${(() => {
const date = new Date(versionData.publishedAt);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
})()}`
: `v${versionData.version}`}
</p>
</div>
<div className="flex-shrink-0">
<span
className={clsx(
"inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium",
isVersionSigned
? "bg-green-100 text-green-800"
: isSelected
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-700"
)}
>
{isVersionSigned
? __("Signed")
: isSelected
? __("In review")
: __("Waiting signature")}
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { useQueryLoader } from "react-relay";
import { Suspense, useEffect } from "react";
import { PageSkeleton } from "/components/skeletons/PageSkeleton";
import { useParams } from "react-router";
import type { EmployeeDocumentSignaturePageQuery } from "./__generated__/EmployeeDocumentSignaturePageQuery.graphql";
import {
EmployeeDocumentSignaturePage,
employeeDocumentSignaturePageQuery,
} from "./EmployeeDocumentSignaturePage";
import { CoreRelayProvider } from "/providers/CoreRelayProvider";
function EmployeeDocumentSignaturePageLoader() {
const { documentId } = useParams();
const [queryRef, loadQuery] =
useQueryLoader<EmployeeDocumentSignaturePageQuery>(
employeeDocumentSignaturePageQuery
);
useEffect(() => {
if (documentId) {
loadQuery({
documentId,
});
}
}, [loadQuery, documentId]);
if (!queryRef) {
return <PageSkeleton />;
}
return (
<Suspense fallback={<PageSkeleton />}>
<EmployeeDocumentSignaturePage queryRef={queryRef} />
</Suspense>
);
}
export default function () {
return (
<CoreRelayProvider>
<EmployeeDocumentSignaturePageLoader />
</CoreRelayProvider>
);
}

View File

@@ -1,100 +1,47 @@
import { graphql, usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { useTranslate } from "@probo/i18n";
import {
PageHeader,
Tbody,
Thead,
Tr,
Th,
Td,
Badge,
Card,
} from "@probo/ui";
import { SortableTable } from "/components/SortableTable";
import {
useFragment,
usePaginationFragment,
usePreloadedQuery,
type PreloadedQuery,
} from "react-relay";
import { graphql } from "relay-runtime";
import type { EmployeeDocumentsPageListQuery } from "./__generated__/EmployeeDocumentsPageListQuery.graphql";
import type { EmployeeDocumentsPageListFragment$key } from "./__generated__/EmployeeDocumentsPageListFragment.graphql";
import { PageHeader, Tbody, Thead, Tr, Th, Card } from "@probo/ui";
import type { EmployeeDocumentsPageQuery } from "./__generated__/EmployeeDocumentsPageQuery.graphql";
import { usePageTitle } from "@probo/hooks";
import { getDocumentClassificationLabel, getDocumentTypeLabel, formatDate } from "@probo/helpers";
import type { EmployeeDocumentsPageRowFragment$key } from "./__generated__/EmployeeDocumentsPageRowFragment.graphql";
import { useEffect } from "react";
import { useParams } from "react-router";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { DocumentRow } from "./_components/DocumentRow";
export const employeeDocumentsQuery = graphql`
query EmployeeDocumentsPageListQuery($organizationId: ID!) {
viewer {
id
...EmployeeDocumentsPageListFragment @arguments(organizationId: $organizationId)
}
}
`;
const employeeDocumentsFragment = graphql`
fragment EmployeeDocumentsPageListFragment on Viewer
@refetchable(queryName: "EmployeeDocumentsListQuery")
@argumentDefinitions(
organizationId: { type: "ID!" }
first: { type: "Int", defaultValue: 50 }
order: {
type: "DocumentOrder"
defaultValue: { field: CREATED_AT, direction: DESC }
}
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
) {
signableDocuments(
organizationId: $organizationId
first: $first
after: $after
last: $last
before: $before
orderBy: $order
) @connection(key: "EmployeeDocumentsListQuery_signableDocuments") {
__id
edges {
node {
id
...EmployeeDocumentsPageRowFragment
export const employeeDocumentsPageQuery = graphql`
query EmployeeDocumentsPageQuery($organizationId: ID!) {
viewer @required(action: THROW) {
signableDocuments(
organizationId: $organizationId
first: 1000
orderBy: { field: CREATED_AT, direction: DESC }
) @required(action: THROW) {
edges @required(action: THROW) {
node @required(action: THROW) {
id
...DocumentRowFragment
}
}
}
}
}
`;
type Props = {
queryRef: PreloadedQuery<EmployeeDocumentsPageListQuery>;
};
export default function EmployeeDocumentsPage(props: Props) {
export function EmployeeDocumentsPage(props: {
queryRef: PreloadedQuery<EmployeeDocumentsPageQuery>;
}) {
const { __ } = useTranslate();
const params = useParams<{ organizationId: string }>();
const organizationId = params.organizationId!;
const organizationId = useOrganizationId();
const data = usePreloadedQuery(
employeeDocumentsQuery,
props.queryRef
const { queryRef } = props;
const {
viewer: {
signableDocuments: { edges: initialDocuments },
},
} = usePreloadedQuery<EmployeeDocumentsPageQuery>(
employeeDocumentsPageQuery,
queryRef
);
const pagination = usePaginationFragment(
employeeDocumentsFragment,
data.viewer as EmployeeDocumentsPageListFragment$key
);
const { refetch } = pagination;
useEffect(() => {
refetch({ organizationId }, { fetchPolicy: 'network-only' });
}, [organizationId, refetch]);
const documents = pagination.data.signableDocuments?.edges
?.map((edge) => edge?.node)
.filter(Boolean) || [];
const documents = initialDocuments.map((edge) => edge.node).filter(Boolean);
usePageTitle(__("Documents"));
@@ -102,22 +49,28 @@ export default function EmployeeDocumentsPage(props: Props) {
<div className="space-y-6">
<PageHeader title={__("Documents")} />
{documents.length > 0 ? (
<SortableTable {...pagination}>
<Thead>
<Tr>
<Th className="min-w-0 pr-12">{__("Name")}</Th>
<Th className="w-48">{__("Type")}</Th>
<Th className="w-36">{__("Classification")}</Th>
<Th className="w-40">{__("Last update")}</Th>
<Th className="w-32">{__("Signed")}</Th>
</Tr>
</Thead>
<Tbody>
{documents.map((document) => (
<DocumentRow key={document.id} document={document} organizationId={organizationId} />
))}
</Tbody>
</SortableTable>
<Card>
<table className="w-full">
<Thead>
<Tr>
<Th className="min-w-0 pr-12">{__("Name")}</Th>
<Th className="w-48">{__("Type")}</Th>
<Th className="w-36">{__("Classification")}</Th>
<Th className="w-40">{__("Last update")}</Th>
<Th className="w-32">{__("Signed")}</Th>
</Tr>
</Thead>
<Tbody>
{documents.map((document) => (
<DocumentRow
key={document.id}
fKey={document}
organizationId={organizationId}
/>
))}
</Tbody>
</table>
</Card>
) : (
<Card padded>
<div className="text-center py-12">
@@ -133,48 +86,3 @@ export default function EmployeeDocumentsPage(props: Props) {
</div>
);
}
const rowFragment = graphql`
fragment EmployeeDocumentsPageRowFragment on SignableDocument {
id
title
documentType
classification
signed
updatedAt
}
`;
function DocumentRow({
document: documentKey,
organizationId,
}: {
document: EmployeeDocumentsPageRowFragment$key;
organizationId: string;
}) {
const document = useFragment<EmployeeDocumentsPageRowFragment$key>(
rowFragment,
documentKey
);
const { __ } = useTranslate();
return (
<Tr
to={`/organizations/${organizationId}/employee/${document.id}`}
>
<Td className="min-w-0 pr-12">{document.title}</Td>
<Td className="w-48">{getDocumentTypeLabel(__, document.documentType)}</Td>
<Td className="w-36">
<Badge variant="neutral">
{getDocumentClassificationLabel(__, document.classification)}
</Badge>
</Td>
<Td className="w-40">{formatDate(document.updatedAt)}</Td>
<Td className="w-32">
<Badge variant={document.signed ? "success" : "danger"}>
{document.signed ? __("Yes") : __("No")}
</Badge>
</Td>
</Tr>
);
}

View File

@@ -0,0 +1,41 @@
import { useQueryLoader } from "react-relay";
import { Suspense, useEffect } from "react";
import { PageSkeleton } from "/components/skeletons/PageSkeleton";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { EmployeeDocumentsPageQuery } from "./__generated__/EmployeeDocumentsPageQuery.graphql";
import {
EmployeeDocumentsPage,
employeeDocumentsPageQuery,
} from "./EmployeeDocumentsPage";
import { CoreRelayProvider } from "/providers/CoreRelayProvider";
function EmployeeDocumentsPageLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<EmployeeDocumentsPageQuery>(
employeeDocumentsPageQuery
);
useEffect(() => {
loadQuery({
organizationId,
});
}, [loadQuery, organizationId]);
if (!queryRef) {
return <PageSkeleton />;
}
return (
<Suspense fallback={<PageSkeleton />}>
<EmployeeDocumentsPage queryRef={queryRef} />
</Suspense>
);
}
export default function () {
return (
<CoreRelayProvider>
<EmployeeDocumentsPageLoader />
</CoreRelayProvider>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<29c63d1f06c36d3670b5a4757b725b95>>
* @generated SignedSource<<30cf9ee1e00a276d2719e8d7d5ea0949>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -18,7 +18,7 @@ export type EmployeeDocumentSignaturePageDocumentFragment$data = {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"EmployeeDocumentSignaturePageVersionFragment">;
readonly " $fragmentSpreads": FragmentRefs<"VersionActionsFragment" | "VersionRowFragment">;
};
}>;
};
@@ -59,57 +59,74 @@ return {
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
"kind": "RequiredField",
"field": {
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
}
}
}
],
"concreteType": "DocumentVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "DocumentVersionEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
],
"concreteType": "DocumentVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "DocumentVersion",
"concreteType": "DocumentVersionEdge",
"kind": "LinkedField",
"name": "node",
"plural": false,
"name": "edges",
"plural": true,
"selections": [
(v0/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "EmployeeDocumentSignaturePageVersionFragment"
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "DocumentVersion",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "VersionActionsFragment"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "VersionRowFragment"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:100,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
},
"action": "THROW"
}
],
"storageKey": "versions(first:100,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
},
"action": "THROW"
}
],
"type": "SignableDocument",
@@ -117,6 +134,6 @@ return {
};
})();
(node as any).hash = "a516f97725320f4fe0282d70cef83a62";
(node as any).hash = "aaa20cc1293a2b1ce5d760631c30804c";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9e8c0459987993bb4b217618a46fdbf5>>
* @generated SignedSource<<92e104374ca160bd4ce96fe2c60aecc6>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,7 +15,6 @@ export type EmployeeDocumentSignaturePageQuery$variables = {
};
export type EmployeeDocumentSignaturePageQuery$data = {
readonly viewer: {
readonly id: string;
readonly signableDocument: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"EmployeeDocumentSignaturePageDocumentFragment">;
@@ -35,20 +34,20 @@ var v0 = [
"name": "documentId"
}
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v2 = [
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "documentId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
@@ -64,33 +63,36 @@ return {
"name": "EmployeeDocumentSignaturePageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "SignableDocument",
"kind": "LinkedField",
"name": "signableDocument",
"plural": false,
"selections": [
(v1/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "EmployeeDocumentSignaturePageDocumentFragment"
}
],
"storageKey": null
}
],
"storageKey": null
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "SignableDocument",
"kind": "LinkedField",
"name": "signableDocument",
"plural": false,
"selections": [
(v2/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "EmployeeDocumentSignaturePageDocumentFragment"
}
],
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
@@ -110,16 +112,15 @@ return {
"name": "viewer",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": (v2/*: any*/),
"args": (v1/*: any*/),
"concreteType": "SignableDocument",
"kind": "LinkedField",
"name": "signableDocument",
"plural": false,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
{
"alias": null,
"args": null,
@@ -166,7 +167,8 @@ return {
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
@@ -174,7 +176,6 @@ return {
"name": "version",
"storageKey": null
},
(v3/*: any*/),
{
"alias": null,
"args": null,
@@ -193,23 +194,24 @@ return {
}
],
"storageKey": null
}
},
(v2/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "a4866db288e1bf46e6562aa06bff4231",
"cacheID": "6ae29e4a10d7e0eb4afa03eef60d6762",
"id": null,
"metadata": {},
"name": "EmployeeDocumentSignaturePageQuery",
"operationKind": "query",
"text": "query EmployeeDocumentSignaturePageQuery(\n $documentId: ID!\n) {\n viewer {\n id\n signableDocument(id: $documentId) {\n id\n ...EmployeeDocumentSignaturePageDocumentFragment\n }\n }\n}\n\nfragment EmployeeDocumentSignaturePageDocumentFragment on SignableDocument {\n id\n title\n signed\n versions(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n ...EmployeeDocumentSignaturePageVersionFragment\n }\n }\n }\n}\n\nfragment EmployeeDocumentSignaturePageVersionFragment on DocumentVersion {\n id\n version\n signed\n publishedAt\n}\n"
"text": "query EmployeeDocumentSignaturePageQuery(\n $documentId: ID!\n) {\n viewer {\n signableDocument(id: $documentId) {\n id\n ...EmployeeDocumentSignaturePageDocumentFragment\n }\n id\n }\n}\n\nfragment EmployeeDocumentSignaturePageDocumentFragment on SignableDocument {\n id\n title\n signed\n versions(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n ...VersionActionsFragment\n ...VersionRowFragment\n }\n }\n }\n}\n\nfragment VersionActionsFragment on DocumentVersion {\n id\n signed\n}\n\nfragment VersionRowFragment on DocumentVersion {\n id\n version\n signed\n publishedAt\n}\n"
}
};
})();
(node as any).hash = "a2ac190853e8f078ff90213605a66e29";
(node as any).hash = "4b2db4d00f02f0826166b729e61e4e5d";
export default node;

View File

@@ -1,334 +0,0 @@
/**
* @generated SignedSource<<6caec1429cd03b026975c0e9ef7c76f6>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DocumentOrderField = "CREATED_AT" | "DOCUMENT_TYPE" | "TITLE";
export type OrderDirection = "ASC" | "DESC";
export type DocumentOrder = {
direction: OrderDirection;
field: DocumentOrderField;
};
export type EmployeeDocumentsListQuery$variables = {
after?: any | null | undefined;
before?: any | null | undefined;
first?: number | null | undefined;
last?: number | null | undefined;
order?: DocumentOrder | null | undefined;
organizationId: string;
};
export type EmployeeDocumentsListQuery$data = {
readonly viewer: {
readonly " $fragmentSpreads": FragmentRefs<"EmployeeDocumentsPageListFragment">;
};
};
export type EmployeeDocumentsListQuery = {
response: EmployeeDocumentsListQuery$data;
variables: EmployeeDocumentsListQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
{
"defaultValue": 50,
"kind": "LocalArgument",
"name": "first"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
{
"defaultValue": {
"direction": "DESC",
"field": "CREATED_AT"
},
"kind": "LocalArgument",
"name": "order"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = {
"kind": "Variable",
"name": "after",
"variableName": "after"
},
v2 = {
"kind": "Variable",
"name": "before",
"variableName": "before"
},
v3 = {
"kind": "Variable",
"name": "first",
"variableName": "first"
},
v4 = {
"kind": "Variable",
"name": "last",
"variableName": "last"
},
v5 = {
"kind": "Variable",
"name": "organizationId",
"variableName": "organizationId"
},
v6 = [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
},
(v5/*: any*/)
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EmployeeDocumentsListQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"args": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
{
"kind": "Variable",
"name": "order",
"variableName": "order"
},
(v5/*: any*/)
],
"kind": "FragmentSpread",
"name": "EmployeeDocumentsPageListFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EmployeeDocumentsListQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"concreteType": "SignableDocumentConnection",
"kind": "LinkedField",
"name": "signableDocuments",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SignableDocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SignableDocument",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "classification",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "signed",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
},
{
"alias": null,
"args": (v6/*: any*/),
"filters": [
"organizationId",
"orderBy"
],
"handle": "connection",
"key": "EmployeeDocumentsListQuery_signableDocuments",
"kind": "LinkedHandle",
"name": "signableDocuments"
},
(v7/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "32e793fba0e3d2e46857cd1b8c436ba2",
"id": null,
"metadata": {},
"name": "EmployeeDocumentsListQuery",
"operationKind": "query",
"text": "query EmployeeDocumentsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: DocumentOrder = {field: CREATED_AT, direction: DESC}\n $organizationId: ID!\n) {\n viewer {\n ...EmployeeDocumentsPageListFragment_KjvVI\n id\n }\n}\n\nfragment EmployeeDocumentsPageListFragment_KjvVI on Viewer {\n signableDocuments(organizationId: $organizationId, first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n ...EmployeeDocumentsPageRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment EmployeeDocumentsPageRowFragment on SignableDocument {\n id\n title\n documentType\n classification\n signed\n updatedAt\n}\n"
}
};
})();
(node as any).hash = "ebd2703f79cdf6900b5e42fc3b28932a";
export default node;

View File

@@ -1,231 +0,0 @@
/**
* @generated SignedSource<<47bb54b1ca3acc8736d619a74455a9c3>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type EmployeeDocumentsPageListFragment$data = {
readonly signableDocuments: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"EmployeeDocumentsPageRowFragment">;
};
}>;
};
readonly " $fragmentType": "EmployeeDocumentsPageListFragment";
};
export type EmployeeDocumentsPageListFragment$key = {
readonly " $data"?: EmployeeDocumentsPageListFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"EmployeeDocumentsPageListFragment">;
};
import EmployeeDocumentsListQuery_graphql from './EmployeeDocumentsListQuery.graphql';
const node: ReaderFragment = (function(){
var v0 = [
"signableDocuments"
];
return {
"argumentDefinitions": [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
{
"defaultValue": 50,
"kind": "LocalArgument",
"name": "first"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
{
"defaultValue": {
"direction": "DESC",
"field": "CREATED_AT"
},
"kind": "LocalArgument",
"name": "order"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "bidirectional",
"path": (v0/*: any*/)
}
],
"refetch": {
"connection": {
"forward": {
"count": "first",
"cursor": "after"
},
"backward": {
"count": "last",
"cursor": "before"
},
"path": (v0/*: any*/)
},
"fragmentPathInResult": [
"viewer"
],
"operation": EmployeeDocumentsListQuery_graphql
}
},
"name": "EmployeeDocumentsPageListFragment",
"selections": [
{
"alias": "signableDocuments",
"args": [
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
},
{
"kind": "Variable",
"name": "organizationId",
"variableName": "organizationId"
}
],
"concreteType": "SignableDocumentConnection",
"kind": "LinkedField",
"name": "__EmployeeDocumentsListQuery_signableDocuments_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SignableDocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SignableDocument",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "EmployeeDocumentsPageRowFragment"
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
}
],
"type": "Viewer",
"abstractKey": null
};
})();
(node as any).hash = "ebd2703f79cdf6900b5e42fc3b28932a";
export default node;

View File

@@ -1,272 +0,0 @@
/**
* @generated SignedSource<<fef5f504f43186b294a5acb2b063d1d1>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type EmployeeDocumentsPageListQuery$variables = {
organizationId: string;
};
export type EmployeeDocumentsPageListQuery$data = {
readonly viewer: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"EmployeeDocumentsPageListFragment">;
};
};
export type EmployeeDocumentsPageListQuery = {
response: EmployeeDocumentsPageListQuery$data;
variables: EmployeeDocumentsPageListQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v2 = {
"kind": "Variable",
"name": "organizationId",
"variableName": "organizationId"
},
v3 = [
{
"kind": "Literal",
"name": "first",
"value": 50
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
}
},
(v2/*: any*/)
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EmployeeDocumentsPageListQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v1/*: any*/),
{
"args": [
(v2/*: any*/)
],
"kind": "FragmentSpread",
"name": "EmployeeDocumentsPageListFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EmployeeDocumentsPageListQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": (v3/*: any*/),
"concreteType": "SignableDocumentConnection",
"kind": "LinkedField",
"name": "signableDocuments",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SignableDocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SignableDocument",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "classification",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "signed",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
},
{
"alias": null,
"args": (v3/*: any*/),
"filters": [
"organizationId",
"orderBy"
],
"handle": "connection",
"key": "EmployeeDocumentsListQuery_signableDocuments",
"kind": "LinkedHandle",
"name": "signableDocuments"
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "e0d165b36ce3e65b6c3e7a4d3620ba7e",
"id": null,
"metadata": {},
"name": "EmployeeDocumentsPageListQuery",
"operationKind": "query",
"text": "query EmployeeDocumentsPageListQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n ...EmployeeDocumentsPageListFragment_4xMPKw\n }\n}\n\nfragment EmployeeDocumentsPageListFragment_4xMPKw on Viewer {\n signableDocuments(organizationId: $organizationId, first: 50, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n ...EmployeeDocumentsPageRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment EmployeeDocumentsPageRowFragment on SignableDocument {\n id\n title\n documentType\n classification\n signed\n updatedAt\n}\n"
}
};
})();
(node as any).hash = "8c281fd3823eb1894c0b46807e04e370";
export default node;

View File

@@ -0,0 +1,247 @@
/**
* @generated SignedSource<<7bf19a133f9a98ea87492298d98312fa>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type EmployeeDocumentsPageQuery$variables = {
organizationId: string;
};
export type EmployeeDocumentsPageQuery$data = {
readonly viewer: {
readonly signableDocuments: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"DocumentRowFragment">;
};
}>;
};
};
};
export type EmployeeDocumentsPageQuery = {
response: EmployeeDocumentsPageQuery$data;
variables: EmployeeDocumentsPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Literal",
"name": "first",
"value": 1000
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
}
},
{
"kind": "Variable",
"name": "organizationId",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EmployeeDocumentsPageQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": (v1/*: any*/),
"concreteType": "SignableDocumentConnection",
"kind": "LinkedField",
"name": "signableDocuments",
"plural": false,
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "SignableDocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "SignableDocument",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "DocumentRowFragment"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EmployeeDocumentsPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "SignableDocumentConnection",
"kind": "LinkedField",
"name": "signableDocuments",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SignableDocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SignableDocument",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "classification",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "signed",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
(v2/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "6b86b68f881b9cce5eb16c3d02efba06",
"id": null,
"metadata": {},
"name": "EmployeeDocumentsPageQuery",
"operationKind": "query",
"text": "query EmployeeDocumentsPageQuery(\n $organizationId: ID!\n) {\n viewer {\n signableDocuments(organizationId: $organizationId, first: 1000, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n ...DocumentRowFragment\n }\n }\n }\n id\n }\n}\n\nfragment DocumentRowFragment on SignableDocument {\n id\n title\n documentType\n classification\n signed\n updatedAt\n}\n"
}
};
})();
(node as any).hash = "df39499ecf83f117576ac2456aeb094a";
export default node;

View File

@@ -0,0 +1,52 @@
import { useTranslate } from "@probo/i18n";
import { Tr, Td, Badge } from "@probo/ui";
import { useFragment } from "react-relay";
import { graphql } from "react-relay";
import {
getDocumentClassificationLabel,
getDocumentTypeLabel,
formatDate,
} from "@probo/helpers";
import type { DocumentRowFragment$key } from "./__generated__/DocumentRowFragment.graphql";
const fragment = graphql`
fragment DocumentRowFragment on SignableDocument {
id
title
documentType
classification
signed
updatedAt
}
`;
export function DocumentRow({
fKey,
organizationId,
}: {
fKey: DocumentRowFragment$key;
organizationId: string;
}) {
const document = useFragment<DocumentRowFragment$key>(fragment, fKey);
const { __ } = useTranslate();
return (
<Tr to={`/organizations/${organizationId}/employee/${document.id}`}>
<Td className="min-w-0 pr-12">{document.title}</Td>
<Td className="w-48">
{getDocumentTypeLabel(__, document.documentType)}
</Td>
<Td className="w-36">
<Badge variant="neutral">
{getDocumentClassificationLabel(__, document.classification)}
</Badge>
</Td>
<Td className="w-40">{formatDate(document.updatedAt)}</Td>
<Td className="w-32">
<Badge variant={document.signed ? "success" : "danger"}>
{document.signed ? __("Yes") : __("No")}
</Badge>
</Td>
</Tr>
);
}

View File

@@ -0,0 +1,57 @@
import { useTranslate } from "@probo/i18n";
import { Button, Spinner } from "@probo/ui";
import { useFragment } from "react-relay";
import { graphql } from "react-relay";
import type { VersionActionsFragment$key } from "./__generated__/VersionActionsFragment.graphql";
const fragment = graphql`
fragment VersionActionsFragment on DocumentVersion {
id
signed
}
`;
export function VersionActions({
fKey,
isSigning,
onSign,
onBack,
}: {
fKey: VersionActionsFragment$key;
isSigning: boolean;
onSign: (versionId: string) => void;
onBack: () => void;
}) {
const { __ } = useTranslate();
const versionData = useFragment<VersionActionsFragment$key>(fragment, fKey);
const isSigned = versionData.signed;
if (isSigned) {
return (
<>
<Button onClick={onBack} className="h-10 w-full" variant="secondary">
{__("Back to Documents")}
</Button>
<p className="text-xs text-txt-tertiary mt-2 h-5" />
</>
);
}
return (
<>
<Button
onClick={() => onSign(versionData.id)}
className="h-10 w-full"
disabled={isSigning}
icon={isSigning ? Spinner : undefined}
>
{__("I acknowledge and agree")}
</Button>
<p className="text-xs text-txt-tertiary mt-2 h-5">
{__(
"By clicking 'I acknowledge and agree', your digital signature will be recorded."
)}
</p>
</>
);
}

View File

@@ -0,0 +1,85 @@
import { useTranslate } from "@probo/i18n";
import { IconCircleCheck, IconRadioUnchecked } from "@probo/ui";
import clsx from "clsx";
import { useFragment } from "react-relay";
import { graphql } from "react-relay";
import type { VersionRowFragment$key } from "./__generated__/VersionRowFragment.graphql";
const fragment = graphql`
fragment VersionRowFragment on DocumentVersion {
id
version
signed
publishedAt
}
`;
export function VersionRow({
fKey,
isSelected,
onSelect,
}: {
fKey: VersionRowFragment$key;
isSelected: boolean;
onSelect: () => void;
}) {
const { __ } = useTranslate();
const versionData = useFragment<VersionRowFragment$key>(fragment, fKey);
const isVersionSigned = versionData.signed;
return (
<div
onClick={onSelect}
className={clsx(
"flex items-center gap-3 py-3 px-4 transition-colors cursor-pointer",
isSelected
? "bg-blue-50 border-l-4 border-blue-500"
: "bg-transparent hover:bg-level-1"
)}
>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-level-2 flex-shrink-0">
{isVersionSigned ? (
<IconCircleCheck size={20} className="text-txt-success" />
) : (
<IconRadioUnchecked size={20} className="text-txt-tertiary" />
)}
</div>
<div className="flex-1 min-w-0">
<p
className={clsx(
"text-sm font-medium truncate",
isVersionSigned ? "text-txt-tertiary" : "text-txt-primary"
)}
>
{versionData.publishedAt
? `v${versionData.version} - ${(() => {
const date = new Date(versionData.publishedAt);
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
return `${day}/${month}/${year}`;
})()}`
: `v${versionData.version}`}
</p>
</div>
<div className="flex-shrink-0">
<span
className={clsx(
"inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium",
isVersionSigned
? "bg-green-100 text-green-800"
: isSelected
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-700"
)}
>
{isVersionSigned
? __("Signed")
: isSelected
? __("In review")
: __("Waiting signature")}
</span>
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<f3d939f80d769a19b8f8ef64207ae911>>
* @generated SignedSource<<74a0df62fb3bfbd741d69cc1b7c023bb>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,25 +12,25 @@ import { ReaderFragment } from 'relay-runtime';
export type DocumentClassification = "CONFIDENTIAL" | "INTERNAL" | "PUBLIC" | "SECRET";
export type DocumentType = "ISMS" | "OTHER" | "POLICY" | "PROCEDURE";
import { FragmentRefs } from "relay-runtime";
export type EmployeeDocumentsPageRowFragment$data = {
export type DocumentRowFragment$data = {
readonly classification: DocumentClassification;
readonly documentType: DocumentType;
readonly id: string;
readonly signed: boolean;
readonly title: string;
readonly updatedAt: any;
readonly " $fragmentType": "EmployeeDocumentsPageRowFragment";
readonly " $fragmentType": "DocumentRowFragment";
};
export type EmployeeDocumentsPageRowFragment$key = {
readonly " $data"?: EmployeeDocumentsPageRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"EmployeeDocumentsPageRowFragment">;
export type DocumentRowFragment$key = {
readonly " $data"?: DocumentRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DocumentRowFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "EmployeeDocumentsPageRowFragment",
"name": "DocumentRowFragment",
"selections": [
{
"alias": null,
@@ -79,6 +79,6 @@ const node: ReaderFragment = {
"abstractKey": null
};
(node as any).hash = "929301e6f6216fb0678b32061b70dd17";
(node as any).hash = "43959adf3e4545cce78ba06f34c82cd2";
export default node;

View File

@@ -0,0 +1,50 @@
/**
* @generated SignedSource<<f777a3e905bf0a68614d31011688f76d>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type VersionActionsFragment$data = {
readonly id: string;
readonly signed: boolean;
readonly " $fragmentType": "VersionActionsFragment";
};
export type VersionActionsFragment$key = {
readonly " $data"?: VersionActionsFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"VersionActionsFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "VersionActionsFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "signed",
"storageKey": null
}
],
"type": "DocumentVersion",
"abstractKey": null
};
(node as any).hash = "9d3f7cc67712ad429dc0b1072fe2fe97";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<f05da6a037d3f282ff9ff6d60e2d684d>>
* @generated SignedSource<<9cf1f475999548049b019e29f83411dd>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,23 +10,23 @@
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type EmployeeDocumentSignaturePageVersionFragment$data = {
export type VersionRowFragment$data = {
readonly id: string;
readonly publishedAt: any | null | undefined;
readonly signed: boolean;
readonly version: number;
readonly " $fragmentType": "EmployeeDocumentSignaturePageVersionFragment";
readonly " $fragmentType": "VersionRowFragment";
};
export type EmployeeDocumentSignaturePageVersionFragment$key = {
readonly " $data"?: EmployeeDocumentSignaturePageVersionFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"EmployeeDocumentSignaturePageVersionFragment">;
export type VersionRowFragment$key = {
readonly " $data"?: VersionRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"VersionRowFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "EmployeeDocumentSignaturePageVersionFragment",
"name": "VersionRowFragment",
"selections": [
{
"alias": null,
@@ -61,6 +61,6 @@ const node: ReaderFragment = {
"abstractKey": null
};
(node as any).hash = "4a85fbbc1bf8b2610f554aa439fd0e95";
(node as any).hash = "421d770febb7c38b46562e03ecd95a41";
export default node;

View File

@@ -4,10 +4,9 @@ import {
redirect,
useRouteError,
} from "react-router";
import { EmployeeLayout } from "./layouts/EmployeeLayout";
import { EmployeeLayout } from "./layouts/EmployeeLayout.tsx";
import { CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui";
import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx";
import { loadQuery } from "react-relay";
import { riskRoutes } from "./routes/riskRoutes.ts";
import { measureRoutes } from "./routes/measureRoutes.ts";
import { documentsRoutes } from "./routes/documentsRoutes.ts";
@@ -30,18 +29,10 @@ import { processingActivityRoutes } from "./routes/processingActivityRoutes.ts";
import { statesOfApplicabilityRoutes } from "./routes/statesOfApplicabilityRoutes.ts";
import { CoreRelayProvider } from "./providers/CoreRelayProvider.tsx";
import { lazy } from "@probo/react-lazy";
import {
loaderFromQueryLoader,
routeFromAppRoute,
withQueryRef,
type AppRoute,
} from "@probo/routes";
import { employeeDocumentsQuery } from "./pages/organizations/employee/EmployeeDocumentsPage";
import { employeeDocumentSignatureQuery } from "./pages/organizations/employee/EmployeeDocumentSignaturePage";
import { routeFromAppRoute, type AppRoute } from "@probo/routes";
import { Role } from "@probo/helpers";
import { PermissionsContext } from "./providers/PermissionsContext";
import { use } from "react";
import { coreEnvironment } from "./environments.ts";
import {
ForbiddenError,
UnAuthenticatedError,
@@ -139,32 +130,17 @@ const routes = [
children: [
{
path: "",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ organizationId }) =>
loadQuery(coreEnvironment, employeeDocumentsQuery, {
organizationId: organizationId!,
})
),
Component: withQueryRef(
lazy(
() => import("./pages/organizations/employee/EmployeeDocumentsPage")
)
Component: lazy(
() =>
import("./pages/organizations/employee/EmployeeDocumentsPageLoader")
),
},
{
path: ":documentId",
Fallback: PageSkeleton,
ErrorBoundary: ErrorBoundary,
loader: loaderFromQueryLoader(({ documentId }) =>
loadQuery(coreEnvironment, employeeDocumentSignatureQuery, {
documentId: documentId!,
})
),
Component: withQueryRef(
lazy(
() =>
import("./pages/organizations/employee/EmployeeDocumentSignaturePage")
)
Component: lazy(
() =>
import("./pages/organizations/employee/EmployeeDocumentSignaturePageLoader")
),
},
],