spiderly 19.11.9 → 19.12.0

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.
@@ -190,7 +190,7 @@ Every paginated table offers a rows-per-page dropdown — `rowsPerPageOptions`,
190
190
  ```typescript
191
191
  cols: Column<ProductDTO>[] = [
192
192
  new Column({ field: 'title', name: 'Title', filterType: 'text', lockVisible: true }),
193
- new Column({ field: 'price', name: 'Price', filterType: 'numeric', showMatchModes: true, decimalPlaces: 2 }),
193
+ new Column({ field: 'price', name: 'Price', filterType: 'numeric', decimalPlaces: 2 }),
194
194
  new Column({ field: 'createdAt', name: 'Created', filterType: 'date', showTime: true, visible: false }),
195
195
  new Column({ field: 'isActive', name: 'Active', filterType: 'boolean' }),
196
196
  new Column({ field: 'categoryDisplayName', name: 'Category', filterType: 'multiselect',
@@ -209,28 +209,68 @@ Headers are click-to-sort by default; disable with `sortable: false`. Columns wh
209
209
 
210
210
  The table lays its columns out from their **declared widths, never from the rows on screen** (`table-layout: fixed`). The browser default would size every column from the content of the page currently rendered, so a column is one width on this page and another on the next, and a filter — which narrows the variety of values in the column it filters — shifts the whole grid under an operator who is scanning it.
211
211
 
212
- Widths default per `filterType` and are sized for the **header** — the filter input plus its match-mode dropdown — so a column of short values (an amount, a status code) reserves more than it uses. `width: '8rem'` overrides the default for one column.
212
+ Widths default per `filterType` (the column's value shape) and are generous, so a column of short values (an amount, a status code) may reserve more than it uses. `width: '8rem'` overrides the default for one column.
213
213
 
214
214
  Declared widths behave as **ratios**, not caps: the table shares its surplus in proportion to them, so a column carrying long values wants the larger number. A table whose columns need more room than its container gets is widened to their sum and scrolls horizontally, rather than crushing the columns.
215
215
 
216
- Because a column no longer widens to fit its text, long values wrap and would grow the row instead. The default cell clamps to one line with an ellipsis. **A column rendering its own `spiderlyCellTemplate` clamps its own lines** — the library's rule cannot reach markup that carries the consumer's style encapsulation. Two traps when you write that rule: a flex item needs `min-width: 0` (its default `min-width: auto` floors it at the content width and the ellipsis never fires), and any pill or badge sitting beside the text needs `flex: none` so the text is what gives way.
216
+ Because a column no longer widens to fit its text, long values wrap and would grow the row instead. The default cell clamps to one line with an ellipsis. **A column rendering its own `spiderlyCellTemplate` clamps its own lines** — the library's rule cannot reach markup that carries the consumer's style encapsulation. Write the clamp against the cell custom properties, with your clamp as the fallback:
217
217
 
218
- `matchModes` narrows the match modes a column offers: declaration order is display order, and the **first entry becomes the column's default** match mode. Use it when a mode can only mislead — e.g. a datetime column, where an exact-equals match on a timestamp can never hit:
218
+ ```scss
219
+ .my-cell-line {
220
+ overflow: var(--spiderly-cell-overflow, hidden);
221
+ text-overflow: ellipsis;
222
+ white-space: var(--spiderly-cell-white-space, nowrap);
223
+ }
224
+ ```
225
+
226
+ The table sets those two properties on a cell whose column the operator wrapped from the header menu ("wrap text"), and they inherit into projected markup — so a clamp written this way follows the toggle for free, while a hard-coded `white-space: nowrap` turns the menu item into a silent no-op for your column. (`text-overflow` stays literal: it has no effect once `overflow` is `visible`, so the contract doesn't carry it.) Two traps when you write that rule: a flex item needs `min-width: 0` (its default `min-width: auto` floors it at the content width and the ellipsis never fires), and any pill or badge sitting beside the text needs `flex: none` so the text is what gives way.
227
+
228
+ ### Filtering — the filter store and chip bar
229
+
230
+ The table's **whole filter surface is a store the page owns** and hands to `[filters]`; a table given none has no filtering at all (`Column.filterType` describes the value's shape — width, cell rendering — never a filter). Declare the store beside the columns, one filter per question the screen answers:
219
231
 
220
232
  ```typescript
