Skip to content

Commit c5d5e1b

Browse files
committed
Add two-field upload independence tests + document bind destructure requirement [skip-release]
1 parent a690262 commit c5d5e1b

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,15 @@ Two phases: (1) reduce the bare `<Spinner>` flicker/layout-shift in `ResourceLoa
414414
**[#252](https://github.com/components-web-app/cwa-nuxt-module/issues/252) — DX: multiple uploadable file fields + rename Image APIs to File/Uploadable** ✅ Complete (closed)
415415
Multiple file fields were a faff (admin) or structurally unsupported (display). **Display side ✅ done** (hard-swap rename, pre-alpha): `withImage`/`useCwaImage`/`useCwaImageResource`/`ImageOpsType` **removed** → `withFile`/`useCwaFile`/`FileOpsType`; `withFile()` exposes its field under a single `files` map keyed by `fileProp` (reactive entries) and `useCwaComponent`'s merge accumulates the `files` key; per-field `useCwaFileField(props, { fileProp })` returns the flat refs. CLI scaffold type `'image'`→`'file'`. See `## Composable pipeline design → Built-in plugins`. **Admin side ✅ done (bind only):** `useCwaResourceUpload` now returns a typed `bind` object (`CwaResourceUploadBind`) to spread onto `CwaUiFormFile` (`v-bind="upload.bind"`) — covers `v-model`/`fileExists`/`disabled`/`change`/`delete`, leaving `label`/`accept` per field; default `fileDisplayType` `'Image'`→`'File'`. Deliberately **no** `<CwaResourceFileField>` wrapper and **no** `useCwaResourceUploads` — keep each field a separate composable call so fields can use different UI (avoid a monolithic component). Fully-auto `<CwaResourceFileFields>` rendering (zero declaration) was **dropped by decision** — not building it.
416416

