Skip to content

Gallery coverField reads the raw field value as a URL, so an ADR-0104-conforming image value can never render a cover #3317

Description

@xuyushun441-sys

Found while fixing framework#4774 / framework#4891 (the app-showcase seeded data: URIs into an image field, which ADR-0104 rejects). Removing the bad seed data surfaced this: with a correct value the cover still would not render, so the gallery has no working path for the platform's current file contract.

The contract, backend side

Since ADR-0104 D3 wave 2 the stored value of a file / image / avatar / video / audio field is an opaque sys_file id (FileReferenceIdValueSchema/^[A-Za-z0-9_-]{1,64}$/). On read, the engine expands it in place into the rich form (ObjectQLEngine.resolveFileReferences, packages/objectql/src/engine.ts ~3857):

Resolve file-field id references to their expanded FileValueSchema form (ADR-0104 D3 wave 2). A file/image/… value stored as an opaque sys_file id string is enriched, in place, to { id, name, size, mimeType, url }url derived from the stable /files/:fileId resolver, never stored.

So a conforming record arrives at the client with cover as an object carrying url, not a string.

What the gallery does

packages/plugin-list/src/ObjectGallery.tsx:

const coverField = gallery?.coverField ?? schema.imageField ?? 'image';
// …
const hasAnyCover = useMemo(
    () => items.some((it) => typeof (it as any)[coverField] === 'string' && !!(it as any)[coverField]),
    [items, coverField],
);
// …
const imageUrl = item[coverField] as string | undefined;

Both reads assume the field value is the URL string. Against the expanded shape:

  • hasAnyCover is false (the value is an object, not a string), so showCoverArea is false and the cover area collapses for the whole gallery;
  • were it shown, imageUrl would be an object and '<img src={…}>' would fall to the onError letter-avatar.

Net: for a spec-correct image value the cover never appears. The only values that render today are exactly the ones ADR-0104 retired — an inline data: URI or an external URL — which is why this stayed invisible: the showcase was feeding it a data: URI.

coverField is documented in the spec as "Attachment/image field to display as card cover" (packages/spec/src/ui/view.zod.ts ~376), so image fields are the intended input, not an edge case.

Suggested direction

Resolve the cover through the value shape rather than assuming a string — accept { url } (the expanded read form), a bare string (legacy inline/external, still valid during the dual-mode window), and treat an opaque id with no expansion as "no cover" rather than a broken src. hasAnyCover has to use the same resolver or the area keeps collapsing.

Worth checking the same assumption in the sibling paths that thread imageField around — packages/plugin-list/src/ListView.tsx (~1319, ~1570), packages/app-shell/src/views/ObjectView.tsx (~1518), packages/plugin-view/src/ObjectView.tsx (~696) — and any avatar/attachment cell renderer with the same shape.

Repro

  1. showcase_task.cover is Field.image() and TaskViews binds gallery: { coverField: 'cover' }.
  2. Upload a cover on a task (which is how a managed file is meant to come into existence), so cover holds a real committed sys_file id.
  3. Open the task Gallery view — no cover renders.

Observed against framework main @ 6bc93dc.

Filed unassigned.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions