spiderly 19.11.8 → 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.
@@ -1,15 +1,17 @@
1
- import { AfterViewInit, EventEmitter, OnChanges, OnDestroy, OnInit, QueryList, TemplateRef } from '@angular/core';
1
+ import { AfterViewInit, ErrorHandler, EventEmitter, OnChanges, OnDestroy, OnInit, QueryList, TemplateRef } from '@angular/core';
2
2
  import { ActivatedRoute, Router } from '@angular/router';
3
3
  import { TranslocoService } from '@jsverse/transloco';
4
- import { SelectItem, SortMeta } from 'primeng/api';
4
+ import { SortMeta } from 'primeng/api';
5
5
  import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
6
- import { Table, TableFilterEvent, TableLazyLoadEvent } from 'primeng/table';
6
+ import { Table, TableLazyLoadEvent } from 'primeng/table';
7
7
  import { Observable } from 'rxjs';
8
8
  import { CellTemplateContext, SpiderlyCellTemplateDirective } from '../../directives/spiderly-cell-template.directive';
9
9
  import { Filter } from '../../entities/filter';
10
10
  import { LazyLoadSelectedIdsResult } from '../../entities/lazy-load-selected-ids-result';
11
11
  import { PaginatedResult } from '../../entities/paginated-result';
12
12
  import { PrimengOption } from '../../entities/primeng-option';
13
+ import { FilterSource, SortKeyLabel } from '../../filters/filter-store';
14
+ import { SpiderlyFilterTemplateDirective } from '../../directives/spiderly-filter-template.directive';
13
15
  import { ConfigServiceBase } from '../../services/config.service.base';
14
16
  import { SpiderlyMessageService } from '../../services/spiderly-message.service';
15
17
  import { SpiderlyFormControl } from '../spiderly-form-control/spiderly-form-control';
@@ -21,9 +23,56 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
21
23
  private messageService;
22
24
  private translocoService;
23
25
  private configService;
26
+ private errorHandler;
24
27
  private locale;
25
28
  private readonly destroy$;
26
29
  table: Table;
30
+ private readonly injector;
31
+ private readonly zone;
32
+ /**
33
+ * The selection column's declared share. Under the fixed layout this is a RATIO, not a width.
34
+ *
35
+ * 4rem, down from 6: the original reserved room for a five-digit select-all counter sitting at
36
+ * body text size beside the box, which is the widest thing it can ever hold and not what it
37
+ * holds on any ordinary screen. The counter is a smaller step now (see the SCSS), so the same
38
+ * five digits fit in two thirds of the space.
39
+ */
40
+ protected readonly selectionColumnWidth = "4rem";
41
+ /**
42
+ * The selection column's MEASURED width, which is what the frozen identity column offsets
43
+ * itself by. Declaring `6rem` on both looked like the one-telling answer and is wrong: widths
44
+ * are shares, so that column renders at whatever proportion of the table it gets — 148px for a
45
+ * declared 6rem in one spec — and the identity column would sit 52px inside it, overlapping the
46
+ * checkboxes the moment anyone scrolled. The spec caught it; nothing about the page would have.
47
+ */
48
+ protected frozenOffsetPx: number;
49
+ private selectionWidthObserver?;
50
+ /** Aborts an in-flight column resize's document listeners, from `onUp` or from teardown. */
51
+ private resizeAbort;
52
+ /**
53
+ * Whether this column is pinned against the left edge. Only the FIRST column, and only when it
54
+ * is `lockVisible`: a sticky column in the middle of the grid pins the wrong thing and leaves a
55
+ * hole where it used to be. Once the grid scrolls sideways a row otherwise loses its identity
56
+ * — "Intesa, plaćeno, 12.400" with no idea whose order it is — and NN/g's Data Tables is
57
+ * explicit that the leftmost header column must lock in place. `lockVisible` already names
58
+ * exactly that column in every table, so this needs no new API (CLAUDE.md -> decision 8).
59
+ */
60
+ isColumnFrozen(col: Column): boolean;
61
+ /**
62
+ * The rightmost pinned cell, which carries the seam's shadow. It is the identity column when
63
+ * there is one and the checkbox column otherwise — a fact only this side knows, which is why
64
+ * the SCSS asking for it as `:last-of-type` drew nothing at all.
65
+ */
66
+ isLastFrozenColumn(col: Column): boolean;
67
+ get isSelectionColumnLastFrozen(): boolean;
68
+ /** A frozen column starts after the checkbox column, when there is one. */
69
+ frozenColumnOffset(): string;
70
+ /**
71
+ * Tracks the selection column's rendered width. A ResizeObserver rather than a one-off read:
72
+ * every column drag and every window resize re-splits the shares, so the offset moves without
73
+ * anything about the selection column itself changing.
74
+ */
75
+ private observeSelectionWidth;
27
76
  /**
28
77
  * Custom toolbar content projected via `<ng-template spiderlyDataTableActions>`.
29
78
  * Rendered in the caption action area ahead of the built-in buttons.
@@ -41,12 +90,47 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
41
90
  /** Paginator page-size choices; merge rules and persistence: CLAUDE.md → "Rows-per-page". */
42
91
  rowsPerPageOptions: number[];
43
92
  cols: Column[];
