Add document types filtering and rename ISMS to GOVERNANCE

Adds 5 new document types (PLAN, REGISTER, RECORD, REPORT, TEMPLATE), renames ISMS to GOVERNANCE, and implements type-based filtering across GraphQL, MCP, and frontend. Includes migration, enum updates, filter implementation with SQL array support, and frontend dropdown UI with Relay refetch pattern.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-19 11:24:45 +01:00
parent 2b08dea600
commit 4f54241382
15 changed files with 178 additions and 37 deletions

View File

@@ -6,7 +6,7 @@ export const documentSchema = z.object({
title: z.string().min(1, "Title is required"),
content: z.string().min(1, "Content is required"),
approverIds: z.array(z.string()).min(1, "At least one approver is required"),
documentType: z.enum(["OTHER", "ISMS", "POLICY", "PROCEDURE"]),
documentType: z.enum(["OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"]),
classification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]),
});

View File

@@ -29,7 +29,7 @@ export const documentsPageQuery = graphql`
... on Organization {
canCreateDocument: permission(action: "core:document:create")
...DocumentListFragment @arguments(first: 50, order: { field: TITLE, direction: ASC })
documents(first: 50, orderBy: { field: TITLE, direction: ASC }) {
allDocuments: documents(first: 50, orderBy: { field: TITLE, direction: ASC }) {
edges {
node {
canSendSigningNotifications: permission(
@@ -63,7 +63,7 @@ export default function DocumentsPage(props: {
usePageTitle(__("Documents"));
const canSendAnySignatureNotifications = organization.documents.edges.some(
const canSendAnySignatureNotifications = organization.allDocuments.edges.some(
({ node: { canSendSigningNotifications } }) => canSendSigningNotifications,
);
@@ -71,7 +71,10 @@ export default function DocumentsPage(props: {
ConnectionHandler.getConnectionID(
organizationId,
"DocumentsListQuery_documents",
{ orderBy: { direction: "ASC", field: "TITLE" } },
{
orderBy: { direction: "ASC", field: "TITLE" },
filter: { documentTypes: null },
},
),
);

View File

@@ -1,13 +1,13 @@
import { sprintf } from "@probo/helpers";
import { documentTypes, getDocumentTypeLabel, sprintf } from "@probo/helpers";
import { useList } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Card, Checkbox, IconArrowDown, IconCheckmark1, IconCrossLargeX, IconSignature, IconTrashCan, Tbody, Th, Thead, Tr, useConfirm } from "@probo/ui";
import { type ComponentProps, use, useRef } from "react";
import { Button, Card, Checkbox, IconArrowDown, IconCheckmark1, IconCrossLargeX, IconSignature, IconTrashCan, Option, Select, Tbody, Th, Thead, Tr, useConfirm } from "@probo/ui";
import { type ComponentProps, use, useRef, useState, useTransition } from "react";
import { usePaginationFragment } from "react-relay";
import { ConnectionHandler, graphql } from "relay-runtime";
import type { DocumentListFragment$key } from "#/__generated__/core/DocumentListFragment.graphql";
import type { DocumentsListQuery } from "#/__generated__/core/DocumentsListQuery.graphql";
import type { DocumentsListQuery, DocumentType } from "#/__generated__/core/DocumentsListQuery.graphql";
import { BulkExportDialog, type BulkExportDialogRef } from "#/components/documents/BulkExportDialog";
import { type Order, SortableTable, SortableTh } from "#/components/SortableTable";
import { useBulkDeleteDocumentsMutation, useBulkExportDocumentsMutation } from "#/hooks/graph/DocumentGraph";
@@ -30,6 +30,7 @@ const fragment = graphql`
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
documentTypes: { type: "[DocumentType!]", defaultValue: null }
) {
documents(
first: $first
@@ -37,7 +38,8 @@ const fragment = graphql`
last: $last
before: $before
orderBy: $order
) @connection(key: "DocumentsListQuery_documents" filters: ["orderBy"]) {
filter: { documentTypes: $documentTypes }
) @connection(key: "DocumentsListQuery_documents" filters: ["orderBy", "filter"]) {
__id
edges {
node {
@@ -79,6 +81,19 @@ export function DocumentList(props: {
= useBulkExportDocumentsMutation();
const { list: selection, toggle, clear, reset } = useList<string>([]);
const confirm = useConfirm();
const [isPending, startTransition] = useTransition();
const [documentTypeFilter, setDocumentTypeFilter] = useState<DocumentType | null>(null);
const handleDocumentTypeFilterChange = (value: string) => {
const newType = value === "ALL" ? null : (value as DocumentType);
setDocumentTypeFilter(newType);
startTransition(() => {
pagination.refetch(
{ documentTypes: newType ? [newType] : null },
{ fetchPolicy: "network-only" },
);
});
};
const canDeleteAny = documents.some(({ canDelete }) => canDelete);
const canUpdateAny = documents.some(({ canUpdate }) => canUpdate);
@@ -134,12 +149,31 @@ export function DocumentList(props: {
ConnectionHandler.getConnectionID(
organizationId,
"DocumentsListQuery_documents",
{ orderBy: order },
{
orderBy: order,
filter: { documentTypes: documentTypeFilter ? [documentTypeFilter] : null },
},
),
);
};
return documents.length > 0
return (
<div className="space-y-4">
<div className="flex items-center gap-4">
<Select
value={documentTypeFilter ?? "ALL"}
onValueChange={handleDocumentTypeFilterChange}
>
<Option value="ALL">{__("All types")}</Option>
{documentTypes.map((type) => (
<Option key={type} value={type}>
{getDocumentTypeLabel(__, type) ?? type}
</Option>
))}
</Select>
</div>
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>
{documents.length > 0
? (
<SortableTable
{...pagination}
@@ -273,5 +307,8 @@ export function DocumentList(props: {
</p>
</div>
</Card>
);
)}
</div>
</div>
);
}