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:
Sacha Al Himdani
2026-03-31 19:57:52 +02:00
parent 28cf3f167a
commit 9a418a7711
29 changed files with 396 additions and 314 deletions

View File

@@ -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} />

View File

@@ -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"

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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>
)}

View File

@@ -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)}

View File

@@ -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">

View File

@@ -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">

View File

@@ -64,8 +64,8 @@ const overviewFragment = graphql`
edges {
node {
id
...DocumentRowFragment
documentType
...DocumentRowFragment
}
}
}