221
- new Column({ field: 'createdAt', name: 'Created', filterType: 'date', showTime: true,
222
- showMatchModes: true, matchModes: [MatchModeCodes.GreaterThan, MatchModeCodes.LessThan] }),
233
+ import { booleanFilter, createFilterStore, dateFilter, MatchModeCodes, numberFilter, textFilter } from 'spiderly';
234
+
235
+ function createProductFilters(t: (key: string) => string) {
236
+ return createFilterStore({
237
+ name: textFilter({ label: t('Name') }),
238
+ price: numberFilter({ label: t('Price') }),
239
+ isActive: booleanFilter({ label: t('IsActive') }),
240
+ // `operators` narrows what the editor offers; the FIRST entry is the default. Typed per
241
+ // kind, so an operator the backend cannot answer is a build error at the declaration.
242
+ createdAt: dateFilter({ label: t('CreatedAt'),
243
+ operators: [MatchModeCodes.GreaterThan, MatchModeCodes.LessThan] }),
244
+ // options make it a pick-list (the `in` operator); only numberFilter accepts them.
245
+ statusId: numberFilter({ label: t('Status'), options: this.statusOptions }),
246
+ });
247
+ }
223
248
  ```
224
249
 
225
- Three things to know:
250
+ ```typescript
251
+ filters = createProductFilters((key) => this.translocoService.translate(key));
252
+ ```
253
+
254
+ ```html
255
+ <spiderly-data-table [cols]="cols" [filters]="filters" ...></spiderly-data-table>
256
+ ```
257
+
258
+ The table renders the store as a **chip bar** above the header: one chip per *applied* filter, a read-only sort chip, the result count, `+ Filter` (a searchable popover of every declared filter) and Clear filters. Rules that follow:
259
+
260
+ - **Store ids are backend property names** — they go straight into the paginated request — so they normally repeat the column's `field`. A column whose filter id differs links to it with `Column.filterId` (that is what the header menu's `Filter…` opens); columns never *declare* filters.
261
+ - **Filters live independently of columns.** A filter with no column, or with a hidden one, is still offered and still narrows the grid — the chip is the visible surface, so hiding a column keeps its filter and sort.
262
+ - **`[filters]` is read once, in the table's `ngOnInit`.** A store assigned later is silently ignored; a page that must swap stores at runtime destroys and recreates the table around the swap (`@if` on a view-model field).
263
+ - **Late-arriving options go through `store.setOptions(id, options)`** — it re-resolves the offered operators and open editors read it live. Never write `definitions[id].options` by hand.
264
+ - **Programmatic writes use `setAndCommit(id, { operator, value })`** (a view's `apply`, a bulk-action flow). A bare `set` without `commit` changes only the draft and fails silently.
265
+ - **`offered: false`** keeps a filter out of `+ Filter` when a dedicated control on the page drives it through `filters.get(id)` (render the handle's `value()` draft, `commit()` on your own cadence — a search box, typically). Its chip still renders when applied.
266
+ - **Applied filters persist** under `` `${stateKey}:filters` `` in the `stateStorage` storage (session by default), separately from PrimeNG's own sort/pagination blob under `` `${stateKey}` ``.
267
+ - **A custom editor control for one filter** is a projected `<ng-template spiderlyFilterTemplate="filterId" let-f>`: drive it through the handle (`f.value()`, `f.set(...)`, `f.commit()`). Filters without a template keep the control the bar draws for their kind.
226
268
 
227
- - The offered list is only *visible* with `showMatchModes: true`, but the **default applies either way** — declaring `matchModes` on a column with no picker is the supported way to change just that column's default mode.
228
- - It is read at **declaration time**. PrimeNG builds the dropdown once, so reassigning `col.matchModes` later never reaches the rendered menu.
229
- - Only modes the filter type actually offers are honored (`text`: starts-with / contains / equals; `numeric` and `date`: their three each). Anything else is logged to the console and ignored, and a narrowing that leaves nothing falls back to the full list — the alternative is an empty dropdown you cannot pick from, plus a match mode the backend rejects with a 400.
269
+ **Views** (`[views]`, `TableView[]`) are saved questions rendered as tabs above the bar: each `apply` receives the cleared store (a view is a state, not an addition) and writes with `setAndCommit`; a view whose apply is a function of *now* ("received today") declares `transient: true` so selecting it re-derives instead of restoring yesterday's answer. Column layout (visibility, order, widths, wrap) persists per view.
230
270
 
231
271
  ### Column chooser (show/hide columns)
232
272
 
233
- Lazy tables render a **Columns** toolbar button opening a checkbox list of all data columns. Because a column's header is the table's only filter surface, showing a column is what makes it filterable — declare rarely-needed but filter-worthy columns with `visible: false` (available in the chooser, hidden by default) and pin the row's identifying column with `lockVisible: true`. Rules the table enforces: hiding a column clears its active filter + sort (one reload; plain hides don't hit the server), choices persist per table in `localStorage` under `` `${stateKey}:columns` `` (durable even with `stateStorage: 'session'`; only explicit toggles are stored, so later declared-default changes flow through), the last visible data column can't be hidden, and *Reset to default* restores the declared configuration.
273
+ Lazy tables render a **Columns** toolbar button opening a checkbox list of all data columns. Declare rarely-needed columns with `visible: false` (available in the chooser, hidden by default) and pin the row's identifying column with `lockVisible: true`. Hiding a column never touches its filter or sort — the chip bar is where both stay visible. Choices persist per table in `localStorage` under `` `${stateKey}:columns` `` (durable even with `stateStorage: 'session'`; only explicit toggles are stored, so later declared-default changes flow through), the last visible data column can't be hidden, and *Reset to default* restores the declared configuration.
234
274
 
235
275
  The `onClick` callback receives an `ActionClickEvent` — `{ id, row, element, originalEvent }`. Use `element` (or `originalEvent`) to anchor an overlay/popover to the clicked action; `row` gives you the full row object. It fires only for custom `field` values (never `'Details'` / `'Delete'`).
236
276
 
@@ -283,14 +323,14 @@ Project an `<ng-template spiderlyCellTemplate="field">` to render one column's c
283
323
  ```