93
+ /**
94
+ * The consumer's filter store — the table's WHOLE filter surface, rendered as the chip bar. A
95
+ * table given none has no filtering at all (`Column.filterType` is only the value's shape; the
96
+ * legacy header-filter fallback was deleted — CLAUDE.md -> "Operator-owned view").
97
+ *
98
+ * READ ONCE, in ngOnInit — a store assigned later is silently ignored: the requery effect
99
+ * stays subscribed to the first store's `applied()`, and `resolvedStateKey` (which
100
+ * `additionalFilterIdLong` feeds) is never re-derived either. A consumer that must swap
101
+ * stores at runtime destroys and recreates the table around the swap — PACMS
102
+ * `integration-matching-products` nulls its view model for exactly this. Teaching
103
+ * ngOnChanges to re-arm both is the open upstream fix.
104
+ */
105
+ filters?: FilterSource;
106
+ /** Saved questions for this table, rendered as tabs above the bar. */
107
+ views?: TableView[];
108
+ /**
109
+ * The view currently selected. Seeded from the first view in `ngOnInit`, never left null on a
110
+ * table that has views: `viewScope` keys the layout off it, so an unseeded table writes its
111
+ * width and order to the SAME key an unmigrated table uses — orphaned the moment the operator
112
+ * clicks the first tab — and no tab reads as selected until they do.
113
+ */
114
+ activeViewId: string | null;
44
115
  /** Whether the paginator is shown. Pass only when `hasLazyLoad === false`. Defaults to `true`. */
45
116
  showPaginator: boolean;
46
117
  /** Whether the table is wrapped in a card container. Defaults to `false`. */
47
118
  showCardWrapper: boolean;
48
119
  readonly: boolean;
49
120
  idField: string;
121
+ /**
122
+ * Sizes the columns from their declared widths instead of from the rendered rows.
123
+ *
124
+ * The browser default is `table-layout: auto`, which computes every column from the content
125
+ * of the rows currently on screen — so a column is one width on this page and another on the
126
+ * next, and a filter (which narrows the variety of values in the column it filters) moves the
127
+ * whole grid. PrimeNG sets no `table-layout` of its own, so without this the table inherits
128
+ * that default. Bound to a field, never an object literal in the template: a literal is a new
129
+ * reference on every change-detection pass.
130
+ */
131
+ protected readonly tableStyle: {
132
+ 'table-layout': string;
133
+ };
50
134
  totalRecords: number;
51
135
  onTotalRecordsChange: EventEmitter<number>;
52
136
  getPaginatedListObservableMethod: (tableFilter: Filter) => Observable<PaginatedResult>;
@@ -54,7 +138,7 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
54
138
  deleteItemFromTableObservableMethod: (rowId: number) => Observable<any>;
55
139
  deleteListFromTableObservableMethod: (ids: number[]) => Observable<any>;
56
140
  lastLazyLoadEvent: TableLazyLoadEvent;
57
- loading: boolean;
141
+ private loading;
58
142
  newlySelectedItems: number[];
59
143
  fakeSelectedItems: number[];
60
144
  currentPageSelectedItemsFromDb: number[];
@@ -71,8 +155,6 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
71
155
  onIsAllSelectedChange: EventEmitter<AllClickEvent>;
72
156
  selectedLazyLoadObservableMethod: (tableFilter: Filter) => Observable<LazyLoadSelectedIdsResult>;
73
157
  additionalFilterIdLong: number;
74
- matchModeDateOptions: SelectItem[];
75
- matchModeNumberOptions: SelectItem[];
76
158
  /** Whether the "Add" button is shown. Defaults to `true`. */
77
159
  showAddButton: boolean;
78
160
  /** Whether the "Export to Excel" button is shown. Defaults to `true`. */
@@ -126,7 +208,7 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
126
208
  * Example: 'details' will navigate to '/details/{rowId}'.
127
209
  */
128
210
  rowNavigationPath: string;
129
- constructor(router: Router, dialogService: DialogService, route: ActivatedRoute, messageService: SpiderlyMessageService, translocoService: TranslocoService, configService: ConfigServiceBase, locale: string);
211
+ constructor(router: Router, dialogService: DialogService, route: ActivatedRoute, messageService: SpiderlyMessageService, translocoService: TranslocoService, configService: ConfigServiceBase, errorHandler: ErrorHandler, locale: string);
130
212
  ngAfterViewInit(): void;
131
213
  /**
132
214
  * The template that renders this column's cells, or null to use the built-in rendering.
@@ -155,12 +237,6 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
155
237
  * default later flows through to every user who never touched that column.
156
238
  */
157
239
  private columnVisibilityOverrides;
158
- /**
159
- * Columns revealed by the load-time reconciliation. Kept apart from
160
- * `columnVisibilityOverrides` so persisting a later unrelated toggle can't promote a
161
- * transient safety reveal into a durable choice.
162
- */
163
- private revealedByConstraint;
164
240
  /** A data column shows values for a `field`; anything else (the actions column) always renders. */
165
241
  private static isDataColumn;
166
242
  /**
@@ -169,8 +245,46 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
169
245
  * are a transient working set — different natural lifetimes.
170
246
  */
171
247
  private get columnsStateKey();
172
- /** Actions columns always render; data columns follow reveal/override, then declared default. */
248
+ /** Actions columns always render; data columns follow the override, then declared default. */
173
249
  isColumnVisible(col: Column): boolean;
