Open
Conversation
Implements a full-featured, reusable template builder component for designing
invoice templates, shipping label templates, report templates, receipt templates,
and other document types.
## Components
- `TemplateBuilder` — top-level orchestrator, composes all panels
- `TemplateBuilder::Canvas` — pixel-perfect free-canvas with interact.js drag/resize
- `TemplateBuilder::ElementRenderer` — renders all element types (text, image, table, line, shape, qr_code, barcode)
- `TemplateBuilder::Toolbar` — element type picker, zoom controls, undo/redo, save/preview
- `TemplateBuilder::LayersPanel` — element tree with visibility toggle, reorder, delete
- `TemplateBuilder::PropertiesPanel` — contextual properties editor with Position, Size, Typography, Appearance, Border, Effects sections
- `TemplateBuilder::PropertiesPanel::Section` — collapsible section wrapper
- `TemplateBuilder::PropertiesPanel::Field` — labelled field row
- `TemplateBuilder::VariablePicker` — modal for browsing context schema variables and composing formulas
## Key Features
- Free-canvas drag and resize via interact.js with grid snapping
- 50-step undo/redo history
- Variable syntax: `{invoice.total}`, `{company.name}`
- Formula syntax: `[{ {invoice.subtotal} * 1.1 }]`
- Iteration syntax: `{{#each orders}}...{{/each}}`
- Three-tier context: global/ambient → primary subject → query collections
- Variable picker with search, namespace grouping, type icons, and formula editor
- Full dark mode support via Tailwind + data-theme='dark'
- Paper size and orientation support (A4, Letter, Legal, custom)
- Element types: text, image, table, line, shape, qr_code, barcode
## Dependencies
- interactjs ^1.10.27 (drag/resize)
Refs: fleetbase/core-api#198
roncodes
pushed a commit
to fleetbase/fleetbase
that referenced
this pull request
Mar 3, 2026
Adds two Ember Data models to the console app to support the new template builder system introduced in fleetbase/core-api#198 and fleetbase/ember-ui#121. ## Models ### template - Full attribute set matching the backend Template model - Computed: isDraft, isPublished, contextTypeLabel, dimensionLabel - hasMany: template-query (inverse) - Supports: name, description, context_type, paper_size, orientation, width, height, unit, background_color, content (array), is_default, status ### template-query - Full attribute set matching the backend TemplateQuery model - Computed: variableName, variableToken, resourceTypeLabel - belongsTo: template (inverse) - Supports: name, label, description, resource_type, filters (object), sort_by, sort_direction, limit Both models follow the existing Fleetbase model conventions: - @ember-data/model with attr/belongsTo/hasMany decorators - Standard date computed properties (updatedAgo, updatedAt, createdAt, etc.) - date-fns for date formatting Refs: fleetbase/core-api#198, fleetbase/ember-ui#121
…cycle hook collision Glimmer components throw immediately in debug mode if a method named 'didInsertElement' or 'willDestroyElement' is defined on the class, even when decorated with @action and intended as a modifier callback. Renamed: didInsertElement → setupElement willDestroyElement → teardownElement Updated canvas.hbs to pass the renamed actions via fn helper. Fixes: Uncaught Error: You attempted to define the 'didInsertElement' method on a Glimmer Component, but that lifecycle hook does not exist...
JSON.stringify() cannot serialise Ember Data model instances because
their attributes are tracked prototype getters, not plain enumerable
own properties — resulting in an empty {} clone and a broken editor.
Fix: detect Ember Data models via eachAttribute() and extract each
attribute value explicitly before JSON round-tripping. Plain objects
continue to use the existing JSON.parse(JSON.stringify()) path.
Fixes: TemplateBuilder crashes when @template is an Ember Data model.
…contamination Two bugs fixed: 1. layers-panel.js — isSelected/isVisible/isRenaming/elementIcon/elementLabel called as Glimmer helpers via (this.method arg) in HBS. Plain methods are invoked with this=undefined in that context. Fix: decorate all five with @action so they are bound to the component instance. 2. template-builder.js — _updateTemplate used spread { ...this.template } which can carry Ember tracked tags into the new plain object. When Glimmer detects a tracked write during a render pass it throws a setter assertion. Fix: use Object.assign then JSON.parse(JSON.stringify()) to guarantee a clean POJO with no tracking baggage.
Two bugs fixed:
1. canvas.js — isSelected and elementStyle called as Glimmer helpers via
@isSelected={{this.isSelected element}} in HBS. Without @action the
methods are invoked with this=undefined. Fix: add @action decorator to
both isSelected and elementStyle.
2. template-builder.js — _updateTemplate writes to this._template (a
@Tracked property) synchronously inside actions that can fire while a
render pass is still in progress (e.g. addElement from toolbar click).
Glimmer throws a setter assertion when a tracked value is modified after
it was consumed during the current render transaction.
Fix: defer the this._template write using schedule('afterRender') so it
always happens after the current render pass completes cleanly.
…next() Full audit of all template-builder HBS files for (this.method arg) helper invocations — any plain method called this way is invoked with this=undefined in Glimmer. Fixed by adding @action decorator to bind them to the instance. Files fixed: - properties-panel.js: isSectionOpen (called as @isopen={{this.isSectionOpen ...}}) - variable-picker.js: isExpanded (called as {{if (this.isExpanded ...) ...}}) Also: switch _updateTemplate from schedule('afterRender') to next() from @ember/runloop. schedule('afterRender') still fires within the same runloop flush and can still trigger the setter-during-render assertion. next() defers to the next runloop tick entirely, which is the correct Ember idiom for deferring a tracked write past the current render transaction.
All .tb-input elements (text, number, select) now share an explicit height: 26px with padding: 0 6px and box-sizing: border-box. - appearance: none applied globally to strip browser-default sizing - select.tb-input restores appearance: auto to keep the native dropdown arrow - input[type=number] restores inner-spin-button with height: 24px so the spinners stay compact and don't overflow the 26px container - Fixes number inputs rendering taller than select inputs in the properties panel (visible in screenshot: X/Y/Width/Height/Size vs Font Family/Weight)
The canvas component was checking window.interact (UMD global) which is never set in an Ember build — causing drag/resize to silently no-op. Replace with a proper ES6 import: import interact from 'interactjs'; interactjs is already declared in package.json so no new dependency is needed. The graceful-degrade guard is also removed since the import will throw at build time if the package is missing, which is the correct failure mode for a hard dependency.
CSS fixes (template-builder.css): - Explicitly re-declare font-size (11px), color (#374151) and border-radius (4px) on the [type='number'] and select.tb-input overrides so browser UA stylesheets cannot override these values after -webkit-appearance resets them. - Add textarea.tb-input variant with auto height and correct padding. - Align border colour to #d1d5db (gray-300) across all input types for visual consistency with the existing select inputs. - Dark-mode colour updated to #d1d5db for all input variants. Canvas drag/resize fixes (canvas.js + element-renderer.js): - Switch element positioning from left/top to transform: translate(x, y) only. Having both left/top and a transform offset caused elements to jump on the first drag and made interact.js misidentify drag gestures as resize gestures. - element-renderer.js: wrapperStyle now emits left:0; top:0. handleInsert seeds el.dataset.x/y from stored values and sets the initial transform so the element renders at the correct position without waiting for an interact.js event. - canvas.js: resize edges now use CSS selector strings (.tb-handle-nw etc.) so resize gestures are restricted to the four corner handle elements. Any drag on the element body is therefore unambiguously a move, not a resize. - Add moveElement action (template-builder.js) that mutates element position in-place without replacing the content array, preventing Glimmer from scheduling a re-render after every drag/resize end event. This keeps interact.js instances alive across gestures. - Wire @onMoveElement to the canvas in template-builder.hbs. - Replace Object-based _interactables map with a proper Map for cleaner lifecycle management. - Add stale-interactable cleanup in setupElement for safety on re-renders.
…anvas.js
Babel's class-fields transform rejects .bind(this) chained onto a method
shorthand (e.g. end(event) { ... }.bind(this)) inside an object literal that
lives inside a class method body. The fix is to use arrow functions for all
interact.js listener callbacks so that `this` is captured lexically without
needing .bind().
…o template builder
Paper size & orientation (template-builder.js):
- _updateTemplate now calls _dimensionsForPaperSize() whenever paper_size or
orientation changes, writing the resolved width/height/unit back onto the
template object so the canvas reacts immediately.
- _dimensionsForPaperSize() contains the canonical mm dimensions for A4, A3,
A5, Letter and Legal; landscape simply swaps width and height.
- Custom paper size leaves width/height unchanged (user sets them manually).
Image element (properties-panel.hbs + properties-panel.js):
- Replaced the raw Source URL text input with a three-state UI:
empty → dashed upload dropzone + 'or use variable' button
variable → blue badge showing the token with edit/clear buttons
uploaded → filename badge with replace-upload and clear buttons
- Upload is handled via fetch.uploadFile.perform() (same pattern as
driver/form.js and custom-field/input.js). The uploaded URL is stored on
the element's src property; only the filename is shown in the UI.
- Variable tokens (containing '{') continue to work via the variable picker.
- isUploadingImage tracked state drives the 'Uploading...' placeholder.
Table element (properties-panel.hbs + properties-panel.js):
- Added three new collapsible sections for table elements:
Columns – add/remove columns; each column has a Label and a data Key.
Renaming a key migrates existing row data to the new key.
Removing a column deletes its key from all rows.
Data Source – toggle between Variable and Manual modes.
Variable: a text field (+ variable picker) for a data_source
token that resolves to an array of objects at render time.
Manual: an inline row editor where each row shows one input
per column; rows can be added and removed.
Table Style – header background/text colour, border colour, cell padding,
and alternating row colour.
- tableColumns / tableRows getters read directly from the element object.
- tableDataMode getter derives the current mode from whether data_source is set.
… bugs Root cause analysis ------------------- 1. Elements become non-interactive after first drag/resize moveElement() previously wrote to keep the properties panel in sync. Writing to any @Tracked property triggers a Glimmer re-render. Re-rendering causes the {{#if @isSelected}} block in element-renderer.hbs to insert/remove the .tb-handle-* divs, and in some cases destroys and recreates the element wrapper DOM node entirely. When the DOM node is recreated, interact.js loses its internal pointer state and the element becomes permanently unresponsive until the page reloads. Fix: remove the selectedElement write from moveElement(). The properties panel will reflect updated values the next time the user clicks the element (selectElement is called, which sets selectedElement to the same mutated object). No re-render occurs during drag/resize. 2. Mouse snaps to element center during drag interact.modifiers.snap() was configured with: relativePoints: [{ x: 0, y: 0 }] This tells interact.js to snap the element's top-left corner to the grid, not the pointer. Because the pointer is typically somewhere in the middle of the element, interact.js compensates by jumping the element so its top-left lands on the nearest grid point — which feels like the pointer is being pulled to the element's origin (center-snap). Fix: remove the snap modifier entirely. Free movement is smooth and accurate. Grid snapping can be re-added later using offset:'startCoords' which snaps relative to where the drag started, not the element origin. 3. Choppy movement The snap modifier (relativePoints) combined with restrict (parent bounding box) ran two constraint passes per pointer event, causing visible jank. Fix: both modifiers removed from draggable. Only restrictSize (minimum element dimensions) is kept on resizable, as it has no pointer-position side effects. 4. Zoom read at event time The previous implementation captured zoom at interactable-creation time via a closure. If the user changed zoom after an element was placed, the drag deltas would be scaled by the wrong factor. Fix: zoom is now read from this.args.zoom inside each event handler via a getZoom() closure, so it always reflects the current zoom level.
…on addElement; add canvas boundary clamping Root cause of persistent drag/resize breakage ---------------------------------------------- The previous implementation stored the entire template (including content array) in a single @Tracked _template object. Every mutation went through _updateTemplate(), which did: JSON.parse(JSON.stringify(merged)) This deep-clone creates brand-new JS object references for EVERY element in the content array, even elements that were not changed. Glimmer's {{#each}} loop compares item references to decide whether to reuse or recreate a component. Because every object was a new reference after every operation (addElement, updateElement, deleteElement, reorderElement), Glimmer destroyed and recreated EVERY ElementRenderer component on every single mutation. This fired will-destroy (teardownElement -> interactable.unset()) followed by did-insert (setupElement -> new interactable) for all elements — not just the one that changed. Fix: split state into @Tracked _meta (non-content template fields) and @Tracked _content (element array). The invariant is that existing element objects keep their JS identity across renders: addElement -> push new object onto _content; existing objects unchanged; Glimmer creates exactly one new ElementRenderer updateElement -> Object.assign onto the existing object (preserves identity); replace _content with [..._content] (same refs) to notify Glimmer; {{#each}} reuses all existing components moveElement -> Object.assign only; no _content replacement; zero re-renders deleteElement -> filter to new array without target; only that component is destroyed reorderElement-> mutate z_index in-place; replace array ref for reactivity undo/redo -> restore from deep-cloned snapshot; full re-render acceptable since it is an explicit user action Canvas boundary clamping ------------------------ Added position and size clamping in both the drag.move and resize.move handlers in canvas.js. Elements are now constrained to [0, canvasWidth - elW] on the x axis and [0, canvasHeight - elH] on the y axis. Canvas dimensions are read at event time (getCanvasDims()) so paper-size changes are reflected without recreating interactables.
Toggle stuck on manual — root cause
-------------------------------------
The previous tableDataMode getter used:
return this.element?.data_source ? 'variable' : 'manual';
When setTableDataMode('variable') was called it wrote { data_source: '' }.
An empty string is falsy in JS, so tableDataMode immediately evaluated back
to 'manual' — making the toggle appear stuck.
Fix: store the mode explicitly as data_source_mode on the element object.
tableDataMode now reads element.data_source_mode ?? 'manual'. The mode is
independent of whether the variable/query fields have been filled in yet.
Query data source mode
-----------------------
Added a third 'Query' option to the toggle. When selected, the UI shows:
Endpoint — the API path to call at render time (e.g. /api/v1/orders)
Response Path — dot-path to unwrap the array from the JSON envelope
(e.g. 'data' for { "data": [...] })
Query Parameters — key/value pairs appended as query string; values support
{variable} syntax for dynamic filters
setTableDataMode clears the fields for the previous mode when switching:
-> manual clears data_source, query_endpoint, query_params, query_response_path
-> variable clears query_endpoint, query_params, query_response_path
-> query clears data_source; seeds query_params: [] if not present
…dialog, and variable picker integration - Remove incorrect 'Query' toggle from table data source (was raw endpoint input) - Add TemplateBuilder::QueriesPanel component: lists queries per template, add/edit/delete via fetch service - Add TemplateBuilder::QueryForm dialog: resource type picker, condition builder (field/operator/value), sort directives, limit, eager-load (with) - Add Layers/Queries tab switcher to left panel - Inject saved query variable tokens as a 'Queries' section in the variable picker - Table data source now uses Variable | Manual toggle; variable tokens from queries are inserted via the variable picker
- Seed _queries from args.template.queries in constructor - Include queries array in template getter (save payload) - QueryForm: remove all API calls — validate and return data to parent only - QueriesPanel: receive @queries from parent, no API fetch, all CRUD via @onQueriesChange - template-builder.hbs: pass @queries to QueriesPanel instead of @templateUuid - Queries created before template is saved get a temp _new_ UUID; backend strips these on upsert - On save, backend upserts all queries in one request alongside the template record
…ponents Ember addons require a re-export shim in app/components/ for each component defined in addon/components/. Without these files Glimmer cannot resolve TemplateBuilder::QueriesPanel or TemplateBuilder::QueryForm at runtime.
…-derive, template-builder service - canvas.js: use @Tracked selectedUuid for proper Glimmer reactivity on selection; add interact.js tap listener to ensure selectElement fires even when interact intercepts pointer events; pass @rotation and @isSelected as explicit primitive args to ElementRenderer so Glimmer tracks changes correctly - canvas.hbs: pass @isSelected={{eq element.uuid this.selectedUuid}} and @rotation={{element.rotation}} as explicit primitive arguments so did-update fires correctly on rotation change - element-renderer.hbs: use @rotation in did-update instead of @element.rotation - element-renderer.js: _applyTransform reads @rotation arg directly - query-form.js: inject service:template-builder for resource types; fix variable_name auto-derive; add @resourceTypes arg override support - query-form.hbs: section spacing, filter condition row layout fix, sort row layout fix - services/template-builder.js: new service for extension resource type registration - app/services/template-builder.js: re-export shim
…ression - canvas.js: remove side-effect from selectedUuid getter — writing @Tracked inside a getter causes Glimmer's infinite render loop assertion, which prevented the canvas from rendering any elements at all - toolbar.hbs: remove rotate buttons from center section (duplicate); move them to the right section before undo/redo, which is where the user expects them (left side of Preview/Save action buttons)
… and resource type namespaces - Fix undo/redo buttons permanently disabled: _undoStack and _redoStack were plain class fields (not @Tracked), so canUndo/canRedo getters never re-evaluated when items were pushed/popped. Changed to @Tracked and replaced all array mutations (.push/.pop/.shift) with immutable array operations (spread + reassignment) so Glimmer detects the changes. - Fix rotation not reflecting until element is dragged: added imperative DOM update in updateElement() when changes.rotation is defined. Reads the current x/y from data-element-uuid dataset attributes (maintained by interact.js) and applies the full translate+rotate transform immediately, matching the pattern used by interact.js for drag/resize. - Fix query form resource types: updated default resource types from incorrect Fleetbase\Models\* namespace to correct Fleetbase\FleetOps\Models\* namespace. Removed Payload, Entity, TrackingStatus, Zone, ServiceArea, and Route (not valid query targets). Added Issue, FuelReport, and PurchaseRate.
… canvas Root cause: updateElement and reorderElement were mutating element objects in-place (Object.assign / direct property assignment). Glimmer's reactivity system tracks argument changes by reference — when @element is the same object reference, Glimmer considers it unchanged and does NOT re-render the ElementRenderer component. As a result, getters like textContent, textStyle, wrapperStyle, and wrapperStyle never re-evaluated after a property update, making all properties panel changes invisible on the canvas. Fix: replace the element object with a new spread copy in both updateElement and reorderElement. This gives @element a new reference, which Glimmer detects and uses to re-render the affected ElementRenderer. The interact.js instance is torn down (will-destroy) and immediately re-created (did-insert) with the new object — setupElement handles stale interactables by uuid, so this is seamless. The element's current position (x/y from moveElement's in-place mutations) is preserved in the spread copy, so handleInsert correctly seeds the data attributes for interact.js.
The previous fix (replacing element objects in updateElement) caused
interact.js to be destroyed and recreated on every property change because
Glimmer was tracking {{#each}} by array index — a new object reference at
the same index meant destroy-old + create-new for the ElementRenderer.
Root cause of drag/resize breaking:
- Without a key, Glimmer destroys/recreates ElementRenderer on every
updateElement call, tearing down the interact.js instance each time.
- The new instance was set up correctly, but the stale closure in
_setupInteract still read element.rotation from the old captured object.
Fix (three-part):
1. canvas.hbs: Add key="uuid" to the {{#each}} loop.
Glimmer now tracks each ElementRenderer by uuid. When updateElement
replaces the element object (same uuid, new reference), Glimmer REUSES
the existing component instance and DOM node — interact.js stays alive —
but passes the new @element, causing the template to re-render and all
getters (textContent, textStyle, wrapperStyle, etc.) to re-evaluate.
2. canvas.js: Read rotation from el.dataset.rotation in applyTransform.
The _setupInteract closure captured element by reference at setup time.
With key="uuid", setupElement is not called again on property updates,
so the closure would read stale rotation from the old object. Reading
from el.dataset.rotation (a live DOM attribute) avoids this.
3. element-renderer.js: Write el.dataset.rotation in _applyTransform.
Keeps the DOM attribute in sync whenever rotation changes (via
handleInsert on first render and handleUpdate on subsequent changes),
so the canvas closure always reads the current value.
…derer Each component now owns exactly its own responsibilities: ElementRenderer - Imports interact.js and sets up its own interactable in did-insert - Tears it down in will-destroy (no parent involvement) - Emits @onmove(uuid, {x,y}) when a drag ends - Emits @onresize(uuid, {x,y,width,height}) when a resize ends - Emits @onselect(element) when tapped - Reads @canvasWidth/@canvasHeight for boundary clamping - Reads @zoom at event time (no closure staleness) - Reads rotation from el.dataset.rotation (written by _applyTransform) so the applyTransform closure never holds a stale element reference Canvas - Removed: interact.js import, _interactables map, setupElement, teardownElement, _setupInteract, didInsertCanvas, willDestroyCanvas - Owns only: canvas dimensions/style, selectedUuid tracking, handleSelectElement, handleDeselectAll - Passes @canvasWidth/@canvasHeight/@zoom to each ElementRenderer - Forwards @onMoveElement → @onmove, @onResizeElement → @onresize TemplateBuilder - Added resizeElement action (separate from moveElement) - moveElement: drag-end only, syncs {x,y} - resizeElement: resize-end only, syncs {x,y,width,height} - Both are silent in-place mutations (no re-render, no undo entry) - Removed @onUpdateElement from canvas wiring (canvas never needed it) This eliminates the entire class of bugs caused by the parent managing child DOM lifecycle: no more interact.js destruction on property updates, no stale closure references, no need for data-attribute workarounds.
…dex change
Bug 1 — Selection not working
Root cause: canvas.js was tracking _selectedUuid locally. When the layers
panel selected an element, the parent's selectedElement updated but the
canvas's _selectedUuid never changed, so @isSelected was always false.
Additionally, the canvas's {{on "click" handleDeselectAll}} was firing even
when a child element was tapped (interact.js tap does not stop the native
click from bubbling), immediately clearing the selection.
Fix:
- canvas.js: removed local _selectedUuid state entirely. selectedUuid is
now a getter derived from @selectedElement (the parent's source of truth).
This means any selection source (canvas tap, layers panel, keyboard) is
automatically reflected in the canvas highlight.
- canvas.js: handleDeselectAll now guards with
event.target !== event.currentTarget so it only fires when the canvas
background itself is clicked, not when a child element is tapped.
Bug 2 — z-index change repositions all elements to 0,0
Root cause: reorderElement replaces element objects with new spread copies.
Glimmer re-renders the ElementRenderer's style attribute (wrapperStyle).
wrapperStyle intentionally omits the CSS transform (which is managed
imperatively by interact.js). When Glimmer writes the new style attribute,
it clears the transform, snapping every element to 0,0.
Fix:
- element-renderer.hbs: added {{did-update this.handleUpdate @element}}.
- element-renderer.js: handleUpdate re-applies _applyTransform(el) after
Glimmer updates the style attribute, restoring the correct position.
Bonus fix — reorderElement now syncs selectedElement to the new object so
the properties panel reflects the updated z_index immediately.
…ove namespace display - toolbar: change undo icon from arrow-rotate-left to step-backward, redo icon from arrow-rotate-right to step-forward - query-form: remove the PHP namespace confirmation block shown below the resource type grid after a type is selected — unnecessary detail that adds visual noise for users - CSS: add .tb-query-form scoped overrides so input[type=text] and select inside the query dialog share the same height (30px), border-radius (6px), font-size (12px), and padding as each other, eliminating the mismatch between text inputs and select inputs
…onsole errors - Split query-form state into individual @Tracked fields (label, variableName, description, modelType, limit, withRelations) plus separate @Tracked conditions and sort arrays. Previously, every keystroke replaced the entire form object, causing Glimmer to destroy and recreate all input DOM elements in the {{#each}} loops — stealing focus from the active input. - conditions and sort now use in-place Object.assign mutation + array reference bump (this.conditions = [...this.conditions]). Glimmer re-evaluates the list but reuses existing DOM nodes (same array indices), so the focused input keeps focus across keystrokes. - Updated query-form.hbs to bind to individual tracked properties and dedicated action handlers (updateLabel, updateVariableName, updateDescription, updateLimit) instead of the generic updateField dispatcher. - Added addon/helpers/string-starts-with.js + app/helpers/string-starts-with.js to fix the 'Attempted to resolve string-starts-with' crash that occurred when queries-panel.hbs rendered after a successful Create Query. The helper was referenced but never registered in this addon.
- toolbar.hbs: render a close/back button on the left side of the toolbar when @onclose is provided; separated from element-type buttons by a vertical divider. Defaults to a chevron-left icon; @closeIcon and @closeLabel args allow customisation. - toolbar.js: add close() @action that calls @onclose if present; update JSDoc to document @onclose, @closeIcon, @closeLabel args. - template-builder.hbs: forward @onclose, @closeIcon, @closeLabel from the root component down to <TemplateBuilder::Toolbar>. - template-builder.js: document the three new optional args in the class JSDoc block.
The contextSchemas getter now returns [] if the passed value is not an array, preventing 'filteredSchemas.some is not a function' crashes when the consumer passes an API response object instead of a normalised array. hasResults also guards each schema's variables with ?? [] for safety.
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.
Template Builder Component System
Implements a full-featured, reusable template builder component for designing invoice templates, shipping label templates, report templates, receipt templates, and other document types.
Companion PR: fleetbase/core-api#198
Components
TemplateBuilderTemplateBuilder::CanvasTemplateBuilder::ElementRenderertext,image,table,line,shape,qr_code,barcodeTemplateBuilder::ToolbarTemplateBuilder::LayersPanelTemplateBuilder::PropertiesPanelTemplateBuilder::PropertiesPanel::SectionTemplateBuilder::PropertiesPanel::FieldTemplateBuilder::VariablePickerKey Features
interact.jswith 5px grid snapping and rotation support{invoice.total},{company.name},{order.tracking_number}[{ {invoice.subtotal} * 1.1 }]{{#each orders}}...{{/each}}with{this.property},{loop.index}data-theme=darktext,image,table,line,shape,qr_code,barcodeUsage
Dependencies Added
interactjs ^1.10.27— drag, resize, and snap behaviour for canvas elements