Skip to content

feat(sheets)!: upload cell media through the tenant storage endpoint - #59

Merged
yyyyaaa merged 5 commits into
mainfrom
phat/sheets-cell-uploads
Nov 5, 2026
Merged

yyyyaaa merged 5 commits into
mainfrom
phat/sheets-cell-uploads

Conversation

@yyyyaaa

@yyyyaaa yyyyaaa commented Nov 4, 2026

Copy link
Copy Markdown
Contributor
  • SheetsConfig.storage: { endpoint, execute? } sends upload discovery, the presigned upload and preview lookups to the tenant storage endpoint. Cell patches stay on the data endpoint. Without storage or an injected uploader, media cells are read-only and show a tooltip.
  • Uploads omit bucketKey and send isPublic: true, so files land in the tenant's default-public bucket. upload.bucketKey remains an explicit override. Discovery prefers uploadAppFile and requires the file projection.
  • Image and upload cells store { id, key, bucket_id, mime, size, filename, url? }. Previews resolve signed downloadUrls in batches of up to 100 and refresh at about 50 min. Attachment cells store the public URL, or report that the bucket needs a public URL prefix.
  • A failed PUT no longer deletes the files row; the server expires it.
  • Media writes use the table's real key and inflection. A draft row is created once and a retry targets that row. Clear persists null.
  • Cell editor popovers open and close with the transitions.dev dropdown recipe (0.97 → 1 in 250ms; 150ms close).
  • The Blocks console passes its storage transport. Adds prepack builds for data, sheets and blocks-ui.

Breaking: SheetsUploadResult is now { file: SheetsFileValue; url?: string }, so injected executeUpload functions must return the file projection. executeFieldUpload(target, file, transport) has a new signature. useSheetsImageUpload is removed; use useSheetsFieldUpload.

Before/after screenshots to follow.

🤖 Generated with Claude Code

https://claude.ai/code/session_017WbsG7JQsKEMKNgVsc4psk

- SheetsConfig.storage routes upload discovery, the presigned upload and
  preview lookups to the storage endpoint; cell patches stay on the data
  endpoint. Without storage or an injected uploader, media cells are
  read-only with a tooltip.
- Uploads omit bucketKey and request the public default (isPublic: true)
  unless the host sets upload.bucketKey. Discovery prefers uploadAppFile
  and requires the file projection.
- Image/upload cells store the file projection {id, key, bucket_id, mime,
  size, filename, url?}; previews resolve signed downloadUrls in batches
  and refresh before expiry. Attachment cells store the public URL, or
  fail with a clear cause when the bucket has no public URL prefix.
- Failed PUTs no longer delete the files row (the server expires it).
- Media writes use the table's real key and inflection; draft rows create
  once and retry against the created row; clear persists null.
- Remove the unused upload editors and useSheetsImageUpload; update docs.

BREAKING CHANGE: SheetsUploadResult is now { file: SheetsFileValue; url?: string }
instead of { url, ...metadata }, so injected executeUpload functions must return
the file projection. executeFieldUpload takes (target, file, transport).
useSheetsImageUpload is removed; use useSheetsFieldUpload.
Editor overlays grow from the anchor cell's corner (0.97 to 1, 250ms) and
fade out to 0.99 over 150ms on close, reading the host's --dropdown-*
tokens with the transitions.dev defaults. The close plays on an inert copy
of the last frame; reduced motion skips both. Placement measures layout
size and re-clamps when the editor resizes.
@tenki-reviewer

tenki-reviewer Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 2 medium

💬 Inline comments (2)


This PR moves sheet media handling to a presigned-upload flow: capability discovery against a storage endpoint, a SheetsFileValue projection, and a new useSheetsFieldUpload hook that uploads then persists via the PostGraphile adapter. Signed preview URLs are fetched in batches of up to 100 through a new useSheetsMediaUrl hook scoped per identity, and editors, overlays, and cell views are rewired to the new contract.

Files Change
packages/sheets/src/context/*, packages/sheets/src/hooks/* Add the storage transport/presigned upload seam, batched signed-URL hook, and rewritten upload hook
packages/sheets/src/adapter/*, packages/sheets/src/cell-model/*, packages/sheets/src/cell-types/* Adapter executeFieldUpload persistence, image/attachment cell model and type mapping
packages/sheets/src/grid-dom/*, packages/sheets/src/grid/* Image/upload editors (DOM and grid variants), overlay controller motion, batch-commit gating by canUpload
packages/sheets/src/index.ts, packages/sheets/registry.json, packages/sheets/scripts/build-registry.ts, package manifests Public exports and registry entries updated; upload-editor removed, media-url hook added
packages/sheets/src/testing/*, packages/sheets/src/utils/*, test files Mocks, file utilities, image-status race fix, and updated/new tests
apps/blocks/src/blocks/feature-packs/data/data-console-module.tsx, docs Data console wiring for the new storage config and docs refresh

Reviewed commit: a91d6f8

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworks the sheets media pipeline around a dedicated presigned-upload seam with batched signed-URL previews, a new attachment cell type, and registry/docs updates.

Key findings

Comment on lines +71 to +73
if (isAttachment) {
if (!projection.url) throw new Error('attachment columns need a public bucket with a public URL prefix');
value = projection.url;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

Attachment upload on private bucket leaves orphan file row

For attachment columns, executeFieldUpload runs the full upload first — the server creates the file row and the PUT stores the bytes — and only afterwards throws when projection.url is missing (packages/sheets/src/hooks/use-sheets-upload.ts:72). On a private bucket with no public URL prefix every attachment upload therefore succeeds at the storage layer and then fails, leaving an orphaned file row on each attempt, since the post-upload error path performs no cleanup.

📋 Prompt for AI Agents

In packages/sheets/src/hooks/use-sheets-upload.ts around lines 63-80, the attachment public-URL check (if (!projection.url) throw ...) at line 72 runs after await executeUpload(...) has already created the server file row and completed the PUT, so on private buckets every attempt orphans a file row. Move the gate ahead of the upload: before calling executeUpload, determine whether the target column/bucket yields a public url (e.g. via discovered storage capability or config) and throw the 'attachment columns need a public bucket' error before any server row is created; keep the post-upload check as a defensive assertion only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this one. Sheets always uploads with isPublic: true and no bucketKey, so files land in default-public. A missing url there means the server's public URL prefix isn't configured, and the client can't learn that before it uploads. Deleting file rows from the client is out of scope for this phase. Superseded or orphaned rows are left to the server-side sweep planned for the backend follow-up, so the post-upload error stays as the guard.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood — that's a deliberate design decision rather than an oversight. The reasoning makes sense: since uploads always target the default-public bucket, a missing url reflects a server misconfiguration that the client cannot detect pre-flight, and server-side cleanup is the planned remediation path. The post-upload check remains a valid defensive guard in that model.

I'll leave this open since the original concern (orphaned file rows on failure) still exists structurally, but I accept that the fix is deferred to a backend sweep rather than a client-side pre-flight gate.

- Cache unresolved signed-URL lookups for a minute so remounts and focus
  don't resend them.
- Resolve download URLs on storage endpoints without an upload mutation.
- Refetch the host grid whenever a draft row was persisted, even when the
  created row has no resolvable identity.
- Expose upload progress as a progressbar and announce the busy status.
@yyyyaaa
yyyyaaa merged commit 91ff12b into main Nov 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant