77 * atomic Immer producer → single Cmd+Z undo step.
88 */
99
10- import { Type , type Static } from '@sinclair/typebox'
1110import type { SiteImportAdapter , SiteImportTransaction } from '@core/siteImport'
12- import { parseJsonResponse } from '@core/utils/jsonValidate'
13- import { responseErrorMessage } from '@core/http'
1411import { installCmsGoogleFont } from '@core/persistence/cmsFonts'
12+ import {
13+ createCmsMediaFolder ,
14+ listCmsMediaFolders ,
15+ setCmsMediaAssetFolders ,
16+ uploadCmsMediaAsset ,
17+ type CmsMediaFolder ,
18+ } from '@core/persistence/cmsMedia'
19+ import { getErrorMessage } from '@core/utils/errorMessage'
1520import { useEditorStore } from '@site/store/store'
1621
17- // Minimal TypeBox schema for the upload response — both `id` and `publicPath`
18- // are needed: id to assign the asset to its destination folder, publicPath to
19- // stitch back into the imported HTML/CSS so references resolve.
20- const MediaUploadResponseSchema = Type . Object (
21- {
22- asset : Type . Object (
23- { id : Type . String ( ) , publicPath : Type . String ( ) } ,
24- { additionalProperties : true } ,
25- ) ,
26- } ,
27- { additionalProperties : true } ,
28- )
29-
30- // Subset of the folder list response — name / parentId let us match against an
31- // existing folder tree so a re-run of the wizard re-uses the same folders
32- // instead of accumulating `assets-2`, `assets-3` next to the originals.
33- const MediaFolderListResponseSchema = Type . Object (
34- {
35- folders : Type . Array (
36- Type . Object (
37- {
38- id : Type . String ( ) ,
39- name : Type . String ( ) ,
40- parentId : Type . Union ( [ Type . String ( ) , Type . Null ( ) ] ) ,
41- } ,
42- { additionalProperties : true } ,
43- ) ,
44- ) ,
45- } ,
46- { additionalProperties : true } ,
47- )
48-
49- const MediaFolderCreateResponseSchema = Type . Object (
50- {
51- folder : Type . Object (
52- { id : Type . String ( ) } ,
53- { additionalProperties : true } ,
54- ) ,
55- } ,
56- { additionalProperties : true } ,
57- )
58-
5922interface AdapterCallbacks {
6023 /** Stable id for the upload session (for logging). */
6124 sessionId : string
@@ -81,20 +44,14 @@ function dirSegments(path: string): string[] {
8144 return dir . split ( '/' ) . filter ( ( s ) => s . length > 0 )
8245}
8346
84- interface FolderIndexEntry {
85- id : string
86- parentId : string | null
87- name : string
88- }
89-
9047/**
9148 * In-memory cache of folder ids keyed by their full slash-delimited path
9249 * (`'assets'`, `'assets/img'`, …). Folders the user already has from
9350 * previous imports / manual uploads are matched against this index so a
9451 * second wizard run doesn't duplicate the tree.
9552 */
96- function buildFolderIndex ( folders : ReadonlyArray < FolderIndexEntry > ) : Map < string , string > {
97- const byId = new Map < string , FolderIndexEntry > ( )
53+ function buildFolderIndex ( folders : ReadonlyArray < CmsMediaFolder > ) : Map < string , string > {
54+ const byId = new Map < string , CmsMediaFolder > ( )
9855 for ( const f of folders ) byId . set ( f . id , f )
9956
10057 // Resolve each folder's full path by walking parents.
@@ -128,14 +85,7 @@ export function createSiteImportAdapter(opts: AdapterCallbacks): SiteImportAdapt
12885 if ( folderIndex ) return folderIndex
12986 if ( ! folderIndexPromise ) {
13087 folderIndexPromise = ( async ( ) => {
131- const res = await fetch ( '/admin/api/cms/media/folders' , { method : 'GET' } )
132- if ( ! res . ok ) {
133- throw new Error (
134- `[siteImportAdapter] Could not load folder index: ${ await responseErrorMessage ( res , 'Folder list failed' ) } ` ,
135- )
136- }
137- const payload = await parseJsonResponse ( res , MediaFolderListResponseSchema )
138- const idx = buildFolderIndex ( payload . folders )
88+ const idx = buildFolderIndex ( await listCmsMediaFolders ( ) )
13989 folderIndex = idx
14090 return idx
14191 } ) ( )
@@ -164,40 +114,22 @@ export function createSiteImportAdapter(opts: AdapterCallbacks): SiteImportAdapt
164114 parentId = cached
165115 continue
166116 }
167- // Explicit annotations break a TS inference cycle: `parentId` is
168- // reassigned from `payload.folder.id`, and without the annotation the
169- // compiler tries to resolve `payload`'s (and `res`'s) types through the
170- // captured `parentId` recursively (TS7022).
171- const res : Response = await fetch ( '/admin/api/cms/media/folders' , {
172- method : 'POST' ,
173- headers : { 'content-type' : 'application/json' } ,
174- body : JSON . stringify ( { name : segment , parentId } ) ,
175- } )
176- if ( ! res . ok ) {
177- throw new Error (
178- `[siteImportAdapter] Folder create failed for "${ cumulative } ": ${ await responseErrorMessage ( res , 'Create folder failed' ) } ` ,
179- )
180- }
181- const payload : Static < typeof MediaFolderCreateResponseSchema > =
182- await parseJsonResponse ( res , MediaFolderCreateResponseSchema )
183- index . set ( cumulative , payload . folder . id )
184- parentId = payload . folder . id
117+ const folder = await createCmsMediaFolder ( { name : segment , parentId } )
118+ index . set ( cumulative , folder . id )
119+ parentId = folder . id
185120 }
186121 return parentId
187122 }
188123
189124 async function assignAssetToFolder ( assetId : string , folderId : string ) : Promise < void > {
190- const res = await fetch ( `/admin/api/cms/media/${ assetId } /folders` , {
191- method : 'POST' ,
192- headers : { 'content-type' : 'application/json' } ,
193- body : JSON . stringify ( { add : [ folderId ] } ) ,
194- } )
195- if ( ! res . ok ) {
125+ try {
126+ await setCmsMediaAssetFolders ( assetId , { add : [ folderId ] } )
127+ } catch ( err ) {
196128 // Surface as a non-fatal log: the asset uploaded fine, only the
197129 // folder placement failed. The user can drag it to the right folder
198130 // by hand afterwards.
199131 console . warn (
200- `[siteImportAdapter] Asset ${ assetId } placed at the media root; folder assign returned ${ res . status } .` ,
132+ `[siteImportAdapter] Asset ${ assetId } placed at the media root; ${ getErrorMessage ( err , ' folder assignment failed' ) } .` ,
201133 )
202134 }
203135 }
@@ -209,17 +141,11 @@ export function createSiteImportAdapter(opts: AdapterCallbacks): SiteImportAdapt
209141
210142 async uploadAsset ( { path, bytes, mimeType } ) {
211143 opts . onUploadStart ?.( { path } )
212- const form = new FormData ( )
213144 // bytes comes from fflate/File APIs — always backed by a plain ArrayBuffer.
214145 // TypeScript's BlobPart constraint excludes SharedArrayBuffer; the cast is safe.
215146 const blobData : ArrayBuffer = bytes . slice ( ) . buffer as ArrayBuffer
216- form . append ( 'file' , new Blob ( [ blobData ] , { type : mimeType } ) , basename ( path ) )
217- const res = await fetch ( '/admin/api/cms/media' , { method : 'POST' , body : form } )
218- if ( ! res . ok ) {
219- const errMsg = await responseErrorMessage ( res , 'Upload failed' )
220- throw new Error ( `[siteImportAdapter] Upload failed for ${ path } : ${ errMsg } ` )
221- }
222- const payload = await parseJsonResponse ( res , MediaUploadResponseSchema )
147+ const file = new File ( [ blobData ] , basename ( path ) , { type : mimeType } )
148+ const asset = await uploadCmsMediaAsset ( file )
223149
224150 // Place the asset under a folder that mirrors its source bundle path.
225151 // Folder creation happens lazily here so a flat bundle (every asset at
@@ -230,11 +156,11 @@ export function createSiteImportAdapter(opts: AdapterCallbacks): SiteImportAdapt
230156 const segments = dirSegments ( path )
231157 if ( segments . length > 0 ) {
232158 const folderId = await ensureFolderPath ( segments )
233- if ( folderId ) await assignAssetToFolder ( payload . asset . id , folderId )
159+ if ( folderId ) await assignAssetToFolder ( asset . id , folderId )
234160 }
235161
236- opts . onUploadComplete ?.( { path, url : payload . asset . publicPath } )
237- return payload . asset . publicPath
162+ opts . onUploadComplete ?.( { path, url : asset . publicPath } )
163+ return asset . publicPath
238164 } ,
239165
240166 async commit ( recipe ) {
0 commit comments