Move document type from document to document version
Follow the same pattern used for classification: document type now lives exclusively on DocumentVersion. A migration copies existing values from documents to their versions. The document filter uses a subquery on the latest version. All three API surfaces (GraphQL, MCP, CLI), resolvers, frontend, and e2e tests are updated accordingly. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -44,11 +44,11 @@ const linkedDocumentFragment = graphql`
|
||||
fragment LinkedDocumentsCardFragment on Document {
|
||||
id
|
||||
title
|
||||
documentType
|
||||
versions(first: 1) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
documentType
|
||||
status
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ function DocumentRow(props: {
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<DocumentTypeBadge type={document.documentType} />
|
||||
<DocumentTypeBadge type={document.versions.edges[0].node.documentType} />
|
||||
</Td>
|
||||
<Td>
|
||||
<DocumentVersionBadge state={document.versions.edges[0].node.status} />
|
||||
|
||||
@@ -72,7 +72,13 @@ const documentsFragment = graphql`
|
||||
node {
|
||||
id
|
||||
title
|
||||
documentType
|
||||
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
edges {
|
||||
node {
|
||||
documentType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +193,7 @@ function DocumentRow(props: RowProps) {
|
||||
onClick={() => onClick(props.document.id)}
|
||||
>
|
||||
{props.document.title}
|
||||
<DocumentTypeBadge type={props.document.documentType} />
|
||||
<DocumentTypeBadge type={props.document.versions.edges[0].node.documentType} />
|
||||
<Button
|
||||
disabled={props.disabled}
|
||||
className="ml-auto"
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import type { TrustCenterDocumentAccessStatus } from "@probo/coredata";
|
||||
import {
|
||||
getTrustCenterDocumentAccessInfo,
|
||||
type TrustCenterDocumentAccessInfo,
|
||||
} from "@probo/helpers";
|
||||
import type { TrustCenterDocumentAccessInfo } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
@@ -31,8 +28,9 @@ import {
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
} from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { graphql, readInlineData } from "relay-runtime";
|
||||
|
||||
import type { CompliancePageAccessEditDialogDocumentAccessFragment$data, CompliancePageAccessEditDialogDocumentAccessFragment$key } from "#/__generated__/core/CompliancePageAccessEditDialogDocumentAccessFragment.graphql";
|
||||
import type { CompliancePageAccessEditDialogQuery as CompliancePageAccessEditDialogQueryType } from "#/__generated__/core/CompliancePageAccessEditDialogQuery.graphql";
|
||||
import type { CompliancePageAccessEditDialogUpdateMutation } from "#/__generated__/core/CompliancePageAccessEditDialogUpdateMutation.graphql";
|
||||
import type { CompliancePageAccessListItemFragment$data } from "#/__generated__/core/CompliancePageAccessListItemFragment.graphql";
|
||||
@@ -40,6 +38,90 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
import { CompliancePageDocumentAccessList } from "#/pages/organizations/compliance-page/access/_components/CompliancePageDocumentAccessList";
|
||||
import { ElectronicSignatureSection } from "#/pages/organizations/compliance-page/access/_components/ElectronicSignatureSection";
|
||||
|
||||
const documentAccessFragment = graphql`
|
||||
fragment CompliancePageAccessEditDialogDocumentAccessFragment on TrustCenterDocumentAccess @inline {
|
||||
id
|
||||
status
|
||||
document {
|
||||
id
|
||||
title
|
||||
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
edges {
|
||||
node {
|
||||
documentType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
report {
|
||||
id
|
||||
filename
|
||||
audit {
|
||||
id
|
||||
framework {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
trustCenterFile {
|
||||
id
|
||||
name
|
||||
category
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function getTrustCenterDocumentAccessInfo(
|
||||
fragmentRef: CompliancePageAccessEditDialogDocumentAccessFragment$key,
|
||||
__: (key: string) => string,
|
||||
): TrustCenterDocumentAccessInfo {
|
||||
const node = readInlineData(documentAccessFragment, fragmentRef);
|
||||
return toDocumentAccessInfo(node, __);
|
||||
}
|
||||
|
||||
function toDocumentAccessInfo(
|
||||
node: CompliancePageAccessEditDialogDocumentAccessFragment$data,
|
||||
__: (key: string) => string,
|
||||
): TrustCenterDocumentAccessInfo {
|
||||
if (node.document) {
|
||||
return {
|
||||
persisted: node.id !== node.document.id,
|
||||
variant: "info",
|
||||
name: node.document.title,
|
||||
type: "document",
|
||||
typeLabel: __("Document"),
|
||||
category: node.document.versions?.edges[0]?.node.documentType ?? "",
|
||||
id: node.document.id,
|
||||
status: node.status,
|
||||
};
|
||||
}
|
||||
if (node.report) {
|
||||
return {
|
||||
persisted: node.id !== node.report.id,
|
||||
variant: "success",
|
||||
name: node.report.filename,
|
||||
type: "report",
|
||||
typeLabel: __("Report"),
|
||||
category: node.report.audit?.framework?.name ?? "",
|
||||
id: node.report.id,
|
||||
status: node.status,
|
||||
};
|
||||
}
|
||||
if (node.trustCenterFile) {
|
||||
return {
|
||||
persisted: node.id !== node.trustCenterFile.id,
|
||||
variant: "highlight",
|
||||
name: node.trustCenterFile.name,
|
||||
type: "file",
|
||||
typeLabel: __("File"),
|
||||
category: node.trustCenterFile.category,
|
||||
id: node.trustCenterFile.id,
|
||||
status: node.status,
|
||||
};
|
||||
}
|
||||
throw new Error("Unknown trust center access document type");
|
||||
}
|
||||
|
||||
const compliancePageAccessEditDialogQuery = graphql`
|
||||
query CompliancePageAccessEditDialogQuery($accessId: ID!) {
|
||||
node(id: $accessId) {
|
||||
@@ -54,38 +136,7 @@ const compliancePageAccessEditDialogQuery = graphql`
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
status
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
document {
|
||||
id
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
title
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
documentType
|
||||
}
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
report {
|
||||
id
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
filename
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
audit {
|
||||
id
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
framework {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
trustCenterFile {
|
||||
id
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
name
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
category
|
||||
}
|
||||
...CompliancePageAccessEditDialogDocumentAccessFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ const compliancePageFragment = graphql`
|
||||
const documentFragment = graphql`
|
||||
fragment CompliancePageDocumentListItem_documentFragment on Document {
|
||||
id
|
||||
documentType
|
||||
trustCenterVisibility
|
||||
latestPublishedVersion: versions(
|
||||
first: 1
|
||||
@@ -43,6 +42,7 @@ const documentFragment = graphql`
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
documentType
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,8 @@ export function CompliancePageDocumentListItem(props: {
|
||||
[document.id, updateDocumentVisibility],
|
||||
);
|
||||
|
||||
const versionTitle = document.latestPublishedVersion.edges[0]?.node.title;
|
||||
const latestVersion = document.latestPublishedVersion.edges[0]?.node;
|
||||
const versionTitle = latestVersion?.title;
|
||||
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
|
||||
@@ -110,7 +111,7 @@ export function CompliancePageDocumentListItem(props: {
|
||||
<div className="flex gap-4 items-center">{versionTitle}</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<DocumentTypeBadge type={document.documentType} />
|
||||
{latestVersion && <DocumentTypeBadge type={latestVersion.documentType} />}
|
||||
</Td>
|
||||
<Td noLink width={130} className="pr-0">
|
||||
<Field
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import { documentClassifications, documentTypes, formatDate, getDocumentClassificationLabel, getDocumentTypeLabel } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Badge, Button, Drawer, IconCheckmark1, IconCrossLargeX, IconPencil, PropertyRow } from "@probo/ui";
|
||||
import { Badge, Button, Drawer, IconCheckmark1, IconCrossLargeX, IconPencil, PropertyRow, useToast } from "@probo/ui";
|
||||
import { useState } from "react";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
@@ -31,7 +31,6 @@ import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
const documentFragment = graphql`
|
||||
fragment DocumentLayoutDrawer_documentFragment on Document {
|
||||
id
|
||||
documentType
|
||||
status
|
||||
archivedAt
|
||||
canUpdate: permission(action: "core:document:update")
|
||||
@@ -41,6 +40,7 @@ const documentFragment = graphql`
|
||||
const versionFragment = graphql`
|
||||
fragment DocumentLayoutDrawer_versionFragment on DocumentVersion {
|
||||
id
|
||||
documentType
|
||||
classification
|
||||
major
|
||||
minor
|
||||
@@ -50,10 +50,10 @@ const versionFragment = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const updateDocumentMutation = graphql`
|
||||
mutation DocumentLayoutDrawerMutation($input: UpdateDocumentInput!) {
|
||||
updateDocument(input: $input) {
|
||||
document {
|
||||
const updateDocumentTypeMutation = graphql`
|
||||
mutation DocumentLayoutDrawerMutation($input: UpdateDocumentVersionInput!) {
|
||||
updateDocumentVersion(input: $input) {
|
||||
documentVersion {
|
||||
id
|
||||
documentType
|
||||
}
|
||||
@@ -91,6 +91,7 @@ export function DocumentLayoutDrawer(props: {
|
||||
const [isEditingType, setIsEditingType] = useState(false);
|
||||
const [isEditingClassification, setIsEditingClassification] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
const document = useFragment<DocumentLayoutDrawer_documentFragment$key>(documentFragment, documentFragmentRef);
|
||||
const version = useFragment<DocumentLayoutDrawer_versionFragment$key>(versionFragment, versionFragmentRef);
|
||||
|
||||
@@ -101,7 +102,7 @@ export function DocumentLayoutDrawer(props: {
|
||||
schema,
|
||||
{
|
||||
defaultValues: {
|
||||
documentType: document.documentType,
|
||||
documentType: version.documentType,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -119,8 +120,8 @@ export function DocumentLayoutDrawer(props: {
|
||||
},
|
||||
);
|
||||
|
||||
const [updateDocument, isUpdatingDocument]
|
||||
= useMutation<DocumentLayoutDrawerMutation>(updateDocumentMutation);
|
||||
const [updateDocumentType, isUpdatingDocumentType]
|
||||
= useMutation<DocumentLayoutDrawerMutation>(updateDocumentTypeMutation);
|
||||
|
||||
const [updateClassification, isUpdatingClassification]
|
||||
= useMutation<DocumentLayoutDrawer_updateClassificationMutation>(updateClassificationMutation);
|
||||
@@ -128,15 +129,27 @@ export function DocumentLayoutDrawer(props: {
|
||||
const handleUpdateDocumentType = (data: {
|
||||
documentType: (typeof documentTypes)[number];
|
||||
}) => {
|
||||
updateDocument({
|
||||
updateDocumentType({
|
||||
variables: {
|
||||
input: {
|
||||
id: document.id,
|
||||
documentVersionId: version.id,
|
||||
documentType: data.documentType,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
setIsEditingType(false);
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Document type updated successfully"),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: __("Failed to update document type"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -153,6 +166,18 @@ export function DocumentLayoutDrawer(props: {
|
||||
},
|
||||
onCompleted: () => {
|
||||
setIsEditingClassification(false);
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Document classification updated successfully"),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: __("Failed to update document classification"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -171,7 +196,7 @@ export function DocumentLayoutDrawer(props: {
|
||||
setIsEditingType(false);
|
||||
reset();
|
||||
}}
|
||||
disabled={isUpdatingDocument}
|
||||
disabled={isUpdatingDocumentType}
|
||||
>
|
||||
<ControlledField
|
||||
name="documentType"
|
||||
@@ -185,10 +210,10 @@ export function DocumentLayoutDrawer(props: {
|
||||
: (
|
||||
<ReadOnlyPropertyContent
|
||||
onEdit={() => setIsEditingType(true)}
|
||||
canEdit={canEdit}
|
||||
canEdit={canEdit && isDraft}
|
||||
>
|
||||
<div className="text-sm text-txt-secondary">
|
||||
{getDocumentTypeLabel(__, document.documentType)}
|
||||
{getDocumentTypeLabel(__, version.documentType)}
|
||||
</div>
|
||||
</ReadOnlyPropertyContent>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,6 @@ const fragment = graphql`
|
||||
fragment DocumentListItemFragment on Document {
|
||||
id
|
||||
title
|
||||
documentType
|
||||
updatedAt
|
||||
canDelete: permission(action: "core:document:delete")
|
||||
recentVersions: versions(first: 2 orderBy: { field: CREATED_AT direction: DESC }) {
|
||||
@@ -36,6 +35,7 @@ const fragment = graphql`
|
||||
status
|
||||
major
|
||||
minor
|
||||
documentType
|
||||
classification
|
||||
approvalQuorums(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
edges {
|
||||
@@ -165,7 +165,7 @@ export function DocumentListItem(props: {
|
||||
{lastVersion.minor}
|
||||
</Td>
|
||||
<Td className="w-28">
|
||||
{getDocumentTypeLabel(__, document.documentType)}
|
||||
{getDocumentTypeLabel(__, lastVersion.documentType)}
|
||||
</Td>
|
||||
<Td className="w-32">
|
||||
{getDocumentClassificationLabel(__, lastVersion.classification)}
|
||||
|
||||
@@ -27,12 +27,12 @@ const fragment = graphql`
|
||||
fragment ApprovableDocumentRowFragment on EmployeeDocument {
|
||||
id
|
||||
title
|
||||
documentType
|
||||
approvalState
|
||||
updatedAt
|
||||
lastVersion: versions(first: 1 orderBy: { field: CREATED_AT direction: DESC }) {
|
||||
edges {
|
||||
node {
|
||||
documentType
|
||||
classification
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export function ApprovableDocumentRow({
|
||||
<Tr to={`/organizations/${organizationId}/employee/approvals/${document.id}`}>
|
||||
<Td>{document.title}</Td>
|
||||
<Td className="w-48">
|
||||
{getDocumentTypeLabel(__, document.documentType)}
|
||||
{getDocumentTypeLabel(__, lastVersion.documentType)}
|
||||
</Td>
|
||||
<Td className="w-36">
|
||||
<Badge variant="neutral">
|
||||
|
||||
@@ -27,12 +27,12 @@ const fragment = graphql`
|
||||
fragment DocumentRowFragment on EmployeeDocument {
|
||||
id
|
||||
title
|
||||
documentType
|
||||
signed
|
||||
updatedAt
|
||||
lastVersion: versions(first: 1 orderBy: { field: CREATED_AT direction: DESC }) {
|
||||
edges {
|
||||
node {
|
||||
documentType
|
||||
classification
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function DocumentRow({
|
||||
<Tr to={`/organizations/${organizationId}/employee/signatures/${document.id}`}>
|
||||
<Td>{document.title}</Td>
|
||||
<Td className="w-48">
|
||||
{getDocumentTypeLabel(__, document.documentType)}
|
||||
{getDocumentTypeLabel(__, lastVersion.documentType)}
|
||||
</Td>
|
||||
<Td className="w-36">
|
||||
<Badge variant="neutral">
|
||||
|
||||
@@ -64,8 +64,8 @@ const overviewFragment = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...DocumentRowFragment
|
||||
documentType
|
||||
...DocumentRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user