Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
23 KiB
23 KiB
SCRATCHPAD — Invoice Designer Unified Component AST
Context
Goal: collapse designer state into a unified generic node tree so adding new properties (for example borderRadius) is mostly schema work, not store plumbing.
This plan intentionally continues the simplification arc:
- Templates are data (
templateAstJSON) not code/WASM. - Layout math is delegated to the DOM/CSS engine (flex/grid + dnd-kit).
- Designer state becomes a single generic AST with schema-driven editing.
Current State (Starting Point)
- Designer store uses
DesignerNode[]with typed fields:position,size,parentId,childIds,allowedChildren,layout,style,metadata, etc.
- Property editing is largely hardcoded in
packages/billing/src/components/invoice-designer/DesignerShell.tsx. - Nesting rules are defined in
packages/billing/src/components/invoice-designer/state/hierarchy.ts. - Palette metadata defaults live in
packages/billing/src/components/invoice-designer/constants/componentCatalog.ts.
Decisions (Implementation Choices That Minimize Trouble)
- Source of truth for hierarchy is
childrenarrays only.- Do not persist
parentId(redundant and easy to desync). - Parent lookup is derived when needed (tree sizes are small enough for O(n) searches).
- Do not persist
- Patch API uses dot-notation paths (for example
style.width,metadata.bindingKey) rather than JSON Pointer to keep call sites readable.- Internally, patch operations should be applied immutably (structural sharing).
- This patch API is a hard requirement for the strategy: without it, the refactor cost is paid without getting the “new prop = schema change” velocity gain.
- Component schema becomes the single source of truth for:
- palette labels/descriptions/categories
- defaults (initial props)
- editable props schema (inspector UI)
- allowed parents/children (nesting rules)
- Inspector will support a small set of core field widgets plus opt-in custom widgets for complex objects/arrays (tables).
Notes / Gotchas
- Ensure history/undo remains stable:
- Either snapshot
nodesById + rootIdper commit, or store patches and replay. - Snapshotting is simpler and aligns with current approach; patch replay is smaller but more error-prone.
- Either snapshot
- Some existing code expects
allowedChildrento be present on nodes; that must become a derived selector from schema. - Export/import to invoice-template AST needs a single authoritative mapping; avoid letting schema defaults leak into exported templates unexpectedly.
File Targets (Likely Touchpoints)
- Store:
packages/billing/src/components/invoice-designer/state/designerStore.ts
- Hierarchy rules removal:
packages/billing/src/components/invoice-designer/state/hierarchy.ts(delete)
- Component schema/catalog:
packages/billing/src/components/invoice-designer/constants/componentCatalog.ts(likely merged into or replaced by schema module)
- Inspector refactor:
packages/billing/src/components/invoice-designer/DesignerShell.tsx
- Canvas rendering:
packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx
- Workspace AST mapping:
packages/billing/src/components/invoice-designer/ast/workspaceAst.ts
Validation Runbook (When Implementing)
- Tree invariants:
- every child id exists in
nodesById - no cycles (walk from root and track visited)
- all nodes are reachable from root (or explicitly allow unattached nodes, but prefer not to)
- every child id exists in
- Quick grep for legacy references:
rg -n "parentId|allowedChildren|updateNodeStyle\\(|updateNodeLayout\\(|updateNodeMetadata\\(" packages/billing/src/components/invoice-designer -S
Developer Guide (Adding Props / Components)
Component Schema Source Of Truth
- File:
packages/billing/src/components/invoice-designer/schema/componentSchema.ts - Each component defines:
label/description/category(palette + inspector display metadata)defaults: initialname,layout,style,metadata,sizehierarchy:allowedParents+allowedChildreninspector: panels/fields (schema-driven Inspector)
- Helpers now live on the schema module:
getAllowedChildrenForType(type)getAllowedParentsForType(type)canNestWithinParent(childType, parentType)
Inspector Schema Format
- Files:
packages/billing/src/components/invoice-designer/schema/inspectorSchema.ts(field/panel types)packages/billing/src/components/invoice-designer/inspector/DesignerSchemaInspector.tsx(renderer)
- Field kinds supported:
- primitives:
string,number,boolean,enum - CSS-ish:
css-length,css-color - complex:
widget(custom React editor for component-specific metadata)
- primitives:
- Visibility rules:
visibleWhensupportsnodeIsContainer,pathEquals,parentPathEquals
- Widgets:
- Implement widgets in
packages/billing/src/components/invoice-designer/inspector/widgets/ - Register by referencing the widget
idin schema (example:table-editorformetadata.columns)
- Implement widgets in
Patch API (How To Mutate State)
- File:
packages/billing/src/components/invoice-designer/state/designerStore.ts - Only supported mutation surface (for anything persistent/undoable):
setNodeProp(nodeId, path, value, commit?)unsetNodeProp(nodeId, path, commit?)insertChild(parentId, childId, index)removeChild(parentId, childId)moveNode(nodeId, nextParentId, nextIndex)deleteNode(nodeId)
- Dot-path conventions (recommended):
namestyle.*(sizing/typography/media CSS-like props)layout.*(container layout props)metadata.*(component-specific config)
- History/undo semantics:
commitdefaults totrue- For multi-step edits that should be a single undo step: use
commit=falsefor intermediate writes andcommit=truefor the final write.
Persistence Rules (What Gets Serialized)
- Persisted workspace snapshot is
{ rootId, nodesById, canvas settings }. exportWorkspace()sanitizes nodepropsto drop runtime/editor-only keys:position,size,baseSize,layoutPresetId
- If a prop should survive reload and affect output, it should live under:
props.style,props.layout, orprops.metadata(and be surfaced via schema).
Conventions For Adding A New Property
- Add/extend the schema field in
componentSchema.ts(usually viaCOMMON_INSPECTORor component-specificinspector). - Provide a default in
defaults.style/defaults.layout/defaults.metadataonly if it must exist on insertion. - Avoid adding store actions/reducers: use
setNodeProp/unsetNodePropfrom inspector and other UI.
Conventions For Adding A New Component
- Add a
DesignerComponentSchemaentry inDESIGNER_COMPONENT_SCHEMAS. - Define
hierarchy.allowedParentsandhierarchy.allowedChildren(the only authority for nesting rules). - Define
defaultsandinspectorpanels/fields. - Canvas/palette/outline should not need bespoke wiring if schema is complete.
Progress Log
- 2026-02-13: Implemented unified AST type definitions in
packages/billing/src/components/invoice-designer/state/designerAst.ts:DesignerAstNodewith{ id, type, props, children }DesignerAstWorkspacewith{ rootId, nodesById }- Stable document root id constant:
DESIGNER_AST_DOCUMENT_ID = 'designer-document-root'
- 2026-02-13: Added canonical indexing to the designer store state in
packages/billing/src/components/invoice-designer/state/designerStore.ts:- Store state now includes
rootIdandnodesByIdkept in sync with the existingnodesarray via asetWithIndexwrapper. - This is an incremental cutover step so downstream UI/tests can migrate off
nodesprogressively.
- Store state now includes
- 2026-02-13: Implemented generic patch operations in
packages/billing/src/components/invoice-designer/state/patchOps.tsand exposed them on the store:setNodeProp/unsetNodePropfor dot-path updates (immutable deep updates with empty-object cleanup).insertChild/removeChild/moveNode/deleteNodefor hierarchy mutations (cycle prevention inmoveNode).- Renamed legacy coordinate nudge API to
moveNodeByDeltato freemoveNodefor tree moves.
- 2026-02-13: Refined undo/redo history behavior in
packages/billing/src/components/invoice-designer/state/designerStore.ts:- Store now initializes history with a baseline snapshot so the first committed mutation can be undone.
setNodeProp/unsetNodePropnow commit to history by default to match existing property-edit behavior.
- 2026-02-13: Introduced component schema definitions in
packages/billing/src/components/invoice-designer/schema/componentSchema.ts:- Defines per-component label/description/category, defaults (size/layout/metadata), and hierarchy allowlists.
packages/billing/src/components/invoice-designer/constants/componentCatalog.tsnow derives palette definitions from schema (schema is the new source of truth for palette metadata/defaults).
- 2026-02-13: Hierarchy allowlists are now resolved via schema:
packages/billing/src/components/invoice-designer/state/hierarchy.tsis now a thin wrapper overgetComponentSchema(type).hierarchy.
- 2026-02-13: Palette insertion now uses schema defaults and generic tree ops:
packages/billing/src/components/invoice-designer/state/designerStore.tsaddNodeFromPalettenow pulls size/layout/style/metadata defaults fromgetComponentSchema(type).defaultsand attaches nodes usinginsertChildsemantics (patchOps.insertChild).- Default metadata normalization for repeated insertions (table columns, attachment items) moved into the store (
normalizeDefaultMetadataForNewNode), removing hardcoded defaults fromDesignerShell.tsx.
- 2026-02-13: Outline + breadcrumbs now traverse the tree via children arrays:
packages/billing/src/components/invoice-designer/palette/OutlineView.tsxnow renders fromrootIdandnodesById, deriving parent lookup only for expand behavior.packages/billing/src/components/invoice-designer/DesignerShell.tsxbreadcrumbs now derive parent links fromchildIdsinstead of relying on persistedparentId.
- 2026-02-13: Selection/hover state now validates ids against
nodesById:packages/billing/src/components/invoice-designer/state/designerStore.tsselectNode/setHoverNodeclear invalid ids anddeleteNodeclears selection/hover if the referenced node is removed as part of a subtree delete.
- 2026-02-13: Canvas rendering now resolves from unified node props:
- Added
packages/billing/src/components/invoice-designer/utils/nodeProps.tsas the canonical accessor forprops.name,props.layout,props.style,props.metadata(with temporary legacy fallbacks during cutover). packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsxandpackages/billing/src/components/invoice-designer/canvas/previewScaffolds.tsnow render usingprops.*accessors.- Store mutations keep
propsin sync with legacy fields for now (packages/billing/src/components/invoice-designer/state/designerStore.ts).
- Added
- 2026-02-13: Drag-drop reparent/reorder now uses generic tree ops:
packages/billing/src/components/invoice-designer/DesignerShell.tsxnow callsstore.moveNode(...)(tree move) instead ofmoveNodeToParentAtIndex.packages/billing/src/components/invoice-designer/state/designerStore.flowDnd.test.tsupdated to exercisemoveNode(...)directly.
- 2026-02-13: Resizing now writes through the generic patch API:
- Removed
updateNodeSizefrompackages/billing/src/components/invoice-designer/state/designerStore.ts. packages/billing/src/components/invoice-designer/DesignerShell.tsxnow implementsresizeNode(...)usingsetNodePropwrites (size.*,baseSize.*,style.*) with a single history commit on mouse-up.setNodeProp/unsetNodePropnow mirrorname/style.*/layout.*/metadata.*updates into both legacy fields andprops.*during cutover.
- Removed
- 2026-02-13: Started schema-driven Inspector cutover:
- Added a minimal inspector schema format in
packages/billing/src/components/invoice-designer/schema/inspectorSchema.tsand attached schemas to component definitions inpackages/billing/src/components/invoice-designer/schema/componentSchema.ts. - Implemented
packages/billing/src/components/invoice-designer/inspector/DesignerSchemaInspector.tsxwhich renders panels/fields from the selected node's component schema and writes edits viasetNodeProp/unsetNodeProp(commit-on-blur for text fields). - Updated
packages/billing/src/components/invoice-designer/DesignerShell.tsxto render the schema-driven inspector for supported metadata panels while leaving complex editors (tables/attachments/media) on the legacy path for now.
- Added a minimal inspector schema format in
- 2026-02-13: Expanded schema-driven Inspector field types and conditional panels:
packages/billing/src/components/invoice-designer/schema/inspectorSchema.tsnow supportsnumber,css-length,css-colorfield kinds plusvisibleWhenrules (nodeIsContainer,pathEquals,parentPathEquals).packages/billing/src/components/invoice-designer/schema/componentSchema.tsnow defines aCOMMON_INSPECTOR(Layout, Sizing, Flex Item) and merges it into all component schemas, so layout/style edits are schema-defined rather than hardcoded in the shell.packages/billing/src/components/invoice-designer/DesignerShell.tsxremoved the hardcoded Layout/Sizing/Flex Item inspector blocks and relies onDesignerSchemaInspectorfor those panels; legacy metadata editors remain only for complex types (table columns, attachment items, media).
- 2026-02-13: Added first complex schema widget for metadata:
- Implemented
packages/billing/src/components/invoice-designer/inspector/widgets/TableEditorWidget.tsxand wired it into the schema via awidgetinspector field (table-editor). packages/billing/src/components/invoice-designer/schema/componentSchema.tsnow attaches the table editor widget to bothtableanddynamic-tableschemas.- Removed the hardcoded table/dynamic-table inspector block from
packages/billing/src/components/invoice-designer/DesignerShell.tsx(tables now render their metadata editor via schema widget).
- Implemented
- 2026-02-13: Updated designer <-> invoice-template AST mapping to prefer unified props + children:
packages/billing/src/components/invoice-designer/ast/workspaceAst.tsnow readsname/layout/style/metadataviapackages/billing/src/components/invoice-designer/utils/nodeProps.tshelpers and traverseschildren(fallback tochildIdsfor legacy nodes).- Import now materializes
propsandchildrenon generated nodes and keeps them in sync with legacy fields during cutover. - Updated
packages/billing/src/components/invoice-designer/ast/workspaceAst.test.tsfixtures to setprops+childrenso export/import exercises the unified tree.
- 2026-02-13: Added undo/redo history regression test in
packages/billing/src/components/invoice-designer/state/designerStore.undoRedo.test.ts:- Verifies tree state returns exactly to prior snapshots after a
moveNodefollowed bydeleteNode, via sequentialundo()andredo().
- Verifies tree state returns exactly to prior snapshots after a
- 2026-02-13: Added schema invariants test in
packages/billing/src/components/invoice-designer/schema/componentSchema.test.ts:- Ensures every component type has defaults, an inspector schema, and reciprocal nesting allowlists (parent allowedChildren aligns with child allowedParents).
- 2026-02-13: Added repo/unit guard ensuring
packages/billing/src/components/invoice-designer/state/hierarchy.tsstays deleted and is not imported anywhere in the invoice designer code. - 2026-02-13: Added palette insertion integration test in
packages/billing/src/components/invoice-designer/state/designerStore.addNodeFromPalette.test.ts:- Verifies
addNodeFromPalettepullslayout/metadata/sizedefaults from the component schema and attaches the new node id into the parentchildrenarray.
- Verifies
- 2026-02-13: Added outline rendering integration test in
packages/billing/src/components/invoice-designer/palette/OutlineView.integration.test.tsx:- Verifies outline order matches
childIdsordering and selection state drives highlight styling.
- Verifies outline order matches
- 2026-02-13: Added breadcrumbs hierarchy integration coverage in
packages/billing/src/components/invoice-designer/DesignerShell.breadcrumbs.test.ts:- Validates breadcrumb path is derived from
childIds(children-only hierarchy) and does not depend on persistedparentId.
- Validates breadcrumb path is derived from
- 2026-02-13: Added unified props rendering integration test in
packages/billing/src/components/invoice-designer/canvas/DesignCanvas.unifiedProps.integration.test.tsx:- Ensures
node.props.layoutandnode.props.styleare sufficient to drive DOM styles (without relying onnode.layout/node.style).
- Ensures
- 2026-02-13: Added drag-drop reorder integration test in
packages/billing/src/components/invoice-designer/state/designerStore.dragDropReorder.integration.test.ts:- Uses
loadWorkspace({ rootId, nodesById })to validatemoveNode(...)reorderschildrenfor flow (flex) containers in the unified snapshot format.
- Uses
- 2026-02-13: Added drag-drop reparent integration test in
packages/billing/src/components/invoice-designer/state/designerStore.dragDropMoveAcross.integration.test.ts:- Validates moving across containers updates only the unified
childrenarrays in the exported workspace snapshot and rejects invalid parent targets via schema nesting rules.
- Validates moving across containers updates only the unified
- 2026-02-13: Added resize-to-DOM integration test in
packages/billing/src/components/invoice-designer/canvas/DesignCanvas.resizeProps.integration.test.tsx:- Simulates resize writes via
store.setNodeProp('style.width'/'style.height', ...)and verifies the rendered canvas node element updates its inline sizing.
- Simulates resize writes via
- 2026-02-13: Persisted workspace snapshots now omit runtime geometry/editor-only props:
packages/billing/src/components/invoice-designer/state/designerStore.tsexportWorkspace()sanitizesnode.propsto dropposition,size,baseSize, andlayoutPresetId(new saves are unified-only).- Updated
packages/billing/src/actions/invoicePreviewPdfParity.integration.test.tsworkspace fixture to use{ rootId, nodesById }(nonodes/constraints). - Updated
packages/billing/src/components/invoice-designer/state/designerStore.constraints.test.tsassertions to validate unified snapshots.
- 2026-02-13: Removed legacy hierarchy module:
- Deleted
packages/billing/src/components/invoice-designer/state/hierarchy.ts. - Moved
getAllowedChildrenForType/getAllowedParentsForType/canNestWithinParenthelpers intopackages/billing/src/components/invoice-designer/schema/componentSchema.tsand updated call sites to import from schema.
- Deleted
- 2026-02-13: Removed per-property store actions in favor of patch ops:
- Deleted legacy store APIs:
updateNodeName,updateNodeMetadata,updateNodeLayout,updateNodeStyle,setNodePosition,moveNodeByDelta,moveNodeToParentAtIndex. - Kept label text behavior by normalizing label
name<->metadata.textchanges insidesetNodeProp/unsetNodeProp(only when mutatingnameormetadata.*). - Fixed
rootIdindexing soexportWorkspace()snapshots can be round-tripped vialoadWorkspace()even when legacy fixtures use non-canonical document ids. - Updated affected tests and UI call sites to use
setNodeProp/unsetNodePropexclusively.
- Deleted legacy store APIs:
- 2026-02-13: Added deterministic unified-tree traversal helper and tests:
packages/billing/src/components/invoice-designer/state/designerAst.tsnow exportstraverseDesignerAstNodeIds.- Added
packages/billing/src/components/invoice-designer/state/designerAst.test.ts.
- 2026-02-13: Added patch ops unit coverage:
- Added
packages/billing/src/components/invoice-designer/state/patchOps.setNodeProp.test.tsto validate immutable deep dot-path updates. - Added
packages/billing/src/components/invoice-designer/state/patchOps.unsetNodeProp.test.tsto validate cleanup behavior for empty objects. - Added
packages/billing/src/components/invoice-designer/state/patchOps.insertChild.test.tsto validate deterministic child insertion ordering. - Added
packages/billing/src/components/invoice-designer/state/patchOps.moveNode.test.tsto validate reorder/reparent semantics + cycle prevention. - Added
packages/billing/src/components/invoice-designer/state/patchOps.deleteNode.test.tsto validate subtree deletion behavior.
- Added
- 2026-02-13: Added schema-driven inspector integration coverage:
- Added
packages/billing/src/components/invoice-designer/inspector/DesignerSchemaInspector.integration.test.tsxto assert the inspector renders fields from schema and writes unifiedprops.*via generic patch operations. - Testing gotcha: our shared
<Input>doesn’t reliably forwardidto the native<input>, so tests query controls by placeholder rather than label association.
- Added
- 2026-02-13: Extracted Inspector input normalizers and added unit coverage:
- Added
packages/billing/src/components/invoice-designer/inspector/normalizers.ts(+normalizeString,normalizeCssLength,normalizeCssColor,normalizeNumber) and wiredDesignerSchemaInspectorto use them. - Added
packages/billing/src/components/invoice-designer/inspector/normalizers.test.tsto validate canonicalization behavior (for example unitless CSS lengths becomepx, empty strings unset).
- Added
- 2026-02-13: Added table editor schema widget integration coverage:
- Added
packages/billing/src/components/invoice-designer/inspector/TableEditorWidget.integration.test.tsxto assert thetable-editorwidget updatesmetadata.columnsand the canvas preview header reacts to the updated columns list.
- Added
- 2026-02-13: Made workspace <-> template AST export/import deterministic:
- Updated
packages/billing/src/components/invoice-designer/ast/workspaceAst.tsimport to treat a single top-level ASTsectionwrapper as the designerpagenode (avoids nested page->section wrapping on roundtrip). - Added
export -> import -> exportdeterministic roundtrip assertion inpackages/billing/src/components/invoice-designer/ast/workspaceAst.test.ts.
- Updated
- 2026-02-13: Added persisted workspace snapshot format coverage:
- Added
packages/billing/src/components/invoice-designer/state/designerStore.exportWorkspace.test.tsto ensureexportWorkspace()returns only{ rootId, nodesById, snapToGrid, gridSize, showGuides, showRulers, canvasScale }and strips runtime/editor-onlypropskeys (position,size,baseSize,layoutPresetId).
- Added
- 2026-02-13: Added repo guards for refactor completeness:
- Added
packages/billing/src/components/invoice-designer/state/noLegacyApis.guard.test.tsto enforce removed per-property store action names stay gone, and to keep hierarchy traversal usingchildren(nochildIdsusage outsidestate/). - Updated
packages/billing/src/components/invoice-designer/palette/OutlineView.tsx,packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx, andpackages/billing/src/components/invoice-designer/DesignerShell.tsxto traversechildrenrather thanchildIds.
- Added