284
324
 
285
325
  - Context: `let-row` (`$implicit`, the full row), `let-value="value"` (raw), `let-displayValue="displayValue"` (**what the table would have rendered** — formatted for the column's `filterType` and the app's `LOCALE_ID`, so a template that only decorates the value never re-implements that), `let-col="col"`. `value` / `displayValue` mean the same here as on `CellClickEvent`, so a column carrying both a click handler and a template reads one vocabulary.
286
- - **Cells only.** The header, its filter and its sorting are untouched, and the filter still works on the underlying `field` whatever the template draws.
326
+ - **Cells only.** The header and its sorting are untouched, and the column's filter (a store entry, see Filtering above) works whatever the template draws.
287
327
  - The template binds to the **consuming component**, so its handlers and pipes are yours.
288
328
  - A real `<a [routerLink]>` here beats `onCellClick` for navigation — middle-click and Ctrl+click open a new tab, which a JavaScript `navigate()` does not.
289
329
  - This is also the way out of the enum-cell limit: a `multiselect` column renders the raw stored value in its cell (the table does not map it through `dropdownOrMultiselectValues`), so a template that draws the label from the row's id gets the checkbox filter *and* readable cells.
290
330
 
291
331
  ### Custom Toolbar Actions
292
332
 
293
- Project an `<ng-template spiderlyDataTableActions>` to add your own buttons (or any markup) to the table's toolbar, alongside the built-in Clear Filters / Export to Excel / Reload / Delete Selected buttons. Import `SpiderlyDataTableActionsDirective` in the consuming component.
333
+ Project an `<ng-template spiderlyDataTableActions>` to add your own buttons (or any markup) to the table's toolbar, alongside the built-in Columns / Export to Excel / Reload / Delete Selected buttons. Import `SpiderlyDataTableActionsDirective` in the consuming component.
294
334
 
