toga-ai 1.0.691 โ†’ 1.0.693

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.
@@ -235,10 +235,41 @@ Applied for Quad: the sales-orders listing now defaults to `dateOrder DESC` (mos
235
235
  first) instead of `number DESC` (`dbchanges2/Client_Quad/2026-08-18c -
236
236
  SalesOrderListingSortByDateOrderDesc.sql`).
237
237
 
238
+ ### ๐Ÿšจ An ACL gap on the DEFAULT-SORT column surfaces as a 500 SQL error, not an authorization message
239
+
240
+ Found 2026-08-28 on the NYCHH `transfer-orders` view. The two mechanisms are individually sane and
241
+ compose into a MySQL 1054:
242
+
243
+ 1. **`AclFieldPermissions` filters the SELECT column list, and an ungranted field is dropped
244
+ SILENTLY** โ€” no message, no `EZ-2`. (That is the same silent-drop behaviour as a missing
245
+ `AclRecordPermissions` grant deleting a *join*.)
246
+ 2. **`_Model_Client_TableView::meta()` still emits `table.sort` naming that field's slug.** The sort
247
+ block (`TableView.php:343`) sits **OUTSIDE** the permitted-field branch, so it does not know the
248
+ column was just filtered away.
249
+ 3. blox's `getDataTableData` then misses on `lookupTableViewFieldsBySlug`, **`console.warn`s โ€” and
250
+ pushes `"-" + undefined` anyway**, producing `ORDER BY TransferOrders.undefined DESC` โ†’
251
+ **MySQL 1054**, surfaced as a 500.
252
+
253
+ So the visible failure is a broken SQL query while the actual defect is one missing field grant, on
254
+ one column, in one tenant. **Any tenant missing a field grant on its view's default-sort column hits
255
+ this** โ€” it is not transfer-order-specific.
256
+
257
+ Two candidate fixes, neither applied yet (worth a ticket):
258
+
259
+ - **Backend:** emit `meta.table.sort` only for fields that survived the ACL filter.
260
+ - **Frontend (blox):** skip a sort whose slug does not resolve โ€” it *already* warns; it just pushes
261
+ the bad value regardless.
262
+
263
+ Until one lands, treat "table 500s with `ORDER BY <table>.undefined`" as a **field-ACL diagnosis**,
264
+ not a SQL-authoring one.
265
+
238
266
  ## Gotchas / known issues
239
267
 
240
268
  - **Default-sort updates must JOIN `TableViewFields`, not subquery `TableViews`** โ€” a subquery
241
269
  on the same view triggers MySQL error 1093. See *Default sort* above.
270
+ - **`ORDER BY <table>.undefined` (MySQL 1054) means a missing FIELD GRANT, not a bad migration.**
271
+ The default-sort column was dropped from the projection by `AclFieldPermissions` while
272
+ `meta.table.sort` kept naming it. See *An ACL gap on the default-sort column* above.
242
273
  - **`recordFieldId` is cross-DB** โ€” it indexes `Core.RecordFields`, not anything in the client DB.
243
274
  Resolve the id in `Core`, but insert the `TableViewFields` row in the client DB.
244
275
  - **Don't assume `Core.RecordFields`/`Core.Records` ids are stable across environments** โ€” look them
