@lavalogic/scoria 0.37.18 → 0.37.20
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/dist/Components/Table/Body/QuickSearchCell.svelte +3 -2
- package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +1 -7
- package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +30 -6
- package/dist/Components/Table/Body/TableBody.svelte +5 -0
- package/dist/Components/Table/ColumnFactory.svelte.d.ts +0 -12
- package/dist/Components/Table/ColumnFactory.svelte.js +42 -1
- package/dist/Components/Table/Misc/FilterInput.svelte +22 -4
- package/dist/Components/Table/NestedTable.svelte +6 -1
- package/dist/Components/Table/Table.svelte +12 -1
- package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.d.ts +6 -0
- package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.js +3 -0
- package/dist/Components/Table/Types/Public/ColumnOptions.d.ts +6 -0
- package/dist/scss/_mixins.scss +14 -11
- package/package.json +1 -1
|
@@ -724,10 +724,11 @@
|
|
|
724
724
|
div.input-cell {
|
|
725
725
|
background-color: #f4f7f9;
|
|
726
726
|
height: var(--height);
|
|
727
|
-
overflow:
|
|
727
|
+
overflow-x: clip;
|
|
728
|
+
overflow-y: visible;
|
|
728
729
|
padding-left: 0;
|
|
729
730
|
padding-right: 0;
|
|
730
|
-
border-right:
|
|
731
|
+
border-right: none;
|
|
731
732
|
box-sizing: border-box;
|
|
732
733
|
}
|
|
733
734
|
div.input-cell div.stick-right {
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import Action from '../../../Action.svelte';
|
|
8
8
|
import Checkbox from '../../../Checkbox.svelte';
|
|
9
9
|
import ThreeStateRadio from '../../../ThreeStateRadio.svelte';
|
|
10
|
-
import { ColumnType } from '../../Types/Columns/ColumnType.js';
|
|
11
10
|
import { TableContext } from '../../Types/Context/TableContext.svelte.js';
|
|
12
11
|
import type { Primitive } from '../../Types/Context/TableInitOptions.js';
|
|
13
12
|
import { Size } from '../../../../Types/Internal/Size.js';
|
|
@@ -18,7 +17,6 @@
|
|
|
18
17
|
|
|
19
18
|
const { columnDef }: ColumnListRowProps<T> = $props();
|
|
20
19
|
|
|
21
|
-
const isExpandColumn = $derived(columnDef?.columnType === ColumnType.Expand);
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
22
|
* Position of this column inside the sorted column list, used to
|
|
@@ -412,13 +410,9 @@
|
|
|
412
410
|
Expand columns can never be pinned, so the group goes
|
|
413
411
|
fully disabled for them.
|
|
414
412
|
-->
|
|
415
|
-
<div
|
|
416
|
-
class="pin-selector"
|
|
417
|
-
class:disabled={isExpandColumn}
|
|
418
|
-
>
|
|
413
|
+
<div class="pin-selector">
|
|
419
414
|
<ThreeStateRadio
|
|
420
415
|
value={pinSide}
|
|
421
|
-
disabled={isExpandColumn}
|
|
422
416
|
noLabel="Pin to left"
|
|
423
417
|
neutralLabel="Unpinned"
|
|
424
418
|
yesLabel="Pin to right"
|
|
@@ -83,11 +83,17 @@
|
|
|
83
83
|
>
|
|
84
84
|
<div class="bubble-parent">
|
|
85
85
|
{#await values then values}
|
|
86
|
-
<!--
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
<!-- `maxBubbles` (default 3) caps how many pills render inline;
|
|
87
|
+
anything beyond gets a single `"+N more"` overflow chip with
|
|
88
|
+
the hidden labels as a hover tooltip. Key combines index and
|
|
89
|
+
value because duplicate labels would otherwise collapse and
|
|
90
|
+
reordering with identical labels would force unrelated
|
|
91
|
+
bubbles to swap. -->
|
|
92
|
+
{@const maxBubbles = (cell.column.columnDef as BubbleDef<T>).maxBubbles ?? 3}
|
|
93
|
+
{@const visibleBubbles = values.slice(0, maxBubbles)}
|
|
94
|
+
{@const overflowCount = Math.max(0, values.length - maxBubbles)}
|
|
95
|
+
{@const overflowTitle = values.slice(maxBubbles).join(', ')}
|
|
96
|
+
{#each visibleBubbles as value, index (`${index}:${value}`)}
|
|
91
97
|
{@const colourPromise = (cell.column.columnDef as BubbleDef<T>).getColour?.(
|
|
92
98
|
cell.row.original,
|
|
93
99
|
cell.row.originalIndex,
|
|
@@ -105,6 +111,12 @@
|
|
|
105
111
|
{/await}
|
|
106
112
|
{/if}
|
|
107
113
|
{/each}
|
|
114
|
+
{#if overflowCount > 0}
|
|
115
|
+
<span
|
|
116
|
+
class="bubble-overflow"
|
|
117
|
+
title={overflowTitle}
|
|
118
|
+
>+{overflowCount} more</span>
|
|
119
|
+
{/if}
|
|
108
120
|
{/await}
|
|
109
121
|
</div>
|
|
110
122
|
</div>
|
|
@@ -186,7 +198,7 @@
|
|
|
186
198
|
|
|
187
199
|
div.bubble-parent {
|
|
188
200
|
display: flex;
|
|
189
|
-
flex-flow: row
|
|
201
|
+
flex-flow: row nowrap;
|
|
190
202
|
max-width: 100%;
|
|
191
203
|
height: 100%;
|
|
192
204
|
overflow-x: clip;
|
|
@@ -194,4 +206,16 @@ div.bubble-parent {
|
|
|
194
206
|
align-items: center;
|
|
195
207
|
gap: 0.75rem;
|
|
196
208
|
--line-height: 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.bubble-overflow {
|
|
212
|
+
flex-shrink: 0;
|
|
213
|
+
padding: 0.125rem 0.5rem;
|
|
214
|
+
border-radius: 9999px;
|
|
215
|
+
background-color: #e9ecf1;
|
|
216
|
+
color: #647d8e;
|
|
217
|
+
font-size: 0.75rem;
|
|
218
|
+
font-weight: 500;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
cursor: default;
|
|
197
221
|
}</style>
|
|
@@ -183,6 +183,11 @@
|
|
|
183
183
|
font-size: 0.875rem;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
.datatable-row.quick-filter-row > .cell {
|
|
187
|
+
border-right: solid 1px #cbd4da;
|
|
188
|
+
box-sizing: border-box;
|
|
189
|
+
}
|
|
190
|
+
|
|
186
191
|
.datatable-row.quick-filter-row {
|
|
187
192
|
transition: line-height 0.2s ease, height 0.2s ease;
|
|
188
193
|
height: 61px;
|
|
@@ -28,16 +28,4 @@ export interface ColumnSpecInternals<TRow extends object> {
|
|
|
28
28
|
* spec was not produced by `createColumnFactory(...)` (defensive; the
|
|
29
29
|
* public type's `_scoriaColumnSpec` brand prevents this at compile time). */
|
|
30
30
|
export declare function unwrapColumnSpec<TRow extends object>(spec: ColumnSpec<TRow>): ColumnSpecInternals<TRow>;
|
|
31
|
-
/**
|
|
32
|
-
* Construct a `ColumnFactory<TRow>` for use inside the `columns:` closure of
|
|
33
|
-
* `createTable({ ... })`. Each method on the returned object is a typed
|
|
34
|
-
* builder that wraps the matching internal `<Name>Def` constructor and
|
|
35
|
-
* returns a public `ColumnSpec` whose internal payload is hidden behind a
|
|
36
|
-
* symbol-keyed property.
|
|
37
|
-
*
|
|
38
|
-
* Consumers do not call this directly. It is exported so the `createTable`
|
|
39
|
-
* implementation can instantiate it inside the user-supplied closure and so
|
|
40
|
-
* tests can exercise individual factory methods without round-tripping
|
|
41
|
-
* through a full `createTable(...)` call.
|
|
42
|
-
*/
|
|
43
31
|
export declare function createColumnFactory<TRow extends object>(): ColumnFactory<TRow>;
|
|
@@ -178,6 +178,30 @@ function normaliseOptionsLoader(options) {
|
|
|
178
178
|
* tests can exercise individual factory methods without round-tripping
|
|
179
179
|
* through a full `createTable(...)` call.
|
|
180
180
|
*/
|
|
181
|
+
/** Format a Date or ISO-8601 date string for display using the browser
|
|
182
|
+
* locale. Returns null / empty input unchanged. Used by `col.display` when
|
|
183
|
+
* the column declares `filterValueType: 'Date'` so backend timestamps
|
|
184
|
+
* surface as a properly localised date+time string instead of the raw
|
|
185
|
+
* ISO payload (`2026-04-16T09:44:11+01:00`). */
|
|
186
|
+
function formatLocaleDate(value) {
|
|
187
|
+
if (value == null || value === '') {
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
let d;
|
|
191
|
+
if (value instanceof Date) {
|
|
192
|
+
d = value;
|
|
193
|
+
}
|
|
194
|
+
else if (typeof value === 'string' || typeof value === 'number') {
|
|
195
|
+
const parsed = new Date(value);
|
|
196
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
197
|
+
d = parsed;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (d == null) {
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
return d.toLocaleString();
|
|
204
|
+
}
|
|
181
205
|
export function createColumnFactory() {
|
|
182
206
|
const factory = {
|
|
183
207
|
// ─── Accessor kinds ─────────────────────────────────────────
|
|
@@ -301,10 +325,26 @@ export function createColumnFactory() {
|
|
|
301
325
|
: opts.filterValueType === 'String'
|
|
302
326
|
? 'text'
|
|
303
327
|
: 'display';
|
|
328
|
+
// When the consumer flagged the column as a Date filter, auto-
|
|
329
|
+
// format the displayed value via `toLocaleString` so timestamps
|
|
330
|
+
// surface in the user's locale rather than the raw ISO 8601
|
|
331
|
+
// payload returned by the server. Promise / scalar / null
|
|
332
|
+
// inputs are all handled. Filter / sort logic still operates
|
|
333
|
+
// on the raw accessor value below.
|
|
334
|
+
const rawValue = opts.value;
|
|
335
|
+
const valueAccessor = opts.filterValueType === 'Date'
|
|
336
|
+
? (row, index) => {
|
|
337
|
+
const v = rawValue(row, index);
|
|
338
|
+
if (v instanceof Promise) {
|
|
339
|
+
return v.then(formatLocaleDate);
|
|
340
|
+
}
|
|
341
|
+
return formatLocaleDate(v);
|
|
342
|
+
}
|
|
343
|
+
: rawValue;
|
|
304
344
|
const def = new DisplayDef({
|
|
305
345
|
id,
|
|
306
346
|
...commonProps(opts, inferredKind),
|
|
307
|
-
accessorFn:
|
|
347
|
+
accessorFn: valueAccessor,
|
|
308
348
|
modal: opts.modal,
|
|
309
349
|
// Display columns have no implicit filter type (unlike the
|
|
310
350
|
// typed kinds where the factory pins one based on the
|
|
@@ -330,6 +370,7 @@ export function createColumnFactory() {
|
|
|
330
370
|
id,
|
|
331
371
|
...commonProps(opts, 'bubble'),
|
|
332
372
|
accessorFn: opts.value,
|
|
373
|
+
maxBubbles: opts.maxBubbles,
|
|
333
374
|
getColour: opts.colour,
|
|
334
375
|
// Default to a string-typed filter so bubble columns appear in
|
|
335
376
|
// the quick-filter row and advanced filter panel by default
|
|
@@ -35,6 +35,24 @@
|
|
|
35
35
|
|
|
36
36
|
const customFilter = $derived(customFilters?.get(def.id));
|
|
37
37
|
|
|
38
|
+
/** Human-readable label used by every inner input (QuerySelect /
|
|
39
|
+
* MultiSelect / SingleSelect / etc.). Priority:
|
|
40
|
+
* 1. The column's `header` (when it is a plain string, which is the
|
|
41
|
+
* usual case in flowms migrations).
|
|
42
|
+
* 2. The customFilter's `label` (set on the remoteFilters entry).
|
|
43
|
+
* 3. The raw `customFilter.id` (last-ditch fallback).
|
|
44
|
+
* Avoids surfacing dotted-path / camelCase IDs like
|
|
45
|
+
* `orderLines.allowSplitFulfilment` as the visible label. */
|
|
46
|
+
const displayLabel: string = $derived.by(() => {
|
|
47
|
+
if (typeof def.header === 'string' && def.header.length > 0) {
|
|
48
|
+
return def.header;
|
|
49
|
+
}
|
|
50
|
+
if (customFilter?.label) {
|
|
51
|
+
return customFilter.label;
|
|
52
|
+
}
|
|
53
|
+
return customFilter?.id ?? def.id;
|
|
54
|
+
});
|
|
55
|
+
|
|
38
56
|
const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
|
|
39
57
|
|
|
40
58
|
let value: unknown = $state(null);
|
|
@@ -263,7 +281,7 @@
|
|
|
263
281
|
{#if isSearchable}
|
|
264
282
|
<!-- TODO remove this check once datePicker is supported -->
|
|
265
283
|
{#if def instanceof DateInputDef || def instanceof CheckboxDef}
|
|
266
|
-
<label for={inputId}>{
|
|
284
|
+
<label for={inputId}>{displayLabel}</label>
|
|
267
285
|
{/if}
|
|
268
286
|
<div class="row">
|
|
269
287
|
{#if def instanceof DateInputDef}
|
|
@@ -353,7 +371,7 @@
|
|
|
353
371
|
{#await options then options}
|
|
354
372
|
{#if hasQuery(customFilter)}
|
|
355
373
|
{#if customFilter.valueAsObject !== true}
|
|
356
|
-
{@const label =
|
|
374
|
+
{@const label = displayLabel}
|
|
357
375
|
|
|
358
376
|
<!-- TODO: multiselect -->
|
|
359
377
|
|
|
@@ -370,7 +388,7 @@
|
|
|
370
388
|
onchange={generateOnChangePrimitive(label)}
|
|
371
389
|
/>
|
|
372
390
|
{:else}
|
|
373
|
-
{@const label =
|
|
391
|
+
{@const label = displayLabel}
|
|
374
392
|
|
|
375
393
|
<!-- TODO: multiselect -->
|
|
376
394
|
|
|
@@ -389,7 +407,7 @@
|
|
|
389
407
|
{/if}
|
|
390
408
|
{:else}
|
|
391
409
|
<MultiSelect
|
|
392
|
-
label={
|
|
410
|
+
label={displayLabel}
|
|
393
411
|
labelProp="label"
|
|
394
412
|
valueProp="value"
|
|
395
413
|
name={customFilter.objectName ?? 'Item'}
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
> = Object.freeze({
|
|
29
29
|
allowCopy: false,
|
|
30
30
|
enableResize: false,
|
|
31
|
-
|
|
31
|
+
// Was `false`; flipped to `true` so nested-table columns can be
|
|
32
|
+
// reordered and pinned by default like the outer table. The
|
|
33
|
+
// `inner.allowReorder` option on `col.expand` (and the matching
|
|
34
|
+
// `allowReorder` on top-level `createTable`) still lets callers
|
|
35
|
+
// opt out per inner table when they want to lock the layout.
|
|
36
|
+
allowColumnReordering: true,
|
|
32
37
|
allowQuickFiltering: true,
|
|
33
38
|
allowAdvancedFiltering: true,
|
|
34
39
|
toolbar: ToolbarPosition.None,
|
|
@@ -129,10 +129,21 @@
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
element?.focus({ preventScroll: true });
|
|
132
|
+
// Same pinned-cell horizontal-scroll skip as the focusEnd
|
|
133
|
+
// effect below. Sticky-pinned cells are always visually
|
|
134
|
+
// in view, but their underlying layout position is at
|
|
135
|
+
// their real column track, so an unconditional
|
|
136
|
+
// `inline: 'nearest'` here scrolls the table sideways
|
|
137
|
+
// every time focus lands inside a pinned column (e.g.
|
|
138
|
+
// every click on a right-pinned Actions button).
|
|
139
|
+
const startCell = element?.closest('[role="cell"]');
|
|
140
|
+
const isStartPinned =
|
|
141
|
+
startCell?.classList.contains('pinned-left') ||
|
|
142
|
+
startCell?.classList.contains('pinned-right');
|
|
132
143
|
element?.scrollIntoView({
|
|
133
144
|
behavior: 'instant',
|
|
134
145
|
block: 'nearest',
|
|
135
|
-
inline: 'nearest',
|
|
146
|
+
inline: isStartPinned ? undefined : 'nearest',
|
|
136
147
|
});
|
|
137
148
|
}
|
|
138
149
|
});
|
|
@@ -14,6 +14,10 @@ import { AbstractDisplayDef, type AbstractDisplayDefProps } from './AbstractDisp
|
|
|
14
14
|
export interface BubbleDefProps<T extends object, in FilterValueType, out FilterOptionsType> extends AbstractDisplayDefProps<T, FilterValueType, FilterOptionsType> {
|
|
15
15
|
/** Per-bubble colour mapper. Invoked per bubble value per render. */
|
|
16
16
|
getColour?: (item: T, index: number, value: string) => BubbleColour | Promise<BubbleColour | null | undefined>;
|
|
17
|
+
/** Maximum number of pills shown per cell before the overflow chip
|
|
18
|
+
* kicks in. Pills beyond this cap are hidden and a single
|
|
19
|
+
* `"+N more"` chip is rendered in their place. Defaults to 3. */
|
|
20
|
+
maxBubbles?: number;
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* `BubbleDef<T, FilterValueType, FilterOptionsType>` renders the cell
|
|
@@ -29,4 +33,6 @@ export declare class BubbleDef<T extends object, in FilterValueType = StringFilt
|
|
|
29
33
|
set filterFn(v: FilterFn<T, FilterValueType> | undefined);
|
|
30
34
|
/** Per-bubble colour mapper. */
|
|
31
35
|
readonly getColour: ((item: T, index: number, value: string) => BubbleColour | Promise<BubbleColour | null | undefined>) | undefined;
|
|
36
|
+
/** Cap on rendered pills before the overflow chip. */
|
|
37
|
+
readonly maxBubbles: number;
|
|
32
38
|
}
|
|
@@ -15,6 +15,7 @@ export class BubbleDef extends AbstractDisplayDef {
|
|
|
15
15
|
// props.getColour re-derives the wrapper and discards the stale cache.
|
|
16
16
|
this.getColour = $derived(memoiseRowValueAccessor(props.getColour));
|
|
17
17
|
this._filterFn = $state(props.filterFn);
|
|
18
|
+
this.maxBubbles = $derived(props.maxBubbles ?? 3);
|
|
18
19
|
}
|
|
19
20
|
_filterFn;
|
|
20
21
|
/** Filter predicate. Reassignable. */
|
|
@@ -26,4 +27,6 @@ export class BubbleDef extends AbstractDisplayDef {
|
|
|
26
27
|
}
|
|
27
28
|
/** Per-bubble colour mapper. */
|
|
28
29
|
getColour;
|
|
30
|
+
/** Cap on rendered pills before the overflow chip. */
|
|
31
|
+
maxBubbles;
|
|
29
32
|
}
|
|
@@ -208,6 +208,12 @@ export interface BubbleColumnOptions<TRow extends object, TValue = unknown> exte
|
|
|
208
208
|
* supported (scoria internally wraps it in `Promise.resolve`).
|
|
209
209
|
*/
|
|
210
210
|
getOptions?: () => ReadonlyArray<SelectOption<TValue>> | Promise<ReadonlyArray<SelectOption<TValue>>>;
|
|
211
|
+
/**
|
|
212
|
+
* Maximum number of pills rendered before they collapse into an
|
|
213
|
+
* overflow chip (`"+N more"`). Defaults to 3. Set a larger number
|
|
214
|
+
* to show more pills inline; set `Infinity` to disable the cap.
|
|
215
|
+
*/
|
|
216
|
+
maxBubbles?: number;
|
|
211
217
|
}
|
|
212
218
|
/** Options for `col.progress({ header, value, ... })`. Renders a horizontal
|
|
213
219
|
* progress bar from a numeric ratio. */
|
package/dist/scss/_mixins.scss
CHANGED
|
@@ -1866,19 +1866,22 @@
|
|
|
1866
1866
|
div.input-cell {
|
|
1867
1867
|
background-color: colours.$ui-blue-1;
|
|
1868
1868
|
height: var(--height);
|
|
1869
|
-
overflow:
|
|
1869
|
+
// `overflow-x: clip` so a narrow filter cell's internal widget
|
|
1870
|
+
// (select chevron, filter-mode funnel, etc.) cannot paint past
|
|
1871
|
+
// the column boundary. The column-separator border now lives on
|
|
1872
|
+
// the outer grid item (`.quick-filter-row > .cell` in
|
|
1873
|
+
// `TableBody.svelte`), so clipping here keeps both the widget
|
|
1874
|
+
// AND the cell's own background inside the track and the border
|
|
1875
|
+
// stays visible. `overflow-y: visible` preserves the inline
|
|
1876
|
+
// dropdown spillage for select / date popups.
|
|
1877
|
+
overflow-x: clip;
|
|
1878
|
+
overflow-y: visible;
|
|
1870
1879
|
padding-left: 0;
|
|
1871
1880
|
padding-right: 0;
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
1874
|
-
//
|
|
1875
|
-
|
|
1876
|
-
// visually merge because the inner widget's background covers
|
|
1877
|
-
// the cell's table-cell-basic border. `!important` is here to
|
|
1878
|
-
// override the more-specific .focusable rule from
|
|
1879
|
-
// table-cell-basic that occasionally re-paints the right edge
|
|
1880
|
-
// transparent during focus.
|
|
1881
|
-
border-right: solid 1px colours.$ui-blue-4 !important;
|
|
1881
|
+
// Inner widget no longer paints a border-right - the outer cell
|
|
1882
|
+
// wrapper owns the column divider so adjacent filters cannot
|
|
1883
|
+
// visually merge regardless of what the widget paints inside.
|
|
1884
|
+
border-right: none;
|
|
1882
1885
|
box-sizing: border-box;
|
|
1883
1886
|
|
|
1884
1887
|
div.stick-right {
|