toga-ai 1.0.399 → 1.0.401
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/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/assortment-name-translation.md +19 -6
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +51 -10
- package/knowledge/2.0/apps/api2/features/language-translation-layer.md +54 -3
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +28 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +70 -26
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
24
24
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
|
|
25
25
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
|
|
26
|
-
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
|
|
26
|
+
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
|
|
27
27
|
| [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
|
|
28
28
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
29
29
|
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/AssortmentTranslation.php
|
|
@@ -25,9 +25,15 @@ Serves Assortment (product-grouping) **names** in multiple languages by adding a
|
|
|
25
25
|
English stays in `Assortments.name`; the sidecar holds only non-English overlays. Because the
|
|
26
26
|
translation layer is fully metadata-driven (see
|
|
27
27
|
[Language Translation Layer](../../api2/features/language-translation-layer.md)), wiring this up
|
|
28
|
-
required
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
required only a new table, model, Core record registration, ACL grant, and a single field link.
|
|
29
|
+
Compass Canada's French (fr-CA) assortment names are seeded as the first consumer.
|
|
30
|
+
|
|
31
|
+
> **Correction (2026-07-21):** the original "no api2 code change needed" claim was only true for the
|
|
32
|
+
> **FK/top-level** read paths. Assortment names are surfaced on the storefront **category browse** via a
|
|
33
|
+
> `join=` request (`Assortments.name`), and the joined-select-fields read path did **not** consult the
|
|
34
|
+
> sidecar — it copied the raw SQL column straight into the response, so the seeded French names stayed
|
|
35
|
+
> English there. That required a real api2 change (the 5th serialization site) — see the
|
|
36
|
+
> [Language Translation Layer](../../api2/features/language-translation-layer.md) 2026-07-21 change history.
|
|
31
37
|
|
|
32
38
|
## Key files / entry points
|
|
33
39
|
|
|
@@ -51,8 +57,10 @@ The mechanism itself is the data-driven translation layer documented in
|
|
|
51
57
|
4. **The field link (what makes it translate automatically).** The translatable `Assortments.name`
|
|
52
58
|
RecordField gets its `Core.RecordFields.translationRecordFieldId` set to the sidecar's `name`
|
|
53
59
|
RecordField. The api2 V2 layer reads `translationRecordFieldId` for every field and automatically
|
|
54
|
-
returns the sidecar value for the caller's resolved language (falling back to English when absent)
|
|
55
|
-
|
|
60
|
+
returns the sidecar value for the caller's resolved language (falling back to English when absent).
|
|
61
|
+
This works with no api2 code change for FK/top-level reads; the storefront's `join=` (joined-select)
|
|
62
|
+
read path additionally required the 5th-serialization-site fix landed 2026-07-21 (see the correction
|
|
63
|
+
note above and the Language Translation Layer doc).
|
|
56
64
|
5. **ACL.** A full ACL chain grants the **Base** role read/write on the new record. Because the
|
|
57
65
|
record's `aclDatabase = 'CLIENT'`, the grant lives in each client DB — see
|
|
58
66
|
[ACL Permission Chain](./acl-permission-chain.md).
|
|
@@ -75,6 +83,11 @@ The mechanism itself is the data-driven translation layer documented in
|
|
|
75
83
|
|
|
76
84
|
## Change history
|
|
77
85
|
|
|
86
|
+
- 2026-07-21 — Correction: the "no api2 code change needed" claim held only for FK/top-level reads. The
|
|
87
|
+
storefront category browse surfaces `Assortments.name` via a `join=` request, and the joined-select read
|
|
88
|
+
path in api2 did not consult the sidecar (raw column copied through), so seeded fr-CA names stayed English
|
|
89
|
+
there. Fixed by the 5th-serialization-site change in the Language Translation Layer (V2.php); no change to
|
|
90
|
+
this sidecar/model/metadata was required. (bala)
|
|
78
91
|
- 2026-06-26 — Initial build: `AssortmentTranslations` sidecar table + `_Model_Client_AssortmentTranslation`
|
|
79
92
|
model + `Core.Records` 332 registration + `translationRecordFieldId` link on `Assortments.name` + Base-role
|
|
80
93
|
ACL chain. Reuses the existing metadata-driven translation layer, so no api2 code change was needed.
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Surface.php
|
|
@@ -36,6 +36,7 @@ files:
|
|
|
36
36
|
- _underscore/Model/Client/MessageTranslation.php
|
|
37
37
|
- _underscore/Model/Client/ThemeToken.php
|
|
38
38
|
- _underscore/Model/Core/Page.php
|
|
39
|
+
- api2/Component/Api/V2/V2.php
|
|
39
40
|
related:
|
|
40
41
|
- ../../dbchanges2/features/surface-layer-schema.md
|
|
41
42
|
- ../../api2/features/surface-meta-option.md
|
|
@@ -253,22 +254,34 @@ rule would hit. **Scope:** the Approve element on **surface 8** (`sales-order-re
|
|
|
253
254
|
and **surface 3** (`sales-order-listing-row-actions`, el 29). The approval-workflow-modal Approve
|
|
254
255
|
element does not exist yet and is **deferred**.
|
|
255
256
|
|
|
256
|
-
> ⚠ **
|
|
257
|
-
> which the backend Tier-2 path cannot resolve (
|
|
258
|
-
> `resolveSurfaceActionState`, no backend `stepTwoAssigned`)
|
|
259
|
-
>
|
|
260
|
-
> (`
|
|
261
|
-
>
|
|
257
|
+
> ⚠ **Resolved on the FE, not server-side (Option B, 2026-07-21).** The rule carries a
|
|
258
|
+
> `{"type":"stepTwoAssigned"}` named node, which the backend Tier-2 path cannot resolve (no
|
|
259
|
+
> `resolveSurfaceActionState`, no backend `stepTwoAssigned`). Rather than build the Tier-2 capability,
|
|
260
|
+
> **Option B** teaches the FE `evaluateSurfaceRule` a `SURFACE_NAMED_RULES` escape hatch
|
|
261
|
+
> (`stepTwoAssigned`/`poNumberEntered`) and feeds it live approval-stage data on both consuming
|
|
262
|
+
> surfaces — an **intentional deviation** from the frozen-grammar/escalate-to-Tier-2 design note. The
|
|
263
|
+
> gate is now evaluated live on the FE. See
|
|
262
264
|
> [surface-frontend](../../toga25-supply/features/surface-frontend.md).
|
|
263
265
|
- **MANAGER**: opts into **approve + deny only** (`IS_VISIBLE=1`); rules left at the Core base
|
|
264
266
|
(`pendingApproval` only, single-stage). No approvals-filter override → inherits Core `isVisible=0`
|
|
265
267
|
(hidden).
|
|
266
|
-
- **Quad** — Approve + Deny
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
- **Quad** — Approve + Deny visible for **role ids 7/8/9** (`IS_VISIBLE=1`, deliberately per-role
|
|
269
|
+
unlike Quad's usual client-wide pattern). **New 2026-07-21:** Approve carries a client-wide
|
|
270
|
+
(`roleId NULL`) `ENABLED_RULE` = `{"type":"poNumberEntered"}` on **both** surface 8 (el 17) and
|
|
271
|
+
surface 3 (el 29) — "disable Approve until a PO is entered" (the `isPoComplete` semantic), plus a
|
|
272
|
+
`MessageTranslations` row overriding the disabled-tooltip text. Approvals filter button is
|
|
269
273
|
client-wide with `CONFIG` `_status=[pendingApproval]`; the "Enter PO Details" button (element 32) is
|
|
270
274
|
made client-wide visible (⚠ dual-gate — see the schema doc; the FE may also read `config.isVisible`).
|
|
271
275
|
|
|
276
|
+
**Approvals-gate marker (Compass / Compass Canada, 2026-07-21).** The corrected Approve `ENABLED_RULE`
|
|
277
|
+
(`(pendingInitialApproval AND stepTwoAssigned) OR pendingApproval`) was applied to both surfaces (el 17
|
|
278
|
+
+ el 29, ADMIN-scoped, rule JSON in `c_longValue`, `value` NULL). Because Compass's `2026-07-17a`
|
|
279
|
+
already inserted an `ENABLED_RULE` for approve el 17, the migration **UPDATEs** that row then
|
|
280
|
+
guarded-INSERTs el 29. This also required enabling the **approvals-gate marker** for Compass/CC
|
|
281
|
+
(`Client_<X>/2026-07-21b - SalesOrderApprovalsGateEnable.sql`) — without it `approvalsEnabled=false`
|
|
282
|
+
FE-side, stage data never fetches, and `stepTwoAssigned` **fails open** (silently enabling the button).
|
|
283
|
+
See the fail-open gotcha in [surface-frontend](../../toga25-supply/features/surface-frontend.md).
|
|
284
|
+
|
|
272
285
|
> ✅ **Regression (mostly) closed:** the two-stage rule overrides and the record-action/filter opt-ins
|
|
273
286
|
> for Compass / Compass Canada that were wiped in the earlier rebuild were **re-added 2026-07-17**
|
|
274
287
|
> (`Client_Compass`/`Client_CompassCanada/2026-07-17a`+`b`). These surface DB changes remain **local/
|
|
@@ -309,6 +322,22 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
|
|
|
309
322
|
|
|
310
323
|
## Gotchas
|
|
311
324
|
|
|
325
|
+
- **Per-client surface message overrides silently never apply unless the JWT carries
|
|
326
|
+
`id.client.languageId`.** `_Model_Core_Surface::_loadMessages` overlays `Core.Messages.defaultValue`
|
|
327
|
+
with `Client_<X>.MessageTranslations` rows **only when `languageId > 0`**, and `Surface.php` resolves
|
|
328
|
+
`languageId` **solely** from the JWT `id.client.languageId` claim. But auth (`api2/V2.php`)
|
|
329
|
+
historically set only `id.language` (the string code `"en"`) and **never** `id.client.languageId`, so
|
|
330
|
+
`languageId` was always `0`, the overlay was skipped, and every per-client surface message override
|
|
331
|
+
(e.g. a client-specific disabled-tooltip) fell back to the Core default with **no error**. (There was
|
|
332
|
+
an explicit TODO at `V2.php` ~L5738.) **Fix (2026-07-21):** `/auth/login` now embeds
|
|
333
|
+
`$id->client->languageId = (int)$language->id` in **both** the configured-language and default-`en`
|
|
334
|
+
branches (English gets a real `languageId` too; the overlay keys on `languageId > 0` and falls back
|
|
335
|
+
to Core defaults where a client has no `MessageTranslations` row). ⚠ **CAVEAT — only `/auth/login`
|
|
336
|
+
was fixed.** The refresh (`V2.php` ~L1227), oauth, api, delegator, encrypted, and public token paths
|
|
337
|
+
still don't embed `id.client.languageId`, so **after an access-token refresh (~3600s) `languageId` is
|
|
338
|
+
lost** and per-client surface messages revert until those paths are updated too. (This is a **separate
|
|
339
|
+
claim** from `id.language`, which drives the data-translation sidecar — see
|
|
340
|
+
[language-translation-layer](../../api2/features/language-translation-layer.md); do not conflate.)
|
|
312
341
|
- **A `SurfaceOverrides` row with `attribute='CONFIG'` carrying a RULE value is silently DEAD —
|
|
313
342
|
encode rules as `VISIBILITY_RULE`/`ENABLED_RULE`, never as `CONFIG`.** `_castOverride` routes the
|
|
314
343
|
`CONFIG` attribute into `$config`, **NOT** `$visibilityRule`/`$enabledRule`; a `CONFIG` row whose
|
|
@@ -391,6 +420,18 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
|
|
|
391
420
|
match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
392
421
|
|
|
393
422
|
## Change history
|
|
423
|
+
- 2026-07-21 — Recorded **Option B**: the Approve step-two/PO gate is now evaluated **live on the FE**
|
|
424
|
+
(FE `evaluateSurfaceRule` gained a `SURFACE_NAMED_RULES` escape hatch) rather than via the no-op
|
|
425
|
+
backend Tier-2 — an intentional deviation from the frozen-grammar/escalate note (updated the
|
|
426
|
+
"not resolvable server-side" caveat). Added the **Quad** Approve `ENABLED_RULE`
|
|
427
|
+
`{"type":"poNumberEntered"}` (client-wide, both surfaces) + disabled-tooltip `MessageTranslations`
|
|
428
|
+
override, and the **Compass/CC** approvals-gate marker enable (`2026-07-21b`) needed so
|
|
429
|
+
`stepTwoAssigned` doesn't fail open. New gotcha: per-client surface **message** overrides silently
|
|
430
|
+
never apply unless the JWT carries `id.client.languageId` (`_loadMessages` overlays
|
|
431
|
+
`MessageTranslations` only when `languageId>0`; `Surface.php` reads it solely from that claim, but
|
|
432
|
+
auth only set `id.language`) — fixed in `api2/V2.php` `/auth/login` (both language branches), with the
|
|
433
|
+
caveat that refresh/oauth/api/delegator/encrypted/public paths still lose it after a token refresh.
|
|
434
|
+
(apeterson)
|
|
394
435
|
- 2026-07-20 — Clarified the **backend Tier-2 gate reality**: `_resolveTier2State` (Surface.php ~280)
|
|
395
436
|
dispatches by **action slug** to `resolveSurfaceActionState()` (`TIER2_CAPABILITY_METHOD`), **not**
|
|
396
437
|
by `config.tier2` — `config.tier2` is a **dead marker nothing reads** (backend or FE). The surface
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -110,8 +110,11 @@ RecordField. `buildLookups`/RecordFields-load resolves this into
|
|
|
110
110
|
`lookupTranslatableFieldByRecordFieldId[sourceRecordFieldId] => {sidecarRecordId, sidecarField}`.
|
|
111
111
|
|
|
112
112
|
**Read path (PHP `_Model::load()`, NOT a SQL JOIN — deliberate choice).** At every response
|
|
113
|
-
serialization site (
|
|
114
|
-
|
|
113
|
+
serialization site `getTranslatedFieldValue($record, $field, $sourceModel, $defaultValue)` is called.
|
|
114
|
+
There are now **five** serialization sites, all of which must route through the helper: (1) top-level
|
|
115
|
+
full-model fields, (2) custom-field options, (3) foreign-key child objects, (4) inherent-child arrays
|
|
116
|
+
(both the specific-fields and the all-fields/expand-everything branches), and (5) **joined-select
|
|
117
|
+
fields** (fields requested from a `join=` table, e.g. `Assortments.name`). When a non-base
|
|
115
118
|
language is active and the field is translatable, it loads the sidecar row for that source row +
|
|
116
119
|
`languageId` (cached per row so multiple fields = one load) and returns the sidecar value if non-null;
|
|
117
120
|
otherwise it returns the English default and queues a fallback warning. The warning now carries the
|
|
@@ -119,6 +122,21 @@ missing record's `uuid` in its `identifiers` and dedupes per-field-**and**-per-r
|
|
|
119
122
|
(key `recordFieldId:uuid`) rather than once-per-field — so every record lacking a translation for a
|
|
120
123
|
field is reported individually.
|
|
121
124
|
|
|
125
|
+
**Joined-select fields (site 5) — how the source row is reached.** A `join=` field's value arrives as
|
|
126
|
+
a flat SQL column (`$row->{Alias_field}`), so the helper needs the joined row's identity to load its
|
|
127
|
+
sidecar. In the LIST/GET branch of `processRoutePairs` (joined-select SQL build ~L3700-3800,
|
|
128
|
+
serialization ~L4038-4060): when `shouldTranslate()` is true and a selected joined field is translatable
|
|
129
|
+
(`lookupRecordFieldByRecordIdAndField` + `lookupTranslatableFieldByRecordFieldId`), the SQL build also
|
|
130
|
+
SELECTs that joined table's primary key (`` `Alias`.`id` AS `Alias_id` ``), tracked in
|
|
131
|
+
`$joinedTableRecordsByAlias` and `$joinedTablePrimaryKeyAliasByAlias`; the hidden PK column is only
|
|
132
|
+
appended when not already requested (`in_array` guard). At serialization the joined source model is
|
|
133
|
+
instantiated by that PK (`new $joinedSourceModelName((int)$row->$alias)`) and each joined value routes
|
|
134
|
+
through `getTranslatedFieldValue()`. English and API-credential/public traffic is byte-identical (all
|
|
135
|
+
new logic gated behind `shouldTranslate()`, which is false for en / API / public — no extra SQL column,
|
|
136
|
+
no extra model load, output unchanged). This is why a metadata-only sidecar (e.g. `AssortmentTranslations`)
|
|
137
|
+
still needed an api2 code change to translate on the `join=` read path even though FK/top-level reads
|
|
138
|
+
worked with no code change.
|
|
139
|
+
|
|
122
140
|
**Write path.** On create and update, `extractTranslationWrites()` pulls translatable fields out of
|
|
123
141
|
the write set for a non-base language (so the English source is never overwritten), and after the
|
|
124
142
|
source row saves, `saveTranslationWrites()` upserts them into the sidecar for the current language.
|
|
@@ -191,6 +209,25 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
191
209
|
(matching `ItemTranslations`) while the source fields are `utf8mb4_0900_ai_ci`. Ad-hoc queries that
|
|
192
210
|
compare a sidecar string against a source string must add `COLLATE utf8mb4_bin` or MySQL throws a
|
|
193
211
|
collation-mismatch error.
|
|
212
|
+
- **GROUP BY gotcha on translatable joined fields (site 5).** The sibling joined-field code pushes each
|
|
213
|
+
selected joined field into `$groupBy` under aggregates; the hidden PK column added for translation must
|
|
214
|
+
**also** be pushed to `$groupBy` when `!empty($aggregateFunctionFields)`, or a non-English list request
|
|
215
|
+
combining a translatable `join=` with an aggregate/`group=` throws MySQL 1055 under `ONLY_FULL_GROUP_BY`.
|
|
216
|
+
Mirror the sibling `$groupBy[]` push for the PK column.
|
|
217
|
+
- **DISTINCT granularity (site 5).** The list path always runs `SELECT DISTINCT`, so adding the hidden PK
|
|
218
|
+
to the SELECT can in theory change DISTINCT granularity. Only reachable with distinct + join + a
|
|
219
|
+
translatable joined field whose rows share values but differ by PK; verified harmless for the real
|
|
220
|
+
storefront query (a `WHERE` pinning one assortment yields the same 4 rows). Worth checking if a
|
|
221
|
+
translatable join is added to a query that relies on DISTINCT collapsing duplicate rows.
|
|
222
|
+
- **Per-row cost on translatable-join lists (site 5).** `getTranslatedFieldValue` dereferences the source
|
|
223
|
+
model key via `_Model __get`, forcing a full row load, so ~2 queries per row (source + sidecar) for
|
|
224
|
+
non-English translatable-join lists — consistent with the feature's per-row-load tradeoff. Candidate
|
|
225
|
+
future optimization: let the helper accept a known id instead of a live `_Model`.
|
|
226
|
+
- **Pre-existing (not introduced by site 5): `join=`/`ojoin=` identifiers are unvalidated.** Table/alias/
|
|
227
|
+
ON-clause identifiers flow from the query string into SQL with no validation/escaping (`V2.php` ~L3416-3463,
|
|
228
|
+
`parseOptionsJoin` ~L7874-7920). The new PK-select line reuses the same already-tainted `$tableAlias` as the
|
|
229
|
+
adjacent pre-existing lines, so it adds no new injection class — but the underlying tainted-identifier path
|
|
230
|
+
is a standing concern flagged for separate follow-up.
|
|
194
231
|
- Reading a possibly-absent magic field off a generic `_Model` needs `array_key_exists(...)` +
|
|
195
232
|
`__get` — `isset()`/`?? null` always read false/null. See
|
|
196
233
|
[_Model magic-field access](../../_underscore/features/model-magic-field-access.md); this was the
|
|
@@ -202,6 +239,20 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
202
239
|
|
|
203
240
|
## Change history
|
|
204
241
|
|
|
242
|
+
- 2026-07-21 — Wired translation into the **joined-select-fields** path (the 5th and final read-path
|
|
243
|
+
serialization site) in `V2.php` `processRoutePairs` (joined-select SQL build ~L3700-3800, serialization
|
|
244
|
+
~L4038-4060). `join=` fields (e.g. `Assortments.name`) previously copied the raw SQL column straight into
|
|
245
|
+
the response and never consulted the sidecar, so Compass Canada's already-seeded French assortment/category
|
|
246
|
+
names stayed English on the storefront category browse. Fix (gated behind `shouldTranslate()`, so en/API/
|
|
247
|
+
public output is byte-identical): SELECT the joined table's PK as a hidden `` `Alias`.`id` AS `Alias_id` ``
|
|
248
|
+
(tracked in `$joinedTableRecordsByAlias`/`$joinedTablePrimaryKeyAliasByAlias`, `in_array`-guarded), then at
|
|
249
|
+
serialization instantiate the joined source model by that PK and route the value through
|
|
250
|
+
`getTranslatedFieldValue()`. Also fixed a GROUP BY 1055 bug (the hidden PK must be pushed to `$groupBy` when
|
|
251
|
+
aggregates are present). Noted the DISTINCT-granularity edge, the ~2-queries-per-row cost, and that the
|
|
252
|
+
pre-existing unvalidated `join=` identifier path is unchanged (no new injection class). No toga2-commerce or
|
|
253
|
+
DB/metadata change needed — sidecar rows, the `Assortments.name` `translationRecordFieldId` link, and the
|
|
254
|
+
fr-CA storefront request all already existed. cso: SAFE TO SHIP; php-reviewer: safe after the GROUP BY fix
|
|
255
|
+
(applied), remaining items non-blocking follow-ups. (bala)
|
|
205
256
|
- 2026-07-13 — Extended the translation layer to item **feature** text: three new sidecar tables
|
|
206
257
|
(`FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`, `ItemFeatureTranslations`; Records
|
|
207
258
|
343/344/345, RecordFields 2442–2457) with `translationRecordFieldId` wired to the source fields,
|
|
@@ -3,5 +3,5 @@
|
|
|
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
|
-
| [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 | 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 |
|
|
6
|
+
| [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 | 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 |
|
|
7
7
|
| [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/ |
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
|
|
@@ -54,6 +54,12 @@ files:
|
|
|
54
54
|
- dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql
|
|
55
55
|
- dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql
|
|
56
56
|
- dbchanges2/Core/2026-07-17 - README - RUN ORDER.md
|
|
57
|
+
- dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql
|
|
58
|
+
- dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql
|
|
59
|
+
- dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql
|
|
60
|
+
- dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql
|
|
61
|
+
- dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql
|
|
62
|
+
- dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql
|
|
57
63
|
related:
|
|
58
64
|
- ../../_underscore/features/surface-resolver.md
|
|
59
65
|
---
|
|
@@ -403,6 +409,17 @@ rule resumes.
|
|
|
403
409
|
earlier `2026-06-25c` / `2026-06-29c` / `2026-06-25d` surface seeds **likely share the same
|
|
404
410
|
anti-pattern** and need auditing. Core→Core seeds are fine (same cluster). Also still open:
|
|
405
411
|
re-seeding NYCHH/Prudential/SPGlobal (deferred).
|
|
412
|
+
- **A `VISIBILITY_RULE`/`ENABLED_RULE` override row can exist in the DB with the CORRECT `c_longValue`
|
|
413
|
+
yet be silently ignored because its `attribute` coerced to `''`.** The `SurfaceOverrides.attribute`
|
|
414
|
+
ENUM must be widened per client (`Client/2026-07-15 - SurfaceOverridesAttributeRuleValues.sql`)
|
|
415
|
+
**before** any rule-override insert. If a client (this session: **Quad**) never ran it, MySQL
|
|
416
|
+
**non-strict mode silently coerces** the invalid `VISIBILITY_RULE`/`ENABLED_RULE` value to `''`
|
|
417
|
+
(empty string) on INSERT — the row lands with the right `c_longValue` but `attribute=''`, so the
|
|
418
|
+
resolver ignores it and the rule never ships. **Symptom:** the override row is visibly present in the
|
|
419
|
+
DB (correct `c_longValue`) but `enabledRule`/`visibilityRule` is absent or wrong in the
|
|
420
|
+
`/surfaces/meta` API response. **Fix:** run the ENUM-widening file for the client, DELETE the junk
|
|
421
|
+
`attribute=''` rows, then re-insert. (`NOT EXISTS` guards keyed on `attribute` never match the junk
|
|
422
|
+
rows, so duplicates also pile up.)
|
|
406
423
|
- **Display-section visibility cannot be per-role-overridden without an element.** The override
|
|
407
424
|
cascade overrides **ELEMENTS only**, never a surface's own `isVisible`. The 5 display-toggle SECTION
|
|
408
425
|
surfaces therefore each carry a single `sectionVisibility` marker element so a Client override can
|
|
@@ -425,6 +442,16 @@ rule resumes.
|
|
|
425
442
|
override is added). **Open follow-up.**
|
|
426
443
|
|
|
427
444
|
## Change history
|
|
445
|
+
- 2026-07-21 — Added the sales-order **Approve enable-gate** per-client overrides (`Client_*/2026-07-21a`):
|
|
446
|
+
Compass/CC ADMIN `ENABLED_RULE` = `(pendingInitialApproval AND stepTwoAssigned) OR pendingApproval`
|
|
447
|
+
on surface 8 el 17 + surface 3 el 29 (rule JSON in `c_longValue`; Compass UPDATEs its existing
|
|
448
|
+
`07-17a` el-17 row then guarded-INSERTs el 29), plus a Compass/CC **approvals-gate enable**
|
|
449
|
+
(`2026-07-21b`) required so the FE `stepTwoAssigned` predicate doesn't fail open. Quad = client-wide
|
|
450
|
+
Approve `ENABLED_RULE` `{"type":"poNumberEntered"}` on both surfaces + a disabled-tooltip
|
|
451
|
+
`MessageTranslations` override. Sharpened the ENUM-coercion gotcha: a rule-override row can exist with
|
|
452
|
+
the correct `c_longValue` yet be silently ignored because `attribute` coerced to `''` (Quad never ran
|
|
453
|
+
the ENUM-widening file) — symptom is the row present in DB but the rule absent from the `/surfaces/meta`
|
|
454
|
+
response; fix = run the ENUM file, delete the `attribute=''` junk rows, re-insert. (apeterson)
|
|
428
455
|
- 2026-07-20 — Corrected the **row-actions surface** (`sales-order-listing-row-actions`, Core id 3;
|
|
429
456
|
distinct from the header-actions surface 9) default visibility and added the client opt-in.
|
|
430
457
|
`Core/2026-07-20e` narrows an earlier over-broad `2026-07-20d` hide — re-enables Approve (el 29) +
|
|
@@ -264,40 +264,70 @@ in [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md):
|
|
|
264
264
|
|
|
265
265
|
## Two FE rule engines (surface Tier-1 vs legacy named-rule) — do not conflate
|
|
266
266
|
|
|
267
|
-
There are **two** rule evaluators in this app
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
Tier-2 PHP capability method").
|
|
267
|
+
There are **two** rule evaluators in this app. As of 2026-07-21 the surface evaluator now has a
|
|
268
|
+
**sanctioned named-predicate escape hatch** (Option B — see below), so a business predicate like the
|
|
269
|
+
Approve gate can finally run on the surface path FE-side:
|
|
270
|
+
|
|
271
|
+
- **`src/surface/evaluateSurfaceRule.ts`** — the **surface Tier-1** evaluator. Base grammar is
|
|
272
|
+
`all`/`any`/`none` + leaf field ops (`eq`/`ne`/`in`/`nin`/`gt`/`gte`/`lt`/`lte`). It historically
|
|
273
|
+
**THREW on any `{type:…}` node** ("escalate to a Tier-2 PHP capability method"). It now instead
|
|
274
|
+
resolves `{type:<SurfaceNamedRule>}` nodes against a **`SURFACE_NAMED_RULES` map** (still throws
|
|
275
|
+
`"Unknown rule node"` on an unregistered type). `SurfaceNamedRule` (`src/surface/types.ts`) =
|
|
276
|
+
`"stepTwoAssigned" | "poNumberEntered"`. Both predicates read off the `{order}`-shaped surface
|
|
277
|
+
record: `stepTwoAssigned` reads `order.currentStage`/`order.stages`; `poNumberEntered` =
|
|
278
|
+
`!!order.purchaseOrderDetails.purchaseOrder` (mirrors the legacy `isPoComplete`). Evaluated
|
|
279
|
+
client-side against the loaded record.
|
|
274
280
|
- **`src/pages/SalesOrders/helpers/evaluateEnableRule.ts`** (`evaluateRule`) — the **legacy** engine:
|
|
275
|
-
`all`/`any`/`not` + `field` + **`{type}` NAMED_RULES
|
|
276
|
-
`ctx.currentStage`/`ctx.stages`).
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
`all`/`any`/`not` + `field` + **`{type}` NAMED_RULES** (`stepTwoAssigned` reading
|
|
282
|
+
`ctx.currentStage`/`ctx.stages`). Still present for the JSON-driven path.
|
|
283
|
+
|
|
284
|
+
### Option B — named predicates evaluated live on the FE (design deviation, intentional)
|
|
285
|
+
This **intentionally deviates** from the original "frozen grammar → escalate to a Tier-2 PHP
|
|
286
|
+
capability method" design note (see
|
|
287
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md)). Per **Option B** the Approve
|
|
288
|
+
step-two / PO gate is evaluated **live on the FE**, because the backend Tier-2 path is a no-op (no
|
|
289
|
+
model implements `resolveSurfaceActionState`, no backend `stepTwoAssigned`) and the rule is not in
|
|
290
|
+
legacy JSON. Named predicates (`stepTwoAssigned`/`poNumberEntered`) are the sanctioned FE-side escape
|
|
291
|
+
hatch for logic the frozen field grammar cannot express; the rule JSON itself lives in each client's
|
|
292
|
+
`SurfaceOverride` `c_longValue` (Core stays neutral).
|
|
293
|
+
|
|
294
|
+
### `resolveElementState` — the flag composition
|
|
295
|
+
`resolveElementState.ts` computes an element's `isEnabled` as **`staticFlag AND enabledRule AND
|
|
296
|
+
meta.surface`** (static flag AND the Tier-1/named-predicate `enabledRule` result AND any
|
|
297
|
+
server-computed `meta.surface` state). ⚠ **`SurfaceActionBar` carries its own DUPLICATE copy of
|
|
298
|
+
`resolveElementState`** — any change to the composition logic must be made in **both** places or the
|
|
299
|
+
action bar and the generic path will diverge.
|
|
285
300
|
|
|
286
301
|
### Approval-stage data plumbing for `stepTwoAssigned`
|
|
287
302
|
`stepTwoAssigned` needs the order's approval-stage data on the surface record. The surface record is
|
|
288
|
-
shaped `{ order }`, so the predicate reads **`order.currentStage` / `order.stages`**.
|
|
303
|
+
shaped `{ order }`, so the predicate reads **`order.currentStage` / `order.stages`**. Both surfaces
|
|
304
|
+
now carry it (2026-07-21):
|
|
289
305
|
|
|
290
|
-
- **Record-modal action bar (surface 8) — DONE.** `useSalesOrderRecordModalLayoutModel.tsx`
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
- **Listing row dropdown (surface 3) —
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
`currentStage`/`stages
|
|
297
|
-
`evaluateSurfaceRule` still needs the named-check support above.
|
|
306
|
+
- **Record-modal action bar (surface 8) — DONE.** `useSalesOrderRecordModalLayoutModel.tsx` fetches
|
|
307
|
+
`approvalCurrentStage`/`approvalStages`, merged onto the order (`orderWithPo`) as
|
|
308
|
+
`currentStage`/`stages` (parallel to the `purchaseOrderDetails` merge). Additive.
|
|
309
|
+
- **Listing row dropdown (surface 3) — DONE.** `useSalesOrderRowRecordState.ts` now calls
|
|
310
|
+
`useFetchApprovalStages` (gated on `enabled && approvalsEnabled`) and merges `currentStage`/`stages`
|
|
311
|
+
onto `record.order`, mirroring the record-modal view model. Both FE surfaces now carry
|
|
312
|
+
`order.currentStage`/`order.stages`/`order.purchaseOrderDetails`.
|
|
298
313
|
|
|
299
314
|
## Gotchas
|
|
300
315
|
|
|
316
|
+
- **A named predicate that FAILS OPEN silently enables a button when its data isn't fetched.**
|
|
317
|
+
`stepTwoAssigned` returns `true` when `order.currentStage` is `null` (a fail-OPEN default). So if the
|
|
318
|
+
surface's approval-stage data is not plumbed in (e.g. `approvalsEnabled=false`, so
|
|
319
|
+
`useFetchApprovalStages` never runs, or the row-path merge is missing), the predicate resolves `true`
|
|
320
|
+
and the Approve button is **silently enabled** regardless of stage. A named predicate's default
|
|
321
|
+
branch is load-bearing — decide fail-open vs fail-closed deliberately, and confirm the data it reads
|
|
322
|
+
is actually fetched on **every** surface that uses the rule (surface 8 AND surface 3). This is why
|
|
323
|
+
the Compass/CC approvals-gate marker (`approvalsEnabled`) must be enabled per client — without it the
|
|
324
|
+
stage data never fetches and `stepTwoAssigned` fails open.
|
|
325
|
+
- **Surface meta is cached `staleTime: Infinity` / `gcTime: Infinity` for the whole session
|
|
326
|
+
(`useFetchSurfaceMeta.ts`).** After ANY `SurfaceOverride` / `MessageTranslation` DB change, a **hard
|
|
327
|
+
reload / re-login is required** or the browser keeps the pre-change meta for the entire session. This
|
|
328
|
+
repeatedly presents as "I ran the SQL but nothing changed" during debugging — the SQL is fine; the
|
|
329
|
+
meta is cached. (Server-side raw-SQL surface edits ALSO need the on-disk resolver cache cleared — see
|
|
330
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md); both caches must be busted.)
|
|
301
331
|
- **The single-meta envelope can hide the real bundle behind a DECOY `elements: []`.** The V2 envelope
|
|
302
332
|
can arrive as `{ surfaces: { meta: <real-bundle> }, elements: [] }` — a decoy empty `elements: []`
|
|
303
333
|
sits at the top level with **no `surface` descriptor**. A `looksLikeBundle` check matching on
|
|
@@ -394,6 +424,20 @@ shaped `{ order }`, so the predicate reads **`order.currentStage` / `order.stage
|
|
|
394
424
|
treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
|
|
395
425
|
|
|
396
426
|
## Change history
|
|
427
|
+
- 2026-07-21 — **Option B: moved the Approve enable-gate onto the surface FE path.** Added a
|
|
428
|
+
`SURFACE_NAMED_RULES` map + a `{type:SurfaceNamedRule}` branch to `evaluateSurfaceRule.ts`
|
|
429
|
+
(`SurfaceNamedRule` = `"stepTwoAssigned"|"poNumberEntered"` in `types.ts`) — the sanctioned FE-side
|
|
430
|
+
escape hatch for logic the frozen field grammar can't express, an intentional deviation from the
|
|
431
|
+
original "frozen grammar → escalate to Tier-2 PHP" note (backend Tier-2 is a no-op). `stepTwoAssigned`
|
|
432
|
+
reads `order.currentStage`/`order.stages`; `poNumberEntered` = `!!order.purchaseOrderDetails.purchaseOrder`.
|
|
433
|
+
Plumbed approval-stage data onto the **row-actions** surface (surface 3): `useSalesOrderRowRecordState.ts`
|
|
434
|
+
now calls `useFetchApprovalStages` (gated on `enabled && approvalsEnabled`) + merges `currentStage`/`stages`
|
|
435
|
+
onto `record.order` (previously PENDING; record-modal surface 8 was already done). Documented that
|
|
436
|
+
`resolveElementState` composes `isEnabled = staticFlag AND enabledRule AND meta.surface` and that
|
|
437
|
+
`SurfaceActionBar` holds a DUPLICATE copy. Gotchas added: (1) `stepTwoAssigned` FAILS OPEN when
|
|
438
|
+
`currentStage` is null → silently enables the button if stage data isn't fetched (hence the per-client
|
|
439
|
+
approvals-gate marker); (2) surface meta is `staleTime/gcTime: Infinity` so a DB override change needs a
|
|
440
|
+
hard reload/re-login ("ran the SQL but nothing changed"). (apeterson)
|
|
397
441
|
- 2026-07-21 — Fixed the surface-driven **status badge dot rendering colorless** in the sales-order
|
|
398
442
|
record-modal header: the resolved bundle ships vocabulary color TOKEN SLUGS
|
|
399
443
|
(`color.status.*`/`bg.status.*`) but its `theme` map is empty (`{}`), so `resolveThemeToken` returns
|
package/package.json
CHANGED