@@ -255,6 +286,14 @@ SalesOrderListingSortByDateOrderDesc.sql`).
255
286
  *and* that query โ€” and a grep for the property alone under-reports what a missing column breaks.
256
287
 
257
288
  ## Change history
289
+ - 2026-08-28 โ€” Recorded a **platform bug worth a ticket**: a missing `AclFieldPermissions` grant on a
290
+ view's **default-sort** column produces a 500 (MySQL 1054), not an authorization message. The ACL
291
+ filter drops the column from the SELECT silently, `TableView::meta()`'s sort block
292
+ (`TableView.php:343`) sits outside the permitted-field branch and still emits `table.sort` for it,
293
+ and blox's `getDataTableData` warns but pushes `"-" + undefined` anyway โ†’
294
+ `ORDER BY <table>.undefined`. Two candidate fixes noted (emit the sort only for surviving fields;
295
+ or have blox skip an unresolvable sort slug). Found on the NYCHH `transfer-orders` view.
296
+ (apeterson)
258
297
  - 2026-08-28 โ€” Pinned **where `isGroupable` actually exists**: present on local and sandbox-client
259
298
  (both hand-applied), **absent on production and beta**. So a `TableViewFields` INSERT naming the
260
299
  column passes locally and fails on prod โ€” omit it from new TableView migrations until the fan-out
@@ -3,8 +3,8 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
- | [Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql, dbchanges2/Client_Compass/2026-08-28 - GrantViewOrdersRoleSalesOrdersNavigation.sql, dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql |
7
- | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.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, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
6
+ | [Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql, dbchanges2/Client_Compass/2026-08-28 - GrantViewOrdersRoleSalesOrdersNavigation.sql, dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql |
7
+ | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.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, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
8
8
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2โ€“9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) โ€” plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
9
9
  | [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
10
10
  | [Framework branch running AHEAD of schema โ€” environment-wide 1054/EO-1 after a deploy](workflows/framework-branch-ahead-of-schema.md) | A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database went backwards โ€” **the code went forwards**. | _underscore/Model/Core/RecordField.php, _underscore/Model/Client/TableViewField.php, _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/Urgency.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql, dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql |
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)
2
+ title: Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)
3
3
  framework: "2.0"
4
4
  repo: dbchanges2
5
5
  project: Database Changes
@@ -163,6 +163,43 @@ open-orders cron that reads the dimension it fills runs at 3:00 AM.
163
163
  **Test a registration file twice against a throwaway local copy of the table**: the first run must
164
164
  insert exactly one row, the second must insert zero and raise no error. That is the whole contract.
165
165
 
166
+ ### Re-runnable ALTERs: MySQL 8 has no `ADD COLUMN IF NOT EXISTS`
167
+
168
+ A `Client/` file runs against **every tenant**, and tenants disagree about which columns they
169
+ already have โ€” so a bare `ALTER TABLE โ€ฆ ADD COLUMN` breaks on the tenants that already have it and
170
+ aborts the rest of the file. MySQL 8 offers no `IF NOT EXISTS` for `ADD COLUMN` (MariaDB does; do
171
+ not copy MariaDB SQL here). Gate each ALTER on `information_schema` and run it through
172
+ `PREPARE`/`EXECUTE`, so it compiles to a harmless `DO 0` where the column already exists:
173
+
174
+ ```sql
175
+ SET @sql = (
176
+ SELECT IF(
177
+ COUNT(*) = 0,
178
+ 'ALTER TABLE TransferOrderItems ADD COLUMN dtCreated DATETIME NULL',
179
+ 'DO 0'
180
+ )
181
+ FROM information_schema.COLUMNS
182
+ WHERE
183
+ TABLE_SCHEMA = DATABASE() AND
184
+ TABLE_NAME = 'TransferOrderItems' AND
185
+ COLUMN_NAME = 'dtCreated'
186
+ );
187
+ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
188
+ ```
189
+
190
+ - **`TABLE_SCHEMA = DATABASE()`** keeps it cluster-safe โ€” the file never names a database, so the
191
+ same statement is correct in every `Client_*` schema.
192
+ - Guard **indexes** the same way against `information_schema.STATISTICS` (and remember a composite
193
+ unique key is only recorded against its first column โ€” see
194
+ [tableview-field-metadata](../../api2/features/tableview-field-metadata.md)).
195
+ - The `PREPARE` shape is what makes the guard work at all: **DDL cannot sit inside an `IF`** in a
196
+ plain script, so the condition has to select the *statement text*, not branch around it.
197
+
198
+ Worked example: `Client/2026-08-28b - TransferOrderItemsTimestamps.sql` (adds
199
+ `TransferOrderItems.dtCreated`/`dtUpdated` + indexes, and a guarded `qtyCommitted`). **The existing
200
+ `Client/2026-08-18` `qtyCommitted` file uses a bare `ALTER` and is exactly the shape this avoids** โ€”
201
+ it is not the template to copy.
202
+
166
203
  ### `LAST_INSERT_ID()` into a session variable ties the whole file to one session
167
204
 
168
205
  The common shape โ€” `INSERT` a parent, `SET @parentId = LAST_INSERT_ID();`, then insert children
@@ -201,6 +238,14 @@ concluding a migration is production-safe.
201
238
  [dbchanges2 architecture](../architecture.md).
202
239
 
203
240
  ## Change history
241
+ - 2026-08-28 โ€” Added **conditional (re-runnable) ALTERs**: MySQL 8 has no
242
+ `ADD COLUMN IF NOT EXISTS`, so a `Client/` all-tenant file must gate each `ALTER` on
243
+ `information_schema` (with `TABLE_SCHEMA = DATABASE()` for cluster safety) and run it through
244
+ `PREPARE`/`EXECUTE`, compiling to `DO 0` where the column already exists โ€” DDL cannot sit inside an
245
+ `IF`, which is why the condition selects the statement *text*. From
246
+ `Client/2026-08-28b - TransferOrderItemsTimestamps.sql`; the existing `Client/2026-08-18`
247
+ `qtyCommitted` file's bare `ALTER` is the anti-pattern it replaces. Retitled the doc, since it now
248
+ covers re-runnable migrations generally rather than INSERTs only. (apeterson)
204
249
  - 2026-08-28 โ€” Added the **local dry-run pattern for destructive/rebuild migrations**: replay the
205
250
  file against a local copy inside `START TRANSACTION; โ€ฆ ROLLBACK;`, then prove idempotence by
206
251
  concatenating the file to itself and running both copies in one transaction. Used while writing
@@ -14,6 +14,7 @@ files:
14
14
  - dbchanges2/Client_Compass/
15
15
  - dbchanges2/Client_CompassCanada/
16
16
  - dbchanges2/Client_Quad/
17
+ - dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql
17
18
  - dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql
18
19
  - dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql
19
20
  - dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql
@@ -181,6 +182,14 @@ it `isVisible: true`. Both `Core/2026-06-29a` (Item) and `Core/2026-06-29d` (Ven
181
182
  off." A client opts a role in via a `SurfaceOverride` (`IS_VISIBLE=1`), scoped by role/persona. This is
182
183
  the seed-authoring corollary of the Core neutral-default re-baseline recorded in the resolver doc.
183
184
 
185
+ **Neutral does NOT automatically mean OFF โ€” test (b), not the habit (2026-08-28).** The rule is
186
+ "universal **and** safe-if-silently-inherited", so a control that passes both seeds **ON**.
187
+ `Core/2026-08-28b` seeds the three `transfer-orders-list-actions` buttons (Refresh / Columns /
188
+ Transfer Order) **`isVisible = 1`**: a list screen with no refresh and no create is not a useful
189
+ neutral default, none of the three is an unsafe affordance to inherit, and only NYCHH can reach the
190
+ page at all. Contrast the record-modal **Edit** button, which fails (b) outright. State which of the
191
+ two tests you applied in the file header.
192
+
184
193
  ## Sales-order button overrides โ€” element identities & the id-agnostic authoring house style
185
194
 
186
195
  When a client opts its roles into the sales-order action/filter buttons (the resolved per-client
@@ -589,7 +598,11 @@ See [sales-order-status-filter-surface](../../_underscore/features/sales-order-s
589
598
 
590
599
  Blocks assigned by `Core/2026-08-28a - TransferOrderSurfaceSeed.sql`: **`Core.Messages` 227โ€“250**,
591
600
  **`Core.Surfaces` 47โ€“51**, **`Core.SurfaceElements` 170โ€“186** (the `transfer-orders` nav element +
592
- the LIST surface + 4 record-modal SECTION surfaces + the `transfer-order-stage` vocabulary). See
601
+ the LIST surface + 4 record-modal SECTION surfaces + the `transfer-order-stage` vocabulary), then
602
+ **extended the same day** by `Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql` to
603
+ **`Messages` 227โ€“260**, **`Surfaces` 47โ€“52**, **`SurfaceElements` 170โ€“189** (the
604
+ `transfer-orders-list-actions` BUTTON_BAR + the shared columns-modal messages). Every id was
605
+ verified free on **local AND production** before authoring. See
593
606
  [Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
594
607
 
595
608
  **โš  The reserved blocks are not an inventory of every surface id โ€” pre-2026-08-21 surfaces sit
@@ -905,6 +918,41 @@ next reserved block, and a second confirmation of the "verify the block is free
905
918
  environment, not just yours" rule from the 2026-08-24 incident โ€” sandbox-client's lower ceiling would
906
919
  have made 45โ€“46 look free.
907
920
 
921
+ ### SHARED (`common.*`) message keys โ€” guard on `messageKey`, not on `id`
922
+
923
+ Copy that belongs to a **generic component** rather than to one record type is seeded under a
924
+ **shared `common.*` key**, alongside the existing `common.yes` / `common.no` / `common.active` /
925
+ `common.inactive`. `Core/2026-08-28b` moved the seven column-visibility modal strings to
926
+ **`common.columns.modal.*`** for exactly this reason: a tenant then translates them **once for every
927
+ list screen** instead of once per record type. (The older per-record
928
+ `items.columns.modal.*` / `salesOrder.columns.modal.*` keys are left alone โ€” folding those three
929
+ shipped screens onto the common keys is a clean follow-up.)
930
+
931
+ **The guard changes with the key's ownership.** A record-scoped seed can be guarded on its literal
932
+ reserved `id`; a **shared** key cannot, because another seed may legitimately create it first, at a
933
+ different id. So guard a `common.*` insert with `NOT EXISTS (โ€ฆ WHERE messageKey = 'โ€ฆ')` โ€” the
934
+ *semantic* key โ€” and accept that the row may end up outside your reserved block.
935
+
936
+ ### A partial-migration cleanup: fix the ENVIRONMENT, not the migration file
937
+
938
+ When a developer has already run an **earlier revision** of a seed, re-running the corrected file
939
+ hits `Duplicate entry '251' for key 'messages.PRIMARY'`. The resolution that keeps the repo honest:
940
+
941
+ - **Leave the migration file UNCHANGED.** It is correct for a fresh environment, and prod/sandbox
942
+ never ran it. Editing it to dodge one developer's local state corrupts the record of intent (see
943
+ the 2.0 standard: *migrations record intent, not deployed state*).
944
+ - **Hand over a scratchpad-only undo** โ€” never a committed file, because the executor runs every
945
+ folder against production.
946
+ - **Delete BY UUID, never by id range.** Per the 2026-08-24 incident, an id-range cleanup nearly
947
+ took the legitimate deny-action toast rows with it.
948
+ - **Delete in FK order: `SurfaceElements` โ†’ `Surfaces` โ†’ `Messages`.** Both
949
+ `SurfaceElements.labelMessageId` and `SurfaceElements.surfaceId` are `RESTRICT`.
950
+
951
+ > โš  **`SurfaceOverrides.surfaceElementId` is a SOFT FK โ€” no constraint.** So an override written
952
+ > against a deleted element **survives the delete** and silently re-attaches to whatever row takes
953
+ > that id next. This is the same failure shape as the NYCHH inventory-topology landmine above:
954
+ > sweep the overrides yourself when you delete elements; the database will not.
955
+
908
956
  ## A new-vertical Core seed: two authoring rules confirmed on the transfer-order build (2026-08-28)
909
957
 
910
958
  - **A TABLE surface names its view by SLUG, never by `tableViewId`.** `Core/2026-08-28a` seeds
@@ -922,8 +970,28 @@ have made 45โ€“46 look free.
922
970
  the same distinction). โš  Any `Client/` file writing to `ThemeTokens` inherits a dependency on
923
971
  `Client/2026-06-25a - SurfaceClientTables.sql` having run in that schema โ€” audit the tenant list
924
972
  before a prod run.
973
+ - **A BUTTON_BAR element needs an `Actions` row only when it needs a REGISTRY HANDLER.** The three
974
+ `transfer-orders-list-actions` elements seed with **no `Actions` rows at all**: with nothing
975
+ registered in the FE `actionRegistry`, `SurfaceActionBar` falls through to
976
+ `onDispatch(config.valueKey)`, which is precisely the host-owned behaviour Refresh / Columns /
977
+ Create want. Seed an `Action` when the *behaviour* lives in the registry; seed a bare
978
+ `config.valueKey` when it lives in the page.
925
979
 
926
980
  ## Change history
981
+ - 2026-08-28 โ€” **Extended the transfer-order reserved blocks** to `Messages` **227โ€“260**, `Surfaces`
982
+ **47โ€“52**, `SurfaceElements` **170โ€“189** (`Core/2026-08-28b`, the `transfer-orders-list-actions`
983
+ BUTTON_BAR; all ids verified free on local **and** production first). Three authoring rules added:
984
+ a **neutral default can be ON** โ€” apply the safe-if-inherited test rather than the seed-OFF habit
985
+ (the three list-action buttons seed `isVisible=1`; a record-modal Edit button still seeds off);
986
+ a BUTTON_BAR element needs an **`Actions` row only when it needs a registry handler**, otherwise a
987
+ bare `config.valueKey` falls through to `onDispatch`; and **shared `common.*` Messages must be
988
+ guarded on `messageKey`, not id**, since another seed may create the key first at a different id
989
+ (the columns-modal copy moved to `common.columns.modal.*` so a tenant translates it once for every
990
+ list screen). Also recorded the **partial-migration cleanup** procedure โ€” leave the migration file
991
+ unchanged, hand over a scratchpad-only undo, delete **by uuid** in FK order
992
+ (`SurfaceElements` โ†’ `Surfaces` โ†’ `Messages`, both element FKs are RESTRICT) โ€” and that
993
+ **`SurfaceOverrides.surfaceElementId` is a soft FK with no constraint**, so an override outlives a
994
+ deleted element and silently re-attaches to whatever takes that id next. (apeterson)
927
995
  - 2026-08-28 โ€” Registered the **transfer-order reserved blocks** (`Messages` 227โ€“250, `Surfaces`
928
996
  47โ€“51, `SurfaceElements` 170โ€“186, `Core/2026-08-28a`) and recorded the read-only ceiling snapshot
929
997
  taken before reserving them: local **and** production identical at 46/169/226 while sandbox-client
@@ -84,6 +84,15 @@ Order matters โ€” each step assumes the prior one ran.
84
84
  - **Drift before consolidation** โ€” loose `Client/` files must already be applied to all existing
85
85
  clients before they're consolidated/archived (otherwise the new blank diverges from live
86
86
  clients). See the dbchanges2 architecture doc.
87
+ - **๐Ÿšจ The blank can be WRONG, not just behind โ€” an open example (2026-08-28).**
88
+ `Client/2026-06-03- BLANK_CLIENT_DATABASE.sql` creates `TransferOrderItems` (`CREATE` at ~line
89
+ 9462) **without `dtCreated`/`dtUpdated`**, but `_Model_Client_TransferOrderItem` declares them as
90
+ `FIELD_DATETIME_CREATED`/`FIELD_DATETIME_UPDATED`, so `_Model` puts them in **every SELECT it
91
+ builds**. **Every tenant provisioned from this blank therefore has a table the model cannot
92
+ read** โ€” nothing in a request has to ask for the columns. `Client/2026-08-28b -
93
+ TransferOrderItemsTimestamps.sql` repairs existing tenants; **the blank itself is still unfixed**,
94
+ so the next onboarding reproduces it. When a model-vs-blank mismatch like this is found, fix
95
+ **both**: a catch-up `Client/` file for live tenants *and* the blank.
87
96
 
88
97
  ## Gotchas
89
98
 
@@ -100,6 +109,12 @@ Order matters โ€” each step assumes the prior one ran.
100
109
 
101
110
  ## Change history
102
111
 
112
+ - 2026-08-28 โ€” Recorded an **open blank-template defect**: `TransferOrderItems` is created without
113
+ the `dtCreated`/`dtUpdated` columns the model declares as `FIELD_DATETIME_CREATED/UPDATED` and
114
+ therefore selects on every read, so every tenant built from the blank ships a table the model
115
+ cannot read. Live tenants are repaired by `Client/2026-08-28b`; the blank is still unfixed. Added
116
+ the general rule: a model-vs-blank mismatch needs **both** a catch-up `Client/` file and a blank
117
+ fix. (apeterson)
103
118
  - 2026-07-20 โ€” Noted that a local browser wizard now automates steps 2โ€“9 and the dbchanges2 blank
104
119
  consolidation (TRUE-79864). (mhammontree)
105
120
  - 2026-06-23 โ€” Initial capture from the Fordham onboarding (TRUE-79702). (mhammontree)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-11
9
+ updated: 2026-08-28
10
10
  owners: ["bala", "mhammontree", "apeterson"]
11
11
  files:
12
12
  - dbchanges2/Client_Growrk/2026-05-28.sql
@@ -101,6 +101,33 @@ part, and it is what makes a missing column fatal. Standard, non-`c_` fields use
101
101
  `Core.RecordFields` + `AclFieldPermissions` instead; see
102
102
  [ACL permission chain](../../_underscore/features/acl-permission-chain.md).)
103
103
 
104
+ ## The drift can run the OTHER way: prod has state the repo never created
105
+
106
+ Everything above assumes the client DB is **behind** the repo. The mirror case is just as common and
107
+ harder to see: **production carries hand-applied state that no migration file creates**, so the
108
+ *fresh* environment is the broken one.
109
+
110
+ Worked example (NYCHH transfer orders, 2026-08-28) โ€” four layers, all applied by hand when the
111
+ NetSuite TransferOrder POST was enabled, none of them in `dbchanges2`: `AclRecordPermissions` for
112
+ records 312/313, their `AclLogicGroups`/`AclLogicGroupExpressions`/`AclRecordExpressions` chain,
113
+ `AclFieldPermissions` for all 17 of their `RecordFields`, and the
114
+ `TransferOrderItems.dtCreated`/`dtUpdated` columns. Local had none of it and **failed one layer at a
115
+ time, each with a completely different-looking error**.
116
+
117
+ Three things this changes about the audit:
118
+
119
+ - **ACL rows are drift too, not just columns.** An `information_schema` diff will never find a
120
+ missing `AclFieldPermissions` row. Diff the ACL tables for the records in scope as well.
121
+ - **Diff the WHOLE feature area up front rather than chasing one 500 at a time.** One systematic
122
+ local-vs-prod diff of every table (and ACL row) the feature touches is cheaper than four
123
+ sequential debugging sessions, and it is the only way to find layers that have not failed *yet*.
124
+ - **The blast radius is never just the screen you are on.** The same missing rows meant the NetSuite
125
+ transfer-order **importer** could not be stood up in a fresh environment either.
126
+
127
+ The repair is a **faithful reproduction** file โ€” same roles, same permission bits as prod, no
128
+ widening โ€” anti-joined on natural keys so it is a no-op where the state already exists. Detail:
129
+ [NYCHH transfer-order inventory & quantities](../../../../clients/nychh/features/transfer-order-inventory-quantities.md).
130
+
104
131
  ## Steps
105
132
 
106
133
  1. **Get the real exception first.** Resolve `error.id` from the response envelope against
@@ -162,6 +189,15 @@ part, and it is what makes a missing column fatal. Standard, non-`c_` fields use
162
189
 
163
190
  ## Change history
164
191
 
192
+ - 2026-08-28 โ€” Added the **inverse drift direction**: production carrying hand-applied state the repo
193
+ never created, so the *fresh* environment is the broken one. From the NYCHH transfer-order case โ€”
194
+ four untracked layers (record ACL for 312/313, their logic-group/expression chain, field ACL for
195
+ all 17 RecordFields, and two `TransferOrderItems` timestamp columns) that failed locally one at a
196
+ time with different-looking errors. Durable additions: **ACL rows are drift too** and an
197
+ `information_schema` diff will never see them; **diff the whole feature area up front** instead of
198
+ chasing sequential 500s; the blast radius extends past the UI (the NetSuite importer was equally
199
+ unbuildable); and the repair is a **faithful reproduction** โ€” same roles, same bits, no widening,
200
+ anti-joined so it no-ops where the state exists. (apeterson)
165
201
  - 2026-08-27 โ€” Added a pointer to the **third drift direction**: when the 1054 starts right after a
166
202
  deploy, is not client-specific, and the column exists in no environment, the **framework branch is
167
203
  ahead of schema** and the repair is a branch merge, not a catch-up file โ€” see
@@ -7,7 +7,7 @@
7
7
  | [AdvancedSelect (virtualized single/multi select)](features/advanced-select.md) | `AdvancedSelect<T>` is a **fully controlled, virtualized** single/multi select built from scratch (not react-select) on **`@tanstack/react-virtual`**, for large | toga-blox/src/components/AdvancedSelect/AdvancedSelect.tsx, toga-blox/src/components/AdvancedSelect/AdvancedSelect.types.ts, toga-blox/src/components/AdvancedSelect/AdvancedSelect.module.css |
8
8
  | [API client (axios wrapper, auth, table-data fetchers)](features/api-client.md) | `src/api/` is a thin **axios** wrapper that standardizes the **2.0 API envelope**, manages auth (Bearer + refresh), serializes complex query options, and provid | toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/api/index.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/api/apiFunctions.ts, toga-blox/src/api/auth.ts, toga-blox/src/api/genericApi.ts, toga-blox/src/api/types.ts, toga-blox/src/api/tableData |
9
9
  | [BaseInput (react-hook-form field factory)](features/base-input.md) | `BaseInput` is a **form-field factory** driven by react-hook-form. | toga-blox/src/components/BaseInput/BaseInput.tsx, toga-blox/src/components/BaseInput/BaseInput.types.ts, toga-blox/src/components/BaseInput/BaseInput.module.css, toga-blox/src/components/BaseInput/components |
10
- | [blox authoring defect backlog (fix IN blox, not around it)](features/blox-authoring-defects.md) | The standing list of **defects to fix inside `@agilant/toga-blox`**. | toga-blox/src/api/auth.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/components/BaseInput, toga-blox/tsconfig.json, toga-blox/package.json |
10
+ | [blox authoring defect backlog (fix IN blox, not around it)](features/blox-authoring-defects.md) | The standing list of **defects to fix inside `@agilant/toga-blox`**. | toga-blox/src/components/BaseButton, toga-blox/src/api/auth.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/components/BaseInput, toga-blox/tsconfig.json, toga-blox/package.json |
11
11
  | [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/components/Table/themeConfig/toga.module.css, toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
12
12
  | [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail โ€” typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
13
13
  | [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
@@ -15,4 +15,4 @@
15
15
  | [Toaster โ€” host-token styling contract](features/toaster.md) | The blox `Toaster` is a CSS-Module component whose every visual property reads a `--toaster-*` custom property supplied by the **host app**. | toga-blox/src/components/Toaster/Toaster.module.css, toga-blox/src/components/Toaster/Toaster.tsx, toga-blox/src/components/Toaster/ToasterContext.tsx, toga2-commerce/src/styles/index.css, toga2-commerce/src/components/Toasters/ToasterList.tsx |
16
16
  | [Dynamic npm Publish Pipeline (branch โ†’ channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/package.json, toga-blox/src/utils/getFontAwesomeIcon.tsx |
17
17
  | [Landing a Large Long-Lived Feature Branch (merge, never rebase)](workflows/landing-a-large-feature-branch.md) | The procedure for landing a long-lived, very large feature branch (months old, hundreds of commits) into `_production` in `toga-blox-npm`. | toga-blox/package.json, toga-blox/.github/workflows/publish.yml |
18
- | [Local-linking an unpublished blox into a consumer app (incl. a React 19 / Next host)](workflows/local-link-into-a-consumer-app.md) | How to develop a **blox component that is not published yet** against a real consuming app, on Windows, including the hard case: a **React 19 / Next.js (Turbopa | toga-blox/package.json, toga-blox/postcss.config.cjs |
18
+ | [Local-linking an unpublished blox into a consumer app (incl. a React 19 / Next host)](workflows/local-link-into-a-consumer-app.md) | How to develop a **blox component that is not published yet** against a real consuming app, on Windows, including the hard case: a **React 19 / Next.js (Turbopa | toga-blox/package.json, toga-blox/postcss.config.cjs, toga25-supply/vite.config.ts |
@@ -6,9 +6,10 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-27
9
+ updated: 2026-08-28
10
10
  owners: [apeterson]
11
11
  files:
12
+ - toga-blox/src/components/BaseButton
12
13
  - toga-blox/src/api/auth.ts
13
14
  - toga-blox/src/api/axiosInstance.ts
14
15
  - toga-blox/src/reactQuery/queryHelpers.ts
@@ -89,6 +90,24 @@ mechanism), and a **fix direction** (a suggestion, not a decision). Every one of
89
90
  today's `"/"` for compatibility), or have `performLogout` simply not navigate and let the caller's
90
91
  `onLogout` own it.
91
92
 
93
+ ## Styling defects
94
+
95
+ ### 4. `BaseButton` โ€” `primary` and `primaryAction` are NOT interchangeable variants
96
+
97
+ **Mechanism.** The two class names look like a naming pair but carry different property sets:
98
+ `.primaryBtnAction` sets a **font-size** (default 14px) **and** `height: 26px`; `.primaryBtn` sets
99
+ **neither** โ€” it inherits font-size from context and has no height at all.
100
+
101
+ **Consumer symptom.** Switching a button between the two variants โ€” the obvious move when a surface
102
+ element is seeded `PRIMARY` โ€” silently changes **type size and button height**, with nothing in the
103
+ prop name suggesting geometry is involved. It reads as a CSS bug in the consuming app.
104
+
105
+ **Fix direction.** Make the two variants differ in **colour only**, moving the shared size/height
106
+ declarations to the base button class. Until then a consumer must compensate at the instance โ€”
107
+ `toga25-supply`'s `SurfaceActionBar` variant map pins `fontSize: 14px` for `primary` (see
108
+ [surface-frontend](../../toga25-supply/features/surface-frontend.md)). **Do not patch the shared blox
109
+ stylesheet from a consumer**: every one of the ~40 call sites inherits it.
110
+
92
111
  ## Already-known authoring items (carried over from ยง22)
93
112
 
94
113
  These are long-standing and were previously recorded only as consumer-side advice:
@@ -122,6 +141,11 @@ These are long-standing and were previously recorded only as consumer-side advic
122
141
  time**, with no PR. Sequence auth fixes deliberately.
123
142
 
124
143
  ## Change history
144
+ - 2026-08-28 โ€” Added styling defect **4: `BaseButton` `primary` vs `primaryAction` are not
145
+ interchangeable** โ€” `.primaryBtnAction` sets font-size **and** `height: 26px`, `.primaryBtn` sets
146
+ neither, so swapping variants silently changes type size and button height. Found wiring the
147
+ `transfer-orders-list-actions` PRIMARY button in toga25-supply; compensated at the instance (the
148
+ `SurfaceActionBar` variant map pins 14px) rather than by editing the shared stylesheet. (apeterson)
125
149
  - 2026-08-27 โ€” Created the doc ยง22 had been pointing at since 2026-08-18. Seeded with the three auth
126
150
  defects found while debugging toga25-supply SSO (`handleClientAuthentication`'s misleading name +
127
151
  untyped return, `fetchPublicToken` persisting the public token to `localStorage`,
@@ -7,10 +7,11 @@ client: shared
7
7
  type: workflow
8
8
  status: active
9
9
  updated: 2026-08-28
10
- owners: [tcox]
10
+ owners: [tcox, apeterson]
11
11
  files:
12
12
  - toga-blox/package.json
13
13
  - toga-blox/postcss.config.cjs
14
+ - toga25-supply/vite.config.ts
14
15
  related:
15
16
  - ./dynamic-publish-pipeline.md
16
17
  - ../features/talos-assistant.md
@@ -59,6 +60,35 @@ Result: exactly one React in the graph. **framer-motion 11 and Pro FontAwesome b
59
60
  React 19** โ€” the `^18` peer range is a declaration, not a runtime wall (a published consumer
60
61
  still needs the peer range widened).
61
62
 
63
+ ### 2b. Vite consumers: dedupe EVERY peer that carries a React context โ€” not just React
64
+
65
+ **The rule: any peer dep that provides a React context or hooks must be in `resolve.dedupe` when
66
+ blox is consumed through a symlink.** It is not enough that blox declares the peer correctly โ€” Vite
67
+ resolves a symlinked package's imports **relative to the link target**, so blox's hooks pick up
68
+ whatever sits in `toga-blox-npm/node_modules`, and you end up with two copies of the library.
69
+
70
+ Worked example (toga25-supply, 2026-08-28) โ€” **"No QueryClient set, use QueryClientProvider" with
71
+ `QueryClientProvider` plainly visible in the component stack.** Two copies of
72
+ `@tanstack/react-query`: the app's provider wrote one React context while blox's hooks
73
+ (`useFetchTablePageMeta`, `useTableData`) read another. `vite.config.ts` deduped
74
+ `react` / `react-dom` / `@tanstack/react-table` but **not react-query**.
75
+
76
+ ```ts
77
+ dedupe: [
78
+ "react", "react-dom", "@tanstack/react-table",
79
+ "@tanstack/react-query", // โ† the fix
80
+ "@tanstack/react-query-persist-client",
81
+ "@tanstack/react-query-devtools",
82
+ ]
83
+ ```
84
+
85
+ - **Editing `dedupe` is not enough on its own โ€” `rm -rf node_modules/.vite` and hard-reload.** The
86
+ duplicate is baked into Vite's **pre-bundle**, so the old graph is served until the cache is
87
+ cleared.
88
+ - **This only reproduces on machines using the local checkout**, so across a team it reads as an
89
+ *intermittent* bug that "works for everyone else." A `QueryClient`/context error with the provider
90
+ visibly mounted is almost always this.
91
+
62
92
  ### 3. Disable blox's PostCSS config while linked
63
93
 
64
94
  Rename `toga-blox-npm/postcss.config.cjs` to `postcss.config.cjs.off` for the duration.
@@ -89,6 +119,13 @@ The consumer reads blox's `dist`, so rebuild blox after every change.
89
119
  drags every component's CSS into the consumer bundle.
90
120
 
91
121
  ## Change history
122
+ - 2026-08-28 โ€” Added the Vite half of the one-copy rule: **every peer carrying a React context must
123
+ be in `resolve.dedupe`**, not just React. `toga25-supply` threw "No QueryClient set" **with
124
+ `QueryClientProvider` visibly in the stack** because `@tanstack/react-query` was missing from
125
+ `dedupe` while `react-table` was there โ€” the app's provider and blox's hooks held different
126
+ contexts. Fix = add the react-query family to `dedupe` **and** `rm -rf node_modules/.vite` (the
127
+ duplicate is baked into the pre-bundle). Reproduces only on machines using the local checkout, so
128
+ it presents as intermittent across a team. (apeterson)
92
129
  - 2026-08-27 โ€” Initial doc. Recorded the procedure proven while wiring blox `Talos` into `ai-bdr`
93
130
  from an unpublished branch: junction the checkout in, junction the host's React 19 over blox's
94
131
  bundled React 18 (keeping `.r18` originals) so there is one React, and rename blox's Tailwind
@@ -6,7 +6,7 @@
6
6
  | [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | A declarative, fully config-driven rule engine that resolves the boolean-ish flags (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. | toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.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/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
7
7
  | [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | The mechanism that resolves a client's **API-fetch projection** โ€” which `fields` / `ojoin` / `join` / `where` to request from the TOGa API for a given page โ€” fr | toga25-supply/src/fieldsConfig/resolveApiConfig.ts, toga25-supply/src/fieldsConfig/useClientApiFields.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx |
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
- | [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
+ | [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/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql |
10
10
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
11
11
  | [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/TransferOrders/viewModel/useTransferOrdersPageViewModel.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 |
12
12
  | [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/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, 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/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts |
@@ -14,7 +14,7 @@
14
14
  | [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
15
15
  | [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/App.tsx, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, 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/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.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/hooks/useSalesOrderVip.ts, 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, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
16
16
  | [Talos Integration (AppLayout host + adapter wiring)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
17
- | [Transfer Orders page (TableView โ†’ Core surfaces โ†’ React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen โ€” list + read-only record modal โ€” built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
18
- | [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 |
17
+ | [Transfer Orders page (TableView โ†’ Core surfaces โ†’ React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen โ€” list + read-only record modal โ€” built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferableItemRows.ts, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql |
18
+ | [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, toga25-supply/.env.development |
19
19
  | [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 |
20
20
  | [Porting a page (or query) from toga2-supply to toga25-supply](workflows/porting-a-page-from-toga2-supply.md) | `toga25-supply` re-implements pages that already exist in `toga2-supply`. | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/components/ui/Tables/hooks/useExportableData.tsx |