toga-ai 1.0.246 → 1.0.248

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.
@@ -70,20 +70,20 @@ This mirrors the toga2.5 / toga25-supply "named rule" / config-driven cart patte
70
70
  behavior is expressed in config, not in code branches.
71
71
 
72
72
  ## Gotchas
73
- - **Compass MacBook catalog categorization is INCONSISTENT, and it breaks the computer-kit
74
- assumption.** Verified in prod `Client_Compass.Items`: MacBooks are spread across at least
75
- **four** categories — `"APPLE LAPTOP"` (e.g. 14" MacBook Pro M5, partNumber `MDE54LL/A-S`),
76
- `"COMPUTERS"` (14"/16" MacBook Pro M4, `MW2W3LLA-S` / `MX2X3LLA-S`), `"MAC & ACCESSORIES"`
77
- (~18 older M1/M2 MacBook Air/Pro models), and `"PDC NEW HIRE - APPLE"`. The gate matches
78
- `itemCategory.name === "COMPUTERS"` **only**, so MacBooks tagged `APPLE LAPTOP` /
79
- `MAC & ACCESSORIES` do **not** qualify for expedited shipping (and would not trigger the
80
- one-per-order limit either). Confirmed live on beta and prod: the "Compass 14\" Macbook"
81
- kit's primary item resolves to `APPLE LAPTOP`. The backend assumption (Alex) was that all
82
- MacBooks were already `COMPUTERS`; the data shows otherwise. **Team decision this session:**
83
- keep the rule keyed to `COMPUTERS` only (intended). The fix is **catalog data
84
- normalization** (or extending the config `value` list to
85
- `["COMPUTERS","APPLE LAPTOP"]`) — it is *not* a code gap. Hand the categorization to the
86
- catalog/data team.
73
+ - **Computer-kit eligibility depends on consistent `COMPUTERS` categorization (was a data gap,
74
+ now resolved).** The gate matches the primary item's `itemCategory.name === "COMPUTERS"`
75
+ **only**. MacBook kits were initially mis-categorized: during this session
76
+ `Client_Compass.Items` showed MacBooks spread across **four** categories — `"COMPUTERS"`
77
+ (14"/16" MacBook Pro M4, `MW2W3LLA-S` / `MX2X3LLA-S`), `"APPLE LAPTOP"` (14" MacBook Pro M5,
78
+ `MDE54LL/A-S`), `"MAC & ACCESSORIES"` (~18 older M1/M2 models), and `"PDC NEW HIRE - APPLE"`.
79
+ A MacBook tagged anything other than `COMPUTERS` did **not** qualify for expedited (nor
80
+ trigger the one-per-order limit). **Resolved 2026-06-30:** the catalog/data team
81
+ re-categorized the computer/laptop kits (incl. MacBooks) to `COMPUTERS`; the tested
82
+ "Compass 14\" Macbook" was confirmed updated and now shows expedited. **Lesson:** the rule is
83
+ only as correct as the catalog — new computer/laptop kits must be filed under `COMPUTERS`
84
+ (the same category the bundle-page one-per-order limit keys off). If a future kit
85
+ legitimately needs a different category, extend the config `value` to a list (e.g.
86
+ `["COMPUTERS","APPLE LAPTOP"]`) rather than adding code.
87
87
  - **The gate is UI-only, not server-enforced.** Shipping methods come from `/shipping-methods`
88
88
  (UPS carrier, fetched in `useCartViewModel`) and are filtered client-side. The gate is a
89
89
  selection restriction, not order validation. Follow-up recommended: enforce
@@ -104,5 +104,8 @@ behavior is expressed in config, not in code branches.
104
104
  base-name compare via `getBaseShippingOptionName`; edit-order silent-downgrade/load race →
105
105
  reset guarded on dirty + loaded options. Documented the Compass MacBook category
106
106
  inconsistency as a known gotcha (data-normalization item, not a code gap). (tcox)
107
- </content>
108
- </invoke>
107
+ - 2026-06-30 — Catalog data normalized: the computer/laptop kits (incl. MacBooks) were
108
+ re-categorized to `COMPUTERS`, so MacBook kits now qualify for expedited; the tested
109
+ "Compass 14\" Macbook" was confirmed updated. Updated the MacBook-categorization gotcha to
110
+ reflect the resolution, and removed stray closing tags accidentally written into the doc.
111
+ (tcox)
@@ -8,5 +8,5 @@
8
8
  | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
9
9
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
10
10
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
11
- | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
11
+ | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
12
12
  | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-06-30
10
- owners: [jcardinal]
10
+ owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga25-supply/src/surface/useFetchSurfaceMeta.ts
13
13
  - toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
@@ -15,6 +15,7 @@ files:
15
15
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts
16
16
  - toga25-supply/src/surface/evaluateSurfaceRule.ts
17
17
  - toga25-supply/src/surface/actionRegistry.ts
18
+ - toga25-supply/src/surface/componentRegistry.tsx
18
19
  - toga25-supply/src/surface/SurfaceActionBar.tsx
19
20
  - toga25-supply/src/surface/SurfaceSection.tsx
20
21
  - toga25-supply/src/surface/resolve.ts
@@ -50,16 +51,35 @@ feature flag — see below). Backend: [surface-resolver](../../_underscore/featu
50
51
  meta. Loading a different *record* into the same surface reuses cached meta. Builds the URL as
51
52
  `/surfaces/meta?slug=...` (query string), **not** `/surfaces/<slug>/meta` (the path form makes the
52
53
  engine parse the slug as a record uuid → 404 EV-6). Unwraps the bundle via **`extractBundle()`**,
53
- which locates the bundle by its **structural fingerprint** (the `elements` array) across
54
- `raw` / `raw.data` / `raw.data.<route-key>` — because V2 nests a scripted-API return value one
55
- level deeper, under a route-keyed slot of `data` (e.g. `data.meta`, `data.surfaces`). On a malformed
56
- payload it falls back to an empty-elements bundle so the page never crashes.
54
+ which uses a recursive **`findBundle(raw, predicate, depth=4)`** to walk the envelope (replacing the
55
+ old hard-coded `raw`/`raw.data`/`raw.data.<key>` probing) and locates the bundle by its
56
+ **structural fingerprint**: `looksLikeBundle` now requires **both** an `elements` array **and** a
57
+ `surface` descriptor object (not the `elements` array alone — see the decoy gotcha below).
58
+ `extractBundle(raw, slug)` then **prefers** the bundle whose `surface.slug === slug`, falling back to
59
+ first-found, so a stray sibling bundle can't win. `looksLikeGroup` requires the documented `group`
60
+ discriminator key so a single-bundle envelope is not mistaken for a group. On a malformed payload it
61
+ falls back to a safe empty bundle (empty `elements` + empty `messages`/`theme`/`vocabularies` maps)
62
+ so the page never crashes.
57
63
  - **`useFetchSurfaceMetaGroup(slugs[])`** — the grouped sibling of `useFetchSurfaceMeta`, for a screen
58
64
  needing many section surfaces at once. **Order-insensitive** (the slug list is normalized so cache
59
65
  hits don't depend on argument order), `staleTime: Infinity`, hits the grouped endpoint
60
66
  `GET /v2/surfaces/meta-group?slugs=...`, and extracts the `{surfaces}` map via `extractGroup()` /
61
67
  `looksLikeGroup()` (the same structural-fingerprint approach as `extractBundle`). Returns `{}` for
62
68
  unmigrated screens (`enabled:false`), so a non-roster client does no fetch.
69
+ - **`componentRegistry`** (`src/surface/componentRegistry.tsx`) — the **presentation sibling** of
70
+ `actionRegistry`, both keyed by the surface meta's `action.key`. `actionRegistry` maps key→behavior
71
+ handler; `componentRegistry` maps key→custom React component. `SurfaceActionBar.renderElement` calls
72
+ `getSurfaceComponent(element.action?.key)` and renders the registered component (in a keyed Fragment)
73
+ **before** falling through to the default icon button — so `action.key` drives **both** which
74
+ component renders and its behavior, while `messages.label` supplies its text and clicks route through
75
+ the same `handle`→dispatch path. First entry: `"salesOrder.viewLog"` → `<ViewLogButton>`. Extending
76
+ is one surface element in meta + one registry entry (same model as `actionRegistry` and the Pattern-3
77
+ cell registry).
78
+ - **element `config` vs `action.payload`.** Per-element `config` (`Record<string,unknown>|null`) is the
79
+ opaque element-level seed bag (the SurfaceElements JSON long-tail). At runtime the **backend resolver
80
+ merges the Action's own config with the element `config` to produce `action.payload`**, which is what
81
+ the FE consumes — the FE renderer never reads `element.config` directly. A separate surface-level
82
+ `surface.config` lives on the descriptor (e.g. Inventory's `groupByLabels`/`entityTags`).
63
83
  - **`evaluateSurfaceRule`** — the Tier-1 rule evaluator: a **frozen `all/any/none` + `{field,op,value}`
64
84
  grammar** that **throws on an unknown op** (generalized from the SalesOrders
65
85
  `buildPatchedTenantFields`/`evaluateEnableRule`/`resolveFlag` helpers). Evaluated client-side against
@@ -155,6 +175,25 @@ seeding NYCHH/Prudential/SPGlobal is deferred; the Client-DB prod cross-cluster
155
175
 
156
176
  ## Gotchas
157
177
 
178
+ - **The single-meta envelope can hide the real bundle behind a DECOY `elements: []`.** The V2 envelope
179
+ can arrive as `{ surfaces: { meta: <real-bundle> }, elements: [] }` — a decoy empty `elements: []`
180
+ sits at the top level with **no `surface` descriptor**. A `looksLikeBundle` check matching on
181
+ `Array.isArray(value.elements)` alone short-circuits on the decoy and never finds the real nested
182
+ bundle → the action bar renders nothing. Fix: `looksLikeBundle` must require **both** `elements` AND
183
+ a `surface` descriptor; use recursive `findBundle` + slug-preferring `extractBundle` (see How it
184
+ works). The durable backend follow-up is to confirm/stabilize the single-meta envelope contract.
185
+ - **The surface `record` shape must match the rule field namespace — wrap as `{ order }`.** Seeded
186
+ Tier-1 rules namespace their field as `order._status`, so `getByPath(record, "order._status")` only
187
+ resolves if the host passes `record={{ order }}`. Passing the bare order object resolves to
188
+ `order.order._status` = undefined → every `in`/`eq` op is false → buttons hidden. The fix is at the
189
+ **host** (`SalesOrderTopBar.tsx`: `record={order ? { order } : null}`), **not** in
190
+ `evaluateSurfaceRule.ts` (the evaluator was correct).
191
+ - **`tokens.icon` is a LITERAL FontAwesome icon name, not a theme-token slug.** Only `tokens.color` and
192
+ `tokens.bg` are theme slugs resolved via `resolveThemeToken(theme, ...)` (the `theme` map only holds
193
+ `color.status.*`/`bg.status.*` keys). `tokens.icon` is a direct name (e.g. `check`, `xmark`,
194
+ `clipboardListCheck`, `penToSquare`) and must be used as-is (`element.tokens?.icon ?? "check"`).
195
+ Running it through `resolveThemeToken` misses on every icon and silently falls back to `"check"` for
196
+ every button.
158
197
  - **Scripted-API GET return values land under `data.<scriptRoute>`, not `data` directly.** V2 nests a
159
198
  scripted-API return value inside the envelope under a route-keyed slot of `data` (e.g. `data.meta`,
160
199
  `data.surfaces`), one level deeper than a normal data fetch. The bundle hook unwraps this with
@@ -179,6 +218,17 @@ seeding NYCHH/Prudential/SPGlobal is deferred; the Client-DB prod cross-cluster
179
218
  don't inherit Compass's shared base. Deleted the migrated section blocks from COMPASS/COMPASSCANADA/
180
219
  QUAD `orderViewFields.json` (data-wiring kept; DEFAULT intact). Action-bar/approval-modal still
181
220
  JSON. (jcardinal)
221
+ - 2026-06-30 — Debugged + extended the client-side action-bar renderer (no client-specific work).
222
+ Fixed three rendering bugs: (1) the single-meta envelope's **decoy `elements: []`** masked the real
223
+ nested bundle — `looksLikeBundle` now requires `elements` AND a `surface` descriptor, with recursive
224
+ `findBundle(depth=4)` + slug-preferring `extractBundle` + `group`-discriminator `looksLikeGroup` +
225
+ safe empty `messages`/`theme`/`vocabularies` fallback (`useFetchSurfaceMeta.ts`); (2) Tier-1 rules
226
+ always false because the host passed the bare order object instead of the `{ order }` envelope the
227
+ `order.*` field namespace requires (`SalesOrderTopBar.tsx`); (3) every action icon rendered as
228
+ `check` because `tokens.icon` (a literal FA name) was wrongly run through `resolveThemeToken`
229
+ (`SurfaceActionBar.tsx`). Built `componentRegistry.tsx` — the presentation sibling of
230
+ `actionRegistry`, key→custom component (`salesOrder.viewLog` → `ViewLogButton`). Clarified element
231
+ `config` vs backend-merged `action.payload`. (apeterson)
182
232
  - 2026-06-29 — Migrated VendorItems (screen 2, recipe-exact) and Inventory (screen 3,
183
233
  **presentation-only by decision** — fetch topology stays JSON; only title/buttons/modal copy/entity
184
234
  tags moved). Fixed the "s.elements is not iterable" crash: `extractBundle()` now finds the bundle by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.246",
3
+ "version": "1.0.248",
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",