250
+ /**
251
+ * `cols` in the operator's order. A field the stored order has never seen — a column added in a
252
+ * later release — sorts to the END rather than displacing a layout someone built by hand
253
+ * (CLAUDE.md -> "Operator-owned view", decision 7); the sort is stable, so those keep their
254
+ * declared order among themselves. Actions columns carry no field and are always last, which is
255
+ * where every consumer already declares them.
256
+ */
257
+ private orderedCols;
258
+ /**
259
+ * Whether this column can move one place in `direction` (-1 left, 1 right). A locked column is
260
+ * the row's anchor — and, once the left edge freezes, the thing a horizontal scroll keeps in
261
+ * view — so it does not move and nothing moves in front of it.
262
+ */
263
+ canMoveColumn(col: Column, direction: -1 | 1): boolean;
264
+ moveMenuColumn(direction: -1 | 1): void;
265
+ /** The move itself, shared by the menu and the drag — two entry points, one rule. */
266
+ private placeColumn;
267
+ private dataColumnsInOrder;
268
+ /**
269
+ * Header drag, ours rather than PrimeNG's `pReorderableColumn`, for two reasons: its
270
+ * `onMouseDown` arms the th for anything that is not an INPUT, a TEXTAREA or its OWN resizer —
271
+ * so our menu chevron and our resize grip would each start a column drag, and reaching for a
272
+ * width would silently reorder the grid — and its `onColumnDrop` reorders blind, with no notion
273
+ * of a locked column (CLAUDE.md -> decision 6).
274
+ *
275
+ * A shortcut for neighbours, never the mechanism: HTML5 dnd has no edge auto-scroll, so a column
276
+ * scrolled off the right is unreachable this way, and there is no keyboard path at all. The menu
277
+ * carries both.
278
+ *
279
+ * This half is the arming rule; the drop rule is `canDropOn` below.
280
+ */
281
+ onHeaderMouseDown(event: MouseEvent): void;
282
+ onHeaderDragStart(col: Column, event: DragEvent): void;
283
+ onHeaderDragOver(col: Column, event: DragEvent): void;
284
+ onHeaderDrop(col: Column, event: DragEvent): void;
285
+ /** Nothing lands on, or in front of, the locked column. Enforced here as well as in the menu:
286
+ * a drop is a separate entry point, and the rule is about the grid rather than about a button. */
287
+ private canDropOn;
174
288
  private refreshVisibleCols;
175
289
  /**
176
290
  * Locked columns can't be toggled; the last visible data column can't be hidden — the
@@ -178,16 +292,56 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
178
292
  */
179
293
  canToggleColumn(col: Column): boolean;
180
294
  toggleColumn(col: Column, visible: boolean): void;
181
- /** Restores every column to its declared default and forgets the stored override. */
182
- resetColumnVisibility(): void;
183
295
  /**
184
- * A hidden column contributes nothing to filtering or sorting: the header is the only
185
- * filter surface, so a kept constraint would restrict the data invisibly. Reloads (once)
186
- * only when a constraint was actually cleared — plain hides don't need a server round-trip.
296
+ * Restores every column to its declared default and forgets the stored overrides — visibility,
297
+ * wrap, order and width alike. It undid visibility only until the header menu shipped three
298
+ * more gestures beside it, and a layout with no way back is worse than one with no knobs.
299
+ *
300
+ * Hiding — here or from the menu — never touches filters or sort: the chip bar names every
301
+ * applied filter and the sort chip names the sort, hidden column or not (decision 2b). The
302
+ * legacy "hidden contributes nothing" apparatus died with the header-filter path.
303
+ */
304
+ resetColumnLayout(): void;
305
+ /**
306
+ * Per-column wrap, by `field`. A SEPARATE key from the visibility overrides on purpose: folding
307
+ * both into one object would invalidate every layout already in an operator's localStorage, and
308
+ * this is the key the width and order overrides will join (decision 5).
309
+ */
310
+ private columnWrap;
311
+ /** Data-column fields in the operator's order. Empty means "as declared". */
312
+ private columnOrder;
313
+ /**
314
+ * Per-column width overrides, as SHARES in the same unit `getColWidth` emits — never pixels.
315
+ * Under the fixed layout the browser splits surplus in proportion to these, so a pixel would
316
+ * pin the column and stop it answering the window: the operator on a 1280 laptop and the one
317
+ * on a 2560 monitor would get the same frozen column (decision 4).
187
318
  */
188
- private clearHiddenColumnConstraints;
189
- /** Whether filter metadata (single or array) carries a real constraint, not just an empty slot. */
190
- private static isActiveFilterMeta;
319
+ private columnWidths;
320
+ /**
321
+ * Layout keys carry the ACTIVE VIEW, which is what makes a view more than a saved filter: a
322
+ * picking view and a payments view want different COLUMNS, not just different rows. Global
323
+ * layout would re-create the original complaint one level up — the operator still re-picking
324
+ * columns every time the job changes.
325
+ *
326
+ * A table with no views has no segment, so the twenty-six tables that have not migrated keep
327
+ * reading exactly the keys they already wrote.
328
+ */
329
+ private get viewScope();
330
+ /**
331
+ * Where the applied filters live. Follows `stateStorage` rather than the layout's always-local
332
+ * rule, because that is where they lived before the bar took them off the header: PrimeNG's
333
+ * stateful table persisted them for free, and nothing replaced it — a refresh dropped every
334
+ * filter (Filip, on /tags). A filter is a question you are in the middle of asking, not a
335
+ * durable preference like a column's width.
336
+ */
337
+ private get filtersStateKey();
338
+ private get filterStorage();
339
+ private persistAppliedFilters;
340
+ private restoreAppliedFilters;
341
+ private get layoutStateKey();
342
+ /** One key for the whole layout — wrap now, widths next (decision 5). */
343
+ private persistColumnLayout;
344
+ private restoreColumnLayout;
191
345
  private persistColumnVisibility;
