@lavalogic/scoria 0.37.17 → 0.37.19
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 +2 -0
- package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +89 -59
- package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +1 -0
- 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 +41 -1
- package/dist/Components/Table/Misc/FilterInput.svelte +22 -4
- package/dist/Components/Table/NestedTable.svelte +6 -1
- package/dist/Components/Table/Table.svelte +28 -2
- package/dist/scss/_mixins.scss +11 -0
- package/package.json +1 -1
|
@@ -368,59 +368,71 @@
|
|
|
368
368
|
ondragleave={disableDragVisual}
|
|
369
369
|
ondrop={disableDragVisual}
|
|
370
370
|
>
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
Tri-state pin selector rendered via `ThreeStateRadio`,
|
|
384
|
-
matching the visual segmented-control used elsewhere in
|
|
385
|
-
the table (filter Active state, etc.). The three
|
|
386
|
-
`Ternary` slots map to:
|
|
387
|
-
False → pinned LEFT (pin-left icon, leftmost)
|
|
388
|
-
Neutral → unpinned (circle-cross icon, middle)
|
|
389
|
-
True → pinned RIGHT (pin-right icon, rightmost)
|
|
390
|
-
Expand columns can never be pinned, so the group goes
|
|
391
|
-
fully disabled for them.
|
|
392
|
-
-->
|
|
393
|
-
<div
|
|
394
|
-
class="pin-selector"
|
|
395
|
-
class:disabled={isExpandColumn}
|
|
371
|
+
<!--
|
|
372
|
+
`.row-main` stacks the column label on top and the
|
|
373
|
+
visibility / pin controls underneath, so the label gets
|
|
374
|
+
the full card width to wrap into (instead of competing
|
|
375
|
+
horizontally with the controls). `.reorder-buttons` stays
|
|
376
|
+
as a sibling outside `.row-main` so the up / down arrows
|
|
377
|
+
remain vertically centred on the right edge of the card.
|
|
378
|
+
-->
|
|
379
|
+
<div class="row-main">
|
|
380
|
+
<span
|
|
381
|
+
class="column-label"
|
|
382
|
+
title={typeof columnDef.header === 'string' ? columnDef.header : columnDef.id}
|
|
396
383
|
>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
384
|
+
{#if typeof columnDef.header === 'function'}
|
|
385
|
+
<!-- FIXME this needs testing -->
|
|
386
|
+
{@render columnDef.header()}
|
|
387
|
+
{:else}
|
|
388
|
+
{columnDef.header}
|
|
389
|
+
{/if}
|
|
390
|
+
</span>
|
|
391
|
+
|
|
392
|
+
<div class="row-controls">
|
|
393
|
+
<div class="visibility-wrapper">
|
|
394
|
+
<Checkbox
|
|
395
|
+
icon={{ name: 'assessment', size: Size.MediumSmall }}
|
|
396
|
+
size={Size.FillWidth}
|
|
397
|
+
id={`is-visible-${tableContext.tableName}-${columnDef.id}`}
|
|
398
|
+
value={tableContext.columnVisibility[columnDef.id] !== false}
|
|
399
|
+
onchange={onVisibilityChange}
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
{#if tableContext.allowColumnReordering}
|
|
404
|
+
<!--
|
|
405
|
+
Tri-state pin selector rendered via `ThreeStateRadio`,
|
|
406
|
+
matching the visual segmented-control used elsewhere in
|
|
407
|
+
the table (filter Active state, etc.). The three
|
|
408
|
+
`Ternary` slots map to:
|
|
409
|
+
False → pinned LEFT (pin-left icon, leftmost)
|
|
410
|
+
Neutral → unpinned (circle-cross icon, middle)
|
|
411
|
+
True → pinned RIGHT (pin-right icon, rightmost)
|
|
412
|
+
Expand columns can never be pinned, so the group goes
|
|
413
|
+
fully disabled for them.
|
|
414
|
+
-->
|
|
415
|
+
<div
|
|
416
|
+
class="pin-selector"
|
|
417
|
+
class:disabled={isExpandColumn}
|
|
418
|
+
>
|
|
419
|
+
<ThreeStateRadio
|
|
420
|
+
value={pinSide}
|
|
421
|
+
disabled={isExpandColumn}
|
|
422
|
+
noLabel="Pin to left"
|
|
423
|
+
neutralLabel="Unpinned"
|
|
424
|
+
yesLabel="Pin to right"
|
|
425
|
+
noIcon={{ name: 'pin-left', size: Size.MediumSmall }}
|
|
426
|
+
neutralIcon={{ name: 'circle-cross', size: Size.MediumSmall }}
|
|
427
|
+
yesIcon={{ name: 'pin-right', size: Size.MediumSmall }}
|
|
428
|
+
onchange={(next: Ternary) => {
|
|
429
|
+
setPin(next);
|
|
430
|
+
}}
|
|
431
|
+
/>
|
|
432
|
+
</div>
|
|
433
|
+
{/if}
|
|
410
434
|
</div>
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<span
|
|
414
|
-
class="column-label"
|
|
415
|
-
title={typeof columnDef.header === 'string' ? columnDef.header : columnDef.id}
|
|
416
|
-
>
|
|
417
|
-
{#if typeof columnDef.header === 'function'}
|
|
418
|
-
<!-- FIXME this needs testing -->
|
|
419
|
-
{@render columnDef.header()}
|
|
420
|
-
{:else}
|
|
421
|
-
{columnDef.header}
|
|
422
|
-
{/if}
|
|
423
|
-
</span>
|
|
435
|
+
</div>
|
|
424
436
|
|
|
425
437
|
{#if tableContext.allowColumnReordering}
|
|
426
438
|
<!--
|
|
@@ -481,10 +493,11 @@ li.targeted {
|
|
|
481
493
|
|
|
482
494
|
li.single-column:not(.anchor) {
|
|
483
495
|
display: flex;
|
|
484
|
-
|
|
485
|
-
padding: 0.5rem 0.75rem;
|
|
486
|
-
margin-bottom: 0.5rem;
|
|
496
|
+
flex-flow: row nowrap;
|
|
487
497
|
align-items: center;
|
|
498
|
+
gap: 0.5rem;
|
|
499
|
+
padding: 0.5rem 0.625rem;
|
|
500
|
+
margin-bottom: 0.375rem;
|
|
488
501
|
background-color: #ffffff;
|
|
489
502
|
border: solid 1px #cbd4da;
|
|
490
503
|
border-radius: 4px;
|
|
@@ -502,15 +515,30 @@ li.single-column:not(.anchor):last-of-type {
|
|
|
502
515
|
margin-bottom: 0;
|
|
503
516
|
}
|
|
504
517
|
|
|
518
|
+
.row-main {
|
|
519
|
+
flex: 1 1 auto;
|
|
520
|
+
min-width: 0;
|
|
521
|
+
display: flex;
|
|
522
|
+
flex-flow: column nowrap;
|
|
523
|
+
gap: 0.375rem;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.row-controls {
|
|
527
|
+
display: flex;
|
|
528
|
+
flex-flow: row nowrap;
|
|
529
|
+
align-items: center;
|
|
530
|
+
gap: 0.5rem;
|
|
531
|
+
}
|
|
532
|
+
|
|
505
533
|
.visibility-wrapper {
|
|
506
|
-
width:
|
|
507
|
-
height:
|
|
534
|
+
width: 2.25rem;
|
|
535
|
+
height: 2.25rem;
|
|
508
536
|
display: flex;
|
|
509
537
|
flex-shrink: 0;
|
|
510
538
|
}
|
|
511
539
|
|
|
512
540
|
.pin-selector {
|
|
513
|
-
--input-height:
|
|
541
|
+
--input-height: 2.25rem;
|
|
514
542
|
--input-width: auto;
|
|
515
543
|
flex-shrink: 0;
|
|
516
544
|
display: flex;
|
|
@@ -522,10 +550,12 @@ li.single-column:not(.anchor):last-of-type {
|
|
|
522
550
|
}
|
|
523
551
|
|
|
524
552
|
.column-label {
|
|
525
|
-
|
|
553
|
+
width: 100%;
|
|
526
554
|
min-width: 0;
|
|
527
555
|
font-size: 1.125rem;
|
|
528
|
-
|
|
556
|
+
font-weight: 500;
|
|
557
|
+
line-height: 1.25;
|
|
558
|
+
color: #3a4952;
|
|
529
559
|
display: -webkit-box;
|
|
530
560
|
-webkit-line-clamp: 2;
|
|
531
561
|
-webkit-box-orient: vertical;
|
|
@@ -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
|
|
@@ -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
|
});
|
|
@@ -156,10 +167,25 @@
|
|
|
156
167
|
`#${tableContext.tableName}-cell-${row}-${def.id}`
|
|
157
168
|
) as HTMLElement | undefined;
|
|
158
169
|
|
|
170
|
+
// Skip horizontal scroll-into-view for pinned (sticky)
|
|
171
|
+
// cells. Sticky positioning makes the cell visually
|
|
172
|
+
// "always on screen", but its true layout position is at
|
|
173
|
+
// its real column track - which for a right-pinned column
|
|
174
|
+
// in a wider-than-viewport table is offscreen. Asking the
|
|
175
|
+
// browser to `scrollIntoView({inline: 'nearest'})` then
|
|
176
|
+
// scrolls the table horizontally to bring the cell's
|
|
177
|
+
// underlying position into view, which is the user-facing
|
|
178
|
+
// "table jumps right when I click the Actions button"
|
|
179
|
+
// bug. The vertical block axis still scrolls because the
|
|
180
|
+
// row's vertical position is the user's actual scroll
|
|
181
|
+
// target.
|
|
182
|
+
const isPinned =
|
|
183
|
+
cell?.classList.contains('pinned-left') ||
|
|
184
|
+
cell?.classList.contains('pinned-right');
|
|
159
185
|
cell?.scrollIntoView({
|
|
160
186
|
behavior: 'instant',
|
|
161
187
|
block: 'nearest',
|
|
162
|
-
inline: 'nearest',
|
|
188
|
+
inline: isPinned ? undefined : 'nearest',
|
|
163
189
|
});
|
|
164
190
|
}
|
|
165
191
|
});
|
package/dist/scss/_mixins.scss
CHANGED
|
@@ -1869,6 +1869,17 @@
|
|
|
1869
1869
|
overflow: visible;
|
|
1870
1870
|
padding-left: 0;
|
|
1871
1871
|
padding-right: 0;
|
|
1872
|
+
// Reassert the column-separator border-right here so it survives
|
|
1873
|
+
// even when a narrow select-filter cell's internal widget (the
|
|
1874
|
+
// SingleSelect / MultiSelect dropdown trigger) paints flush to
|
|
1875
|
+
// the right edge. Without this rule, two adjacent select filters
|
|
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;
|
|
1882
|
+
box-sizing: border-box;
|
|
1872
1883
|
|
|
1873
1884
|
div.stick-right {
|
|
1874
1885
|
position: absolute;
|