toga-ai 1.0.881 → 1.0.883
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/1.0/apps/tools/INDEX.md +1 -0
- package/knowledge/1.0/apps/tools/architecture.md +8 -0
- package/knowledge/1.0/apps/tools/features/email-queue-review.md +54 -0
- package/knowledge/2.0/apps/_underscore/features/email-send-pipeline.md +21 -6
- package/knowledge/2.0/apps/_underscore/features/email-template-sending.md +28 -3
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +16 -2
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +34 -1
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +30 -1
- package/knowledge/2.0/apps/toga-blox/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga-blox/features/environment-badge.md +70 -0
- package/knowledge/2.0/apps/toga25-supply/features/meta-driven-table-data.md +23 -1
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/nychh/features/transfer-order-notification-emails.md +8 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [Compass User & Persona Admin (tools /compass/users, /compass/personas)](features/compass-user-persona-admin.md) | Two pages in the Tools `developers` nav folder that let staff do, in a browser, the Compass requests that used to mean "email a developer, who writes a `dbchang |
|
|
10
10
|
| [Design Demo Admin](features/design-demo-admin.md) | SSO-gated Tools admin at `/design` that publishes self-contained "Claude Design" HTML exports as versioned demos into the `forward` repo over the GitHub REST AP |
|
|
11
11
|
| [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | Tools "Developers" folder — the Generate UUID and Generate Password tools (reference example of the one-tool-per-`mvc/<route>/get.php` pattern); open for the ge |
|
|
12
|
+
| [/emails Email Queue Review (Tools → 2.0 Logs_<Client>.Email)](features/email-queue-review.md) | Staff page to browse any 2.0 client's `Logs_<Client>.Email` queue and release/reject `HELD` review-hold emails. |
|
|
12
13
|
| [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only 1.0 Tools triage/curation screen (`/errors`) for the 2.0 Issue/Event error pipeline, reading the shared Core Logs DB via `db_toga2logs`; curates, |
|
|
13
14
|
| [GitHub Audit (monthly code-churn report)](features/github-audit.md) | Internal monthly code-churn report at `/developers/github-audit` (lines added/removed across every `agilantsolutions` repo for one month); open for the two-phas |
|
|
14
15
|
| [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | SSO-gated admin tool at `/email-migration/notify` that emails employees a digest of mail still hitting their legacy domains (asks senders to switch to `@togatec |
|
|
@@ -79,9 +79,17 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
|
|
|
79
79
|
|
|
80
80
|
## Gotchas
|
|
81
81
|
|
|
82
|
+
- **Bare `<header>` violated again (2026-09-23, /emails)** — use a `<div>` with a page class; see Critical rules.
|
|
83
|
+
- **The app-wide input rule outranks class selectors.** `style.css` ~line 769 styles inputs with five chained `:not()`; that specificity beats a page's class selector. A page restyling its inputs must repeat the same `:not()` chain under its page scope.
|
|
84
|
+
- **`.app-content` caps width at 960px** unless the page root has `.tool--wide`.
|
|
85
|
+
- **`.btn-secondary` and `.btn-secondary-outline` are RED (danger).** There is no neutral button class; do not use them for non-destructive actions.
|
|
86
|
+
- **CSRF is not a token.** `App_Page::validateCrossSiteRequestForgery` checks Origin/Referer only; POST handlers rely on it.
|
|
82
87
|
- **⚠ `tools` runs PHP 8, not 7.2 — check the vhost, not the framework label.** Despite being a 1.0 `App_` app, its vhost lives in the PHP 8 Apache (`DocumentRoot "C:/WWW/tools"`), and `App_Compass::tenantKey(mixed $key)` uses the `mixed` type hint (PHP 8.0+). The trap: `mixed` **passes** `php -l` on 7.2 (parsed as a class name) and only fatals at runtime — "Argument 1 passed to f() must be an instance of mixed, string given". So linting `tools` on 7.2 fails in the dangerous direction: it accepts code that would die.
|
|
83
88
|
- **Plaintext secrets were publicly web-readable (fixed 2026-07-24; rotation still owed).** `config.*.ini` (incl. `config.prod.ini`) holds plaintext production secrets (RDS master password, SMTP, Payeezy, NetSuite). The front controller routed through `index.php` on a `RewriteCond !-f` rule **only**, which is not access control — a direct `GET /config.production.ini` served the file **verbatim**. Fixed 2026-07-24 by adding `<FilesMatch>` `Require all denied` blocks (sensitive extensions incl. `.ini`, and any `config.*`) — see the [1.0 back-end security standard](../../standards/backend-php.md). **Action still owed:** rotate every secret in `config.production.ini` — the file was reachable, so treat all of it as compromised. (Location only — no values recorded here.)
|
|
84
89
|
- **Hardcoded GitHub Personal Access Token committed in plaintext (rotation owed).** `.ebextensions/020_setup_git_libraries.config` contains a GitHub PAT in plaintext, committed to the repo and therefore exposed in git history. **Action owed:** rotate the token and move it to an EB environment property (read via `getenv()`/`$_SERVER`/`$_ENV` per the deploy workflow's clear_env gotcha); a rotation does not un-expose git history. (Location only — no value recorded here.)
|
|
85
90
|
- **SSO initiation + replay defense are open items** — see `features/saml-sso-auth.md`.
|
|
86
91
|
- **~~Unproxied third-party polling from the browser~~ (resolved 2026-08-18).** The monitor column used to pull ~250 KB from OneUptime cross-origin every poll (per open tab) and hardcode the status-page ID in the HTML. It is now served by a same-origin server-side proxy `/v2/monitors/status/` (`App_Clickup_Monitors`) that fetches, reduces to ~1 KB and caches ~5s (APCu), with the status-page ID in config — one upstream fetch shared across all tabs, no cross-origin dependency. See `features/oneuptime-monitor-status-panel.md`. **Framework gotcha found building it (prod incident CV-2):** never call `curl_close()` in App_ code — it is deprecated in PHP 8.5 and the strict error handler promotes that `E_DEPRECATED` to a fatal (500). It is a no-op since PHP 8.0; drop it (or `unset($ch)`).
|
|
87
92
|
- **Framework-level Sentry error reporting is an open gap.** Prod printed "Sentry is not installed" because `sentry/sentry` was missing from `composer.json` (added `^4.10`; sibling apps use `^4.10`/`^4.19`). But even with Sentry in `vendor/`, the 1.0 framework does **not** auto-require `vendor/autoload.php` (the `App_Talos_S3` pattern `require_once`s it explicitly), so framework-level Sentry reporting for Tools still does not load. Any code path that needs a composer package must `require_once` the autoloader itself.
|
|
93
|
+
|
|
94
|
+
## Change history
|
|
95
|
+
- 2026-09-23 — Added gotchas from the /emails build: input :not chain, 960px cap/.tool--wide, red btn-secondary, Origin/Referer CSRF. (jcardinal)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: /emails Email Queue Review (Tools → 2.0 Logs_<Client>.Email)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: tools
|
|
5
|
+
project: Tools
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-23
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- tools/_/app/emails.php
|
|
13
|
+
- tools/mvc/emails/get.php
|
|
14
|
+
- tools/mvc/emails/post.php
|
|
15
|
+
- tools/_/app/nav.php
|
|
16
|
+
- tools/assets/css/style.css
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- ../../../../2.0/apps/_underscore/features/email-send-pipeline.md
|
|
20
|
+
- ../../../../2.0/apps/_underscore/features/email-template-sending.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
Staff page to browse any 2.0 client's `Logs_<Client>.Email` queue and release/reject `HELD`
|
|
26
|
+
review-hold emails. Nav: Developers group, Development Team persona only.
|
|
27
|
+
|
|
28
|
+
## Key files / entry points
|
|
29
|
+
|
|
30
|
+
- `tools/_/app/emails.php` — `App_Emails`: client list, queue queries, actions.
|
|
31
|
+
- `tools/mvc/emails/get.php` / `post.php` — page + actions.
|
|
32
|
+
- `tools/assets/css/style.css` — `.tool--emails` block.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
1. Client list from Core (`db_toga2core`). Queue queries go over `db_toga2logs` against one
|
|
37
|
+
`Logs_<Client>` db named from `Core.Clients.logDatabaseId`, checked against
|
|
38
|
+
`^Logs_[A-Za-z0-9]+$`. Never joins Core with Logs.
|
|
39
|
+
2. Status tabs with counts, search, pagination, split-pane list/detail.
|
|
40
|
+
3. Actions:
|
|
41
|
+
- **Approve** — guarded `UPDATE ... WHERE status='HELD'`, requires `affectedRows === 1`; sets
|
|
42
|
+
`PENDING`, `dtReviewed`, `reviewedBy` = staff email.
|
|
43
|
+
- **Reject** — `HELD` → `CANCELLED`, same guard.
|
|
44
|
+
- **Send copy** — new `PENDING` row + attachments via `INSERT ... SELECT`, PHP v4 uuid per row.
|
|
45
|
+
4. Deep link `/emails?client=&emailId=` is the target of `[_underscore] email_review_url`.
|
|
46
|
+
|
|
47
|
+
## Gotchas / known issues
|
|
48
|
+
|
|
49
|
+
- Preview is an `<iframe sandbox="" srcdoc>` — the **only** XSS barrier for stored email HTML.
|
|
50
|
+
Never add `allow-scripts`.
|
|
51
|
+
- CSRF is `App_Page` Origin/Referer check, not a token (see architecture).
|
|
52
|
+
|
|
53
|
+
## Change history
|
|
54
|
+
- 2026-09-23 — Created: browse queue, Approve/Reject held emails, Send copy. (jcardinal)
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: ["bala", "tcox", "dfranks", "apeterson"]
|
|
9
|
+
updated: 2026-09-23
|
|
10
|
+
owners: ["bala", "tcox", "dfranks", "apeterson", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Email.php
|
|
13
13
|
- _underscore/String.php
|
|
@@ -39,6 +39,9 @@ SMTP** using PHPMailer, then flips each row to `SENT` or `FAILED`. So a 2.0 emai
|
|
|
39
39
|
unless both `setClientIdentifier(...)` and a From address (`fromEmailAddress`) are set. The
|
|
40
40
|
`Logs_[Client]` databases live on a separate cluster (`production-logs-*`), so `send()` points
|
|
41
41
|
`DB_CLIENT_LOGS` at the correct `Logs_[Client]` schema before inserting.
|
|
42
|
+
- Setters (2026-09-23): `setIsHeld(true)` writes the row as `HELD` instead of `PENDING`;
|
|
43
|
+
`setEmailTemplateId()` / `setReviewOfEmailId()` fill the matching columns. `send()` returns the
|
|
44
|
+
**int `Logs_<Client>.Email` id**.
|
|
42
45
|
- `worker2/Worker/Infrastructure/Email/Send.php` — `Run()` fetches up to 100 `PENDING` rows for
|
|
43
46
|
the client, and `sendEmail()` transmits each via SES SMTP. Constants: `STATUS_PENDING`,
|
|
44
47
|
`STATUS_SENT`, `STATUS_FAILED`, `MAX_RETRIES`.
|
|
@@ -54,6 +57,17 @@ SMTP** using PHPMailer, then flips each row to `SENT` or `FAILED`. So a 2.0 emai
|
|
|
54
57
|
4. On success the row flips to `SENT`; on failure `retryCount` is incremented and the row is
|
|
55
58
|
retried on subsequent runs until `MAX_RETRIES` (3), after which it is marked `FAILED`.
|
|
56
59
|
|
|
60
|
+
### Statuses + review columns (2026-09-23)
|
|
61
|
+
|
|
62
|
+
`status` enum = `PENDING` / `SENT` / `FAILED` / `HELD` / `CANCELLED`. The Send cron reads only
|
|
63
|
+
`PENDING`, so `HELD` and `CANCELLED` rows are never sent (worker2 unchanged). Columns:
|
|
64
|
+
`emailTemplateId` (after `dtCreated`), `reviewOfEmailId` (self FK, `ON DELETE SET NULL` — links a
|
|
65
|
+
review copy to the held original), `dtReviewed` (after `dtSent`), `reviewedBy` (after
|
|
66
|
+
`failureReason`). Migration: `dbchanges2/Logs_Client/2026-09-23a - EmailReview.sql` — must run
|
|
67
|
+
**before** the `_underscore` deploy or every `send()` fails. Who writes `HELD`: see
|
|
68
|
+
[email-template-sending.md](email-template-sending.md) *Review hold*; who releases it: Tools
|
|
69
|
+
[/emails](../../../../1.0/apps/tools/features/email-queue-review.md).
|
|
70
|
+
|
|
57
71
|
### `send()` resolves the log database itself — from the clientIdentifier
|
|
58
72
|
|
|
59
73
|
`send()` does not take a database name. It resolves `Logs_<Client>` by joining
|
|
@@ -76,10 +90,8 @@ derive one from the other).
|
|
|
76
90
|
|
|
77
91
|
## Gotchas / known issues
|
|
78
92
|
|
|
79
|
-
- **It is a POLL, not an enqueue
|
|
80
|
-
|
|
81
|
-
`send()` only INSERTs a `PENDING` row (`Logs_<Client>.Email`, `status` enum
|
|
82
|
-
`PENDING`/`SENT`/`FAILED`), and `worker2/Worker/Infrastructure/Email/Send.php` loops the clients
|
|
93
|
+
- **It is a POLL, not an enqueue.** (The old docblock claiming `send()` "enqueues a Worker 2 job"
|
|
94
|
+
was corrected 2026-09-23.) `send()` only INSERTs a `PENDING` (or `HELD`) row, and `worker2/Worker/Infrastructure/Email/Send.php` loops the clients
|
|
83
95
|
and `SELECT`s `PENDING` rows. Two consequences: nothing is queued anywhere but the database, and
|
|
84
96
|
**a `PENDING` row written from a local machine is never delivered**, because the worker polls the
|
|
85
97
|
sandbox/prod clusters, not your laptop. Local "the email never arrived" is expected — verify by
|
|
@@ -190,6 +202,9 @@ derive one from the other).
|
|
|
190
202
|
for a hotfix.
|
|
191
203
|
|
|
192
204
|
## Change history
|
|
205
|
+
- 2026-09-23 — Review hold: `HELD`/`CANCELLED` statuses, `emailTemplateId`/`reviewOfEmailId`/
|
|
206
|
+
`dtReviewed`/`reviewedBy` columns, `setIsHeld`/`setEmailTemplateId`/`setReviewOfEmailId`;
|
|
207
|
+
`send()` now returns the log id; stale "enqueues" docblock fixed. (jcardinal)
|
|
193
208
|
- 2026-09-17 — Three gotchas from building the NYCHH transfer-order emails, no code change: the
|
|
194
209
|
`Logs_<Client>.Email` `body` column holds the **finished HTML**, so a new email is verified by
|
|
195
210
|
reading the row rather than an inbox; **`send_debug_emails_to` redirects at QUEUE time only** (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-23
|
|
10
10
|
owners: ["jcardinal", "bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
@@ -65,6 +65,29 @@ sends via `_Email`. The only client-specific input the send actually needs is th
|
|
|
65
65
|
4. Replace `{key}` placeholders in subject and body from the `$args` variadic map
|
|
66
66
|
(`replaceTemplateVariables`), then `send()`.
|
|
67
67
|
|
|
68
|
+
### Review hold (`EmailTemplates.isReviewRequired`, 2026-09-23)
|
|
69
|
+
|
|
70
|
+
The **template** decides (not `_Email`; direct `_Email` callers are never held). When
|
|
71
|
+
`isReviewRequired = 1`, `dispatch()` calls `_Email::send()` twice:
|
|
72
|
+
1. The real email, real recipients, `setIsHeld(true)` → row status `HELD` (Send cron skips it).
|
|
73
|
+
2. A `PENDING` copy, subject prefixed `[REVIEW] `, sent **only** to the template's `toCcBcc =
|
|
74
|
+
REVIEW` addresses. Body gets a yellow banner inserted after `<body>` listing the real
|
|
75
|
+
To/CC/BCC + held id, plus an approve link if `[_underscore] email_review_url` is set (Tools
|
|
76
|
+
`/emails?client=&emailId=`). Linked via `reviewOfEmailId`.
|
|
77
|
+
|
|
78
|
+
Release/reject happens in Tools [/emails](../../../../1.0/apps/tools/features/email-queue-review.md).
|
|
79
|
+
Rules:
|
|
80
|
+
- **Fail loud:** review on + no `REVIEW` rows → throws `Email template <uuid> requires review but
|
|
81
|
+
has no REVIEW addresses configured`; nothing is queued. Unknown `toCcBcc` → throws.
|
|
82
|
+
- `REVIEW` addresses are never merged into real To/CC/BCC.
|
|
83
|
+
- Out of scope by decision: no expiry of held rows, no body/PII hiding, no time-sensitive guard.
|
|
84
|
+
- Rejected design: an approve button inside the email (mail scanners pre-fetch links). Runner-up:
|
|
85
|
+
single held row, preview only in Tools.
|
|
86
|
+
|
|
87
|
+
Migrations: `Core/2026-09-23e - EmailTemplateReviewRecordField.sql` (RecordFields 2632 on
|
|
88
|
+
Records 202), `Client/2026-09-23a - EmailTemplateReview.sql`, `Logs_Client/2026-09-23a -
|
|
89
|
+
EmailReview.sql`. Deploy: SQL (Logs_Client first) → `_underscore` → tools.
|
|
90
|
+
|
|
68
91
|
Both `sendEmail()` and `send()` are thin wrappers that forward to `dispatch()`, so the API
|
|
69
92
|
and non-API paths run identical code — no behavioral drift between them.
|
|
70
93
|
|
|
@@ -76,10 +99,10 @@ gets a real error instead of a `true`/`false` it would otherwise ignore.
|
|
|
76
99
|
|
|
77
100
|
## Data model
|
|
78
101
|
|
|
79
|
-
- `EmailTemplates` (client DB): `uuid`, `isActive`, `sendFromEmailAddress`, `sendFromName`,
|
|
102
|
+
- `EmailTemplates` (client DB): `uuid`, `isActive`, `isReviewRequired`, `sendFromEmailAddress`, `sendFromName`,
|
|
80
103
|
`priority`, `subject`, `body`.
|
|
81
104
|
- `EmailTemplateOutgoingEmailAddress` (client DB): `emailTemplateId`, `emailAddress`,
|
|
82
|
-
`toCcBcc` (`TO`/`CC`/`BCC`).
|
|
105
|
+
`toCcBcc` (`TO`/`CC`/`BCC`/`REVIEW`).
|
|
83
106
|
|
|
84
107
|
## Client variations
|
|
85
108
|
|
|
@@ -207,6 +230,8 @@ worker method) in-process instead.
|
|
|
207
230
|
|
|
208
231
|
## Change history
|
|
209
232
|
|
|
233
|
+
- 2026-09-23 — Added the review hold (`isReviewRequired` + `REVIEW` addresses → `HELD` real email +
|
|
234
|
+
`[REVIEW]` copy); fail-loud on missing REVIEW rows / unknown `toCcBcc`. (jcardinal)
|
|
210
235
|
- 2026-09-17 — Added two caller-side gotchas found building the NYCHH transfer-order emails:
|
|
211
236
|
`replaceTemplateVariables()` does **no escaping** (plain `str_replace`, and the Send worker forces
|
|
212
237
|
HTML mode), so a stored value containing `<` arrives as live markup and every scalar must be
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-23
|
|
10
10
|
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/TableView.php
|
|
@@ -18,13 +18,15 @@ files:
|
|
|
18
18
|
- dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql
|
|
19
19
|
- dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
20
20
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
21
|
-
- dbchanges2/
|
|
21
|
+
- dbchanges2/Core/2026-09-23a - BundleRecordFieldTypes.sql
|
|
22
|
+
- dbchanges2/Client_Compass/2026-09-23a - BundlesTableViewFieldFlags.sql
|
|
22
23
|
related:
|
|
23
24
|
- tableview-apiwhereclause-row-filtering.md
|
|
24
25
|
- ../../_underscore/features/page-meta-context-field-settings.md
|
|
25
26
|
- ../../_underscore/features/acl-permission-chain.md
|
|
26
27
|
- ../../toga25-supply/features/meta-driven-table-data.md
|
|
27
28
|
- ../../_underscore/features/tableview-joins.md
|
|
29
|
+
- v2-rest-query-contract.md
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
How a 2.0 table view's columns/order/sort are defined by DB metadata (`TableViews`/`TableViewFields`/`TableViewJoins`), the per-client type override, hidden projected columns, and the safe reorder/rebuild patterns; open before writing any TableView migration.
|
|
@@ -45,6 +47,12 @@ The columns of a 2.0 table view are defined by DB metadata, not code. Per-client
|
|
|
45
47
|
|
|
46
48
|
**A column's `type` / `precision` are NOT per-view — they come from `Core.RecordFields`.** A field's rendered **type and precision are canonical**, read from the shared `Core.RecordFields.type` / `Core.RecordFields.precision` — there is **no per-view type/precision override** on `TableViewFields`. Changing `Core.RecordFields.type`/`precision` therefore changes the column's type/format **everywhere that field appears, for every client**. Intended canonical behavior — if you only want the change in one view, this mechanism cannot do it. (Reference: `Core.RecordFields` id 369 = `ItemFulfillmentItems.quantity` set to `type=NUMBER`, `precision=0`, which normalizes that column across all fulfillment views/clients.)
|
|
47
49
|
|
|
50
|
+
**`Core.RecordFields.type` controls DISPLAY and FILTER only — it does NOT change sorting.** `_Model_Client_TableView::meta()` emits `'type' => $recordField->type` straight off `Core.RecordFields` (`_underscore/Model/Client/TableView.php` ~L355). The type decides which **cell component** renders and which **filter widget** appears — nothing more. Sorting is done by the database on the real column, so a **text** column typed `NUMBER` still sorts alphabetically (`10` before `9`). Verified across sales-orders, items, vendor-items and bundles (2026-09-23). To get numeric ordering you must change the column or the `ORDER BY`, not the metadata — see [V2 REST query contract](v2-rest-query-contract.md).
|
|
51
|
+
|
|
52
|
+
**🚨 Typing a text column `NUMBER` BLANKS every non-numeric row.** `NumberCell` runs `Number(value)`; `Number("kit 207")` is `NaN` and renders empty. `Client_Compass` has 3 bundles whose `number` is literally `kit 205` / `kit 206` / `kit 207`, so setting `Bundles.number` to `NUMBER` emptied those cells. **Only type a column `NUMBER` after confirming every tenant's values actually convert** (`WHERE col REGEXP '^[0-9]+$' = 0`). `BOOLEAN` has the opposite story and is usually right: a JS boolean renders as nothing in a plain text cell, so a genuinely blank flag column (`Bundles.isActive`) is fixed by typing it `BOOLEAN`. Reference: `dbchanges2/Core/2026-09-23a - BundleRecordFieldTypes.sql`.
|
|
53
|
+
|
|
54
|
+
**Flag columns are NULLable — a migration guarded with `isFilterable = 0` updates NOTHING.** `isSortable`/`isFilterable`/`isCopyable`/`isGroupable` are left **NULL** by a seed that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`index`/`slug` (which is what the 2026-04-07 bundles view did), and `NULL = 0` is unknown in SQL. Guard with `COALESCE(col, 0) = 0` — full write-up in [re-runnable additive migrations](../../dbchanges2/features/rerunnable-additive-inserts.md).
|
|
55
|
+
|
|
48
56
|
**Per-client type override — point the column at a same-named `CustomRecordFields` row.** "No per-view override" is about `TableViewFields` having no `type` column of its own. It does **not** mean a client is stuck with the Core type: a column does not have to point at a Core `RecordField` at all. `TableViewFields.customRecordFieldId` is the alternative to `recordFieldId`, so **one client can retype a column without touching Core**:
|
|
49
57
|
1. Insert a `Client_<X>.CustomRecordFields` row with the **same `field` name** as the Core `RecordField` (e.g. `_unitPrice`) and the wanted `type` (e.g. `STRING`).
|
|
50
58
|
2. Repoint that client's `TableViewFields` row at `customRecordFieldId` instead of `recordFieldId`.
|
|
@@ -120,6 +128,9 @@ So the visible failure is a broken SQL query while the actual defect is one miss
|
|
|
120
128
|
- **Don't assume `Core.RecordFields`/`Core.Records` ids are stable across environments** — look them up by `field` name + record.
|
|
121
129
|
- **Make column migrations idempotent** (`NOT EXISTS` guard) — they run per client DB.
|
|
122
130
|
- **`isVisible=0` still costs a fetch** — the field is selected into the projection; only the render is suppressed.
|
|
131
|
+
- **`type` drives rendering + filtering, NOT sort order** — a text column typed `NUMBER` still sorts alphabetically.
|
|
132
|
+
- **`NUMBER` on a column with non-numeric values renders those cells BLANK** (`Number("kit 207")` → NaN). Check every tenant's data first.
|
|
133
|
+
- **Flag columns are NULLable** — guard flag migrations with `COALESCE(col, 0) = 0`, never `col = 0`, or the UPDATE matches 0 rows and looks like caching.
|
|
123
134
|
- **`type`/`precision` are canonical (Core-wide), not per-view.** Editing `Core.RecordFields` changes every view/client that shows the field; there is no per-view type override. `isCopyable`, `isSortable`, `isFilterable`, `isGroupable`, `isVisible` are the per-view knobs (on `TableViewFields`).
|
|
124
135
|
- **`TableView::meta()` contains a hand-written `SELECT` that names `TableViewFields` columns explicitly.** So a new column on that table is required by **two** mechanisms — the model property *and* that query — and a grep for the property alone under-reports what a missing column breaks.
|
|
125
136
|
- **`isGroupable` has no migration** — omit it from new TableView migrations (fails on prod/beta) until the fan-out migration lands. Composite unique keys read as unconstrained in `information_schema.COLUMNS`; use `information_schema.STATISTICS` / `SHOW INDEX`.
|
|
@@ -131,3 +142,6 @@ So the visible failure is a broken SQL query while the actual defect is one miss
|
|
|
131
142
|
- [ACL permission chain](../../_underscore/features/acl-permission-chain.md)
|
|
132
143
|
- [Meta-driven table data](../../toga25-supply/features/meta-driven-table-data.md)
|
|
133
144
|
- [TableView joins](../../_underscore/features/tableview-joins.md)
|
|
145
|
+
|
|
146
|
+
## Change history
|
|
147
|
+
- 2026-09-23 — Recorded that `Core.RecordFields.type` drives display/filter only and never sorting; that `NUMBER` on a text column blanks non-numeric rows (Compass `kit 205-207` bundles) while `BOOLEAN` fixes genuinely blank flag columns; and that the flag columns are NULLable so a `= 0` migration guard matches nothing. (apeterson)
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-23
|
|
10
10
|
owners: [tcox, bala, apeterson, jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -18,6 +18,7 @@ related:
|
|
|
18
18
|
- nested-fk-acl-embedding.md
|
|
19
19
|
- ../../_underscore/features/calculated-sql-fields.md
|
|
20
20
|
- ../../toga25-supply/features/transfer-orders-page.md
|
|
21
|
+
- tableview-field-metadata.md
|
|
21
22
|
---
|
|
22
23
|
|
|
23
24
|
## Summary
|
|
@@ -63,6 +64,33 @@ controller.
|
|
|
63
64
|
the join is genuinely 1:1, or the duplicates agree. Worked example:
|
|
64
65
|
[category tile sort order](../../toga2-commerce/features/category-tile-sort-order.md).
|
|
65
66
|
|
|
67
|
+
### How `sort` becomes `ORDER BY` — and why "descending sort is broken" is usually just NULLs
|
|
68
|
+
|
|
69
|
+
`ORDER BY` is a **plain string concatenation** of the resolved column and `ASC`/`DESC` — no cast, no
|
|
70
|
+
`NULLS FIRST/LAST` handling, no per-field hook (`V2.php` plain-column branch ~L3782-3784,
|
|
71
|
+
calculated-field branch ~L3771-3773).
|
|
72
|
+
|
|
73
|
+
Two things follow.
|
|
74
|
+
|
|
75
|
+
**1. Empty/NULL columns fill page 1 of an ascending sort.** MySQL puts NULLs **first** on `ASC` and
|
|
76
|
+
**last** on `DESC`. A column with many empties — e.g. the Compass sales-orders MITS PO number (slug
|
|
77
|
+
`purchaseOrderNumber`, backed by the calculated `_purchaseOrders`) — therefore shows a page of
|
|
78
|
+
dashes when sorted ascending. That is correct SQL, **not a bug**; triage it as data coverage. A
|
|
79
|
+
"blanks always last" behaviour would be `ORDER BY (col IS NULL), col`, and should be a **per-field
|
|
80
|
+
opt-in**, never blanket.
|
|
81
|
+
|
|
82
|
+
**2. The same expression is the keyset (seek) pagination cursor.** `$seekSortColumn` is reused in the
|
|
83
|
+
seek `WHERE` (~L4042) and the uuid tiebreaker (~L4123). So **any change to the `ORDER BY` expression
|
|
84
|
+
must be mirrored in the seek `WHERE` and in `getSeekFieldType()`** — otherwise page 1 looks right and
|
|
85
|
+
page 2 onward silently returns the wrong rows, with no error.
|
|
86
|
+
|
|
87
|
+
**Text columns sort as text — that is the column, not the engine.** Every genuinely numeric column
|
|
88
|
+
sorts correctly (`_qtyItems`, `_totalPurchase`, `_unitPrice`, `_unitCost`, `sortOrder`). Metadata
|
|
89
|
+
cannot fix it: `Core.RecordFields.type` affects rendering/filtering only
|
|
90
|
+
([TableView field metadata](tableview-field-metadata.md)). **Decision (2026-09-23, TRUE-82137): do
|
|
91
|
+
not add a shared-engine cast** for a one-column problem — it risks the keyset pagination contract
|
|
92
|
+
above and loses the index. Fix the column type in the schema, or cast per-field, deliberately.
|
|
93
|
+
|
|
66
94
|
### ⚠ `depth` is not free — the default 3 re-reads child rows and races concurrent deletes
|
|
67
95
|
|
|
68
96
|
**`depth` defaults to 3 on every request, writes included.** Serialization (`getFullModelData()` in
|
|
@@ -557,6 +585,11 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
|
|
|
557
585
|
[V2 request logging](request-logging.md).
|
|
558
586
|
|
|
559
587
|
## Change history
|
|
588
|
+
- 2026-09-23 — Documented how `sort` becomes `ORDER BY` (plain concatenation, no cast or NULL
|
|
589
|
+
handling), that the same expression doubles as the keyset-pagination cursor so any change must be
|
|
590
|
+
mirrored in the seek `WHERE`/`getSeekFieldType()`, and that "descending sort is broken" reports are
|
|
591
|
+
usually normal MySQL NULL ordering. Decided against an engine-level numeric cast for Bundles
|
|
592
|
+
(TRUE-82137). (apeterson)
|
|
560
593
|
- 2026-09-18 — Recorded that swapping an OR-chain of `contains` for a single `in` on a
|
|
561
594
|
calculated field only shortens the SQL text and does not speed it up: the Compass approvals list
|
|
562
595
|
(`_status:in:...`) still takes 14.66 s with the `in` clause live (TRUE-81379). (apeterson)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-23
|
|
10
10
|
owners: [bala, kyalamarthi, apeterson, ajean, jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
- dbchanges2/_modules/netsuite/2026-09-11c - ItemClassRecordPermission.sql
|
|
17
17
|
- dbchanges2/Client_Compass/2026-09-18a - BackfillUpstreamItemFulfillmentTracking.sql
|
|
18
18
|
- dbchanges2/Client_Compass/2026-09-21a - BackfillUpstreamItemFulfillmentUnits.sql
|
|
19
|
+
- dbchanges2/Client_Compass/2026-09-23a - BundlesTableViewFieldFlags.sql
|
|
19
20
|
related:
|
|
20
21
|
- ../architecture.md
|
|
21
22
|
- ../../worker2/features/netsuite-salesorder-open-orders-sync.md
|
|
@@ -413,8 +414,35 @@ Both are local-only evidence: pair them with the
|
|
|
413
414
|
[local-vs-prod config parity check](../workflows/local-vs-prod-mysql-config-parity.md) before
|
|
414
415
|
concluding a migration is production-safe.
|
|
415
416
|
|
|
417
|
+
### ⚠ A `col = 0` guard on a NULLable flag matches NOTHING — use `COALESCE(col, 0) = 0`
|
|
418
|
+
|
|
419
|
+
The house pattern for a "turn these flags on" migration guards the UPDATE so a re-run is a no-op:
|
|
420
|
+
|
|
421
|
+
```sql
|
|
422
|
+
-- WRONG — silently updates 0 rows when the flags are NULL
|
|
423
|
+
UPDATE TableViewFields SET isFilterable = 1, isCopyable = 1
|
|
424
|
+
WHERE tableViewId = @tv AND (isFilterable = 0 OR isCopyable = 0);
|
|
425
|
+
|
|
426
|
+
-- CORRECT
|
|
427
|
+
WHERE tableViewId = @tv AND (COALESCE(isFilterable, 0) = 0 OR COALESCE(isCopyable, 0) = 0);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
In SQL `NULL = 0` is **unknown**, not true, so the guard excludes exactly the rows that most need
|
|
431
|
+
the update. MySQL reports success with `0 rows affected` and the UI is unchanged — which reads as a
|
|
432
|
+
caching problem, not a migration bug. Hit live on 2026-09-23 on the Compass bundles TableView, whose
|
|
433
|
+
flags were left NULL by the original seed.
|
|
434
|
+
|
|
435
|
+
**Applies to every NULLable flag column**, and the flags on `TableViewFields`
|
|
436
|
+
(`isSortable`/`isFilterable`/`isCopyable`/`isGroupable`) are NULLable and are routinely left NULL by
|
|
437
|
+
an INSERT that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`index`/`slug`.
|
|
438
|
+
|
|
439
|
+
> **Known outstanding hole:** the precedent files `2026-08-26 - EnableTableViewFieldFilters.sql`
|
|
440
|
+
> (NYCHH, Quad, Compass USA, Compass Canada) all use the bare `= 0` guard, so any row that was NULL
|
|
441
|
+
> rather than 0 was never updated by them. Not yet audited.
|
|
442
|
+
|
|
416
443
|
## Gotchas
|
|
417
444
|
|
|
445
|
+
- **⚠ `col = 0` never matches a NULL** — guard flag-enabling UPDATEs with `COALESCE(col, 0) = 0`, or the migration reports success and changes nothing.
|
|
418
446
|
- **⚠ An unguarded ACL grant migration duplicates silently — `AclRecordPermissions` has no unique key
|
|
419
447
|
that stops it (found in prod 2026-09-15).** `_modules/netsuite/2026-09-11c - ItemClassRecordPermission.sql`
|
|
420
448
|
is a plain `INSERT … VALUES` for `(recordId 105, roleId 3)` with **no `NOT EXISTS` guard**. Every
|
|
@@ -477,6 +505,7 @@ concluding a migration is production-safe.
|
|
|
477
505
|
[FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
|
|
478
506
|
|
|
479
507
|
## Change history
|
|
508
|
+
- 2026-09-23 — Added the NULL-unsafe flag-guard trap (`col = 0` matches no NULL row; use `COALESCE(col, 0) = 0`), found when a bundles TableViewFields migration updated 0 rows; flagged the 2026-08-26 `EnableTableViewFieldFilters` files as carrying the same hole. (apeterson)
|
|
480
509
|
- 2026-09-22 — Added multi-tier chain backfill rules from the Compass tracking/serial backfills: re-run the same file until the verify count is 0 (each pass moves data up one tier; both files needed 3 passes); `GROUP BY` the target key pair instead of `SELECT DISTINCT` when many downstream rows roll up to one upstream row (bundles tripped a bridge UNIQUE 6-to-1); measure the safe/ambiguous/no-match split before writing bulk DML and skip the ambiguous rows (4,900 broken links = 1,982 safe + 0 ambiguous + 2,918 no-match). Sharpened the unique-key rule with measured composite keys across the four item-fulfillment tables — `ItemFulfillmentItemUnits` has none and already carries 254 duplicates. (rgirish)
|
|
481
510
|
- 2026-09-21 — Added three lessons from the platform-wide `Users.contactId` NOT NULL backfill: a guarded
|
|
482
511
|
`NOT NULL` ALTER (`sql_mode ''` silently coerces leftover NULLs to `0`, so gate on a residual count —
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
| [API client (axios wrapper, auth, table-data fetchers)](features/api-client.md) | blox axios/React-Query client — envelope, auth (Bearer + refresh), option serialization, CRUD, table-data fetchers; open for init, query shapes, and the many si |
|
|
9
9
|
| [BaseInput (react-hook-form field factory)](features/base-input.md) | react-hook-form field factory rendering an input per `inputType`; open for its config surfaces, the input-type switch, validation rules, and the `<FormProvider> |
|
|
10
10
|
| [blox authoring defect backlog (fix IN blox, not around it)](features/blox-authoring-defects.md) | Backlog of defects to fix INSIDE `@agilant/toga-blox` (the counterpart to frontend §22's app-side workarounds); open when editing blox itself or deciding whethe |
|
|
11
|
+
| [EnvironmentBadge & endpoint classifier](features/environment-badge.md) | The on-screen badge that names which API a build is talking to, and the URL classifier behind it; open before changing its hide rule, adding an environment, or |
|
|
11
12
|
| [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | The production wired-up table (server/client templates, measure-and-freeze sizing, virtualization, sticky columns); open for props, column-sizing/sticky mechani |
|
|
12
13
|
| [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | Generic presentational record-detail modal shell (position/animation/lifecycle only); open for its props, the `data-blox-modal-open` root marker, and the no-por |
|
|
13
14
|
| [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | TanStack-v8 Table building block (column building, cell types, filters/sorts, data hooks, theming); open for cell resolution, the filter components, hooks, and |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: EnvironmentBadge & endpoint classifier
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga-blox
|
|
5
|
+
project: TOGa Blox
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-23
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- toga-blox/src/components/EnvironmentBadge/EnvironmentBadge.tsx
|
|
13
|
+
- toga-blox/src/components/EnvironmentBadge/EnvironmentBadge.css
|
|
14
|
+
- toga-blox/src/utils/getEndpointFromEnvironment.ts
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ../../../standards/frontend-deploy.md
|
|
18
|
+
- ../../../standards/frontend.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
The on-screen badge that names which API a build is talking to, and the URL classifier behind it; open before changing its hide rule, adding an environment, or debugging a build that shows no badge.
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`EnvironmentBadge` is a dev/QA warning indicator shipped by blox and mounted by the apps (see `toga2-view` `App.tsx`, commerce `AuthLayout.tsx`). It labels the resolved API endpoint so nobody mistakes a non-production site for production — or worse, a non-production site that is wired to the **production** API.
|
|
26
|
+
|
|
27
|
+
It is the client-side detector for a broken deploy rule: `frontend-deploy.md` says never build with `npm run build` (no `--mode`), because Vite then defaults to mode `production` and loads `.env.production`. When that happens the site points at the production API. The badge exists to make that visible.
|
|
28
|
+
|
|
29
|
+
**Hiding is this component's worst failure mode.** Treat "show a wrong-ish label" as always better than "show nothing".
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
### Hide rule (three conditions, all required)
|
|
34
|
+
|
|
35
|
+
The badge hides only when **all** of:
|
|
36
|
+
|
|
37
|
+
1. build mode is `production` (`import.meta.env.MODE`),
|
|
38
|
+
2. the classified endpoint is `production`, and
|
|
39
|
+
3. `isNonProductionHost(window.location.hostname)` is false.
|
|
40
|
+
|
|
41
|
+
The hostname overrides the build. Before 2026-09-23 only conditions 1 and 2 existed, so the exact incident the badge is for — a sandbox site built without `--mode`, defaulting to mode `production` and `.env.production` — matched both and rendered nothing. Now a sandbox URL talking to the prod API shows a red `Endpoint: Production`.
|
|
42
|
+
|
|
43
|
+
### `isNonProductionHost(hostname)` (exported)
|
|
44
|
+
|
|
45
|
+
Returns true when the hostname contains any marker in `NON_PRODUCTION_HOST_MARKERS`, case-insensitive: `.client.sandbox`, `.dev.sandbox`, `.beta`, `.gamma`.
|
|
46
|
+
|
|
47
|
+
### `getEndPointFromEnvironment()` — explicit match, never a fallback
|
|
48
|
+
|
|
49
|
+
Classifies `VITE_API` into `production` | `local` | `unknown` (plus the existing named environments):
|
|
50
|
+
|
|
51
|
+
- `production` — only an explicit match on `/^(https?:\/\/)?api\d?\.togahub\.com(:\d+)?(\/|$)/i` (api, api1, api2, api3).
|
|
52
|
+
- `local` — a **dotless** host (the XAMPP vhost `http://api/v2`); a dotless host can never be a public production host.
|
|
53
|
+
- `unknown` — any other fully-qualified host.
|
|
54
|
+
|
|
55
|
+
**The general rule this encodes: never let the "hide the warning" value be the fallback.** Match it explicitly and label the leftovers. Previously `production` was returned for any unrecognised base URL, so an unrecognised `VITE_API` went completely unlabelled.
|
|
56
|
+
|
|
57
|
+
### Styling
|
|
58
|
+
|
|
59
|
+
`EnvironmentBadge.css` classes are `env-<endpoint>`. `.env-unknown` is styled **red like production**, so an unclassified backend reads as a warning, not a normal environment.
|
|
60
|
+
|
|
61
|
+
## Gotchas
|
|
62
|
+
|
|
63
|
+
- **The marker list is exhaustive, not a heuristic.** A differently shaped non-prod host (e.g. `staging.togasupply.com`) still hides. Adding an environment means adding its marker to `NON_PRODUCTION_HOST_MARKERS`.
|
|
64
|
+
- **A CSS class typo silently unstyles the badge.** `.env-localost` (missing `h`) shipped for a long time; the localhost badge rendered as plain text and nobody noticed. Class names must match the endpoint strings the classifier returns — add both together.
|
|
65
|
+
- **Testing `import.meta.env`:** use `vi.stubEnv("MODE", …)` / `vi.stubEnv("VITE_API", …)`. To set the hostname without navigating jsdom, redefine `window.location` with `Object.defineProperty`.
|
|
66
|
+
- **Consumers import it in ways that matter.** A barrel `import { EnvironmentBadge } from "@agilant/toga-blox"` drags in unrelated peers at build time; older published versions exported it as **default only**. See [toga2-commerce Amplify build & deploy](../../toga2-commerce/workflows/amplify-build-and-deploy.md).
|
|
67
|
+
- Do not remove the badge from an app to "clean up" — it is a deliberate QA tool.
|
|
68
|
+
|
|
69
|
+
## Change history
|
|
70
|
+
- 2026-09-23 — Hostname now overrides the hide rule (`isNonProductionHost`); endpoint classifier matches `production` explicitly and adds `local`/`unknown`; fixed `.env-localost` typo, added `.env-local`/`.env-unknown`; 24 tests added (apeterson)
|
|
@@ -6,13 +6,14 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-23
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
|
|
13
13
|
- toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx
|
|
14
14
|
- toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts
|
|
15
15
|
- toga25-supply/src/hooks/useTablePageMeta.ts
|
|
16
|
+
- toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx
|
|
16
17
|
- toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts
|
|
17
18
|
- toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts
|
|
18
19
|
- toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts
|
|
@@ -90,6 +91,23 @@ The view model then returns `salesOrdersTableMeta` (after column-visibility adju
|
|
|
90
91
|
1. a legacy `<slug>-listing` **page-meta record** (matches every existing page), or
|
|
91
92
|
2. extending the **list surface** with per-column `FIELD` elements carrying a `labelMessageId` — which additionally makes headers translatable through `MessageTranslations`, something the page-meta route cannot do. See [surface-frontend](./surface-frontend.md).
|
|
92
93
|
|
|
94
|
+
### 🚨 Never hardcode a table meta in a view model — it fakes the flags too
|
|
95
|
+
|
|
96
|
+
The Bundles page (fixed 2026-09-23, TRUE-82137) carried a **hardcoded `meta` object** as its table
|
|
97
|
+
meta, called `useFetchPageMeta` with the **wrong page slug** (`"inventory"`, copied from the
|
|
98
|
+
Inventory page), and called `useTablePageMeta` but **threw the fetched result away**, keeping only
|
|
99
|
+
`isLoadingFilterOptions`. Two failures, and the second is the dangerous one:
|
|
100
|
+
|
|
101
|
+
1. No labels reached the table, so headers rendered raw camelCase slugs (`number`, `isActive`, …) —
|
|
102
|
+
the visible "lowercase headers" bug.
|
|
103
|
+
2. The fake meta declared `isSortable`/`isFilterable`/`isCopyable` **`true` on every column**, which
|
|
104
|
+
**masked** the fact that the real served meta had them all off (the flags were NULL in
|
|
105
|
+
`TableViewFields`). The page looked configured while the database said otherwise.
|
|
106
|
+
|
|
107
|
+
Use the fetched server meta plus a `humanizeSlug` label fallback — the pattern in
|
|
108
|
+
`useTransferOrdersPageViewModel`. A hardcoded meta is never a workaround for missing data; it hides
|
|
109
|
+
the data problem.
|
|
110
|
+
|
|
93
111
|
### What's in the table meta (the columns)
|
|
94
112
|
|
|
95
113
|
`useFetchTablePageMeta` returns `{ table, nestedTable }`. The `table` object is the `TableViewMeta` (alias of `DataTableMetaProp`) and carries:
|
|
@@ -137,5 +155,9 @@ App-side glue (NOT in toga-blox):
|
|
|
137
155
|
- **Don't shrink the fetch meta for column hiding.** `getDataTableData` builds its API field list from `meta.fields`; adjust `isVisible` only on the *display* meta (see column-visibility).
|
|
138
156
|
- **Infinite scroll needs a height-constrained scroll container** (`max-height` + `overflow-y: auto`); inline-expanded nested tables where the page scrolls won't load more. Pagination is unaffected.
|
|
139
157
|
- **`additionalData` must be in the query key** (it is, inside `useTableData`) — required for WHERE-filtered modal tables or React Query serves a stale unfiltered first render.
|
|
158
|
+
- **Never hardcode a table meta object in a view model.** It silently fakes `isSortable`/`isFilterable`/`isCopyable` and hides a real `TableViewFields` gap; always render the fetched meta.
|
|
140
159
|
- **toga-blox edits need a rebuild** — the app consumes `dist/`; editing only `src` is a silent no-op.
|
|
141
160
|
- **A new page with no page-meta record has no column labels at all** — the table meta carries no `label` and the merge hook no-ops on a null `pageMeta`, so headers *and* the Columns modal render raw camelCase slugs. Plan the label source before building the screen (see above).
|
|
161
|
+
|
|
162
|
+
## Change history
|
|
163
|
+
- 2026-09-23 — Added the hardcoded-table-meta anti-pattern found on the Bundles page: it produced raw slug headers and, worse, faked `isSortable`/`isFilterable`/`isCopyable` as true, masking NULL flags in `TableViewFields` (TRUE-82137). (apeterson)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
15
15
|
- **test** (Test) — 16 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
16
16
|
- **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
17
17
|
- **store** (Store) — 3 doc(s) → [1.0/apps/store/INDEX.md](1.0/apps/store/INDEX.md)
|
|
18
|
-
- **tools** (Tools) —
|
|
18
|
+
- **tools** (Tools) — 22 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
|
|
19
19
|
|
|
20
20
|
## 2.0 framework
|
|
21
21
|
|
|
@@ -34,7 +34,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
34
34
|
- **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
35
35
|
- **toga2-commerce** (TOGa Commerce) — 26 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
36
36
|
- **toga25-supply** (TOGa 2.5 Supply) — 21 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
|
|
37
|
-
- **toga-blox** (TOGa Blox) —
|
|
37
|
+
- **toga-blox** (TOGa Blox) — 15 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
|
|
38
38
|
- **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
|
|
39
39
|
|
|
40
40
|
## standalone framework
|
|
@@ -7,7 +7,7 @@ client: nychh
|
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-09-23
|
|
10
|
-
owners: ["bala", "rgirish"]
|
|
10
|
+
owners: ["bala", "rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Client/Nychh/TransferOrderEmails.php
|
|
13
13
|
- _underscore/Model/Nychh/TransferOrder.php
|
|
@@ -231,6 +231,12 @@ Cost of the split, stated so nobody is surprised later:
|
|
|
231
231
|
|
|
232
232
|
## Gotchas / known issues
|
|
233
233
|
|
|
234
|
+
- **Submitted uses the review hold** (template "Transfer Order - Submitted", `Client_Nychh`
|
|
235
|
+
`EmailTemplates` id 3, `isReviewRequired = 1`) — first prod user; see
|
|
236
|
+
[email-template-sending.md](../../../2.0/apps/_underscore/features/email-template-sending.md).
|
|
237
|
+
Turning it on **without** a `REVIEW` address row fails `Client/Nychh/TransferOrderEmails/SendSubmitted`
|
|
238
|
+
(prod Logs.Issue Y6) — expected fail-loud. Fix: add the `REVIEW` row, retry the job.
|
|
239
|
+
|
|
234
240
|
- **⚠ There is NO `try`/`catch` anywhere in this feature, by design.**
|
|
235
241
|
`sendTransferOrderEmail()` returns `void` and **throws `_Exception`** when
|
|
236
242
|
`_Model_Client_EmailTemplate::send()` returns `false` (which happens only when the template row is
|
|
@@ -260,6 +266,7 @@ loaded out of the dbchanges2 file so an unresolved `{placeholder}` fails a test.
|
|
|
260
266
|
unescaped values and both detail-box defects were all found there, not in production.
|
|
261
267
|
|
|
262
268
|
## Change history
|
|
269
|
+
- 2026-09-23 — Submitted template put on review hold; missing REVIEW row → job failure note. (jcardinal)
|
|
263
270
|
|
|
264
271
|
- 2026-09-23 — **The greeting now addresses whoever raised the order, not the site contact.**
|
|
265
272
|
`readTransferOrderForEmail()` selects `CreatedByUsers.firstName AS submittedByFirstName`, and
|
package/package.json
CHANGED