-
Notifications
You must be signed in to change notification settings - Fork 16
Template fields: fill a design from data instead of hand-editing canvas JSON - #6
Open
pallaoro wants to merge 3 commits into
Open
Template fields: fill a design from data instead of hand-editing canvas JSON #6pallaoro wants to merge 3 commits into
pallaoro wants to merge 3 commits into
Conversation
A design becomes a template the moment an object carries a `fieldName`. Two routes make that usable without the caller ever parsing canvas JSON: GET /api/designs/:id/fields — the fill schema (name, type, value, pages) POST /api/designs/:id/fill — substitute values; `save` writes a variant Naming happens in the editor: select a text or image object and fill in "Field name" in the properties panel. A name may repeat across objects and pages, which is how one logo or headline fills a whole carousel at once. Fabric v6 drops unknown properties on serialize and `canvas.toJSON()` no longer accepts a property list, so `fieldName` is registered once via `FabricObject.customProperties` and every existing toJSON call site is left alone. Also fixes the properties panel losing track of what is selected. `updateSelectedObject` forced a re-render by replacing the selected object with a spread copy, which strips the prototype — so after a single edit a textbox reported no `type`, failed every `instanceof`, rendered shape controls, and threw on the next edit because `.set()` had gone with it. A version counter re-renders instead, keeping the live Fabric instance.
Also pins the serialised type names in a test. Fabric v6 writes "Textbox", "Image" and "Rect" once a canvas has been through the editor, while the seed templates use the lowercase spellings — field detection has to accept both.
`save: true` writes an editable design, not a rendered image, and every one is a row that GET /api/designs returns in full with no pagination — a three-object card is ~2.5KB serialised, a branded one with an image 10-40KB. So a few hundred saved variants are a few hundred gallery entries, not a few hundred finished graphics, and the docs should not have said otherwise. Real bulk output wants the fill loop plus browser-side rasterising, which is the follow-up. Until then this is a small-N convenience, and agent.md now says so where an agent will read it before looping.
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.
Uh oh!
There was an error while loading. Please reload this page.
Why
A design in OpenDesign can only be changed by replacing its whole
canvas_jsonblob. To swap one headline, a caller has to fetch the design, parse Fabric JSON,
guess which
objects[]entry is the right textbox, mutate.text, and PUT thelot back. That rules out the thing this category is actually bought for:
producing many on-brand variants of one design from a list of data.
Everything else needed for that is already here — self-hosted, embeddable,
white-label, MIT, no per-seat pricing. The missing piece was a schema.
What this adds
Give any text or image object a Field name in the properties panel. The
design is now a template:
/api/designs/:id/fieldsname,type, currentvalue,page_ids/api/designs/:id/fillsave: truewrites the result as a new designBehaviour worth calling out:
logoacross five pages fills all five — carousels.take
text, images takesrc. A name shared by both still does the rightthing on each.
unmatchedwith a 200.filldoes not mutate the stored design unless you ask it to.ratio is stretched. Documented in the README.
Two Fabric v6 details that shaped the implementation
canvas.toJSON()takes no arguments in v6 — the JSDoc shipped in thepackage still shows the v5
toJSON([...])form, which is a trap. The v6mechanism is the static
FabricObject.customProperties(
fabric/dist/index.mjs:8111). RegisteringfieldNamethere once inmain.tsxmakes it survive every save and reload, so none of the sixexisting
toJSON()call sites had to change.typeas"Textbox"/"Image"/"Rect", while the seed templates use lowercase. Detectioncase-folds, and there is a test pinning both spellings.
Bug fixed along the way
updateSelectedObjectforced a re-render by replacing the selected object witha spread copy:
Spreading a class instance drops the prototype, and in Fabric v6
typeis aprototype getter and
set()is a prototype method. Verified in the browser:So after a single property edit the panel lost the object's type — a textbox
rendered shape controls — and the next edit on the same selection would throw
because
.set()had gone with the prototype. Replaced with a version counterthat re-renders while keeping the live Fabric instance.
Verification
The API was exercised end to end against a running Worker (
wrangler devwithPR #4 merged in locally, since the worker does not boot on
mainwithout it):/fieldsreturns both fields, withlogocorrectly listing twopage_ids/fillfills text and image, fills the same name on both pages, leaves anuntagged object alone, returns
unmatched: ["nope"], and leaves the storeddesign byte-identical
save: truecreates a distinct design with fresh page ids, preserving pagetitles and
sort_order; a numeric value is stringifiedthe objects come back Fabric-serialised (
"Textbox", withfontFamily) andfieldNameintact, which is the property most at risk13 unit tests cover the pure fill logic including groups, corrupt canvases,
blank/non-string names, empty-string fills, and non-mutation.
In the editor, the control renders in the Text panel and reads back an existing
name (observed live in the DOM as
header=TEXT | fieldInputValue="author").Typing a new name and the two-consecutive-edits regression were not driven
interactively — the browser harness kept clearing the canvas selection between
calls. The commit path is
updateSelectedObject({ fieldName }), the same callevery other control in that panel uses.
Notes for the reviewer
esbuild+@types/nodeas devDependencies and atestscript. Run itwith
pnpm test.pnpm install,pnpm devandpnpm testall fail on a fresh clone, fora reason that predates this branch:
pnpm-workspace.yamlstill has pnpm'splaceholder
allowBuildsvalues (canvas: set this to true or false, and thesame for
esbuild,sharp,workerd). Left alone here because picking thosevalues is a supply-chain call and PR Carousel PDF export, per-page PNGs, and a display-independent export size #5 already edits that file — but it needs
deciding.
esbuildandworkerdhave to betruefor anything to run;canvasandsharplook like they should befalse, which matches PR Carousel PDF export, per-page PNGs, and a display-independent export size #5 'sstated reason for dropping node-canvas.
tscerrors remain untouched:canvas-area.tsx:179callsaddPage(page.id)against a zero-arg signature, andD1Databaseis unresolvedbecause
@cloudflare/workers-typesis not installed.browser. Bulk render is a caller loop over
fillplus the existing export.Pressure-test follow-up (added after review of my own claim)
save: truewrites an editable design, not a rendered image — there is noserver-side rasterizer. Measured from the local D1 after an editor round-trip, a
three-object quote card is 2,470 bytes of
canvas_json; a branded designwith an image is 10-40 KB.
GET /api/designs(src/server/index.ts:58) isunpaginated and returns every row's full
canvas_json, and the gallery rendersone card per design (
home.tsx:110).So a few hundred saved variants are a few hundred gallery entries and megabytes
of list payload — not a few hundred finished graphics. An earlier revision of
this PR's README said otherwise; that claim is now scoped down in README.md,
agent.md (where an agent reads it before looping) and clawnify.json.
The API itself is unchanged and I'd defend it: at N=1-10 — an agent fills a card,
opens it, exports — this is exactly the right primitive. Real bulk wants the
fillloop plus browser-side rasterizing, since the canvas is already there;that is the follow-up, not a server-side renderer.
Two other things checked while reviewing:
@clawnify/dbexposes no raw-SQL batch or transaction (initDB/query/get/run/ json/assertBindableonly, per itsdist/index.d.ts); batching exists solelyvia
getDB(...).batch(), D1-only, with that file's own docs noting the Facetpath runs statements individually. So the N+1 inserts in
save: trueare nota missed primitive — but a partial variant is possible on failure, which is a
known constraint of this path rather than something introduced here.
editable documents. That difference is the rasterizer, and it is why the
scoping above matters.