From 67ec34c62447950696d432d2ea33354e36d61c61 Mon Sep 17 00:00:00 2001 From: Alan Lail Date: Wed, 11 Nov 2026 09:47:37 -0500 Subject: [PATCH] Show the stored adoptionStatus on framework labels (CGE-70) The framework badge renamed Implemented to Published and Pending Implementation to Pending. It now shows the stored value as-is, including legacy and custom values. The editor also stopped converting legacy values (Adopted, Deprecated) on load, so saving no longer rewrites a status the author didn't change. --- .../case/adoptionStatusRoundTrip.test.ts | 35 +++++++++++++++++++ .../mappers/case/caseToDomainFramework.ts | 3 +- .../domain/framework/model/adoptionStatus.ts | 3 ++ apps/editor/src/ui/home/frameworkStore.ts | 6 ++-- .../components/FrameworkLabels.test.tsx | 18 +++++++--- .../ui/shared/components/FrameworkLabels.tsx | 15 +++----- 6 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 apps/editor/src/application/framework/mappers/case/adoptionStatusRoundTrip.test.ts diff --git a/apps/editor/src/application/framework/mappers/case/adoptionStatusRoundTrip.test.ts b/apps/editor/src/application/framework/mappers/case/adoptionStatusRoundTrip.test.ts new file mode 100644 index 0000000..762309f --- /dev/null +++ b/apps/editor/src/application/framework/mappers/case/adoptionStatusRoundTrip.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest' +import { frameworkToCfPackage } from './toCasePackage' +import { loadFrameworkFromCfPackage } from '@/application/framework/services/FrameworkLoader' +import type { CFPackage } from '@/domain/case/types' + +const DOC_ID = 'doc-1' + +function makePackage(adoptionStatus: string): CFPackage { + return { + CFDocument: { + identifier: DOC_ID, + uri: `/ims/case/v1p1/CFDocuments/${DOC_ID}`, + title: 'Curriculum', + creator: 'Importer', + lastChangeDateTime: '2026-01-01T00:00:00.000Z', + caseVersion: '1.1', + adoptionStatus, + }, + CFItems: [], + CFAssociations: [], + } as unknown as CFPackage +} + +// CGE-70: legacy CASE 1.0 values must survive load + save unchanged, so a save +// never rewrites a status the author didn't touch. +describe('adoptionStatus round trip', () => { + it.each(['Adopted', 'Deprecated', 'Implemented', 'Derived'])('keeps %s as stored', (status) => { + const framework = loadFrameworkFromCfPackage(makePackage(status)) + if (!framework) throw new Error('failed to load framework') + expect(framework.metadata.adoptionStatus).toBe(status) + + const saved = frameworkToCfPackage({ framework, caseVersion: '1.1' }) + expect(saved.CFDocument.adoptionStatus).toBe(status) + }) +}) diff --git a/apps/editor/src/application/framework/mappers/case/caseToDomainFramework.ts b/apps/editor/src/application/framework/mappers/case/caseToDomainFramework.ts index dfff902..0b5e4e5 100644 --- a/apps/editor/src/application/framework/mappers/case/caseToDomainFramework.ts +++ b/apps/editor/src/application/framework/mappers/case/caseToDomainFramework.ts @@ -1,7 +1,6 @@ import type { CasePackageSnapshot } from './CasePackageSnapshot' import type { Framework, FrameworkMetadata, Item, Association, AssociationType, ItemType } from '@/domain/framework/model/types' import type { FrameworkId, ItemId, AssociationId } from '@/domain/shared/types' -import { normalizeAdoptionStatus } from '@/domain/framework/model/adoptionStatus' function mapItemType(rawType?: string): ItemType | undefined { const raw = (rawType ?? '').toLowerCase() @@ -44,7 +43,7 @@ export function mapCaseSnapshotToDomainFramework(snapshot: CasePackageSnapshot): caseUri: doc.uri, publisher: doc.publisher, frameworkType: doc.frameworkType, - adoptionStatus: normalizeAdoptionStatus(doc.adoptionStatus) ?? doc.adoptionStatus, + adoptionStatus: doc.adoptionStatus, caseVersion: doc.caseVersion ?? (snapshot.version === 'unknown' ? undefined : snapshot.version), version: doc.version, language: doc.language, diff --git a/apps/editor/src/domain/framework/model/adoptionStatus.ts b/apps/editor/src/domain/framework/model/adoptionStatus.ts index 645645f..bf171f9 100644 --- a/apps/editor/src/domain/framework/model/adoptionStatus.ts +++ b/apps/editor/src/domain/framework/model/adoptionStatus.ts @@ -65,6 +65,9 @@ export const LEGACY_STATUS_MAP: Readonly> = { * Normalise an adoption status value: * 1. Map known legacy values to their modern equivalents. * 2. Leave all other values (including custom / imported) unchanged. + * + * For classification only (badge color, draft vs. finalized) — never write + * the result back to a framework or show it in place of the stored value. */ export function normalizeAdoptionStatus(raw: string | undefined): string | undefined { if (!raw) return raw diff --git a/apps/editor/src/ui/home/frameworkStore.ts b/apps/editor/src/ui/home/frameworkStore.ts index d952b22..99f7c7b 100644 --- a/apps/editor/src/ui/home/frameworkStore.ts +++ b/apps/editor/src/ui/home/frameworkStore.ts @@ -79,14 +79,12 @@ function createFrameworkFromMetadata(params: { adoptionStatus?: string caseVersion?: string }): Framework { - const normalized = normalizeAdoptionStatus(params.adoptionStatus) ?? params.adoptionStatus - const metadata: FrameworkMetadata = { title: params.title, description: params.description, creator: params.creator, frameworkType: params.frameworkType, - adoptionStatus: normalized, + adoptionStatus: params.adoptionStatus, caseVersion: params.caseVersion ?? '1.1', // Default to CASE 1.1 for new frameworks lastChangeDateTime: new Date().toISOString(), } @@ -96,7 +94,7 @@ function createFrameworkFromMetadata(params: { metadata, items: new Map(), associations: new Map(), - status: adoptionStatusToDomainStatus(normalized), + status: adoptionStatusToDomainStatus(params.adoptionStatus), } } diff --git a/apps/editor/src/ui/shared/components/FrameworkLabels.test.tsx b/apps/editor/src/ui/shared/components/FrameworkLabels.test.tsx index 18251f5..6f9080f 100644 --- a/apps/editor/src/ui/shared/components/FrameworkLabels.test.tsx +++ b/apps/editor/src/ui/shared/components/FrameworkLabels.test.tsx @@ -11,16 +11,26 @@ describe('FrameworkLabels', () => { it.each([ ['Draft', 'Draft'], - ['Pending Implementation', 'Pending'], - ['Implemented', 'Published'], + ['Pending Implementation', 'Pending Implementation'], + ['Implemented', 'Implemented'], ['Retired', 'Retired'], - ['Adopted', 'Published'], - ['Deprecated', 'Retired'], + ['Adopted', 'Adopted'], + ['Deprecated', 'Deprecated'], ])('shows adoption status %s as "%s"', (status, label) => { render() expect(screen.getByText(label)).toBeInTheDocument() }) + it('no longer relabels Implemented as "Published"', () => { + render() + expect(screen.queryByText('Published')).not.toBeInTheDocument() + }) + + it('colors a legacy adoption status like its modern equivalent', () => { + render() + expect(screen.getByText('Adopted')).toHaveClass('bg-emerald-100', 'text-emerald-800') + }) + it('shows an unrecognized adoption status as-is with the neutral badge', () => { render() expect(screen.getByText('Derived')).toHaveClass('bg-gray-100', 'text-gray-600') diff --git a/apps/editor/src/ui/shared/components/FrameworkLabels.tsx b/apps/editor/src/ui/shared/components/FrameworkLabels.tsx index 7c3adb7..c590773 100644 --- a/apps/editor/src/ui/shared/components/FrameworkLabels.tsx +++ b/apps/editor/src/ui/shared/components/FrameworkLabels.tsx @@ -3,27 +3,23 @@ import { cn } from '@/lib/utils' import { normalizeAdoptionStatus } from '@/domain/framework/model/adoptionStatus' /** Status badge styling — prominent, with strong color coding */ -const STATUS_STYLE: Record = { +const STATUS_STYLE: Record = { Draft: { - label: 'Draft', bg: 'bg-amber-100', text: 'text-amber-800', dot: 'bg-[#FF941F]', }, 'Pending Implementation': { - label: 'Pending', bg: 'bg-sky-100', text: 'text-sky-800', dot: 'bg-[#29ABE3]', }, Implemented: { - label: 'Published', bg: 'bg-emerald-100', text: 'text-emerald-800', dot: 'bg-emerald-500', }, Retired: { - label: 'Retired', bg: 'bg-gray-100', text: 'text-gray-500', dot: 'bg-[#919496]', @@ -31,7 +27,7 @@ const STATUS_STYLE: Record) { - const adoptionStatus = normalizeAdoptionStatus(rawAdoptionStatus) ?? rawAdoptionStatus - const statusInfo = adoptionStatus ? STATUS_STYLE[adoptionStatus] : undefined + const statusInfo = adoptionStatus ? STATUS_STYLE[normalizeAdoptionStatus(adoptionStatus) ?? adoptionStatus] : undefined return (
{adoptionStatus && statusInfo ? ( - {statusInfo.label} + {adoptionStatus} ) : adoptionStatus ? (