toga-ai 1.0.804 → 1.0.805

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.
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-31
9
+ updated: 2026-09-14
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga-blox/src/components/Table/themeConfig/toga.module.css
@@ -22,6 +22,7 @@ files:
22
22
  related:
23
23
  - ../architecture.md
24
24
  - table.md
25
+ - ../../toga25-supply/features/server-table-url-state.md
25
26
  ---
26
27
 
27
28
  ## Summary
@@ -132,6 +133,15 @@ runs unconditionally — it no longer no-ops while `isExpanded`.
132
133
 
133
134
  ## Gotchas
134
135
 
136
+ - **`PrimaryTableServerTemplate` is FULLY controlled — its change handlers fire only on real user
137
+ interaction.** `state: { sorting, columnFilters }` is passed in and `manualSorting`,
138
+ `manualFiltering`, and `manualPagination` are all `true`, so `onSortingChange` /
139
+ `onColumnFiltersChange` **never fire on mount**. That is what makes it safe for the consumer to
140
+ mutate other URL state (e.g. reset `page` to 1) inside those handlers: no handler runs without
141
+ user intent, so a shared deep link like `?…_page=3` can never be clobbered on load. blox only
142
+ *reads* the view state; the app owns the URL. See
143
+ [toga25-supply server table URL state](../../toga25-supply/features/server-table-url-state.md).
144
+
135
145
  - **Never gate a hover affordance on a JS `mouseleave` inside a virtualized table.** `mouseleave`
136
146
  is not guaranteed to fire: a virtualized row can translate out from under a stationary cursor on
137
147
  wheel-scroll, and any early-return in the clear handler strands the "hovered" flag set. The toggle
@@ -164,6 +174,9 @@ runs unconditionally — it no longer no-ops while `isExpanded`.
164
174
  not the column set.
165
175
 
166
176
  ## Change history
177
+ - 2026-09-14 — Recorded that the server template is fully controlled (`manual*` + passed-in
178
+ `state`), so its sort/filter handlers never fire on mount — the property a consumer relies on
179
+ when resetting pagination from inside those handlers. No code change. (apeterson)
167
180
  - 2026-08-31 — Host-tokenized the `supply`-skin table shadow: `.tableAndPaginationInner.supply` /
168
181
  `.tableWrapper.supply` now use `box-shadow: var(--primaryTable-box-shadow, 0 1px 3px rgba(0,0,0,0.1))`,
