@lavalogic/scoria 0.8.2 → 0.9.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.
Files changed (26) hide show
  1. package/dist/Components/Table/Body/QuickSearchCell.svelte +28 -43
  2. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +3 -3
  3. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +3 -3
  4. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +3 -3
  5. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +3 -3
  6. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +3 -3
  7. package/dist/Components/Table/ColumnDefinitions/{ExampleItemColumnDef.svelte.d.ts → ExampleItemColumnDefRepository.svelte.d.ts} +2 -2
  8. package/dist/Components/Table/ColumnDefinitions/{ExampleItemColumnDef.svelte.js → ExampleItemColumnDefRepository.svelte.js} +11 -10
  9. package/dist/Components/Table/Misc/FilterInput.svelte +22 -46
  10. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.d.ts +1 -1
  11. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.js +3 -4
  12. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.d.ts +3 -0
  13. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.js +2 -0
  14. package/dist/Components/Table/Types/Context/Row.d.ts +1 -1
  15. package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +6 -1
  16. package/dist/Components/Table/Types/Context/TableContext.svelte.js +100 -45
  17. package/dist/Components/Table/Types/DataRepository/LocalTableDataRepository.svelte.d.ts +4 -4
  18. package/dist/Components/Table/Types/DataRepository/LocalTableDataRepository.svelte.js +16 -12
  19. package/dist/Components/Table/Types/Filtering/CustomFilter.d.ts +0 -1
  20. package/dist/Components/Table/Types/Filtering/CustomFilterFn.d.ts +1 -1
  21. package/dist/Components/Table/Types/Filtering/FilterFn.d.ts +1 -1
  22. package/dist/Helpers/Helpers.svelte.d.ts +8 -0
  23. package/dist/Helpers/Helpers.svelte.js +56 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/package.json +4 -4
@@ -45,7 +45,9 @@
45
45
  const customFilter = $derived(customFilters?.get(def.id));
46
46
 
47
47
  const isSelect = $derived(
48
- def instanceof SelectDef || customFilter?.type === CustomFilterType.Select
48
+ def instanceof SelectDef ||
49
+ def.filterValueType == CustomFilterType.Select ||
50
+ customFilter?.type === CustomFilterType.Select
49
51
  );
50
52
 
51
53
  const hasFilterFn = $derived(!!def.filterFn && typeof def.filterFn === 'function');
@@ -65,8 +67,13 @@
65
67
  def.filterValueType == 'Number' ||
66
68
  customFilter?.type === CustomFilterType.Number
67
69
  );
70
+
71
+ function emptyOptsAndWarn(): Array<never> {
72
+ console.warn(`def ${def.id} is declared as a select filter but has no getOptions() function`);
73
+ return [];
74
+ }
68
75
  const options = $derived(
69
- isSelect ? ((def as SelectDef<T>)?.getOptions?.() ?? customFilter?.options ?? []) : []
76
+ isSelect ? (def?.getOptions?.() ?? customFilter?.options ?? emptyOptsAndWarn()) : []
70
77
  );
71
78
  const isDate = $derived(def instanceof DateInputDef);
72
79
  const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
@@ -339,47 +346,25 @@
339
346
  {/if}
340
347
  {:else if isSelect}
341
348
  {#if hasQuery}
342
- {#if customFilter!.valueAsObject == false}
343
- {@const label =
344
- customFilter?.id ??
345
- customFilter?.label ??
346
- customFilter?.objectName ??
347
- customFilter?.queryKey ??
348
- def.id}
349
- <!-- TODO: multiselect -->
350
-
351
- <QuerySelect
352
- valueAsObject={false}
353
- clearable
354
- name={customFilter?.objectName ?? 'Item'}
355
- valueProp="value"
356
- labelProp="label"
357
- value={value as string | number | null}
358
- query={customFilter!.query!}
359
- bind:options={lastQueryOptions}
360
- onchange={generateOnChangePrimitive(label)}
361
- />
362
- {:else}
363
- {@const label =
364
- customFilter?.id ??
365
- customFilter?.label ??
366
- customFilter?.objectName ??
367
- customFilter?.queryKey ??
368
- def.id}
369
- <!-- TODO: multiselect -->
370
-
371
- <QuerySelect
372
- valueAsObject={true}
373
- clearable
374
- name={customFilter?.objectName ?? 'Item'}
375
- valueProp="value"
376
- labelProp="label"
377
- value={value as null}
378
- query={customFilter!.query!}
379
- bind:options={lastQueryOptions}
380
- onchange={generateOnChangeObject(label)}
381
- />
382
- {/if}
349
+ {@const label =
350
+ customFilter?.id ??
351
+ customFilter?.label ??
352
+ customFilter?.objectName ??
353
+ customFilter?.queryKey ??
354
+ def.id}
355
+ <!-- TODO: multiselect -->
356
+
357
+ <QuerySelect
358
+ valueAsObject={false}
359
+ clearable
360
+ name={customFilter?.objectName ?? 'Item'}
361
+ valueProp="value"
362
+ labelProp="label"
363
+ value={value as string | number | null}
364
+ query={customFilter!.query!}
365
+ bind:options={lastQueryOptions}
366
+ onchange={generateOnChangePrimitive(label)}
367
+ />
383
368
  {:else}
384
369
  {#await options then options}
385
370
  <MultiSelect
@@ -47,16 +47,16 @@
47
47
  const isEditable = await editableAsync;
48
48
  if (asyncCounter === localCounter) {
49
49
  editable = isEditable;
50
- } else if (dev) {
50
+ } else if (cell.column.columnDef.debug) {
51
51
  console.log('skipping async editable because it is stale');
52
52
  }
53
53
  } catch (e) {
54
- if (dev) {
54
+ if (cell.column.columnDef.debug) {
55
55
  console.error(e);
56
56
  }
57
57
  if (asyncCounter === localCounter) {
58
58
  editable = false;
59
- } else if (dev) {
59
+ } else if (cell.column.columnDef.debug) {
60
60
  console.log('skipping async editable because it is stale');
61
61
  }
62
62
  }
@@ -64,16 +64,16 @@
64
64
  const isEditable = await editableAsync;
65
65
  if (asyncCounter === localCounter) {
66
66
  editable = isEditable;
67
- } else if (dev) {
67
+ } else if (cell.column.columnDef.debug) {
68
68
  console.log('skipping async editable because it is stale');
69
69
  }
70
70
  } catch (e) {
71
- if (dev) {
71
+ if (cell.column.columnDef.debug) {
72
72
  console.error(e);
73
73
  }
74
74
  if (asyncCounter === localCounter) {
75
75
  editable = false;
76
- } else if (dev) {
76
+ } else if (cell.column.columnDef.debug) {
77
77
  console.log('skipping async editable because it is stale');
78
78
  }
79
79
  }
@@ -72,16 +72,16 @@
72
72
  value: awaitedValue
73
73
  });
74
74
  }
