toga-ai 1.0.692 → 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.
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +39 -0
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +2 -2
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +46 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +69 -1
- package/knowledge/2.0/apps/dbchanges2/workflows/client-onboarding.md +15 -0
- package/knowledge/2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md +37 -1
- package/knowledge/2.0/apps/toga-blox/INDEX.md +2 -2
- package/knowledge/2.0/apps/toga-blox/features/blox-authoring-defects.md +25 -1
- package/knowledge/2.0/apps/toga-blox/workflows/local-link-into-a-consumer-app.md +38 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +3 -3
- package/knowledge/2.0/apps/toga25-supply/features/column-visibility.md +48 -3
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +25 -0
- package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +141 -6
- package/knowledge/2.0/apps/toga25-supply/workflows/amplify-deployment.md +26 -1
- package/knowledge/clients/nychh/INDEX.md +2 -2
- package/knowledge/clients/nychh/features/transfer-order-inventory-quantities.md +66 -2
- package/knowledge/clients/nychh/profile.md +12 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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)
|
|
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-
|
|
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-
|
|
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 |
|
|
@@ -6,17 +6,23 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-28
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/components/ColumnVisibilityModal/
|
|
13
|
+
- toga25-supply/src/surface/useColumnVisibilityModalConfig.ts
|
|
14
|
+
- toga25-supply/src/surface/index.ts
|
|
13
15
|
- toga25-supply/src/pages/SalesOrders/SalesOrders.tsx
|
|
14
16
|
- toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
|
|
15
17
|
- toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx
|
|
18
|
+
- dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql
|
|
16
19
|
related:
|
|
17
20
|
- ../architecture.md
|
|
18
21
|
- client-configurable-fields.md
|
|
19
22
|
- meta-driven-table-data.md
|
|
23
|
+
- surface-frontend.md
|
|
24
|
+
- transfer-orders-page.md
|
|
25
|
+
- ../../dbchanges2/features/surface-layer-schema.md
|
|
20
26
|
---
|
|
21
27
|
|
|
22
28
|
## What it is
|
|
@@ -75,11 +81,40 @@ const isFieldVisible = (f) => visibleSlugSet ? visibleSlugSet.has(f.slug) : f.is
|
|
|
75
81
|
6. **Verify:** `npx tsc --noEmit`, validate each JSON parses, click live (toggle, Apply, copy URL,
|
|
76
82
|
reload → state persists).
|
|
77
83
|
|
|
84
|
+
## Where the copy comes from — SHARED Core Messages, and NO FE fallbacks (2026-08-28)
|
|
85
|
+
|
|
86
|
+
Step 5's per-page `FIELDS/<CLIENT>/…` block is the **legacy** source. New screens take the modal's
|
|
87
|
+
copy from the **Surface layer**, under **shared `common.columns.modal.*` `Core.Messages` keys**
|
|
88
|
+
(`Core/2026-08-28b`).
|
|
89
|
+
|
|
90
|
+
- **Why `common.*` and not per-record.** Every page previously hardcoded the same seven strings,
|
|
91
|
+
which put the English **in the bundle, where `MessageTranslations` can never reach it**. Shared
|
|
92
|
+
keys — the pattern already used by `common.yes` / `common.no` / `common.active` /
|
|
93
|
+
`common.inactive` — mean a tenant translates the modal **once for every list screen** instead of
|
|
94
|
+
once per record type.
|
|
95
|
+
- **The English fallbacks were removed from the FE deliberately.** A hardcoded default silently
|
|
96
|
+
masks a missing seed and reintroduces the second source of truth this whole migration exists to
|
|
97
|
+
delete. **Trade-off accepted and recorded: an unseeded environment renders the raw message key.**
|
|
98
|
+
(This is a deliberate divergence from the generic surface guidance that every consumer keeps a
|
|
99
|
+
design-literal fallback — for *this* component the missing seed must be visible.)
|
|
100
|
+
- **`useColumnVisibilityModalConfig`** (`src/surface/`, exported from the `@/surface` barrel) is the
|
|
101
|
+
reusable reader: given any `BUTTON_BAR` bundle carrying a `config.role = 'columnsButton'` element,
|
|
102
|
+
it reads that element's `config.modal` and resolves the copy + icon. It lives with the other
|
|
103
|
+
surface infra and carries the same `TODO(blox)` promotion marker.
|
|
104
|
+
- **Seed guard:** because these are shared keys, the `INSERT` is `NOT EXISTS`-guarded on
|
|
105
|
+
**`messageKey`**, not on a literal id — another seed may legitimately create them first at a
|
|
106
|
+
different id. See
|
|
107
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
108
|
+
- **Follow-up (clean, not done):** the three shipped screens still on
|
|
109
|
+
`items.columns.modal.*` / `salesOrder.columns.modal.*` can fold onto the common keys + this hook.
|
|
110
|
+
Those older keys were deliberately left alone.
|
|
111
|
+
|
|
78
112
|
## Gotchas
|
|
79
113
|
|
|
80
114
|
- **No hardcoded copy in the component** — every user-facing string (incl. Close `aria-label`) is a
|
|
81
|
-
required `labels` prop
|
|
82
|
-
component
|
|
115
|
+
required `labels` prop. On the **legacy** path the fallbacks live in the view model's resolution
|
|
116
|
+
(never in the component or default props); on the **surface** path there are **no fallbacks at
|
|
117
|
+
all** (see above). Only structural glyphs (checkbox tick, close `xmark`) stay.
|
|
83
118
|
- **Remount key is REQUIRED.** toga-blox's `PrimaryTableRow` memoizes
|
|
84
119
|
`useMemo(() => row.getVisibleCells(), [row])`; TanStack caches `row` by data, so a columns-only
|
|
85
120
|
change doesn't change `row` → stale cells. Symptom: **toggling off removes the header but leaves the
|
|
@@ -95,4 +130,14 @@ const isFieldVisible = (f) => visibleSlugSet ? visibleSlugSet.has(f.slug) : f.is
|
|
|
95
130
|
meta later won't appear for an old link until Reset (intended).
|
|
96
131
|
|
|
97
132
|
## Change history
|
|
133
|
+
- 2026-08-28 — Modal copy moved from per-page hardcoded strings / client `FIELDS` to **shared
|
|
134
|
+
`common.columns.modal.*` `Core.Messages`** (`Core/2026-08-28b`), so a tenant translates it once for
|
|
135
|
+
every list screen instead of per record type — the old strings sat in the bundle where
|
|
136
|
+
`MessageTranslations` could never reach them. **Removed the English FE fallbacks deliberately**: a
|
|
137
|
+
hardcoded default masks a missing seed and recreates the second source of truth; the accepted
|
|
138
|
+
trade-off is that an unseeded environment renders the raw key. Added
|
|
139
|
+
`useColumnVisibilityModalConfig` (`src/surface/`, barrel-exported) as the reusable reader for any
|
|
140
|
+
`BUTTON_BAR` slug carrying a `columnsButton` element. Seed is guarded on `messageKey` rather than a
|
|
141
|
+
literal id because the keys are shared. Folding the three shipped screens off
|
|
142
|
+
`items.columns.modal.*` / `salesOrder.columns.modal.*` is an open follow-up. (apeterson)
|
|
98
143
|
- 2026-06-23 — Documented from the `add-column-visibility` skill during initial knowledge seed. (apeterson)
|
|
@@ -574,6 +574,24 @@ developer asked for the **whole component**, not just badges. The old
|
|
|
574
574
|
[surface-resolver](../../_underscore/features/surface-resolver.md) — terms are Core-global, no
|
|
575
575
|
per-term gating).
|
|
576
576
|
|
|
577
|
+
## `SurfaceActionBar` labeled mode honours `element.variant` (2026-08-28)
|
|
578
|
+
|
|
579
|
+
`renderMode="labeled"` previously hardcoded `variant="secondaryAction"` for **every** button, so a
|
|
580
|
+
surface element seeded `PRIMARY` could never render as primary — the seed was silently ignored.
|
|
581
|
+
There is now an **`element.variant` → `BaseButton` variant map**, falling back to `secondaryAction`,
|
|
582
|
+
so Sales Orders / Items / Vendor Items render exactly as before while the new
|
|
583
|
+
`transfer-orders-list-actions` PRIMARY create button renders correctly.
|
|
584
|
+
|
|
585
|
+
The map also **pins `fontSize: 14px` for the `primary` variant**, because blox's two "primary"
|
|
586
|
+
classes are not interchangeable — see
|
|
587
|
+
[blox authoring defects](../../toga-blox/features/blox-authoring-defects.md): `.primaryBtnAction`
|
|
588
|
+
sets font-size **and** `height: 26px`; `.primaryBtn` sets **neither**. Swapping the variant silently
|
|
589
|
+
changes type size and button height. The compensation belongs **at the instance / in this map**, not
|
|
590
|
+
in an edit to the shared blox stylesheet.
|
|
591
|
+
|
|
592
|
+
⚠ This is **shared infra** — `SurfaceActionBar` renders every migrated screen's chrome, so a change
|
|
593
|
+
here is a cross-screen change. Re-check Sales Orders, Items and Vendor Items after touching it.
|
|
594
|
+
|
|
577
595
|
## Gotchas
|
|
578
596
|
|
|
579
597
|
- **The VIP `/contacts` API call is gated by an approval-details element's `labelSuffix.valueKey`,
|
|
@@ -778,6 +796,13 @@ button except **View Log** renders and does nothing on click. Whether the SR mod
|
|
|
778
796
|
actions surface is **still an open decision**, not an oversight.
|
|
779
797
|
|
|
780
798
|
## Change history
|
|
799
|
+
- 2026-08-28 — **`SurfaceActionBar` labeled mode now honours `element.variant`** (shared infra):
|
|
800
|
+
`renderMode="labeled"` had hardcoded `variant="secondaryAction"` for every button, so a `PRIMARY`
|
|
801
|
+
seed could never render as primary. Added an `element.variant` → `BaseButton` variant map with a
|
|
802
|
+
`secondaryAction` fallback (Sales Orders / Items / Vendor Items unchanged), pinning
|
|
803
|
+
`fontSize: 14px` for `primary` because blox's `.primaryBtn` sets neither font-size nor height while
|
|
804
|
+
`.primaryBtnAction` sets both — compensated at the instance rather than by editing the shared blox
|
|
805
|
+
stylesheet. Driven by the new `transfer-orders-list-actions` bar. (apeterson)
|
|
781
806
|
- 2026-08-28 — Recorded two durable FE surface rules from the denial-reason investigation (research
|
|
782
807
|
only; no code written). (1) **A hidden marker's RULE can select a MODE**, not just a section on/off
|
|
783
808
|
— el **108** `sectionLayoutRule` (`renderType TEXT`, `isVisible=0`) is the live precedent; reach for
|
|
@@ -17,20 +17,30 @@ files:
|
|
|
17
17
|
- toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts
|
|
18
18
|
- toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts
|
|
19
19
|
- toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx
|
|
20
|
+
- toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx
|
|
21
|
+
- toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts
|
|
22
|
+
- toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferableItemRows.ts
|
|
23
|
+
- toga25-supply/src/surface/useColumnVisibilityModalConfig.ts
|
|
20
24
|
- toga25-supply/src/routes.tsx
|
|
21
25
|
- dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql
|
|
26
|
+
- dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql
|
|
22
27
|
- dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql
|
|
28
|
+
- dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql
|
|
23
29
|
- dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
|
|
24
30
|
- dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql
|
|
31
|
+
- dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql
|
|
25
32
|
related:
|
|
26
33
|
- ./surface-frontend.md
|
|
27
34
|
- ./meta-driven-table-data.md
|
|
28
35
|
- ./record-modals-and-nested-tables.md
|
|
36
|
+
- ./column-visibility.md
|
|
29
37
|
- ../../dbchanges2/features/surface-layer-schema.md
|
|
30
38
|
- ../../_underscore/features/surface-resolver.md
|
|
31
39
|
- ../../_underscore/features/tableview-joins.md
|
|
32
40
|
- ../../api2/features/v2-rest-query-contract.md
|
|
41
|
+
- ../../api2/features/tableview-field-metadata.md
|
|
33
42
|
- ../../../../clients/nychh/profile.md
|
|
43
|
+
- ../../../../clients/nychh/features/transfer-order-inventory-quantities.md
|
|
34
44
|
---
|
|
35
45
|
|
|
36
46
|
## Summary
|
|
@@ -43,12 +53,16 @@ reserved-id seed → per-tenant TableView → per-tenant nav opt-in → surface-
|
|
|
43
53
|
**It was fully greenfield.** An audit of production, beta, sandbox-client **and all 36 client
|
|
44
54
|
databases** on 2026-08-28 found **no transfer-order TableView anywhere**. Nothing was extended.
|
|
45
55
|
|
|
56
|
+
A **Create Transfer Order** modal was started the same day: the UI and its interaction rules are
|
|
57
|
+
built, the data layer is **disabled behind a flag** pending three unanswered data-model questions
|
|
58
|
+
(see below).
|
|
59
|
+
|
|
46
60
|
The vertical is **platform-shared with NYCHH as the first (and today only) tenant**: the Core
|
|
47
61
|
surfaces and the stage theme tokens are neutral/shared, and only the TableView and the navigation
|
|
48
62
|
opt-in are `Client_Nychh`. That is the same shape as the Sales Order surfaces with Compass as first
|
|
49
63
|
tenant.
|
|
50
64
|
|
|
51
|
-
## The
|
|
65
|
+
## The migrations
|
|
52
66
|
|
|
53
67
|
| File | DB scope | What it does |
|
|
54
68
|
|---|---|---|
|
|
@@ -56,8 +70,11 @@ tenant.
|
|
|
56
70
|
| `Core/2026-08-28a - TransferOrderSurfaceSeed.sql` | Core | nav element, LIST surface, 4 record surfaces, stage vocabulary, 24 Messages |
|
|
57
71
|
| `Client/2026-08-28a - TransferOrderStageThemeTokens.sql` | all tenants (template) | 5 `color.transferStage.*` tokens |
|
|
58
72
|
| `Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql` | one tenant | the single `SurfaceOverrides` row that reveals the nav item |
|
|
73
|
+
| `Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql` | one tenant | reproduces the **untracked** record/field ACL prod already has for records 312/313 |
|
|
74
|
+
| `Client/2026-08-28b - TransferOrderItemsTimestamps.sql` | all tenants (template) | `TransferOrderItems.dtCreated`/`dtUpdated` (+ indexes), guarded `qtyCommitted` |
|
|
75
|
+
| `Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql` | Core | the `transfer-orders-list-actions` BUTTON_BAR + `common.columns.modal.*` Messages |
|
|
59
76
|
|
|
60
|
-
All
|
|
77
|
+
All of them were audited against the cluster-isolation HARD RULE: **no `Core.*` reference in any
|
|
61
78
|
`Client_*` file** (the only such strings are inside comments), no `UUID()`, all table references
|
|
62
79
|
unqualified.
|
|
63
80
|
|
|
@@ -191,6 +208,94 @@ fields. One FK hop out from the order they need `calcDepth 2`; at the default of
|
|
|
191
208
|
the loaded state swaps element types at the same tree position and replays the slide-up animation —
|
|
192
209
|
see the gotcha in [record-modals-and-nested-tables](./record-modals-and-nested-tables.md).
|
|
193
210
|
|
|
211
|
+
## The prod-only state this vertical sits on top of — four UNTRACKED layers
|
|
212
|
+
|
|
213
|
+
Standing the page up locally failed **one layer at a time**, each with a different-looking error,
|
|
214
|
+
because production and sandbox-client carry transfer-order state that **no `dbchanges2` file
|
|
215
|
+
creates**. It was all hand-applied when the NetSuite TransferOrder POST was enabled:
|
|
216
|
+
|
|
217
|
+
1. `AclRecordPermissions` for records **312** (`transfer-orders`) and **313**
|
|
218
|
+
(`transfer-order-items`), roles 1 and 3;
|
|
219
|
+
2. their `AclLogicGroups` → `AclLogicGroupExpressions` → `AclRecordExpressions` chain;
|
|
220
|
+
3. `AclFieldPermissions` for **all 17 `RecordFields`** of 312/313 (roles 1 and 3, `isWritable = 1`);
|
|
221
|
+
4. the `TransferOrderItems.dtCreated` / `dtUpdated` **columns**.
|
|
222
|
+
|
|
223
|
+
So **a fresh environment cannot reproduce production** — and the blast radius is bigger than this
|
|
224
|
+
UI: the NetSuite transfer-order importer cannot be stood up in a fresh environment either.
|
|
225
|
+
`Client_Nychh/2026-08-28c` reproduces layers 1–3 faithfully (**same roles, same permission bits as
|
|
226
|
+
prod — not a widening**; role 2 *Developer* is deliberately **not** granted because prod/sandbox
|
|
227
|
+
don't have it and role 1 satisfies the UI), anti-joined on natural keys: 2 expressions + 4 record
|
|
228
|
+
permissions + 34 field permissions insert on local, **0 on sandbox** — dry-run verified in both
|
|
229
|
+
directions. Layer 4 is `Client/2026-08-28b`. Client-side detail:
|
|
230
|
+
[NYCHH transfer-order inventory & quantities](../../../../clients/nychh/features/transfer-order-inventory-quantities.md).
|
|
231
|
+
|
|
232
|
+
**Follow-up recorded, not done:** run a systematic `information_schema` diff of local vs prod for
|
|
233
|
+
the whole transfer-order area rather than discovering the gaps one 500 at a time — see
|
|
234
|
+
[client schema drift audit](../../dbchanges2/workflows/client-schema-drift-audit.md).
|
|
235
|
+
|
|
236
|
+
## The list-actions BUTTON_BAR (`Core/2026-08-28b`)
|
|
237
|
+
|
|
238
|
+
`transfer-orders-list-actions`, **Surface 52**, elements **187–189**: Refresh (`SECONDARY`,
|
|
239
|
+
`config.valueKey 'refresh'`), Columns (`SECONDARY`, `config.role 'columnsButton'` +
|
|
240
|
+
`config.modal` message keys), Transfer Order (`PRIMARY`, `config.valueKey 'createTransferOrder'`).
|
|
241
|
+
This closes the "no BUTTON_BAR surface" gap the first build left open.
|
|
242
|
+
|
|
243
|
+
- **No `Actions` rows are needed.** With no handler registered in the FE `actionRegistry`,
|
|
244
|
+
`SurfaceActionBar` already falls through to `onDispatch(valueKey)` — which is exactly the
|
|
245
|
+
host-owned behaviour these three want. Only a control that needs a *registry handler* needs an
|
|
246
|
+
`Action` row.
|
|
247
|
+
- **Seeded `isVisible = 1`, deliberately against the usual Core-OFF default.** A list screen with
|
|
248
|
+
no refresh and no create is not a useful neutral default, and none of the three is unsafe if
|
|
249
|
+
inherited — only NYCHH can reach the page at all. See
|
|
250
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md) for when Core-neutral
|
|
251
|
+
means OFF and when it does not.
|
|
252
|
+
- The Columns button's copy comes from **shared `common.columns.modal.*` Messages**, consumed
|
|
253
|
+
through the new `useColumnVisibilityModalConfig` hook — see
|
|
254
|
+
[column-visibility](./column-visibility.md).
|
|
255
|
+
|
|
256
|
+
## Create Transfer Order modal — UI built, data layer deliberately NOT faked
|
|
257
|
+
|
|
258
|
+
`view/CreateTransferOrderModal/` (+ `hooks/useTransferableItemRows.ts`), opened from the header
|
|
259
|
+
button seeded above. The interaction rules are the durable part:
|
|
260
|
+
|
|
261
|
+
- **Single-source lock.** The first checked row fixes the source location for the whole order.
|
|
262
|
+
Source Location is **derived and read-only — never picked**; the Location filter is pinned and
|
|
263
|
+
disabled while locked, and **restores its pre-lock value** when the lock releases.
|
|
264
|
+
- **A row is an ITEM AT A LOCATION**, kept only where availability >= 1.
|
|
265
|
+
- **Dirty means "different from the state the form OPENED IN"**, not "different from empty" — so a
|
|
266
|
+
pre-scoped open is clean, which is what makes the discard guard trustworthy.
|
|
267
|
+
- **Qty error state machine:** touched on first keystroke; the error surfaces on **blur** only if
|
|
268
|
+
touched; then it live-revalidates.
|
|
269
|
+
- **The Review gate names the FIRST unmet condition** rather than a generic "incomplete".
|
|
270
|
+
|
|
271
|
+
**Three open questions block the data layer** (the picker + fetch are written but **disabled behind
|
|
272
|
+
a flag** — nothing is stubbed with fake data):
|
|
273
|
+
|
|
274
|
+
1. **Availability per (item, location) has no API.** `Item._qtyAvailable` is item-level with no
|
|
275
|
+
location dimension; the real source is `Units` (carries `itemId` + `locationId` + a
|
|
276
|
+
disposition). Unresolved: which `UnitDispositions` slug counts as available, and whether to
|
|
277
|
+
aggregate client-side or add a grouped table view. NYCHH has **3,226 active items**, so
|
|
278
|
+
browser-side grouping will not hold.
|
|
279
|
+
2. **The design's target-location picker is a TREE** (breadcrumbs, drill-down, ancestor path), but
|
|
280
|
+
**no parent FK was found on `_Model_Client_Location`**. If Locations are flat, the dialog has to
|
|
281
|
+
become a searchable select and the design needs revisiting. Picker left disabled.
|
|
282
|
+
3. **Commit flips unit dispositions to In Transit — a real inventory mutation.** Unresolved whether
|
|
283
|
+
the API owns that or a worker does.
|
|
284
|
+
|
|
285
|
+
### Line-item fetch is off behind a FLAG, not commented out
|
|
286
|
+
|
|
287
|
+
`TRANSFER_ORDER_LINES_ENABLED = false` until `Client/2026-08-28b` is applied everywhere. A flag,
|
|
288
|
+
not a commented-out call: the repo rule bans committed commented-out code, and a flag keeps the
|
|
289
|
+
hook typechecked.
|
|
290
|
+
|
|
291
|
+
> ⚠ **A DISABLED React Query v5 query reports `status: 'pending'` FOREVER.** Any `isLoading` derived
|
|
292
|
+
> from it must be read **through the flag**, or the UI hangs on its skeleton. This is the **third**
|
|
293
|
+
> place in this codebase to hit it (`useFetchSurfaceMetaGroup` already documents it).
|
|
294
|
+
|
|
295
|
+
With lines off, the three summary tiles render an **em-dash, not `0`** — all three are derived from
|
|
296
|
+
the lines, so `0` would be a *wrong* number rather than a missing one — and the empty row reads
|
|
297
|
+
"temporarily unavailable", not "no items".
|
|
298
|
+
|
|
194
299
|
## v1 scope — seeded but hidden
|
|
195
300
|
|
|
196
301
|
Money (Unit Value / Total / Total Value) and Target Contact are **dropped for v1 but seeded as real
|
|
@@ -208,15 +313,21 @@ visible in Core (neutral default; other tenants populate `TransferOrders.purchas
|
|
|
208
313
|
|
|
209
314
|
## Rollout status & open items
|
|
210
315
|
|
|
211
|
-
- **Applied and verified on LOCAL only.** Not run on sandbox-client or production.
|
|
212
|
-
|
|
316
|
+
- **Applied and verified on LOCAL only.** Not run on sandbox-client or production.
|
|
317
|
+
- **Git (2026-08-28):** commit **`9293904`** on branch `TRUE-80852` in toga25-supply (3 files — the
|
|
318
|
+
surface-driven header + the extracted hook), **not pushed**. The dbchanges2 migrations are
|
|
319
|
+
uncommitted in a separate repo, and **the FE commit is inert without `Core/2026-08-28b`**, so the
|
|
320
|
+
two must land together (the commit body names the dependency). `.env.development` was
|
|
321
|
+
**deliberately excluded** from the commit — its diff repoints every developer's dev server, which
|
|
322
|
+
must not ride inside a feature ticket (see
|
|
323
|
+
[amplify-deployment](../workflows/amplify-deployment.md)).
|
|
213
324
|
- **Beta is blocked.** It lacks the `TransferOrderStages` table entirely (the `Client/2026-08-18*`
|
|
214
325
|
files never landed there), so `Client_Nychh/2026-08-28a` would fail on a missing FK target.
|
|
215
326
|
- **Open dependency:** `Client/2026-08-28a` writes to `ThemeTokens`, a table created by
|
|
216
327
|
`Client/2026-06-25a - SurfaceClientTables.sql`. Any prod tenant that never received `2026-06-25a`
|
|
217
328
|
will error, and the full 36-tenant list was **not** audited.
|
|
218
|
-
-
|
|
219
|
-
|
|
329
|
+
- ~~No `transfer-orders-list-actions` BUTTON_BAR surface~~ — **closed** by `Core/2026-08-28b`
|
|
330
|
+
(Surface 52, elements 187–189); the header is now `SurfaceActionBar`-driven like Sales Orders.
|
|
220
331
|
- **Test data lives in the session scratchpad, never in dbchanges2** (24 seed orders + an unseed
|
|
221
332
|
script). The dbchanges2 executor runs **every** folder against production, so throwaway fixtures
|
|
222
333
|
must not be committed there. Origin/destination were picked by co-prime strides over ranked active
|
|
@@ -226,6 +337,13 @@ visible in Core (neutral default; other tenants populate `TransferOrders.purchas
|
|
|
226
337
|
|
|
227
338
|
- **A missing `AclRecordPermissions` grant on a joined record deletes a column, not a request.** The
|
|
228
339
|
Status column would have silently vanished; record 351 had no grants at all.
|
|
340
|
+
- **A missing `AclFieldPermissions` grant on the DEFAULT-SORT column is a 500, not a 403.** The
|
|
341
|
+
field is dropped from the SELECT list silently, but `TableView::meta()` still emits
|
|
342
|
+
`table.sort` naming its slug → the FE builds `ORDER BY TransferOrders.undefined` → MySQL 1054.
|
|
343
|
+
Mechanism and the two candidate platform fixes:
|
|
344
|
+
[tableview-field-metadata](../../api2/features/tableview-field-metadata.md).
|
|
345
|
+
- **A disabled React Query v5 query is `pending` forever** — derive `isLoading` through the flag,
|
|
346
|
+
never from the query alone (see the line-item flag above).
|
|
229
347
|
- **Never bind a Core TABLE surface to a `tableViewId`** — TableView ids are per-tenant and would not
|
|
230
348
|
survive the Core/Client cluster split. Use `config.tableViewSlug`.
|
|
231
349
|
- **Renaming a `TransferOrderStages` row silently greys out the badge** — the vocabulary matches the
|
|
@@ -241,6 +359,23 @@ visible in Core (neutral default; other tenants populate `TransferOrders.purchas
|
|
|
241
359
|
earlier migration without names, so a name-based lookup on them finds nothing.
|
|
242
360
|
|
|
243
361
|
## Change history
|
|
362
|
+
- 2026-08-28 — Second pass, same day. Discovered the vertical sits on **four untracked layers** of
|
|
363
|
+
prod/sandbox state (record + field ACL for records 312/313 and their logic-group chain, plus the
|
|
364
|
+
`TransferOrderItems.dtCreated/dtUpdated` columns) — a fresh environment cannot reproduce
|
|
365
|
+
production, and the NetSuite importer is blocked by the same gap; wrote `Client_Nychh/2026-08-28c`
|
|
366
|
+
(faithful reproduction, role 2 deliberately excluded) and `Client/2026-08-28b`. Closed the
|
|
367
|
+
BUTTON_BAR gap with `Core/2026-08-28b` (`transfer-orders-list-actions`, Surface 52, elements
|
|
368
|
+
187–189, seeded **visible** and needing no `Actions` rows because unregistered keys fall through to
|
|
369
|
+
`onDispatch(valueKey)`). Started the **Create Transfer Order modal** — interaction rules
|
|
370
|
+
implemented (single-source lock with a derived read-only Source Location, dirty-vs-opened-state,
|
|
371
|
+
the qty touched/blur error machine, a Review gate that names the first unmet condition) with the
|
|
372
|
+
data layer **disabled behind a flag rather than faked**, blocked on three open questions: no
|
|
373
|
+
per-(item, location) availability API (`Units` + a disposition is the real source; 3,226 active
|
|
374
|
+
items rules out client-side grouping), no parent FK found on `_Model_Client_Location` so the
|
|
375
|
+
design's location *tree* may not be buildable, and unowned responsibility for the In-Transit
|
|
376
|
+
disposition flip. Recorded the React-Query-v5 disabled-query `pending` trap (third occurrence) and
|
|
377
|
+
the ACL-gap-on-default-sort 500. Committed `9293904` (TRUE-80852), not pushed;
|
|
378
|
+
`.env.development` deliberately left out. (apeterson)
|
|
244
379
|
- 2026-08-28 — First capture: built the Transfer Orders vertical from scratch after confirming no
|
|
245
380
|
transfer-order TableView existed in prod, beta, sandbox-client or any of the 36 client DBs. Seeded
|
|
246
381
|
the `Client_Nychh` `transfer-orders` TableView (3 joins, 4 columns) plus the full 4-table ACL chain
|
|
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-28
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/amplify.yml
|
|
@@ -14,8 +14,11 @@ files:
|
|
|
14
14
|
- toga25-supply/src/hooks/useAuthenticationFlow.ts
|
|
15
15
|
- toga25-supply/vite.config.ts
|
|
16
16
|
- toga25-supply/package.json
|
|
17
|
+
- toga25-supply/.env.development
|
|
17
18
|
related:
|
|
18
19
|
- ../architecture.md
|
|
20
|
+
- ../features/transfer-orders-page.md
|
|
21
|
+
- ../../toga-blox/workflows/local-link-into-a-consumer-app.md
|
|
19
22
|
---
|
|
20
23
|
|
|
21
24
|
## What it is
|
|
@@ -90,6 +93,22 @@ i.e. an optional per-subdomain override, falling back to `VITE_API`. The `.env.<
|
|
|
90
93
|
are committed and git-tracked; they contain **only public `VITE_API` base URLs, no secrets**,
|
|
91
94
|
and are the single source of truth for environment API endpoints.
|
|
92
95
|
|
|
96
|
+
## Local development — `npm run dev` does NOT point at your local API
|
|
97
|
+
|
|
98
|
+
`npm run dev` runs Vite in mode **`development`**, so **`.env.development` wins**, and that file
|
|
99
|
+
ships `VITE_API = https://api.dev.sandbox.togahub.com/v2`. A developer who assumes "dev = my
|
|
100
|
+
machine" is reading and writing **dev-sandbox** data.
|
|
101
|
+
|
|
102
|
+
Worse, **`.env.development` is git-TRACKED**, so developers keep flipping it to their local API —
|
|
103
|
+
which generates noise diffs and accidental commits, and a stray commit **repoints every other
|
|
104
|
+
developer's dev server**. (This is why the transfer-orders FE commit deliberately excluded it; see
|
|
105
|
+
[Transfer Orders page](../features/transfer-orders-page.md).)
|
|
106
|
+
|
|
107
|
+
**Suggested fix, not yet done:** keep a neutral committed default in `.env.development` and let each
|
|
108
|
+
developer override in a **gitignored `.env.development.local`** (Vite loads `.local` last and it is
|
|
109
|
+
the intended per-developer layer). Until that lands, check `git status` before committing on any
|
|
110
|
+
branch where you pointed the dev server at localhost.
|
|
111
|
+
|
|
93
112
|
## Adding a new environment
|
|
94
113
|
|
|
95
114
|
1. Create `.env.<mode>` with the public `VITE_API` base URL.
|
|
@@ -131,6 +150,12 @@ and are the single source of truth for environment API endpoints.
|
|
|
131
150
|
has ever been developed against a linked local checkout.
|
|
132
151
|
|
|
133
152
|
## Change history
|
|
153
|
+
- 2026-08-28 — Recorded that **`npm run dev` does not point at a local API**: mode `development`
|
|
154
|
+
loads the tracked `.env.development`, which ships
|
|
155
|
+
`VITE_API = https://api.dev.sandbox.togahub.com/v2` — so "dev" reads/writes dev-sandbox data. The
|
|
156
|
+
file being tracked is itself a hazard (flip-and-commit noise repoints everyone's dev server);
|
|
157
|
+
suggested a neutral committed default plus a gitignored `.env.development.local` — not done.
|
|
158
|
+
(apeterson)
|
|
134
159
|
- 2026-08-27 — Fixed `_production` never declaring `@agilant/toga-blox` (no dependency entry, no
|
|
135
160
|
lock entry, ~200 imports) — it resolved only via a local **symlink**, and `npm ci` in `preBuild`
|
|
136
161
|
would have failed. Added blox plus the four peers the symlink had masked
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_ | library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/common_sync_togasupply.php |
|
|
6
6
|
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
7
|
-
| [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql |
|
|
8
|
-
| [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, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
|
|
7
|
+
| [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
8
|
+
| [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, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.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, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
@@ -6,18 +6,23 @@ project: _Underscore
|
|
|
6
6
|
client: nychh
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-08-28
|
|
10
|
+
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Nychh/Item.php
|
|
13
13
|
- _underscore/Model/Nychh/TransferOrder.php
|
|
14
14
|
- _underscore/Model/Nychh/TransferOrderStage.php
|
|
15
15
|
- _underscore/Model/Client/TransferOrder.php
|
|
16
|
+
- _underscore/Model/Client/TransferOrderItem.php
|
|
16
17
|
- dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql
|
|
17
18
|
- dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql
|
|
19
|
+
- dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql
|
|
20
|
+
- dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql
|
|
18
21
|
related:
|
|
19
22
|
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
20
23
|
- ../../../2.0/apps/api2/features/v2-api-error-codes.md
|
|
24
|
+
- ../../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md
|
|
25
|
+
- ../../../2.0/apps/toga25-supply/features/transfer-orders-page.md
|
|
21
26
|
- ../../growrk/features/transfer-order-flow.md
|
|
22
27
|
- ./netsuite-transfer-order-import.md
|
|
23
28
|
- ../profile.md
|
|
@@ -31,6 +36,10 @@ that let a NetSuite-sourced TransferOrder actually POST through the V2 API. The
|
|
|
31
36
|
feeds these POSTs is in
|
|
32
37
|
[NYCHH NetSuite → TransferOrders import](./netsuite-transfer-order-import.md).
|
|
33
38
|
|
|
39
|
+
> 🚨 **Most of that enablement was hand-applied and never written into `dbchanges2`** (four layers —
|
|
40
|
+
> see below). It was reconstructed on 2026-08-28; until those files run, a fresh environment can
|
|
41
|
+
> reproduce neither the UI nor the importer.
|
|
42
|
+
|
|
34
43
|
The general, framework-level lessons from the field-exposure work here are recorded on the shared
|
|
35
44
|
[ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md) and
|
|
36
45
|
[V2 API error codes](../../../2.0/apps/api2/features/v2-api-error-codes.md) docs; this doc keeps the
|
|
@@ -83,6 +92,51 @@ peeled in order:
|
|
|
83
92
|
> dead end.) Recorded framework-wide on the
|
|
84
93
|
> [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
|
|
85
94
|
|
|
95
|
+
## 🚨 The enablement above was HAND-APPLIED — four layers exist on prod with no migration behind them
|
|
96
|
+
|
|
97
|
+
Discovered 2026-08-28 while standing the Transfer Orders UI up locally: beyond the two 08-27 files,
|
|
98
|
+
**production and sandbox-client carry transfer-order state that no `dbchanges2` file creates.** It
|
|
99
|
+
was all applied by hand when the NetSuite TransferOrder POST was enabled:
|
|
100
|
+
|
|
101
|
+
| Layer | What is missing from the repo |
|
|
102
|
+
|---|---|
|
|
103
|
+
| a | `AclRecordPermissions` for records **312** (`transfer-orders`) / **313** (`transfer-order-items`), roles **1** and **3** |
|
|
104
|
+
| b | their `AclLogicGroups` → `AclLogicGroupExpressions` → `AclRecordExpressions` chain |
|
|
105
|
+
| c | `AclFieldPermissions` for **all 17 `RecordFields`** of 312/313 (roles 1 and 3, `isWritable = 1`) |
|
|
106
|
+
| d | the `TransferOrderItems.dtCreated` / `dtUpdated` **columns** |
|
|
107
|
+
|
|
108
|
+
**Consequence: a fresh environment cannot reproduce production**, and that is not just a UI problem
|
|
109
|
+
— **the NetSuite transfer-order importer cannot be stood up in a fresh environment either.** Local
|
|
110
|
+
had none of it and failed **one layer at a time, each with a different-looking error** (a dropped
|
|
111
|
+
column, an `EZ-1`, a 500), which is what makes this class of drift so expensive to diagnose.
|
|
112
|
+
|
|
113
|
+
**`Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql`** reproduces (a)–(c) **faithfully — same
|
|
114
|
+
roles, same permission bits as prod. It is not a widening.** Role **2** (Developer) is deliberately
|
|
115
|
+
**not** granted: prod and sandbox don't have it, and role 1 satisfies the UI. Every statement is
|
|
116
|
+
anti-joined on natural keys, so it inserts **2 expressions + 4 record permissions + 34 field
|
|
117
|
+
permissions on local and 0 on sandbox** — dry-run verified in both directions.
|
|
118
|
+
Layer (d) is `Client/2026-08-28b` (see the next section).
|
|
119
|
+
|
|
120
|
+
**Follow-up, recorded not done:** a systematic `information_schema` diff of local vs prod across the
|
|
121
|
+
whole transfer-order area, instead of discovering gaps one 500 at a time — procedure in
|
|
122
|
+
[client schema drift audit](../../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md).
|
|
123
|
+
|
|
124
|
+
## `TransferOrderItems` is missing `dtCreated`/`dtUpdated` in the BLANK TEMPLATE — every tenant
|
|
125
|
+
|
|
126
|
+
`_Model_Client_TransferOrderItem` declares `dtCreated`/`dtUpdated` as
|
|
127
|
+
`FIELD_DATETIME_CREATED`/`FIELD_DATETIME_UPDATED`, so `_Model` puts them in **every SELECT it
|
|
128
|
+
builds** — nothing in a request has to ask for them. But the blank-client template
|
|
129
|
+
(`Client/2026-06-03- BLANK_CLIENT_DATABASE.sql`, `CREATE` at ~line 9462) **does not create them**.
|
|
130
|
+
So **every tenant provisioned from the template has a table the model cannot read.** This is not
|
|
131
|
+
NYCHH-specific; NYCHH is just where it surfaced.
|
|
132
|
+
|
|
133
|
+
`Client/2026-08-28b - TransferOrderItemsTimestamps.sql` adds both columns (+ their indexes) and,
|
|
134
|
+
guarded, `qtyCommitted`. ⚠ **The template itself still needs fixing** or the next new tenant
|
|
135
|
+
reproduces the defect — not done (it is a generated dump plus an already-run migration). Recorded on
|
|
136
|
+
[new-client onboarding](../../../2.0/apps/dbchanges2/workflows/client-onboarding.md); the
|
|
137
|
+
conditional-`ALTER` authoring technique the file uses is in
|
|
138
|
+
[re-runnable additive migrations](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md).
|
|
139
|
+
|
|
86
140
|
## `isActive` is defaulted in the ORM, not sent in the payload (shared model)
|
|
87
141
|
|
|
88
142
|
`TransferOrders.isActive` is **NOT NULL**, but `isActive` is **not a registered/writable V2 field** —
|
|
@@ -104,6 +158,16 @@ pattern (default a NOT NULL non-writable column in the model, never send it) is
|
|
|
104
158
|
|
|
105
159
|
## Change history
|
|
106
160
|
|
|
161
|
+
- 2026-08-28 — Discovered that the transfer-order enablement on **prod and sandbox-client is
|
|
162
|
+
UNTRACKED**: record ACL for 312/313 (roles 1 and 3), their logic-group/expression chain, field ACL
|
|
163
|
+
for all 17 RecordFields, and the `TransferOrderItems.dtCreated`/`dtUpdated` columns all exist with
|
|
164
|
+
**no `dbchanges2` file behind them** — so a fresh environment can reproduce neither the UI nor the
|
|
165
|
+
NetSuite importer, and local failed one layer at a time with a different-looking error each time.
|
|
166
|
+
Wrote `Client_Nychh/2026-08-28c` (faithful reproduction of the ACL layers — same roles, same bits,
|
|
167
|
+
role 2 deliberately excluded; 2+4+34 rows on local, 0 on sandbox) and `Client/2026-08-28b` for the
|
|
168
|
+
columns. Root cause of the column gap is the **blank-client template**, which never creates the two
|
|
169
|
+
timestamps the model always selects — every tenant is affected and the template is still unfixed.
|
|
170
|
+
(apeterson)
|
|
107
171
|
- 2026-08-27 — Built the NYCHH two-branch inventory quantity model on `_Model_Nychh_Item` (on hand =
|
|
108
172
|
received − fulfilled; available = on hand − committed; fulfilled = Σ shipped TO fulfillments;
|
|
109
173
|
backordered = ordered − received; added `_qtyOnOrder`), per the 2026-08-17 TOGa Supply meeting.
|
|
@@ -25,6 +25,8 @@ files:
|
|
|
25
25
|
- dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql
|
|
26
26
|
- dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
|
|
27
27
|
- dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql
|
|
28
|
+
- dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql
|
|
29
|
+
- dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql
|
|
28
30
|
related:
|
|
29
31
|
- ../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
30
32
|
- ../../2.0/apps/_underscore/features/tableview-joins.md
|
|
@@ -72,8 +74,17 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
72
74
|
tenant already grants Sales Orders and Inventory — NYCHH does not scope navigation by role). The
|
|
73
75
|
Core surfaces and the stage colour tokens are **shared/neutral**, hidden for every other client
|
|
74
76
|
until they opt in. Not yet run on sandbox-client or production; **beta is blocked** because it
|
|
75
|
-
lacks the `TransferOrderStages` table.
|
|
77
|
+
lacks the `TransferOrderStages` table. A **Create Transfer Order** modal is started (UI built, data
|
|
78
|
+
layer flagged off pending three open data-model questions). See
|
|
76
79
|
[Transfer Orders page](../../2.0/apps/toga25-supply/features/transfer-orders-page.md).
|
|
80
|
+
- 🚨 **The transfer-order ACL + schema on prod/sandbox are UNTRACKED (2026-08-28).** Record and field
|
|
81
|
+
ACL for records 312/313, their logic-group/expression chain, and the
|
|
82
|
+
`TransferOrderItems.dtCreated`/`dtUpdated` columns were all hand-applied when the NetSuite
|
|
83
|
+
TransferOrder POST was enabled — **no `dbchanges2` file creates any of it**, so a fresh environment
|
|
84
|
+
can reproduce neither the UI nor the importer. Reconstructed in `Client_Nychh/2026-08-28c` (ACL,
|
|
85
|
+
faithful — role 2 deliberately excluded) and `Client/2026-08-28b` (columns; root cause is the blank
|
|
86
|
+
client template, which affects **every** tenant). See
|
|
87
|
+
[NYCHH transfer-order inventory & quantities](./features/transfer-order-inventory-quantities.md).
|
|
77
88
|
- **Two-branch inventory quantities + TransferOrder V2 enablement** — NYCHH-only on-hand/available/
|
|
78
89
|
fulfilled/backordered model on `_Model_Nychh_Item`, plus the client-override models + ACL rows that
|
|
79
90
|
let a NetSuite TransferOrder POST succeed. See
|
package/package.json
CHANGED