192
346
  private restoreColumnVisibility;
193
347
  /**
@@ -196,16 +350,9 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
196
350
  */
197
351
  private persistedTableState;
198
352
  /**
199
- * Guards the "hidden contributes nothing" invariant against state this component didn't
200
- * write (older blobs, hand-edited storage): a hidden column the persisted table state
201
- * still filters or sorts by is revealed rather than constraining the data invisibly.
202
- * In-memory only — once the constraint is gone, the user's stored choice reapplies.
203
- */
204
- private reconcileVisibilityWithPersistedConstraints;
205
- /**
206
- * The declared default as PrimeNG sort meta, or null when none is declared — or when its
207
- * column is hidden ("hidden contributes nothing"): the table then falls back to the
208
- * backend's implicit Id DESC, exactly as if no default were declared.
353
+ * The declared default as PrimeNG sort meta, or null when none is declared. Applies even
354
+ * while its column is hidden: the sort chip names the ordering whether or not the column
355
+ * is on screen (decision 2b).
209
356
  */
210
357
  private defaultMultiSortMeta;
211
358
  private persistedMultiSortMeta;
@@ -220,10 +367,26 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
220
367
  */
221
368
  private keepSortableMeta;
222
369
  private applyDefaultSortIfUnsorted;
370
+ onPageChange(container: HTMLElement): void;
371
+ /**
372
+ * The one pending predicate: PrimeNG's overlay and the container's `aria-busy` must never
373
+ * disagree about whether a fetch is in flight.
374
+ *
375
+ * It used to also test `items === undefined`, for the first load. That clause is dead now
376
+ * that `loading` initialises true and every lazyLoad raises it — except in the one state
377
+ * where it was actively wrong: a FAILED first load lowers the flag with `items` still
378
+ * undefined, which pinned the overlay up forever. Same stranded overlay this component was
379
+ * fixed for once already, through the other door.
380
+ */
381
+ get isPending(): boolean;
223
382
  lazyLoad(event: TableLazyLoadEvent): void;
383
+ /**
384
+ * Repaints the checkbox column for the page that just landed. Split out of the subscriber so
385
+ * the try/finally that guarantees the pending flag wraps an await, not fifty lines of
386
+ * selection algebra.
387
+ */
388
+ private reconcileSelectionForLoadedPage;
224
389
  clientLoad(): void;
225
- private clientFilterCount;
226
- filter(event: TableFilterEvent): void;
227
390
  private loadFormArrayItems;
228
391
  /**
229
392
  * Single source for whether a column header offers click-to-sort (drives both
@@ -233,11 +396,157 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
233
396
  * sort fields are rejected with a 400, a clickable header here would be a guaranteed error toast.
234
397
  */
235
398
  isColumnSortable(col: Column): boolean;