169
182
  adding `--primaryTable-box-shadow` to the host-token set (a design review needed the shadow gone and
@@ -11,6 +11,7 @@
11
11
  | [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 |
12
12
  | [Persisted React Query cache (localStorage `supply-chain-query-cache`)](features/persisted-query-cache.md) | `localStorage["supply-chain-query-cache"]` is **not a hand-written cache**. |
13
13
  | [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`. |
14
+ | [Server Table URL State (page, sort, filters) — `useServerTableUrlState`](features/server-table-url-state.md) | `src/hooks/useServerTableUrlState.ts` is the app's single source of truth for a server-paged table's **view state**: page, records-per-page, sorting, column fil |
14
15
  | [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. |
15
16
  | [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. |
16
17
  | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J |
@@ -23,6 +23,7 @@ related:
23
23
  - ../architecture.md
24
24
  - client-configurable-fields.md
25
25
  - meta-driven-table-data.md
26
+ - server-table-url-state.md
26
27
  - surface-frontend.md
27
28
  - transfer-orders-page.md
28
29
  - ../../dbchanges2/features/surface-layer-schema.md
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
9
+ updated: 2026-09-14
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
@@ -22,6 +22,7 @@ related:
22
22
  - column-visibility.md
23
23
  - record-modals-and-nested-tables.md
24
24
  - transfer-orders-page.md
25
+ - server-table-url-state.md
25
26
  - ../../_underscore/features/page-meta-context-field-settings.md
26
27
  ---
27
28
 
@@ -162,7 +163,10 @@ App-side glue (NOT in toga-blox):
162
163
  - `useTablePageMeta` (`src/hooks/useTablePageMeta.ts`) — thin wrapper over `useFetchTablePageMeta`
163
164
  that re-types `table`/`nestedTable` as `TableViewMeta | null` and re-exposes the rest unchanged.
164
165
  - `useSalesOrdersTableState` → `useServerTableUrlState` (`src/hooks/`) — derives the **table slug**
165
- from the route pathname and owns URL-backed sorting/filters/pagination/active-row state.
166
+ from the route pathname and owns URL-backed sorting/filters/pagination/active-row state. It is the
167
+ app's only writer of `<slug>_page`, and it resets the page to 1 on any filter/sort/page-size
168
+ change — the full param grammar and that rule live in
169
+ [server-table-url-state](./server-table-url-state.md).
166
170
 
167
171
  ## Wiring a new page (recipe)
168
172
 
@@ -198,6 +202,8 @@ App-side glue (NOT in toga-blox):
198
202
  raw camelCase slugs. Plan the label source before building the screen (see above).
199
203
 
200
204
  ## Change history
205
+ - 2026-09-14 — Pointed the table-state bullet at the new `server-table-url-state` doc, which now
206
+ owns the `<slug>_*` param grammar and the reset-to-page-1 rule. No code change. (apeterson)
201
207
  - 2026-08-28 — Recorded that `_Model_Client_TableView::meta()` emits **no `label`** for a column, so
202
208
  every header comes from the page meta and `useAssignTableFieldLabels` **bails on a null `pageMeta`**
203
209
  (`if (!pageFields) return table`): a new list screen without a legacy `<slug>-listing` page-meta
@@ -0,0 +1,147 @@
1
+ ---
2
+ title: Server Table URL State (page, sort, filters) — `useServerTableUrlState`
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-09-14
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/src/hooks/useServerTableUrlState.ts
13
+ - toga25-supply/src/hooks/useServerTableUrlState.test.ts
14
+ - toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts
15
+ related:
16
+ - ../architecture.md
17
+ - meta-driven-table-data.md
18
+ - column-visibility.md
19
+ - record-modals-and-nested-tables.md
20
+ - ../../toga-blox/features/primary-table-templates.md
21
+ - ../../../standards/frontend.md
22
+ ---
23
+
24
+ ## What it is
25
+
26
+ `src/hooks/useServerTableUrlState.ts` is the app's single source of truth for a server-paged
27
+ table's **view state**: page, records-per-page, sorting, column filters, filter/sort ordering,
28
+ and the active (clicked) row. It reads that state out of the query string and gives the page
29
+ back handlers that write it. Every list page goes through it — Sales Orders via
30
+ `useSalesOrdersTableState`, and the same shape for the other pages.
31
+
32
+ All params are **namespaced by the table slug**, so two tables on one URL never collide.
33
+
34
+ > It is the **only writer of `<slug>_page` in the whole app** (verified by grep across `src/`,
35
+ > 2026-09-14). There is no second pagination code path to keep in sync. `useEntitySearch` is for
36
+ > `advancedSelect` dropdown pickers and holds no pagination URL state; the only "search" on a
37
+ > main table view is blox's per-column `HeaderFilterSearch`, which routes into
38
+ > `handleColumnFiltersChange` here.
39
+
40
+ ## The param grammar
41
+
42
+ | Param | Meaning |
43
+ |---|---|
44
+ | `<slug>_page` | current page (default `1`) |
45
+ | `<slug>_recordsPerPage` | page size (default `15`) |
46
+ | `<slug>_sort[i]` | sort at position `i`, value prefixed `+` asc / `-` desc |
47
+ | `<slug>_<field>` | bare form = `includes` (substring match) |
48
+ | `<slug>_<field>_starts` / `_ends` / `_eq` / `_excludes` | `startsWith` / `endsWith` / `exactly` / `excludes` |
49
+ | `<slug>_<field>_in` | multi-select over a closed option set — **exact** values, one SQL `IN` |
50
+ | `<slug>_<field>_between` / `_before` / `_after` / `_min` / `_max` | range params, collected into one `{ params }` filter value |
51
+ | `<slug>_<field>_filter` | multi-chip text filter: comma-separated `mode:value` chips, values URL-encoded, all AND'd |
52
+ | `<slug>_<field>_order` | the filter's display order (default `999`) |
53
+ | `<slug>_actionOrder` | shared order of applied filter/sort chips |
54
+ | `<slug>` (bare) | `activeRowUuid` — the open record modal |
55
+
56
+ The reader loops `searchParams`, matches by suffix, and falls through to `includes` for the bare
57
+ form — which is why the fall-through explicitly **excludes** `_order`, `_sort`, `_page`,
58
+ `_recordsPerPage`, and `_actionOrder` so a control param is never mistaken for a filter. Add a new
59
+ control param and you must add it to that exclusion list.
60
+
61
+ ### `_in` is exact, not `includes`
62
+
63
+ `_in` used to alias the bare `includes` form, so picking `fulfilled` in a multi-select also matched
64
+ `partiallyFulfilled`. It is now its own `mode = "in"` on the read side with a matching
65
+ `case "in":` on the write side: exact values, serialized downstream as one SQL `IN`.
66
+
67
+ ## How it works — any result-changing param resets to page 1
68
+
69
+ 🚨 **This is the load-bearing rule of the hook.** A change to *which records the server returns* —
70
+ a filter/search, a sort, or a page size — must send the user back to page 1.
71
+
72
+ One helper, one home:
73
+
74
+ ```ts
75
+ const FIRST_PAGE = "1";
76
+
77
+ const resetToFirstPage = useCallback(
78
+ (prev: URLSearchParams) => { prev.set(`${slug}_page`, FIRST_PAGE); },
79
+ [slug],
80
+ );
81
+ ```
82
+
83
+ Called by all three result-changing handlers — `handleColumnFiltersChange`,
84
+ `handleSortingChange`, `handleRecordsPerPageChange`.
85
+
86
+ Two details that matter:
87
+
88
+ 1. **The reset is written inside the SAME `setSearchParams(prev => …)` updater** as the
89
+ filter/sort write. One history entry, one React Query refetch — so the back button undoes the
90
+ filter and the page together instead of leaving a half state.
91
+ 2. **`handleRowClick` deliberately does NOT reset.** Opening a record modal must not throw the
92
+ user back to page 1. Neither does `handlePageChange`, obviously.
93
+
94
+ This is the concrete implementation of the rule in
95
+ [`standards/frontend.md` §5](../../../standards/frontend.md) — a param that changes the query
96
+ result belongs in the React Query key *and* resets to page 1.
97
+
98
+ ### Why an app-side reset is safe (it does not clobber deep links)
99
+
100
+ blox's `PrimaryTableServerTemplate` builds the table **fully controlled**: `state: { sorting,
101
+ columnFilters }` with `manualSorting` / `manualFiltering` / `manualPagination: true`. So
102
+ `onColumnFiltersChange` / `onSortingChange` fire **only on real user interaction, never on mount**.
103
+ Nothing runs a reset on load, which is what lets a shared `?…_page=3` URL survive. The app owns the
104
+ URL state; blox only reads it. The fix therefore needed **no blox change, no publish, no pin bump**
105
+ — confirmed by the install being a real registry copy (no symlink), pinned exactly.
106
+
107
+ ## Gotchas
108
+
109
+ - **A "backend search returns nothing" report is usually this bug.** Symptom: user on page 2
110
+ searches for a PO number that lives on page 1 and gets an empty table. The front end sent the
111
+ new filter with `<slug>_page=2` still set, api2 paged the *new* (filtered) result set from page
112
+ 2, and the single match — now on page 1 — fell outside the slice. It reads exactly like an api2
113
+ search defect and is not one. Check the query string before you open the API.
114
+ - **Page size worked while search did not.** `handleRecordsPerPageChange` already reset the page;
115
+ the filter and sort handlers never did. If only *some* controls behave, suspect an incomplete
116
+ reset rather than a data-layer problem.
117
+ - **Clearing all filters also resets to page 1** — by decision, see below. Do not re-add an
118
+ `if (next.length > 0)` guard around the reset.
119
+ - **The hook's filter read/write block is a merge hot spot but not a conflict magnet.** Two
120
+ independent fixes landed in it within days (the page reset, and the `_in` exact-match change a
121
+ few lines away) and `git merge` resolved them clean with both surviving. Verify both markers by
122
+ grep after such a merge rather than assuming a loss.
123
+
124
+ ## Tests
125
+
126
+ Co-located `src/hooks/useServerTableUrlState.test.ts` (Vitest). The reset rule is covered by 8
127
+ cases, every one **seen to fail before the fix** (5 red on the original code): filter applied,
128
+ multi-chip text filter applied, filters cleared, one of two filters removed, sort applied, sort
129
+ cleared, records-per-page changed, and the negative control — **row click must NOT reset**.
130
+
131
+ Type-check with `npx tsc --noEmit -p tsconfig.app.json`. Bare `npx tsc --noEmit` is a silent no-op
132
+ in this repo (references-only root config — see `standards/frontend.md` §2).
133
+
134
+ ## Decisions
135
+
136
+ - **2026-09-14 — clearing filters DOES reset to page 1. The opposite was tried and reversed.**
137
+ Mid-session the reverse was implemented (keep the page on a filter clear, on the reasoning that
138
+ clearing only widens the result set so page 3 still exists), verified with a mutation test, then
139
+ deliberately undone. Final behavior: **every** filter change resets, including a full clear.
140
+ Recorded so nobody re-litigates it or re-adds the guard thinking it was an oversight.
141
+
142
+ ## Change history
143
+ - 2026-09-14 — Documented the hook as its own subject. Added `resetToFirstPage` so filters, sorts,
144
+ and page-size changes all return to page 1 (fixes "search on page 2 finds nothing", TRUE-81877);
145
+ recorded the reversed keep-the-page-on-clear decision; noted `_in` is now exact-match not
146
+ `includes`; deleted ~104 lines of commented-out dead `activeRowUuid`/handler variants from the
147
+ bottom of the file (503 → 399 lines). (apeterson)
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-03
9
+ updated: 2026-09-14
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/cypress.config.ts
@@ -253,10 +253,11 @@ Every `cy:*` script goes through **`node scripts/cypress.mjs`** — see the firs
253
253
 
254
254
  - **Vitest is the other half.** `npm test` (`vitest run`) currently has **one failing test file**:
255
255
  `src/hooks/useTalosSurface.test.ts` fails with
256
- `TypeError: Unknown file extension ".css"` on `@agilant/toga-blox/dist/global.css`. 89 tests pass
257
- across the other 13 files. **Pre-existing** — the file was last touched in commit `604a779` and
258
- the Cypress work changed neither it nor `vitest.config.ts`. Needs a separate fix (Vitest CSS
259
- handling for blox imports).
256
+ `TypeError: Unknown file extension ".css"` on `@agilant/toga-blox/dist/global.css` — the suite
257
+ fails to **load**, so it is silently skipped in full. **Pre-existing** — the file was last
258
+ touched in commit `604a779` and the Cypress work changed neither it nor `vitest.config.ts`.
259
+ Re-confirmed on a clean tree 2026-09-14, with every other file green (148/148 on
260
+ `_sandbox-client`). Needs a separate fix (Vitest CSS handling for blox imports).
260
261
  - **No other mechanical guardrails exist in this repo:** no `.github/workflows`, no husky, no
261
262
  lint-staged. The only `.git/hooks/pre-commit` is a machine-local `code-review-graph` shim that
262
263
  always exits 0 and is not checked in.
@@ -264,6 +265,9 @@ Every `cy:*` script goes through **`node scripts/cypress.mjs`** — see the firs
264
265
  `SurfaceOverrides IS_VISIBLE` row in `dbchanges2`; the fixture only simulates the grant.
265
266
 
266
267
  ## Change history
268
+ - 2026-09-14 — Re-confirmed the `useTalosSurface.test.ts` / blox `dist/global.css` Vitest load
269
+ failure on a clean tree and refreshed the pass counts (148/148 on the rest). Still unfixed; it
270
+ silently costs one whole suite. No code change. (apeterson)
267
271
  - 2026-09-03 — **Rebuilt the harness from scratch on `TRUE-81735`** (the `TRUE-79813` harness this
268
272
  doc used to describe was never merged and is stranded there). New shape: all `/v2` calls stubbed,
269
273
  four tenants (COMPASS/COMPASSCANADA/NYCHH/QUAD), the three-axes model (client = baseUrl, role =
@@ -5,12 +5,13 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-09-03
8
+ updated: 2026-09-14
9
9
  owners: [jcardinal, apeterson]
10
10
  files: []
11
11
  related:
12
12
  - ../apps/toga25-supply/architecture.md
13
13
  - ../apps/toga-blox/architecture.md
14
+ - ../apps/toga25-supply/features/server-table-url-state.md
14
15
  - frontend-deploy.md
15
16
  - backend-php.md
16
17
  ---
@@ -220,7 +221,18 @@ you are in.
220
221
  explicitly cleared" are different states. Use `searchParams.has(key)`, not a truthiness check on
221
222
  the value.
222
223
  - **A param that changes the query result MUST be part of the React Query key.** Page, sort, and
223
- filters belong in the key so a change refetches (and resets to page 1).
224
+ filters belong in the key so a change refetches.
225
+ - **A param that changes WHICH RECORDS the server returns MUST also reset the page to 1** — in the
226
+ **same** `setSearchParams(prev => …)` updater as the change itself, so the two land in one
227
+ history entry and one refetch. This covers filters/search, sort, and page size. It does **not**
228
+ cover opening a record (a row click must not throw the user back to page 1) or a presentation-only
229
+ param. Miss it and the app sends the new filter with the old page: the server pages the *new*
230
+ result set from page 2, the single match now on page 1 falls outside the slice, and the report
231
+ arrives as "the backend search returns nothing." Verified 2026-09-14 in `toga25-supply` — the
232
+ rule above was read as query-key-only, so `handleRecordsPerPageChange` reset the page while
233
+ `handleColumnFiltersChange` and `handleSortingChange` never did. Keep the reset in **one** helper
234
+ called by every result-changing handler, not inlined per handler. (See
235
+ `../apps/toga25-supply/features/server-table-url-state.md`.)
224
236
  - **A presentation-only param MUST be excluded from the key.** Column visibility changes what you
225
237
  see, not what the server returns — putting it in the key forces a pointless refetch. (See
226
238
  `../apps/toga25-supply/features/column-visibility.md`.)
@@ -785,6 +797,10 @@ Copy from there rather than reinventing.
785
797
 
786
798
  ## Change history
787
799
 
800
+ - 2026-09-14 — Split §5's query-key bullet: reset-to-page-1 is now its own MUST (same
801
+ `setSearchParams` updater, one helper, row click excluded). It was previously a parenthetical
802
+ inside the query-key rule and went unimplemented for filters and sorts in toga25-supply for
803
+ months. (apeterson)
788
804
  - 2026-09-03 — §23 correction: replaced the two "seen to fail" worked examples. The first pass
789
805
  listed a guardrail check plus an **unverified** component-config mutation; both are now the two
790
806
  mutations that were actually run and observed red (a registry/fixture nav mismatch, and forcing
@@ -31,7 +31,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
31
31
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
32
32
  - **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
33
33
  - **toga2-commerce** (TOGa Commerce) — 21 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
34
- - **toga25-supply** (TOGa 2.5 Supply) — 20 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
+ - **toga25-supply** (TOGa 2.5 Supply) — 21 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
35
35
  - **toga-blox** (TOGa Blox) — 14 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
36
36
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
37
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.804",
3
+ "version": "1.0.805",
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",