toga-ai 1.0.778 → 1.0.780
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/acl-permission-chain.md +20 -0
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +34 -1
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +38 -2
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +16 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +60 -0
- package/knowledge/2.0/apps/dbchanges2/workflows/targeting-the-right-database.md +76 -14
- package/knowledge/2.0/apps/toga-blox/features/blox-authoring-defects.md +29 -1
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga2-commerce/features/catalog-cache-freshness.md +167 -21
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +4 -2
- package/knowledge/2.0/apps/toga25-supply/features/column-visibility.md +101 -4
- package/knowledge/2.0/apps/toga25-supply/features/persisted-query-cache.md +211 -0
- package/knowledge/2.0/apps/toga25-supply/features/toolbar-button-chrome.md +92 -0
- package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +190 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nychh/profile.md +8 -1
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | A **proposal / handoff recommendation** (not implemented) that the Surface layer's *authoring* model move off hand-authored SQL against the `SurfaceOverrides` E | _underscore/Model/Core/Surface.php, _underscore/Model/Client/SurfaceOverride.php |
|
|
6
6
|
| [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
|
|
7
|
-
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, _underscore/Model/Core/Page.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql |
|
|
7
|
+
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, _underscore/Model/Core/Page.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, dbchanges2/Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql |
|
|
8
8
|
| [Address Uniqueness Normalization (unit identifier + 5-digit ZIP comparison)](features/address-uniqueness-normalization.md) | When a business rule says *"only one X per physical address"*, comparing address rows field-for-field does **not** work: the same dwelling is spelled many diffe | _underscore/Model/Rate/Entitlement.php |
|
|
9
9
|
| [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php, _underscore/Component/Library/Carriers/Usps/Usps.php |
|
|
10
10
|
| [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, _underscore/Component/Api/Clickup/Clickup.php |
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql
|
|
23
23
|
- dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql
|
|
24
24
|
- dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql
|
|
25
|
+
- dbchanges2/Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql
|
|
25
26
|
---
|
|
26
27
|
|
|
27
28
|
## Summary
|
|
@@ -97,6 +98,17 @@ Two distinct failure shapes, and the second is a page-killer:
|
|
|
97
98
|
`undefined`. Code that checks `=== null` will not detect it.
|
|
98
99
|
- **A requested JOINED field with no grant fails the ENTIRE request with `EZ-2`.** One missing grant
|
|
99
100
|
on one joined column therefore blanks every record the page asked for — not just that column.
|
|
101
|
+
- **⚠ An ungranted `uuid` fails the ENTIRE request too — even though you never asked for it.**
|
|
102
|
+
`uuid` is V2's `IDENTIFIER_FIELD` (`api2 Component/Api/V2/V2.php:13`) and is **force-added to every
|
|
103
|
+
record read**, so it is always "requested" whether or not it appears in your `fields=` list. If the
|
|
104
|
+
calling role has no `AclFieldPermissions` grant on that record's `uuid` RecordField, the response is
|
|
105
|
+
**403 `EZ-2`** naming `"fields": ["uuid"]`. This is the opposite of the first bullet: an ordinary
|
|
106
|
+
ungranted field is silently dropped, `uuid` is fatal.
|
|
107
|
+
|
|
108
|
+
**Rule: grant the `uuid` RecordField alongside every field you actually read.** Seen 2026-09-03 on
|
|
109
|
+
`/transfer-order-statuses` — the fetch asked only for `slug,name,colorHex` and still 403'd on
|
|
110
|
+
`uuid`. The error text is self-diagnosing here: an `EZ-2` whose `fields` list contains a field you
|
|
111
|
+
never sent is this.
|
|
100
112
|
|
|
101
113
|
**Two practices follow directly:**
|
|
102
114
|
|
|
@@ -652,6 +664,14 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
652
664
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
653
665
|
|
|
654
666
|
## Change history
|
|
667
|
+
- 2026-09-04 — ⚠ Recorded that an **ungranted `uuid` 403s the whole request**, unlike an ordinary
|
|
668
|
+
ungranted field which is silently dropped: `uuid` is V2's `IDENTIFIER_FIELD`
|
|
669
|
+
(`api2 Component/Api/V2/V2.php:13`) and is force-added to **every** record read, so a role with no
|
|
670
|
+
`AclFieldPermissions` row on that record's `uuid` RecordField gets `EZ-2` listing
|
|
671
|
+
`"fields": ["uuid"]` even when `uuid` was never requested. Hit on `/transfer-order-statuses` while
|
|
672
|
+
wiring the transfer-order status badge (the fetch asked only for `slug,name,colorHex`). Rule: grant
|
|
673
|
+
the `uuid` RecordField alongside the fields you read; and an `EZ-2` naming a field you never sent
|
|
674
|
+
is this. (apeterson)
|
|
655
675
|
- 2026-09-04 — Recorded the **custom-field silent-omission trap** after a production incident (Rate
|
|
656
676
|
AIG): a `c_` field with no `AclCustomFieldPermissions` row for the calling role is **dropped from a
|
|
657
677
|
GET response with HTTP 200** and a correct `totalRecordCount` when `fields=` is not sent — the
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-03
|
|
10
10
|
owners: [jcardinal, apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Language.php
|
|
@@ -155,6 +155,32 @@ boolean rows; the item-record modal (the first boolean consumer) exposed the gap
|
|
|
155
155
|
`_underscore`, so it takes effect only once the framework branch is deployed (framework is pulled at
|
|
156
156
|
deploy).
|
|
157
157
|
|
|
158
|
+
### 🚨 A message key inside `config` JSON is NEVER resolved — copy must ride a `*MessageId` column
|
|
159
|
+
|
|
160
|
+
`_buildBundle`/`_serializeElement` build `bundle.messages` by scanning **exactly six columns** on
|
|
161
|
+
`SurfaceElements` — `labelMessageId`, `tooltipMessageId`, `placeholderMessageId`,
|
|
162
|
+
`disabledTooltipMessageId`, `trueMessageId`, `falseMessageId` (`Surface.php` ~L600). **`config` is
|
|
163
|
+
passed through untouched and is never scanned for message keys.** A key written into `config`
|
|
164
|
+
therefore ships to the frontend with **no value behind it**, and the screen renders the raw key.
|
|
165
|
+
|
|
166
|
+
This is a load-bearing constraint on how *any* surface-driven copy is modelled: **one string = one
|
|
167
|
+
`SurfaceElement` message column.** There is no way to attach copy to a nested `config` blob.
|
|
168
|
+
|
|
169
|
+
**Real case (2026-09-03).** The Columns modal rendered `common.columns.modal.apply` as its Apply
|
|
170
|
+
button label. `Core/2026-08-28b` had put the seven `common.columns.modal.*` keys inside the
|
|
171
|
+
`columnsButton` element's `config.modal` — data that looks right and can never resolve. The chosen
|
|
172
|
+
fix was **not** to teach the resolver to scan `config` (that widens the message-resolution contract
|
|
173
|
+
for every surface); it was to give the modal **its own MODAL Surface** so every string rides a real
|
|
174
|
+
`*MessageId` on its own element and the existing collector resolves it with **zero `_underscore`
|
|
175
|
+
change**. See
|
|
176
|
+
[column-visibility](../../toga25-supply/features/column-visibility.md).
|
|
177
|
+
|
|
178
|
+
**Diagnostic order.** When the FE shows a raw message key, first check whether the key rides a
|
|
179
|
+
`*MessageId` column — *before* suspecting an unrun seed. The resolved bundle in the API response is
|
|
180
|
+
the proof: if the element is present but `bundle.messages` has no entry for the key, the seed ran and
|
|
181
|
+
the modelling is wrong. (This session's first diagnosis — "the seed hasn't run" — was wrong, and the
|
|
182
|
+
pasted API response is what disproved it.)
|
|
183
|
+
|
|
158
184
|
## FIELD elements bound to a RecordField — see the dedicated doc
|
|
159
185
|
|
|
160
186
|
A `renderType = FIELD` element with a non-null `recordFieldId` gets `Core.RecordFields` metadata and
|
|
@@ -755,6 +781,13 @@ full stop. See
|
|
|
755
781
|
match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
756
782
|
|
|
757
783
|
## Change history
|
|
784
|
+
- 2026-09-03 — Recorded a **hard constraint on message resolution**: the resolver collects
|
|
785
|
+
`bundle.messages` from **only** the six `*MessageId` columns on `SurfaceElements`; a message key
|
|
786
|
+
placed inside `config` JSON is **never** resolved and renders raw on screen. Found when the Columns
|
|
787
|
+
modal displayed `common.columns.modal.apply`; `Core/2026-08-28b` had seeded those keys into
|
|
788
|
+
`config.modal`. Fixed in data, not in the framework — the modal became its own MODAL Surface
|
|
789
|
+
(Core 60) so each string rides a real `*MessageId`. Also recorded the diagnostic order: a raw key
|
|
790
|
+
means "check the modelling" before "check whether the seed ran". (apeterson)
|
|
758
791
|
- 2026-09-01 — Split the new **FIELD ↔ RecordField binding** capability out into its own doc
|
|
759
792
|
([surface-field-recordfield-binding](surface-field-recordfield-binding.md)) rather than growing this
|
|
760
793
|
one: for a `renderType=FIELD` element with a `recordFieldId`, `_buildBundle` now folds on
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
|
|
13
13
|
| [/auth/login resolves the client from the email domain, not the Bearer token (cross-client user path)](features/login-cross-client-user-resolution.md) | `POST /v2/auth/login` (email/password user login) can silently swap the target client mid-request. | api2/Component/Api/V2/V2.php, _underscore/String.php |
|
|
14
14
|
| [Nested FK object embedding is gated by the CHILD record's own ACL](features/nested-fk-acl-embedding.md) | When the V2 JSON engine serializes a foreign-key field into a **nested object** (in `getFullModelData()`, ~V2.php L6016-6060), it re-checks the **child** record | api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql |
|
|
15
|
-
| [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ContactEmailAddress.php |
|
|
15
|
+
| [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ContactEmailAddress.php, _underscore/Model/Client/TransferOrderStage.php |
|
|
16
16
|
| [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Query.php |
|
|
17
17
|
| [V2 Request Logging & Where Requests Land (client vs core log DB)](features/request-logging.md) | The V2 engine logs **every inbound request** — success *and* failure, with response code and payload — and routes each log entry to the **client** log or the ** | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/Logs/Api.php, _underscore/Model/Core/Logs/Api.php |
|
|
18
18
|
| [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
|
|
@@ -6,11 +6,12 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["bala", "mhammontree", "tcox"]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: ["bala", "mhammontree", "tcox", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Client/ContactEmailAddress.php
|
|
14
|
+
- _underscore/Model/Client/TransferOrderStage.php
|
|
14
15
|
related:
|
|
15
16
|
- ../architecture.md
|
|
16
17
|
- ../../toga2-supply/features/currency-amount-lines-editor.md
|
|
@@ -311,6 +312,33 @@ completely invisible to the UI — see
|
|
|
311
312
|
Same family as the 201-with-dropped-data case above: **a success status from V2 proves only that the
|
|
312
313
|
parent was written.** Assert child row counts, or write children explicitly.
|
|
313
314
|
|
|
315
|
+
## ⚠ Not every record can be linked by `uuid` — read `searchableIdentifierFields`
|
|
316
|
+
|
|
317
|
+
`{ uuid }` is the house convention for a nested FK, and it works so consistently that it reads like a
|
|
318
|
+
platform guarantee. **It is not.** A record is linkable only by the fields flagged as identifiers,
|
|
319
|
+
and a handful of lookup tables have **no `uuid` identifier at all**. Sending `{ uuid }` to one of
|
|
320
|
+
those is a **400 `EV-12`**, not a silent miss.
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
// WRONG — TransferOrderStages has no uuid identifier
|
|
324
|
+
transferOrderStage: { uuid: "…" }
|
|
325
|
+
// → 400 EV-12, "searchableIdentifierFields": ["id","c_netsuiteInternalTransferOrderStatus"]
|
|
326
|
+
|
|
327
|
+
// CORRECT
|
|
328
|
+
transferOrderStage: { id: 7 }
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- **The error body is self-diagnosing — read `searchableIdentifierFields` first.** It lists exactly
|
|
332
|
+
what that record accepts. No DB query needed.
|
|
333
|
+
- **When you fall back to `id`, resolve it at request time by a stable natural key (name/slug), never
|
|
334
|
+
hardcode it.** `id` values differ per tenant database, so a literal that works in one client DB
|
|
335
|
+
points at a different row (or nothing) in another. The transfer-order create modal looks its
|
|
336
|
+
opening stage up by name `"Pending"` on each request for exactly this reason.
|
|
337
|
+
- **The other EV-12 flavor is different.** `searchableIdentifierFields: ["id"]` on a nested FK
|
|
338
|
+
resolved *through* a custom field means a `c_` field is missing `isIdentifier = 1` — that is a
|
|
339
|
+
metadata bug to fix (see above), whereas "this record genuinely has no `uuid`" is a payload shape
|
|
340
|
+
to accept.
|
|
341
|
+
|
|
314
342
|
## Gotcha
|
|
315
343
|
|
|
316
344
|
- **Nested write with only a non-identifier field silently creates duplicates.** This is a
|
|
@@ -322,6 +350,14 @@ parent was written.** Assert child row counts, or write children explicitly.
|
|
|
322
350
|
|
|
323
351
|
## Change history
|
|
324
352
|
|
|
353
|
+
- 2026-09-04 — ⚠ Recorded that **`{ uuid }` is a convention, not a guarantee**: some lookup records
|
|
354
|
+
(e.g. `TransferOrderStages`) have **no `uuid` identifier**, so a nested `{ uuid }` is a hard 400
|
|
355
|
+
`EV-12` naming `searchableIdentifierFields: ["id","c_netsuiteInternalTransferOrderStatus"]`. Read
|
|
356
|
+
that list first — it is self-diagnosing — and when falling back to `{ id }`, resolve the id at
|
|
357
|
+
request time from a stable natural key (name/slug), because ids differ per tenant DB. Distinguished
|
|
358
|
+
this from the existing `searchableIdentifierFields: ["id"]` flavor, which is a missing
|
|
359
|
+
`isIdentifier` on a `c_` field. Found building the transfer-order create POST. (apeterson)
|
|
360
|
+
|
|
325
361
|
- 2026-08-26 — Added: **the UPDATE path sets status 200 unconditionally after its children loop**, so
|
|
326
362
|
a nested `PUT` reports success even when a child write failed (unique-key rejection, ACL denial).
|
|
327
363
|
Any parent-with-children editor that needs to know whether the children landed must write them
|
|
@@ -4,7 +4,7 @@
|
|
|
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
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/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.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 | dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, 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 |
|
|
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 | dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, 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, dbchanges2/Core/2026-09-03c - ColumnsModalSurfaceSeed.sql, dbchanges2/Core/2026-09-03d - ColumnsModalRetireConfigModal.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, _underscore/Model/Client/RecordFieldSetting.php, _underscore/Model/Client/ClientRecordFieldSetting.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 |
|
|
@@ -278,6 +278,17 @@ concluding a migration is production-safe.
|
|
|
278
278
|
**`(id, uuid, dtCreated, messageKey, name, defaultValue)`** — a guess of `slug` / `message`
|
|
279
279
|
(inferred from a sibling's values row) died on `Unknown column 'slug'`. When in doubt,
|
|
280
280
|
`SHOW COLUMNS FROM <Table>` costs one round-trip.
|
|
281
|
+
- **⚠ Rewriting a `VALUES` seed as `INSERT … SELECT` silently FLATTENS per-row columns.** The rewrite
|
|
282
|
+
is the normal way to add a `NOT EXISTS` guard, and it is easy to carry one row's value into the
|
|
283
|
+
constant list for every row. Caught 2026-09-03: converting a `Core.SurfaceElements` seed hardcoded
|
|
284
|
+
`'NEUTRAL'` as the `variant` for all 11 elements, losing the `PRIMARY` / `SECONDARY` ones. Nothing
|
|
285
|
+
errors — the enum accepts it. **Diff the rewritten file column-by-column against the original
|
|
286
|
+
`VALUES` rows before running it**, and treat any column that differed between rows as suspect.
|
|
287
|
+
- **⚠ A scalar sub-select for a foreign key writes `NULL` when it misses — it does not fail.**
|
|
288
|
+
`(SELECT id FROM Core.Messages WHERE messageKey = '…')` returns `NULL` if the key was never seeded,
|
|
289
|
+
and a nullable FK column takes it, leaving a structurally broken row that looks inserted. Prefer a
|
|
290
|
+
`JOIN` (so a missing lookup inserts **no row** at all) or add the lookup to the guard's
|
|
291
|
+
`WHERE EXISTS`.
|
|
281
292
|
- **`Core.RecordFields` has NO `sql` column.** A calculated field's SQL is **not** stored in
|
|
282
293
|
metadata — it lives in the `_underscore` model as a `FIELD_SQL` static method. Do not go looking
|
|
283
294
|
for a migration to change a calculated field's expression; there is none. See
|
|
@@ -288,7 +299,11 @@ concluding a migration is production-safe.
|
|
|
288
299
|
`SEQ_IN_INDEX = 1`**, not on `INDEX_NAME` — an existing index under a different name goes
|
|
289
300
|
undetected by a name-based guard and you add a duplicate. Found while sizing (and then dropping)
|
|
290
301
|
the Compass `Users` search indexes. (ajean)
|
|
291
|
-
|
|
302
|
+
- 2026-09-03 — Two silent-failure traps found self-auditing a `Core.SurfaceElements` seed against the
|
|
303
|
+
team rules: (1) rewriting a `VALUES` seed into `INSERT … SELECT` to add the `NOT EXISTS` guard
|
|
304
|
+
**flattened** the per-row `variant` to a constant `'NEUTRAL'`, dropping `PRIMARY`/`SECONDARY` with
|
|
305
|
+
no error; (2) a scalar sub-select used for a message FK writes `NULL` rather than failing when the
|
|
306
|
+
key is missing. Both leave a row that looks inserted and is wrong. (apeterson)
|
|
292
307
|
- 2026-09-01 — Added the **fan-out rollback rule**: a `Client/` grant migration that fans out across
|
|
293
308
|
every tenant must use **hardcoded v4 uuid literals**, not the generator, so its rollback
|
|
294
309
|
(`DELETE … WHERE uuid IN (…)`) can only touch rows that file created. A natural-key rollback on
|
|
@@ -101,10 +101,13 @@ files:
|
|
|
101
101
|
- dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql
|
|
102
102
|
- dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql
|
|
103
103
|
- dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql
|
|
104
|
+
- dbchanges2/Core/2026-09-03c - ColumnsModalSurfaceSeed.sql
|
|
105
|
+
- dbchanges2/Core/2026-09-03d - ColumnsModalRetireConfigModal.sql
|
|
104
106
|
related:
|
|
105
107
|
- ../../_underscore/features/surface-resolver.md
|
|
106
108
|
- ../../_underscore/features/sales-order-denial-reason.md
|
|
107
109
|
- ../../toga25-supply/features/transfer-orders-page.md
|
|
110
|
+
- ../../toga25-supply/features/column-visibility.md
|
|
108
111
|
---
|
|
109
112
|
|
|
110
113
|
## What it is
|
|
@@ -1292,7 +1295,64 @@ before your own run — that handoff is a snapshot, and the migrations that cons
|
|
|
1292
1295
|
uncommitted at the time. See
|
|
1293
1296
|
[Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
|
|
1294
1297
|
|
|
1298
|
+
## A MODAL that needs surface-driven copy becomes its OWN Surface (`columns-modal`, Core 60, 2026-09-03)
|
|
1299
|
+
|
|
1300
|
+
**Rule: never park copy in `config` JSON.** The resolver collects `bundle.messages` from **only** the
|
|
1301
|
+
six `*MessageId` columns on `SurfaceElements`; `config` is passed through untouched and is never
|
|
1302
|
+
scanned. A message key seeded into `config` therefore has no value behind it and the screen renders
|
|
1303
|
+
the raw key. `Core/2026-08-28b` had put the seven `common.columns.modal.*` keys inside the
|
|
1304
|
+
`columnsButton` element's `config.modal` — data that looks right and can never resolve. See
|
|
1305
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md).
|
|
1306
|
+
|
|
1307
|
+
The fix is a **data** fix, not a framework fix: `Core/2026-09-03c - ColumnsModalSurfaceSeed.sql`
|
|
1308
|
+
creates a shared `columns-modal` MODAL surface where **every string rides its own element's
|
|
1309
|
+
`labelMessageId`**, which the existing collector already resolves.
|
|
1310
|
+
|
|
1311
|
+
- **Surface 60** `columns-modal`; **Messages 293–298**; **SurfaceElements 222–232** (11 elements,
|
|
1312
|
+
`config.role` = `shownSection`, `hideAll`, `hiddenSection`, `showAll`, `reorder`, `pinToggle`,
|
|
1313
|
+
`visibilityToggle`, `reset`, `cancel`, `apply`, `close`).
|
|
1314
|
+
- **Elements are matched by `config.role`** — `SurfaceElements` has no `slug` column and the bundle
|
|
1315
|
+
exposes only a uuid as `key`. Same convention as `filterButton` / `columnsButton`.
|
|
1316
|
+
- **Element visibility gates CAPABILITY, not just chrome.** `isVisible = 0` on `pinToggle` removes
|
|
1317
|
+
pinning; on `reorder` removes drag-to-reorder. A tenant switches a feature off from data — the same
|
|
1318
|
+
marker-element idea as the Talos feature switch above, applied inside a shared component.
|
|
1319
|
+
|
|
1320
|
+
### Split the seed from the cleanup so the rollout is order-independent
|
|
1321
|
+
|
|
1322
|
+
Retiring the dead `config.modal` is a **second** file, `Core/2026-09-03d -
|
|
1323
|
+
ColumnsModalRetireConfigModal.sql`, which must run **only after toga25-supply is deployed**. With the
|
|
1324
|
+
two split, either order is safe: seed-then-deploy leaves a harmless unused `config`, and
|
|
1325
|
+
deploy-then-seed is the normal path. Putting both in one file would make the migration a
|
|
1326
|
+
deploy-blocker.
|
|
1327
|
+
|
|
1328
|
+
> ⚠ Neither file has been run on **any** environment yet (as of 2026-09-03).
|
|
1329
|
+
|
|
1330
|
+
### Id block consumed — and PROD did not match sandbox
|
|
1331
|
+
|
|
1332
|
+
The first id choice **collided on production**: `Messages` 289–294 had four already taken
|
|
1333
|
+
(`MAX(id)` = 292) and `Surfaces` **59 was taken**. Renumbered after querying the **production Core
|
|
1334
|
+
reader**. Sandbox Core differed substantially from prod — surface 59 was free on prod but taken on
|
|
1335
|
+
sandbox, and the `common.columns.modal.*` keys sit at **different ids per environment**.
|
|
1336
|
+
|
|
1337
|
+
This is the third confirmation of the rule above: verify a reserved block is free on **every**
|
|
1338
|
+
environment and take the **highest** `MAX(id)+1`, never the one in front of you, and never the repo.
|
|
1339
|
+
|
|
1340
|
+
**Handoff published (verify before your own run):** next `Core.Surfaces.id` = **61**, next
|
|
1341
|
+
`Core.SurfaceElements.id` = **233**, next `Core.Messages.id` = **299**. Both consuming files were
|
|
1342
|
+
still unrun at the time, so this is a reservation, not a deployed state.
|
|
1343
|
+
|
|
1295
1344
|
## Change history
|
|
1345
|
+
- 2026-09-03 — Recorded the rule that **a modal needing surface-driven copy becomes its own Surface**:
|
|
1346
|
+
the resolver reads messages only from `*MessageId` columns and never from `config`, so
|
|
1347
|
+
`Core/2026-08-28b`'s `config.modal` keys could never resolve and the Columns modal rendered raw
|
|
1348
|
+
keys. `Core/2026-09-03c` seeds a shared `columns-modal` MODAL surface (**60**, Messages **293–298**,
|
|
1349
|
+
SurfaceElements **222–232**) with one message column per string; elements are matched by
|
|
1350
|
+
`config.role`, and `isVisible = 0` on `pinToggle` / `reorder` gates those **capabilities** per
|
|
1351
|
+
tenant. Cleanup of the dead `config.modal` is deliberately a separate file (`Core/2026-09-03d`) that
|
|
1352
|
+
runs only after the FE deploy, so the rollout is safe in either order. The first id block collided
|
|
1353
|
+
on **production** (Messages `MAX(id)` = 292, Surfaces 59 taken) while sandbox looked free — third
|
|
1354
|
+
confirmation to verify a block on every environment. Handoff: next Surfaces = **61**,
|
|
1355
|
+
SurfaceElements = **233**, Messages = **299** (both files still unrun everywhere). (apeterson)
|
|
1296
1356
|
- 2026-09-03 — Flagged that the Compass / Compass Canada stacked-Total `CONFIG` override must **not**
|
|
1297
1357
|
gain a `"suffix":"+"`: blox renders a field `suffix` unconditionally while the stacked lease line
|
|
1298
1358
|
auto-hides on null/0, so a seeded `+` dangles on non-lease orders. The joiner is conditional FE code
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-04
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Config/production.ini
|
|
@@ -65,6 +65,20 @@ setpw() {
|
|
|
65
65
|
`MYSQL_PWD` keeps the password out of `ps` and history the same way a defaults file does — use it
|
|
66
66
|
when the client rejects `--defaults-extra-file` (some builds do).
|
|
67
67
|
|
|
68
|
+
**Check the password's line SHAPE without ever seeing the value.** Mask every character that is not
|
|
69
|
+
a space, tab, `;`, or `"` — you learn whether the value is quote-wrapped and whether an inline `;`
|
|
70
|
+
comment is in play, and the secret never reaches your screen or scrollback:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
awk -v S=database '$0=="["S"]"{f=1;next} /^\[/{f=0} f && /^password/{
|
|
74
|
+
k=$0; sub(/=.*/,"=",k); v=$0; sub(/^[^=]*=/,"",v); gsub(/[^ \t;"]/,"x",v);
|
|
75
|
+
print "[" k v "]"; exit}' Config/production.ini
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
On `production.ini` `[database]` this printed `[password = "xxxxxxxx"]` (checked 2026-09-04): the
|
|
79
|
+
double-quote wrap is real, and there is **no inline `;` comment** on password lines — so `setpw`
|
|
80
|
+
above, which strips quotes only, is correct as written and needs no comment handling.
|
|
81
|
+
|
|
68
82
|
**Keep the password out of your shell history and out of `ps`** by building a throwaway
|
|
69
83
|
credentials file from the ini instead of passing `-p` on the command line:
|
|
70
84
|
|
|
@@ -93,10 +107,25 @@ Delete the file when you are done.
|
|
|
93
107
|
|
|
94
108
|
## Resetting a shared environment from production (dump → drop → reload)
|
|
95
109
|
|
|
96
|
-
Used 2026-09-03 to bring sandbox-client's stale `Client_Nychh` TableViews back in line with prod
|
|
97
|
-
The developer runs every one of these; do not
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
Used 2026-09-03 to bring sandbox-client's stale `Client_Nychh` TableViews back in line with prod,
|
|
111
|
+
and again 2026-09-04 (`Core` + `Client_Nychh`). The developer runs every one of these; do not
|
|
112
|
+
execute them for them.
|
|
113
|
+
|
|
114
|
+
1. 🚨 **Inventory what the reset will DESTROY — before you drop anything.** A shared non-prod
|
|
115
|
+
environment is normally *ahead* of prod, because that is where changes get tried first, so a
|
|
116
|
+
prod→sandbox reload silently rolls that lead back. List every `dbchanges2` file dated after the
|
|
117
|
+
last prod release, plus anything parked in `git stash`, and decide which ones must be re-run.
|
|
118
|
+
Candidates from the 2026-09-04 sandbox-client reset, all *potentially* sandbox-only:
|
|
119
|
+
- `Core/` — 2026-09-01a, -01c, -01d, -02a, -02b, -03a, -03b, -03c, -03d
|
|
120
|
+
- `Client/` — 2026-09-01b, -01c, -02a
|
|
121
|
+
- `Client_Nychh/` — 2026-09-02a, -03a, -03b
|
|
122
|
+
- `dbchanges2` `stash@{0}` ("TO qty fulfilled core seed"), which is applied nowhere
|
|
123
|
+
|
|
124
|
+
**Verify every candidate against the environment — never assume the list.** After this reset
|
|
125
|
+
sandbox read `Core.Surfaces` = 54, which means some of those Core surface files were **already in
|
|
126
|
+
prod** and must not be re-run. The repo records intent, not deployed state — see
|
|
127
|
+
[Verifying whether a migration actually ran](./verifying-a-migration-ran.md).
|
|
128
|
+
2. **Dump each source database**, one file each — prod `Core` from
|
|
100
129
|
`reader1.core.database.togahub.com`, prod tenant from `reader1.client.database.togahub.com`:
|
|
101
130
|
```
|
|
102
131
|
mysqldump --single-transaction --quick --routines --triggers --events \
|
|
@@ -104,14 +133,17 @@ The developer runs every one of these; do not execute them for them.
|
|
|
104
133
|
```
|
|
105
134
|
`--set-gtid-purged=OFF` because RDS has GTID on; `--no-tablespaces` because the `admin` user
|
|
106
135
|
lacks `PROCESS` (otherwise **error 1227**).
|
|
107
|
-
|
|
136
|
+
3. 🚨 **`tail -1` every dump file and confirm it ends with `-- Dump completed` BEFORE you drop
|
|
108
137
|
anything.** A truncated dump loads without complaining. DNS dropped mid-session on a host that
|
|
109
138
|
had resolved seconds earlier (`ERROR 2005 Unknown MySQL server host`) — the dump just stops.
|
|
110
|
-
|
|
139
|
+
4. **Prove which environment each connection actually reached.** Better than `@@hostname`: check the
|
|
111
140
|
resolved IP — production resolves into **`10.201.x`**, sandbox into **`10.200.x`**.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
you intended to copy.
|
|
141
|
+
5. **Drop + create the target schema, then pipe the dump in.**
|
|
142
|
+
6. **Verify with a row count known to differ** (step 1 of *before you trust ANY tenant dump*,
|
|
143
|
+
above), against the schema you intended to copy.
|
|
144
|
+
7. **Re-run the step-1 migrations that prod did not already have, in `Core/` → `Client/` fan-out →
|
|
145
|
+
`Client_<Tenant>/` order.** Client files reference Core record ids, and the client work needs the
|
|
146
|
+
columns the `Client/` fan-out adds — any other order fails or half-applies.
|
|
115
147
|
|
|
116
148
|
### ⚠ Which `mysql` / `mysqldump` binary you are running is load-bearing
|
|
117
149
|
|
|
@@ -120,10 +152,23 @@ The developer runs every one of these; do not execute them for them.
|
|
|
120
152
|
`xamppfiles/bin/mysqldump` is **MariaDB 10.4** (rejects the flag);
|
|
121
153
|
`xamppfiles/mysql/bin/mysqldump` is **MySQL 8.0.44** (correct). Run `mysqldump --version` before
|
|
122
154
|
you trust it.
|
|
123
|
-
- **A shell alias
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
155
|
+
- 🚨 **A shell alias hijacks a remote connection — and the error names the wrong problem.** On an
|
|
156
|
+
XAMPP machine `~/.zshrc:31` defines
|
|
157
|
+
`alias mysql='/Applications/XAMPP/xamppfiles/mysql/bin/mysql -u root -p<pw> --socket=…'`. A later
|
|
158
|
+
`-u admin` on the command line wins for the **user**, but the alias's `-p<pw>` **overrides
|
|
159
|
+
`MYSQL_PWD`** — so every `mysql -h <remote-host> -u admin …` dies with
|
|
160
|
+
`ERROR 1045 (28000): Access denied for user 'admin'@'<your ip>' (using password: YES)`, *against
|
|
161
|
+
the correct remote host*. That reads as a rotated credential, a missing grant, a VPN problem, or
|
|
162
|
+
the double-quote trap above, and it is none of them. (The same alias can also silently talk to
|
|
163
|
+
localhost when its `--socket` wins — two symptoms, one cause.)
|
|
164
|
+
- **The tell: `mysqldump` to the SAME host with the SAME `MYSQL_PWD` succeeds while `mysql`
|
|
165
|
+
1045s.** Only `mysql` is aliased, not `mysqldump`. "Dump works, `mysql` denied" identifies the
|
|
166
|
+
alias immediately — check that before you go near the password extractor or the ini quoting.
|
|
167
|
+
- **Fix:** call the binary by full path
|
|
168
|
+
(`MYSQL=/Applications/XAMPP/xamppfiles/mysql/bin/mysql`). `\mysql` or `command mysql` also skip
|
|
169
|
+
alias expansion.
|
|
170
|
+
- An exact match with your local DB is evidence of a bad restore, not of a good one (see the
|
|
171
|
+
gotcha below).
|
|
127
172
|
|
|
128
173
|
## Gotchas
|
|
129
174
|
|
|
@@ -137,6 +182,12 @@ The developer runs every one of these; do not execute them for them.
|
|
|
137
182
|
makes the dump a point-in-time snapshot, so a live table keeps moving after it: `Units` read 8,627
|
|
138
183
|
on prod and 8,618 on the restored sandbox. Do not chase that; only a *category* difference
|
|
139
184
|
(missing tables, order-of-magnitude counts) means the restore was wrong.
|
|
185
|
+
- **The reference counts written down here go stale too.** After the 2026-09-04 reset,
|
|
186
|
+
sandbox-client read `Core.Surfaces` = **54** and `Client_Nychh.TransferOrders` = **1,932**,
|
|
187
|
+
against the prod figures previously recorded here — 53 (2026-09-03) and 1,927 (2026-08-31).
|
|
188
|
+
Prod itself moved on. Restored counts landing slightly **above** an older recorded prod number
|
|
189
|
+
is the expected pattern, not evidence of a bad restore. Re-read the count from the source at
|
|
190
|
+
reset time instead of comparing to a number in this doc.
|
|
140
191
|
- 🚨 **The `dbchanges2` folder name IS the target database — pipe each folder to its own DB.**
|
|
141
192
|
`Core/` → `Core`, `Client_<Name>/` → that one tenant, `Client/` → **every** tenant. Files reference
|
|
142
193
|
tables **unqualified** by design (the cluster-isolation rule), so running a `Core/` file against a
|
|
@@ -157,6 +208,17 @@ The developer runs every one of these; do not execute them for them.
|
|
|
157
208
|
[Verifying whether a migration actually ran](./verifying-a-migration-ran.md).
|
|
158
209
|
|
|
159
210
|
## Change history
|
|
211
|
+
- 2026-09-04 — Second sandbox-client reset (`Core` + `Client_Nychh`) from prod; the procedure held,
|
|
212
|
+
three gaps closed. Added a mandatory **step 1 — inventory what the reset will destroy** (a shared
|
|
213
|
+
non-prod environment is normally ahead of prod, so the reload rolls that lead back), with the
|
|
214
|
+
2026-09-04 candidate file list and the unapplied stash, plus a **step 7** to re-run them in
|
|
215
|
+
`Core/` → `Client/` → `Client_<Tenant>/` order. Sharpened the shell-alias bullet: the real symptom
|
|
216
|
+
is **`ERROR 1045` against the CORRECT remote host** (the alias's `-p<pw>` beats `MYSQL_PWD`), not a
|
|
217
|
+
silent localhost connection — and the tell is that `mysqldump` works while `mysql` is denied,
|
|
218
|
+
because only `mysql` is aliased. Added a value-safe way to inspect an ini password's line shape
|
|
219
|
+
(confirms the double-quote wrap and that there is no inline `;` comment, so `setpw` needs no
|
|
220
|
+
change). Refreshed the expected-drift numbers and noted that recorded reference counts age.
|
|
221
|
+
(apeterson)
|
|
160
222
|
- 2026-09-03 — Added the **reset-a-shared-environment-from-production** procedure (the `mysqldump`
|
|
161
223
|
flag set, `--set-gtid-purged=OFF` for RDS GTID, `--no-tablespaces` for the missing `PROCESS`
|
|
162
224
|
grant, the mandatory `tail -1` "`-- Dump completed`" check because a truncated dump loads
|
|
@@ -6,7 +6,7 @@ project: TOGa Blox
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-03
|
|
10
10
|
owners: [apeterson, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- toga-blox/src/components/BaseButton
|
|
@@ -135,6 +135,28 @@ declares, on class names `getStickyClass` never emits. Dead rules like this look
|
|
|
135
135
|
someone greps for the divider. Both were deleted 2026-08-31; grep the emitting code before trusting a
|
|
136
136
|
CSS rule, and check the token it names actually exists in a host theme.
|
|
137
137
|
|
|
138
|
+
### 6. Button HEIGHT is a literal with no token — a toolbar cannot be sized from the host theme
|
|
139
|
+
|
|
140
|
+
Defect 4 is about swapping variants; this is the shape of the same problem for a consumer who never
|
|
141
|
+
swaps anything. Measured 2026-09-03 in
|
|
142
|
+
`dist/components/BaseButton/BaseButton.module.css`:
|
|
143
|
+
|
|
144
|
+
- `.secondaryBtnAction` hard-codes **`height: 26px`** — **no `var(--…)`, so no host token can reach
|
|
145
|
+
it**.
|
|
146
|
+
- `.primaryBtn` declares **no height at all** (computes to ~25px).
|
|
147
|
+
- `font-size` **is** tokened (14px default).
|
|
148
|
+
|
|
149
|
+
**Consumer symptom.** One page toolbar renders its buttons at three different heights, and the
|
|
150
|
+
developer cannot fix it from `themeConfig.json` — the number they need to change is not exposed.
|
|
151
|
+
Nothing in the theme layer hints that geometry is off-limits, so the time is lost hunting the app's
|
|
152
|
+
own CSS.
|
|
153
|
+
|
|
154
|
+
**Fix direction.** Same as defect 5: convert both to `var(--baseButton-height, <existing literal>)`
|
|
155
|
+
(and give `.primaryBtn` the same base height), which is non-breaking because the fallback preserves
|
|
156
|
+
today's look. Until then a consumer must own the chrome: `toga25-supply` built its own
|
|
157
|
+
`ToolbarButton` rather than retheme tokens shared by ~all buttons in the app — see
|
|
158
|
+
[toolbar button chrome](../../toga25-supply/features/toolbar-button-chrome.md).
|
|
159
|
+
|
|
138
160
|
## Already-known authoring items (carried over from §22)
|
|
139
161
|
|
|
140
162
|
These are long-standing and were previously recorded only as consumer-side advice:
|
|
@@ -168,6 +190,12 @@ These are long-standing and were previously recorded only as consumer-side advic
|
|
|
168
190
|
time**, with no PR. Sequence auth fixes deliberately.
|
|
169
191
|
|
|
170
192
|
## Change history
|
|
193
|
+
- 2026-09-03 — Added styling defect **6: button height is a literal with no token**.
|
|
194
|
+
`.secondaryBtnAction` hard-codes `height: 26px` and `.primaryBtn` sets no height at all, so a
|
|
195
|
+
consumer cannot size a toolbar from the host theme and the cause is invisible from the theme layer.
|
|
196
|
+
Found building toga25-supply's page toolbar (three buttons, three heights); compensated by an
|
|
197
|
+
app-owned `ToolbarButton`, not by retheming shared tokens. Fix direction is defect 5's:
|
|
198
|
+
`var(--baseButton-height, <literal>)`, non-breaking via the fallback. (apeterson)
|
|
171
199
|
- 2026-08-31 — Added styling defect **5: hardcoded visual values in the shared stylesheets cannot be
|
|
172
200
|
removed by a consumer** — a literal has no host-token escape hatch. Worked example: the `supply`
|
|
173
201
|
table card shadow, which a toga25-supply design review needed gone; converted to
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
| [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
|
|
9
9
|
| [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | The Cart summary section (subtotal / shipping / tax / total) is **data-driven** from `cartData`. | toga2-commerce/src/pages/Cart/viewModel/useCartViewModel.ts, toga2-commerce/src/pages/Cart/CartPage.tsx, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/api/CartApi.ts |
|
|
10
10
|
| [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | The Cart page (`src/pages/Cart/`) is the most config-heavy page in `toga2-commerce`. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/pages/Cart/view/cartForm/CartFormSection.tsx, src/pages/Cart/view/cartForm/CartFormRenderer.tsx, src/pages/Cart/view/EditCart.tsx, src/pages/Cart/view/EditOrder.tsx, src/pages/Cart/viewModel/useEditOrderOrEditCartViewModel.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts, src/hooks/useAssignClientFields.ts |
|
|
11
|
-
| [Catalog cache freshness — the 24h persisted query cache, and how to opt a query out of it](features/catalog-cache-freshness.md) | TOGa Commerce runs a **single `QueryClient` with a 24-hour default `staleTime`**, and persists it to **`localStorage["commerce"]`** through `PersistQueryClientP | toga2-commerce/src/App.tsx, toga2-commerce/src/contexts/AuthContext.tsx, toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts |
|
|
11
|
+
| [Catalog cache freshness — the 24h persisted query cache, and how to opt a query out of it](features/catalog-cache-freshness.md) | TOGa Commerce runs a **single `QueryClient` with a 24-hour default `staleTime`**, and persists it to **`localStorage["commerce"]`** through `PersistQueryClientP | toga2-commerce/src/App.tsx, toga2-commerce/src/contexts/AuthContext.tsx, toga2-commerce/src/api/axiosInstance.ts, toga2-commerce/src/hooks/useAuthenticationFlow.ts, toga2-commerce/src/components/NavIcons/NavIconList.tsx, toga2-commerce/src/pages/BundleView/hooks/useBundleQueryData.ts, toga2-commerce/src/pages/Home/viewModel/useHomeViewModel.ts, toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts |
|
|
12
12
|
| [Category Tile Order (AssortmentItems.sortOrder) — merchandising a storefront category](features/category-tile-sort-order.md) | **"Move item X to the front of category Y" is a DATA change, not a code change.** The order of item tiles on a storefront category page is driven by exactly one | src/pages/Filter/api/FilterApi.ts, src/pages/Filter/viewModel/useFilterViewModel.ts, api2/Component/Api/V2/V2.php, toga2-supply/src/pages/Items/api/itemsApi.ts |
|
|
13
13
|
| [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/pages/Account/view/MySettingsView.tsx, src/contexts/helpers/getLoginSettings.ts, src/pages/Account/viewModel/useAccountViewModel.ts, src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/pages/Cart/api/CartApi.ts, src/hooks/useAuthenticationFlow.ts, src/contexts/AuthContext.tsx, src/pages/Login/viewModel/useLoginPageViewModel.ts, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx, src/components/NavIcons/NavIconItem.tsx, src/components/Submenus/AlertSubmenu.tsx, src/components/Submenus/types.ts, src/pages/Account/AccountPage.tsx, src/pages/Account/view/MyOrdersView.tsx, src/pages/GetSupport/GetSupportPage.tsx, src/pages/GetSupport/viewModel/useGetSupportViewModel.ts, src/queries/queries.ts, src/App.tsx, src/pages/Filter/FilterPage.tsx, src/pages/Filter/viewModel/FIELDS/COMPASS/ENGLISH/USER/FILTERPAGEFIELDS.json, src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/USER/HEADERFIELDS.json, src/components/AuthLayout/AuthLayout.tsx |
|
|
14
14
|
| [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | On the toga2-commerce **Cart** page, expedited shipping options (**"2nd Day EOB"** and **"Next Day Air"**) are only offered in the *Shipping Method* dropdown wh | toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/viewModel/FIELDS/shared/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/CartPage.tsx |
|