236
- /** The column's declared {@link Column.minWidth}, or the default for its filter type. */
237
- getColHeaderWidth(col: Column): string;
238
- getColMatchModeOptions(filterType: string): SelectItem<any>[];
239
- getColMatchMode(filterType: string): any;
240
- isDropOrMulti(filterType: string): boolean;
399
+ /** The column the header menu is currently open for, and its header cell. */
400
+ menuColumn: Column | null;
401
+ private menuHeaderCell;
402
+ private draggedColumn;
403
+ /** False while a gesture that began on the menu chevron or the resize grip is in flight. */
404
+ private headerDragArmed;
405
+ /**
406
+ * A ViewChild rather than a template reference passed in from the header. The popover lives in
407
+ * the CAPTION template and the button that opens it in the HEADER one, and a reference variable
408
+ * does not cross an ng-template boundary — it arrived as undefined, and the click did nothing
409
+ * at all.
410
+ */
411
+ private readonly columnMenu;
412
+ private readonly filterBar;
413
+ /**
414
+ * Consumer-projected filter controls, forwarded to the bar. Collected here rather than by the
415
+ * bar itself because the bar is OURS: a consumer's template is projected at the table, and
416
+ * content queries only see what was projected into the component that declares them.
417
+ */
418
+ filterTemplates: QueryList<SpiderlyFilterTemplateDirective>;
419
+ private readonly selectionHeader;
420
+ openColumnMenu(col: Column, event: Event): void;
421
+ /**
422
+ * Switches to a view. Clears first, always: a view is a STATE rather than an addition, and two
423
+ * of them composed produce a question nobody asked. The clear runs through the store directly
424
+ * rather than through `clear(table)`, which would also wipe the sort and the persisted state a
425
+ * view is entitled to keep.
426
+ */
427
+ selectView(view: TableView): void;
428
+ /**
429
+ * The filter this column stands for. Defaults to the column's own `field`, because a store id
430
+ * IS a backend property name — `toFilterPayload` emits it straight into `Filter.filters`. So
431
+ * `filterId` is an escape hatch for the rare mismatch, not something every column restates:
432
+ * all eight of the first migrated table's declarations were the field name written a second
433
+ * time.
434
+ */
435
+ private filterIdFor;
436
+ /**
437
+ * Asks the STORE, not the column. Checking only that an id was declared let a typo through to
438
+ * `store.get(id)`, which reads `definitions[id].label` — so a mistyped id threw on click rather
439
+ * than greying the item out. With 195 declarations to migrate, that is the failure mode worth
440
+ * closing here rather than in each consumer.
441
+ */
442
+ canFilterColumn(col: Column): boolean;
443
+ /**
444
+ * Opens the bar's editor for this column's filter. The bar took the filter off the header and
445
+ * the header is still where an operator reaches for it, which is twenty-seven tables' worth of
446
+ * habit; this is the shortcut back rather than a second filter surface — it drives the same
447
+ * editor, on the same bar, that `+ Filter` opens.
448
+ */
449
+ filterMenuColumn(): void;
450
+ /**
451
+ * Sorts by this column in the NAMED direction. Clicking a header cycles asc, desc and off,
452
+ * which is fine for one column and a guess for a direction someone wants now — you click, look,
453
+ * and click again if it went the other way. This is also the only sort path that works from a
454
+ * keyboard.
455
+ *
456
+ * A column the generated paginator has no sort case for answers every load with a 400, so the
457
+ * items are disabled by the same predicate that keeps its header from being clickable.
458
+ */
459
+ sortMenuColumn(order: 1 | -1): void;
460
+ /**
461
+ * Sizes the column to the widest thing in it. The cheaper escape from a cramped column: no
462
+ * drag, no aim. Unlike a drag it does NOT trade with the neighbour — a minimum that grew is
463
+ * exactly the case decision 4 lets push the table past its container and start scrolling.
464
+ *
465
+ * Measures the CELL CONTENT, not the cell: the clamp lives on the inner span, so a `td` reports
466
+ * a scrollWidth that fits while the text inside it does not.
467
+ */
468
+ fitMenuColumn(): void;
469
+ /**
470
+ * The share that actually yields `needed` pixels — not `needed` scaled by the column's current
471
+ * px-per-share, which is the obvious answer and the wrong one. Shares are a PROPORTION of
472
+ * whatever width the table has, so growing this column's share also shrinks the realized width
473
+ * of every other column, and the naive figure lands short. The spec caught it as a cell that
474
+ * was still clipped after being told to fit.
475
+ *
476
+ * While the columns fit, the table keeps the container's width and the proportion has to solve
477
+ * needed / tableWidth = newShare / (otherShares + newShare)
478
+ * with `otherShares` recovered from what this column currently renders at. Once `needed` is the
479
+ * whole container the proportional regime has no answer: the table is about to overrun its
480
+ * container and scroll (decision 4), and there a share IS its own length, so the pixels convert
481
+ * straight through the root font size.
482
+ */
483
+ private shareThatFits;
484
+ hideMenuColumn(): void;
485
+ /**
486
+ * Whether this column's cells give up the one-line clamp. The DEFAULT stays clamped
487
+ * (CLAUDE.md -> "Operator-owned view", decision 9); what changes is that the operator picks
488
+ * which column pays the row height, not whoever wrote `cols`.
489
+ *
490
+ * Lands on the td as `.cell-wrap`, whose inherited `--spiderly-cell-*` properties are how the
491
+ * toggle crosses into a projected `spiderlyCellTemplate` too — the contract's one telling is
492
+ * on that SCSS rule; a consumer clamp that hard-codes its values opts out.
493
+ */
494
+ isColumnWrapped(col: Column): boolean;
495
+ get isMenuColumnWrapped(): boolean;
496
+ toggleWrapForMenuColumn(): void;
497
+ /** Exposed for the template: actions columns carry no field and get no menu. */
498
+ isDataColumn(col: Column): boolean;
499
+ /**
500
+ * What the grid is ordered by, resolved to COLUMN NAMES for the bar. Read off PrimeNG's live
501
+ * meta rather than mirrored into a field, so it cannot fall out of step with the sort the table
502
+ * is actually applying. A field with no column left (hidden, or removed in a later release)
503
+ * falls back to its own name rather than vanishing from the list.
504
+ */
505
+ /**
506
+ * What the grid is ordered by, resolved to COLUMN NAMES for the bar.
507
+ *
508
+ * A field refreshed per fetch, not a getter: it is bound to the bar's `sort`, which is a SIGNAL
509
+ * input, so a freshly mapped array failed `Object.is` on every change-detection pass and marked
510
+ * the bar's consumers dirty forever — for a value that changes once per request. Same reason
511
+ * `tableStyle` is a field.
512
+ */
513
+ sortKeys: SortKeyLabel[];
514
+ /**
515
+ * Read off the last REQUEST's sort, not `table._multiSortMeta`. On a first load with a declared
516
+ * default that field is still null: `applyDefaultSortIfUnsorted` writes the default onto the
517
+ * lazy-load event, and PrimeNG fills its own meta only once someone clicks a header. Reading the
518
+ * table there showed no sort on exactly the tables that always have one. A field with no column
519
+ * left falls back to its own name rather than vanishing.
520
+ */
521
+ private refreshSortKeys;
522
+ /** This column's share, override first, then its declared width, then the per-type default. */
523
+ columnShare(col: Column): number;
524
+ /**
525
+ * A declared width expressed in SHARE units. rem IS the share unit and px converts; any other
526
+ * unit falls back to the per-type default rather than being read as a bare number.
527
+ *
528
+ * `parseFloat` alone shipped here first, so `width: '150px'` resolved to share 150 against
529
+ * neighbours at 12 — one column eating the grid on the first drag or fit. Every declared width
530
+ * in the consumer happens to be rem today, which is exactly why nothing would have caught it.
531
+ */
532
+ private shareOfDeclaredWidth;
533
+ private defaultShare;
534
+ /**
535
+ * Trades share between the column being dragged and the one on its right, so the table keeps
536
+ * its total: widening a column never starts a horizontal scroll by itself, which only happens
537
+ * once the SUM of the minimums stops fitting. The px delta is converted through the column's
538
+ * own rendered width, which is the only place the share-to-pixel scale is actually known.
539
+ */
540
+ startColumnResize(col: Column, event: MouseEvent, th: HTMLElement): void;
541
+ /**
542
+ * The column's declared {@link Column.width}, or the default for its filter type.
543
+ *
544
+ * A width, not a minimum: under the fixed layout {@link tableStyle} establishes, this is what
545
+ * the column is sized from, and the table shares its surplus in PROPORTION to these numbers.
546
+ * Columns declaring none would otherwise take an equal share, which throws away what the
547
+ * per-type defaults say — a boolean holds "Da"/"Ne", a text column holds a name.
548
+ */
549
+ getColWidth(col: Column): string;
241
550
  navigateToDetails(rowId: number): void;
