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.
- package/agent/docs/angular-customization/index.md +55 -13
- package/agent/docs/angular-customization/references/helper-functions.generated.md +1 -0
- package/agent/docs/e2e-testing/index.md +10 -15
- package/agent/skills/add-entity/SKILL.md +12 -1
- package/fesm2022/spiderly.mjs +1900 -402
- package/fesm2022/spiderly.mjs.map +1 -1
- package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +369 -142
- package/lib/directives/spiderly-filter-template.directive.d.ts +34 -0
- package/lib/directives/spiderly-overflow-title.directive.d.ts +33 -0
- package/lib/entities/filter-rule.d.ts +5 -7
- package/lib/filters/allowed-operators.d.ts +23 -0
- package/lib/filters/filter-store.d.ts +255 -0
- package/lib/filters/spiderly-filter-bar.component.d.ts +132 -0
- package/lib/services/helper-functions.d.ts +8 -0
- package/package.json +1 -1
- package/public-api.d.ts +5 -0
- package/styles/components/spiderly-data-table/spiderly-data-table.component.scss +150 -18
- package/styles/filters/spiderly-filter-bar.component.scss +163 -0
- package/styles/styles/layout/_mixins.scss +13 -0
|
@@ -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',
|
|
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`
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
##
|
|
98
|
+
## Driving the filter bar
|
|
105
99
|
|
|
106
|
-
|
|
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
|
-
- **
|
|
109
|
-
- **
|
|
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
|
|
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
|
|
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
|
|
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):
|