75
- } else if (dev || columnDef.debug) {
75
+ } else if (columnDef.debug) {
76
76
  console.log('skipping async option insertion because it is stale');
77
77
  }
78
78
  } catch (e) {
79
- if (dev || columnDef.debug) {
79
+ if (columnDef.debug) {
80
80
  console.error(e);
81
81
  }
82
82
  if (valueAsyncCounter === localCounter) {
83
83
  //nothing happens
84
- } else if (dev || columnDef.debug) {
84
+ } else if (columnDef.debug) {
85
85
  console.log('skipping caught async option insertion error because it is stale');
86
86
  }
87
87
  }
@@ -53,16 +53,16 @@
53
53
  const isEditable = await editableAsync;
54
54
  if (asyncCounter === localCounter) {
55
55
  editable = isEditable;
56
- } else if (dev) {
56
+ } else if (cell.column.columnDef.debug) {
57
57
  console.log('skipping async editable because it is stale');
58
58
  }
59
59
  } catch (e) {
60
- if (dev) {
60
+ if (cell.column.columnDef.debug) {
61
61
  console.error(e);
62
62
  }
63
63
  if (asyncCounter === localCounter) {
64
64
  editable = false;
65
- } else if (dev) {
65
+ } else if (cell.column.columnDef.debug) {
66
66
  console.log('skipping async editable because it is stale');
67
67
  }
68
68
  }
@@ -38,16 +38,16 @@
38
38
  const isEditable = await editableAsync;
39
39
  if (asyncCounter === localCounter) {
40
40
  editable = isEditable;
41
- } else if (dev) {
41
+ } else if (cell.column.columnDef.debug) {
42
42
  console.log('skipping async editable because it is stale');
43
43
  }
44
44
  } catch (e) {
45
- if (dev) {
45
+ if (cell.column.columnDef.debug) {
46
46
  console.error(e);
47
47
  }
48
48
  if (asyncCounter === localCounter) {
49
49
  editable = false;
50
- } else if (dev) {
50
+ } else if (cell.column.columnDef.debug) {
51
51
  console.log('skipping async editable because it is stale');
52
52
  }
53
53
  }
@@ -1,7 +1,7 @@
1
1
  import type { ExampleItem } from '../../../Types/Examples/ExampleItem.js';
2
- import type { Component } from 'svelte';
2
+ import { type Component } from 'svelte';
3
3
  import type { DefsWrapper } from '../Types/Columns/DefsWrapper.js';
4
4
  import type { DisplayDefModalProps } from '../Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js';
5
- export declare class ExampleItemColumnDef {
5
+ export declare class ExampleItemColumnDefRepository {
6
6
  static generateExampleItemColumnDefs(modal: Component<DisplayDefModalProps<ExampleItem>, any, any>): DefsWrapper<ExampleItem>;
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { Colour } from '../../../scss/colours.js';
2
2
  import { ExampleSelectEnum } from '../../../Types/Examples/ExampleSelectEnum.js';
3
3
  import { Validity } from '../../../Types/Internal/Validity.js';
4
+ import { tick, untrack } from 'svelte';
4
5
  import { CheckboxDef } from '../Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js';
5
6
  import { DateInputDef } from '../Types/Columns/Definitions/Accessors/DateInputDef.svelte.js';
6
7
  import { NumberInputDef } from '../Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js';
@@ -12,7 +13,7 @@ import { ProgressBarDef } from '../Types/Columns/Definitions/Display/ProgressBar
12
13
  import { ValidityDef } from '../Types/Columns/Definitions/Display/ValidityDef.svelte.js';
13
14
  import { asyncSleep } from '../../../Helpers/Helpers.svelte.js';
14
15
  import { QueryDef } from '../Types/Columns/Definitions/Accessors/QueryDef.svelte.js';
15
- export class ExampleItemColumnDef {
16
+ export class ExampleItemColumnDefRepository {
16
17
  static generateExampleItemColumnDefs(modal) {
17
18
  let defs = $state([
18
19
  new ValidityDef({
@@ -35,17 +36,16 @@ export class ExampleItemColumnDef {
35
36
  },
36
37
  modal
37
38
  }),
38
- new DisplayDef({
39
- id: 'account_id',
40
- header: 'Account',
41
- accessorFn: (item) => `Unknown (id ${item.id})`,
42
- filterValueType: 'Number',
43
- debug: true
44
- }),
45
39
  new DisplayDef({
46
40
  header: 'ID',
47
41
  id: 'id',
48
- filterValueType: 'Number'
42
+ filterValueType: 'Number',
43
+ // TODO make filtering work with the below:
44
+ accessorFn: async (item) => {
45
+ void item.id;
46
+ await asyncSleep(500);
47
+ return item.id;
48
+ }
49
49
  }),
50
50
  new QueryDef({
51
51
  header: 'simulated query def',
@@ -55,7 +55,8 @@ export class ExampleItemColumnDef {
55
55
  isEditable: () => true,
56
56
  id: 'simulatedQuery',
57
57
  query: async (v, item, index) => {
58
- asyncSleep(25);
58
+ void item.someDate;
59
+ await asyncSleep(25);
59
60
  return item.someDate
60
61
  ? new Array(5).fill(0).map((it, index) => {
61
62
  return {
@@ -85,7 +85,6 @@
85
85
 
86
86
  const customFilter = $derived(customFilters?.get(def.id));
87
87
  const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
88
- const isValueAsObject = $derived(!(customFilter?.valueAsObject == false));
89
88
 
90
89
  let value: SelectOption<unknown> | unknown | null | undefined = $state(null);
91
90
  let valueMin: number | null = $state(null);
@@ -383,7 +382,7 @@
383
382
  valueProp="value"
384
383
  name={customFilter?.objectName ?? 'Item'}
385
384
  {options}
386
- valueAsObject={isValueAsObject as true}
385
+ valueAsObject={false}
387
386
  onchange={(e) => {
388
387
  tableContext.paginationRepo?.filterBy(
389
388
  def.id,
@@ -402,49 +401,26 @@
402
401
  <div class="contents">
403
402
  {#await options then options}
404
403
  {#if hasQuery}
405
- {#if customFilter!.valueAsObject == false}
406
- {@const label =
407
- customFilter?.id ??
408
- customFilter?.label ??
409
- customFilter?.objectName ??
410
- customFilter?.queryKey ??
411
- def.id}
412
- <!-- TODO: multiselect -->
413
-
414
- <QuerySelect
415
- valueAsObject={false}
416
- {label}
417
- clearable
418
- name={customFilter?.objectName ?? 'Item'}
419
- valueProp="value"
420
- labelProp="label"
421
- value={value as string | number | null}
422
- query={customFilter!.query!}
423
- bind:options={lastQueryOptions}
424
- onchange={generateOnChangePrimitive(label)}
425
- />
426
- {:else}
427
- {@const label =
428
- customFilter?.id ??
429
- customFilter?.label ??
430
- customFilter?.objectName ??
431
- customFilter?.queryKey ??
432
- def.id}
433
- <!-- TODO: multiselect -->
434
-
435
- <QuerySelect
436
- valueAsObject={true}
437
- {label}
438
- clearable
439
- name={customFilter?.objectName ?? 'Item'}
440
- valueProp="value"
441
- labelProp="label"
442
- value={value as null}
443
- query={customFilter!.query!}
444
- bind:options={lastQueryOptions}
445
- onchange={generateOnChangeObject(label)}
446
- />
447
- {/if}
404
+ {@const label =
405
+ customFilter?.id ??
406
+ customFilter?.label ??
407
+ customFilter?.objectName ??
408
+ customFilter?.queryKey ??
409
+ def.id}
410
+ <!-- TODO: multiselect -->
411
+
412
+ <QuerySelect
413
+ valueAsObject={false}
414
+ {label}
415
+ clearable
416
+ name={customFilter?.objectName ?? 'Item'}
417
+ valueProp="value"
418
+ labelProp="label"
419
+ value={value as string | number | null}
420
+ query={customFilter!.query!}
421
+ bind:options={lastQueryOptions}
422
+ onchange={generateOnChangePrimitive(label)}
423
+ />
448
424
  {:else}
449
425
  <MultiSelect
450
426
  label={customFilter?.id ??
@@ -456,7 +432,7 @@
456
432
  valueProp="value"
457
433
  name={customFilter?.objectName ?? 'Item'}
458
434
  {options}
459
- valueAsObject={isValueAsObject as true}
435
+ valueAsObject={false}
460
436
  onchange={(e) => {
461
437
  tableContext.paginationRepo?.filterBy(
462
438
  def.id,
@@ -21,5 +21,5 @@ export declare class SelectDef<T extends object> extends AccessorDef<T, SelectFi
21
21
  readonly optional: boolean;
22
22
  readonly clearable: boolean;
23
23
  readonly getSpecificOptions?: (item: T, index: number) => Array<SelectOption<unknown>> | Promise<Array<SelectOption<unknown>>>;
24
- readonly getOptions: () => Array<SelectOption<unknown>> | Promise<Array<SelectOption<unknown>>>;
24
+ getOptions: () => Array<SelectOption<unknown>> | Promise<Array<SelectOption<unknown>>>;
25
25
  }
@@ -3,12 +3,12 @@ import { AccessorType } from './AccessorType.js';
3
3
  export class SelectDef extends AccessorDef {
4
4
  constructor(props) {
5
5
  super(props);
6
- this.getSpecificOptions = $derived(props.getSpecificOptions);
7
- this.getOptions = $derived(props.getOptions);
6
+ this.getSpecificOptions = $derived(props.getSpecificOptions); //done in the base class now
7
+ // this.getOptions = $derived(props.getOptions); // done in the base class now
8
8
  this.sortingFn = $derived(props.sortingFn);
9
9
  this.optional = $derived(props.optional ?? false);
10
10
  this.clearable = $derived(props.clearable ?? true);
11
- this.getDisplayValue = $derived(props.getDisplayValue);
11
+ // this.getDisplayValue = $derived(props.getDisplayValue); //done in the base class now
12
12
  this._filterFn = $state(props.filterFn);
13
13
  }
14
14
  accessorType = AccessorType.Select;
@@ -23,5 +23,4 @@ export class SelectDef extends AccessorDef {
23
23
  optional;
24
24
  clearable;
25
25
  getSpecificOptions;
26
- getOptions;
27
26
  }
@@ -6,6 +6,7 @@ import { ColumnType } from '../ColumnType.js';
6
6
  import type { AccessorFn } from './AccessorFn.js';
7
7
  import type { ValidationFn } from './ValidationFn.js';
8
8
  import type { FilterValueType } from '../../Filtering/FilterValueType.js';
9
+ import type { SelectOption } from '../../../../../Types/Internal/SelectOption.js';
9
10
  export interface ColumnDefProps<T extends object, FilterFnType = undefined> {
10
11
  id: string;
11
12
  header: string | Snippet;
@@ -21,6 +22,7 @@ export interface ColumnDefProps<T extends object, FilterFnType = undefined> {
21
22
  isValid?: ValidationFn<T>;
22
23
  filterFn?: FilterFn<T, FilterFnType> | CustomFilterFn;
23
24
  filterValueType?: FilterValueType;
25
+ getOptions?: () => Array<SelectOption<unknown>> | Promise<Array<SelectOption<unknown>>>;
24
26
  accessorFn?: AccessorFn<T>;
25
27
  getDisplayValue?: (item: T, index: number) => string | null | Promise<string | null>;
26
28
  }
@@ -60,4 +62,5 @@ export declare abstract class ColumnDef<T extends object, FilterFnType = undefin
60
62
  readonly isEditable: (item: T, index: number) => boolean | Promise<boolean>;
61
63
  readonly resizable: boolean;
62
64
  readonly header: string | Snippet;
65
+ readonly getOptions?: () => Array<SelectOption<unknown>> | Promise<Array<SelectOption<unknown>>>;
63
66
  }
@@ -16,6 +16,7 @@ export class ColumnDef {
16
16
  this._filterValueType = $derived(props.filterValueType);
17
17
  this._getDisplayValue = $derived(props.getDisplayValue);
18
18
  this.debug = $derived(props.debug ?? false);
19
+ this.getOptions = $derived(props.getOptions);
19
20
  }
20
21
  id;
21
22
  debug;
@@ -87,4 +88,5 @@ export class ColumnDef {
87
88
  isEditable;
88
89
  resizable;
89
90
  header;
91
+ getOptions;
90
92
  }
@@ -1,6 +1,6 @@
1
1
  import type { TableCell } from '../Columns/TableCell.js';
2
2
  export interface Row<T extends object> {
3
- getValue: (columnId: string) => any;
3
+ getValue: (columnId: string) => Promise<any>;
4
4
  original: T;
5
5
  visibleIndex: number;
6
6
  originalIndex: number;
@@ -142,7 +142,12 @@ export declare class TableContext<T extends object> {
142
142
  private columnDefsById;
143
143
  private readonly getSortingFn;
144
144
  readonly originalRowIndices: Map<T, number>;
145
- readonly filteredIndeces: number[];
145
+ private _filterTimer;
146
+ private get filterTimer();
147
+ private set filterTimer(value);
148
+ private _filteredIndices;
149
+ get filteredIndices(): number[];
150
+ set filteredIndices(v: number[]);
146
151
  readonly filteredData: T[];
147
152
  readonly sortedData: T[];
148
153
  readonly sortedRowModel: RowModel<T>;
@@ -4,7 +4,7 @@ import { ToolbarPosition } from '../Toolbar/ToolbarPosition.js';
4
4
  import { RowSelectionDef } from '../Columns/Definitions/RowSelection/RowSelectionDef.svelte.js';
5
5
  import { tick } from 'svelte';
6
6
  import { PaginationType } from '../Pagination/PaginationType.js';
7
- import { getCanvasContext } from '../../../../Helpers/Helpers.svelte.js';
7
+ import { asyncAll, asyncFilter, asyncFilterIndices, asyncSleep, asyncSome, getCanvasContext, getErrorMessage } from '../../../../Helpers/Helpers.svelte.js';
8
8
  import { AccessorDef } from '../Columns/Definitions/Accessors/AccessorDef.svelte.js';
9
9
  import { NumberInputDef } from '../Columns/Definitions/Accessors/NumberInputDef.svelte.js';
10
10
  import { SelectDef } from '../Columns/Definitions/Accessors/SelectDef.svelte.js';
@@ -13,7 +13,7 @@ import { CheckboxDef } from '../Columns/Definitions/Accessors/CheckboxDef.svelte
13
13
  import { DateInputDef } from '../Columns/Definitions/Accessors/DateInputDef.svelte.js';
14
14
  import { ProgressBarDef } from '../Columns/Definitions/Display/ProgressBarDef.svelte.js';
15
15
  import { DisplayDef } from '../Columns/Definitions/Display/DisplayDef.svelte.js';
16
- import { browser } from '$app/environment';
16
+ import { browser, dev } from '$app/environment';
17
17
  import { ActionsDef } from '../Columns/Definitions/Actions/ActionsDef.svelte.js';
18
18
  import { BubbleDef } from '../Columns/Definitions/Display/BubbleDef.svelte.js';
19
19
  const SORTING_ICON_WIDTH = 24;
@@ -142,6 +142,83 @@ export class TableContext {
142
142
  }
143
143
  });
144
144
  }
145
+ $effect(() => {
146
+ void this.paginationRepo?.paginationType;
147
+ void this.dataRepo?.filtering;
148
+ void this.dataRepo?.filterModes.values();
149
+ void this.dataRepo?.data;
150
+ (async () => {
151
+ const startTime = Date.now();
152
+ let currentTimerPromise;
153
+ const abortController = new AbortController();
154
+ try {
155
+ if (this.paginationRepo?.paginationType == PaginationType.Local) {
156
+ const filterState = this.dataRepo?.filtering ?? [];
157
+ const data = this.dataRepo?.data ?? [];
158
+ if (!filterState.length) {
159
+ this.filteredIndices = data.map((_, index) => index) ?? [];
160
+ }
161
+ currentTimerPromise = this.filterTimer = this.filterTimer + 1;
162
+ await asyncSleep(150);
163
+ const currentTimer = await currentTimerPromise;
164
+ if (currentTimerPromise != this.filterTimer) {
165
+ throw new Error('stale filter');
166
+ }
167
+ let settled = false;
168
+ const filterPromise = asyncFilterIndices(data, async (item, index) => {
169
+ const row = this.createRow(item, index, index);
170
+ return asyncAll(filterState, async (filter) => {
171
+ abortController.signal.throwIfAborted();
172
+ const def = this.columnDefsById.get(filter.id);
173
+ if (!def) {
174
+ console.warn(`Could not find column def for filter with id ${filter.id}`);
175
+ return false;
176
+ }
177
+ if (def.filterFn) {
178
+ return def.filterFn(row, filter.id, filter.value);
179
+ }
180
+ if (dev) {
181
+ console.warn(`column ${def.id} did not have a filter function`);
182
+ }
183
+ return false;
184
+ });
185
+ });
186
+ filterPromise
187
+ .then((indices) => {
188
+ this.filteredIndices = indices;
189
+ })
190
+ .catch((e) => {
191
+ if (dev) {
192
+ console.log(`caught filter rejection:`, e);
193
+ console.log(`iteration ${currentTimer} cancelled after ${Date.now() - startTime}ms`);
194
+ }
195
+ })
196
+ .finally(() => {
197
+ settled = true;
198
+ });
199
+ for (;;) {
200
+ if (currentTimerPromise !== this.filterTimer) {
201
+ abortController.abort('stale data');
202
+ }
203
+ if (settled) {
204
+ console.log(`iteration ${await currentTimerPromise} finished ${Date.now() - startTime}ms`);
205
+ return;
206
+ }
207
+ await tick();
208
+ }
209
+ }
210
+ }
211
+ catch (e) {
212
+ if (dev) {
213
+ const message = getErrorMessage(e);
214
+ if (!message.toLocaleLowerCase().startsWith('stale')) {
215
+ console.error(e);
216
+ }
217
+ console.log(`iteration ${await currentTimerPromise} cancelled after ${Date.now() - startTime}ms`);
218
+ }
219
+ }
220
+ })();
221
+ });
145
222
  }
146
223
  // #endregion
147
224
  // #region stores
@@ -504,49 +581,27 @@ export class TableContext {
504
581
  };
505
582
  // eslint-disable-next-line svelte/prefer-svelte-reactivity
506
583
  originalRowIndices = $derived(new Map((this.dataRepo?.data ?? []).map((item, index) => [item, index])));
507
- filteredIndeces = $derived.by(() => {
508
- if (this.paginationRepo?.paginationType == PaginationType.Local) {
509
- const filterState = this.dataRepo?.filtering ?? [];
510
- const data = this.dataRepo?.data ?? [];
511
- if (!filterState.length) {
512
- return data.map((_, index) => index) ?? [];
513
- }
514
- const filtered = data
515
- .map((item, index) => {
516
- const row = this.createRow(item, index, index);
517
- const x = filterState.every((filter) => {
518
- const def = this.columnDefsById.get(filter.id);
519
- if (!def) {
520
- console.warn(`Could not find column def for filter with id ${filter.id}`);
521
- return true;
522
- }
523
- if (def.filterFn) {
524
- const x = def.filterFn(row, filter.id, filter.value);
525
- // debugger;
526
- return x;
527
- }
528
- console.warn(`row ${row.id} did not have a filter function`);
529
- return false;
530
- });
531
- // debugger;
532
- if (x) {
533
- return index;
534
- }
535
- else {
536
- return null;
537
- }
538
- })
539
- .filter((it) => it != null);
540
- return filtered;
541
- }
542
- return this.dataRepo?.data.map((_, index) => index) ?? [];
543
- });
584
+ // private _filterTimer: ReturnType<typeof setTimeout> | undefined;
585
+ _filterTimer = 1;
586
+ get filterTimer() {
587
+ return this._filterTimer;
588
+ }
589
+ set filterTimer(v) {
590
+ this._filterTimer = v;
591
+ }
592
+ _filteredIndices = $state([]);
593
+ get filteredIndices() {
594
+ return this._filteredIndices;
595
+ }
596
+ set filteredIndices(v) {
597
+ this._filteredIndices = v;
598
+ }
544
599
  filteredData = $derived.by(() => {
545
600
  if (this.paginationRepo?.paginationType == PaginationType.Local) {
546
601
  const data = this.showSelectedOnly
547
602
  ? // FIXME this will show the selected items by order of insertion to the selectedItems map.
548
603
  [...this.selectedItems.values()]
549
- : (this.filteredIndeces.map((index) => {
604
+ : (this.filteredIndices.map((index) => {
550
605
  return this.dataRepo.data[index];
551
606
  }) ?? []);
552
607
  return data;
@@ -600,7 +655,7 @@ export class TableContext {
600
655
  const rows = sortedData.map((item, index) => {
601
656
  const unsortedIndex = this.originalRowIndices.get(item) ?? index;
602
657
  const row = {
603
- getValue: (key) => {
658
+ getValue: async (key) => {
604
659
  const gotValue = this.columnDefsById.get(key).accessorFn?.(item, index);
605
660
  if (gotValue !== undefined) {
606
661
  return gotValue;
@@ -1597,7 +1652,7 @@ export class TableContext {
1597
1652
  // #region private methods
1598
1653
  createRow(item, index, unsortedIndex) {
1599
1654
  const row = {
1600
- getValue: (key) => {
1655
+ getValue: async (key) => {
1601
1656
  const gotValue = this.columnDefsById.get(key).accessorFn?.(item, unsortedIndex);
1602
1657
  if (gotValue !== undefined) {
1603
1658
  return gotValue;
@@ -1740,11 +1795,11 @@ export class TableContext {
1740
1795
  it.index >= minColumnIndex &&
1741
1796
  it.index <= maxColumnIndex)
1742
1797
  .forEach((it) => affectedColumns.add(it.id));
1743
- const indeces = new SvelteSet();
1798
+ const indices = new SvelteSet();
1744
1799
  for (let i = minRowIndex; i <= maxRowIndex; i++) {
1745
- indeces.add(parseInt(model.rows[i].id));
1800
+ indices.add(parseInt(model.rows[i].id));
1746
1801
  }
1747
- this.focusedRowIndexIds = indeces;
1802
+ this.focusedRowIndexIds = indices;
1748
1803
  this.focusedColumnIds = affectedColumns;
1749
1804
  }
1750
1805
  async getHighlightedRowData() {
@@ -42,11 +42,11 @@ export declare class LocalTableDataRepository<T extends object> extends TableDat
42
42
  prevPage: () => void;
43
43
  readonly dateFilter: ILocalDataRepository<T>['dateFilter'];
44
44
  readonly numberFilter: (row: Row<T>, columnId: string, // & keyof T,
45
- values: NumberFilterValueType) => boolean;
45
+ values: NumberFilterValueType) => Promise<boolean>;
46
46
  readonly selectFilter: <columnType = T[keyof T]>(row: Row<T>, columnId: string, // & keyof T,
47
- filterValue: SelectFilterValueType<T>) => boolean;
47
+ filterValue: SelectFilterValueType<T>) => Promise<boolean>;
48
48
  readonly boolFilter: (row: Row<T>, columnId: string, // & keyof T,
49
- filterValue: BoolFilterValueType) => boolean;
49
+ filterValue: BoolFilterValueType) => Promise<boolean>;
50
50
  readonly stringFilter: (row: Row<T>, columnId: string, // & keyof T,
51
- filterValue: StringFilterValueType) => any;
51
+ filterValue: StringFilterValueType) => Promise<any>;
52
52
  }
@@ -130,12 +130,12 @@ export class LocalTableDataRepository extends TableDataRepository {
130
130
  };
131
131
  };
132
132
  // #region filters
133
- dateFilter = (row, columnId, // & keyof T,
133
+ dateFilter = async (row, columnId, // & keyof T,
134
134
  [dateFrom, dateTo]) => {
135
135
  if (!dateFrom && !dateTo) {
136
136
  return true;
137
137
  }
138
- const columnValue = row.getValue(columnId)?.valueOf();
138
+ const columnValue = (await row.getValue(columnId))?.valueOf();
139
139
  if (!columnValue) {
140
140
  return false;
141
141
  }
@@ -159,9 +159,9 @@ export class LocalTableDataRepository extends TableDataRepository {
159
159
  }
160
160
  return true;
161
161
  };
162
- numberFilter = (row, columnId, // & keyof T,
162
+ numberFilter = async (row, columnId, // & keyof T,
163
163
  values) => {
164
- const columnValue = row.getValue(columnId);
164
+ const columnValue = await row.getValue(columnId);
165
165
  if (!columnValue) {
166
166
  return false;
167
167
  }
@@ -246,12 +246,13 @@ export class LocalTableDataRepository extends TableDataRepository {
246
246
  //default
247
247
  return !!values[0] && columnValue.toString().includes(values[0].toString());
248
248
  };
249
- selectFilter = (row, columnId, // & keyof T,
249
+ selectFilter = async (row, columnId, // & keyof T,
250
250
  filterValue) => {
251
251
  if (!filterValue) {
252
252
  return true;
253
253
  }
254
- const columnValue = row.getValue(columnId);
254
+ void filterValue;
255
+ const columnValue = (await row.getValue(columnId));
255
256
  if (Array.isArray(filterValue)) {
256
257
  return filterValue.find((it) => it === columnValue) != null;
257
258
  }
@@ -263,21 +264,21 @@ export class LocalTableDataRepository extends TableDataRepository {
263
264
  }
264
265
  return filterValue == columnValue;
265
266
  };
266
- boolFilter = (row, columnId, // & keyof T,
267
+ boolFilter = async (row, columnId, // & keyof T,
267
268
  filterValue) => {
268
- const columnValue = row.getValue(columnId);
269
+ const columnValue = (await row.getValue(columnId));
269
270
  return filterValue == null || filterValue === columnValue;
270
271
  };
271
- stringFilter = (row, columnId, // & keyof T,
272
+ stringFilter = async (row, columnId, // & keyof T,
272
273
  filterValue) => {
273
274
  if (!filterValue) {
274
275
  return true;
275
276
  }
276
- const columnValue = row.getValue(columnId)?.toString().trim().toLocaleLowerCase();
277
+ filterValue = filterValue.trim().toLocaleLowerCase();
278
+ const columnValue = (await row.getValue(columnId))?.toString().trim().toLocaleLowerCase();
277
279
  if (!columnValue) {
278
280
  return true;
279
281
  }
280
- filterValue = filterValue.trim().toLocaleLowerCase();
281
282
  const filterType = this.filterModes.get(columnId) ?? StringFilterMode.Contains;
282
283
  if (filterType === StringFilterMode.EndsWith) {
283
284
  return columnValue.endsWith(filterValue);
@@ -288,10 +289,13 @@ export class LocalTableDataRepository extends TableDataRepository {
288
289
  if (filterType === StringFilterMode.StartsWith) {
289
290
  return columnValue.startsWith(filterValue);
290
291
  }
292
+ if (filterType === StringFilterMode.NotEquals) {
293
+ return columnValue !== filterValue;
294
+ }
295
+ //last ditch effort and fallback
291
296
  if (filterType !== StringFilterMode.Contains) {
292
297
  console.warn(`unhandled filter type value ${filterType}`);
293
298
  }
294
- //default
295
299
  return columnValue.includes(filterValue);
296
300
  };
297
301
  }
@@ -4,7 +4,6 @@ export interface CustomFilter {
4
4
  queryKey: string;
5
5
  type: CustomFilterType;
6
6
  id: string;
7
- valueAsObject?: boolean;
8
7
  options?: Array<SelectOption<string | number>> | Promise<Array<SelectOption<string | number>>>;
9
8
  multiSelect?: boolean;
10
9
  objectName?: string;
@@ -1,2 +1,2 @@
1
1
  import type { FilterMode } from './FilterMode.js';
2
- export type CustomFilterFn = (mode: FilterMode, value: unknown) => boolean;
2
+ export type CustomFilterFn = (mode: FilterMode, value: unknown) => Promise<boolean>;
@@ -6,4 +6,4 @@ export type SelectFilterValueType<T, columnType = T[keyof T]> = string | Array<u
6
6
  } | undefined;
7
7
  export type BoolFilterValueType = boolean | null;
8
8
  export type StringFilterValueType = string | null;
9
- export type FilterFn<T extends object, FilterValueType> = (row: Row<T>, columnId: string, filterValue: FilterValueType) => boolean;
9
+ export type FilterFn<T extends object, FilterValueType> = (row: Row<T>, columnId: string, filterValue: FilterValueType) => Promise<boolean>;
@@ -28,4 +28,12 @@ export declare function isValidEmailAddress(emailAddress: string): RegExpMatchAr
28
28
  export declare function gridItem(element: HTMLElement): (() => void) | undefined;
29
29
  export type Milliseconds = number;
30
30
  export declare function asyncSleep(delay: Milliseconds): Promise<void>;
31
+ export declare function asyncMap<T, R>(array: Array<T>, asyncCallback: (item: T, index: number) => Promise<R>): Promise<Awaited<R>[]>;
32
+ export declare function asyncForEach<T>(array: Array<T>, asyncCallback: (item: T, index: number) => Promise<void>): Promise<void>;
33
+ export declare function asyncFlatMap<T, R>(array: Array<T>, asyncCallback: (item: T, index: number) => Promise<R>): Promise<(Awaited<R> extends infer T_1 ? T_1 extends Awaited<R> ? T_1 extends readonly (infer InnerArr)[] ? InnerArr : T_1 : never : never)[]>;
34
+ export declare function asyncSome<T>(array: Array<T>, asyncCallback: (item: T, index: number) => Promise<boolean>): Promise<boolean>;
35
+ export declare function asyncAll<T>(array: Array<T>, asyncCallback: (item: T, index: number) => Promise<boolean>): Promise<boolean>;
36
+ export declare const asyncEvery: typeof asyncAll;
37
+ export declare function asyncFilterIndices<T>(array: Array<T>, asyncCallback: (item: T, index: number) => Promise<boolean>): Promise<Array<number>>;
38
+ export declare function asyncFilter<T>(array: Array<T>, asyncCallback: (item: T, index: number) => Promise<boolean>): Promise<Array<T>>;
31
39
  export {};
@@ -167,5 +167,60 @@ export function gridItem(element) {
167
167
  return () => observer.unobserve(firstChild);
168
168
  }
169
169
  export async function asyncSleep(delay) {
170
- await new Promise((r) => setTimeout(r, delay));
170
+ await untrack(() => new Promise((r) => {
171
+ void setTimeout(() => {
172
+ void r(undefined);
173
+ }, delay);
174
+ }));
175
+ }
176
+ export async function asyncMap(array, asyncCallback) {
177
+ return Promise.all(array.map(asyncCallback));
178
+ }
179
+ export async function asyncForEach(array, asyncCallback) {
180
+ await Promise.all(array.map(asyncCallback));
181
+ }
182
+ export async function asyncFlatMap(array, asyncCallback) {
183
+ return (await Promise.all(array.map(asyncCallback))).flat();
184
+ }
185
+ const emptyPromise = new Promise(() => null);
186
+ export async function asyncSome(array, asyncCallback) {
187
+ const promises = array.map(asyncCallback);
188
+ return Promise.race([
189
+ Promise.race(
190
+ // empty promise is required here
191
+ // because otherwise the first false would immediately end the race without checking the rest of the array
192
+ promises.map(async (p) => !!(await p) || emptyPromise)),
193
+ Promise.all(promises).then((r) => r.some(Boolean))
194
+ ]);
195
+ }
196
+ export async function asyncAll(array, asyncCallback) {
197
+ const promises = array.map(asyncCallback);
198
+ return promises.length == 0
199
+ ? true
200
+ : Promise.race([
201
+ Promise.race(
202
+ // empty promise is required here
203
+ // because otherwise the first false would immediately end the race without checking the rest of the array
204
+ promises.map(async (p) => !!(await p) || emptyPromise)),
205
+ Promise.all(promises).then((r) => r.every(Boolean))
206
+ ]);
207
+ }
208
+ export const asyncEvery = asyncAll;
209
+ export async function asyncFilterIndices(array, asyncCallback) {
210
+ const filtered = [];
211
+ await asyncForEach(array, async (item, index) => {
212
+ if (await asyncCallback(item, index)) {
213
+ filtered.push(index);
214
+ }
215
+ });
216
+ return filtered.sort((a, b) => a - b);
217
+ }
218
+ export async function asyncFilter(array, asyncCallback) {
219
+ const filtered = [];
220
+ await asyncForEach(array, async (item, index) => {
221
+ if (await asyncCallback(item, index)) {
222
+ filtered.push(index);
223
+ }
224
+ });
225
+ return filtered.sort((a, b) => a - b).map((i) => array[i]);
171
226
  }
package/dist/index.d.ts CHANGED
@@ -123,7 +123,7 @@ export { TooltipContext } from './Contexts/TooltipContext.svelte.js';
123
123
  export { UntypedInputContext } from './Contexts/UntypedInputContext.svelte.js';
124
124
  export { Delay } from './Delay/Delay.js';
125
125
  export { DATAMatrix } from './Helpers/Datamatrix.js';
126
- export { gridItem, autoPluralise, generateUUID, generateShortUUID, refWrapper, SQLFriendly, SQLFriendlyWithLocalTime, passthrough, getCanvasContext, getErrorMessage, isValidEmailAddress, localTimeFormat, vowels, asyncSleep } from './Helpers/Helpers.svelte.js';
126
+ export { gridItem, autoPluralise, generateUUID, generateShortUUID, refWrapper, SQLFriendly, SQLFriendlyWithLocalTime, passthrough, getCanvasContext, getErrorMessage, isValidEmailAddress, localTimeFormat, vowels, asyncSleep, asyncMap, asyncFilterIndices, asyncFilter, asyncForEach, asyncFlatMap, asyncSome, asyncAll, asyncEvery } from './Helpers/Helpers.svelte.js';
127
127
  export { type INamed } from './Helpers/INamed.js';
128
128
  export { AttributeOperation } from './Types/Internal/AttributeOperation.js';
129
129
  export { type BigRadioOption } from './Types/Internal/BigRadioOption.js';
package/dist/index.js CHANGED
@@ -130,7 +130,7 @@ export { TooltipContext } from './Contexts/TooltipContext.svelte.js';
130
130
  export { UntypedInputContext } from './Contexts/UntypedInputContext.svelte.js';
131
131
  export { Delay } from './Delay/Delay.js';
132
132
  export { DATAMatrix } from './Helpers/Datamatrix.js';
133
- export { gridItem, autoPluralise, generateUUID, generateShortUUID, refWrapper, SQLFriendly, SQLFriendlyWithLocalTime, passthrough, getCanvasContext, getErrorMessage, isValidEmailAddress, localTimeFormat, vowels, asyncSleep } from './Helpers/Helpers.svelte.js';
133
+ export { gridItem, autoPluralise, generateUUID, generateShortUUID, refWrapper, SQLFriendly, SQLFriendlyWithLocalTime, passthrough, getCanvasContext, getErrorMessage, isValidEmailAddress, localTimeFormat, vowels, asyncSleep, asyncMap, asyncFilterIndices, asyncFilter, asyncForEach, asyncFlatMap, asyncSome, asyncAll, asyncEvery } from './Helpers/Helpers.svelte.js';
134
134
  export {} from './Helpers/INamed.js';
135
135
  export { AttributeOperation } from './Types/Internal/AttributeOperation.js';
136
136
  export {} from './Types/Internal/BigRadioOption.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lavalogic/scoria",
3
3
  "description": "Svelte components used for the FloWMS Web Frontend",
4
- "version": "0.8.2",
4
+ "version": "0.9.0",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },
@@ -41,7 +41,7 @@
41
41
  "@sveltejs/adapter-node": "^5.4.0",
42
42
  "@sveltejs/kit": "^2.49.3",
43
43
  "@sveltejs/package": "^2.5.7",
44
- "@sveltejs/vite-plugin-svelte": "^6.2.2",
44
+ "@sveltejs/vite-plugin-svelte": "^6.2.3",
45
45
  "@types/eslint": "^9.6.1",
46
46
  "@types/node": "^24.10.4",
47
47
  "@typescript-eslint/eslint-plugin": "^8.52.0",
@@ -50,7 +50,7 @@
50
50
  "eslint": "^9.39.2",
51
51
  "eslint-config-prettier": "^10.1.8",
52
52
  "eslint-plugin-storybook": "10.1.4",
53
- "eslint-plugin-svelte": "^3.13.1",
53
+ "eslint-plugin-svelte": "^3.14.0",
54
54
  "globals": "^16.5.0",
55
55
  "mdsvex": "^0.12.6",
56
56
  "playwright": "^1.57.0",
@@ -64,7 +64,7 @@
64
64
  "svelte-render-scan": "^1.1.0",
65
65
  "typescript": "^5.9.3",
66
66
  "typescript-eslint": "^8.52.0",
67
- "vite": "^7.3.0",
67
+ "vite": "^7.3.1",
68
68
  "vite-plugin-devtools-json": "^1.0.0",
69
69
  "vitest": "^4.0.16",
70
70
  "vitest-browser-svelte": "^2.0.1"