diff --git a/apps/console/src/hooks/forms/useDocumentForm.tsx b/apps/console/src/hooks/forms/useDocumentForm.tsx index 813e9de77..21453280a 100644 --- a/apps/console/src/hooks/forms/useDocumentForm.tsx +++ b/apps/console/src/hooks/forms/useDocumentForm.tsx @@ -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"]), }); diff --git a/apps/console/src/pages/organizations/documents/DocumentsPage.tsx b/apps/console/src/pages/organizations/documents/DocumentsPage.tsx index 2cacfd042..6ee3ed22a 100644 --- a/apps/console/src/pages/organizations/documents/DocumentsPage.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentsPage.tsx @@ -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 }, + }, ), ); diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx index b63d02bef..962fcf2ec 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx @@ -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([]); const confirm = useConfirm(); + const [isPending, startTransition] = useTransition(); + const [documentTypeFilter, setDocumentTypeFilter] = useState(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 ( +
+
+ +
+
+ {documents.length > 0 ? (
- ); + )} +
+ + ); } diff --git a/apps/trust/src/helpers/documents.ts b/apps/trust/src/helpers/documents.ts index 9795ba288..454864778 100644 --- a/apps/trust/src/helpers/documents.ts +++ b/apps/trust/src/helpers/documents.ts @@ -2,8 +2,20 @@ export function documentTypeLabel(type: string, __: (s: string) => string) { switch (type) { case "POLICY": return __("Policy"); - case "ISMS": - return __("Security"); + case "GOVERNANCE": + return __("Governance"); + case "PROCEDURE": + return __("Procedure"); + case "PLAN": + return __("Plan"); + case "REGISTER": + return __("Register"); + case "RECORD": + return __("Record"); + case "REPORT": + return __("Report"); + case "TEMPLATE": + return __("Template"); default: return __("Other"); } diff --git a/e2e/console/document_test.go b/e2e/console/document_test.go index df1ca5c7e..469d89a89 100644 --- a/e2e/console/document_test.go +++ b/e2e/console/document_test.go @@ -72,15 +72,15 @@ func TestDocument_Create(t *testing.T) { assertValue: "PROCEDURE", }, { - name: "with ISMS type", + name: "with GOVERNANCE type", input: map[string]any{ - "title": "ISMS Document", - "content": "ISMS content", - "documentType": "ISMS", + "title": "Governance Document", + "content": "Governance content", + "documentType": "GOVERNANCE", "classification": "INTERNAL", }, assertField: "documentType", - assertValue: "ISMS", + assertValue: "GOVERNANCE", }, { name: "with OTHER type", diff --git a/packages/helpers/src/documents.ts b/packages/helpers/src/documents.ts index b997e3f0d..3c37f5a84 100644 --- a/packages/helpers/src/documents.ts +++ b/packages/helpers/src/documents.ts @@ -1,17 +1,27 @@ type Translator = (s: string) => string; -export const documentTypes = ["OTHER", "ISMS", "POLICY", "PROCEDURE"] as const; +export const documentTypes = ["OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"] as const; export function getDocumentTypeLabel(__: Translator, type: string) { switch (type) { case "OTHER": return __("Other"); - case "ISMS": - return __("ISMS"); + case "GOVERNANCE": + return __("Governance"); case "POLICY": return __("Policy"); case "PROCEDURE": return __("Procedure"); + case "PLAN": + return __("Plan"); + case "REGISTER": + return __("Register"); + case "RECORD": + return __("Record"); + case "REPORT": + return __("Report"); + case "TEMPLATE": + return __("Template"); } } diff --git a/pkg/coredata/document_filter.go b/pkg/coredata/document_filter.go index bd6fd46cd..b752b6eb1 100644 --- a/pkg/coredata/document_filter.go +++ b/pkg/coredata/document_filter.go @@ -25,6 +25,7 @@ type ( trustCenterVisibilities []TrustCenterVisibility published *bool userEmail *mail.Addr + documentTypes []DocumentType } ) @@ -55,6 +56,11 @@ func (f *DocumentFilter) WithUserEmail(userEmail *mail.Addr) *DocumentFilter { return f } +func (f *DocumentFilter) WithDocumentTypes(documentTypes []DocumentType) *DocumentFilter { + f.documentTypes = documentTypes + return f +} + func (f *DocumentFilter) SQLArguments() pgx.NamedArgs { var visibilities []string if f.trustCenterVisibilities != nil { @@ -63,11 +69,21 @@ func (f *DocumentFilter) SQLArguments() pgx.NamedArgs { visibilities[i] = v.String() } } + + var documentTypes []string + if f.documentTypes != nil { + documentTypes = make([]string, len(f.documentTypes)) + for i, dt := range f.documentTypes { + documentTypes[i] = dt.String() + } + } + return pgx.NamedArgs{ "query": f.query, "trust_center_visibilities": visibilities, "published": f.published, "user_email": f.userEmail, + "document_types": documentTypes, } } @@ -109,5 +125,11 @@ func (f *DocumentFilter) SQLFragment() string { AND dvs.state IN ('REQUESTED', 'SIGNED') ) END + AND + CASE + WHEN @document_types::document_type[] IS NOT NULL THEN + document_type = ANY(@document_types::document_type[]) + ELSE TRUE + END )` } diff --git a/pkg/coredata/document_type.go b/pkg/coredata/document_type.go index 6974d2013..bf67e72e4 100644 --- a/pkg/coredata/document_type.go +++ b/pkg/coredata/document_type.go @@ -24,18 +24,28 @@ type ( ) const ( - DocumentTypeOther DocumentType = "OTHER" - DocumentTypeISMS DocumentType = "ISMS" - DocumentTypePolicy DocumentType = "POLICY" - DocumentTypeProcedure DocumentType = "PROCEDURE" + DocumentTypeOther DocumentType = "OTHER" + DocumentTypeGovernance DocumentType = "GOVERNANCE" + DocumentTypePolicy DocumentType = "POLICY" + DocumentTypeProcedure DocumentType = "PROCEDURE" + DocumentTypePlan DocumentType = "PLAN" + DocumentTypeRegister DocumentType = "REGISTER" + DocumentTypeRecord DocumentType = "RECORD" + DocumentTypeReport DocumentType = "REPORT" + DocumentTypeTemplate DocumentType = "TEMPLATE" ) func DocumentTypes() []DocumentType { return []DocumentType{ DocumentTypeOther, - DocumentTypeISMS, + DocumentTypeGovernance, DocumentTypePolicy, DocumentTypeProcedure, + DocumentTypePlan, + DocumentTypeRegister, + DocumentTypeRecord, + DocumentTypeReport, + DocumentTypeTemplate, } } @@ -49,12 +59,22 @@ func (dt *DocumentType) UnmarshalText(data []byte) error { switch val { case DocumentTypeOther.String(): *dt = DocumentTypeOther - case DocumentTypeISMS.String(): - *dt = DocumentTypeISMS + case DocumentTypeGovernance.String(): + *dt = DocumentTypeGovernance case DocumentTypePolicy.String(): *dt = DocumentTypePolicy case DocumentTypeProcedure.String(): *dt = DocumentTypeProcedure + case DocumentTypePlan.String(): + *dt = DocumentTypePlan + case DocumentTypeRegister.String(): + *dt = DocumentTypeRegister + case DocumentTypeRecord.String(): + *dt = DocumentTypeRecord + case DocumentTypeReport.String(): + *dt = DocumentTypeReport + case DocumentTypeTemplate.String(): + *dt = DocumentTypeTemplate default: return fmt.Errorf("invalid DocumentType value: %q", val) } diff --git a/pkg/coredata/migrations/20260319T130000Z.sql b/pkg/coredata/migrations/20260319T130000Z.sql new file mode 100644 index 000000000..938d436d9 --- /dev/null +++ b/pkg/coredata/migrations/20260319T130000Z.sql @@ -0,0 +1,6 @@ +ALTER TYPE document_type RENAME VALUE 'ISMS' TO 'GOVERNANCE'; +ALTER TYPE document_type ADD VALUE 'PLAN'; +ALTER TYPE document_type ADD VALUE 'REGISTER'; +ALTER TYPE document_type ADD VALUE 'RECORD'; +ALTER TYPE document_type ADD VALUE 'REPORT'; +ALTER TYPE document_type ADD VALUE 'TEMPLATE'; diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 3cac4142a..959381aa9 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -971,10 +971,18 @@ enum VendorCategory enum DocumentType @goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentType") { OTHER @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeOther") - ISMS @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeISMS") + GOVERNANCE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeGovernance") POLICY @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePolicy") PROCEDURE @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeProcedure") + PLAN @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePlan") + REGISTER + @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeRegister") + RECORD @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeRecord") + REPORT @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeReport") + TEMPLATE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeTemplate") } enum DocumentClassification @@ -1571,6 +1579,7 @@ input ControlFilter { input DocumentFilter { query: String + documentTypes: [DocumentType!] } input MeasureFilter { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index bcadf6005..921f0fff4 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -567,7 +567,8 @@ func (r *controlResolver) Documents(ctx context.Context, obj *types.Control, fir var documentFilter = coredata.NewDocumentFilter(nil) if filter != nil { - documentFilter = coredata.NewDocumentFilter(filter.Query) + documentFilter = coredata.NewDocumentFilter(filter.Query). + WithDocumentTypes(filter.DocumentTypes) } page, err := prb.Documents.ListForControlID(ctx, obj.ID, cursor, documentFilter) @@ -6477,7 +6478,8 @@ func (r *organizationResolver) Documents(ctx context.Context, obj *types.Organiz var documentFilter = coredata.NewDocumentFilter(nil) if filter != nil { - documentFilter = coredata.NewDocumentFilter(filter.Query) + documentFilter = coredata.NewDocumentFilter(filter.Query). + WithDocumentTypes(filter.DocumentTypes) } page, err := prb.Documents.ListByOrganizationID(ctx, obj.ID, cursor, documentFilter) @@ -7844,7 +7846,8 @@ func (r *riskResolver) Documents(ctx context.Context, obj *types.Risk, first *in var documentFilter = coredata.NewDocumentFilter(nil) if filter != nil { - documentFilter = coredata.NewDocumentFilter(filter.Query) + documentFilter = coredata.NewDocumentFilter(filter.Query). + WithDocumentTypes(filter.DocumentTypes) } page, err := prb.Documents.ListForRiskID(ctx, obj.ID, cursor, documentFilter) diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index c951920b3..e16d7ba6a 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -2019,7 +2019,8 @@ func (r *Resolver) ListDocumentsTool(ctx context.Context, req *mcp.CallToolReque query = input.Filter.Query } - documentFilter = coredata.NewDocumentFilter(query) + documentFilter = coredata.NewDocumentFilter(query). + WithDocumentTypes(input.Filter.DocumentTypes) } docPage, err := prb.Documents.ListByOrganizationID(ctx, input.OrganizationID, cursor, documentFilter) diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 1c929c7d0..b08bcbfb6 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -5038,9 +5038,14 @@ components: type: string enum: - OTHER - - ISMS + - GOVERNANCE - POLICY - PROCEDURE + - PLAN + - REGISTER + - RECORD + - REPORT + - TEMPLATE go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentType DocumentClassification: @@ -5314,6 +5319,11 @@ components: items: $ref: "#/components/schemas/TrustCenterVisibility" description: Trust center visibilities + document_types: + type: array + items: + $ref: "#/components/schemas/DocumentType" + description: Document types ListDocumentsOutput: type: object diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index f881d716c..d6bf676f8 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -52,10 +52,18 @@ type Organization implements Node { enum DocumentType @goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentType") { OTHER @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeOther") - ISMS @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeISMS") + GOVERNANCE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeGovernance") POLICY @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePolicy") PROCEDURE @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeProcedure") + PLAN @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePlan") + REGISTER + @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeRegister") + RECORD @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeRecord") + REPORT @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeReport") + TEMPLATE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeTemplate") } type Document implements Node @nda { diff --git a/pkg/trust/document_service.go b/pkg/trust/document_service.go index 823fa13a4..82bf7e4aa 100644 --- a/pkg/trust/document_service.go +++ b/pkg/trust/document_service.go @@ -177,7 +177,7 @@ func (s *DocumentService) exportPDFData( switch document.DocumentType { case coredata.DocumentTypePolicy: classification = docgen.ClassificationConfidential - case coredata.DocumentTypeISMS: + case coredata.DocumentTypeGovernance: classification = docgen.ClassificationSecret }