toga-ai 1.0.608 → 1.0.609

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.
@@ -39,6 +39,7 @@
39
39
  | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
40
40
  | [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, api2/Component/Api/V2/V2.php |
41
41
  | [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 |
42
+ | [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
42
43
  | [TogaIQ Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | `_Component_Api_Togaiq` is the 2.0 framework's client for the **TogaIQ** (Talos) AI gateway. | _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/ApiRequest.php |
43
44
  | [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, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql |
44
45
  | [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 | _underscore/Model/Client/ServiceRequest.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql |
@@ -0,0 +1,171 @@
1
+ ---
2
+ title: TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-18
10
+ owners: [bala]
11
+ files:
12
+ - _underscore/Model/Client/TableView.php
13
+ - dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
14
+ - dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
15
+ - dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
16
+ related:
17
+ - ../../api2/features/tableview-field-metadata.md
18
+ - ./page-meta-context-field-settings.md
19
+ - ./tableview-hyperlink-columns.md
20
+ - ../../api2/features/tableview-apiwhereclause-row-filtering.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its
26
+ base record. `_Model_Client_TableView::meta()` (`_underscore/Model/Client/TableView.php`) turns each
27
+ row into one entry of `table.joins` (`table`, `alias`, `type`, `onA`, `onB`). Two things about that
28
+ translation are load-bearing and non-obvious:
29
+
30
+ - **`onB` is emitted with the RAW TABLE NAME of the parent field's record, never with an alias.**
31
+ - **`joinOnTableViewJoinId` does not affect the emitted ON clause at all.**
32
+
33
+ Together those mean a **two-hop (chained) join works only because the first join of a table is
34
+ aliased to the plain table name**. Get the order or the duplication wrong and the view emits SQL
35
+ referencing a table that was never joined.
36
+
37
+ ## How it works
38
+
39
+ The joins for a view are read with (paraphrased, ~line 55-73):
40
+
41
+ ```sql
42
+ SELECT TableViewJoins.id, joinRecordId, joinOnRecordFieldId, joinOnTableViewJoinId,
43
+ parentRecordFieldId, parentCustomRecordFieldId, `type`
44
+ FROM TableViewJoins
45
+ INNER JOIN AclRecordPermissions ON AclRecordPermissions.recordId = TableViewJoins.joinRecordId
46
+ WHERE TableViewJoins.tableViewId = <id> AND AclRecordPermissions.roleId IN (<user roles>)
47
+ ORDER BY TableViewJoins.sortOrder ASC, TableViewJoins.id ASC
48
+ ```
49
+
50
+ Then, per join:
51
+
52
+ - **Alias.** A counter (`$tableViewAliasOrds`) starts at `65` (`A`) and is seeded with the view's
53
+ **base** table. The first join of a given table gets `ord == 65`, which means its alias is the
54
+ **plain table name**. Any later join of that same table becomes `<Table>_<letter>`.
55
+ - **ON clause.**
56
+ - `onA` = `` `<this join's alias>`.`<joinOnRecordField.field>` ``
57
+ - `onB` = `` `<($parentRecord->model)::TABLE>`.`<parentRecordField.field>` `` — the **raw table
58
+ name** of the record that owns `parentRecordFieldId`.
59
+ - `joinOnTableViewJoinId` is used for exactly one thing: setting `joinOnTableAlias` on the join
60
+ object. The current code **never emits `joinOnTableAlias` into `onA`/`onB`**. Treat it as
61
+ documentation of the chain, not the mechanism.
62
+
63
+ ## Chained (multi-hop) joins
64
+
65
+ Example shipped 2026-08-18 on the `inventory_units` view (base record **Units**, 31): to show
66
+ `Manufacturers.name` you need **Units → Items → Manufacturers**.
67
+
68
+ - Hop 1: join **Items** (record 21) `ON Items.id (105) = Units.itemId (238)`
69
+ - Hop 2: join **Manufacturers** (record 20) `ON Manufacturers.id (104) = Items.manufacturerId (107)`
70
+
71
+ Hop 2's `onB` renders as `` `Items`.`manufacturerId` ``. That resolves **only** because hop 1 is the
72
+ first Items join on the view and therefore aliased `Items`. So:
73
+
74
+ - **The parent hop must sort first.** Joins are ordered `sortOrder ASC, id ASC`, so give the parent
75
+ hop the lower `sortOrder` or the SQL references a table that has not been joined yet.
76
+ - **A chain breaks if its parent table is joined more than once on the same view.** The extra join
77
+ gets a suffixed alias while `onB` still says the bare table name, so the chain silently binds to
78
+ the first join — or fails outright if the bare-name join is not the one you meant.
79
+ - Before this change there were **zero** rows with a non-null `joinOnTableViewJoinId` anywhere in
80
+ `Client_Elite`, so chained joins were previously unused in these schemas.
81
+
82
+ ## Look for an existing unused join before adding one
83
+
84
+ The `inventory_units` view already carried a Units → Items join
85
+ (`TableViewJoins.uuid = '7894941a-d00b-49db-af5f-63835c29b1a4'`, joinRecordId 21, joinOnRecordFieldId
86
+ 105 `Items.id`, parentRecordFieldId 238 `Units.itemId`, `OUTER`, `sortOrder 0`) in **both** clients
87
+ and in **both** prod and client-sandbox — referenced by **zero** `TableViewFields`. It came in with
88
+ the all-clients template `dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql`.
89
+ Only the second hop had to be created. Always check for a usable join first; a duplicate Items join
90
+ would also have broken the chain above.
91
+
92
+ ## The joined record needs an AclRecordPermissions row
93
+
94
+ The joins query **INNER JOINs `AclRecordPermissions` on `joinRecordId`**. With no row for the
95
+ caller's role, the join is silently dropped from the result set, and any `TableViewFields` row
96
+ pointing at it then throws:
97
+
98
+ ```
99
+ There is no AclRecordPermissions record permitting access to TableViewJoins.id = N for this user
100
+ ```
101
+
102
+ So a new joined table (here `Manufacturers`, record 20) needs its `AclRecordPermissions` grant in the
103
+ same migration.
104
+
105
+ ## Context RecordFieldSettings are OPTIONAL for a joined column to render
106
+
107
+ `meta()` looks up `contextRecordFieldId`-scoped settings (`DefaultRecordFieldSettings` plus the
108
+ Client / Persona / User / Language `RecordFieldSettings` tables) only to decide **which settings row
109
+ supplies the column label**. When no context row exists for `(recordFieldId, contextRecordFieldId)`,
110
+ it nulls `contextTable` / `contextField` and falls back to the field's own settings. A new joined
111
+ column therefore does **not** require adding `RecordFieldSettings` rows just to appear — those rows
112
+ are only needed to control the **header text** (see
113
+ [page-meta-context-field-settings](./page-meta-context-field-settings.md), which is also where the
114
+ "header renders as a camelCase slug" fallback is explained).
115
+
116
+ ## Authoring the migration (house style used here)
117
+
118
+ The 2026-08-18 change added Part Number (`Items.partNumber` 108), Manufacturer
119
+ (`Manufacturers.name` 103) and Item Description (`Items.description` 109) to `inventory_units`
120
+ (`TableViews.uuid 86b3f376-2a15-4a46-ac0a-9b25146e40ac`), positioned directly after Asset Tag,
121
+ mirroring how `inventory_items` surfaces the same three fields. Applied to **prod and
122
+ client-sandbox for both Elite and NYCHH** as one file per client with identical content.
123
+
124
+ - **Key everything off `uuid`, never off `id`.** `TableViews.id` happened to be 21 in both clients,
125
+ but the pre-existing `TableViewJoins.id` differed (Elite 142, NYCHH 165). Resolve ids with
126
+ `(SELECT id FROM TableViewJoins WHERE uuid = ...)`.
127
+ - **One idempotent file beats an insert file plus a follow-up reorder file.** The insert had already
128
+ run in all four targets when the reposition was requested; rather than ship a second migration, the
129
+ same file was rewritten as `INSERT ... ON DUPLICATE KEY UPDATE` keyed on `uuid` for the join and the
130
+ three fields, followed by an unconditional renumber. On an already-applied schema the net effect is
131
+ just the reorder — no duplicate rows, no row-id churn.
132
+ - **`@` session variables mean the file must run in ONE connection.** Splitting the statements across
133
+ connections (or across two client-tool tabs) silently loses the variables and the renumber writes
134
+ garbage.
135
+ - Reordering columns has its own hazard — `TableViewFields` has `UNIQUE (tableViewId, index)`, so a
136
+ naive `index = index + N` can collide mid-statement. The safe park-shift-pull pattern is documented
137
+ in [tableview-field-metadata](../../api2/features/tableview-field-metadata.md).
138
+
139
+ Core-level reference ids used (identical across all clients and environments): **Records** — Units
140
+ 31, Items 21, Manufacturers 20. **RecordFields** — `Units.itemId` 238, `Items.id` 105,
141
+ `Items.partNumber` 108, `Items.description` 109, `Items.manufacturerId` 107, `Manufacturers.id` 104,
142
+ `Manufacturers.name` 103.
143
+
144
+ ## Gotchas
145
+
146
+ - **`onB` never uses an alias.** Any chain depends on the parent hop owning the bare table name.
147
+ - **`joinOnTableViewJoinId` is inert in the emitted SQL** — setting it does not make a chain work,
148
+ and omitting it does not break one. It is metadata for humans until the ON-clause builder starts
149
+ using `joinOnTableAlias`.
150
+ - **Alias suffixes are not `_B` for the first duplicate.** The counter is incremented both inside the
151
+ `isset` branch and again at the end of the loop, so the second join of a non-base table lands on
152
+ `chr(67)` → `<Table>_C`; `<Table>_B` only appears when the view joins its own **base** table. Do
153
+ not hand-write an alias from memory — check the counter logic before relying on one.
154
+ - **A missing `AclRecordPermissions` row for the joined record drops the join, not just the column** —
155
+ and the resulting error names a `TableViewJoins.id`, which is why it reads like config corruption
156
+ rather than an ACL gap.
157
+ - **An unused join can already exist.** Confirm before inserting; a duplicate join of the same table
158
+ is what breaks chained joins.
159
+
160
+ ## Change history
161
+
162
+ - 2026-08-18 — Documented how `TableViewJoins` compile into SQL after adding a chained
163
+ **Units → Items → Manufacturers** join for the Part Number / Manufacturer / Item Description columns
164
+ on `inventory_units` (Elite + NYCHH): `onB` is emitted with the **raw table name** (so a chain only
165
+ works off the first, unaliased join of a table), `joinOnTableViewJoinId` feeds only
166
+ `joinOnTableAlias` and is **not** in the ON clause, joins are ordered `sortOrder ASC, id ASC`, the
167
+ joins query INNER JOINs `AclRecordPermissions` on `joinRecordId` (missing grant = dropped join plus
168
+ the "no AclRecordPermissions record permitting access to TableViewJoins.id" error), and context
169
+ `RecordFieldSettings` are optional for rendering (label-only). Also recorded the pre-existing unused
170
+ Units→Items join `7894941a-...` present in both clients, and the uuid-keyed idempotent single-file
171
+ migration style. (bala)
@@ -18,7 +18,7 @@
18
18
  | [TOGa IQ Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The internal **TOGa IQ sprint dashboard** is served in production by **six api2 Record Scripts** on `_Model_Team_Sprint` (`_underscore/Model/Team/Sprint.php`), | _underscore/Model/Team/Sprint.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-07-24a - SprintDashboardRecordScripts.sql, dbchanges2/Client_True/2026-07-24b - SprintDashboardScriptAcl.sql |
19
19
  | [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 |
20
20
  | [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 |
21
- | [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, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql |
21
+ | [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, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
22
22
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
23
23
  | [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, api2/Component/Api/V2/Response/Response.php, api2/Component/Api/V2/Response/Oauth/Oauth.php, api2/Controller/Index.php, toga2-supply/src/globalTypes.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/api/toga.ts, _underscore/Model/Client/TrackingNumber.php |
24
24
  | [V2 request deadlock-retry — route-scoped in-process replay](features/v2-deadlock-retry.md) | api2's front controller can **detect a MySQL deadlock (1213) / lock-wait timeout (1205) and replay the whole request in-process**, so a transient lock collision | api2/Controller/Index.php, _underscore/Database.php, _underscore/Query.php |
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
10
- owners: [apeterson]
9
+ updated: 2026-08-18
10
+ owners: [apeterson, bala]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
13
13
  - api2/Component/Api/V2/V2.php
@@ -15,9 +15,12 @@ files:
15
15
  - dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql
16
16
  - dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql
17
17
  - dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql
18
+ - dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
19
+ - dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
18
20
  related:
19
21
  - tableview-apiwhereclause-row-filtering.md
20
22
  - ../../toga25-supply/features/meta-driven-table-data.md
23
+ - ../../_underscore/features/tableview-joins.md
21
24
  ---
22
25
 
23
26
  ## Summary
@@ -52,6 +55,8 @@ it. (Reference: `Core.RecordFields` id 369 = `ItemFulfillmentItems.quantity` set
52
55
  `type=NUMBER`, `precision=0`, which normalizes that column across all fulfillment views/clients.)
53
56
  - **`Client_*.TableViewJoins`** — the view's joins; also drive `options.join`/`ojoin` on the data
54
57
  request (see [tableview-apiwhereclause-row-filtering](tableview-apiwhereclause-row-filtering.md)).
58
+ How a join row becomes SQL — aliasing, chained multi-hop joins, and the ACL row the joined record
59
+ needs — is in [tableview-joins](../../_underscore/features/tableview-joins.md).
55
60
 
56
61
  ### `recordFieldId` is a cross-DB reference into shared `Core.RecordFields`
57
62
 
@@ -65,6 +70,32 @@ Reference ids observed for the `Items` record: **`Core.Records` id 21 = Items**,
65
70
  **`Core.RecordFields` id 106 = `Items.uuid`**. (Look these up per field before writing a migration —
66
71
  do not assume ids across environments.)
67
72
 
73
+ ## Column order (`index`) — the UNIQUE keys make reordering non-trivial
74
+
75
+ `TableViewFields` carries **`UNIQUE (tableViewId, index)`** and **`UNIQUE (tableViewId, slug)`**.
76
+ Columns are emitted `ORDER BY TableViewFields.index ASC`, so repositioning a column means rewriting
77
+ other rows' `index` — and a plain `UPDATE ... SET index = index + N` can hit the unique key
78
+ **mid-statement**, because MySQL checks per row, not at statement end.
79
+
80
+ The safe pattern (used to slot Part Number / Manufacturer / Item Description in right after Asset
81
+ Tag on `inventory_units`, 2026-08-18) is **park → shift → pull → drop**, so that every target slot is
82
+ empty at the moment it is written:
83
+
84
+ 1. Park the moving rows in a high band (`200+`).
85
+ 2. Shift the rows that must move down into a second band (`100+`).
86
+ 3. Pull those back down into the freed low slots.
87
+ 4. Drop the parked rows into their final positions.
88
+
89
+ Notes that matter when writing it:
90
+
91
+ - **`index` is `TINYINT UNSIGNED`** — the parking band must stay under 256.
92
+ - **Gaps in `index` are harmless** (it is display order only), but finishing with a contiguous
93
+ renumber makes the script **re-runnable to the same final state**.
94
+ - Do the whole thing **uuid-keyed** and unconditional at the end, so an already-applied migration
95
+ can simply be re-run instead of shipping a separate "reorder" file — see the authoring notes in
96
+ [tableview-joins](../../_underscore/features/tableview-joins.md). If the script uses `@` session
97
+ variables, all statements must run in **one connection**.
98
+
68
99
  ## Hidden (projected-but-not-rendered) columns
69
100
 
70
101
  A `TableViewFields` row with `isVisible=0` is still selected into the row's data projection but
@@ -148,6 +179,14 @@ SalesOrderListingSortByDateOrderDesc.sql`).
148
179
  `meta.table.sort=[{slug,dir}]`; and the JOIN-`TableViewFields`-on-slug UPDATE recipe that
149
180
  avoids MySQL error 1093 (a self-subquery on `TableViews` is rejected). Applied to Quad's
150
181
  sales-orders listing (default `dateOrder DESC`). (apeterson)
182
+ - 2026-08-18 — Recorded that `TableViewFields` has **UNIQUE (tableViewId, `index`)** and
183
+ **UNIQUE (tableViewId, slug)**, so a column reorder cannot be a plain `index = index + N` (the
184
+ unique key is checked per row, mid-statement); documented the park → shift → pull → drop pattern,
185
+ the `TINYINT UNSIGNED` ceiling on the parking band, and that a closing contiguous renumber is what
186
+ makes such a migration re-runnable. Found while inserting Part Number / Manufacturer / Item
187
+ Description after Asset Tag on the `inventory_units` view for Elite + NYCHH. Cross-linked the new
188
+ [tableview-joins](../../_underscore/features/tableview-joins.md) doc for how a join row becomes
189
+ SQL. (bala)
151
190
  - 2026-08-17 — Documented that a column's `type`/`precision` are canonical from `Core.RecordFields`
152
191
  (no per-view override — a change hits every view/client) while `isCopyable`/`isSortable`/
153
192
  `isFilterable`/`isVisible` are per-view `TableViewFields` flags. Confirmed setting
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
18
18
 
19
19
  ## 2.0 framework
20
20
 
21
- - **_underscore** (_Underscore) _(framework core)_ — 58 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
+ - **_underscore** (_Underscore) _(framework core)_ — 59 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 51 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 24 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -5,7 +5,7 @@
5
5
  | [Elite — NetSuite → TOGa Supply inbound sync (TRUE-80499 onboarding)](features/netsuite-togasupply-sync.md) | 1.0 | Elite is the 18th client on the shared NetSuite → TOGa Supply importer ([engine](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)). | worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/PurchaseOrder.php, _underscore/Model/Elite/ItemReceipt.php, dbchanges2/Client_Elite/_modules.txt, test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php, test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php |
6
6
  | [Elite SalesOrder → NetSuite Push (postPost/postPut interceptors → worker2)](features/salesorder-netsuite-push.md) | 2.0 | Elite orders created in Toga are pushed into NetSuite **event-driven**, not on a cron. | _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql |
7
7
  | [Elite — Sales Order stage change posts a reply on the TOGa Desk (1.0) ticket](features/salesorder-status-togadesk-reply.md) | 2.0 | When an Elite sales order's **stage** changes, a reply is posted on the originating **TOGa Desk (1.0)** ticket so the requester sees progress where they raised | worker2/Worker/Sync/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrder.php |
8
- | [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/ELITE/BASEFIELDS.json, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/hooks/useFetchData.tsx, toga2-supply/src/components/ui/Toaster.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/formatCustomerName.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json |
8
+ | [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/ELITE/BASEFIELDS.json, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/hooks/useFetchData.tsx, toga2-supply/src/components/ui/Toaster.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/formatCustomerName.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql |
9
9
  | [Elite — stale TableView config (11 dead Core.RecordFields across 9 views; 2 now fixed)](features/supply2-tableview-config-drift.md) | 2.0 | `Client_Elite`'s `TableViewJoins` predate **two** platform bridge-table migrations and still reference **11 deleted `Core.RecordFields` ids (211, 321, 932, 358, | dbchanges2/Client_Elite/, dbchanges2/Client_Elite/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql, dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
10
10
  | [Elite — raising a Service Request from a TOGa Desk ticket (App_Api_ServiceRequest)](features/togadesk-service-request-intake.md) | 1.0 | An Elite agent raises a **Service Request** from a TOGa Desk (1.0) ticket via a modal. | library/app/api/servicerequest.php, library/app/model/togadesk/ticket.php, togadesk/desk/includes/classes/class.ticket.php, togadesk/desk/template/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/actions/tickets/serviceRequest.php, dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql, test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php |
11
11
  | [Elite](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, worker2/Worker/Sync/ServiceRequest.php, library/app/api/toga2.php, library/app/api/servicerequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, togadesk/desk/includes/classes/class.ticket.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php |
@@ -32,11 +32,13 @@ files:
32
32
  - toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx
33
33
  - toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx
34
34
  - toga2-supply/package.json
35
+ - dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
35
36
  related:
36
37
  - ../../../2.0/apps/toga2-supply/workflows/client-host-scoping.md
37
38
  - ../../../2.0/apps/toga2-supply/features/order-detail-field-config-and-customer-name.md
38
39
  - ./supply2-tableview-config-drift.md
39
40
  - ../profile.md
41
+ - ../../../2.0/apps/_underscore/features/tableview-joins.md
40
42
  ---
41
43
 
42
44
  ## Summary
@@ -73,6 +75,15 @@ One host-scoped group-by view only: **"Units By Items"**, no group-by modal.
73
75
  - Local run: `npm run elite` (`vite --host elite.togasupply`) plus a
74
76
  `127.0.0.1 elite.togasupply` hosts-file entry.
75
77
 
78
+ **The nested `inventory_units` view now shows what the hardware actually is (2026-08-18).** Unit rows
79
+ previously carried only serial / asset tag, so a user could not identify the item without opening it.
80
+ Part Number, Manufacturer and Item Description were added directly after Asset Tag, mirroring
81
+ `inventory_items`, via `dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql` (an
82
+ identical file was applied for NYCHH). Manufacturer needs a **chained** Units → Items → Manufacturers
83
+ join — mechanism, ACL requirement and the column-reorder hazard are in
84
+ [tableview-joins](../../../2.0/apps/_underscore/features/tableview-joins.md). Purely backend metadata;
85
+ no frontend change.
86
+
76
87
  **Hardening from the CodeRabbit review (2026-08-10)** — guardrail-preserving variants, not the
77
88
  reviewer's literal diffs:
78
89
 
@@ -200,6 +211,12 @@ byte-identical in behavior** — that is the bar for editing shared UI here (see
200
211
  2026-08-07 prod→beta refresh that wiped the hand-built `service-requests` view.
201
212
 
202
213
  ## Change history
214
+ - 2026-08-18 — Added **Part Number / Manufacturer / Item Description** to the `inventory_units` table
215
+ view directly after Asset Tag (`dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql`,
216
+ same file content shipped for NYCHH; prod + client-sandbox), so unit rows identify the hardware
217
+ instead of showing serial/asset tag alone. Manufacturer comes through the first chained
218
+ Units → Items → Manufacturers join in `Client_Elite`; backend metadata only, no frontend change.
219
+ (bala)
203
220
  - 2026-08-18 - Added `ELITE: "Elite"` to `formatCustomerName.tsx`'s `CUSTOMER_MAP` so the
204
221
  order-details **Customer** row shows a name instead of a dash. Recorded that this row is
205
222
  **hardcoded in the frontend for every host** - the shared `valueKey`
@@ -2,4 +2,4 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | |
5
+ | [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
@@ -15,11 +15,13 @@ project: _Underscore
15
15
  client: nychh
16
16
  type: profile
17
17
  status: active
18
- updated: 2026-08-17
18
+ updated: 2026-08-18
19
19
  owners: ["jcardinal", "apeterson", "bala"]
20
- files: []
20
+ files:
21
+ - dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
21
22
  related:
22
23
  - ../../2.0/apps/_underscore/features/tracking-number-bridges.md
24
+ - ../../2.0/apps/_underscore/features/tableview-joins.md
23
25
  ---
24
26
 
25
27
  ## Summary
@@ -34,6 +36,11 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
34
36
  run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
35
37
 
36
38
  ## Key features (this client)
39
+ - **`inventory_units` table view carries item detail as of 2026-08-18** — Part Number, Manufacturer
40
+ and Item Description sit directly after Asset Tag, reached through a **chained** Units → Items →
41
+ Manufacturers join (`dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql`, identical
42
+ to Elite's file; applied in prod and client-sandbox). Mechanism and the reorder hazard:
43
+ [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md).
37
44
  - Item-fulfillment table views (`item-fulfillments-for-sales-orders`,
38
45
  `item-fulfillments-for-sales-order-items`) rebuilt 2026-06-19 to mirror the Compass fix —
39
46
  re-rooted at `ItemFulfillmentItems` with item-level (record 318) tracking. See
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.608",
3
+ "version": "1.0.609",
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",