Skip to content

Commit ad90bbb

Browse files
committed
Always display the absolute uri for relevant fields when data is provided by the server. When working with a local copy, estimate absolute path using local origin
1 parent f786966 commit ad90bbb

7 files changed

Lines changed: 80 additions & 30 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.env
22
AGENTS.md
3+
CLAUDE.md
34
.DS_Store

apps/editor/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/editor/src/app/App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,12 @@ function AppInner() {
395395
removeFrameworkFromStorage(activeFrameworkId)
396396
}, [api, tenantId, activeFrameworkId, caseApiVersion, removeFrameworkFromStorage])
397397

398+
// Handler to fetch the published CFPackage from the server (returns CASE JSON with absolute URIs)
399+
const handleFetchCfPackage = useCallback(async () => {
400+
if (!activeFrameworkId) throw new Error('No active framework')
401+
return api.getCfPackage({ docId: activeFrameworkId, caseVersion: caseApiVersion })
402+
}, [api, activeFrameworkId, caseApiVersion])
403+
398404
// Handler to save the CFPackage to the server
399405
// Must be defined before early returns (React hooks rules)
400406
const handleSaveToServer = useCallback(
@@ -493,6 +499,7 @@ function AppInner() {
493499
onSaveToServer={tenantId ? handleSaveToServer : undefined}
494500
isPublishedToOpenCase={activeFrameworkId ? publishedFrameworkIds.has(activeFrameworkId) : false}
495501
onArchiveFramework={tenantId && activeFrameworkId ? handleArchiveFramework : undefined}
502+
onFetchCfPackage={activeFrameworkId ? handleFetchCfPackage : undefined}
496503
/>
497504
</EditorProvider>
498505
)

apps/editor/src/application/framework/mappers/case/toCasePackage.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,33 @@ export function toOpenCaseFormat(cfPackage: CFPackage): CaseV1p1Package {
749749
}
750750
}
751751

752+
/**
753+
* Walk a CASE JSON object and prepend baseUrl to any relative /ims/case/ URI strings.
754+
* Leaves already-absolute URIs and non-CASE strings untouched.
755+
*/
756+
export function absolutizeCaseUris<T>(payload: T, baseUrl: string): T {
757+
const seen = new WeakSet<object>()
758+
759+
const normalize = (v: string): string =>
760+
v.startsWith('/ims/case/') ? `${baseUrl}${v}` : v
761+
762+
const walk = (value: unknown): unknown => {
763+
if (value === null || value === undefined) return value
764+
if (typeof value === 'string') return normalize(value)
765+
if (typeof value !== 'object') return value
766+
if (seen.has(value as object)) return value
767+
seen.add(value as object)
768+
if (Array.isArray(value)) return value.map(walk)
769+
const out: Record<string, unknown> = {}
770+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
771+
out[k] = k === 'uri' && typeof v === 'string' ? normalize(v) : walk(v)
772+
}
773+
return out
774+
}
775+
776+
return walk(payload) as T
777+
}
778+
752779
/**
753780
* Convenience type for the export parameters.
754781
*/

apps/editor/src/ui/editor/EditorCanvas.tsx

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { CaseEditorNodeType, CaseEditorEdge } from '@/ui/editor/reactflow/t
2121
import type { CFDocument, CFItem, CFPackage } from '@/domain/case/types'
2222
import { useAuth } from '@/app/providers/AuthProvider'
2323
import { fromEditorGraph } from '@/ui/editor/reactflow/mapping/fromEditorGraph'
24-
import { frameworkToCfPackage, toOpenCaseFormat } from '@/application/framework/mappers/case/toCasePackage'
24+
import { absolutizeCaseUris, frameworkToCfPackage, toOpenCaseFormat } from '@/application/framework/mappers/case/toCasePackage'
2525

