Skip to content

Commit be86d90

Browse files
committed
Adds ability to import frameworks from JSON payloads
1 parent 2285700 commit be86d90

14 files changed

Lines changed: 543 additions & 126 deletions

File tree

apps/editor/src/infrastructure/caseApi/CaseApiClient.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,21 +154,27 @@ export class CaseApiClient {
154154
}
155155

156156
/**
157-
* Import a CFPackage from an external CASE endpoint via the OpenCASE backend.
157+
* Import a CFPackage into the tenant's framework store via the OpenCASE backend,
158+
* either by fetching it from an external CASE endpoint (avoiding CORS) or from a
159+
* CFPackage JSON payload provided directly (e.g. pasted by the user). Exactly one
160+
* of `endpointUrl` or `cfPackage` should be provided.
158161
*
159-
* The backend fetches the package (avoiding CORS), validates it, injects
160-
* source provenance metadata, and stores it in the tenant's framework store.
162+
* The backend validates the package, injects source provenance metadata
163+
* (when imported from a URL), and stores it in the tenant's framework store.
161164
*/
162165
async importCfPackage(params: {
163166
tenantId: string
164-
endpointUrl: string
167+
endpointUrl?: string
168+
cfPackage?: object
165169
caseVersion?: 'v1p0' | 'v1p1'
166170
accessToken?: string
167171
}): Promise<{ status: string; id: string; version: number; validationWarnings?: string[] }> {
168172
const v = params.caseVersion ?? 'v1p1'
169173
const url = `/management/tenants/${encodeURIComponent(params.tenantId)}/ims/case/${v}/CFPackages/import`
170174

171-
const body: Record<string, unknown> = { endpointUrl: params.endpointUrl }
175+
const body: Record<string, unknown> = {}
176+
if (params.endpointUrl) body.endpointUrl = params.endpointUrl
177+
if (params.cfPackage) body.cfPackage = params.cfPackage
172178
if (params.accessToken) body.accessToken = params.accessToken
173179

174180
const res = (await this._http.post(url, body)) as unknown

apps/editor/src/ui/home/HomeScreen.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,23 @@ export default function HomeScreen({
338338
return result
339339
}, [api, tenantId, loadFrameworks, onOpenRemoteFramework])
340340

341+
// Handle import from pasted CFPackage JSON
342+
const handleImportJson = useCallback(async (cfPackage: object) => {
343+
if (!tenantId) throw new Error('Not authenticated')
344+
const result = await api.importCfPackage({
345+
tenantId,
346+
cfPackage,
347+
})
348+
// Refresh the framework list
349+
void loadFrameworks()
350+
// Open the imported framework in the editor
351+
if (result.id && onOpenRemoteFramework) {
352+
void onOpenRemoteFramework(result.id)
353+
}
354+
setImportOpen(false)
355+
return result
356+
}, [api, tenantId, loadFrameworks, onOpenRemoteFramework])
357+
341358
const isAuthenticated = status === 'authenticated'
342359

343360
// IDs of server frameworks, used to exclude drafts that have since been saved
@@ -895,6 +912,7 @@ export default function HomeScreen({
895912
open={importOpen}
896913
onCancel={() => setImportOpen(false)}
897914
onImport={handleImport}
915+
onImportJson={handleImportJson}
898916
/>
899917

900918
<UploadFrameworkDialog
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { describe, it, expect, vi } from 'vitest'
2+
import '@testing-library/jest-dom'
3+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
4+
import ImportFrameworkDialog, { type ImportResult } from './ImportFrameworkDialog'
5+
6+
const importResult: ImportResult = { status: 'imported', id: 'doc-1', version: 1 }
7+
8+
describe('ImportFrameworkDialog', () => {
9+
it('imports from a URL by default', async () => {
10+
const onImport = vi.fn().mockResolvedValue(importResult)
11+
const onImportJson = vi.fn()
12+
13+
render(
14+
<ImportFrameworkDialog open onCancel={vi.fn()} onImport={onImport} onImportJson={onImportJson} />,
15+
)
16+
17+
fireEvent.change(screen.getByLabelText('Framework URL'), {
18+
target: { value: 'https://case.example.org/ims/case/v1p1/CFPackages/doc-1' },
19+
})
20+
fireEvent.click(screen.getByRole('button', { name: /import framework/i }))
21+
22+
await waitFor(() => expect(onImport).toHaveBeenCalledWith(
23+
'https://case.example.org/ims/case/v1p1/CFPackages/doc-1',
24+
undefined,
25+
))
26+
expect(onImportJson).not.toHaveBeenCalled()
27+
})
28+
29+
it('switches to Paste JSON mode and imports a parsed CFPackage', async () => {
30+
const onImport = vi.fn()
31+
const onImportJson = vi.fn().mockResolvedValue(importResult)
32+
33+
render(
34+
<ImportFrameworkDialog open onCancel={vi.fn()} onImport={onImport} onImportJson={onImportJson} />,
35+
)
36+
37+
fireEvent.click(screen.getByRole('button', { name: 'Paste JSON' }))
38+
39+
const cfPackage = { CFDocument: { identifier: 'doc-1' } }
40+
fireEvent.change(screen.getByLabelText('Framework JSON'), {
41+
target: { value: JSON.stringify(cfPackage) },
42+
})
43+
fireEvent.click(screen.getByRole('button', { name: /import framework/i }))
44+
45+
await waitFor(() => expect(onImportJson).toHaveBeenCalledWith(cfPackage))
46+
expect(onImport).not.toHaveBeenCalled()
47+
})
48+
49+
it('shows an inline error for malformed JSON without calling onImportJson', async () => {
50+
const onImport = vi.fn()
51+
const onImportJson = vi.fn()
52+
53+
render(
54+
<ImportFrameworkDialog open onCancel={vi.fn()} onImport={onImport} onImportJson={onImportJson} />,
55+
)
56+
57+
fireEvent.click(screen.getByRole('button', { name: 'Paste JSON' }))
58+
fireEvent.change(screen.getByLabelText('Framework JSON'), {
59+
target: { value: '{ not valid json' },
60+
})
61+
fireEvent.click(screen.getByRole('button', { name: /import framework/i }))
62+
63+
expect(await screen.findByText(/doesn.t look like valid json/i)).toBeInTheDocument()
64+
expect(onImportJson).not.toHaveBeenCalled()
65+
expect(onImport).not.toHaveBeenCalled()
66+
})
67+
68+
it('surfaces validation warnings returned from a successful import', async () => {
69+
const onImport = vi.fn()
70+
const onImportJson = vi.fn().mockResolvedValue({
71+
...importResult,
72+
validationWarnings: ['CFDocument.title is required'],
73+
})
74+
75+
render(
76+
<ImportFrameworkDialog open onCancel={vi.fn()} onImport={onImport} onImportJson={onImportJson} />,
77+
)
78+
79+
fireEvent.click(screen.getByRole('button', { name: 'Paste JSON' }))
80+
fireEvent.change(screen.getByLabelText('Framework JSON'), {
81+
target: { value: '{ "CFDocument": { "identifier": "doc-1" } }' },
82+
})
83+
fireEvent.click(screen.getByRole('button', { name: /import framework/i }))
84+
85+
expect(await screen.findByText('CFDocument.title is required')).toBeInTheDocument()
86+
})
87+
88+
it('surfaces a thrown error from onImportJson', async () => {
89+
const onImport = vi.fn()
90+
const onImportJson = vi.fn().mockRejectedValue(new Error('import_failed: Schema validation failed'))
91+
92+
render(
93+
<ImportFrameworkDialog open onCancel={vi.fn()} onImport={onImport} onImportJson={onImportJson} />,
94+
)
95+
96+
fireEvent.click(screen.getByRole('button', { name: 'Paste JSON' }))
97+
fireEvent.change(screen.getByLabelText('Framework JSON'), {
98+
target: { value: '{ "CFDocument": { "identifier": "doc-1" } }' },
99+
})
100+
fireEvent.click(screen.getByRole('button', { name: /import framework/i }))
101+
102+
expect(await screen.findByText('import_failed: Schema validation failed')).toBeInTheDocument()
103+
})
104+
})

0 commit comments

Comments
 (0)