242
551
  /**
243
552
  * Handle row click event. Arbitration between "this click navigates the row" and "this click
@@ -257,6 +566,19 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
257
566
  ngOnDestroy(): void;
258
567
  get showSelectAllCheckbox(): boolean;
259
568
  private resetSelection;
569
+ /**
570
+ * A COMMITTED filter re-queries; a draft does not. Re-fetching per keystroke would be the same
571
+ * lie the chip bar exists to prevent, only paid for in requests rather than in credibility.
572
+ *
573
+ * Back to page one first, and off FRESH metadata rather than `reload()`: that one replays the
574
+ * cached lazy-load event, whose `first` was copied when the event was built, so a filter applied
575
+ * from page 3 would keep asking the server to skip 50 rows a narrower result set does not have.
576
+ * The grid comes back empty for a value that is definitely in it.
577
+ *
578
+ * The first run is PrimeNG's: it fires the initial lazy load itself, and re-querying here would
579
+ * make every table load twice.
580
+ */
581
+ private requeryOnAppliedFilters;
260
582
  reload(): void;
261
583
  showActions(): boolean;
262
584
  getStyleForBodyColumn(col: Column<any>): string;
@@ -267,6 +589,11 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
267
589
  colTrackByFn(index: any, item: any): any;
268
590
  actionTrackByFn(index: any, item: Action): string;
269
591
  exportListToExcel(): void;
592
+ /**
593
+ * Clears everything that narrows the grid. With a store supplied it has to reach BOTH — the
594
+ * store holds the constraints and PrimeNG holds the persisted state and sort — or the bar goes
595
+ * empty while a reload brings the old filters back.
596
+ */
270
597
  clear(table: Table): void;
271
598
  setFakeIsAllSelected(): void;
272
599
  selectAll(checked: boolean): void;
@@ -299,7 +626,7 @@ export declare class SpiderlyDataTableComponent implements OnInit, OnChanges, Af
299
626
  rowUnselect(id: number): void;
300
627
  getFormArrayControlByIndex(formControlName: string, index: number): SpiderlyFormControl;
301
628
  static ɵfac: i0.ɵɵFactoryDeclaration<SpiderlyDataTableComponent, never>;
