toga-ai 1.0.161 → 1.0.163

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.
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Library (1.0 Framework) Architecture](architecture.md) | `library` is the shared library repository for **all 1.0 (legacy) applications** — the `App_` framework. | library/_.php, library/app/, library/browser/ |
6
+ | [Diagnostic Dialog — View Recommended Services Routing](features/diagnostic-dialog-view-recommended-services.md) | `App_Model_Toga_Diagnostic::initializeDiagnosticDialog()` renders the device modal used across all TOGa service request views. | library/app/model/toga/diagnostic.php |
6
7
  | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
7
8
  | [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
8
9
  | [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem |
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: Diagnostic Dialog — View Recommended Services Routing
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [snaredla]
11
+ files:
12
+ - library/app/model/toga/diagnostic.php
13
+ related:
14
+ - ../../../clients/office-depot/features/togarefresh2026-linkbuilder-routing.md
15
+ ---
16
+
17
+ ## Summary
18
+ `App_Model_Toga_Diagnostic::initializeDiagnosticDialog()` renders the device modal
19
+ used across all TOGa service request views. It includes a "View Recommended Services"
20
+ button whose URL depends on whether the store is a TOGaRefresh 2026 store and whether
21
+ the diagnostic passed or failed.
22
+
23
+ ## How it works
24
+ 1. Checks `$_SESSION['isTogaRefresh2023']` or `$_SESSION['isTogaRefresh2026']` to
25
+ determine if this is a refresh store.
26
+ 2. For refresh stores, casts `$serviceRequestId` to `(int)` then queries `Diagnostics`
27
+ for the latest `pass` value for the service request.
28
+ 3. Routes:
29
+ - **Diagnostic pass** (`pass >= 1`) → `?content=togarefresh2026_servicerequests_techsupport&serviceRequestId=...`
30
+ - **Diagnostic fail / no diagnostic** (`pass < 1`) → `?content=togarefresh2026_servicerequests_serviceselection&jobJacketId=1&showServices=195,101899&serviceRequestId=...`
31
+ 4. For non-refresh stores, falls back to `App_Model_ServiceRequest::linkBuilder()`.
32
+
33
+ ## Data model
34
+ - Table: `Diagnostics` (db_toga)
35
+ - Field read: `pass` (TINYINT boolean — 1 = passed, 0 = failed)
36
+ - Query: latest record by `id DESC LIMIT 1` for the given `serviceRequestId`
37
+
38
+ ## Gotchas / known issues
39
+ - `$serviceRequestId` is cast to `(int)` before SQL interpolation — do not remove this cast (SQL injection prevention).
40
+ - If no diagnostic record exists, `$diagnosticPass` defaults to `0` (fail path) — shows service selection with `showServices=195,101899`.
41
+ - The same pass/fail routing logic exists in `toga/app/togarefresh2026/servicerequests/view.php` (lines 2358–2381) — keep both in sync.
42
+ - `isTogaRefresh2023` is kept alongside `isTogaRefresh2026` for backward compatibility — do not remove it.
43
+ - `$_SESSION['isTogaRefresh2026']` is set in `toga/_/app/frameworkindex.php` and `toga/app/home/action.php` at login time.
44
+
45
+ ## Related docs
46
+ - `clients/office-depot/features/togarefresh2026-linkbuilder-routing.md`
47
+
48
+ ## Change history
49
+ - 2026-06-23 — Initial doc. Diagnostic modal now routes pass→techsupport, fail→serviceselection with showServices=195,101899. (snaredla)
@@ -0,0 +1,4 @@
1
+ # toga (TOGa) — 1.0 knowledge
2
+
3
+ | Doc | Summary | Files |
4
+ |-----|---------|-------|
@@ -0,0 +1,9 @@
1
+ # toga25-supply (TOGa 2.5 Supply) — 2.0 knowledge
2
+
3
+ | Doc | Summary | Files |
4
+ |-----|---------|-------|
5
+ | [TOGa 2.5 Supply — Architecture](architecture.md) | `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply application — an iteration and improvement of `toga2-supply`. | toga25-supply/src/main.tsx, toga25-supply/src/App.tsx, toga25-supply/src/routes.tsx, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/layout/, toga25-supply/src/pages/, toga25-supply/src/hooks/useTableCellInteractions.ts |
6
+ | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md |
7
+ | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
8
+ | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
9
+ | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: TOGa 2.5 Supply — Architecture
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: architecture
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/src/main.tsx
13
+ - toga25-supply/src/App.tsx
14
+ - toga25-supply/src/routes.tsx
15
+ - toga25-supply/src/fieldsConfig/index.ts
16
+ - toga25-supply/src/fieldsConfig/useClientFields.ts
17
+ - toga25-supply/src/layout/
18
+ - toga25-supply/src/pages/
19
+ - toga25-supply/src/hooks/useTableCellInteractions.ts
20
+ related:
21
+ - features/client-configurable-fields.md
22
+ - features/meta-driven-table-data.md
23
+ - features/column-visibility.md
24
+ - features/record-modals-and-nested-tables.md
25
+ ---
26
+
27
+ ## Summary
28
+
29
+ `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply
30
+ application — an iteration and improvement of `toga2-supply`. It is a Vite + React 18 SPA
31
+ (react-router-dom 7, TanStack Query 5, react-hook-form 7) that talks to the 2.0 backend API
32
+ (`_underscore`). It does **not** depend on `toga2-supply`; its components, table templates, and
33
+ data hooks come from the shared in-house library **`@agilant/toga-blox`** (repo `toga-blox-npm`,
34
+ currently consumed at `1.0.318-beta.37`).
35
+
36
+ The app is **multi-client and meta-driven by design**. The same build serves many clients
37
+ (Compass USA/Canada, NYC Health & Hospitals, Quad, Prudential, GroWrk, and others — there are
38
+ per-client build scripts such as `nychh`, `compass`, `compasscanada`, `growrk`, `prudential`,
39
+ `spglobal`, `bankofamerica`, `wmchealth`, `quad`). Almost nothing is hardcoded per client:
40
+ table columns/labels come from server **table meta**, and client/role-specific UI config
41
+ (filter buttons, group-by options, column pickers, layout toggles) is resolved at runtime
42
+ through `src/fieldsConfig` + `useClientFields()`. Pages are thin; the real logic lives in
43
+ per-page **view models** (`use*ViewModel` hooks) and reusable **layout** components that wrap
44
+ toga-blox table/modal templates.
45
+
46
+ **Critical rules:**
47
+ - **Don't hardcode per-client behavior in components or view models** — resolve it through
48
+ `useClientFields()` / `src/fieldsConfig`, keyed by `clientSlug` (from `useHostnameStore`) ×
49
+ `role`. Every config must have a `DEFAULT` fallback or a client/role with no entry won't render.
50
+ - **A table's pagination-vs-infinite-scroll mode is decided solely by `isPaginationEnabled` from
51
+ the table meta**, consumed via the single `useTableData` hook — never pick a data hook or
52
+ hardcode the mode per page.
53
+ - **`@agilant/toga-blox` is symlinked from the local `toga-blox-npm` repo and the app consumes its
54
+ `dist/`** — after editing toga-blox source you MUST rebuild (`cd toga-blox-npm && npm run build`)
55
+ or the change won't reach runtime. Patching only `src` is a silent no-op.
56
+ - **Source files in this repo use TAB indentation** — match exactly or string-replace edits fail.
57
+
58
+ ## Tech stack & entry points
59
+
60
+ - **Build/dev:** Vite. `src/main.tsx` → `src/App.tsx`; routes in `src/routes.tsx`. Per-client
61
+ dev/build scripts in `package.json` (`dev`, `build`, plus `nychh`, `compass`, `growrk`, etc.).
62
+ - **Data:** TanStack Query 5 (`@tanstack/react-query`). The canonical table query key prefix is
63
+ `["table-data", slug, …]`; a blanket `invalidateQueries({ queryKey: ["table-data"] })` refetches
64
+ any table after a mutation.
65
+ - **Forms:** react-hook-form 7 (`FormProvider` + `BaseInput`), used by the create/edit record modals.
66
+ - **Routing:** react-router-dom 7. URL search params are the primary state store for tables
67
+ (sorting, filters, pagination, active-row uuid, column visibility, nested-table swap keys).
68
+ - **UI library:** `@agilant/toga-blox` provides `PrimaryTable*Layout`, `TableRecordModal`,
69
+ `BaseInput`, `BaseButton`, `DetailSection`, the `useTableData` hook, FontAwesome helpers, etc.
70
+
71
+ ## Directory layout (`src/`)
72
+
73
+ - `pages/<Entity>/` — page component (thin) + `viewModel/` (the `use*PageViewModel` hook) +
74
+ per-page `FIELDS/<CLIENT>/*.json` client config + `hooks/` + `dummyData/` (column-width JSON).
75
+ - `layout/<Thing>Layout/` — reusable table/modal layout wrappers around toga-blox templates,
76
+ each with its own `use*ViewModel`. Key ones: `SalesOrdersTableLayout`,
77
+ `SalesOrderRecordModalLayout`, `SalesOrderItemsTableLayout`, `ItemFulfillmentModal`,
78
+ `ItemRecordModalLayout`, `GenericNestedTables`, `RecordApprovalModalLayout`.
79
+ - `fieldsConfig/` — `index.ts` (the `ClientFields` bundle + `FIELDS[clientSlug][role]` map) and
80
+ `useClientFields.ts` (resolves active client/role, falls back to `DEFAULT_CLIENT_FIELDS`).
81
+ - `globalFieldsConfig/`, `hooks/`, `stores/` (e.g. `useHostnameStore`), `contexts/`, `utils/`,
82
+ `types/`, `api/`, `styles/`, `themeConfig.json`.
83
+
84
+ ## Architectural pillars (see feature docs)
85
+
86
+ 1. **Client/role-driven config** — runtime UI config without rebuilds. See
87
+ [client-configurable-fields](features/client-configurable-fields.md).
88
+ 2. **Meta-driven table data** — one `useTableData` hook; meta's `isPaginationEnabled` picks the
89
+ fetch strategy. See [meta-driven-table-data](features/meta-driven-table-data.md).
90
+ 3. **URL-driven column visibility** — shareable show/hide-columns, client-gated button. See
91
+ [column-visibility](features/column-visibility.md).
92
+ 4. **Layered modals over tables** — record modals, nested client/server tables, cell-click
93
+ modals, and the `GenericNestedTables` config-driven renderer. See
94
+ [record-modals-and-nested-tables](features/record-modals-and-nested-tables.md).
95
+
96
+ ## toga-blox relationship
97
+
98
+ `@agilant/toga-blox` is the shared frontend component/template/utility library (repo
99
+ `toga-blox-npm`). It is **not published to npm for this app's purposes** — the app is currently
100
+ its primary consumer and resolves it via a symlinked `node_modules/@agilant/toga-blox` →
101
+ `toga-blox-npm`. Because the app imports the built `dist/` (JS + `.d.ts`), any change to toga-blox
102
+ **source** requires a rebuild before it is visible at runtime; in urgent cases the `dist/` files
103
+ (and their `.d.ts`) have been patched directly. The `/update-blox` skill covers version bumps.
104
+
105
+ ## Conventions & gotchas
106
+
107
+ - **Tabs, not spaces** for indentation in this repo's source.
108
+ - **`BaseButton` is a default import:** `import BaseButton from "@agilant/toga-blox/dist/components/BaseButton/BaseButton.js"`.
109
+ - **Per-consumer React Query cache scope:** when two components fetch the same record with
110
+ different field configs, share a `scope` discriminator in the query key so they don't clobber
111
+ each other's cached data (e.g. record modal vs. approval modal — `scope="record-modal"` vs
112
+ `scope="approval-modal"`). Including `additionalData` in the query key is mandatory for
113
+ WHERE-filtered modal tables, or React Query serves a stale unfiltered first render.
114
+ - **Typecheck with `npx tsc --noEmit` / `npx tsc -b`** after changes; adding a required key to
115
+ `ClientFields` surfaces every bundle missing it.
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: Client-Configurable Fields (useClientFields / fieldsConfig)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/src/fieldsConfig/index.ts
13
+ - toga25-supply/src/fieldsConfig/useClientFields.ts
14
+ - toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/
15
+ - toga25-supply/src/pages/Inventory/viewModel/FIELDS/
16
+ - toga25-supply/src/pages/Inventory/README.md
17
+ related:
18
+ - ../architecture.md
19
+ - column-visibility.md
20
+ ---
21
+
22
+ ## What it is
23
+
24
+ The mechanism for config that **varies by client** (or client × role) — field overrides,
25
+ filter buttons, group-by options, column pickers, layout toggles — without hardcoding it in
26
+ the view model or rebuilding per client. All such config resolves through `src/fieldsConfig`
27
+ and the `useClientFields()` hook.
28
+
29
+ ## How it works
30
+
31
+ 1. **Per-client JSON lives next to the consuming page**, under `FIELDS/<CLIENT>/`
32
+ (e.g. `src/pages/SalesOrders/viewModel/FIELDS/COMPASS/salesOrdersPageFields.json`). There is
33
+ **always a `DEFAULT/`** folder every client falls back to. `<CLIENT>` must match the hostname
34
+ `clientSlug` (uppercase — `COMPASS`, `NYCHH`, …).
35
+ 2. **`src/fieldsConfig/index.ts`** imports those JSON files and wires them into the
36
+ `ClientFields` bundle — a normalized `FIELDS[clientSlug][role]` map merged over `DEFAULT`.
37
+ 3. **`useClientFields()`** resolves the active `clientSlug` (from `useHostnameStore`) + `role`
38
+ (from `resolveRole`), looks up the bundle, falls back to `DEFAULT_CLIENT_FIELDS`, and caches
39
+ the result in React Query. It **always returns a defined bundle** (DEFAULT while loading/missing),
40
+ so reading `clientFields.yourKey` is safe without optional chaining — but guard array access.
41
+ 4. **The view model reads its slice:**
42
+ ```ts
43
+ const { fields: clientFields } = useClientFields();
44
+ const tenantFields = clientFields.salesOrdersPageFields; // your key
45
+ ```
46
+
47
+ ### By client × role vs. by client only
48
+
49
+ - **Client × role** (e.g. `salesOrdersPageFields` — ADMIN vs MANAGER see different filter buttons):
50
+ the JSON is keyed by role at the top level (`{ "ADMIN": {...}, "MANAGER": {...} }`); use a
51
+ `pick*`/`merge*` helper to merge `DEFAULT[role]` with the client's `[role]`.
52
+ - **Client only** (e.g. `inventoryGroupings` — every role sees the same group-by options): the JSON
53
+ is flat, and you **inject** it across every role bundle for that client rather than hand-writing
54
+ it into each. Author the map literal as `FIELDS_BASE` (typed `Omit<ClientFields, "yourKey">`),
55
+ then build `FIELDS` by mapping each `[clientSlug][role]` entry and grafting
56
+ `yourKey: BY_CLIENT[clientSlug] ?? DEFAULT_*`. Avoids editing all ~13 role bundles.
57
+ `DEFAULT_CLIENT_FIELDS` must also carry the new key (ultimate fallback).
58
+
59
+ ### Hydration (non-serializable bits)
60
+
61
+ Keep JSON **serializable** — strings, enums, arrays. No JSX, no functions, no Tailwind classes.
62
+ Anything non-serializable is referenced by an **enum key** and resolved in the view model via a
63
+ registry that maps enum → real value (e.g. `MODAL_RENDERERS: Record<ModalKey, renderModal>`).
64
+ This is the one place app-owned JSX is grafted back onto client-authored config. Clients never
65
+ author JSX.
66
+
67
+ ## Adding a new client-configurable field
68
+
69
+ 1. Author the serializable JSON under `FIELDS/DEFAULT/<thing>.json` + a folder per overriding client.
70
+ 2. Declare the types next to the JSON (`FIELDS/index.ts`) and export the shape.
71
+ 3. Wire into `fieldsConfig/index.ts`: add the key to `ClientFields`, import each client's JSON,
72
+ add the value to **every** bundle + `DEFAULT_CLIENT_FIELDS` (or use the injection pattern).
73
+ 4. Consume in the view model via `useClientFields()`.
74
+ 5. Hydrate enum→value in the view model if the config references components/render callbacks.
75
+ 6. `npx tsc -b` — a new required `ClientFields` key surfaces every bundle missing it.
76
+
77
+ ## Gotchas
78
+
79
+ - **Always provide `DEFAULT`.** Resolution order: `FIELDS[client]?.[role]` → `FIELDS[client]?.DEFAULT`
80
+ → `DEFAULT_CLIENT_FIELDS`. A client/role with no entry must still render.
81
+ - **New client *file* = no code change** (just import/wiring). **New render style/modal type
82
+ (new enum value) = code change** in the hydration registry — that's the intended boundary
83
+ between client config and app rendering.
84
+ - Worked examples: `salesOrdersPageFields` (client × role) and `inventoryGroupings` (client-only +
85
+ hydration; full write-up in `src/pages/Inventory/README.md`).
86
+
87
+ ## Change history
88
+ - 2026-06-23 — Documented from the `add-client-fields` skill during initial knowledge seed. (apeterson)
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: Column Visibility (URL-driven show/hide columns)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/src/components/ColumnVisibilityModal/
13
+ - toga25-supply/src/pages/SalesOrders/SalesOrders.tsx
14
+ - toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
15
+ - toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx
16
+ related:
17
+ - ../architecture.md
18
+ - client-configurable-fields.md
19
+ - meta-driven-table-data.md
20
+ ---
21
+
22
+ ## What it is
23
+
24
+ A "Columns" header button that opens a modal listing every column from the table meta, lets the
25
+ user show/hide columns, adjusts the table live, and persists the selection in the URL as a
26
+ shareable link. The button's presence/label/icon — and the modal's entire copy — are driven per
27
+ client × role via the FIELDS config. Built for pages using `PrimaryTableServerLayout` (the Sales
28
+ Orders pattern). Reference: `ColumnVisibilityModal` + Sales Orders page/view model/data hook.
29
+
30
+ ## How the table renders columns (key facts)
31
+
32
+ - The table meta (`TableViewMeta` / `DataTableMetaProp`) carries `fields: TableViewField[]`; each
33
+ field has a unique `slug` and an `isVisible`. `buildTanstackColumns` (toga-blox) **filters by
34
+ `isVisible`** before building columns, so the tanstack column `id` is the field `slug`. Hiding =
35
+ `isVisible: false` or removing the field — identical column set.
36
+ - `getDataTableData` builds the API field list **from `meta.fields`**. So the meta used for
37
+ *fetching* must keep all fields; only the meta passed to the *table* should be visibility-adjusted.
38
+ **Keep two metas separate: fetch = full, display = adjusted.**
39
+
40
+ ## URL-state semantics
41
+
42
+ Single param `cols` = comma-separated **visible** column slugs.
43
+ - **Absent** → fall back to each field's default `isVisible`.
44
+ - **Present (even empty)** → only the listed slugs are visible. Distinguish absent from empty with
45
+ `searchParams.has("cols")`, NOT truthiness (so "hide all" works):
46
+
47
+ ```ts
48
+ const visibleSlugSet = searchParams.has("cols")
49
+ ? new Set((searchParams.get("cols") ?? "").split(",").filter(Boolean))
50
+ : null;
51
+ const isFieldVisible = (f) => visibleSlugSet ? visibleSlugSet.has(f.slug) : f.isVisible;
52
+ ```
53
+
54
+ ## Steps
55
+
56
+ 1. **Reuse the modal.** `ColumnVisibilityModal` is generic and holds **zero hardcoded copy** —
57
+ props `{ isOpen, columns: { slug, label, visible }[], labels, icon, onApply(visibleSlugs),
58
+ onReset?, onClose }`. `labels` = `{ title, showAll, hideAll, reset, cancel, apply, close }`;
59
+ `icon` = `{ name, variant }`. It keeps pending state internally, commits only on Apply.
60
+ 2. **View model — derive visibility from URL.** Add `visibleSlugSet`/`isFieldVisible`,
61
+ `columnOptions` (`fields.map(f => ({ slug, label: f.label ?? f.slug, visible: isFieldVisible(f) }))`),
62
+ `displayTableMeta` (`{ ...meta, fields: fields.map(f => ({ ...f, isVisible: isFieldVisible(f) })) }` —
63
+ return THIS as the table meta; data hook keeps full meta), `columnVisibilityKey` (visible slugs
64
+ joined; remount key), and `applyColumnVisibility(slugs)`/`resetColumnVisibility()`.
65
+ 3. **Data hook — exclude `cols` from the query key.** Where the slug key is stripped from
66
+ `tableSearchParams`, also `filtered.delete("cols")` — else every toggle refetches.
67
+ 4. **Page — button + modal + remount key.** Local `isColumnsModalOpen` state; render the button
68
+ from client config; pass `columnsButton.modal.labels`/`.icon` to the modal; put
69
+ `key={columnVisibilityKey}` on the table layout (required — see gotcha).
70
+ 5. **Make button + all modal copy client-configurable** (mirrors `filterButtons`; see
71
+ [client-configurable-fields](client-configurable-fields.md)). Add a `columnsButton` block to each
72
+ role in `FIELDS/<CLIENT>/<page>Fields.json` (and to `DEFAULT` first). Resolve it off `tenantFields`
73
+ in the view model into `{ icon, labels }` with **per-key fallbacks living in the resolution layer**,
74
+ never in the component.
75
+ 6. **Verify:** `npx tsc --noEmit`, validate each JSON parses, click live (toggle, Apply, copy URL,
76
+ reload → state persists).
77
+
78
+ ## Gotchas
79
+
80
+ - **No hardcoded copy in the component** — every user-facing string (incl. Close `aria-label`) is a
81
+ required `labels` prop from client FIELDS; fallbacks live in the view model's resolution, not the
82
+ component, not default prop values. Only structural glyphs (checkbox tick, close `xmark`) stay.
83
+ - **Remount key is REQUIRED.** toga-blox's `PrimaryTableRow` memoizes
84
+ `useMemo(() => row.getVisibleCells(), [row])`; TanStack caches `row` by data, so a columns-only
85
+ change doesn't change `row` → stale cells. Symptom: **toggling off removes the header but leaves the
86
+ body column.** `key={columnVisibilityKey}` forces a fresh table instance — the only fix without
87
+ editing `node_modules`. Real upstream fix: repair that memo in toga-blox.
88
+ - **Don't shrink the fetch meta** — adjust `isVisible` only on the table meta; `getDataTableData`
89
+ builds its field list from `meta.fields`.
90
+ - **`cols` must be stripped from the data query** or every toggle refetches.
91
+ - **`has("cols")` vs truthiness** — empty `cols` is a real "hide all" state, not "absent".
92
+ - **Checkbox can't be a nested `<button>`** — the row is the clickable `<button>`; the checkbox is a
93
+ visual `<span aria-hidden>`. A button-in-button is invalid HTML and silently breaks clicks.
94
+ - **Shareable-link trade-off** — an explicit `cols` set captures an exact view; columns added to the
95
+ meta later won't appear for an old link until Reset (intended).
96
+
97
+ ## Change history
98
+ - 2026-06-23 — Documented from the `add-column-visibility` skill during initial knowledge seed. (apeterson)
@@ -0,0 +1,176 @@
1
+ ---
2
+ title: Meta-Driven Page & Table Setup
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
13
+ - toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts
14
+ - toga25-supply/src/hooks/useTablePageMeta.ts
15
+ - toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts
16
+ - toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts
17
+ - toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts
18
+ - toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts
19
+ related:
20
+ - ../architecture.md
21
+ - column-visibility.md
22
+ - record-modals-and-nested-tables.md
23
+ ---
24
+
25
+ ## What it is
26
+
27
+ A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels,
28
+ sections, ACL) and *table meta* (the columns/fields + table settings), then fetches the *table
29
+ data* — each step keyed by a slug, each hook supplied by `@agilant/toga-blox`. A developer wires a
30
+ new page by composing these hooks in a view model; they never hand-build columns or choose a
31
+ fetch strategy. `useSalesOrdersViewModel`
32
+ (`src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx`) is the canonical example.
33
+
34
+ ## The flow (Sales Orders)
35
+
36
+ ```
37
+ useSalesOrdersViewModel
38
+ │
39
+ │ 1. PAGE META ── page slug "sales-orders-listing", app "supply"
40
+ ├─▶ useFetchPageMeta("sales-orders-listing", { app: "supply", enabled: true }) [toga-blox]
41
+ │ → pageMeta: PageMeta { settings: { sections, fields }, acl }
42
+ │ (field-label/section overrides + ACL for this page)
43
+ │
44
+ │ 2. TABLE SLUG ── from the route, not from pageMeta
45
+ ├─▶ useSalesOrdersTableState() → useServerTableUrlState(slug, …) [app]
46
+ │ slug = location.pathname.replace("/", "") // e.g. "salesorders"
47
+ │ also owns sorting / filters / page / recordsPerPage / activeRowUuid (URL state)
48
+ │
49
+ │ 3. TABLE META ── table slug → columns + table settings
50
+ ├─▶ useTablePageMeta({ slug: tableState.slug }) [app wrapper]
51
+ │ └─ useFetchTablePageMeta({ slug }) [toga-blox]
52
+ │ → { table: TableViewMeta, nestedTable }, isLoadingFilterOptions, …
53
+ │ table.fields[] = every column (TableViewField[])
54
+ │ table.{ slug, route, joins, sort, recordsPerPage,
55
+ │ isPaginationEnabled, isInfiniteScrollingEnabled, apiWhereClause }
56
+ │
57
+ │ 4. MERGE LABELS ── graft page-meta labels onto table fields
58
+ ├─▶ useAssignTableFieldLabels(fetchedTableViewMeta, pageMeta) [toga-blox]
59
+ │ → salesOrdersTableMeta: TableViewMeta | null
60
+ │
61
+ │ 5. TABLE DATA ── same table slug → rows
62
+ └─▶ useTableData({ slug: tableState.slug, tableViewMeta: salesOrdersTableMeta,
63
+ isPaginationEnabled: !!salesOrdersTableMeta?.isPaginationEnabled,
64
+ sorting, columnFilters, page, recordsPerPage,
65
+ searchParams, additionalData }) [toga-blox]
66
+ → { records, isLoadingTableDataLoading, isFetching,
67
+ paginationMeta, fetchNextPage, hasNextPage }
68
+ ```
69
+
70
+ The view model then returns `salesOrdersTableMeta` (after column-visibility adjustment — see
71
+ [column-visibility](column-visibility.md)), `displayRecords`, the pagination/infinite wiring, and
72
+ `tableSettings = { isPaginationEnabled }` for the `*TableLayout` to forward to
73
+ `PrimaryTableServerLayout`.
74
+
75
+ ### Two slugs, two metas — keep them straight
76
+
77
+ - **Page slug** (`"sales-orders-listing"`) is a literal passed to `useFetchPageMeta` and identifies
78
+ the *page* (its label overrides, sections, ACL). It does **not** identify the table.
79
+ - **Table slug** (`tableState.slug`) identifies the *table* and is used for **both** the table-meta
80
+ fetch (step 3) and the table-data fetch (step 5) — they must match so the columns and the rows
81
+ describe the same table. In Sales Orders this slug is derived from the route pathname via
82
+ `useServerTableUrlState`, not read out of `pageMeta`.
83
+ - `useAssignTableFieldLabels` is the bridge: it overlays the page meta's per-field labels
84
+ (`pageMeta.settings.fields[table][field].label`) onto the table meta's fields, so page-scoped
85
+ copy wins without re-fetching.
86
+
87
+ ## What's in the table meta (the columns)
88
+
89
+ `useFetchTablePageMeta` returns `{ table, nestedTable }`. The `table` object is the `TableViewMeta`
90
+ (alias of `DataTableMetaProp`) and carries:
91
+
92
+ - `fields: TableViewField[]` — **every column**. Each field: `slug` (unique id / tanstack column
93
+ id), `label`, `isVisible`, `index`, `type` (`STRING | NUMBER | DATE | DATETIME | SELECT |
94
+ MULTISELECT | IMAGE | HYPERLINK | STATUS | CURRENCY | AVATAR | LINK | URGENCY | CLIENT |
95
+ STATUS_BADGE | BOOLEAN | null`), `isEditable`, `isSortable`, `isFilterable`, `isCopyable`,
96
+ `isExpandableCell`, `width`, `sticky` (`left|right|null`), `precision`, `casing`, `recordRoute`,
97
+ `hyperlinkField`, `imageUrlField`, `filterOptions`, `settings: { table, field, contextTable,
98
+ contextField }`, `selectIdentifierField`, `selectNameField`.
99
+ - Table-level: `slug`, `route`, `table`, `joins[]`, `sort[]`, `recordsPerPage`, `apiWhereClause`,
100
+ `isPaginationEnabled`, `isInfiniteScrollingEnabled`.
101
+ - `nestedTable` — a second `TableViewMeta | null` for nested/expand rows.
102
+
103
+ `buildTanstackColumns` (toga-blox) builds columns from `fields`, filtering by `isVisible` (the
104
+ tanstack column `id` is the field `slug`).
105
+
106
+ ## Pagination vs. infinite scroll is meta-driven too
107
+
108
+ `useTableData` runs **both** a paginated `useQuery` and an infinite `useInfiniteQuery`, gating each
109
+ with `enabled` on `isPaginationEnabled` so only the active one fetches, and returns one normalized
110
+ shape (`records`, `paginationMeta` *or* `fetchNextPage`/`hasNextPage`). The view model passes only
111
+ `isPaginationEnabled` (from the table meta); `PrimaryTable*Layout` derives
112
+ `isInfiniteScrollingEnabled = !isPaginationEnabled`. So a table's mode comes from its meta, never a
113
+ per-page hook choice. Other baked-in behavior: unified `["table-data", slug, …]` query key (a
114
+ blanket `invalidateQueries({ queryKey: ["table-data"] })` refetches either mode); display params
115
+ (`<slug>` active-row uuid, `cols`) stripped from the key/API call; generic record extraction (first
116
+ array in `response.data`).
117
+
118
+ ## From toga-blox `dist` (reference)
119
+
120
+ Everything in the pipeline except the route-derived table slug comes from `@agilant/toga-blox`. The
121
+ app imports the built `dist/` — edits to toga-blox source require a rebuild (`cd toga-blox-npm &&
122
+ npm run build`) before they reach runtime.
123
+
124
+ | Export | `dist` path | Signature / role |
125
+ |---|---|---|
126
+ | `useFetchPageMeta` | `dist/hooks/useFetchPageMeta` | `(slug, { app, enabled?, onError? }) → { pageMeta: PageMeta \| null, labels, isLoading, isError, refetch }`. `PageMeta = { settings: { sections: MetaSections, fields: Record<string,any> }, acl }`. Page-level labels/sections/ACL. |
127
+ | `useFetchTablePageMeta` | `dist/hooks/useFetchTablePageMeta` | `({ slug }) → { tableViewMeta: { table, nestedTable }, isLoadingFilterOptions, isLoadingTableDataLoading, isFetching }`. The columns + table settings. |
128
+ | `useAssignTableFieldLabels` | `dist/hooks/useAssignTableFieldLabels` | `(fetchedTableViewMeta: { table? } \| null, pageMeta: { settings?: { fields? } } \| null) → TableViewMeta \| null`. Merges page-meta field labels onto table fields. |
129
+ | `useTableData` | `dist/components/Table/hooks/useTableData` | `(UseTableDataOptions) → { records, isLoadingTableDataLoading, isFetching, paginationMeta, fetchNextPage, hasNextPage }`. Bimodal data fetch (see above). |
130
+ | `TableViewMeta` (type) | `dist/components/Table/types` → `= DataTableMetaProp` | The `table`/`nestedTable` shape; `TableSkin = "supply" \| "desk" \| "supply-nested" \| "supply-modal-table"`. |
131
+ | `TableViewField` (type) | `dist/api/index` | Per-column shape (see "What's in the table meta"). |
132
+
133
+ App-side glue (NOT in toga-blox):
134
+ - `useTablePageMeta` (`src/hooks/useTablePageMeta.ts`) — thin wrapper over `useFetchTablePageMeta`
135
+ that re-types `table`/`nestedTable` as `TableViewMeta | null` and re-exposes the rest unchanged.
136
+ - `useSalesOrdersTableState` → `useServerTableUrlState` (`src/hooks/`) — derives the **table slug**
137
+ from the route pathname and owns URL-backed sorting/filters/pagination/active-row state.
138
+
139
+ ## Wiring a new page (recipe)
140
+
141
+ 1. `useFetchPageMeta("<page-slug>", { app: "supply", enabled: true })` → `pageMeta`.
142
+ 2. A table-state hook (`use<Page>TableState` → `useServerTableUrlState(slug, …)`) to get the table
143
+ `slug` + URL state (`sorting`, `columnFilters`, `page`, `recordsPerPage`, `searchParams`).
144
+ 3. `useTablePageMeta({ slug })` → `tableViewMeta` (columns + settings).
145
+ 4. `useAssignTableFieldLabels(tableViewMeta, pageMeta)` → the merged meta you hand to the table.
146
+ 5. `useTableData({ slug, tableViewMeta: merged, isPaginationEnabled: !!merged?.isPaginationEnabled,
147
+ sorting, columnFilters, page, recordsPerPage, searchParams, additionalData })` → rows.
148
+ 6. `tableSettings = useMemo(() => ({ isPaginationEnabled: merged?.isPaginationEnabled }), [merged])`.
149
+ 7. Return both pagination wiring (`paginationMeta`, `handlePageChange`, `handleRecordsPerPageChange`)
150
+ **and** infinite wiring (`fetchNextPage`, `hasNextPage`); forward all from the `*TableLayout` to
151
+ `PrimaryTableServerLayout`.
152
+
153
+ ## Gotchas
154
+
155
+ - **Page slug ≠ table slug.** The page slug is a literal for `useFetchPageMeta`; the table slug
156
+ (route-derived here) drives table meta *and* table data — those two must use the **same** slug.
157
+ - **Server meta currently returns `isPaginationEnabled: true` for every slug**, so all tables
158
+ paginate today. A table that should infinite-scroll needs the backend to return `false` for that
159
+ slug — the frontend no longer hardcodes it. (This is why VendorItems, historically infinite, now
160
+ paginates.)
161
+ - **Don't shrink the fetch meta for column hiding.** `getDataTableData` builds its API field list
162
+ from `meta.fields`; adjust `isVisible` only on the *display* meta (see column-visibility).
163
+ - **Infinite scroll needs a height-constrained scroll container** (`max-height` + `overflow-y: auto`);
164
+ inline-expanded nested tables where the page scrolls won't load more. Pagination is unaffected.
165
+ - **`additionalData` must be in the query key** (it is, inside `useTableData`) — required for
166
+ WHERE-filtered modal tables or React Query serves a stale unfiltered first render.
167
+ - **toga-blox edits need a rebuild** — the app consumes `dist/`; editing only `src` is a silent no-op.
168
+
169
+ ## Change history
170
+ - 2026-06-23 — Refocused on the full page→table→data meta pipeline (page meta via `useFetchPageMeta`,
171
+ table meta via `useTablePageMeta`/`useFetchTablePageMeta`, label merge via
172
+ `useAssignTableFieldLabels`, data via `useTableData`), grounded in `useSalesOrdersViewModel`; added
173
+ a toga-blox `dist` reference table. (apeterson)
174
+ - 2026-06-09→06-10 — Migration that established the bimodal `useTableData`: added it to toga-blox;
175
+ made `PrimaryTable*Layout` derive `isInfiniteScrollingEnabled`; migrated Items/SalesOrders/VendorItems
176
+ view models + `GenericNestedTables` + `ItemFulfillmentModal`; deleted per-page data hooks. (apeterson)
@@ -0,0 +1,147 @@
1
+ ---
2
+ title: Record Modals & Nested Tables
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/src/layout/ItemRecordModalLayout/
13
+ - toga25-supply/src/layout/SalesOrderRecordModalLayout/
14
+ - toga25-supply/src/layout/SalesOrderItemsTableLayout/
15
+ - toga25-supply/src/layout/ItemFulfillmentModal/
16
+ - toga25-supply/src/layout/GenericNestedTables/
17
+ - toga25-supply/src/hooks/useTableCellInteractions.ts
18
+ related:
19
+ - ../architecture.md
20
+ - meta-driven-table-data.md
21
+ ---
22
+
23
+ ## What it is
24
+
25
+ The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and
26
+ `PrimaryTable*Layout`. Covers: row-click record modals, nested client/server tables inside a
27
+ modal, cell-click modals, modal-contained server tables, the config-driven `GenericNestedTables`
28
+ renderer, and form-driven create/edit record modals. The canonical examples live in
29
+ `src/layout/*` (Sales Orders + Item). These six patterns are the load-bearing UI architecture for
30
+ this app.
31
+
32
+ ## Pattern 1 — Row click opens a record modal (URL-driven)
33
+
34
+ Page renders a `*TableLayout` and passes a `renderModal` prop. The table layout owns all state via
35
+ `use*ViewModel` → `useServerTableUrlState`. Clicking a row sets `activeRowUuid` in the URL;
36
+ `PrimaryTableServerLayout` calls `renderModal({ uuid, onClose })`, which renders the record modal
37
+ wrapped in `TableRecordModal position="bottom"`.
38
+
39
+ - `renderModal` is `({ uuid, onClose }) => ReactNode | null` — return `null` when `uuid` is falsy.
40
+ - `onClose` clears the URL state (`handleModalClose`).
41
+ - The record modal layout calls its own view model to fetch data for that uuid.
42
+ - **Action-column modals** (e.g. approval) use a separate `useState<Record|null>` in the page,
43
+ rendered as a second `TableRecordModal position="center"` — not URL-driven.
44
+
45
+ ## Pattern 2 — Record modal contains a nested client-side table
46
+
47
+ The record modal view renders e.g. `SalesOrderItemsTableLayout` with `displayRecords` from the
48
+ parent view model. The nested table uses `PrimaryTableClientLayout` (data already in memory),
49
+ `tableSkin="supply-modal-table"`, and its **own** slug + `useSearchParams` + `useServerTableUrlState`
50
+ so URL-state namespacing doesn't collide with the outer table. Column widths come from
51
+ `src/pages/<Entity>/dummyData/*.json`; `headerSpan` adds a spanning header above grouped columns.
52
+
53
+ ## Pattern 3 — Cell click within a nested table opens another modal
54
+
55
+ Driven by the reusable hook `useTableCellInteractions` (`src/hooks/useTableCellInteractions.ts`),
56
+ which owns all click/hover/style logic. Each table's view model only defines the registries:
57
+
58
+ ```ts
59
+ const { activeModal, closeModal, handleTableClick, handleTableMouseOver,
60
+ handleTableMouseOut, applyClickableStyles } = useTableCellInteractions({
61
+ fields, cellClickRegistry, headerClickRegistry /* optional */, contextUuid /* optional */ });
62
+ ```
63
+
64
+ - `cellClickRegistry[key]` (key matches the field's `onClick` in the table meta JSON):
65
+ `{ canOpen?(row), getModalSpec(row) => ({ key, uuid, row }) }`. `canOpen` is the single source
66
+ of "is this cell clickable?" — used by the click handler, hover styles, and cursor styles.
67
+ - `headerClickRegistry[key]` (key matches the field's `headerOnClick`):
68
+ `{ getModalSpec(contextUuid) => ({ key, uuid, row }) }`.
69
+ - Table meta JSON marks a field: `{ "name": "qtyFulfilled", "onClick": "openFulfilledModal",
70
+ "headerOnClick": "openAllFulfillments" }` — both independent.
71
+
72
+ Layout setup: wrap the table in a div with `onClick={(e) => handleTableClick(e, displayRecords)}`
73
+ + mouseover/out; re-apply styles on DOM changes via a `MutationObserver` calling
74
+ `applyClickableStyles` (virtual scroll re-renders rows); render each modal as a sibling switching
75
+ on `activeModal.key`. **Adding a clickable column = 3 edits:** meta JSON `onClick`, one
76
+ `cellClickRegistry` entry, one `{activeModal?.key === "myKey" && <Modal/>}` block.
77
+
78
+ ## Pattern 4 — Modal containing a server-side table
79
+
80
+ For modal tables needing server data, use `PrimaryTableServerLayout` inside `TableRecordModal
81
+ position="bottom"`. The modal view model calls `useFetchTablePageMeta` + `useTablePageData`
82
+ (or `useTableData`) with an `additionalData` WHERE filter, e.g. `{ slug: "SalesOrderItems.uuid",
83
+ uuid: "<row-uuid>" }`. **`additionalData` MUST be in the query key** (in toga-blox) or React Query
84
+ caches the unfiltered first render (uuid null) and never refetches. Guard with `isOpen={!!uuid}`;
85
+ when uuid is null, `additionalData` returns `{}`. Typically `isPaginationEnabled: false` +
86
+ `isInfiniteScrollingEnabled: true` (but see [meta-driven-table-data](meta-driven-table-data.md)).
87
+
88
+ ## Pattern 5 — `GenericNestedTables` (config-driven multi-level / swap tables)
89
+
90
+ A config-driven renderer: declare an array of `TableLevel` objects instead of hand-wiring each
91
+ level. Each level fetches its own meta + data and renders a `GenericTableLayout` (wraps
92
+ `PrimaryTableServerLayout`). Reference: `src/pages/Inventory/`.
93
+
94
+ `TableLevel` carries `fetchSlug`, `metaVariant`, `tableSkin`/`swapTableSkin`, `actionColumns`,
95
+ `renderModal` (Pattern 1), `rowInteraction` (`"expand"` default | `"swap"`), `urlParamKey`
96
+ (required for swap, unique per level), `getAdditionalData(parentUuid)`,
97
+ `getSwapAdditionalData(selfUuid)`.
98
+
99
+ - **`"expand"`** — clicking a row inline-expands the next level beneath it (filtered via
100
+ `getAdditionalData(parentUuid)`).
101
+ - **`"swap"`** — clicking writes `urlParamKey=<rowUuid>`; the renderer makes that level the new
102
+ active root, filtered by `getSwapAdditionalData(selfUuid)` (using `swapTableSkin`).
103
+
104
+ `additionalData` is produced per-level by callbacks (the WHERE-filter shape from Pattern 4). Each
105
+ level runs its own `useServerTableUrlState` and strips its own slug key, isolating URL state.
106
+ A page-level `pageMeta` (from `useFetchPageMeta`) flows down for field-label overrides. The page
107
+ view model builds `TableLevel[]` arrays, picks the active one via a URL param, and passes `levels`
108
+ to `GenericNestedTables` with `key={activeGrouping.key}` to force remount and avoid stale state.
109
+
110
+ ## Pattern 6 — Create/edit record modal (form-driven)
111
+
112
+ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLayout/`). Form-driven
113
+ (react-hook-form + `FormProvider`), fields rendered through a section/field config + `DetailSection`
114
+ + `BaseInput`, inside `TableRecordModal position="bottom" variant="expandable"`.
115
+
116
+ ```
117
+ <Entity>RecordCreateModalLayout ← owns form hook, mounts TableRecordModal
118
+ └─ <Entity>RecordCreate ← FormProvider + form, renders sections
119
+ ├─ use<Entity>CreateForm ← rhf state + apiPost submit
120
+ └─ use<Entity>RecordEditViewModel ← fetches advancedSelect options
121
+ ```
122
+
123
+ - Form hook: `useForm({ defaultValues, mode: "onChange", reValidateMode: "onChange" })`; `onSubmit`
124
+ builds a payload (map relation objects to `.uuid`), `apiPost("/<entities>", payload)`, then
125
+ `queryClient.invalidateQueries({ queryKey: ["table-data"] })`, reset, `onCreated?.()`. Edit
126
+ variant: `form.reset(record)` in a `useEffect`, `apiPut`, expose `isEditing`/`onEnableEdit`/`onCancelEdit`.
127
+ - Field types: `text`, `textArea`, `toggle`, `advancedSelect`, `advancedMultiSelect`; mark required
128
+ with `isRequired` + `showRequiredIndicator`; feed select options via `optionsByFieldUuid` keyed by
129
+ field `uuid`. The edit view model that supplies options is **reused** by both create and edit.
130
+ - Page holds `isCreateOpen` state + a "New <Entity>" `BaseButton`, renders the layout with `isOpen`/`onClose`.
131
+
132
+ ## Gotchas
133
+
134
+ - **Files use TABS** — match exactly or edits won't apply.
135
+ - **`BaseButton` is a default import:** `@agilant/toga-blox/dist/components/BaseButton/BaseButton.js`.
136
+ - **Submit payload diverges from form shape** — send relation `.uuid`, not the `{uuid,name}` object;
137
+ confirm field names against the real POST/PUT contract.
138
+ - **Nested tables need their own slug + URL-state isolation** or sibling tables clobber each other.
139
+ - **`additionalData` in the query key is mandatory** for WHERE-filtered modal tables (Pattern 4).
140
+ - **Per-consumer cache `scope`** — two consumers fetching the same record with different field
141
+ configs must use a `scope` discriminator in the query key or they overwrite each other's cache
142
+ (record modal vs. approval modal: the approval modal omitting item-image fields blanked the
143
+ record modal's images until scoping isolated the entries).
144
+
145
+ ## Change history
146
+ - 2026-06-23 — Documented from CLAUDE.md Modal Patterns 1–5 + the `create-record-modal` skill during
147
+ initial knowledge seed. (apeterson)
@@ -4,13 +4,14 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 6 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 7 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 10 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **togadesk** (TOGa Desk) — 7 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
10
10
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
11
11
  - **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
12
12
  - **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
13
13
  - **test** (Test) — 11 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
14
+ - **toga** (TOGa) — 1 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
14
15
 
15
16
  ## 2.0 framework
16
17
 
@@ -26,6 +27,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
26
27
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
27
28
  - **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
28
29
  - **toga2-commerce** (TOGa Commerce) — 2 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
30
+ - **toga25-supply** (TOGa 2.5 Supply) — 5 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
29
31
 
30
32
  ## standalone framework
31
33
 
@@ -3,4 +3,5 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [ODP Tech Support Agent (Office Depot variant)](features/odp-tech-support.md) | 2.0 | ODP-specific customization of the shared TOGa Voice agent. | voice-to-voice/clients/odp/agent.py, voice-to-voice/clients/odp/agents/tech_support.py, voice-to-voice/clients/odp/prompts.py, voice-to-voice/clients/odp/config.yaml, voice-to-voice/clients/odp/.env.example |
6
+ | [TOGaRefresh 2026 — linkBuilderRefresh URL Routing (ODP)](features/togarefresh2026-linkbuilder-routing.md) | 1.0 | In the TOGaRefresh 2026 UI, any button that navigates to the service builder must use `App_Model_ServiceRequest::linkBuilderRefresh()` (→ `togarefresh2026_servi | toga/app/togarefresh2026/servicerequests/review.php, toga/app/togarefresh2026/customers/view.php, library/app/model/toga/diagnostic.php |
6
7
  | [Office Depot](profile.md) | 2.0 | Office Depot is the first deployed tenant of the **TOGa Voice** (`voice-to-voice`) platform. | |
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: TOGaRefresh 2026 — linkBuilderRefresh URL Routing (ODP)
3
+ framework: "1.0"
4
+ repo: toga
5
+ project: TOGa
6
+ client: office-depot
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [snaredla]
11
+ files:
12
+ - toga/app/togarefresh2026/servicerequests/review.php
13
+ - toga/app/togarefresh2026/customers/view.php
14
+ - library/app/model/toga/diagnostic.php
15
+ related:
16
+ - ../profile.md
17
+ - ../../../1.0/apps/library/features/diagnostic-dialog-view-recommended-services.md
18
+ ---
19
+
20
+ ## Summary
21
+ In the TOGaRefresh 2026 UI, any button that navigates to the service builder must use
22
+ `App_Model_ServiceRequest::linkBuilderRefresh()` (→ `togarefresh2026_servicerequests_serviceselection`)
23
+ instead of the legacy `linkBuilder()` (→ `servicerequests_builder`).
24
+
25
+ ## Key files / entry points
26
+ - `App_Model_ServiceRequest::BUILDER` = `'servicerequests_builder'` (legacy)
27
+ - `App_Model_ServiceRequest::BUILDERREFRESH` = `'togarefresh2026_servicerequests_serviceselection&jobJacketId=1'`
28
+ - Both constants defined in `library/app/model/servicerequest.php` lines 11–12.
29
+
30
+ ## How it works
31
+ Files updated to use `linkBuilderRefresh`:
32
+
33
+ | File | Location | Context |
34
+ |------|----------|---------|
35
+ | `togarefresh2026/servicerequests/review.php` | line 217 | Quote submission POST URL for non-EXISTING job jackets |
36
+ | `togarefresh2026/customers/view.php` | line 64 | "Create Work Order For This Customer" button |
37
+ | `library/app/model/toga/diagnostic.php` | `initializeDiagnosticDialog()` | Device modal button — routes by diagnostic pass/fail (see library doc) |
38
+
39
+ ## Gotchas / known issues
40
+ - The old `servicerequests/view.php` (legacy non-refresh view) intentionally keeps `linkBuilder()` — 2026 stores do not route through it.
41
+ - Do not change `App_Model_ServiceRequest::BUILDER` — legacy stores still depend on it.
42
+ - The diagnostic modal routing is pass/fail conditional — see `1.0/apps/library/features/diagnostic-dialog-view-recommended-services.md` for full details.
43
+ - `toga` app repo was not in registry.json before this session — it has been added (framework 1.0, role app, dependsOn library).
44
+
45
+ ## Related docs
46
+ - `1.0/apps/library/features/diagnostic-dialog-view-recommended-services.md`
47
+
48
+ ## Change history
49
+ - 2026-06-23 — Initial doc. All 2026 builder buttons now use linkBuilderRefresh; diagnostic modal uses pass/fail routing. toga added to registry.json. (snaredla)
@@ -159,5 +159,21 @@
159
159
  "framework": "2.0",
160
160
  "role": "app",
161
161
  "dependsOn": ["api2"]
162
+ },
163
+ {
164
+ "repo": "toga",
165
+ "project": "TOGa",
166
+ "framework": "1.0",
167
+ "role": "app",
168
+ "dependsOn": [
169
+ "library"
170
+ ]
171
+ },
172
+ {
173
+ "repo": "toga25-supply",
174
+ "project": "TOGa 2.5 Supply",
175
+ "framework": "2.0",
176
+ "role": "app",
177
+ "dependsOn": []
162
178
  }
163
- ]
179
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.161",
3
+ "version": "1.0.163",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",