From c815b4844560a43a398c363c1b783329c2732ff2 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 7 Jul 2026 15:41:17 -0400 Subject: [PATCH] fix(isms): production-deploy review follow-ups (CS-701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the ISMS issues cubic flagged on the main->release deploy PR (#3364): - hasApprovedVersion is now status-aware (currentVersionId != null OR status === 'approved'), so a document approved before versioning existed — whose legacy version row the migration drops — still reports an approved version instead of flipping to false. (P2) - ScopeForm re-seeds when the persisted draft content changes: its remount key now reflects draftNarrative, not just the published currentVersionId, so scope text refreshes after "Generate from platform data" / reload. (P3) - export DTO description now matches runtime: omitting versionId exports the current published version (or the draft if never published), not always the draft. (P3) - Approval banner renders "(v2)" instead of "v 2" (single JSX expression). (P3) Migration is intentionally left non-destructive (approvals preserved); the hasApprovedVersion fix makes that state self-consistent without a heavy backfill for pre-GA data. Tests: 319 api + 123 app ISMS tests passing; typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012CweXoSSEP89mX93u3Bdcp --- .../src/isms/dto/export-isms-document.dto.ts | 3 ++- apps/api/src/isms/isms.service.spec.ts | 23 +++++++++++++++++++ apps/api/src/isms/isms.service.ts | 10 +++++--- .../isms/components/IsmsApprovalSection.tsx | 4 ++-- .../documents/isms/components/ScopeClient.tsx | 9 +++++++- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/apps/api/src/isms/dto/export-isms-document.dto.ts b/apps/api/src/isms/dto/export-isms-document.dto.ts index 632b1a292..b91f83c87 100644 --- a/apps/api/src/isms/dto/export-isms-document.dto.ts +++ b/apps/api/src/isms/dto/export-isms-document.dto.ts @@ -12,7 +12,8 @@ export class ExportIsmsDocumentDto { @ApiPropertyOptional({ description: - 'Published version to export. Omit to export the current working draft; ' + + "Published version to export. Omit to export the document's current " + + 'published version (or the working draft if it has never been published); ' + 'provide a version id to download exactly what was approved at that version.', }) @IsOptional() diff --git a/apps/api/src/isms/isms.service.spec.ts b/apps/api/src/isms/isms.service.spec.ts index af4c4e5d0..7d801200f 100644 --- a/apps/api/src/isms/isms.service.spec.ts +++ b/apps/api/src/isms/isms.service.spec.ts @@ -89,6 +89,29 @@ describe('IsmsService ensureSetup', () => { expect(mockDb.ismsDocument.findMany).toHaveBeenCalledTimes(1); expect(result.documents).toHaveLength(1); }); + + it('reports hasApprovedVersion from a published version OR an approved status', async () => { + (mockDb.frameworkEditorFramework.findUnique as jest.Mock).mockResolvedValue({ + id: 'fw_1', + requirements: [], + }); + (mockDb.ismsDocument.findMany as jest.Mock).mockResolvedValueOnce([ + // Published version exists. + { id: 'd1', type: 'isms_scope', status: 'draft', requirementId: null, currentVersionId: 'isms_ver_1' }, + // Approved before versioning existed: no version row, but still approved. + { id: 'd2', type: 'leadership_commitment', status: 'approved', requirementId: null, currentVersionId: null }, + // Never approved. + { id: 'd3', type: 'objectives_plan', status: 'draft', requirementId: null, currentVersionId: null }, + ]); + + const result = await service.ensureSetup({ ...dto, canWrite: false }); + + expect(result.documents.map((d) => d.hasApprovedVersion)).toEqual([ + true, + true, + false, + ]); + }); }); describe('template-driven (templates seeded)', () => { diff --git a/apps/api/src/isms/isms.service.ts b/apps/api/src/isms/isms.service.ts index 15dbc1974..e0a73497f 100644 --- a/apps/api/src/isms/isms.service.ts +++ b/apps/api/src/isms/isms.service.ts @@ -65,9 +65,13 @@ export class IsmsService { type: doc.type, status: doc.status, requirementId: doc.requirementId, - // A published version exists (the document has been approved at least - // once) — independent of whether the draft has since been edited. - hasApprovedVersion: doc.currentVersionId != null, + // The document has an approved artifact: either a published version row + // (approved under the versioning model) or `status === 'approved'` — the + // latter covers documents approved before versioning existed, whose legacy + // version rows were dropped by the migration and which capture a real + // versioned artifact on their next approval. + hasApprovedVersion: + doc.currentVersionId != null || doc.status === 'approved', })), }; } diff --git a/apps/app/src/app/(app)/[orgId]/documents/isms/components/IsmsApprovalSection.tsx b/apps/app/src/app/(app)/[orgId]/documents/isms/components/IsmsApprovalSection.tsx index d5bc90fd5..37ba6d724 100644 --- a/apps/app/src/app/(app)/[orgId]/documents/isms/components/IsmsApprovalSection.tsx +++ b/apps/app/src/app/(app)/[orgId]/documents/isms/components/IsmsApprovalSection.tsx @@ -128,8 +128,8 @@ export function IsmsApprovalSection({ v{publishedVersion} {' '} - stays live and exportable. Your changes are an in-progress draft (v - {nextDraftVersion}) — submit it for approval to publish. + stays live and exportable. Your changes are an in-progress draft{' '} + {`(v${nextDraftVersion})`} — submit it for approval to publish. )} diff --git a/apps/app/src/app/(app)/[orgId]/documents/isms/components/ScopeClient.tsx b/apps/app/src/app/(app)/[orgId]/documents/isms/components/ScopeClient.tsx index f3b006621..391391dd1 100644 --- a/apps/app/src/app/(app)/[orgId]/documents/isms/components/ScopeClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/documents/isms/components/ScopeClient.tsx @@ -64,10 +64,17 @@ export function ScopeClient(props: ScopeClientProps) { }; const narrative = toScopeNarrative(document.draftNarrative); + // Re-seed the form (via remount) whenever the persisted draft content + // changes — e.g. after "Generate from platform data" or a reload — not + // just when the published version changes. ScopeForm reads its defaults + // at mount, so the key must reflect the draft, not currentVersionId alone. + const formKey = `${document.currentVersionId ?? 'draft'}:${JSON.stringify( + document.draftNarrative ?? {}, + )}`; return (