diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx index 6b8e60d9e..b4c16e9f4 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx @@ -12,7 +12,7 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -import { documentTypes, getDocumentTypeLabel, sprintf } from "@probo/helpers"; +import { documentClassifications, documentTypes, getDocumentClassificationLabel, getDocumentTypeLabel, sprintf } from "@probo/helpers"; import { useList } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; import { Button, Card, Checkbox, IconArchive, IconArrowDown, IconCrossLargeX, IconSignature, IconTrashCan, IconUpload, Option, Select, Tbody, Th, Thead, Tr, useConfirm } from "@probo/ui"; @@ -23,7 +23,7 @@ import { ConnectionHandler, graphql } from "relay-runtime"; import type { DocumentListBulkArchiveMutation } from "#/__generated__/core/DocumentListBulkArchiveMutation.graphql"; import type { DocumentListBulkUnarchiveMutation } from "#/__generated__/core/DocumentListBulkUnarchiveMutation.graphql"; import type { DocumentListFragment$key } from "#/__generated__/core/DocumentListFragment.graphql"; -import type { DocumentOrderField, DocumentsListQuery, DocumentType } from "#/__generated__/core/DocumentsListQuery.graphql"; +import type { DocumentClassification, DocumentOrderField, 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"; @@ -49,6 +49,7 @@ const fragment = graphql` last: { type: "Int", defaultValue: null } status: { type: "[DocumentStatus!]", defaultValue: [ACTIVE] } documentTypes: { type: "[DocumentType!]", defaultValue: null } + classifications: { type: "[DocumentClassification!]", defaultValue: null } ) { documents( first: $first @@ -56,7 +57,7 @@ const fragment = graphql` last: $last before: $before orderBy: $order - filter: { status: $status documentTypes: $documentTypes } + filter: { status: $status documentTypes: $documentTypes classifications: $classifications } ) @connection(key: "DocumentsListQuery_documents" filters: ["orderBy", "filter"]) { __id edges { @@ -128,17 +129,22 @@ export function DocumentList(props: { ); const [documentTypeFilter, setDocumentTypeFilter] = useState(null); + const [classificationFilter, setClassificationFilter] = useState(null); const [isPending, startTransition] = useTransition(); const refetch = pagination.refetch; useEffect(() => { startTransition(() => { refetch( - { status: [tab], documentTypes: documentTypeFilter ? [documentTypeFilter] : null }, + { + status: [tab], + documentTypes: documentTypeFilter ? [documentTypeFilter] : null, + classifications: classificationFilter ? [classificationFilter] : null, + }, { fetchPolicy: "store-and-network" }, ); }); - }, [tab, refetch, documentTypeFilter]); + }, [tab, refetch, documentTypeFilter, classificationFilter]); const documents = pagination.data.documents.edges.map(({ node }) => node); const connectionId = pagination.data.documents.__id; @@ -184,7 +190,31 @@ export function DocumentList(props: { "DocumentsListQuery_documents", { orderBy: { direction: "ASC", field: "TITLE" }, - filter: { status: [tab], documentTypes: newType ? [newType] : null }, + filter: { + status: [tab], + documentTypes: newType ? [newType] : null, + classifications: classificationFilter ? [classificationFilter] : null, + }, + }, + ), + ); + }; + + const handleClassificationFilterChange = (value: string) => { + const newClassification = value === "ALL" ? null : (value as DocumentClassification); + clear(); + setClassificationFilter(newClassification); + onConnectionIdChange( + ConnectionHandler.getConnectionID( + organizationId, + "DocumentsListQuery_documents", + { + orderBy: { direction: "ASC", field: "TITLE" }, + filter: { + status: [tab], + documentTypes: documentTypeFilter ? [documentTypeFilter] : null, + classifications: newClassification ? [newClassification] : null, + }, }, ), ); @@ -263,7 +293,11 @@ export function DocumentList(props: { "DocumentsListQuery_documents", { orderBy: order, - filter: { status: [tab], documentTypes: documentTypeFilter ? [documentTypeFilter] : null }, + filter: { + status: [tab], + documentTypes: documentTypeFilter ? [documentTypeFilter] : null, + classifications: classificationFilter ? [classificationFilter] : null, + }, }, ), ); @@ -274,6 +308,7 @@ export function DocumentList(props: { order: { direction: order.direction, field: order.field as DocumentOrderField }, status: [tab], documentTypes: documentTypeFilter ? [documentTypeFilter] : null, + classifications: classificationFilter ? [classificationFilter] : null, }); }; @@ -291,6 +326,17 @@ export function DocumentList(props: { ))} +
{documents.length > 0 diff --git a/pkg/coredata/document_filter.go b/pkg/coredata/document_filter.go index 0a6781769..9690d6fb3 100644 --- a/pkg/coredata/document_filter.go +++ b/pkg/coredata/document_filter.go @@ -28,6 +28,7 @@ type ( userEmail *mail.Addr approverIdentityID *gid.GID documentTypes []DocumentType + classifications []DocumentClassification status []DocumentStatus } ) @@ -70,6 +71,11 @@ func (f *DocumentFilter) WithDocumentTypes(documentTypes []DocumentType) *Docume return f } +func (f *DocumentFilter) WithClassifications(classifications []DocumentClassification) *DocumentFilter { + f.classifications = classifications + return f +} + func (f *DocumentFilter) WithStatus(status []DocumentStatus) *DocumentFilter { f.status = status return f @@ -92,6 +98,14 @@ func (f *DocumentFilter) SQLArguments() pgx.NamedArgs { } } + var classifications []string + if f.classifications != nil { + classifications = make([]string, len(f.classifications)) + for i, c := range f.classifications { + classifications[i] = c.String() + } + } + var status []string if f.status != nil { status = make([]string, len(f.status)) @@ -107,6 +121,7 @@ func (f *DocumentFilter) SQLArguments() pgx.NamedArgs { "user_email": f.userEmail, "approver_identity_id": f.approverIdentityID, "document_types": documentTypes, + "classifications": classifications, "document_status": status, } } @@ -169,6 +184,18 @@ func (f *DocumentFilter) SQLFragment() string { ELSE TRUE END AND + CASE + WHEN @classifications::document_classification[] IS NOT NULL THEN + ( + SELECT dv.classification + FROM document_versions dv + WHERE dv.document_id = documents.id + ORDER BY dv.major DESC, dv.minor DESC + LIMIT 1 + ) = ANY(@classifications::document_classification[]) + ELSE TRUE + END + AND CASE WHEN @document_status::text[] IS NULL THEN TRUE ELSE status::text = ANY(@document_status::text[]) diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 2b947f81b..5741e9c22 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1634,6 +1634,7 @@ input ControlFilter { input DocumentFilter { query: String documentTypes: [DocumentType!] + classifications: [DocumentClassification!] status: [DocumentStatus!] } diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 25f569da4..28a5b0b22 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1,17 +1,3 @@ -// Copyright (c) 2025-2026 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - package console_v1 // This file will be automatically regenerated based on the schema, any resolver @@ -625,7 +611,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). - WithDocumentTypes(filter.DocumentTypes) + WithDocumentTypes(filter.DocumentTypes). + WithClassifications(filter.Classifications) } page, err := prb.Documents.ListForControlID(ctx, obj.ID, cursor, documentFilter) @@ -7262,6 +7249,7 @@ func (r *organizationResolver) Documents(ctx context.Context, obj *types.Organiz if filter != nil { documentFilter = coredata.NewDocumentFilter(filter.Query). WithDocumentTypes(filter.DocumentTypes). + WithClassifications(filter.Classifications). WithStatus(filter.Status) } @@ -8674,7 +8662,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). - WithDocumentTypes(filter.DocumentTypes) + WithDocumentTypes(filter.DocumentTypes). + WithClassifications(filter.Classifications) } 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 81e83e2c3..2e0253989 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -2042,7 +2042,8 @@ func (r *Resolver) ListDocumentsTool(ctx context.Context, req *mcp.CallToolReque } documentFilter = coredata.NewDocumentFilter(query). - WithDocumentTypes(input.Filter.DocumentTypes) + WithDocumentTypes(input.Filter.DocumentTypes). + WithClassifications(input.Filter.Classifications) } 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 b855e7304..93557d4ff 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -5374,6 +5374,11 @@ components: items: $ref: "#/components/schemas/DocumentType" description: Document types + classifications: + type: array + items: + $ref: "#/components/schemas/DocumentClassification" + description: Document classifications ListDocumentsOutput: type: object