302
- static ɵcmp: i0.ɵɵComponentDeclaration<SpiderlyDataTableComponent, "spiderly-data-table", never, { "tableTitle": { "alias": "tableTitle"; "required": false; }; "tableIcon": { "alias": "tableIcon"; "required": false; }; "items": { "alias": "items"; "required": false; }; "rows": { "alias": "rows"; "required": false; }; "rowsPerPageOptions": { "alias": "rowsPerPageOptions"; "required": false; }; "cols": { "alias": "cols"; "required": false; }; "showPaginator": { "alias": "showPaginator"; "required": false; }; "showCardWrapper": { "alias": "showCardWrapper"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "idField": { "alias": "idField"; "required": false; }; "getPaginatedListObservableMethod": { "alias": "getPaginatedListObservableMethod"; "required": false; }; "exportListToExcelObservableMethod": { "alias": "exportListToExcelObservableMethod"; "required": false; }; "deleteItemFromTableObservableMethod": { "alias": "deleteItemFromTableObservableMethod"; "required": false; }; "deleteListFromTableObservableMethod": { "alias": "deleteListFromTableObservableMethod"; "required": false; }; "newlySelectedItems": { "alias": "newlySelectedItems"; "required": false; }; "unselectedItems": { "alias": "unselectedItems"; "required": false; }; "selectionMode": { "alias": "selectionMode"; "required": false; }; "selectedLazyLoadObservableMethod": { "alias": "selectedLazyLoadObservableMethod"; "required": false; }; "additionalFilterIdLong": { "alias": "additionalFilterIdLong"; "required": false; }; "showAddButton": { "alias": "showAddButton"; "required": false; }; "showExportToExcelButton": { "alias": "showExportToExcelButton"; "required": false; }; "showReloadTableButton": { "alias": "showReloadTableButton"; "required": false; }; "getFormArrayItems": { "alias": "getFormArrayItems"; "required": false; }; "hasLazyLoad": { "alias": "hasLazyLoad"; "required": false; }; "stateKey": { "alias": "stateKey"; "required": false; }; "stateStorage": { "alias": "stateStorage"; "required": false; }; "defaultSortField": { "alias": "defaultSortField"; "required": false; }; "defaultSortOrder": { "alias": "defaultSortOrder"; "required": false; }; "getAlreadySelectedItemIds": { "alias": "getAlreadySelectedItemIds"; "required": false; }; "getAlreadySelectedItems": { "alias": "getAlreadySelectedItems"; "required": false; }; "getFormControl": { "alias": "getFormControl"; "required": false; }; "additionalIndexes": { "alias": "additionalIndexes"; "required": false; }; "navigateOnRowClick": { "alias": "navigateOnRowClick"; "required": false; }; "rowNavigationPath": { "alias": "rowNavigationPath"; "required": false; }; }, { "onTotalRecordsChange": "onTotalRecordsChange"; "onLazyLoad": "onLazyLoad"; "onIsAllSelectedChange": "onIsAllSelectedChange"; "onRowSelect": "onRowSelect"; "onRowUnselect": "onRowUnselect"; }, ["actionsTemplate", "cellTemplates"], never, true, never>;
629
+ static ɵcmp: i0.ɵɵComponentDeclaration<SpiderlyDataTableComponent, "spiderly-data-table", never, { "tableTitle": { "alias": "tableTitle"; "required": false; }; "tableIcon": { "alias": "tableIcon"; "required": false; }; "items": { "alias": "items"; "required": false; }; "rows": { "alias": "rows"; "required": false; }; "rowsPerPageOptions": { "alias": "rowsPerPageOptions"; "required": false; }; "cols": { "alias": "cols"; "required": false; }; "filters": { "alias": "filters"; "required": false; }; "views": { "alias": "views"; "required": false; }; "showPaginator": { "alias": "showPaginator"; "required": false; }; "showCardWrapper": { "alias": "showCardWrapper"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "idField": { "alias": "idField"; "required": false; }; "getPaginatedListObservableMethod": { "alias": "getPaginatedListObservableMethod"; "required": false; }; "exportListToExcelObservableMethod": { "alias": "exportListToExcelObservableMethod"; "required": false; }; "deleteItemFromTableObservableMethod": { "alias": "deleteItemFromTableObservableMethod"; "required": false; }; "deleteListFromTableObservableMethod": { "alias": "deleteListFromTableObservableMethod"; "required": false; }; "newlySelectedItems": { "alias": "newlySelectedItems"; "required": false; }; "unselectedItems": { "alias": "unselectedItems"; "required": false; }; "selectionMode": { "alias": "selectionMode"; "required": false; }; "selectedLazyLoadObservableMethod": { "alias": "selectedLazyLoadObservableMethod"; "required": false; }; "additionalFilterIdLong": { "alias": "additionalFilterIdLong"; "required": false; }; "showAddButton": { "alias": "showAddButton"; "required": false; }; "showExportToExcelButton": { "alias": "showExportToExcelButton"; "required": false; }; "showReloadTableButton": { "alias": "showReloadTableButton"; "required": false; }; "getFormArrayItems": { "alias": "getFormArrayItems"; "required": false; }; "hasLazyLoad": { "alias": "hasLazyLoad"; "required": false; }; "stateKey": { "alias": "stateKey"; "required": false; }; "stateStorage": { "alias": "stateStorage"; "required": false; }; "defaultSortField": { "alias": "defaultSortField"; "required": false; }; "defaultSortOrder": { "alias": "defaultSortOrder"; "required": false; }; "getAlreadySelectedItemIds": { "alias": "getAlreadySelectedItemIds"; "required": false; }; "getAlreadySelectedItems": { "alias": "getAlreadySelectedItems"; "required": false; }; "getFormControl": { "alias": "getFormControl"; "required": false; }; "additionalIndexes": { "alias": "additionalIndexes"; "required": false; }; "navigateOnRowClick": { "alias": "navigateOnRowClick"; "required": false; }; "rowNavigationPath": { "alias": "rowNavigationPath"; "required": false; }; }, { "onTotalRecordsChange": "onTotalRecordsChange"; "onLazyLoad": "onLazyLoad"; "onIsAllSelectedChange": "onIsAllSelectedChange"; "onRowSelect": "onRowSelect"; "onRowUnselect": "onRowUnselect"; }, ["actionsTemplate", "cellTemplates", "filterTemplates"], never, true, never>;
303
630
  }
304
631
  export declare class Action {
305
632
  name?: string;
@@ -325,14 +652,60 @@ export declare class Action {
325
652
  onClick?: (event: ActionClickEvent) => void;
326
653
  });
327
654
  }
