spiderly 19.11.7 → 19.11.9

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.
@@ -207,7 +207,26 @@ cols: Column<ProductDTO>[] = [
207
207
 
208
208
  Headers are click-to-sort by default; disable with `sortable: false`. Columns whose `field` ends in `CommaSeparated` are auto-disabled — the backend generates no sort case for collection columns and rejects unknown sort fields with a 400, so the table never offers the click.
209
209
 
210
- Column 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. `minWidth: '8rem'` overrides that floor for one column; it stays a minimum, so the table still distributes the leftover space.
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
+
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.
213
+
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
+
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.
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:
219
+
220
+ ```typescript
221
+ new Column({ field: 'createdAt', name: 'Created', filterType: 'date', showTime: true,
222
+ showMatchModes: true, matchModes: [MatchModeCodes.GreaterThan, MatchModeCodes.LessThan] }),
223
+ ```
224
+
225
+ Three things to know:
226
+
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.
211
230
 
212
231
  ### Column chooser (show/hide columns)
213
232
 
@@ -292,6 +311,20 @@ Project an `<ng-template spiderlyDataTableActions>` to add your own buttons (or
292
311
 
293
312
  **Shift+click selects a range**: click one checkbox, hold Shift, click another — every row between receives the *clicked checkbox's new state* (shift-checking selects the range, shift-unchecking clears it). The anchor is the last clicked checkbox, and a range only ever spans rows visible on the current page: if the anchor is no longer among them, the shift-click degrades to a plain toggle. (A client-side sort that keeps both rows on screen ranges over the *new* visual order, which is what the user sees and expects.) Rows already in the target state are skipped; each actual change emits `onRowSelect`/`onRowUnselect` exactly as a single click would, so delta-based consumers (`newlySelectedItems`/`unselectedItems`) need no changes.
294
313
 
314
+ ### Paging scrolls the list back into view
315
+
316
+ Changing the page or the page size scrolls the table's wrapper back under the top of the viewport, so the new rows are what you land on rather than the paginator you just clicked. It is instant, never animated, and it does nothing when the table's top is already visible — a table placed below other content on a details page will not push that content off-screen.
317
+
318
+ The amount of fixed chrome to clear comes from the `--spiderly-viewport-top-inset` CSS variable, which `spiderly`'s own layout declares on `html` (defaulting to `--spiderly-topbar-height`). If you run your own fixed header, or pin a second bar below spiderly's topbar, declare the inset to match:
319
+
320
+ ```css
321
+ html {
322
+ --spiderly-viewport-top-inset: 8rem;
323
+ }
324
+ ```
325
+
326
+ Set the inset rather than stretching `--spiderly-topbar-height`, which is the topbar's actual height. Leave it undeclared and the table scrolls to the true top of the viewport, which is correct when nothing is pinned there.
327
+
295
328
  ### Key Inputs
296
329
 
297
330
  | Input | Type | Default | Purpose |
@@ -33,6 +33,7 @@ Reusable helpers exported from `helper-functions.ts`. Import the one you need in
33
33
  | `parseDateOnlyLocal(s: string): Date \| null` | |
34
34
  | `pushAction(cols: Column[], action: Action)` | |
35
35
  | `saveResponseAsFile(res: HttpResponse<Blob>, fallbackName: string): void` | |
36
+ | `scrollElementIntoViewIfAboveViewport(element: HTMLElement): void` | Scrolls `element` back under the top of the viewport, but only when it has already scrolled above it. Set the element's `scroll-margin-top` to whatever fixed chrome must stay clear. Assumes the element's nearest scroll container is the viewport itself. |
36
37
  | `selectedTab(tabs: SpiderlyTab[]): number` | |
37
38
  | `singleOrDefault<T>(array: T[], predicate: (item: T) => boolean): T \| undefined` | |
38
39
  | `splitPascalCase(input: string)` | |
@@ -28,6 +28,7 @@
28
28
  | `[MaxFileSize]` | Property | Specifies the maximum allowed file size (in bytes) for a blob property. When not applied, the file upload defaults to 20 MB (20,000,000 bytes). Use this attribute alongside any StorageAttribute subclass for file uploads. |
29
29
  | `[Output]` | All | Specifies the output configuration for the Source Generator. Note: This is a temporary solution and may be replaced in future versions. |
30
30
  | `[ReadOnly]` | Property | Marks a property as server-owned: the client may read it, but can never write it (mirrors OpenAPI readOnly). The property stays in the read {Entity}DTO — so it's still returned by GET endpoints and shown in the list table — but the generator closes the write path for it: no inbound FluentValidation rule is emitted, and the generated DTO→entity Mapster config .Ignore()s it, so a crafted payload can't tamper with it. It also gets no editable control in the generated details form. Use for values only backend code assigns — usage counters, denormalized aggregates, computed timestamps. Combine with [Required] to document the non-null DB column without generating an unsatisfiable inbound rule (the form never sends it). This differs from [UIDoNotGenerate] (hidden from the form but still writable and validated) and [ExcludeFromDTO] (dropped from every DTO, unreadable). |
31
+ | `[RetainReplacedBlobs]` | Property | Opt-out of save-time blob cleanup for a storage property. By default, saving an entity deletes every stored blob under the property's key prefix except the active one — correct when the entity is the only holder of the URL. Put this attribute on the property when copies of its URL outlive the entity's current value (order-line snapshots, emails, exports, third-party feeds): replacing the blob then keeps the old bytes, so historical references keep rendering. An orphaned blob costs fractions of a cent and is invisible; a deleted-but-referenced blob breaks a customer-facing record — when one side of the trade is cents and the other is broken history, don't optimise the cents. |
31
32
  | `[S3PrivateStorage]` | Property | Routes upload/delete operations for the decorated string property through S3PrivateStorageService. The column stores an opaque S3 key; access is expected to be mediated via signed URLs or a backend proxy rather than direct CDN retrieval. Intended for files that contain personal or compliance-sensitive data (warranty receipts, ID documents, customer-uploaded invoices). |
32
33
  | `[S3PublicStorage]` | Property | Routes upload/delete operations for the decorated string property through S3PublicStorageService. The bucket is configured for public access and the column stores a fully-qualified CDN URL; objects are uploaded with Cache-Control: public, max-age=31536000, immutable. |
33
34
  | `[SetNull]` | Property | Specifies that the property should be set to null when the parent entity is deleted. Apply this attribute to a many-to-one relationship property. |