417+
> **Spreading `bind` — always destructure it.** `bind` is a `ComputedRef<CwaResourceUploadBind>`. Vue only auto-unwraps refs that are **top-level** setup bindings, so `v-bind="bind"` works but `v-bind="upload.bind"` (nested access on the plain composable-return object) does **not** unwrap — it spreads the raw ref (`value`, `effect`, `[RefSymbol]`…) and TS complains `modelValue`/`fileExists` are missing (TS2345/TS2322). For one field: `const { bind } = useCwaResourceUpload(iri)`. For **multiple** fields, destructure-and-rename per call:
418+
> ```ts
419+
> const { bind: previewBind } = useCwaResourceUpload(iri, 'preview')
420+
> const { bind: fileBind } = useCwaResourceUpload(iri, 'file')
421+
> // <CwaUiFormFile v-bind="previewBind" label="…" accept="image/*" />
422+
> // <CwaUiFormFile v-bind="fileBind" label="…" />
423+
> ```
424+
> **Multi-field independence:** each `useCwaResourceUpload(iri, <prop>)` call reads/writes its own `mediaObjects[<prop>]` key, so two fields on one resource iri never couple at the composable level (guarded by the `two-field independence` tests in `cwa-resource-upload.spec.ts`). If fields *do* visibly couple in the app, it's a resource-data issue — the stored `_metadata.mediaObjects` lost a key — not this composable (see api-components-bundle #199 for the data-side gotchas: imagine on non-images, file-vs-image semantics).
425+
417426
**[#248](https://github.com/components-web-app/cwa-nuxt-module/issues/248) — Replace deprecated `installModule` with `moduleDependencies`**
418427
`@nuxt/kit`'s `installModule` is `@deprecated Use module dependencies`. `module.ts` uses `await installModule('nuxt-og-image')` in `setup`. Migrate to the `moduleDependencies` field on `defineNuxtModule` and drop the import. Mechanical; verify OG-image + sitemap handlers still work.
419428
@@ -622,7 +631,7 @@ defineExpose(exposeMeta)
622631

623632
`useCwaCollectionResource` is a thin wrapper over its plugin (BC safe).
624633

625-
**File fields (#252 — renamed from Image):** the file APIs handle any uploadable file, not just images. `withFile()` exposes a field under a single **`files` map keyed by `fileProp`** (default `'file'`) on the `useCwaComponent` return — use it multiple times for multiple fields (`files.heroImage.contentUrl`, `files.thumbnail.contentUrl`). Entries are `reactive`, so nested refs unwrap in templates (no `.value`). `useCwaComponent`'s plugin merge **accumulates** the `files` key across plugins rather than shallow-overwriting it. The default template ref name for load detection is the `fileProp`. For the per-field, named-at-call-site style, `useCwaFileField(props, { fileProp })` returns the flat refs (`contentUrl`, `displayMedia`, `handleLoad`, `loaded`) — same `useCwaFile` under the hood. Old `withImage`/`useCwaImage`/`useCwaImageResource`/`ImageOpsType` were **removed** (hard swap, pre-alpha). CLI scaffold type `'image'` → `'file'`. **Admin side of #252 (bind object + `<CwaResourceFileField>` wrapper) still TODO.**
634+
**File fields (#252 — renamed from Image):** the file APIs handle any uploadable file, not just images. `withFile()` exposes a field under a single **`files` map keyed by `fileProp`** (default `'file'`) on the `useCwaComponent` return — use it multiple times for multiple fields (`files.heroImage.contentUrl`, `files.thumbnail.contentUrl`). Entries are `reactive`, so nested refs unwrap in templates (no `.value`). `useCwaComponent`'s plugin merge **accumulates** the `files` key across plugins rather than shallow-overwriting it. The default template ref name for load detection is the `fileProp`. For the per-field, named-at-call-site style, `useCwaFileField(props, { fileProp })` returns the flat refs (`contentUrl`, `displayMedia`, `handleLoad`, `loaded`) — same `useCwaFile` under the hood. Old `withImage`/`useCwaImage`/`useCwaImageResource`/`ImageOpsType` were **removed** (hard swap, pre-alpha). CLI scaffold type `'image'` → `'file'`. **Admin side of #252 done** — `useCwaResourceUpload` returns the typed `bind` object; spread it per field (always destructure — see the #252 issue entry). The `<CwaResourceFileField>` wrapper was dropped by decision.
626635

627636
---
628637

src/runtime/composables/cwa-resource-upload.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,52 @@ describe('useCwaResourceUpload', () => {
179179
expect(mockUpdateResource).not.toHaveBeenCalled()
180180
})
181181
})
182+
183+
// Two independent fields (e.g. `file` + `preview`) over the SAME resource iri. They share only the
184+
// resource; each instance must read/write its own `mediaObjects[filename]` key. This guards against
185+
// the fields visibly coupling — if they ever do, it is a resource-data/store issue (a mediaObjects
186+
// key going missing), NOT this composable. See api-components-bundle #199 for the data-side context.
187+
describe('two-field independence (file + preview on one resource)', () => {
188+
const withBoth = () => ref<any>({
189+
data: { _metadata: { mediaObjects: {
190+
file: [{ formattedFileSize: '1 MB' }],
191+
preview: [{ formattedFileSize: '2 MB' }],
192+
} } },
193+
})
194+
195+
test('each field reads its own mediaObjects key', () => {
196+
mockGetResource.mockReturnValue(withBoth())
197+
const file = useCwaResourceUpload(iri, 'file')
198+
const preview = useCwaResourceUpload(iri, 'preview')
199+
expect(file.filenameInputModel.value).toBe('Existing File (1 MB)')
200+
expect(preview.filenameInputModel.value).toBe('Existing File (2 MB)')
201+
})
202+
203+
test('selecting/typing on one field does not change the other', () => {
204+
mockGetResource.mockReturnValue(withBoth())
205+
const file = useCwaResourceUpload(iri, 'file')
206+
const preview = useCwaResourceUpload(iri, 'preview')
207+
// File.vue does `value.value = file.name` on select → onUpdate:modelValue
208+
file.bind.value['onUpdate:modelValue']('newly-picked.png')
209+
expect(file.filenameInputModel.value).toBe('newly-picked.png')
210+
expect(preview.filenameInputModel.value).toBe('Existing File (2 MB)') // unchanged
211+
})
212+
213+
test('a full-resource update keeping both keys leaves the other field intact', async () => {
214+
const resourceRef = withBoth()
215+
mockGetResource.mockReturnValue(resourceRef)
216+
const file = useCwaResourceUpload(iri, 'file')
217+
const preview = useCwaResourceUpload(iri, 'preview')
218+
// API returns a WHOLE new resource object (new refs) with both keys still present
219+
resourceRef.value = {
220+
data: { _metadata: { mediaObjects: {
221+
file: [{ formattedFileSize: '9 MB' }],
222+
preview: [{ formattedFileSize: '2 MB' }],
223+
} } },
224+
}
225+
await nextTick()
226+
expect(file.filenameInputModel.value).toBe('Existing File (9 MB)')
227+
expect(preview.filenameInputModel.value).toBe('Existing File (2 MB)') // must NOT couple
228+
})
229+
})
182230
})

0 commit comments

Comments
 (0)