655
+ /**
656
+ * A saved question. The bar lets an operator BUILD one; a view is the one they ask every morning,
657
+ * asked in a click — and without them the whole column-and-filter rework is a pile of knobs
658
+ * someone re-sets daily (CLAUDE.md -> "Operator-owned view", decision 10).
659
+ *
660
+ * Declared in consumer code, not stored per user: with a handful of operators and a handful of
661
+ * fixed daily jobs, shared built-in views cover the work, while personal saved views would drift
662
+ * into five variants of the same question. Personal LAYOUT choices still persist on top, per view.
663
+ */
664
+ export interface TableView<TFilters = any> {
665
+ id: string;
666
+ label: string;
667
+ /**
668
+ * Sets the filters this view asks for. Receives the table's own store, so a consumer writes it
669
+ * with the store's typing. Called on a CLEARED store: a view is a state, not an addition.
670
+ */
671
+ apply?: (filters: TFilters) => void;
672
+ /**
673
+ * A view whose `apply` is a function of NOW — "Danas primljene", "this week". Selecting it
674
+ * always re-runs `apply` instead of restoring what an earlier visit stored: under the normal
675
+ * stored-wins rule a relative-date view clicked yesterday would restore yesterday's date under
676
+ * a tab claiming "today". Layout (columns, widths, wrap) still persists per view as usual —
677
+ * transience is about the QUESTION, not the furniture.
678
+ */
679
+ transient?: boolean;
680
+ }
328
681
  export declare class Column<T = any> {
329
682
  name?: string;
330
683
  field?: string & keyof T;
331
- filterField?: string & keyof T;
684
+ /**
685
+ * The column's VALUE SHAPE: it sizes the column (`getColWidth`), picks the cell rendering
686
+ * (`getRowData`, the blob branch) and the numeric right-align. The name is a fossil of the
687
+ * header-filter era — filtering lives in the store handed to `[filters]` — kept because
688
+ * renaming it would be a 195-declaration migration for zero behavior.
689
+ */
332
690
  filterType?: 'text' | 'date' | 'multiselect' | 'boolean' | 'numeric' | 'blob';
333
- filterPlaceholder?: string;
691
+ /**
692
+ * The id of the filter in the table's store that this column stands for, which is what the
693
+ * header menu's `Filter…` opens. A LINK, not a declaration: the filter exists in the store
694
+ * whether or not any column names it, and a column needs this only to offer the shortcut.
695
+ *
696
+ * Untyped against the store on purpose — a `Column<T>` knows its row type and nothing about
697
+ * which store it will be rendered beside, so the compile-time key check lives where the store
698
+ * is declared (`createFilterStore`), not here.
699
+ */
700
+ filterId?: string;
701
+ /**
702
+ * @deprecated Dead since the header-filter deletion: NOTHING reads it. It survives on the type
703
+ * only so details-table cols emitted by an OLDER Spiderly.SourceGenerators (which wrote
704
+ * `showMatchModes: true`) still compile against this library during a mixed-version window —
705
+ * a consumer on published NuGet generators plus an npm-linked library build. Delete it once
706
+ * every consumer regenerates with a generator from this release or later.
707
+ */
334
708
  showMatchModes?: boolean;
335
- showAddButton?: boolean;
336
709
  dropdownOrMultiselectValues?: PrimengOption[];
337
710
  actions?: Action[];
338
711
  editable?: boolean;
@@ -351,12 +724,14 @@ export declare class Column<T = any> {
351
724
  */
352
725
  lockVisible?: boolean;
353
726
  /**
354
- * CSS length overriding this column's default minimum width (e.g. `'8rem'`). The defaults are
355
- * per filter type and sized for the HEADER — filter input plus match-mode dropdown — so they
356
- * are generous for a column of short values. It is a MINIMUM, not a width: the table still
357
- * distributes the leftover space.
727
+ * CSS length fixing this column's width (e.g. `'8rem'`). Overrides the per-type default,
728
+ * which is generous for a column of short values.
729
+ *
730
+ * Declare a width on the columns that must stay NARROW and leave the flexible one undeclared:
731
+ * the table distributes its surplus in proportion to the declared widths, so a column carrying
732
+ * the long values wants the larger number, not a cap.
358
733
  */
359
- minWidth?: string;
734
+ width?: string;
360
735
  /**
361
736
  * Fired when this column's cell is clicked. Receives a {@link CellClickEvent} with the row id,
362
737
  * the column field, the full row, the raw and formatted cell value, the clicked `<td>` element
@@ -367,14 +742,12 @@ export declare class Column<T = any> {
367
742
  * Not applied to editable cells — those belong to their inline input.
368
743
  */
369
744
  onCellClick?: (event: CellClickEvent) => void;
370
- constructor({ name, field, filterField, filterType, filterPlaceholder, showMatchModes, showAddButton, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, minWidth, onCellClick, }?: {
745
+ constructor({ name, field, filterType, filterId, showMatchModes, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, }?: {
371
746
  name?: string;
372
747
  field?: string & keyof T;
373
- filterField?: string & keyof T;
374
748
  filterType?: 'text' | 'date' | 'multiselect' | 'boolean' | 'numeric' | 'blob';
375
- filterPlaceholder?: string;
749
+ filterId?: string;
376
750
  showMatchModes?: boolean;
377
- showAddButton?: boolean;
378
751
  dropdownOrMultiselectValues?: PrimengOption[];
379
752
  actions?: Action[];
380
753
  editable?: boolean;
@@ -383,7 +756,7 @@ export declare class Column<T = any> {
383
756
  sortable?: boolean;
384
757
  visible?: boolean;
385
758
  lockVisible?: boolean;
386
- minWidth?: string;
759
+ width?: string;
387
760
  onCellClick?: (event: CellClickEvent) => void;
388
761
  });
389
762
  }