diff --git a/apps/editor/src/app/App.tsx b/apps/editor/src/app/App.tsx index 70ee11c..cb69939 100644 --- a/apps/editor/src/app/App.tsx +++ b/apps/editor/src/app/App.tsx @@ -521,7 +521,7 @@ function AppInner() { // Handler to save the CFPackage to the server // Must be defined before early returns (React hooks rules) const handleSaveToServer = useCallback( - async (openCasePackage: unknown) => { + async (openCasePackage: unknown, framework: Framework) => { if (!tenantId) { throw new Error('Not signed in to a tenant. Please sign in to save.') } @@ -568,13 +568,20 @@ function AppInner() { const { [oldId]: _dropped, ...rest } = prev return rest }) - } else if (activeFrameworkId && result.isModifiedFromSource !== undefined) { + } else if (activeFrameworkId) { + // Refresh the local cache with the just-saved Framework (items, associations, + // metadata) so a hard refresh reflects the server state instead of the + // pre-save snapshot. Without this, edits made and saved in this session + // (e.g. a newly added item) would vanish on F5 until the framework was + // reopened from the Home screen, which always re-fetches from the server. setFrameworks((prev) => { - const next = prev.map((f) => - f.id === activeFrameworkId - ? { ...f, mirrorStatus: { isModifiedFromSource: result.isModifiedFromSource, sourcePackageURI: result.sourcePackageURI } } - : f - ) + const existingIdx = prev.findIndex((f) => f.id === activeFrameworkId) + if (existingIdx < 0) return prev + const mirrorStatus = result.isModifiedFromSource !== undefined + ? { isModifiedFromSource: result.isModifiedFromSource, sourcePackageURI: result.sourcePackageURI } + : prev[existingIdx].mirrorStatus + const next = [...prev] + next[existingIdx] = createHomeFrameworkFromDomain(framework, mirrorStatus) saveFrameworks(next) return next }) diff --git a/apps/editor/src/application/framework/commands/AddItem.ts b/apps/editor/src/application/framework/commands/AddItem.ts index cfd18ca..8238f85 100644 --- a/apps/editor/src/application/framework/commands/AddItem.ts +++ b/apps/editor/src/application/framework/commands/AddItem.ts @@ -8,7 +8,7 @@ export type AddItem = Command< frameworkId: FrameworkId itemId: ItemId statement: string - type: ItemType + type?: ItemType } > diff --git a/apps/editor/src/application/framework/mappers/case/caseToDomainFramework.ts b/apps/editor/src/application/framework/mappers/case/caseToDomainFramework.ts index 7c1fb24..d15ce99 100644 --- a/apps/editor/src/application/framework/mappers/case/caseToDomainFramework.ts +++ b/apps/editor/src/application/framework/mappers/case/caseToDomainFramework.ts @@ -3,13 +3,13 @@ import type { Framework, FrameworkMetadata, Item, Association, AssociationType, import type { FrameworkId, ItemId, AssociationId } from '@/domain/shared/types' import { normalizeAdoptionStatus } from '@/domain/framework/model/adoptionStatus' -function mapItemType(rawType?: string): ItemType { +function mapItemType(rawType?: string): ItemType | undefined { const raw = (rawType ?? '').toLowerCase() if (raw.includes('skill')) return 'Skill' if (raw.includes('learning') || raw.includes('outcome')) return 'LearningOutcome' if (raw.includes('standard')) return 'Standard' if (raw.includes('compet')) return 'Competency' - return 'Competency' + return undefined } function mapAssociationType(rawType?: string): AssociationType { diff --git a/apps/editor/src/application/framework/services/SpreadsheetToFramework.test.ts b/apps/editor/src/application/framework/services/SpreadsheetToFramework.test.ts index 7d91880..3bc473c 100644 --- a/apps/editor/src/application/framework/services/SpreadsheetToFramework.test.ts +++ b/apps/editor/src/application/framework/services/SpreadsheetToFramework.test.ts @@ -156,7 +156,7 @@ describe('SpreadsheetToFramework', () => { makeRow({ level: 1, fullStatement: 'A', itemType: 'competency' }), makeRow({ level: 1, fullStatement: 'B', itemType: 'LEARNING OUTCOME' }), makeRow({ level: 1, fullStatement: 'C', itemType: 'skill' }), - makeRow({ level: 1, fullStatement: 'D' }), // defaults to Standard + makeRow({ level: 1, fullStatement: 'D' }), // no type entered — should remain unset ] const fw = spreadsheetToFramework(rows, { title: 'Test' }) const items = [...fw.items.values()] @@ -164,7 +164,7 @@ describe('SpreadsheetToFramework', () => { expect(items[0].type).toBe('Competency') expect(items[1].type).toBe('LearningOutcome') expect(items[2].type).toBe('Skill') - expect(items[3].type).toBe('Standard') + expect(items[3].type).toBeUndefined() }) it('handles a single item (flat framework)', () => { diff --git a/apps/editor/src/application/framework/services/SpreadsheetToFramework.ts b/apps/editor/src/application/framework/services/SpreadsheetToFramework.ts index 4d0a1fb..8a06681 100644 --- a/apps/editor/src/application/framework/services/SpreadsheetToFramework.ts +++ b/apps/editor/src/application/framework/services/SpreadsheetToFramework.ts @@ -18,9 +18,9 @@ const VALID_ITEM_TYPES: Record = { skill: 'Skill', } -function normaliseItemType(raw: string | undefined): ItemType { - if (!raw) return 'Standard' - return VALID_ITEM_TYPES[raw.trim().toLowerCase()] ?? 'Standard' +function normaliseItemType(raw: string | undefined): ItemType | undefined { + if (!raw) return undefined + return VALID_ITEM_TYPES[raw.trim().toLowerCase()] ?? undefined } // ── Public API ────────────────────────────────────────────────────── diff --git a/apps/editor/src/domain/framework/model/Item.ts b/apps/editor/src/domain/framework/model/Item.ts index d9ea3ad..5afb7fd 100644 --- a/apps/editor/src/domain/framework/model/Item.ts +++ b/apps/editor/src/domain/framework/model/Item.ts @@ -1,7 +1,7 @@ import type { Item, ItemType } from './types' import type { ItemId } from '@/domain/shared/types' -export function createItem(params: { id: ItemId; statement: string; type: ItemType }): Item { +export function createItem(params: { id: ItemId; statement: string; type?: ItemType }): Item { return { id: params.id, statement: params.statement, diff --git a/apps/editor/src/domain/framework/model/types.ts b/apps/editor/src/domain/framework/model/types.ts index f2e09df..16085dc 100644 --- a/apps/editor/src/domain/framework/model/types.ts +++ b/apps/editor/src/domain/framework/model/types.ts @@ -67,7 +67,7 @@ export type AssociationMetadata = { export type Item = { id: ItemId statement: string - type: ItemType + type?: ItemType metadata?: ItemMetadata } diff --git a/apps/editor/src/infrastructure/caseApi/CaseFrameworkRepository.ts b/apps/editor/src/infrastructure/caseApi/CaseFrameworkRepository.ts index bc5faf1..a180d79 100644 --- a/apps/editor/src/infrastructure/caseApi/CaseFrameworkRepository.ts +++ b/apps/editor/src/infrastructure/caseApi/CaseFrameworkRepository.ts @@ -8,13 +8,13 @@ import type { CaseApiClient } from './CaseApiClient' export class CaseFrameworkRepository implements FrameworkRepository { constructor(private readonly _client: CaseApiClient) {} - private mapItemType(cfItem: CFItem): ItemType { + private mapItemType(cfItem: CFItem): ItemType | undefined { const raw = (cfItem.CFItemType ?? '').toLowerCase() if (raw.includes('skill')) return 'Skill' if (raw.includes('learning') || raw.includes('outcome')) return 'LearningOutcome' if (raw.includes('standard')) return 'Standard' if (raw.includes('compet')) return 'Competency' - return 'Competency' + return undefined } private mapAssociationType(assoc: CFAssociation): AssociationType { diff --git a/apps/editor/src/ui/editor/EditorCanvas.tsx b/apps/editor/src/ui/editor/EditorCanvas.tsx index c3ea320..14de323 100644 --- a/apps/editor/src/ui/editor/EditorCanvas.tsx +++ b/apps/editor/src/ui/editor/EditorCanvas.tsx @@ -32,7 +32,7 @@ type MirrorStatus = { isModifiedFromSource?: boolean; sourcePackageURI?: string type EditorCanvasProps = { onBack?: () => void - onSaveToServer?: (cfPackage: ReturnType) => Promise + onSaveToServer?: (cfPackage: ReturnType, framework: Framework) => Promise /** Whether the current framework has been published to OpenCASE (loaded from or saved to server) */ isPublishedToOpenCase?: boolean /** Archive the current framework on the server and navigate home */ @@ -215,7 +215,7 @@ export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpen setSaveStatus('saving') setSaveError(null) try { - await onSaveToServer(openCasePackage) + await onSaveToServer(openCasePackage, framework) baselineFrameworkRef.current = framework setSaveStatus('success') clearDirty() diff --git a/apps/editor/src/ui/editor/reactflow/mapping/fromEditorGraph.ts b/apps/editor/src/ui/editor/reactflow/mapping/fromEditorGraph.ts index 77e50bb..a10d1af 100644 --- a/apps/editor/src/ui/editor/reactflow/mapping/fromEditorGraph.ts +++ b/apps/editor/src/ui/editor/reactflow/mapping/fromEditorGraph.ts @@ -7,13 +7,13 @@ import type { CFDocument, CFItem } from '@/domain/case/types' const isFrameworkNode = (n: EditorGraph['nodes'][number]) => n.type === 'caseFrameworkNode' const isItemNode = (n: EditorGraph['nodes'][number]) => n.type === 'caseItemNode' -function mapItemType(rawType?: string): ItemType { +function mapItemType(rawType?: string): ItemType | undefined { const raw = (rawType ?? '').toLowerCase() if (raw.includes('skill')) return 'Skill' if (raw.includes('learning') || raw.includes('outcome')) return 'LearningOutcome' if (raw.includes('standard')) return 'Standard' if (raw.includes('compet')) return 'Competency' - return 'Competency' + return undefined } function edgeToAssociationType(edgeId: string, edgeData?: { associationType?: string; cfAssociation?: { associationType?: string } }): AssociationType {