toga-ai 1.0.655 → 1.0.656

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,6 +65,13 @@ writable). Without it the field is rejected on write **and** is unreadable:
65
65
  VISIBILITY too.** Concrete symptom (TRUE-79191): `Measures.measureType` was registered but ungranted,
66
66
  so it came back absent and the frontend unit dropdowns were empty. When a field renders blank with no
67
67
  error, check for a missing `AclFieldPermissions` grant before suspecting the selector or the data.
68
+ - **⚠ Sibling columns of the SAME table routinely carry DIFFERENT role sets, so verify per field
69
+ before you add one to a `fields=` list — and verify as the LEAST-privileged role.** Concrete case
70
+ (`Client_Quad.Currencies`, 2026-08-26): `code` (1424) and `symbol` (1425) are granted to roles
71
+ 1/7/8/9, but `position` (1997) only to 7/8/9. Adding `currency.position` to an existing read would
72
+ have 403'd the **whole** request for every user holding only role 1 — while working perfectly for a
73
+ developer whose own account also carries role 7. **A dev/admin account hides this class of bug
74
+ completely**; a "just one more field" addition is a permission change, not a display change.
68
75
  - **`AclFieldPermissions` is `UNIQUE (recordFieldId, roleId)`** (Client blank DDL) — guard inserts
69
76
  with `NOT EXISTS`. For a **read-only, server-written** field (one populated by a `postPost`
70
77
  interceptor rather than the API caller), grant `isWritable = 0`.
@@ -403,6 +410,12 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
403
410
  and every repo is on the **same branch** so the generated model matches the DB.
404
411
 
405
412
  ## Change history
413
+ - 2026-08-26 - Added the **verify-per-sibling-field, verify-as-the-least-privileged-role** rule:
414
+ columns of the same table often carry different role sets, so adding one more field to a `fields=`
415
+ list is a permission change. `Client_Quad.Currencies` grants `code` (1424) and `symbol` (1425) to
416
+ roles 1/7/8/9 but `position` (1997) only to 7/8/9, so requesting `currency.position` would have
417
+ 403'd the whole read for role-1-only users while looking fine to a developer who also holds role 7 -
418
+ a dev/admin account hides this class of bug entirely. (bala)
406
419
  - 2026-08-26 - Added the **action-flag gate** (`Core.AclActions` + `Client_*.AclActionPermissions`),
407
420
  a third gate alongside record CRUD and record scripts: `Page.php` (~L1285-1302) defaults every
408
421
  action flag to `false` and only a matching `AclActionPermissions` row flips it true, so a missing
@@ -7,7 +7,7 @@
7
7
  | [AdvancedSelect (virtualized single/multi select)](features/advanced-select.md) | `AdvancedSelect<T>` is a **fully controlled, virtualized** single/multi select built from scratch (not react-select) on **`@tanstack/react-virtual`**, for large | toga-blox/src/components/AdvancedSelect/AdvancedSelect.tsx, toga-blox/src/components/AdvancedSelect/AdvancedSelect.types.ts, toga-blox/src/components/AdvancedSelect/AdvancedSelect.module.css |
8
8
  | [API client (axios wrapper, auth, table-data fetchers)](features/api-client.md) | `src/api/` is a thin **axios** wrapper that standardizes the **2.0 API envelope**, manages auth (Bearer + refresh), serializes complex query options, and provid | toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/api/index.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/api/apiFunctions.ts, toga-blox/src/api/auth.ts, toga-blox/src/api/genericApi.ts, toga-blox/src/api/types.ts, toga-blox/src/api/tableData |
9
9
  | [BaseInput (react-hook-form field factory)](features/base-input.md) | `BaseInput` is a **form-field factory** driven by react-hook-form. | toga-blox/src/components/BaseInput/BaseInput.tsx, toga-blox/src/components/BaseInput/BaseInput.types.ts, toga-blox/src/components/BaseInput/BaseInput.module.css, toga-blox/src/components/BaseInput/components |
