Fix compliance page to only show published document versions

The compliance page was leaking draft document content and titles.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-02-23 19:11:49 +01:00
parent 24ad1d3e13
commit 9e788ee558
9 changed files with 143 additions and 77 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<e02fc31002f54ed30be7e53370069aaa>> * @generated SignedSource<<2a0f1332c57638e29eeee8e4113858d3>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -17,6 +17,7 @@ export type CompliancePageDocumentListFragment$data = {
readonly documents: { readonly documents: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly currentPublishedVersion: number | null | undefined;
readonly id: string; readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageDocumentListItem_documentFragment">; readonly " $fragmentSpreads": FragmentRefs<"CompliancePageDocumentListItem_documentFragment">;
}; };
@@ -92,6 +93,13 @@ const node: ReaderFragment = {
"name": "id", "name": "id",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "currentPublishedVersion",
"storageKey": null
},
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -111,6 +119,6 @@ const node: ReaderFragment = {
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "a8d512034c171dc461a42e6ae6e76f12"; (node as any).hash = "48e4f01fb5d80ae0856433598608d4cf";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<112e917e3fb881859a56b794f07ce7e7>> * @generated SignedSource<<85da952604018b5f879924bf3f8d5a27>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,22 +9,19 @@
// @ts-nocheck // @ts-nocheck
import { ReaderFragment } from 'relay-runtime'; import { ReaderFragment } from 'relay-runtime';
export type DocumentStatus = "DRAFT" | "PUBLISHED";
export type DocumentType = "ISMS" | "OTHER" | "POLICY" | "PROCEDURE"; export type DocumentType = "ISMS" | "OTHER" | "POLICY" | "PROCEDURE";
export type TrustCenterVisibility = "NONE" | "PRIVATE" | "PUBLIC"; export type TrustCenterVisibility = "NONE" | "PRIVATE" | "PUBLIC";
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type CompliancePageDocumentListItem_documentFragment$data = { export type CompliancePageDocumentListItem_documentFragment$data = {
readonly documentType: DocumentType; readonly documentType: DocumentType;
readonly id: string; readonly id: string;
readonly lastVersion: { readonly latestPublishedVersion: {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly id: string; readonly title: string;
readonly status: DocumentStatus;
}; };
}>; }>;
}; };
readonly title: string;
readonly trustCenterVisibility: TrustCenterVisibility; readonly trustCenterVisibility: TrustCenterVisibility;
readonly " $fragmentType": "CompliancePageDocumentListItem_documentFragment"; readonly " $fragmentType": "CompliancePageDocumentListItem_documentFragment";
}; };
@@ -33,21 +30,19 @@ export type CompliancePageDocumentListItem_documentFragment$key = {
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageDocumentListItem_documentFragment">; readonly " $fragmentSpreads": FragmentRefs<"CompliancePageDocumentListItem_documentFragment">;
}; };
const node: ReaderFragment = (function(){ const node: ReaderFragment = {
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [], "argumentDefinitions": [],
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "CompliancePageDocumentListItem_documentFragment", "name": "CompliancePageDocumentListItem_documentFragment",
"selections": [ "selections": [
(v0/*: any*/), {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -55,13 +50,6 @@ return {
"name": "documentType", "name": "documentType",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -70,8 +58,15 @@ return {
"storageKey": null "storageKey": null
}, },
{ {
"alias": "lastVersion", "alias": "latestPublishedVersion",
"args": [ "args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"status": "PUBLISHED"
}
},
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
@@ -107,12 +102,11 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "status", "name": "title",
"storageKey": null "storageKey": null
} }
], ],
@@ -122,14 +116,13 @@ return {
"storageKey": null "storageKey": null
} }
], ],
"storageKey": "versions(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" "storageKey": "versions(filter:{\"status\":\"PUBLISHED\"},first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
} }
], ],
"type": "Document", "type": "Document",
"abstractKey": null "abstractKey": null
}; };
})();
(node as any).hash = "ab2b10ffbd76c59d1a3facab0f794402"; (node as any).hash = "4c6d4e935f4a54b6d2ef486f60aab5f6";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<0948c131a30705ee3921f60bbd40c3cb>> * @generated SignedSource<<976e80f8eb6650a229d5af8f1f58b5f5>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -127,13 +127,6 @@ return {
"name": "documentType", "name": "documentType",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -142,8 +135,15 @@ return {
"storageKey": null "storageKey": null
}, },
{ {
"alias": "lastVersion", "alias": "latestPublishedVersion",
"args": [ "args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"status": "PUBLISHED"
}
},
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
@@ -179,14 +179,14 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "status", "name": "title",
"storageKey": null "storageKey": null
} },
(v2/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -194,7 +194,7 @@ return {
"storageKey": null "storageKey": null
} }
], ],
"storageKey": "versions(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" "storageKey": "versions(filter:{\"status\":\"PUBLISHED\"},first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
} }
], ],
"storageKey": null "storageKey": null
@@ -205,12 +205,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "f72f0dd5039dbfeaa609b7914308c0fe", "cacheID": "8bb0348deb10b7f0fe65b953bccf8cb3",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "CompliancePageDocumentListItem_updateVisibilityMutation", "name": "CompliancePageDocumentListItem_updateVisibilityMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation CompliancePageDocumentListItem_updateVisibilityMutation(\n $input: UpdateDocumentInput!\n) {\n updateDocument(input: $input) {\n document {\n ...CompliancePageDocumentListItem_documentFragment\n id\n }\n }\n}\n\nfragment CompliancePageDocumentListItem_documentFragment on Document {\n id\n documentType\n title\n trustCenterVisibility\n lastVersion: versions(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n" "text": "mutation CompliancePageDocumentListItem_updateVisibilityMutation(\n $input: UpdateDocumentInput!\n) {\n updateDocument(input: $input) {\n document {\n ...CompliancePageDocumentListItem_documentFragment\n id\n }\n }\n}\n\nfragment CompliancePageDocumentListItem_documentFragment on Document {\n id\n documentType\n trustCenterVisibility\n latestPublishedVersion: versions(first: 1, orderBy: {field: CREATED_AT, direction: DESC}, filter: {status: PUBLISHED}) {\n edges {\n node {\n title\n id\n }\n }\n }\n}\n"
} }
}; };
})(); })();

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<ccda9818c715d1b77b33d0114c235cb4>> * @generated SignedSource<<56c0070ef19b8dff73eeb111e11fff52>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -156,14 +156,14 @@ return {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "documentType", "name": "currentPublishedVersion",
"storageKey": null "storageKey": null
}, },
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "title", "name": "documentType",
"storageKey": null "storageKey": null
}, },
{ {
@@ -174,8 +174,15 @@ return {
"storageKey": null "storageKey": null
}, },
{ {
"alias": "lastVersion", "alias": "latestPublishedVersion",
"args": [ "args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"status": "PUBLISHED"
}
},
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
@@ -211,14 +218,14 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "status", "name": "title",
"storageKey": null "storageKey": null
} },
(v2/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -226,7 +233,7 @@ return {
"storageKey": null "storageKey": null
} }
], ],
"storageKey": "versions(first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" "storageKey": "versions(filter:{\"status\":\"PUBLISHED\"},first:1,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
} }
], ],
"storageKey": null "storageKey": null
@@ -248,12 +255,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "a094d511faa776c9e8d68f53933b3937", "cacheID": "44c832e5fec9c0e84a0cbd844063d0fe",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "CompliancePageDocumentsPageQuery", "name": "CompliancePageDocumentsPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query CompliancePageDocumentsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...CompliancePageDocumentListFragment\n id\n }\n}\n\nfragment CompliancePageDocumentListFragment on Organization {\n compliancePage: trustCenter {\n ...CompliancePageDocumentListItem_compliancePageFragment\n id\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...CompliancePageDocumentListItem_documentFragment\n }\n }\n }\n}\n\nfragment CompliancePageDocumentListItem_compliancePageFragment on TrustCenter {\n canUpdate: permission(action: \"core:trust-center:update\")\n}\n\nfragment CompliancePageDocumentListItem_documentFragment on Document {\n id\n documentType\n title\n trustCenterVisibility\n lastVersion: versions(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n" "text": "query CompliancePageDocumentsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...CompliancePageDocumentListFragment\n id\n }\n}\n\nfragment CompliancePageDocumentListFragment on Organization {\n compliancePage: trustCenter {\n ...CompliancePageDocumentListItem_compliancePageFragment\n id\n }\n documents(first: 100) {\n edges {\n node {\n id\n currentPublishedVersion\n ...CompliancePageDocumentListItem_documentFragment\n }\n }\n }\n}\n\nfragment CompliancePageDocumentListItem_compliancePageFragment on TrustCenter {\n canUpdate: permission(action: \"core:trust-center:update\")\n}\n\nfragment CompliancePageDocumentListItem_documentFragment on Document {\n id\n documentType\n trustCenterVisibility\n latestPublishedVersion: versions(first: 1, orderBy: {field: CREATED_AT, direction: DESC}, filter: {status: PUBLISHED}) {\n edges {\n node {\n title\n id\n }\n }\n }\n}\n"
} }
}; };
})(); })();

