feat: content engine file fields - #752
Draft
aXenDeveloper wants to merge 5 commits into
Draft
Conversation
`field.file()` stores one file by reference: the column is an `integer`
foreign key into `core_files` with `ON DELETE RESTRICT`, and nothing about
the file is copied onto the content row.
- `maxBytes` is mandatory - there is intentionally no unlimited file field,
and a zero, negative, fractional or infinite value is a definition-time
error.
- `allowedExtensions` and `allowedMimeTypes` are two independent rules; with
both set, both must match, so a PNG renamed to `.gif` is refused.
Extensions normalise, so `GIF`, `.gif` and `.Gif` are one rule.
- One generated multipart route per content type,
`POST /admin/content/{module}/uploads/{field}`, driven from the browser by
TanStack Query. No binary ever crosses a Server Action; the content
mutation stays JSON carrying the identifier.
- A save re-validates the referenced `core_files` row, so uploading a PDF
for one field cannot assign it to a GIF-only one.
- `StorageModel.deleteFile` now deletes the row first and the blob second:
still referenced answers 409 `FILE_IN_USE` with the bytes intact.
- Retained revisions pin the files their snapshots name in
`core_content_file_refs`, so an old revision stays restorable and pruning
releases the file through the cascade.
- Reusable `AutoFormFile` with drag & drop, replace, remove, image preview
and a constraint line that always shows the allowed formats and maximum
size - read from the same descriptor the server validates against.
- Blog articles gain a shared `coverImage` and a localized `coverImageAlt`;
`example.article` gains a GIF-only `animation` field as the strict
reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s as a dialog A page-mode form stays mounted across its own saves, but `expectedVersion` was only ever read once - on mount - and after a conflict was resolved. So the second save of a session guarded on a version the record had already left behind, and a solo editor got "someone else saved this first" about their own previous click. The translation half of this was already handled: the composite save reads every translation back so the next one holds their new versions. The base row's version was simply not reported. `editContentAction` and `editLocalizedContentAction` now return it, and the form advances on it - plus on any newer `data.version` the server hands down, which covers a publish, an unpublish or a restore moving the version while the form is open. Forwards only, so a stale list row cannot drag the precondition back and a reloaded conflict cannot be un-resolved. The notice is now an alert dialog rather than a banner above the fields. A save that did not happen is not something to notice later, and on a form long enough to scroll the banner could be off screen entirely - which is exactly how somebody presses Save, sees nothing, and presses it again. All three rules survive: nothing typed is discarded (the form stays mounted behind the overlay), nothing is overwritten automatically, and no field is merged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
`core.content.conflict.desc` was written as
`{name, select, other {record}}` - a select with only an `other` branch, so
it required a `name` argument and then ignored it, always rendering the word
"record". The dialog passed only `{ version }`.
A missing ICU argument is not a blank in next-intl. It is a
FORMATTING_ERROR, and the fallback it renders is the key path - so an editor
whose save had just been refused read the literal string
`core.content.conflict.desc` where the explanation should have been. The one
screen that has to explain itself said nothing.
The message now takes `{name}` and the dialog passes the content type's
singular label, matching every other message in the namespace ("Add a new
{name}.", "{name} has been updated."). The copy also says the save did not
go through, which the old wording never did - it said the record "moved"
and that nothing was lost, leaving "press Save again" as the obvious next
move.
The test asserts the thing that actually broke: that each message's
placeholders are exactly the arguments its call site passes. Reverting
either half of this fix fails it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…again"
`uploadContentFile` read the error body with `response.json()` and fell back
to "The upload failed. Please try again." for anything else. Hono renders an
`HTTPException`'s message as *plain text*, so every failure outside the
route's own body arrived as text, failed the parse, and was replaced by a
sentence that says nothing:
"File exceeds the maximum size of 5242880 bytes" -> please try again
"Unsupported file type: application/pdf" -> please try again
"Invalid or corrupt image file" -> please try again
"Storage provider not found" -> please try again
The last one is the worst: nothing is wrong with the file, and the editor
retries a misconfiguration for ever.
Three changes:
- The route answers JSON `{ code, message }` for every refusal it owns -
unknown field, forbidden - and re-shapes the `HTTPException`s escaping
`StorageModel` into the same envelope, so a corrupt file and a missing
adapter both arrive as something the browser can read and tell apart.
- The client reads the body in three layers: JSON, then plain text, then the
status. Plain text was the layer that was missing. `413` gets its own
sentence because a body the platform rejects never reaches the route, so
no `maxBytes` check ran and the limit to raise is not the field's.
- The uploader restates the three rules it can say better - too large, wrong
type, wrong extension - in the reader's own language from the field's own
limits, and shows everything else verbatim.
`rawApiFetch` also stops discarding the body of a 500: it read
`statusText ?? errorText`, which never fell through, so the only part that
said what went wrong was thrown away on every 500.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Improving Documentation
pnpm lint:fixto fix formatting issues before opening the PR.Description
What?
Why?