spiderly 19.12.4 → 19.14.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.
@@ -268,9 +268,41 @@ The table renders the store as a **chip bar** above the header: one chip per *ap
268
268
 
269
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. A stored view id whose view is not in `[views]` yet (async-licensed views) is adopted when it arrives — unless the operator picked another tab meanwhile — and a restored transient view re-derives its question. **Sort is view state too**: entering a view resolves the user's per-view sort override ?? the view's declared `sort` ?? the table's `defaultSortField` ?? unsorted — it never inherits the previous tab's ordering, so give the catch-all first view a table-level default when "newest first" isn't what you want there.
270
270
 
271
+ **A view tab can show a count**: `count?: number | null` renders a badge beside the label (`null` or absent shows none, `0` shows one). The table never computes it — run your own count query beside each list load and rebuild `[views]` with the fresh numbers, keeping each view's `id` stable. A rebuild neither re-enters the active view nor sends a request: a stored view id is adopted only from a change that *brings* that view, and the tabs are tracked by `id`, so the focused tab keeps focus.
272
+
273
+ ```typescript
274
+ this.views = this.views.map((view) => ({ ...view, count: counts[view.id] }));
275
+ ```
276
+
277
+ **A count can honor each view's saved filters**: the table's `viewFilterPayload(viewId)` returns the `filters` payload that view's own list request carries, so a count query that applies it counts the rows that tab lists (on screen for the active view, after a click for the others), including bar filters the user saved on that view. The active view answers with the live filters; any other view with what a click would enter: its saved filters, else its `apply` (always `apply` for a transient view). It switches nothing and sends no request. `apply` runs on a scratch store for this, so it must write only into the store it receives. It returns `{}` on a table without `[filters]` and `null` for an id not in `[views]` (yet). Call it beside each list load; a non-static `@ViewChild` is still unresolved during the first one, so hand the table in from a template reference:
278
+
279
+ ```html
280
+ <spiderly-data-table #table [views]="views" (onLazyLoad)="refreshCounts(table)" ...></spiderly-data-table>
281
+ ```
282
+
283
+ ```typescript
284
+ refreshCounts(table: SpiderlyDataTableComponent): void {
285
+ const filtersByView: Record<string, Record<string, FilterRule[]>> = {};
286
+ for (const view of this.views) {
287
+ const filters = table.viewFilterPayload(view.id);
288
+ if (filters) filtersByView[view.id] = filters; // null: the table does not hold this view yet
289
+ }
290
+ // send filtersByView with your count query, then rebuild [views] with the counts
291
+ }
292
+ ```
293
+
294
+ **A view can declare its columns**: `columns` is a field → visible map that overrides `Column.visible` in that view only, naming just the deviation — a column it does not name keeps its `Column.visible`. Visibility resolves per field as the user's own per-view choice ?? the view's `columns` ?? `Column.visible`; the user's choice is stored only where it differs from the view's declaration, *Reset to default* returns to the declaration, and `lockVisible` columns always show. To swap one column for another, declare them next to each other in `cols` (the swapped-in one `visible: false`) and the view's column takes the other's place. Type the row as the second generic to check the field names.
295
+
296
+ ```typescript
297
+ const views: TableView<OrderFilters, Order>[] = [
298
+ { id: 'all', label: 'All' },
299
+ { id: 'late', label: 'Late goods', columns: { why: true, assignedLocation: false } },
300
+ ];
301
+ ```
302
+
271
303
  ### Column chooser (show/hide columns)
272
304
 
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.
305
+ 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. On a table with `[views]` the key carries the view (`` `${stateKey}:${viewId}:columns` ``) and the declared configuration is the active view's `columns` over `Column.visible`.
274
306
 
275
307
  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'`).
276
308
 
@@ -371,7 +403,7 @@ Set the inset rather than stretching `--spiderly-topbar-height`, which is the to
371
403
  | ---------------------------------- | ------------------------ | ------- | ----------------------- |
372
404
  | `cols` | `Column[]` | — | Column definitions |
373
405
  | `filters` | `FilterSource` | — | The page's filter store (see Filtering); read once at init |
374
- | `views` | `TableView[]` | — | Saved questions rendered as tabs above the bar |
406
+ | `views` | `TableView[]` | — | Saved questions rendered as tabs above the bar, each with an optional `count` badge and declared `columns` |
375
407
  | `getPaginatedListObservableMethod` | `(filter) => Observable` | — | Server-side data source |
376
408
  | `additionalFilterIdLong` | `number` | — | Parent entity filter |
377
409
  | `hasLazyLoad` | `boolean` | `true` | Server vs client mode |
@@ -89,8 +89,8 @@ public virtual Brand Brand { get; set; }
89
89
  ### Rules
90
90
 
91
91
  - Naming convention: `{NavigationName}Id` — resolved automatically. Use `[ForeignKey(nameof(OtherName))]` only when you need a different scalar name.
92
- - Nullability must match the relationship: `[Required]` navigation → non-nullable scalar (`long BrandId`); optional nav (`[SetNull]`) → nullable scalar (`long? BrandId`). Mismatch raises **SPID001**.
93
- - Scalar type must match the parent's `Id` type (`byte`/`int`/`long`). Mismatch raises **SPID003**.
92
+ - Nullability must match the relationship: `[Required]` navigation → non-nullable scalar (`long BrandId`); optional nav (`[SetNull]`) → nullable scalar (`long? BrandId`). Mismatch raises **SPIDERLY006**.
93
+ - Scalar type must match the parent's `Id` type (`byte`/`int`/`long`). Mismatch raises **SPIDERLY004**.
94
94
 
95
95
  ### Caveat — generated CRUD still loads the nav
96
96