10
- | [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
10
+ | [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/components/Table/themeConfig/toga.module.css, toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
11
11
  | [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail — typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
12
12
  | [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
13
13
  | [Talos AI-Assistant Component (launcher + slide-out chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a slide-out chat panel (`TalosP | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/index.ts |
@@ -6,9 +6,10 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-17
9
+ updated: 2026-08-26
10
10
  owners: [apeterson]
11
11
  files:
12
+ - toga-blox/src/components/Table/themeConfig/toga.module.css
12
13
  - toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx
13
14
  - toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx
14
15
  - toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx
@@ -82,8 +83,29 @@ spacer rows computed from the virtualizer item `.start`/`.end` minus `scrollMarg
82
83
  `supply-modal-table` skin **disables virtualization** (renders all rows, min-height 300px).
83
84
  Sticky columns use a `--sticky-offset` CSS variable summed from neighboring column widths.
84
85
 
86
+ ## The "Show more" hover affordance is CSS-driven, not JS-driven
87
+
88
+ A truncated body cell reveals its "Show more" toggle **on hover via CSS only**:
89
+ `.bodyCell:hover .showMoreWrapper:not(.expandedWrapper) button` in
90
+ `themeConfig/toga.module.css` (plus a `:focus-visible` rule so keyboard users can reach it).
91
+ `Cellstyles.module.css` already shipped `.toggle { opacity: 0 }` + a hover rule — the CSS
92
+ hover-reveal was the original design. `PrimaryTableBodyCell` must **not** inline-set the collapsed
93
+ `opacity`; inline styles beat the cascade and hand visibility back to React state. Layout props
94
+ (flex/sizing) stay inline; visibility does not. The React `isTruncated` state is now for
95
+ **capability detection only** (does this cell need a toggle at all), and `clearTruncationHint`
96
+ runs unconditionally — it no longer no-ops while `isExpanded`.
97
+
85
98
  ## Gotchas
86
99
 
100
+ - **Never gate a hover affordance on a JS `mouseleave` inside a virtualized table.** `mouseleave`
101
+ is not guaranteed to fire: a virtualized row can translate out from under a stationary cursor on
102
+ wheel-scroll, and any early-return in the clear handler strands the "hovered" flag set. The toggle
103
+ then stays visible on cells the mouse is nowhere near. Drive visibility from CSS `:hover`, which
104
+ **cannot** get stuck, and keep JS state for capability detection.
105
+ - **Residual (accepted):** a stale `isTruncated` still keeps the flex wrapper mounted, so that one
106
+ cell's text ellipsizes marginally earlier until the next `mouseleave`. Cosmetically invisible;
107
+ clearing it properly would need a per-cell scroll listener.
108
+
87
109
  - Frozen widths measure only **mounted** rows; virtualized tables size from the first paint set.
88
110
  - **Header labels must never abbreviate or overlap.** The full header width is a hard floor
89
111
  (above `maxColumnWidth`, `minColumnWidth`, and pinned `columnWidths`). Measure header width
@@ -98,6 +120,15 @@ Sticky columns use a `--sticky-offset` CSS variable summed from neighboring colu
98
120
  not the column set.
99
121
 
100
122
  ## Change history
123
+ - 2026-08-26 — Fixed: the "Show more" toggle got stuck visible on cells the mouse was not hovering.
124
+ `.toggle` already had `opacity: 0` + a CSS hover rule, but `PrimaryTableBodyCell` inline-set
125
+ `opacity: 1` unconditionally (inline beats the cascade), so visibility depended entirely on the
126
+ `isTruncated` state set on `mouseenter`/cleared on `mouseleave` — and `mouseleave` is not
127
+ guaranteed to fire (virtualized row translating out from under a stationary cursor on wheel-scroll;
128
+ `clearTruncationHint` no-opping while `isExpanded`). Visibility is now CSS `:hover`-driven
129
+ (`.bodyCell:hover .showMoreWrapper:not(.expandedWrapper) button` + `:focus-visible`), the collapsed
130
+ inline `opacity` was removed, and the `isExpanded` guard dropped from `clearTruncationHint`.
131
+ Shipped in `1.0.334-sandbox-client.136`. (apeterson)
101
132
  - 2026-08-17 — Fixed clipped/abbreviated headers: full header width is now a hard floor for every
102
133
  column (not capped by max-width, not overridden by `minColumnWidth`, applied even to pinned
103
134
  `columnWidths` — the old `return def.size` bypass left pinned columns clipped). Added a
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-25
9
+ updated: 2026-08-26
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts
@@ -60,6 +60,15 @@ alignment (CVA `cellWrapperVariants`), expandable, and copyable behavior. Empty/
60
60
  render `EmptyCell`. If `imageUrlField` is set, the URL is resolved separately and passed to
61
61
  `ImageCell`.
62
62
 
63
+ **The expandable wrap is destructive, and is now denied for status types.** When the table-meta
64
+ flag `isExpandableCell` is set on a field, the expandable branch does **not** wrap the
65
+ already-rendered `content` — it discards it and re-renders the **raw `value`** inside
66
+ `<ExpandableCell maxLength={40} expandable>`. Any rich cell type loses its component that way
67
+ (a status column rendered as truncated text with a "Show more" toggle instead of a badge).
68
+ `NEVER_EXPANDABLE_TYPES` (`STATUS`, `STATUS_BADGE`) now gates that branch, so status columns keep
69
+ their badge regardless of the meta flag. Fixing it in the renderer covers every client and
70
+ environment instead of chasing per-client `isExpandableCell` data rows.
71
+
63
72
  ## Cell types (`components/cellTypes/`)
64
73
 
65
74
  | Component | Type | Renders |
@@ -153,6 +162,16 @@ layer and emits it — see
153
162
 
154
163
  ## Gotchas
155
164
 
165
+ - **`isExpandableCell` still clobbers every other rich cell type.** Only `STATUS`/`STATUS_BADGE`
166
+ are protected today. `URGENCY`, `BOOLEAN`, `CURRENCY`, `DATE`, `AVATAR` and `CLIENT` carry the
167
+ **same latent defect** — set the flag on one of those fields and the cell degrades to raw
168
+ truncated text. The fuller fix is inverting the deny-list into an **allowlist** (expandable only
169
+ for free-text/default cells); deliberately not done yet.
170
+ - **A cell-rendering difference between local and a deployed environment is usually DB data, not
171
+ code.** `isExpandableCell` is a per-field flag in the **server's table meta**, so the same blox
172
+ build renders differently against two databases. Diff the branches over the cell-rendering files
173
+ **first** (`git diff <feature> origin/_sandbox-client -- src/components/Table/...`); if that comes
174
+ back empty, stop reading code and go look at the table-meta rows.
156
175
  - `MIN_COLUMN_WIDTH = 120` is the shrink floor only for columns with **no** explicit width.
157
176
  - **Never truncate header labels.** `truncateCells` (`.truncateCells .headerCellLabel` in
158
177
  `toga.module.css`) is for **body** values only; it must apply only `white-space: nowrap` to
@@ -198,6 +217,15 @@ layer and emits it — see
198
217
  requires `npm run build` before it takes effect at runtime.
199
218
 
200
219
  ## Change history
220
+ - 2026-08-26 — Fixed: status columns rendered as truncated text with a "Show more" toggle instead of
221
+ a badge. Root cause was in `resolveCellType` — the `isExpandableCell` branch **discards** the
222
+ rendered `content` and re-renders the raw `value` in `ExpandableCell`. Added
223
+ `NEVER_EXPANDABLE_TYPES` (`STATUS`, `STATUS_BADGE`) to gate it. Recorded that the same defect is
224
+ latent for `URGENCY`/`BOOLEAN`/`CURRENCY`/`DATE`/`AVATAR`/`CLIENT` (an allowlist is the fuller
225
+ fix, not done). Also recorded the debugging lesson: the "works locally, broken on deployed
226
+ `_sandbox-client`" symptom was **not** a code difference — the branch diff over the cell-rendering
227
+ files was empty; `isExpandableCell` was set in the sandbox-client DB and unset locally. Shipped in
228
+ `1.0.334-sandbox-client.136`. (apeterson)
201
229
  - 2026-08-25 — `useFetchTablePageMeta` now prefers **server-supplied `filterOptions`** and skips the
202
230
  `recordRoute` fetch for those fields (query list + index lookup share one `needsFetchedOptions`
203
231
  predicate so they can't skew); `filterOptions[].uuid` made optional and `surfaceSlug` declared on
@@ -30,8 +30,9 @@ declaration is invalid-at-computed-value-time and dropped.
30
30
 
31
31
  As of 2026-08-26 the component ships **default values via `var(--token, fallback)`**, so it renders
32
32
  fully styled with no host configuration, while any host-defined `--toaster-*` token still wins.
33
- *(All Toaster changes below are working-tree on blox `feature-new-table` and commerce `TRUE-80707`
34
- not yet merged or published; verify against the installed version before relying on them.)*
33
+ The blox-side changes are **merged and published** as of 2026-08-26 in
34
+ `@agilant/toga-blox@1.0.334-sandbox-client.136` (dist-tag `sandbox-client`). The commerce-side
35
+ rewiring (`TRUE-80707`) is still a separate branch — verify that half against the installed app.
35
36
 
36
37
  ## How it works
37
38
 
@@ -90,6 +91,9 @@ visually in a browser**.
90
91
 
91
92
  ## Change history
92
93
 
94
+ - 2026-08-26 — The blox-side fallback work above was committed, merged to `_sandbox-client` and
95
+ **published** as `1.0.334-sandbox-client.136`; superseded the earlier "working-tree, not yet
96
+ merged or published" caveat. (apeterson)
93
97
  - 2026-08-26 — Toaster now ships **default styling** via `var(--token, fallback)` on every custom
94
98
  property (renders styled with zero host config; host tokens still win) and gained the missing
95
99
  `border-style`/`border-radius`/`font-family`/`font-size`/`margin-block` on `.base`. Fixed two
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-04
9
+ updated: 2026-08-26
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga-blox/.github/workflows/publish.yml
@@ -109,11 +109,36 @@ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
109
109
  `react-router-dom` appeared in blox's `peerDependencies` but only three files referenced it, two of
110
110
  them tests; the single production reference was a **dead `import { Link }`** on line 1 of
111
111
  `src/components/old/BaseButton/BaseButton.tsx` (the component renders an injected `linkComponent`
112
- prop instead). The peer declaration and the dead import were removed *(working-tree on
113
- `feature-new-table` not yet merged or published)*; `react-router-dom` stays a
112
+ prop instead). The peer declaration and the dead import were removed and are **published** as of
113
+ 2026-08-26 in `1.0.334-sandbox-client.136`; `react-router-dom` stays a
114
114
  **devDependency** (`^6.30.1`) for the tests' `MemoryRouter`. Verified clean build, zero
115
115
  `react-router-dom` references in `dist/`, and vitest identical to baseline. This peer was the
116
116
  stated reason consumers pass `--legacy-peer-deps` against a router-7 app.
117
+ - **A feature branch that never touches the version line merges without a version conflict —
118
+ and that is why the bump has to happen afterwards.** Observed 2026-08-26: `feature-new-table` sat
119
+ at base `1.0.322` while `_sandbox-client` was at `1.0.332`. The merge auto-resolved the `version`
120
+ line to `1.0.332` with **no conflict**, because only `_sandbox-client` had ever edited it (the
121
+ "47 behind" commits are that same release plumbing — see the gotcha above). The bump to `1.0.333`
122
+ was then made **on `_sandbox-client`**, at the developer's explicit direction. That **deviates
123
+ from the `deploy-sandbox-client` skill's "never commit the bump directly to `_sandbox-client`"
124
+ rule.** It was safe *only* because the feature branch does not carry the version line, so nothing
125
+ could be clobbered — **record this as a one-off, not a new default.** CI then published base+1 →
126
+ `1.0.334-sandbox-client.136`.
127
+ - **`gh` is not installed on every dev machine.** The skill's `gh run watch` monitoring step simply
128
+ does not work there. Fall back to polling the registry:
129
+ `npm view @agilant/toga-blox@sandbox-client version` (or `npm dist-tag ls @agilant/toga-blox`)
130
+ until the new version appears — the registry is the authoritative confirmation anyway.
131
+ - **Consuming apps pin exactly, so a publish is only half the deploy.** After
132
+ `1.0.334-sandbox-client.136` shipped, `toga25-supply` on `_sandbox-client` was still pinned to
133
+ `1.0.333-sandbox-client.134` and needed a pin bump + reinstall. Likewise the feature branch stays
134
+ at its old base version after the bump lands on `_sandbox-client` — expect the two to be out of
135
+ step and don't read it as a problem.
136
+ - **⚠ Credentials have been found in the local git remote URL.** Verified 2026-08-26: the `origin`
137
+ remote for the `toga-blox-npm` checkout had a **GitHub PAT embedded in plaintext in the URL**
138
+ (visible to anything that runs `git remote get-url origin`, and to any tool that reads
139
+ `.git/config`). Check for this and re-point the remote at SSH or a credential helper; treat any
140
+ token found this way as compromised and rotate it. This is the local-config sibling of the
141
+ committed-`.npmrc` violation already recorded in `../../../standards/frontend.md`.
117
142
  - **FontAwesome v7 `style`-prop TS2322** (fixed 2026-07-21 in `getFontAwesomeIcon.tsx`, and a
118
143
  reusable pattern for any consumer). FA v7 types the `FontAwesomeIcon` `style` prop as
119
144
  `CSSProperties & CSSVariables`, where `CSSVariables` requires a `--fa-*` custom-property index
@@ -143,6 +168,16 @@ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
143
168
  the pattern the consuming apps should follow (see the commerce workflow's secret-hygiene note).
144
169
 
145
170
  ## Change history
171
+ - 2026-08-26 — Deployed `1.0.334-sandbox-client.136` (`feature-new-table` → `_sandbox-client`, 6
172
+ files, no conflicts) and recorded three workflow gotchas from it: (1) a feature branch that never
173
+ touches the `version` line merges without a version conflict, which is why the bump lands on
174
+ `_sandbox-client` afterwards — a documented **deviation** from the `deploy-sandbox-client` skill's
175
+ "never bump on `_sandbox-client`" rule, safe here only because of that, not a new default;
176
+ (2) `gh` is not installed on every machine, so verify the publish by polling
177
+ `npm view @agilant/toga-blox@sandbox-client version` instead of `gh run watch`; (3) consuming apps
178
+ pin exactly and need a separate pin bump + reinstall. Also flagged that a **GitHub PAT was found
179
+ in plaintext in the local `origin` remote URL** (rotate + re-point at SSH/credential helper), and
180
+ marked the phantom `react-router-dom` peer removal as merged and published. (apeterson)
146
181
  - 2026-08-26 — Corrected the branch-topology misreading: `feature-new-table`'s "0 ahead / 47 behind"
147
182
  is release plumbing generated on `_sandbox-client`, not staleness — it remains the active dev
148
183
  branch and correct base (real source delta: 2 files). Noted the `publish.yml` divergence is a
@@ -3,7 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa Supply (toga2-supply) Architecture](architecture.md) | `toga2-supply` is the **React + Vite frontend** for TOGa Supply — warehouse fulfillment tooling (shipment selection, fulfill & ship against carrier APIs, NetSui | toga2-supply/src/api/toga.ts, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx |
6
- | [Per-currency amount lines (CurrencyAmountLines / CurrencyAmountList) and child-row diffing](features/currency-amount-lines-editor.md) | The shared UI for "one amount per currency" on a parent record — an item's prices, a vendor item's costs. | toga2-supply/src/components/ui/CurrencyAmountLines.tsx, toga2-supply/src/pages/Items/api/itemsApi.ts, toga2-supply/src/pages/VendorItems/api/vendorItemsApi.ts |
6
+ | [Per-currency amount lines (CurrencyAmountLines / CurrencyAmountList) and child-row diffing](features/currency-amount-lines-editor.md) | The shared UI for "one amount per currency" on a parent record — an item's prices, a vendor item's costs. | toga2-supply/src/components/ui/CurrencyAmountLines.tsx, toga2-supply/src/pages/Items/api/itemsApi.ts, toga2-supply/src/pages/VendorItems/api/vendorItemsApi.ts, toga2-supply/src/pages/Items/view/modals/viewEdit/ItemsModalContentTemplate.tsx, toga2-supply/src/pages/VendorItems/view/modals/edit/VendorItemsModalContentTemplate.tsx, toga2-supply/src/utils/formatCurrency.tsx |
7
7
  | [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | _underscore/Trait/Netsuite/SalesOrder.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, _underscore/Model/Client/Measure.php, _underscore/Model/Client/TrackingNumber.php, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/getEditShipmentFormOptions.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/validateFormOnSubmit.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/checkDimensions.ts, toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/components/ui/Tables/types.ts, toga2-supply/src/components/ui/GoogleMapsLink.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/ShipmentItems/api/ShipmentItemsApi.ts, toga2-supply/src/pages/ShipmentItems/types.ts, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/RETURNLABELFIELDS.json, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
8
8
  | [Order-details field config (valueKey resolution) & the hardcoded Customer name](features/order-detail-field-config-and-customer-name.md) | The supply order-details modal is **config-driven**: each host has a JSON field list under `src/pages/Orders/view/OrderView/viewModel/FIELDS/<HOST>/`, and every | toga2-supply/src/utils/formatCustomerName.tsx, toga2-supply/src/pages/Orders/view/OrderView/components/invoices/OrderContentSection.tsx, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/api/toga.ts |
9
9
  | [Table-data request filters (`additionalData` → `getDataTableData`)](features/table-data-additional-filters.md) | A listing page adds its own row filters to a table-view data request by passing an **`additionalData`** prop down to the shared `useFetchData` hook, which forwa | toga2-supply/src/components/ui/Tables/hooks/useFetchData.tsx, toga2-supply/src/api/toga.ts, toga2-supply/src/pages/VendorItems/VendorItemsPage.tsx |
@@ -12,10 +12,14 @@ files:
12
12
  - toga2-supply/src/components/ui/CurrencyAmountLines.tsx
13
13
  - toga2-supply/src/pages/Items/api/itemsApi.ts
14
14
  - toga2-supply/src/pages/VendorItems/api/vendorItemsApi.ts
15
+ - toga2-supply/src/pages/Items/view/modals/viewEdit/ItemsModalContentTemplate.tsx
16
+ - toga2-supply/src/pages/VendorItems/view/modals/edit/VendorItemsModalContentTemplate.tsx
17
+ - toga2-supply/src/utils/formatCurrency.tsx
15
18
  related:
16
19
  - ../architecture.md
17
20
  - ../workflows/client-host-scoping.md
18
21
  - ../../api2/features/nested-relationship-writes.md
22
+ - ../../_underscore/features/acl-permission-chain.md
19
23
  - ../../../../clients/quad/features/multi-currency-item-pricing.md
20
24
  ---
21
25
 
@@ -25,8 +29,9 @@ The shared UI for "one amount per currency" on a parent record — an item's pri
25
29
  costs. Two components: **`CurrencyAmountLines`** (editable, `useFieldArray`) for the create/edit
26
30
  modals, and **`CurrencyAmountList`** (read-only) for the detail views. First consumer is Quad's
27
31
  multi-currency item pricing, gated by host (`localStorage` `hostName === 'QUAD'`), but the
28
- components and — more importantly — the **save/diff rules below are generic**: they apply to any
29
- parent whose children are unique per `(parent, type, currency)`.
32
+ components and — more importantly — the **save/diff and load-baseline rules below are generic**:
33
+ they apply to any parent whose children are unique per `(parent, type, currency)` and are fetched
34
+ by a request of their own.
30
35
 
31
36
  ## How it works
32
37
 
@@ -43,6 +48,63 @@ parent whose children are unique per `(parent, type, currency)`.
43
48
  issues explicit child writes (see the rules below). Existing rows carry their `uuid`; new lines
44
49
  do not.
45
50
 
51
+ ## The load baseline has THREE states, and all three must stay distinguishable
52
+
53
+ The child rows arrive from their own `GET`, so the modal holds a **baseline** to diff against. The
54
+ whole correctness story is telling apart *not read yet* / *read and empty* / *read failed* —
55
+ `getItemPricesForItem` / `getVendorItemCostsForVendorItem` therefore return `null` for a failed read
56
+ and `[]` for a genuinely empty one, never `[]` for both.
57
+
58
+ - `null` (**not read yet, or the read failed**) — render "Loading..." / the error, and **refuse the
59
+ save.** An absent baseline makes every submitted line look new, so the creates collide on the
60
+ unique key.
61
+ - `[]` (**read, no rows**) — render "None set." and allow the save.
62
+ - A separate `hasLoadFailed` flag distinguishes the two `null` cases for the message.
63
+
64
+ **Guard the save on `baseline === null` as well as on the failed flag** — not only on the flag. The
65
+ still-loading window is just as destructive as the failed one.
66
+
67
+ ### Clear the baseline BEFORE every read, and reset the failed flag with it
68
+
69
+ ```
70
+ setOriginalCostLines(null);
71
+ setCostLoadFailed(false);
72
+ formMethods.setValue('costs', []); // then fetch
73
+ ```
74
+
75
+ Three defects live in skipping this:
76
+
77
+ 1. **Not clearing on a record switch** keeps the previous record's rows in *both* the form and the
78
+ baseline, so a save in that window writes against the previous record's uuids.
79
+ 2. **Never resetting the failed flag** lets one record's failed read poison **every later record**
80
+ opened in the same mounted modal — the section shows the error forever.
81
+ 3. **A post-save refresh that returns `null` must invalidate the baseline** (set it back to `null`
82
+ and raise the failed flag), not silently do nothing. Leaving the pre-save baseline in place is
83
+ exactly the stale diff a retry then re-sends.
84
+
85
+ ## Two independent reads feeding one save need a save-time invariant
86
+
87
+ The vendor-item modal reads its **details** (`getSingleVendorItem(activeUuid)`) in one effect and
88
+ its **cost rows** in another. `activeUuid` comes from `useDataTableContext`, so **clicking a
89
+ different row changes it WITHOUT remounting the modal** — every effect keyed on it re-runs against a
90
+ live component still holding the previous record's state.
91
+
92
+ - **Reject stale responses** in *both* effects (`let isStillMounted = true`, cleanup sets it false).
93
+ Without it a slow response for the previous record lands after the newer record's costs and
94
+ overwrites the details.
95
+ - **⚠ That is necessary and NOT sufficient.** It does not close the window where the newer record's
96
+ **costs arrive before its details**, leaving the details on the old record with a fresh cost
97
+ baseline. The save then pairs one record's uuid with the other record's lines — destructive,
98
+ because the child `PUT` / `DELETE` address rows by their **own** uuids. Close it with an explicit
99
+ invariant at save time, which no request ordering can defeat:
100
+
101
+ ```
102
+ if (isQuad && uuid !== activeUuid) { toast('still loading'); return; }
103
+ ```
104
+
105
+ The **items** modal does not need this: it receives its record as a `parentData` prop and never
106
+ fetches it, so there is only one read that can go stale.
107
+
46
108
  ## ⚠ Diffing rules — a currency change is a DELETE + CREATE, never an UPDATE
47
109
 
48
110
  The child tables are UNIQUE on `(parentId, priceTypeId, currencyId)`, and that unique key is what
@@ -63,6 +125,46 @@ makes the naive diff fail:
63
125
  5. **Refresh the diff baseline after ANY save attempt, not only a fully successful one.** Otherwise
64
126
  a retry re-sends writes that already landed (duplicate creates, deletes of rows that are gone).
65
127
 
128
+ ### ⚠ A nullable `currencyId` makes "incomplete line" and "removed line" look identical
129
+
130
+ `ItemPrices.currencyId` and `VendorItemCosts.currencyId` are **NULLABLE** (while `price` / `cost`
131
+ are `NOT NULL`). A row with a null `currencyId` returns `currency: null` from the API, so it maps to
132
+ a line with an **empty currency code** through no fault of the user.
133
+
134
+ **Build the "kept" uuid set from EVERY submitted line, not just the complete ones:**
135
+
136
+ ```
137
+ // correct
138
+ const submittedUuids = new Set(input.lines.map(l => l.uuid).filter(Boolean));
139
+ // wrong: input.lines.filter(isCompleteLine) — an incomplete row falls out and is DELETED
140
+ ```
141
+
142
+ Built from only the complete lines, that row is absent from the set, is classified as **removed**,
143
+ and is **DELETED** even though the user never touched it. Worse than a stray delete, because the
144
+ `_unitPrice` / `_unitCost` calculated fields **INNER JOIN `Currencies`**: such a row is **invisible
145
+ in the grid** while the modal quietly destroys it.
146
+
147
+ The state is real in production — **Compass has 1568 `ItemPrices` rows with a null `currencyId`**
148
+ (Quad happens to have 0 today). **Rule: classify a delete on the row being REMOVED from the
149
+ submitted array, never on the row being incomplete.**
150
+
151
+ ## Display conventions for the read-only list
152
+
153
+ To sit flush with the rest of a detail modal, a hand-rolled label/value row must **mirror
154
+ `OldBaseDetailRow`**, not invent its own geometry:
155
+
156
+ - The label div carries padding on the **right only** (`pr-4`, no left padding), inside a
157
+ `flex w-full`, wrapped in `mb-1`. A fixed-width label box instead pushes the label off the left
158
+ edge that every other section label sits on.
159
+ - Amounts go through the repo's existing **`utils/formatCurrency`** (2 dp + thousand separators).
160
+ The API returns the amount as a JSON **number**, so a trailing `.00` is dropped and one row would
161
+ show decimals while the next did not. `formatCurrency` throws on non-numeric input — fall back to
162
+ the raw amount rather than rendering nothing.
163
+ - Final format: **code in the label column, then symbol + amount separated by `gap-1`**, with the
164
+ symbol **conditionally rendered** so a currency without one leaves no stray gap.
165
+ - A host-gated branch that **replaces** a `ModalContentSection` must carry **that section's own
166
+ `sectionContainerClasses`**, or it sits at a different offset from every other section.
167
+
66
168
  ## Gotchas
67
169
 
68
170
  - A read-only currency list that reaches for form context works in the edit modal and throws in the
@@ -70,9 +172,46 @@ makes the naive diff fail:
70
172
  - A calculated field that returns the concatenated per-currency text must be declared as plain
71
173
  `FIELD_SQL` on the model, or the value is cast to `0` before it ever reaches the grid — see
72
174
  [FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
175
+ - **`Currencies.symbol` is requestable; `Currencies.position` may not be.** Grants differ between
176
+ sibling columns of the same table, and one unreadable field **403s the WHOLE `GET`** — so adding
177
+ `currency.position` to a fields list can blank the entire price/cost read for a low-privilege role
178
+ while looking fine to a developer whose account also holds an admin role. Check the grant before
179
+ requesting it; the concrete Quad ids are in
180
+ [Quad multi-currency item pricing](../../../../clients/quad/features/multi-currency-item-pricing.md).
181
+ - **Map the amount defensively: test `null` AND `undefined`.** A `=== null`-only check renders the
182
+ literal string `"undefined"` as an amount when the field is missing from the response — which is
183
+ the normal shape for an ungranted field, see
184
+ [ACL permission chain](../../_underscore/features/acl-permission-chain.md).
185
+
186
+ ## Verified NOT applicable (do not re-raise)
187
+
188
+ - **Pagination on the price/cost read is not needed.** The unique key is
189
+ `owner + priceType + currency` and the query filters `priceType.slug = 'each'`, so the ceiling is
190
+ one row per currency the client trades in — 7 for Quad, against `recordsPerPage: 100` (observed
191
+ max 3 for items, 1 for vendor items). Revisit only for a client with dozens of currencies or more
192
+ than one price type.
193
+ - **A row cannot load with an empty amount from a null value.** `ItemPrices.price` and
194
+ `VendorItemCosts.cost` are both `NOT NULL`; the reachable variant is the nullable **`currencyId`**
195
+ above. And clearing an amount in the UI cannot silently delete a row: the amount input is
196
+ registered `required` and the submit handler is only reachable through `handleSubmit`, which
197
+ validates first.
73
198
 
74
199
  ## Change history
75
200
 
201
+ - 2026-08-26 — CodeRabbit-review pass on the same build. Fixed a **data-loss diff**: the "kept" uuid
202
+ set was built from the complete lines only, so a row with a **null `currencyId`** (nullable column;
203
+ 1568 such rows in Compass) was classified as removed and deleted unseen — invisible in the grid
204
+ because `_unitPrice` / `_unitCost` INNER JOIN `Currencies`. Every submitted line now counts. Added
205
+ the **three-state load baseline** rules (clear baseline + form + failed flag before each read;
206
+ invalidate the baseline when a post-save refresh returns null; refuse the save on
207
+ `baseline === null`, not only on the failed flag) after finding that a record switch kept the
208
+ previous rows and one failed read poisoned every later record. Added the **two-reads-one-save**
209
+ rule: `activeUuid` from `useDataTableContext` changes without remounting, so both effects reject
210
+ stale responses **and** the save asserts `uuid === activeUuid`, because rejecting stale responses
211
+ alone cannot stop the costs arriving before the details. Recorded the read-only display conventions
212
+ (mirror `OldBaseDetailRow`, `formatCurrency`, conditional symbol, carry `sectionContainerClasses`),
213
+ the `Currencies.position` field-grant trap, the `null`-vs-`undefined` amount mapper, and the two
214
+ review findings verified not applicable (pagination ceiling, `NOT NULL` amounts). (bala)
76
215
  - 2026-08-26 — First capture. Added the shared `CurrencyAmountLines` (editable, `useFieldArray`) and
77
216
  `CurrencyAmountList` (read-only, intentionally context-free because view mode renders outside the
78
217
  `FormProvider`) components, wired into the item / vendor-item create and edit modals behind
@@ -426,9 +426,9 @@ for **pin drift** between branches (commerce's `TRUE-80707` was 11 versions behi
426
426
  will not necessarily install them for you: verified 2026-08-18 they include `react-hook-form`,
427
427
  `@tanstack/react-query`, `@tanstack/react-table`, `framer-motion`, and `axios` (pinned exactly by
428
428
  blox at `1.8.4`). **`react-router-dom` was removed as a peer on 2026-08-26** — it was phantom (its
429
- only production reference was a dead `Link` import in `old/BaseButton`); *(change staged on blox
430
- `feature-new-table`, not yet merged/published verify against the installed version before relying
431
- on it)*. A missing peer surfaces as a runtime "undefined is not a component," **or**, because a
429
+ only production reference was a dead `Link` import in `old/BaseButton`); *(published in
430
+ `@agilant/toga-blox@1.0.334-sandbox-client.136`, dist-tag `sandbox-client`apps on an older pin
431
+ still see the old peer until they bump)*. A missing peer surfaces as a runtime "undefined is not a component," **or**, because a
432
432
  **barrel import pulls blox's whole index**, as a Rollup `failed to resolve import` for a component
433
433
  you never referenced.
434
434
 
@@ -589,9 +589,8 @@ Work **around** these shared-lib defects; do **not** imitate them in app code:
589
589
  stacks** (`react-table` v7 + `@tanstack/react-table` v8) — verified 2026-08-18 in blox's peer
590
590
  deps. Know which one a given component uses before you wire it.
591
591
  - **Phantom/unused peer deps in blox's manifest** — confirmed and one now removed:
592
- `react-router-dom` (dead `Link` import in `old/BaseButton`; removal staged 2026-08-26 on
593
- `feature-new-table`, not yet merged/published — it stays a devDependency for the tests'
594
- `MemoryRouter`). Verify a declared peer is actually reachable in `src/` before adding it to your
592
+ `react-router-dom` (dead `Link` import in `old/BaseButton`; removed and **published 2026-08-26 in
593
+ `1.0.334-sandbox-client.136`** — it stays a devDependency for the tests' `MemoryRouter`). Verify a declared peer is actually reachable in `src/` before adding it to your
595
594
  app or reaching for `--legacy-peer-deps`.
596
595
  - Supply/commerce business logic has leaked into the shared lib.
597
596
  - **No custom-validator passthrough on `BaseInput`** — hence the app-side validator layer in §7.
@@ -643,9 +642,11 @@ app work.** In a consuming app, guard against them and move on.
643
642
 
644
643
  - 2026-08-26 — Recorded commerce's missing `resolve.dedupe` (+ the blox-symlink second-React
645
644
  hazard), its caret blox pin and cross-branch pin drift, the barrel-import missing-peer trigger,
646
- and the removal of blox's phantom `react-router-dom` peer. All the underlying code changes are
647
- working-tree only (blox `feature-new-table`, commerce `TRUE-80707`) unmerged and unpublished.
648
- (apeterson)
645
+ and the removal of blox's phantom `react-router-dom` peer. The blox half is now merged and
646
+ published (`1.0.334-sandbox-client.136`); the commerce `TRUE-80707` half remains working-tree
647
+ only. (apeterson)
648
+ - 2026-08-26 — Corrected §13(b) and §22: blox's phantom `react-router-dom` peer removal is no
649
+ longer "staged/unpublished" — it shipped in `1.0.334-sandbox-client.136`. (apeterson)
649
650
  - 2026-08-18 — Initial front-end (React) coding standard: universal React/TS layer + TOGA 2.0
650
651
  platform-conventions layer, mined from @agilant/toga-blox, toga25-supply, and toga25-desk; per-rule
651
652
  convergence tagging; testing/a11y/error-reporting/React-19 flagged as open. (jcardinal)
@@ -59,6 +59,34 @@ This is why the vendor-items grid **looks** broken and is not:
59
59
  filters on (single-object filter form — see
60
60
  [table-data request filters](../../../2.0/apps/toga2-supply/features/table-data-additional-filters.md)).
61
61
 
62
+ ### `currencyId` is NULLABLE on both child tables
63
+
64
+ `ItemPrices.currencyId` and `VendorItemCosts.currencyId` are nullable (the amount columns are
65
+ `NOT NULL`). **Quad has 0 such rows today, Compass has 1568 in `ItemPrices`** — so the state is
66
+ reachable and any diff-based editor must not treat a currency-less row as removed. A row like that
67
+ is also **invisible in the grid**, because `_unitPrice` / `_unitCost` INNER JOIN `Currencies`. Rule
68
+ and fix: [per-currency amount lines](../../../2.0/apps/toga2-supply/features/currency-amount-lines-editor.md).
69
+
70
+ ## ⚠ `Currencies` field grants — `symbol` yes, `position` no
71
+
72
+ `Client_Quad.AclFieldPermissions` does **not** grant the `Currencies` columns uniformly:
73
+
74
+ | Field | id | Granted to roles |
75
+ |---|---|---|
76
+ | `code` | 1424 | 1, 7, 8, 9 |
77
+ | `symbol` | 1425 | 1, 7, 8, 9 |
78
+ | `position` | **1997** | **7, 8, 9 only — not 1** |
79
+
80
+ Requesting a field the caller cannot read fails the **whole request**, so putting
81
+ `currency.position` in a `fields=` list **403s the entire price/cost read** for any user holding only
82
+ role **1** (Base — which every Quad UI user has). It looks fine to a developer whose account also
83
+ carries role 7, which is exactly why it is invisible in dev. `currency.symbol` is safe to request;
84
+ `position` needs id 1997 granted to role 1 first.
85
+
86
+ Not needed today: **all seven Quad currencies are `position = LEFT`**, so rendering the symbol as a
87
+ prefix is faithful without reading the column. Mechanism:
88
+ [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
89
+
62
90
  ## Backend — `_unitPrice` and `_unitCost`
63
91
 
64
92
  - **`_Model_Quad_Item::_unitPrice`** returns the persona-scoped **single amount** when a
@@ -94,9 +122,16 @@ for a custom column is never read by the grid; labels must come from
94
122
 
95
123
  One shared editable component (`CurrencyAmountLines`, `useFieldArray`) plus a read-only
96
124
  `CurrencyAmountList`, wired into the item and vendor-item **create and edit** modals behind
97
- `localStorage` `hostName === 'QUAD'`. The save/diff rules (a currency change is a delete + create;
98
- all deletes before all creates; never a nested `PUT`) are generic and live in
99
- [per-currency amount lines](../../../2.0/apps/toga2-supply/features/currency-amount-lines-editor.md).
125
+ `localStorage` `hostName === 'QUAD'`. The save/diff and load-baseline rules are generic and live in
126
+ [per-currency amount lines](../../../2.0/apps/toga2-supply/features/currency-amount-lines-editor.md)
127
+ — a currency change is a delete + create, all deletes before all creates, never a nested `PUT`, the
128
+ "kept" uuid set is built from every submitted line (nullable `currencyId`), the child-row baseline is
129
+ cleared before each read and refuses a save while absent, and the vendor-item save asserts
130
+ `vendorItem.uuid === activeUuid` because its details and its cost rows come from two separate reads.
131
+
132
+ Quad specifics: the item modal is fed by `parentData` (one read), the vendor-item modal fetches its
133
+ own details (two reads), and **every change is gated on `isQuad`** — the shared table hook touched
134
+ during the build was reverted and is byte-identical to its pre-work state.
100
135
 
101
136
  ## Vendor-items Add / Edit buttons were hidden (fixed)
102
137
 
@@ -126,6 +161,15 @@ import was never broken, only the UI was.
126
161
 
127
162
  ## Change history
128
163
 
164
+ - 2026-08-26 — Review pass on the same build. Recorded the **`Currencies` field-grant asymmetry** in
165
+ `Client_Quad`: `code` (1424) and `symbol` (1425) are granted to roles 1/7/8/9 but `position` (1997)
166
+ only to 7/8/9, so requesting `currency.position` would have 403'd the entire price/cost read for a
167
+ role-1-only user while working fine for a developer who also holds role 7 — all seven Quad
168
+ currencies are `position = LEFT`, so a prefix symbol is faithful without it. Recorded that
169
+ `currencyId` is **nullable** on both child tables (Quad 0 rows, Compass 1568), which made a
170
+ currency-less row look "removed" to the save diff and get deleted while being invisible in the grid.
171
+ Measured the read ceiling (7 currencies x 1 price type ⇒ no pagination needed) and confirmed the
172
+ amount columns are `NOT NULL`. (bala)
129
173
  - 2026-08-26 — First capture. Built Quad per-currency item prices and vendor item costs end to end:
130
174
  `_Model_Quad_Item::_unitPrice` now returns a persona-scoped amount with a `personaId` and a
131
175
  `GROUP_CONCAT` of `'CODE amount'` pairs without one (it previously returned SQL `NULL`, so the
@@ -110,6 +110,12 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
110
110
  concatenated `'CODE amount'` text from `_unitPrice` / `_unitCost`, with those columns retyped
111
111
  `CURRENCY → STRING` for `Client_Quad` only. See
112
112
  [Quad multi-currency item pricing](./features/multi-currency-item-pricing.md).
113
+ - **`Currencies.position` is NOT readable by role 1 (2026-08-26).** `Client_Quad` grants `code`
114
+ (1424) and `symbol` (1425) to roles 1/7/8/9 but `position` (1997) only to 7/8/9 — and one
115
+ unreadable field 403s the **whole** request, so putting `currency.position` in a `fields=` list
116
+ blanks the entire read for a role-1-only user while working for a developer who also holds role 7.
117
+ All seven Quad currencies are `position = LEFT`. See
118
+ [Quad multi-currency item pricing](./features/multi-currency-item-pricing.md).
113
119
  - **BLOCKER (2026-08-26): UI item creation 403s in prod.** `Core` record 21 (`Items`) has
114
120
  `allowCreate = 0` for every Supply role (role 1 appId 1, roles 7/8/9 appId 5); only role 3 (API)
115
121
  may create, so `POST /items` fails for a UI user. Vendor items (19), item prices (117) and vendor
@@ -0,0 +1,86 @@
1
+ ---
2
+ type: session
3
+ slug: blox-toaster-peer-deps
4
+ title: Blox peer-dep ERESOLVE fix + Toaster host-token styling for commerce
5
+ author: apeterson
6
+ repos: [toga-blox, toga2-commerce, toga25-supply]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-08-26
11
+ updated: 2026-08-26
12
+ ---
13
+
14
+ # Session: blox-toaster-peer-deps
15
+ **Date:** 2026-08-26
16
+ **Project/Repo:** toga-blox (`feature-new-table`), toga2-commerce (`TRUE-80707`), toga25-supply (reference) — 2.0
17
+ **Task:** Unblock `npm i` in toga2-commerce (ERESOLVE on blox's `react-router-dom` peer), then fix the blox Toaster rendering unstyled in commerce by giving blox default host-token styling and adding commerce's missing token layer.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+
23
+ - **`legacy-peer-deps=true` in `toga2-commerce/.npmrc`** — resolved the ERESOLVE. Root cause was NOT the peer range: `toga25-supply/.npmrc` already had the flag and commerce did not, at the *same* blox version and *same* router major. `npm i` then succeeded ("added 4 packages, removed 6, changed 70"). `.npmrc` is git-tracked, so this reaches the Amplify build too.
24
+ - **Added blox's two missing peers to commerce** — `@fortawesome/free-solid-svg-icons@^6.7.2` and `react-multi-select-component@^4.3.4`. Required because `legacy-peer-deps` stops npm auto-installing peers. `vite build --mode sandbox-client` went from failing to green.
25
+ - **Removed blox's phantom `react-router-dom` peer + the dead `import { Link }`** in `src/components/old/BaseButton/BaseButton.tsx`. Verified: `npm run build` clean; `grep -rl "react-router-dom" dist/` returns **nothing**; `BaseButton.js` still renders the injected `linkComponent` prop. Tests unchanged from baseline (see below).
26
+ - **Fixed two invalid CSS declarations in blox `Toaster.module.css`** — `min-width: (--toaster-min-width)` and `max-width:` were missing `var()`, so the parser dropped both. supply masked this by re-declaring `min-width` in `.supplyToaster`; commerce had nothing to mask it.
27
+ - **Gave the blox Toaster default styling via `var(--token, fallback)`** on every custom property, plus `border-style`/`border-radius`/`font-family`/`font-size`/`margin-block` on `.base` (it previously set `border-width`/`border-color` with **no** `border-style`, so no border rendered at all). Component now renders fully styled with zero host config; host tokens still win.
28
+ - **Added commerce's missing `--toaster-*` token layer + `.commerceToaster--{success,error,restricted,default}`** to `src/styles/index.css`, and rewired `ToasterList.tsx` to supply's mechanism (variant class remaps `--toaster-default-*`, which blox's `.base` reads). Verified in emitted CSS: `--toaster-success-bg: #f3fdf1` present, all four variant classes present.
29
+ - **Added `resolve.dedupe` to `toga2-commerce/vite.config.ts`** (it had none; frontend.md §11 makes it a MUST). Confirmed `toga-blox-npm/node_modules/` ships its own `react@18.3.1` + `react-dom@18.3.1`, so symlinked blox resolves a second React copy.
30
+ - **Regression checks all green:** blox build clean; blox vitest `12 failed / 11 passed (23)`, `40 failed / 160 passed (200)`, 3 errors — **byte-identical to the stashed baseline**, so zero regression (those 40 failures are pre-existing). toga25-supply `tsc -b` + `vite build` clean. toga2-commerce `tsc -b` + `vite build` clean.
31
+ - **`/capture` published 6 docs** to `_main` (`validate: OK (30 repos)`, index 425 docs), including two elevated standards edits to `frontend.md` and `frontend-deploy.md`.
32
+
33
+ ## What did NOT work — DO NOT RETRY THESE
34
+
35
+ - **Do NOT conclude `feature-new-table` is stale from the behind-count.** It reports `0 ahead / 47 behind` `_sandbox-client` and `git merge-base --is-ancestor` says fully contained — which reads as "already merged, wrong base". **This is wrong.** Those 47 commits are almost entirely `Merge branch 'feature-new-table' into _sandbox-client` and `chore: bump version to 1.0.3xx` — release plumbing generated *on* `_sandbox-client` and never merged back. Real source diff is 2 files (`src/global.css`, `src/index.ts`); the rest of the 334-file diff is `.claude/knowledge/` docs and `publish.yml`. `feature-new-table` is the **active** dev branch (tip 2026-08-24, `feat: prefer server-supplied filter options in table meta`) and is the correct base. I asserted the opposite mid-session and had to retract it.
36
+ - **Do NOT cite toga25-supply's local symlink as evidence about the deployed path.** My local supply checkout was on `launch-aug26` with `node_modules/@agilant/toga-blox -> ../../../toga-blox-npm`. That is a **local dev artifact only**. On `_sandbox-client` supply consumes the *published channel*, pinned exact at `1.0.333-sandbox-client.134`, with `legacy-peer-deps=true` and `npm ci`. I used the symlink as an isolation argument and it did not apply.
37
+ - **Do NOT add `color` to `.prefixIcon` or `.closeBtn` in blox `Toaster.module.css`.** I tried it and reverted. The host's `iconColor` / `closeIconColor` classes land on those **same elements at equal specificity**, so a `color` declared there is resolved by stylesheet source order and can silently beat the host's class (supply's mint/crimson icons losing to a grey default). They must inherit from `.base`. In-code comments now record this — do not "helpfully" re-add it.
38
+ - **Do NOT use `Edit` with hand-typed indentation on `toga2-commerce/src/components/Toasters/ToasterList.tsx`.** Two `Edit` calls failed with "String to replace not found" — the repo indents with **TABS** (7 tabs for the destructure, 8 for members). Verified via `sed 's/\t/<TAB>/g'`. Worked around with a `node -e` script doing exact string replacement with `"\t".repeat(n)`.
39
+ - **Do NOT test the toaster by placing an order in commerce.** No success toaster exists on that path. `useSubmitOrder.ts` calls `addToaster` **only in its `catch` block** (`status: 'error'`). Success paths navigate away: cart → `createNewSalesOrderSubmit()` → `navigate("/order-details?uuid=…&type=orderPlaced")` (no `addToaster` anywhere in it); editOrder → `window.location.href = getSupplyRedirectLink(orderUuid)`. The developer tested this way, saw nothing, and it was **expected behavior, not a regression** — I initially mis-diagnosed it as a rendering failure and chased `resolve.dedupe` for a symptom that did not exist.
40
+ - **Typo to watch:** I emitted a full-width paren `)` inside the commerce `--toaster-default-boxShadow` value. Caught and fixed, but it built "fine" beforehand because the whole declaration was just invalid.
41
+
42
+ ## Not tried yet (candidates for next session)
43
+
44
+ - **Visual verification in a browser** — nothing here has been seen rendered. Only `tsc -b`, `vite build`, and emitted-CSS inspection.
45
+ - **The error variant specifically** — its colors changed to match supply (bg `crimson-50`→`crimson-300`, text `crimson-500`→`crimson-700`). Needs a forced submit failure to hit the catch block.
46
+ - **Committing anything** — all changes in blox and commerce are working-tree only.
47
+ - **Merging `feature-new-table` → `_sandbox-client`** to publish a new blox channel version.
48
+ - **Reconciling commerce's pin drift** — `TRUE-80707` is on `^1.0.322-sandbox-client.117`; `_sandbox-client` is on `1.0.333-sandbox-client.134` (11 versions apart), and the caret violates frontend.md §13(a).
49
+ - **Dropping `legacy-peer-deps` from commerce** once it consumes a blox build without the router peer — the flag would then be unnecessary for this conflict.
50
+ - **Rotating the committed `.npmrc` credentials** in both commerce and supply (npm + FontAwesome Pro tokens live in the tracked `.npmrc` in each repo — values not recorded here). Flagged unremediated in frontend.md Part 3.
51
+
52
+ ## Current file state
53
+
54
+ | File | Status | Notes |
55
+ |------|--------|-------|
56
+ | `toga-blox/package.json` | Modified | `react-router-dom` removed from `peerDependencies`; still a devDependency `^6.30.1` for tests' `MemoryRouter` |
57
+ | `toga-blox/src/components/old/BaseButton/BaseButton.tsx` | Modified | Dead `import { Link } from "react-router-dom"` removed (line 1); component unchanged otherwise |
58
+ | `toga-blox/src/components/Toaster/Toaster.module.css` | Modified | Rewritten: `var(--token, fallback)` throughout, `border-style`/`border-radius`/font/margin added to `.base`, **no `color`** on `.prefixIcon`/`.closeBtn` (commented why) |
59
+ | `toga2-commerce/.npmrc` | Modified | `legacy-peer-deps=true` appended |
60
+ | `toga2-commerce/package.json` | Modified | Added `@fortawesome/free-solid-svg-icons@^6.7.2`, `react-multi-select-component@^4.3.4` |
61
+ | `toga2-commerce/package-lock.json` | Modified | Regenerated |
62
+ | `toga2-commerce/vite.config.ts` | Modified | Added `resolve.dedupe` (react, react-dom, react-hook-form, react-router-dom, framer-motion, @tanstack/react-query, @tanstack/react-table) |
63
+ | `toga2-commerce/src/styles/index.css` | Modified | Added `:root` `--toaster-*` token block (mirrors supply's `themeConfig.json`) + `.commerceToaster` and four variant classes |
64
+ | `toga2-commerce/src/components/Toasters/ToasterList.tsx` | Modified | `getToasterStyles` now returns `{icon, variant}`; Tailwind color utilities replaced by `commerceToaster--${variant}` / `commerceToasterIcon--${variant}` |
65
+ | `toga25-supply` | **Unchanged** | Reference implementation only — nothing modified |
66
+ | `~/toga-tech/knowledge/**` | Committed + pushed | 6 docs published to `_main` via `/capture` |
67
+
68
+ ## Decisions made
69
+
70
+ - **Fix the ERESOLVE with `.npmrc` `legacy-peer-deps`, not by widening the peer range.** Rationale: the asymmetry vs supply was the actual defect, and this unblocks immediately with zero blox republish. Rejected: widening to `^6 || ^7` (still needs a republish to reach commerce); adding a `package.json` `overrides` entry (hides the problem from the next dev).
71
+ - **Delete blox's `react-router-dom` peer entirely rather than widening it.** Rationale: it is genuinely phantom — the only production reference was a dead import; the component takes `linkComponent` by prop. Widening would preserve a false declaration. Tests keep working via the existing devDependency.
72
+ - **Ship defaults as `var(--token, fallback)` inside the CSS Module, not a `:root` block in blox.** Rationale: blox ships no token block by design, and `dist/main.css` is uncompiled and unimportable (frontend.md §13f), so fallbacks are the only mechanism needing no stylesheet import. Rejected: adding `:root` to blox (new pattern, leak/conflict risk, requires an import).
73
+ - **Commerce mirrors supply's variant-remap mechanism** (variant class remaps `--toaster-default-*`) rather than styling with Tailwind utilities. Rationale: Tailwind utilities and blox's `.base` are both single-class selectors on the same element, so source order decides — inherently fragile. Rejected: keeping Tailwind classes on the wrapper div.
74
+ - **Qualify all knowledge-base claims as working-tree state.** Rationale: the KB would otherwise assert a peer removal that has not shipped; frontend.md §13(b)/§22 would be wrong if the blox change never lands.
75
+
76
+ ## Blockers
77
+
78
+ - **Ticket number needed for the blox commit.** CI enforces `TRUE-{ticketNumber}: {Subject}`. It is unknown whether this blox cleanup belongs under `TRUE-80707` (the commerce ticket) or its own. **This is the only hard blocker on committing.**
79
+ - **No visual confirmation of the Toaster.** Everything is build-verified only, so a purely visual defect would not have been caught.
80
+
81
+ ## Exact next step
82
+
83
+ > In `~/Agilant/toga2-commerce` (blox currently symlinked, `node_modules/.vite` already cleared), run `npm run sandbox-client`, log in, then go to **Account → add or edit a shipping address** and confirm the success toaster renders with supply's mint styling (bg `#f3fdf1`, border `#2cb224`, text `#075a11`, 400px min-width, 1rem radius). If it does not appear, check the browser console and whether `[data-testid="toaster"]` exists in the DOM — if the element is absent there is no styling bug and the problem is upstream in `contexts/ToasterContext`, not in any CSS changed this session.
84
+
85
+ ---
86
+ _Saved by /session-save on 2026-08-26_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.655",
3
+ "version": "1.0.656",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",