View File

@@ -16,6 +16,7 @@ const fragment = graphql`
edges { edges {
node { node {
id id
currentPublishedVersion
...CompliancePageDocumentListItem_documentFragment ...CompliancePageDocumentListItem_documentFragment
} }
} }
@@ -29,6 +30,7 @@ export function CompliancePageDocumentList(props: { fragmentRef: CompliancePageD
const { __ } = useTranslate(); const { __ } = useTranslate();
const { compliancePage, documents } = useFragment<CompliancePageDocumentListFragment$key>(fragment, fragmentRef); const { compliancePage, documents } = useFragment<CompliancePageDocumentListFragment$key>(fragment, fragmentRef);
const publishedDocuments = documents.edges.filter(({ node }) => node.currentPublishedVersion != null);
return ( return (
<div className="space-y-[10px]"> <div className="space-y-[10px]">
@@ -37,19 +39,18 @@ export function CompliancePageDocumentList(props: { fragmentRef: CompliancePageD
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{__("Name")}</Th>
<Th>{__("Type")}</Th> <Th>{__("Type")}</Th>
<Th>{__("State")}</Th>
<Th>{__("Visibility")}</Th> <Th>{__("Visibility")}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{documents.edges.length === 0 && ( {publishedDocuments.length === 0 && (
<Tr> <Tr>
<Td colSpan={5} className="text-center text-txt-secondary"> <Td colSpan={3} className="text-center text-txt-secondary">
{__("No documents available")} {__("No documents available")}
</Td> </Td>
</Tr> </Tr>
)} )}
{documents.edges.map(({ node: document }) => ( {publishedDocuments.map(({ node: document }) => (
<CompliancePageDocumentListItem <CompliancePageDocumentListItem
key={document.id} key={document.id}
compliancePageFragmentRef={compliancePage} compliancePageFragmentRef={compliancePage}

View File

@@ -1,6 +1,6 @@
import { getTrustCenterVisibilityOptions } from "@probo/helpers"; import { getTrustCenterVisibilityOptions } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { Badge, DocumentTypeBadge, DocumentVersionBadge, Field, Option, Td, Tr } from "@probo/ui"; import { Badge, DocumentTypeBadge, Field, Option, Td, Tr } from "@probo/ui";
import { useCallback } from "react"; import { useCallback } from "react";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -20,13 +20,15 @@ const documentFragment = graphql`
fragment CompliancePageDocumentListItem_documentFragment on Document { fragment CompliancePageDocumentListItem_documentFragment on Document {
id id
documentType documentType
title
trustCenterVisibility trustCenterVisibility
lastVersion: versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) { latestPublishedVersion: versions(
first: 1
orderBy: { field: CREATED_AT, direction: DESC }
filter: { status: PUBLISHED }
) {
edges { edges {
node { node {
id title
status
} }
} }
} }
@@ -86,19 +88,16 @@ export function CompliancePageDocumentListItem(props: {
[document.id, updateDocumentVisibility], [document.id, updateDocumentVisibility],
); );
const versionTitle = document.latestPublishedVersion.edges[0]?.node.title;
return ( return (
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}> <Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
<Td> <Td>
<div className="flex gap-4 items-center">{document.title}</div> <div className="flex gap-4 items-center">{versionTitle}</div>
</Td> </Td>
<Td> <Td>
<DocumentTypeBadge type={document.documentType} /> <DocumentTypeBadge type={document.documentType} />
</Td> </Td>
<Td>
<DocumentVersionBadge
state={document.lastVersion.edges[0].node.status}
/>
</Td>
<Td noLink width={130} className="pr-0"> <Td noLink width={130} className="pr-0">
<Field <Field
type="select" type="select"

View File

@@ -354,6 +354,67 @@ ORDER BY title ASC
return nil return nil
} }
func (p *Documents) LoadPublishedByOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
cursor *page.Cursor[DocumentOrderField],
filter *DocumentFilter,
) error {
q := `
WITH published_documents AS (
SELECT
d.*,
dv.title AS published_title
FROM
documents d
LEFT JOIN document_versions dv
ON dv.document_id = d.id
AND dv.version_number = d.current_published_version
WHERE
d.deleted_at IS NULL
AND d.organization_id = @organization_id
)
SELECT
id,
organization_id,
COALESCE(published_title, title) AS title,
document_type,
classification,
current_published_version,
trust_center_visibility,
created_at,
updated_at
FROM
published_documents documents
WHERE
%s
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query published documents: %w", err)
}
documents, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Document])
if err != nil {
return fmt.Errorf("cannot collect published documents: %w", err)
}
*p = documents
return nil
}
func (p Document) Insert( func (p Document) Insert(
ctx context.Context, ctx context.Context,
conn pg.Conn, conn pg.Conn,

View File

@@ -35,11 +35,13 @@ func NewDocumentFilter(query *string) *DocumentFilter {
} }
func NewDocumentTrustCenterFilter() *DocumentFilter { func NewDocumentTrustCenterFilter() *DocumentFilter {
published := true
return &DocumentFilter{ return &DocumentFilter{
trustCenterVisibilities: []TrustCenterVisibility{ trustCenterVisibilities: []TrustCenterVisibility{
TrustCenterVisibilityPrivate, TrustCenterVisibilityPrivate,
TrustCenterVisibilityPublic, TrustCenterVisibilityPublic,
}, },
published: &published,
} }
} }

View File

@@ -70,12 +70,7 @@ func (s *DocumentService) ListForOrganizationId(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
filter := coredata.NewDocumentTrustCenterFilter() filter := coredata.NewDocumentTrustCenterFilter()
err := documents.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter) return documents.LoadPublishedByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter)
if err != nil {
return fmt.Errorf("cannot load documents: %w", err)
}
return nil
}, },
) )