toga-ai 1.0.385 → 1.0.387

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.
@@ -24,6 +24,7 @@
24
24
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
25
25
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
26
26
  | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
27
+ | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
27
28
  | [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
28
29
  | [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
29
30
  | [Refreshing a Local Dev Database from Beta (dev-sandbox)](workflows/local-db-refresh-from-beta.md) | How to reset a local 2.0 dev database from the **beta / dev-sandbox** environment: dump each schema (`Core`, `Client_<Id>`, `Logs_<Id>`, …) from the beta host, | api2/Config/, _underscore/Loader.php, _underscore/Model/Client/BundleTranslation.php, api2/Component/Api/V2/V2.php, toga25-supply/sync_compasscanada_schema.sql |
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: Table-View Hyperlink Columns (meta → ACL → computed URL → render)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-20
10
+ owners: ["bala"]
11
+ files:
12
+ - _underscore/Model/Client/TableView.php
13
+ - _underscore/Model/Client/TrackingNumber.php
14
+ - api2/Component/Api/V2/V2.php
15
+ - toga2-supply/src/api/toga.ts
16
+ - toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx
17
+ - toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx
18
+ - toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx
19
+ - dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql
20
+ related:
21
+ - ./acl-permission-chain.md
22
+ - ../../api2/features/tableview-apiwhereclause-row-filtering.md
23
+ - ../../api2/features/surface-meta-option.md
24
+ - ../../toga2-supply/architecture.md
25
+ ---
26
+
27
+ ## Summary
28
+
29
+ Any 2.0 table-view column can render its value as a clickable link instead of plain text.
30
+ The link URL comes from a **second** RecordField (`TableViewFields.hyperlinkRecordFieldId`)
31
+ whose value is a URL — typically a computed `FIELD_SQL` field. The pipeline is
32
+ platform-wide (all clients + the `BLANK_CLIENT_DATABASE` template), driven cross-repo:
33
+ `_underscore` emits a `hyperlinkField` in the view meta **only if the caller's role has ACL
34
+ field-read on the hyperlink field**, and `toga2-supply`'s shared `PrimaryTable` renders it.
35
+ The single most common failure is the silent one — the meta drops `hyperlinkField` (and the
36
+ column renders as plain text) when the ACL field-read grant is missing, not when anything is
37
+ "broken."
38
+
39
+ ## How it works (end to end)
40
+
41
+ 1. **Config** — `TableViewFields.hyperlinkRecordFieldId` on a column points at the Core
42
+ RecordField whose value is the URL. `NULL` = no hyperlink (plain text).
43
+ 2. **Meta emission** — `_Model_Client_TableView::meta()` (`_underscore/Model/Client/TableView.php`)
44
+ emits `hyperlinkField` for a column **only when** the caller's role appears in
45
+ `$permittedRecordFields` for that `hyperlinkRecordFieldId` (~line 290). No field-read
46
+ permission → the value is silently nulled out and the column falls back to plain text.
47
+ 3. **ACL source** — field-read permissions live in **each CLIENT DB's `AclFieldPermissions`**
48
+ (the `TrackingNumbers` record's `aclDatabase` is CLIENT), keyed by
49
+ `(recordFieldId, roleId)`. `api2/Component/Api/V2/V2.php::getAclFieldPermissions()` loads
50
+ them. See [ACL Permission Chain](./acl-permission-chain.md) for the full field-vs-record
51
+ authorization model.
52
+ 4. **Frontend request** — `toga2-supply/src/api/toga.ts` adds `hyperlinkField` to the data
53
+ request and stores the resolved URL on the row as `` data[`${slug}_hyperlink`] ``.
54
+ 5. **Frontend render** — the shared `PrimaryTable` draws the link (see below).
55
+
56
+ ## Computed URL: `TrackingNumbers._hyperlink`
57
+
58
+ - `_Model_Client_TrackingNumber::_hyperlink()` is a `FIELD_SQL` computed field:
59
+ `CONCAT(ShippingCarriers.urlLinkPrefix, TrackingNumbers.number)`.
60
+ - It is **`NULL` when the carrier has no `urlLinkPrefix`**, so a tracking number for a carrier
61
+ without a link prefix gracefully renders as plain text rather than a dead link.
62
+
63
+ ## Shared Core ids (identical across every environment)
64
+
65
+ - **RecordField 339** = `TrackingNumbers.number`
66
+ - **RecordField 1272** = `TrackingNumbers._hyperlink`
67
+ - Core RecordField ids are shared/identical across environments, so migrations key off them
68
+ directly (no per-tenant id lookup).
69
+ - **Role ids**: only `1` (Base), `2` (Developer), `3` (API) are identical across every client;
70
+ ids `>= 4` diverge per client. **Base (1) is the universal role** that carries the
71
+ tracking-number read grant in every client — which is why the ACL grant targets Base.
72
+
73
+ ## Frontend render (toga2-supply PrimaryTable)
74
+
75
+ - `convertData.tsx` — `generateDynamicConfig` sets `hasHyperlink: Boolean(item.hyperlinkField)`;
76
+ `ColumnConfig` gained `hasHyperlink?: boolean`.
77
+ - `useDataTableState.tsx` — `TableFieldValues` gained optional `hyperlinkField?: string`.
78
+ - `formatTableData.tsx` — a cell branch renders the value as
79
+ `<a target="_blank" rel="noopener noreferrer" ...>` (Tailwind `text-supply-blue-500 underline`)
80
+ wrapped in the same `flex/items-center` container as base cells for alignment; falls back to
81
+ plain text otherwise. The branch:
82
+ - runs **before** the `hasSuffix` branch (so hyperlink columns link even at a suffix index),
83
+ - is guarded by `rawConfig.isVisible !== false` (invisible columns are skipped),
84
+ - restricts `href` to `http(s)` URLs only (`/^https?:\/\//i`) as defense-in-depth,
85
+ - **takes precedence over the copy-button branch** — the tracking column now links instead of
86
+ offering copy-to-clipboard.
87
+
88
+ ## The ACL migration that unblocks it
89
+
90
+ `dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql` (in `Client/`,
91
+ so it runs against every client DB + the BLANK template), keyed off shared Core RecordField ids:
92
+
93
+ 1. `UPDATE TableViewFields SET hyperlinkRecordFieldId = 1272 WHERE recordFieldId = 339 AND
94
+ hyperlinkRecordFieldId IS NULL`, scoped to the two views `item-fulfillments-for-sales-orders`
95
+ and `item-fulfillments-for-sales-order-items`.
96
+ 2. `INSERT INTO AclFieldPermissions` granting READ on `1272` to the Base role (`roleId 1`),
97
+ `isWritable 0`, via `FROM DUAL` + `NOT EXISTS` (idempotent).
98
+
99
+ The `TableViewFields` link alone is **not** enough — without the Base `AclFieldPermissions`
100
+ grant the meta silently omits `hyperlinkField` and the tracking column stays plain text. That
101
+ missing grant was the actual blocker.
102
+
103
+ ## Key rules / gotchas
104
+
105
+ - A missing `AclFieldPermissions` read grant on the hyperlink RecordField makes the meta drop
106
+ `hyperlinkField` **silently** — the column just renders as plain text, with no error. Check the
107
+ ACL grant first when a configured hyperlink column isn't linking.
108
+ - Hyperlink resolution needs a URL-valued field; if the computed field is `NULL` (e.g. carrier
109
+ with no `urlLinkPrefix`) the cell correctly falls back to plain text.
110
+ - Grant to Base (roleId 1), not a per-client role id — Base is the only universally-present role
111
+ that holds the read grant across all tenants.
112
+
113
+ ## Change history
114
+ - 2026-07-20 — Added the missing `PrimaryTable` render path so table-view columns with a
115
+ `hyperlinkField` draw as `<a>` links (previously rendered plain text / copy button); hardened
116
+ for branch precedence, column visibility, and `http(s)`-only `href`. Added the cross-client
117
+ migration linking `TrackingNumbers.number` → `_hyperlink` and granting Base ACL read on
118
+ `_hyperlink` (1272), which was the real blocker. Documented the end-to-end meta → ACL →
119
+ computed-URL → render mechanism and the shared Core RecordField / role ids. Driven by Compass
120
+ USA (tracking numbers as clickable carrier links); the mechanism is platform-wide. (bala)
@@ -10,6 +10,7 @@
10
10
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
11
11
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
12
12
  | [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
13
+ | [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql |
13
14
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
14
15
  | [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php |
15
16
  | [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
@@ -14,6 +14,7 @@ files:
14
14
  - toga2-supply/src/api/toga.ts
15
15
  related:
16
16
  - ../../../clients/compass-usa/features/item-fulfillment-tracking-tableview.md
17
+ - tableview-field-metadata.md
17
18
  ---
18
19
 
19
20
  ## Summary
@@ -0,0 +1,97 @@
1
+ ---
2
+ title: TableView field/column metadata (TableViewFields, hidden projected columns)
3
+ framework: "2.0"
4
+ repo: api2
5
+ project: API
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-20
10
+ owners: [apeterson]
11
+ files:
12
+ - _underscore/Model/Client/TableView.php
13
+ - api2/Component/Api/V2/V2.php
14
+ - dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql
15
+ related:
16
+ - tableview-apiwhereclause-row-filtering.md
17
+ - ../../toga25-supply/features/meta-driven-table-data.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ The columns of a 2.0 table view are defined by DB metadata, not code. Per-client tables
23
+ `Client_*.TableViews` / `TableViewFields` / `TableViewJoins` declare the view, its columns, and its
24
+ joins; the frontend consumes the emitted meta (see
25
+ [meta-driven-table-data](../../toga25-supply/features/meta-driven-table-data.md)). Adding or altering
26
+ a column is a **dbchanges2 migration**, not a code change. A `TableViewFields` row with
27
+ `isVisible=0` is still fetched into the row projection but renders **no column** — the sanctioned way
28
+ to make a field available to frontend logic (e.g. a resolver that needs an fk uuid) without showing
29
+ it.
30
+
31
+ ## Data model
32
+
33
+ - **`Client_*.TableViews`** — one row per view (per client DB, e.g. `Client_Nychh`).
34
+ - **`Client_*.TableViewFields`** — one row per **column** of a view. Key columns: `recordFieldId`
35
+ (which field this column shows), `tableViewJoinId` (which join in the view the field is reached
36
+ through), `slug` (the frontend/tanstack column id), `isVisible` (0 = projected-but-hidden).
37
+ - **`Client_*.TableViewJoins`** — the view's joins; also drive `options.join`/`ojoin` on the data
38
+ request (see [tableview-apiwhereclause-row-filtering](tableview-apiwhereclause-row-filtering.md)).
39
+
40
+ ### `recordFieldId` is a cross-DB reference into shared `Core.RecordFields`
41
+
42
+ `TableViewFields.recordFieldId` does **not** point within the client DB — it references the
43
+ **shared** `Core.RecordFields` table. In `Core`:
44
+
45
+ - `Core.RecordFields.field` — the field-name column (the actual column/attribute name).
46
+ - `Core.RecordFields.recordId` → `Core.Records` — the owning record/model.
47
+
48
+ Reference ids observed for the `Items` record: **`Core.Records` id 21 = Items**, and
49
+ **`Core.RecordFields` id 106 = `Items.uuid`**. (Look these up per field before writing a migration —
50
+ do not assume ids across environments.)
51
+
52
+ ## Hidden (projected-but-not-rendered) columns
53
+
54
+ A `TableViewFields` row with `isVisible=0` is still selected into the row's data projection but
55
+ produces no rendered column (on the frontend, `buildTanstackColumns` filters by `isVisible`). This is
56
+ the mechanism behind fields the UI needs but should not display — the existing hidden
57
+ `inventorytype` / `Items.inventoryType` field on the inventory views is the precedent.
58
+
59
+ Use case that drove this doc: the `items-for-purchase-orders` view's rows are sales-order **lines**,
60
+ so `row.uuid` = `SalesOrderItems.uuid`. Opening the item-record modal needs the **catalog**
61
+ `Items.uuid`. Adding a hidden `Items.uuid` column puts `row.Items.uuid` into the projection so the
62
+ frontend `getModalUuid` resolver can open the catalog item (see
63
+ [record-modals-and-nested-tables](../../toga25-supply/features/record-modals-and-nested-tables.md)).
64
+
65
+ ## How to add a hidden projected column (migration recipe)
66
+
67
+ 1. Find the `Core.RecordFields.id` for the field (join `Core.Records` on `recordId`, match on
68
+ `field`). e.g. `Items.uuid` → 106.
69
+ 2. Resolve `tableViewJoinId` — reuse the join through which the field is reached. For an already
70
+ joined table, resolve it from an existing `TableViewFields` row that joins the same table (the
71
+ hidden `inventorytype` Items-join field was the source in the reference migration).
72
+ 3. `INSERT INTO TableViewFields (recordFieldId, tableViewJoinId, slug, isVisible, …)` with
73
+ `isVisible=0` and a distinct `slug` (e.g. `itemUuid`). Make it **idempotent** with a
74
+ `WHERE NOT EXISTS (...)` guard so a re-run is a no-op.
75
+
76
+ Cross-client field/column migrations live in **dbchanges2's `Client/` template folder**, not a
77
+ per-client folder — the reference migration
78
+ `dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql` adds the hidden
79
+ `Items.uuid` column to the `items-for-purchase-orders` view and was verified against `Client_Nychh`.
80
+
81
+ ## Gotchas / known issues
82
+
83
+ - **`recordFieldId` is cross-DB** — it indexes `Core.RecordFields`, not anything in the client DB.
84
+ Resolve the id in `Core`, but insert the `TableViewFields` row in the client DB.
85
+ - **Don't assume `Core.RecordFields`/`Core.Records` ids are stable across environments** — look them
86
+ up by `field` name + record.
87
+ - **Make column migrations idempotent** (`NOT EXISTS` guard) — they run per client DB.
88
+ - **`isVisible=0` still costs a fetch** — the field is selected into the projection; only the render
89
+ is suppressed.
90
+
91
+ ## Change history
92
+
93
+ - 2026-07-20 — Documented the backend TableView column-metadata model (per-client
94
+ `TableViews`/`TableViewFields`/`TableViewJoins`, `recordFieldId` → shared `Core.RecordFields`
95
+ cross-DB ref, `isVisible=0` projected-but-hidden columns) and the dbchanges2 recipe for adding a
96
+ hidden projected column, discovered while making the Inventory item-record modal open the catalog
97
+ `Items.uuid`. (apeterson)
@@ -8,7 +8,7 @@
8
8
  | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
9
9
  | [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 |
10
10
  | [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 |
11
- | [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
+ | [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/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts |
12
12
  | [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/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, 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/SurfaceRowActions.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, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx |
13
13
  | [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json |
14
14
  | [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 |
@@ -6,18 +6,23 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-07-20
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/layout/ItemRecordModalLayout/
13
13
  - toga25-supply/src/layout/SalesOrderRecordModalLayout/
14
14
  - toga25-supply/src/layout/SalesOrderItemsTableLayout/
15
15
  - toga25-supply/src/layout/ItemFulfillmentModal/
16
- - toga25-supply/src/layout/GenericNestedTables/
16
+ - toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx
17
+ - toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx
18
+ - toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
19
+ - toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json
17
20
  - toga25-supply/src/hooks/useTableCellInteractions.ts
21
+ - toga25-supply/src/hooks/useServerTableUrlState.ts
18
22
  related:
19
23
  - ../architecture.md
20
24
  - meta-driven-table-data.md
25
+ - ../../api2/features/tableview-field-metadata.md
21
26
  ---
22
27
 
23
28
  ## What it is
@@ -107,6 +112,29 @@ A page-level `pageMeta` (from `useFetchPageMeta`) flows down for field-label ove
107
112
  view model builds `TableLevel[]` arrays, picks the active one via a URL param, and passes `levels`
108
113
  to `GenericNestedTables` with `key={activeGrouping.key}` to force remount and avoid stale state.
109
114
 
115
+ ### Decoupling "drill to child tier" from "open this record" on a swap level
116
+
117
+ By default a `"swap"` level consumes the **whole-row click** (`getRowHref`) to drill to the next
118
+ tier, which leaves no room for a per-row record modal. Two optional `TableLevel` hooks split the two
119
+ interactions so both are reachable on one nested row (Inventory `items-for-purchase-orders` /
120
+ `purchase-orders_items` levels are the reference):
121
+
122
+ - **`buildSwapTriggerColumns(triggerSwap)`** — when present, the level renders a dedicated
123
+ first-column action button (Inventory's "See units": a `chevronsRight` icon wrapped in a toga-blox
124
+ `BaseToolTip`) whose click calls `triggerSwap`; the whole-row swap (`getRowHref`) is **disabled**
125
+ and inline child expansion is **suppressed while nested**, so a plain row click is free to open
126
+ `renderModal`. Net UX: chevron → child tier; row click → record modal. The Inventory hydrator
127
+ (`hydrateLevel`, role `"swap"`) wires `buildSwapTriggerColumns: makeSeeUnitsColumns` and
128
+ `renderModal` from the level's `modalKey`.
129
+ - **`getModalUuid(row)`** — override which uuid the record modal opens for a row. `GenericTableLayout`
130
+ wraps `handleRowClick` to store `getModalUuid(row) ?? row.uuid` as the active/modal uuid. Needed
131
+ when the row's own `row.uuid` is not the uuid the target record endpoint expects — e.g. the
132
+ `items-for-purchase-orders` rows are sales-order **lines** (`row.uuid` = `SalesOrderItems.uuid`),
133
+ but the item-record modal must open the **catalog** item, so the hydrator sets
134
+ `getModalUuid: (row) => row?.Items?.uuid`. That requires the catalog uuid to be present in the row
135
+ projection: a **hidden (`isVisible=0`) `Items.uuid` column** is added to the table view — see
136
+ [tableview-field-metadata](../../api2/features/tableview-field-metadata.md).
137
+
110
138
  ## Pattern 6 — Create/edit record modal (form-driven)
111
139
 
112
140
  Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLayout/`). Form-driven
@@ -136,6 +164,15 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
136
164
  - **Submit payload diverges from form shape** — send relation `.uuid`, not the `{uuid,name}` object;
137
165
  confirm field names against the real POST/PUT contract.
138
166
  - **Nested tables need their own slug + URL-state isolation** or sibling tables clobber each other.
167
+ - **A swap level's `urlParamKey` must NOT equal (or be a `${slug}_`-prefixed variant of) any level's
168
+ slug.** The record-modal / active-row state is stored in a URL param named **exactly** the table
169
+ slug (`src/hooks/useServerTableUrlState.ts`: `activeRowUuid = searchParams.get(slug)`, and
170
+ `handleRowClick` sets `?<slug>=<uuid>`). If a swap level's `urlParamKey` is the same string as a
171
+ level's fetchSlug/slug, a plain row click writes `?<slug>=<uuid>` which `GenericNestedTables` reads
172
+ as "swap active" and jumps to the child tier instead of opening the modal. Fix: give swap keys a
173
+ `swap-` **prefix** (`items-for-purchase-orders` → `swap-items-for-purchase-orders`). A `${slug}_…`
174
+ **suffix** can't be used because `useServerTableUrlState`'s columnFilters parser treats any
175
+ `${slug}_*` key as a column filter — hence a prefix.
139
176
  - **`additionalData` in the query key is mandatory** for WHERE-filtered modal tables (Pattern 4).
140
177
  - **Per-consumer cache `scope`** — two consumers fetching the same record with different field
141
178
  configs must use a `scope` discriminator in the query key or they overwrite each other's cache
@@ -154,6 +191,12 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
154
191
  `/approval-decisions/{uuid}`.
155
192
 
156
193
  ## Change history
194
+ - 2026-07-20 — Pattern 5: added `buildSwapTriggerColumns(triggerSwap)` and `getModalUuid(row)` to
195
+ `TableLevel` so a swap level's "drill to child tier" (a dedicated "See units" chevron) and
196
+ "open this record" (plain row click → `renderModal`) are decoupled, and a record modal can open a
197
+ different uuid than `row.uuid` (Inventory line rows open the catalog `Items.uuid`). Fixed a URL-param
198
+ collision — swap `urlParamKey` equal to a level slug hijacked the row-click into a swap; swap keys now
199
+ carry a `swap-` prefix — and documented the rule. (apeterson)
157
200
  - 2026-06-30 — Recorded two behaviors confirmed while building approval-modal tests: `poNumber`
158
201
  is handled by a separate `EnterPoNumberModal` (excluded from the approval layout), and the
159
202
  workflow vs single-decision submit payloads differ (string `"1"`/`"0"` + changed-stages-only +
@@ -17,9 +17,9 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 35 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 36 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 31 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
- - **api2** (API) — 12 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
22
+ - **api2** (API) — 13 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
24
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
25
25
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.385",
3
+ "version": "1.0.387",
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",