295
335
  ```html
296
336
  <spiderly-data-table [cols]="cols" [getPaginatedListObservableMethod]="getListMethod">
@@ -330,6 +370,8 @@ Set the inset rather than stretching `--spiderly-topbar-height`, which is the to
330
370
  | Input | Type | Default | Purpose |
331
371
  | ---------------------------------- | ------------------------ | ------- | ----------------------- |
332
372
  | `cols` | `Column[]` | — | Column definitions |
373
+ | `filters` | `FilterSource` | — | The page's filter store (see Filtering); read once at init |
374
+ | `views` | `TableView[]` | — | Saved questions rendered as tabs above the bar |
333
375
  | `getPaginatedListObservableMethod` | `(filter) => Observable` | — | Server-side data source |
334
376
  | `additionalFilterIdLong` | `number` | — | Parent entity filter |
335
377
  | `hasLazyLoad` | `boolean` | `true` | Server vs client mode |
@@ -14,6 +14,7 @@ Reusable helpers exported from `helper-functions.ts`. Import the one you need in
14
14
  | `exportListToExcel(exportListToExcelObservableMethod: (filter: Filter) => Observable<any>, filter: Filter)` | |
15
15
  | `firstCharToUpper(input: string): string` | |
16
16
  | `getFileNameFromContentDisposition(resp: HttpResponse<Blob>, defaultName: string): string` | |
17
+ | `getFilterOptions(namebookList: Namebook[]): FilterOption[]` | The filter store's twin of `getPrimengNamebookOptions` — same namebook in, the store's `{ value, label }` shape out (a `FilterOption` is not a `PrimengOption`: that one's `code` is deliberate, see `primeng-option.ts`). Every consumer filling a pick-list filter from a lookup needs this map; without it each page re-derives the three lines. |
17
18
  | `getHtmlImgDisplayString64(base64String: string)` | |
18
19
  | `getImageDimensions(file: File): Promise<{ width: number; height: number }>` | |
19
20
  | `getMimeTypeForFileName(fileName: string): string` | |
@@ -93,20 +93,15 @@ The backend allows one CORS origin — `Spiderly.Shared:FrontendUrl` in `appsett
93
93
 
94
94
  Spiderly's admin UI is built on PrimeNG v19. A few selectors that look obvious from the docs do not work — match what's actually rendered.
95
95
 
96
- - **Filter Apply / Clear buttons have no identifying class.** PrimeNG's documented `pcFilterApplyButton` / `pcFilterClearButton` style classes are not applied to the rendered `<p-button>` elements. Match by accessible name:
97
- ```ts
98
- overlay.getByRole('button', { name: 'Apply' })
99
- ```
100
- - **Match-mode dropdown is `<p-select>`, not `<p-dropdown>`** — PrimeNG renamed Dropdown to Select in v19. Spiderly's `<spiderly-dropdown>` wraps `<p-select>` internally.
101
- - **Boolean filter is `<p-checkbox [binary]="true" [indeterminate]="value === null">`**, not `pTriStateCheckbox`. Initial state is `null` (rendered as a horizontal dash); each click cycles `null → true → false → null`.
102
- - **Filter overlays for the rightmost column get clipped against the viewport.** PrimeNG repositions the overlay frame-by-frame, so Playwright's stability check on inner elements fails (`waiting for element to be visible, enabled and stable`). Pass `click({ force: true })` to bypass the stability gate. Apply/Clear buttons (matched by role) do not need this — only the elements *inside* the overlay (e.g. `.p-checkbox-box`).
96
+ - **Operator dropdown is `<p-select>`, not `<p-dropdown>`** — PrimeNG renamed Dropdown to Select in v19. Spiderly's `<spiderly-dropdown>` wraps `<p-select>` internally.
103
97
 
104
- ## Match-mode column configuration
98
+ ## Driving the filter bar
105
99
 
106
- For column-config behavior (when the match-mode dropdown renders, how labels resolve), see `Angular/projects/spiderly/src/lib/components/spiderly-data-table/CLAUDE.md`. Two points that commonly bite test authors:
100
+ Filtering is the chip bar above the table (a `spiderly-data-table` given a filter store), addressed by spiderly's own `data-testid`s — stable by contract, unlike PrimeNG's classes: `add-filter` (the `+ Filter` button; its popover teleports to `document.body`), `add-filter-option` (one filter, matched by label), `filter-editor` (the editor row), `filter-editor-operator` (the operator picker's trigger; its option list is PrimeNG-rendered in a teleported overlay, matched by label), `filter-editor-value` (the editor's value control, whatever its kind draws — input, checkbox, datepicker, multiselect), `filter-editor-apply` (every kind commits through it), `filter-chip`, `filter-chip-remove`, `filter-bar-clear` (renders only while something is applied), `filter-bar-count`, `sort-chip`. The flow is always open → draft → Apply; a draft reaches nothing until Apply, and each Apply re-queries, so await the paginated-list response per commit rather than stacking them.
107
101
 