2626
type EditorCanvasProps = {
2727
onBack?: () => void
@@ -30,9 +30,11 @@ type EditorCanvasProps = {
3030
isPublishedToOpenCase?: boolean
3131
/** Archive the current framework on the server and navigate home */
3232
onArchiveFramework?: () => Promise<void>
33+
/** Fetch the published CFPackage from the server (returns CASE JSON with absolute URIs) */
34+
onFetchCfPackage?: () => Promise<CFPackage>
3335
}
3436

35-
export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpenCase, onArchiveFramework }: Readonly<EditorCanvasProps>) {
37+
export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpenCase, onArchiveFramework, onFetchCfPackage }: Readonly<EditorCanvasProps>) {
3638
const { status: authStatus, userName, tenantId, signOut, changePassword } = useAuth()
3739
const {
3840
nodes,
@@ -89,6 +91,7 @@ export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpen
8991
const [externalFwViewportCenter, setExternalFwViewportCenter] = useState<{ x: number; y: number } | undefined>(undefined)
9092
const [cfPackageDialogOpen, setCfPackageDialogOpen] = useState(false)
9193
const [generatedCfPackage, setGeneratedCfPackage] = useState<CFPackage | null>(null)
94+
const [viewCaseLoading, setViewCaseLoading] = useState(false)
9295
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle')
9396
const [saveError, setSaveError] = useState<string | null>(null)
9497

@@ -139,20 +142,33 @@ export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpen
139142
const saveCtxRef = useRef({ caseVersion, edgeType: settings.edgeType, cfItemTypes, cfSubjects, cfConcepts, cfLicenses, cfAssociationGroupings })
140143
saveCtxRef.current = { caseVersion, edgeType: settings.edgeType, cfItemTypes, cfSubjects, cfConcepts, cfLicenses, cfAssociationGroupings }
141144

142-
// Generate CFPackage from current editor state and open the viewer
143-
const handleViewCFPackage = useCallback(() => {
144-
const { nodes: n, edges: e } = graphRef.current
145-
const ctx = saveCtxRef.current
146-
const { framework, layout } = fromEditorGraph({ graph: { nodes: n, edges: e } })
147-
const cfPackage = frameworkToCfPackage({
148-
framework, layout, incrementVersion: false,
149-
caseVersion: ctx.caseVersion, edgeType: ctx.edgeType,
150-
cfItemTypes: ctx.cfItemTypes, cfSubjects: ctx.cfSubjects,
151-
cfConcepts: ctx.cfConcepts, cfLicenses: ctx.cfLicenses, cfAssociationGroupings: ctx.cfAssociationGroupings,
152-
})
153-
setGeneratedCfPackage(cfPackage)
154-
setCfPackageDialogOpen(true)
155-
}, [])
145+
// Open the CFPackage viewer. Fetches from the server when published (absolute URIs);
146+
// falls back to local generation for unsaved/draft frameworks.
147+
const handleViewCFPackage = useCallback(async () => {
148+
if (isPublishedToOpenCase && onFetchCfPackage) {
149+
setCfPackageDialogOpen(true)
150+
setViewCaseLoading(true)
151+
try {
152+
const pkg = await onFetchCfPackage()
153+
setGeneratedCfPackage(pkg)
154+
} finally {
155+
setViewCaseLoading(false)
156+
}
157+
} else {
158+
const { nodes: n, edges: e } = graphRef.current
159+
const ctx = saveCtxRef.current
160+
const { framework, layout } = fromEditorGraph({ graph: { nodes: n, edges: e } })
161+
const cfPackage = frameworkToCfPackage({
162+
framework, layout, incrementVersion: false,
163+
caseVersion: ctx.caseVersion, edgeType: ctx.edgeType,
164+
cfItemTypes: ctx.cfItemTypes, cfSubjects: ctx.cfSubjects,
165+
cfConcepts: ctx.cfConcepts, cfLicenses: ctx.cfLicenses, cfAssociationGroupings: ctx.cfAssociationGroupings,
166+
})
167+
const caseJson = toOpenCaseFormat(cfPackage)
168+
setGeneratedCfPackage(absolutizeCaseUris(caseJson, window.location.origin))
169+
setCfPackageDialogOpen(true)
170+
}
171+
}, [isPublishedToOpenCase, onFetchCfPackage])
156172

157173
// Save: Generate CFPackage with version increment and POST to server
158174
const handleSave = useCallback(async () => {
@@ -167,7 +183,6 @@ export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpen
167183
})
168184
const openCasePackage = toOpenCaseFormat(cfPackage)
169185
console.log('[Save] Generated OpenCASE package:', openCasePackage)
170-
setGeneratedCfPackage(cfPackage)
171186

172187
if (onSaveToServer) {
173188
setSaveStatus('saving')
@@ -1344,6 +1359,7 @@ export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpen
13441359
}}
13451360
cfPackage={generatedCfPackage}
13461361
caseVersion={caseVersion}
1362+
loading={viewCaseLoading}
13471363
/>
13481364

13491365
</div>

apps/editor/src/ui/editor/components/ViewCFPackageDialog.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,25 @@ import { Button } from '@/ui/shared/components/ui/button'
33
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/ui/shared/components/ui/dialog'
44
import type { CFPackage } from '@/domain/case/types'
55
import type { CaseVersion } from '@/application/framework/mappers/case/CasePackageSnapshot'
6-
import { toOpenCaseFormat } from '@/application/framework/mappers/case/toCasePackage'
76

87
type Props = {
98
open: boolean
109
onClose: () => void
1110
cfPackage: CFPackage | null
1211
caseVersion: CaseVersion
12+
loading?: boolean
1313
}
1414

15-
export default function ViewCFPackageDialog({ open, onClose, cfPackage, caseVersion }: Readonly<Props>) {
15+
export default function ViewCFPackageDialog({ open, onClose, cfPackage, caseVersion, loading }: Readonly<Props>) {
1616
const [copied, setCopied] = useState(false)
1717

18-
// Convert to OpenCASE REST API format (lowercase property names)
1918
const jsonString = useMemo(() => {
2019
if (!cfPackage) return ''
21-
const openCasePackage = toOpenCaseFormat(cfPackage)
22-
return JSON.stringify(openCasePackage, null, 2)
20+
return JSON.stringify(cfPackage, null, 2)
2321
}, [cfPackage])
2422

2523
const copyToClipboard = useCallback(async () => {
26-
if (!jsonString) return
24+
if (!jsonString || loading) return
2725
try {
2826
await globalThis.navigator?.clipboard?.writeText(jsonString)
2927
setCopied(true)
@@ -39,10 +37,10 @@ export default function ViewCFPackageDialog({ open, onClose, cfPackage, caseVers
3937
setCopied(true)
4038
globalThis.setTimeout(() => setCopied(false), 2000)
4139
}
42-
}, [jsonString])
40+
}, [jsonString, loading])
4341

4442
const downloadJson = useCallback(() => {
45-
if (!cfPackage) return
43+
if (!cfPackage || loading) return
4644
const blob = new Blob([jsonString], { type: 'application/json' })
4745
const url = URL.createObjectURL(blob)
4846
const a = document.createElement('a')
@@ -53,7 +51,7 @@ export default function ViewCFPackageDialog({ open, onClose, cfPackage, caseVers
5351
a.click()
5452
document.body.removeChild(a)
5553
URL.revokeObjectURL(url)
56-
}, [cfPackage, jsonString])
54+
}, [cfPackage, loading, jsonString])
5755

5856
const stats = useMemo(() => {
5957
if (!cfPackage) return null
@@ -91,18 +89,18 @@ export default function ViewCFPackageDialog({ open, onClose, cfPackage, caseVers
9189

9290
<div className="relative max-h-[50vh] overflow-auto rounded-lg border border-black/10 bg-slate-900">
9391
<pre className="p-4 text-xs leading-relaxed text-slate-100">
94-
<code>{jsonString || 'No CFPackage data'}</code>
92+
<code>{loading ? 'Loading…' : jsonString || 'No CFPackage data'}</code>
9593
</pre>
9694
</div>
9795

9896
<DialogFooter className="gap-2 sm:gap-2">
9997
<Button variant="secondary" onClick={onClose}>
10098
Close
10199
</Button>
102-
<Button variant="secondary" onClick={downloadJson} disabled={!cfPackage}>
100+
<Button variant="secondary" onClick={downloadJson} disabled={!cfPackage || loading}>
103101
Download JSON
104102
</Button>
105-
<Button onClick={copyToClipboard} disabled={!cfPackage}>
103+
<Button onClick={copyToClipboard} disabled={!cfPackage || loading}>
106104
{copied ? 'Copied!' : 'Copy to Clipboard'}
107105
</Button>
108106
</DialogFooter>

apps/opencase/eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default defineConfig([
1212
},
1313
},
1414
{
15-
files: ['**/*.cjs'],
15+
files: ['**/*.cjs', '**/*.mjs'],
1616
languageOptions: {
1717
globals: {
1818
...globals.node,

0 commit comments

Comments
 (0)