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.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/diagnostic-dialog-view-recommended-services.md +49 -0
- package/knowledge/1.0/apps/toga/INDEX.md +4 -0
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +9 -0
- package/knowledge/2.0/apps/toga25-supply/architecture.md +115 -0
- package/knowledge/2.0/apps/toga25-supply/features/client-configurable-fields.md +88 -0
- package/knowledge/2.0/apps/toga25-supply/features/column-visibility.md +98 -0
- package/knowledge/2.0/apps/toga25-supply/features/meta-driven-table-data.md +176 -0
- package/knowledge/2.0/apps/toga25-supply/features/record-modals-and-nested-tables.md +147 -0
- package/knowledge/INDEX.md +3 -1
- package/knowledge/clients/office-depot/INDEX.md +1 -0
- package/knowledge/clients/office-depot/features/togarefresh2026-linkbuilder-routing.md +49 -0
- package/knowledge/registry.json +17 -1
- package/package.json +1 -1
|
@@ -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,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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
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)
|
package/knowledge/registry.json
CHANGED
|
@@ -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