108
- - **Numeric and date columns need `showMatchModes: true`** on the `Column<T>` for the match-mode `<p-select>` to render at all. Without it the match-mode UI is silently absent and Playwright selectors for "More than" / "Less than" will time out.
109
- - **Match-mode option labels are transloco output** (`'More than'`, `'Less than'`), not `MatchModeCodes` keys. Match Playwright selectors against the value in your `en.json` (or the locale your test runs under).
102
+ - **Operator option labels are transloco output** (`'More than'`, `'Less than'`), not `MatchModeCodes` keys. Match Playwright selectors against the value in your `en.json` (or the locale your test runs under). Which operators a filter offers is the store declaration's `operators` (or the kind's full list).
103
+ - **The boolean editor is a binary `<p-checkbox>`** starting unchecked: one click drafts `true`, a second drafts `false`; commit with Apply.
104
+ - **Applied filters persist under `` `${stateKey}:filters` ``** (the store snapshot: `{ id: { operator, value } }` on the wire vocabulary — `'contains'`, `'greaterThan'`, …); sort and pagination stay in PrimeNG's own blob under `` `${stateKey}` ``. Assert each in its own key.
110
105
 
111
106
  ## Test data: seed and clean
112
107
 
@@ -117,12 +112,12 @@ Tests should own their data. Two patterns:
117
112
 
118
113
  Always use `Promise.all` for seed and cleanup batches. Sequential 40× HTTP round-trips noticeably slow CI; the database has no problem with the concurrency.
119
114
 
120
- ## Generated lists ship with the Id column only
115
+ ## Generated lists ship with the Id column and an Id filter only
121
116
 
122
- `spiderly add-new-entity` produces a list component with a single numeric Id column plus Details/Delete actions. If your test needs to drive text/numeric/boolean filters, you have two options:
117
+ `spiderly add-new-entity` produces a list component with a single numeric Id column, a filter store declaring just the `id` filter, and Details/Delete actions. If your test needs to drive text/boolean filters, you have two options:
123
118
 
124
- 1. **Extend the list component** in your app — add the columns you want to filter on (text, numeric, boolean). The Spiderly admin then has the filter UI your test can target.
125
- 2. **Drive filtering through the API directly** — call the paginated-list endpoint with a `FilterDTO` payload and assert on the response, skipping the UI. Faster, less brittle, but doesn't exercise the column-config code path.
119
+ 1. **Extend the list component** in your app — add the columns and the matching store entries (`textFilter`, `numberFilter`, `booleanFilter`, `dateFilter`) for what you want to filter on. The bar then offers them under `+ Filter`.
120
+ 2. **Drive filtering through the API directly** — call the paginated-list endpoint with a `FilterDTO` payload and assert on the response, skipping the UI. Faster, less brittle, but doesn't exercise the store-config code path.
126
121
 
127
122
  ## Debugging a failing Playwright test in CI
128
123
 
@@ -86,7 +86,16 @@ Use PascalCase for migration names. If the migration looks wrong, `spiderly remo
86
86
 
87
87
  ### List component (`{kebab-name}-list.component.ts`)
88
88
 
89
- Update the `cols` array in `ngOnInit()` with actual entity columns:
89
+ Update **both** the filter store (the `create{EntityName}Filters` function the scaffold declares above the component) and the `cols` array in `ngOnInit()` with actual entity columns. The store is the table's whole filter surface — a column without a matching store entry renders fine but cannot be filtered:
90
+
91
+ ```typescript
92
+ function createProductFilters(t: (key: string) => string) {
93
+ return createFilterStore({
94
+ name: textFilter({ label: t("Name") }),
95
+ price: numberFilter({ label: t("Price") }),
96
+ });
97
+ }
98
+ ```
90
99
 
91
100
  ```typescript
92
101
  this.cols = [
@@ -109,6 +118,8 @@ this.cols = [
109
118
  ];
110
119
  ```
111
120
 
121
+ Store ids are backend property names (they go straight into the paginated request), so they normally repeat the column's `field`. Factories: `textFilter`, `numberFilter` (accepts `options` for a pick-list), `booleanFilter`, `dateFilter` — all imported from `spiderly`.
122
+
112
123
  ### Translation keys
113
124
 
114
125
  Add keys to `Frontend/src/assets/i18n/en.json` (and to any other language files your app uses):