@lavalogic/scoria 0.40.13 → 0.40.15

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 (143) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +35 -46
  3. package/dist/Components/AccordionGroupButton.svelte +68 -76
  4. package/dist/Components/Action.svelte +103 -135
  5. package/dist/Components/AlertModal.svelte +171 -190
  6. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +614 -697
  7. package/dist/Components/Base/BaseModal.svelte +90 -116
  8. package/dist/Components/Base/ContextWrapper.svelte +67 -72
  9. package/dist/Components/Base/Pagination.svelte +173 -196
  10. package/dist/Components/Base/Snippets.svelte +34 -36
  11. package/dist/Components/BigRadioSet.svelte +36 -38
  12. package/dist/Components/Bubble.svelte +78 -101
  13. package/dist/Components/Button.svelte +175 -208
  14. package/dist/Components/Checkbox.svelte +75 -86
  15. package/dist/Components/CollapsibleCard.svelte +72 -87
  16. package/dist/Components/CollapsibleMenuGroup.svelte +53 -57
  17. package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +39 -64
  18. package/dist/Components/Contexts/ContextMenu.svelte +141 -156
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +53 -60
  21. package/dist/Components/Contexts/Toast.svelte +81 -86
  22. package/dist/Components/Contexts/ToastContainer.svelte +58 -62
  23. package/dist/Components/Contexts/Tooltip.svelte +36 -37
  24. package/dist/Components/DataMatrixIcon.svelte +95 -108
  25. package/dist/Components/DateInput.svelte +118 -146
  26. package/dist/Components/DatePicker.svelte +255 -294
  27. package/dist/Components/DesktopModal.svelte +70 -90
  28. package/dist/Components/Dial.svelte +167 -197
  29. package/dist/Components/DimensionInput.svelte +124 -169
  30. package/dist/Components/DragMenu/DragMenuHolder.svelte +98 -114
  31. package/dist/Components/DragMenu/DraggableCard.svelte +64 -76
  32. package/dist/Components/FileCard.svelte +21 -24
  33. package/dist/Components/FileUpload.svelte +190 -225
  34. package/dist/Components/GridInput.svelte +159 -196
  35. package/dist/Components/GridInputSet.svelte +24 -29
  36. package/dist/Components/HorizontalTabGroup.svelte +73 -89
  37. package/dist/Components/HorizontalTabGroupButton.svelte +40 -44
  38. package/dist/Components/Icon.svelte +99 -128
  39. package/dist/Components/Icons.js +357 -357
  40. package/dist/Components/InfoAlert.svelte +35 -38
  41. package/dist/Components/LoadingAnimation.svelte +691 -755
  42. package/dist/Components/LoadingModal.svelte +36 -44
  43. package/dist/Components/LoadingOverlay.svelte +22 -26
  44. package/dist/Components/Modal.svelte +204 -254
  45. package/dist/Components/MultiSelect.svelte +186 -216
  46. package/dist/Components/Nav/Nav.svelte +94 -110
  47. package/dist/Components/Nav/NavServiceMenuList.svelte +57 -61
  48. package/dist/Components/Nav/NavTab.svelte +58 -62
  49. package/dist/Components/Nav/ServiceMenuItem.svelte +103 -111
  50. package/dist/Components/Nav/ServicesNav.svelte +72 -79
  51. package/dist/Components/NumberInput.svelte +121 -155
  52. package/dist/Components/OptionCards.svelte +62 -73
  53. package/dist/Components/PageHeading.svelte +23 -25
  54. package/dist/Components/PasswordInput.svelte +69 -88
  55. package/dist/Components/PopoverButton.svelte +151 -0
  56. package/dist/Components/PopoverButton.svelte.d.ts +9 -0
  57. package/dist/Components/PopoverButtonProps.d.ts +21 -0
  58. package/dist/Components/PopoverButtonProps.js +1 -0
  59. package/dist/Components/Portal.svelte +29 -35
  60. package/dist/Components/ProgressBubble.svelte +91 -106
  61. package/dist/Components/QuerySelect.svelte +260 -346
  62. package/dist/Components/SidebarPanel.svelte +34 -36
  63. package/dist/Components/SingleSelect.svelte +196 -244
  64. package/dist/Components/StepButton.svelte +65 -70
  65. package/dist/Components/StepForm.svelte +115 -135
  66. package/dist/Components/Switch.svelte +57 -69
  67. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  68. package/dist/Components/Table/Body/QuickSearchCell.svelte +578 -655
  69. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +42 -51
  70. package/dist/Components/Table/Body/Rows/ColumnList.svelte +42 -58
  71. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +403 -442
  72. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +51 -78
  73. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +165 -184
  74. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +110 -123
  75. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +134 -160
  76. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +190 -231
  77. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +82 -104
  78. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +52 -87
  79. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +122 -150
  80. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +132 -159
  81. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +190 -236
  82. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +173 -211
  83. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +204 -244
  84. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +140 -177
  85. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +188 -217
  86. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +173 -193
  87. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +166 -187
  88. package/dist/Components/Table/Body/Rows/TableRow.svelte +245 -288
  89. package/dist/Components/Table/Body/TableBody.svelte +141 -154
  90. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +35 -41
  91. package/dist/Components/Table/Headers/SelectAllHeader.svelte +24 -30
  92. package/dist/Components/Table/Headers/TableHead.svelte +348 -402
  93. package/dist/Components/Table/Misc/ColumnPanel.svelte +37 -39
  94. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +35 -39
  95. package/dist/Components/Table/Misc/FilterInput.svelte +556 -619
  96. package/dist/Components/Table/Misc/FilterPanel.svelte +170 -183
  97. package/dist/Components/Table/Misc/FilterPanelModal.svelte +25 -31
  98. package/dist/Components/Table/Misc/ShareViewModal.svelte +252 -281
  99. package/dist/Components/Table/Misc/SidePanel.svelte +32 -35
  100. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +574 -629
  101. package/dist/Components/Table/Misc/TableFooter.svelte +326 -332
  102. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +134 -140
  103. package/dist/Components/Table/Misc/TableSidebar.svelte +183 -198
  104. package/dist/Components/Table/Misc/TableViewDropdown.svelte +83 -103
  105. package/dist/Components/Table/Misc/ToolboxPanel.svelte +83 -100
  106. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +88 -107
  107. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +163 -190
  108. package/dist/Components/Table/NestedTable.svelte +102 -128
  109. package/dist/Components/Table/Table.svelte +531 -663
  110. package/dist/Components/Test/TestContextConsumer.svelte +11 -13
  111. package/dist/Components/TextArea.svelte +95 -130
  112. package/dist/Components/TextInput.svelte +125 -176
  113. package/dist/Components/ThreeStateRadio.svelte +111 -130
  114. package/dist/Components/Touch/BoolCard.svelte +33 -45
  115. package/dist/Components/Touch/DateModal.svelte +164 -204
  116. package/dist/Components/Touch/EditCard.svelte +48 -59
  117. package/dist/Components/Touch/InfoCard.svelte +28 -31
  118. package/dist/Components/Touch/InfoTextCard.svelte +26 -28
  119. package/dist/Components/Touch/ModalExplanation.svelte +15 -17
  120. package/dist/Components/Touch/NumberKeyboard.svelte +181 -218
  121. package/dist/Components/Touch/NumberModal.svelte +59 -79
  122. package/dist/Components/Touch/PlusMinusCard.svelte +43 -46
  123. package/dist/Components/Touch/SelectModal.svelte +111 -126
  124. package/dist/Components/Touch/SummaryCard.svelte +80 -93
  125. package/dist/Components/Touch/TextAreaModal.svelte +72 -94
  126. package/dist/Components/Touch/TextModal.svelte +77 -105
  127. package/dist/Components/Touch/TouchCard.svelte +31 -33
  128. package/dist/Components/Touch/TouchModal.svelte +214 -270
  129. package/dist/Components/Touch/UtilityButton.svelte +88 -109
  130. package/dist/Components/VerticalTabGroup.svelte +60 -78
  131. package/dist/Components/VerticalTabGroupButton.svelte +54 -58
  132. package/dist/Helpers/Datamatrix.d.ts +19 -19
  133. package/dist/Helpers/Datamatrix.js +291 -291
  134. package/dist/Types/Examples/ExampleModal.svelte +31 -35
  135. package/dist/index.d.ts +2 -0
  136. package/dist/index.js +1 -0
  137. package/dist/scss/_basics.scss +12 -12
  138. package/dist/scss/_colours.scss +134 -134
  139. package/dist/scss/_mixins.scss +3392 -3392
  140. package/dist/scss/_motion.scss +57 -57
  141. package/dist/scss/_sizing.scss +85 -85
  142. package/dist/scss/_transitions.scss +8 -8
  143. package/package.json +1 -1
@@ -1,643 +1,580 @@
1
- <svelte:options runes />
2
-
3
- <script
4
- lang="ts"
5
- generics="T extends object, IdType extends Primitive, FilterValueType"
6
- >
7
- import { dev } from '$app/environment';
8
- import Button from '../../Button.svelte';
9
- import MultiSelect from '../../MultiSelect.svelte';
10
- import NumberInput from '../../NumberInput.svelte';
11
- import QuerySelect from '../../QuerySelect.svelte';
12
- import SingleSelect from '../../SingleSelect.svelte';
13
- import TextInput from '../../TextInput.svelte';
14
- import DateModal from '../../Touch/DateModal.svelte';
15
- import { hasQuery } from '../TableHelpers.svelte.js';
16
- import { devCatch, isDateTuple, isNumericArray } from '../../../Helpers/Helpers.svelte.js';
17
- import type { SelectOption } from '../../../Types/Internal/SelectOption.js';
18
- import { getContext, onMount, untrack } from 'svelte';
19
- import { CheckboxDef } from '../Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js';
20
- import { DateInputDef } from '../Types/Columns/Definitions/Accessors/DateInputDef.svelte.js';
21
- import { NumberInputDef } from '../Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js';
22
- import { SelectDef } from '../Types/Columns/Definitions/Accessors/SelectDef.svelte.js';
23
- import { TableContext } from '../Types/Context/TableContext.svelte.js';
24
- import type { Primitive } from '../Types/Context/TableInitOptions.js';
25
- import { CustomFilterType } from '../Types/Filtering/CustomFilterType.js';
26
- import { NumberDateFilterMode } from '../Types/Filtering/NumberDateFilterMode.js';
27
- import { StringFilterMode } from '../Types/Filtering/StringFilterMode.js';
28
- import type { FilterInputProps } from './FilterInputProps.js';
29
- import { numberDateFilterTypes } from './numberDateFilterTypes.js';
30
- import { stringFilterTypes } from './stringFilterTypes.js';
31
-
32
- const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
33
-
34
- const { def, customFilters }: FilterInputProps<T, FilterValueType> = $props();
35
-
36
- const customFilter = $derived(customFilters?.get(def.id));
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
-
56
- const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
57
-
58
- let value: unknown = $state(null);
59
- let valueMin: number | null = $state(null);
60
- let valueMax: number | null = $state(null);
61
-
62
- let selectedOptions: Array<SelectOption<unknown>> = $state([]);
63
- let lastQueryOptions: Array<SelectOption<unknown>> = $state([]);
64
-
65
- const isNumeric = $derived(
66
- def instanceof NumberInputDef || customFilter?.type === CustomFilterType.Number
67
- );
68
-
69
- const hasFilterFn = $derived(!!def.filterFn && typeof def.filterFn === 'function');
70
-
71
- // A column is filterable when it has any of:
72
- // - a local `filterFn` (AccessorDef subclasses, or Display/Bubble/
73
- // Progress columns whose data repo is local),
74
- // - a declared `filterValueType` (server-side filtering — the UI
75
- // renders an input matching the declared type),
76
- // - a `customFilters` entry from `defsWrapper.remoteFilters`.
77
- // The `filterValueType` case is required for remote-pagination tables:
78
- // their data repo never assigns `filterFn`, so without it every typed
79
- // column would silently disappear from the filter UI.
80
- const hasDeclaredFilterType = $derived(def.filterValueType != null);
81
-
82
- const isSearchable = $derived(
83
- def.allowFiltering &&
84
- (hasFilterFn || hasDeclaredFilterType || customFilters?.has(def.id) == true)
85
- );
86
-
87
- const isSelect = $derived(
88
- def instanceof SelectDef || customFilter?.type === CustomFilterType.Select
89
- );
90
-
91
- const options = $derived(isSelect ? (def.getOptions?.() ?? customFilter?.options ?? []) : []);
92
-
93
- let optionsPromise: Array<SelectOption<unknown>> | Promise<Array<SelectOption<unknown>>> = $state(
94
- [] as Array<SelectOption<unknown>> | Promise<Array<SelectOption<unknown>>>
95
- );
96
-
97
- onMount(() => {
98
- retryOptionsPromise();
99
- });
100
-
101
- function retryOptionsPromise() {
102
- optionsPromise = def instanceof SelectDef ? def.getOptions() : [];
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ generics="T extends object, IdType extends Primitive, FilterValueType"
6
+ >import { dev } from "$app/environment";
7
+ import Button from "../../Button.svelte";
8
+ import MultiSelect from "../../MultiSelect.svelte";
9
+ import NumberInput from "../../NumberInput.svelte";
10
+ import QuerySelect from "../../QuerySelect.svelte";
11
+ import SingleSelect from "../../SingleSelect.svelte";
12
+ import TextInput from "../../TextInput.svelte";
13
+ import DateModal from "../../Touch/DateModal.svelte";
14
+ import { hasQuery } from "../TableHelpers.svelte.js";
15
+ import { devCatch, isDateTuple, isNumericArray } from "../../../Helpers/Helpers.svelte.js";
16
+ import { getContext, onMount, untrack } from "svelte";
17
+ import { CheckboxDef } from "../Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js";
18
+ import { DateInputDef } from "../Types/Columns/Definitions/Accessors/DateInputDef.svelte.js";
19
+ import { NumberInputDef } from "../Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js";
20
+ import { SelectDef } from "../Types/Columns/Definitions/Accessors/SelectDef.svelte.js";
21
+ import { TableContext } from "../Types/Context/TableContext.svelte.js";
22
+ import { CustomFilterType } from "../Types/Filtering/CustomFilterType.js";
23
+ import { NumberDateFilterMode } from "../Types/Filtering/NumberDateFilterMode.js";
24
+ import { StringFilterMode } from "../Types/Filtering/StringFilterMode.js";
25
+ import { numberDateFilterTypes } from "./numberDateFilterTypes.js";
26
+ import { stringFilterTypes } from "./stringFilterTypes.js";
27
+ const tableContext = getContext(TableContext.identifier);
28
+ const { def, customFilters } = $props();
29
+ const customFilter = $derived(customFilters?.get(def.id));
30
+ /** Human-readable label used by every inner input (QuerySelect /
31
+ * MultiSelect / SingleSelect / etc.). Priority:
32
+ * 1. The column's `header` (when it is a plain string, which is the
33
+ * usual case in flowms migrations).
34
+ * 2. The customFilter's `label` (set on the remoteFilters entry).
35
+ * 3. The raw `customFilter.id` (last-ditch fallback).
36
+ * Avoids surfacing dotted-path / camelCase IDs like
37
+ * `orderLines.allowSplitFulfilment` as the visible label. */
38
+ const displayLabel = $derived.by(() => {
39
+ if (typeof def.header === "string" && def.header.length > 0) {
40
+ return def.header;
103
41
  }
104
-
105
- const filteringStateValue = $derived(
106
- tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value
107
- );
108
-
109
- $effect(() => {
110
- if (isRemoteSelect && !tableContext.paginationRepo?.filtering) {
111
- return;
112
- }
113
- void filteringStateValue;
42
+ if (customFilter?.label) {
43
+ return customFilter.label;
44
+ }
45
+ return customFilter?.id ?? def.id;
46
+ });
47
+ const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
48
+ let value = $state(null);
49
+ let valueMin = $state(null);
50
+ let valueMax = $state(null);
51
+ let selectedOptions = $state([]);
52
+ let lastQueryOptions = $state([]);
53
+ const isNumeric = $derived(def instanceof NumberInputDef || customFilter?.type === CustomFilterType.Number);
54
+ const hasFilterFn = $derived(!!def.filterFn && typeof def.filterFn === "function");
55
+ // A column is filterable when it has any of:
56
+ // - a local `filterFn` (AccessorDef subclasses, or Display/Bubble/
57
+ // Progress columns whose data repo is local),
58
+ // - a declared `filterValueType` (server-side filtering — the UI
59
+ // renders an input matching the declared type),
60
+ // - a `customFilters` entry from `defsWrapper.remoteFilters`.
61
+ // The `filterValueType` case is required for remote-pagination tables:
62
+ // their data repo never assigns `filterFn`, so without it every typed
63
+ // column would silently disappear from the filter UI.
64
+ const hasDeclaredFilterType = $derived(def.filterValueType != null);
65
+ const isSearchable = $derived(def.allowFiltering && (hasFilterFn || hasDeclaredFilterType || customFilters?.has(def.id) == true));
66
+ const isSelect = $derived(def instanceof SelectDef || customFilter?.type === CustomFilterType.Select);
67
+ const options = $derived(isSelect ? def.getOptions?.() ?? customFilter?.options ?? [] : []);
68
+ let optionsPromise = $state([]);
69
+ onMount(() => {
70
+ retryOptionsPromise();
71
+ });
72
+ function retryOptionsPromise() {
73
+ optionsPromise = def instanceof SelectDef ? def.getOptions() : [];
74
+ }
75
+ const filteringStateValue = $derived(tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value);
76
+ $effect(() => {
77
+ if (isRemoteSelect && !tableContext.paginationRepo?.filtering) {
78
+ return;
79
+ }
80
+ void filteringStateValue;
81
+ if (def.debug) {
82
+ console.debug(`${def.id}: `, "FILTER INPUT: filtering state value:", $state.snapshot(filteringStateValue));
83
+ }
84
+ if (hasQuery(customFilter)) {
114
85
  if (def.debug) {
115
- console.debug(
116
- `${def.id}: `,
117
- 'FILTER INPUT: filtering state value:',
118
- $state.snapshot(filteringStateValue)
119
- );
86
+ console.debug(`${def.id}: `, "has query");
120
87
  }
121
-
122
- if (hasQuery(customFilter)) {
123
- if (def.debug) {
124
- console.debug(`${def.id}: `, 'has query');
125
- }
126
-
127
- untrack(() => {
128
- try {
129
- if (filteringStateValue == undefined) {
130
- // Filter cleared externally (saved view applied with an
131
- // empty filter, or "Clear All" hit). Drop the typeahead
132
- // echo so the next selection isn't masked by a stale
133
- // cache, and clear the local value so the chip clears.
134
- if (def.debug) {
135
- console.debug(
136
- `${def.id}: filtering state value was undefined`,
137
- $state.snapshot(tableContext.paginationRepo?.filtering)
138
- );
139
- }
140
- value = null;
141
- tableContext.queryValues.delete(def);
142
- return;
143
- }
144
-
145
- if (def.debug) {
146
- console.debug(`${def.id}: query values:`, tableContext.queryValues.snapshot());
147
- }
148
- // Prefer the typeahead echo — it carries the real label
149
- // from the option the user clicked. Fall back to the saved
150
- // primitive (the usual case after applying a saved view)
151
- // so the chip is at least visible; QuerySelect's own
152
- // `$effect.pre` adds a placeholder option keyed on the
153
- // primitive so it renders.
154
- const cached = tableContext.queryValues.get(def);
88
+ untrack(() => {
89
+ try {
90
+ if (filteringStateValue == undefined) {
91
+ // Filter cleared externally (saved view applied with an
92
+ // empty filter, or "Clear All" hit). Drop the typeahead
93
+ // echo so the next selection isn't masked by a stale
94
+ // cache, and clear the local value so the chip clears.
155
95
  if (def.debug) {
156
- console.debug(`${def.id}: cached value:`, $state.snapshot(cached));
96
+ console.debug(`${def.id}: filtering state value was undefined`, $state.snapshot(tableContext.paginationRepo?.filtering));
157
97
  }
158
- if (cached != null) {
159
- value = cached;
160
- } else if (customFilter.valueAsObject === true) {
161
- value = {
162
- value: filteringStateValue,
163
- label: String(filteringStateValue),
164
- };
165
- } else {
166
- value = filteringStateValue;
167
- }
168
- } catch (e) {
169
- console.warn(e);
170
98
  value = null;
99
+ tableContext.queryValues.delete(def);
100
+ return;
171
101
  }
172
- });
173
- } else if (isSelect) {
174
- if (def.debug) {
175
- console.debug(`${def.id}: is select (not query)`);
176
- }
177
- untrack(async () => {
178
- try {
179
- const opts = await options;
180
- if (def.debug) {
181
- console.debug(`${def.id}: possible options:`, $state.snapshot(options));
182
- }
183
- const x = opts.filter((it) => {
184
- if (it.value != null && (typeof it.value == 'string' || typeof it.value == 'number')) {
185
- return (filteringStateValue as Array<unknown> | undefined)?.includes(it.value);
186
- }
187
- return false;
188
- });
189
- if (def.debug) {
190
- console.debug(`${def.id}: selected options:`, x);
191
- }
192
- selectedOptions = x;
193
- } catch (e) {
194
- if (dev || def.debug) {
195
- console.debug(`${def.id}: `, e);
196
- }
102
+ if (def.debug) {
103
+ console.debug(`${def.id}: query values:`, tableContext.queryValues.snapshot());
197
104
  }
198
- }).catch(devCatch);
199
- } else if (isNumeric) {
200
- if (
201
- selectedNumberFilterType?.value === NumberDateFilterMode.Between &&
202
- tableContext.paginationRepo?.filtering
203
- ) {
204
- if (filteringStateValue != undefined) {
205
- const [min, max] = filteringStateValue as [number, number | undefined];
206
- value = min;
207
- valueMin = min;
208
- valueMax = max ?? null;
209
- } else {
210
- value = valueMin = valueMax = null;
105
+ // Prefer the typeahead echo — it carries the real label
106
+ // from the option the user clicked. Fall back to the saved
107
+ // primitive (the usual case after applying a saved view)
108
+ // so the chip is at least visible; QuerySelect's own
109
+ // `$effect.pre` adds a placeholder option keyed on the
110
+ // primitive so it renders.
111
+ const cached = tableContext.queryValues.get(def);
112
+ if (def.debug) {
113
+ console.debug(`${def.id}: cached value:`, $state.snapshot(cached));
211
114
  }
212
- } else if (tableContext.paginationRepo?.filtering) {
213
- if (filteringStateValue != undefined) {
214
- const [min] = filteringStateValue as [number, number | undefined];
215
- value = min;
216
- valueMin = min;
217
- valueMax = min;
115
+ if (cached != null) {
116
+ value = cached;
117
+ } else if (customFilter.valueAsObject === true) {
118
+ value = {
119
+ value: filteringStateValue,
120
+ label: String(filteringStateValue)
121
+ };
218
122
  } else {
219
- value = valueMin = valueMax = null;
123
+ value = filteringStateValue;
220
124
  }
125
+ } catch (e) {
126
+ console.warn(e);
127
+ value = null;
221
128
  }
222
- } else if (tableContext.paginationRepo?.filtering) {
223
- if (
224
- filteringStateValue == null ||
225
- typeof filteringStateValue == 'string' ||
226
- typeof filteringStateValue == 'number'
227
- ) {
228
- value = filteringStateValue;
229
- } else if (typeof filteringStateValue !== 'boolean') {
230
- console.warn('unknown value type:', $state.snapshot(filteringStateValue));
231
- value = filteringStateValue as string; // HACK attempt to keep production running
232
- }
129
+ });
130
+ } else if (isSelect) {
131
+ if (def.debug) {
132
+ console.debug(`${def.id}: is select (not query)`);
233
133
  }
234
- });
235
-
236
- onMount(() => {
237
- const filterOption =
238
- tableContext.paginationRepo?.filtering &&
239
- tableContext.paginationRepo.filtering.find((it) => it.id === def.id);
240
-
241
- if (filterOption) {
242
- if (isNumericArray(filterOption.value)) {
243
- [valueMin, valueMax] = filterOption.value;
244
- if (valueMax == undefined) {
245
- valueMax = valueMin;
246
- } else if (valueMin == undefined) {
247
- valueMin = valueMax;
134
+ untrack(async () => {
135
+ try {
136
+ const opts = await options;
137
+ if (def.debug) {
138
+ console.debug(`${def.id}: possible options:`, $state.snapshot(options));
248
139
  }
249
- } else if (!hasQuery(customFilter)) {
250
- const gotValue = filterOption.value;
251
- if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
252
- value = gotValue;
253
- } else if (typeof filteringStateValue !== 'boolean') {
254
- console.warn('unknown value type:', $state.snapshot(gotValue));
255
- value = gotValue as string; // HACK attempt to keep production running
140
+ const x = opts.filter((it) => {
141
+ if (it.value != null && (typeof it.value == "string" || typeof it.value == "number")) {
142
+ return filteringStateValue?.includes(it.value);
143
+ }
144
+ return false;
145
+ });
146
+ if (def.debug) {
147
+ console.debug(`${def.id}: selected options:`, x);
148
+ }
149
+ selectedOptions = x;
150
+ } catch (e) {
151
+ if (dev || def.debug) {
152
+ console.debug(`${def.id}: `, e);
256
153
  }
257
154
  }
155
+ }).catch(devCatch);
156
+ } else if (isNumeric) {
157
+ if (selectedNumberFilterType?.value === NumberDateFilterMode.Between && tableContext.paginationRepo?.filtering) {
158
+ if (filteringStateValue != undefined) {
159
+ const [min, max] = filteringStateValue;
160
+ value = min;
161
+ valueMin = min;
162
+ valueMax = max ?? null;
163
+ } else {
164
+ value = valueMin = valueMax = null;
165
+ }
166
+ } else if (tableContext.paginationRepo?.filtering) {
167
+ if (filteringStateValue != undefined) {
168
+ const [min] = filteringStateValue;
169
+ value = min;
170
+ valueMin = min;
171
+ valueMax = min;
172
+ } else {
173
+ value = valueMin = valueMax = null;
174
+ }
258
175
  }
259
- });
260
-
261
- const filterMode = $derived(tableContext.paginationRepo?.filterModes.get(def.id));
262
- const selectedNumberFilterType: SelectOption<NumberDateFilterMode> | null = $derived.by(() => {
263
- const value = (filterMode as NumberDateFilterMode | undefined) ?? NumberDateFilterMode.In;
264
- return numberDateFilterTypes.find((it) => it.value === value) ?? null;
265
- });
266
- const selectedStringFilterType: SelectOption<StringFilterMode> | null = $derived.by(() => {
267
- const value = (filterMode as StringFilterMode | undefined) ?? StringFilterMode.Contains;
268
- return stringFilterTypes.find((it) => it.value === value) ?? null;
269
- });
270
-
271
- const inputId: string = $derived(`${tableContext.tableName}-${def.id}-filter-type`);
272
-
273
- let showFromDateModal = $state(false);
274
- let showToDateModal = $state(false);
275
-
276
- /**
277
- * Debounce window for typed filter inputs (Text / Number / Between).
278
- * Discrete selections (selects, date pickers, filter-mode dropdowns)
279
- * skip this and fire `filterBy` immediately. Combined with the 400 ms
280
- * debounce in `createRemoteRowSource`, server-side filtering lands
281
- * roughly a second after the last keystroke.
282
- */
283
- const FILTER_INPUT_DEBOUNCE_MS = 500;
284
-
285
- let typingDebounceTimer: ReturnType<typeof setTimeout> | undefined;
286
-
287
- /** Fire the table's `filterBy` after the user stops typing for
288
- * `FILTER_INPUT_DEBOUNCE_MS`. Repeated calls coalesce; the most
289
- * recent value wins. */
290
- function debouncedFilterBy(id: string, newValue: unknown): void {
291
- if (typingDebounceTimer !== undefined) {
292
- clearTimeout(typingDebounceTimer);
176
+ } else if (tableContext.paginationRepo?.filtering) {
177
+ if (filteringStateValue == null || typeof filteringStateValue == "string" || typeof filteringStateValue == "number") {
178
+ value = filteringStateValue;
179
+ } else if (typeof filteringStateValue !== "boolean") {
180
+ console.warn("unknown value type:", $state.snapshot(filteringStateValue));
181
+ value = filteringStateValue;
293
182
  }
294
- typingDebounceTimer = setTimeout(() => {
295
- typingDebounceTimer = undefined;
296
- tableContext.paginationRepo?.filterBy(id, newValue);
297
- }, FILTER_INPUT_DEBOUNCE_MS);
298
183
  }
299
-
300
- // Clear any pending debounce when the row unmounts so we do not
301
- // fire `filterBy` against a torn-down `paginationRepo`.
302
- $effect(() => {
303
- return () => {
304
- if (typingDebounceTimer !== undefined) {
305
- clearTimeout(typingDebounceTimer);
306
- typingDebounceTimer = undefined;
184
+ });
185
+ onMount(() => {
186
+ const filterOption = tableContext.paginationRepo?.filtering && tableContext.paginationRepo.filtering.find((it) => it.id === def.id);
187
+ if (filterOption) {
188
+ if (isNumericArray(filterOption.value)) {
189
+ [valueMin, valueMax] = filterOption.value;
190
+ if (valueMax == undefined) {
191
+ valueMax = valueMin;
192
+ } else if (valueMin == undefined) {
193
+ valueMin = valueMax;
307
194
  }
308
- };
309
- });
310
-
311
- function generateOnChangeObject(_label: string) {
312
- return (newValue: SelectOption<unknown> | null) => {
313
- tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
314
- tableContext.queryValues.set(def, newValue ?? null);
315
- value = newValue ?? null;
316
- };
195
+ } else if (!hasQuery(customFilter)) {
196
+ const gotValue = filterOption.value;
197
+ if (gotValue == null || typeof gotValue == "string" || typeof gotValue == "number") {
198
+ value = gotValue;
199
+ } else if (typeof filteringStateValue !== "boolean") {
200
+ console.warn("unknown value type:", $state.snapshot(gotValue));
201
+ value = gotValue;
202
+ }
203
+ }
317
204
  }
318
-
319
- function generateOnChangePrimitive(_label: string) {
320
- // AsPrimitive QuerySelect hands us the primitive value directly.
321
- return (newValue: unknown) => {
322
- tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
323
- tableContext.queryValues.set(def, newValue ?? null);
324
- value = newValue ?? null;
325
- };
205
+ });
206
+ const filterMode = $derived(tableContext.paginationRepo?.filterModes.get(def.id));
207
+ const selectedNumberFilterType = $derived.by(() => {
208
+ const value = filterMode ?? NumberDateFilterMode.In;
209
+ return numberDateFilterTypes.find((it) => it.value === value) ?? null;
210
+ });
211
+ const selectedStringFilterType = $derived.by(() => {
212
+ const value = filterMode ?? StringFilterMode.Contains;
213
+ return stringFilterTypes.find((it) => it.value === value) ?? null;
214
+ });
215
+ const inputId = $derived(`${tableContext.tableName}-${def.id}-filter-type`);
216
+ let showFromDateModal = $state(false);
217
+ let showToDateModal = $state(false);
218
+ /**
219
+ * Debounce window for typed filter inputs (Text / Number / Between).
220
+ * Discrete selections (selects, date pickers, filter-mode dropdowns)
221
+ * skip this and fire `filterBy` immediately. Combined with the 400 ms
222
+ * debounce in `createRemoteRowSource`, server-side filtering lands
223
+ * roughly a second after the last keystroke.
224
+ */
225
+ const FILTER_INPUT_DEBOUNCE_MS = 500;
226
+ let typingDebounceTimer;
227
+ /** Fire the table's `filterBy` after the user stops typing for
228
+ * `FILTER_INPUT_DEBOUNCE_MS`. Repeated calls coalesce; the most
229
+ * recent value wins. */
230
+ function debouncedFilterBy(id, newValue) {
231
+ if (typingDebounceTimer !== undefined) {
232
+ clearTimeout(typingDebounceTimer);
326
233
  }
327
- </script>
328
-
329
- {#if isSearchable}
330
- <!-- TODO remove this check once datePicker is supported -->
331
- {#if def instanceof DateInputDef || def instanceof CheckboxDef}
332
- <label for={inputId}>{displayLabel}</label>
333
- {/if}
334
- <div class="row">
335
- {#if def instanceof DateInputDef}
336
- {@const dateValues =
337
- tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value ?? []}
338
- {#if isDateTuple(dateValues)}
339
- {@const [fromDate, toDate] = dateValues}
340
- <div class="spaced-around">
341
- <Button
342
- onclick={() => {
343
- showFromDateModal = true;
344
- }}>From {fromDate?.toLocaleDateString() ?? 'Any Date'}</Button
345
- >
346
-
347
- <Button
348
- onclick={() => {
349
- showToDateModal = true;
350
- }}>To {toDate?.toLocaleDateString() ?? 'Any Date'}</Button
351
- >
352
- </div>
353
-
354
- {#if showFromDateModal}
355
- <DateModal
356
- header="Edit Date From"
357
- value={fromDate}
358
- cancelEditing={() => {
359
- showFromDateModal = false;
360
- }}
361
- confirm={(e) => {
362
- tableContext.paginationRepo?.filterBy(def.id, [e, toDate]);
363
- showFromDateModal = false;
364
- }}
365
- />
366
- {/if}
367
-
368
- {#if showToDateModal}
369
- <DateModal
370
- inclusive
371
- header="Edit Date To"
372
- value={toDate}
373
- cancelEditing={() => {
374
- showToDateModal = false;
375
- }}
376
- confirm={(e) => {
377
- tableContext.paginationRepo?.filterBy(def.id, [fromDate, e]);
378
- showToDateModal = false;
379
- }}
380
- />
381
- {/if}
382
- {/if}
383
- {:else if def instanceof SelectDef && !isRemoteSelect}
384
- <div class="maxwidth">
385
- {#await optionsPromise then options}
386
- {@const selectedPrimitives = selectedOptions
387
- .map((it) => it.value)
388
- .filter((v): v is string | number => typeof v === 'string' || typeof v === 'number')}
389
- <MultiSelect
390
- label={customFilter?.id ??
391
- customFilter?.label ??
392
- customFilter?.objectName ??
393
- customFilter?.queryKey ??
394
- def.id}
395
- labelProp="label"
396
- valueProp="value"
397
- name={customFilter?.objectName ?? 'Item'}
398
- {options}
399
- valueAsObject={false}
400
- onchange={(e: Array<unknown>) => {
401
- // Phase 7.6: `valueAsObject={false}` returns
402
- // primitives directly.
403
- tableContext.paginationRepo?.filterBy(def.id, e);
404
- }}
405
- selectedValues={selectedPrimitives}
406
- collapseSelection
407
- />
408
- {:catch e}
409
- <span>Error retrieving options: {e.message}</span>
410
- <Button
411
- onclick={() => {
412
- retryOptionsPromise();
413
- }}>Retry</Button
414
- >
415
- {/await}
416
- </div>
417
- {:else if customFilter && isRemoteSelect}
418
- <div class="contents">
419
- {#await options then resolvedOptions}
420
- {@const options = resolvedOptions as Array<SelectOption<unknown>>}
421
- {#if hasQuery(customFilter)}
422
- {#if customFilter.valueAsObject !== true}
423
- {@const label = displayLabel}
424
-
425
- <!-- TODO: multiselect -->
426
-
427
- <QuerySelect
428
- valueAsObject={false}
429
- {label}
430
- clearable
431
- name={customFilter.objectName ?? 'Item'}
432
- valueProp={'value' as never}
433
- labelProp={'label' as never}
434
- value={value as never}
435
- query={customFilter.query}
436
- bind:options={lastQueryOptions as never}
437
- onchange={generateOnChangePrimitive(label)}
438
- />
439
- {:else}
440
- {@const label = displayLabel}
441
-
442
- <!-- TODO: multiselect -->
443
-
444
- <QuerySelect
445
- valueAsObject={true}
446
- {label}
447
- clearable
448
- name={customFilter.objectName ?? 'Item'}
449
- valueProp="value"
450
- labelProp="label"
451
- value={value as null}
452
- query={customFilter.query}
453
- bind:options={lastQueryOptions}
454
- onchange={generateOnChangeObject(label)}
455
- />
456
- {/if}
457
- {:else}
458
- {@const selectedPrimitives = selectedOptions
459
- .map((it) => it.value)
460
- .filter(
461
- (v): v is string | number => typeof v === 'string' || typeof v === 'number'
462
- )}
463
- <!--
464
- With `valueAsObject={false}`, MultiSelect compares
465
- its `selectedValues` against options by primitive
466
- value, so we map `selectedOptions` to their
467
- primitive `.value` rather than passing the
468
- SelectOption objects directly.
469
- -->
470
- <MultiSelect
471
- label={displayLabel}
472
- labelProp="label"
473
- valueProp="value"
474
- name={customFilter.objectName ?? 'Item'}
475
- {options}
476
- valueAsObject={false}
477
- onchange={(e: Array<unknown>) => {
478
- // Phase 7.6: `valueAsObject={false}` returns
479
- // primitives directly.
480
- tableContext.paginationRepo?.filterBy(def.id, e);
481
- }}
482
- selectedValues={selectedPrimitives}
483
- collapseSelection
484
- />
485
- {/if}
486
- {/await}
487
- </div>
488
- {:else if def instanceof CheckboxDef}
489
- <div class="inner-row">
490
- <label>
491
- <input
492
- value={false}
493
- type="radio"
494
- bind:group={value}
495
- onchange={() => {
496
- tableContext.paginationRepo?.filterBy(def.id, value);
497
- }}
498
- />
499
- No
500
- </label>
501
- <label>
502
- <input
503
- value={undefined}
504
- type="radio"
505
- bind:group={value}
506
- onchange={() => {
507
- tableContext.paginationRepo?.filterBy(def.id, value);
508
- }}
509
- />
510
- Any
511
- </label>
512
- <label>
513
- <input
514
- value={true}
515
- type="radio"
516
- bind:group={value}
517
- onchange={() => {
518
- tableContext.paginationRepo?.filterBy(def.id, value);
519
- }}
520
- />
521
- Yes
522
- </label>
523
- </div>
524
- {:else if isNumeric}
525
- {@const label = typeof def.header === 'string' ? def.header : undefined}
526
- <div class="left">
527
- <SingleSelect
528
- {label}
529
- name="Filter Type"
530
- options={numberDateFilterTypes}
531
- labelProp="label"
532
- valueProp="value"
533
- valueAsObject
534
- value={selectedNumberFilterType}
535
- onchange={(newValue: SelectOption<NumberDateFilterMode> | null) => {
536
- const oldFilterType = selectedNumberFilterType;
537
-
538
- const newFilterType = newValue?.value ?? NumberDateFilterMode.In;
539
- tableContext.paginationRepo?.setFilterMode(def.id, newFilterType);
540
-
541
- if (selectedNumberFilterType?.value !== oldFilterType?.value) {
542
- if (oldFilterType?.value === NumberDateFilterMode.Between) {
543
- valueMax = null;
544
- tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMin]);
545
- }
546
-
547
- if (newFilterType === NumberDateFilterMode.In) {
548
- tableContext.paginationRepo?.filterBy(def.id, [value]);
549
- }
550
-
551
- // TODO what about the other modes?
552
- }
553
- }}
554
- onkeydown={(e: KeyboardEvent) => {
555
- e.stopPropagation();
556
- }}
557
- />
558
- </div>
559
- <div class="right">
560
- {#if selectedNumberFilterType?.value === NumberDateFilterMode.Between}
561
- <NumberInput
562
- id={inputId}
563
- bind:value={valueMin}
564
- oninput={() => {
565
- debouncedFilterBy(def.id, [valueMin, valueMax]);
566
- }}
567
- onkeydown={(e: KeyboardEvent) => {
568
- e.stopPropagation();
569
- }}
570
- />
571
- <span>and</span>
572
- <NumberInput
573
- id={inputId}
574
- bind:value={valueMax}
575
- oninput={() => {
576
- debouncedFilterBy(def.id, [valueMin, valueMax]);
577
- }}
578
- onkeydown={(e: KeyboardEvent) => {
579
- e.stopPropagation();
580
- }}
581
- />
582
- {:else if value == null || typeof value == 'string'}
583
- <TextInput
584
- id={inputId}
585
- bind:value
586
- oninput={() => {
587
- debouncedFilterBy(def.id, [value]);
588
- }}
589
- onkeydown={(e: KeyboardEvent) => {
590
- e.stopPropagation();
591
- }}
592
- />
593
- {:else if dev}
594
- error: {value?.toString()}
595
- <!-- eslint-disable-next-line svelte/no-at-debug-tags -->
596
- {@debug value}
597
- {/if}
598
- </div>
599
- {:else}
600
- {@const label = typeof def.header === 'string' ? def.header : undefined}
601
- <div class="left">
602
- <SingleSelect
603
- name="Filter Type"
604
- valueAsObject
605
- {label}
606
- options={stringFilterTypes}
607
- labelProp="label"
608
- valueProp="value"
609
- value={selectedStringFilterType}
610
- onchange={(newValue: SelectOption<StringFilterMode> | null) => {
611
- const filterType = newValue?.value ?? StringFilterMode.Contains;
612
- tableContext.paginationRepo?.setFilterMode(def.id, filterType);
613
- }}
614
- onkeydown={(e: KeyboardEvent) => {
615
- e.stopPropagation();
616
- }}
617
- />
618
- </div>
619
- <div class="right">
620
- {#if value == null || typeof value == 'string'}
621
- <TextInput
622
- id={inputId}
623
- bind:value
624
- oninput={() => {
625
- debouncedFilterBy(def.id, value);
626
- }}
627
- onkeydown={(e: KeyboardEvent) => {
628
- e.stopPropagation();
629
- }}
630
- />
631
- {:else if dev}
632
- error: {value?.toString()}
633
- <!-- eslint-disable-next-line svelte/no-at-debug-tags -->
634
- {@debug value}
635
- {/if}
636
- </div>
637
- {/if}
638
- </div>
639
- {/if}
640
-
234
+ typingDebounceTimer = setTimeout(() => {
235
+ typingDebounceTimer = undefined;
236
+ tableContext.paginationRepo?.filterBy(id, newValue);
237
+ }, FILTER_INPUT_DEBOUNCE_MS);
238
+ }
239
+ // Clear any pending debounce when the row unmounts so we do not
240
+ // fire `filterBy` against a torn-down `paginationRepo`.
241
+ $effect(() => {
242
+ return () => {
243
+ if (typingDebounceTimer !== undefined) {
244
+ clearTimeout(typingDebounceTimer);
245
+ typingDebounceTimer = undefined;
246
+ }
247
+ };
248
+ });
249
+ function generateOnChangeObject(_label) {
250
+ return (newValue) => {
251
+ tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
252
+ tableContext.queryValues.set(def, newValue ?? null);
253
+ value = newValue ?? null;
254
+ };
255
+ }
256
+ function generateOnChangePrimitive(_label) {
257
+ // AsPrimitive QuerySelect hands us the primitive value directly.
258
+ return (newValue) => {
259
+ tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
260
+ tableContext.queryValues.set(def, newValue ?? null);
261
+ value = newValue ?? null;
262
+ };
263
+ }
264
+ </script>
265
+
266
+ {#if isSearchable}
267
+ <!-- TODO remove this check once datePicker is supported -->
268
+ {#if def instanceof DateInputDef || def instanceof CheckboxDef}
269
+ <label for={inputId}>{displayLabel}</label>
270
+ {/if}
271
+ <div class="row">
272
+ {#if def instanceof DateInputDef}
273
+ {@const dateValues =
274
+ tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value ?? []}
275
+ {#if isDateTuple(dateValues)}
276
+ {@const [fromDate, toDate] = dateValues}
277
+ <div class="spaced-around">
278
+ <Button
279
+ onclick={() => {
280
+ showFromDateModal = true;
281
+ }}>From {fromDate?.toLocaleDateString() ?? 'Any Date'}</Button
282
+ >
283
+
284
+ <Button
285
+ onclick={() => {
286
+ showToDateModal = true;
287
+ }}>To {toDate?.toLocaleDateString() ?? 'Any Date'}</Button
288
+ >
289
+ </div>
290
+
291
+ {#if showFromDateModal}
292
+ <DateModal
293
+ header="Edit Date From"
294
+ value={fromDate}
295
+ cancelEditing={() => {
296
+ showFromDateModal = false;
297
+ }}
298
+ confirm={(e) => {
299
+ tableContext.paginationRepo?.filterBy(def.id, [e, toDate]);
300
+ showFromDateModal = false;
301
+ }}
302
+ />
303
+ {/if}
304
+
305
+ {#if showToDateModal}
306
+ <DateModal
307
+ inclusive
308
+ header="Edit Date To"
309
+ value={toDate}
310
+ cancelEditing={() => {
311
+ showToDateModal = false;
312
+ }}
313
+ confirm={(e) => {
314
+ tableContext.paginationRepo?.filterBy(def.id, [fromDate, e]);
315
+ showToDateModal = false;
316
+ }}
317
+ />
318
+ {/if}
319
+ {/if}
320
+ {:else if def instanceof SelectDef && !isRemoteSelect}
321
+ <div class="maxwidth">
322
+ {#await optionsPromise then options}
323
+ {@const selectedPrimitives = selectedOptions
324
+ .map((it) => it.value)
325
+ .filter((v): v is string | number => typeof v === 'string' || typeof v === 'number')}
326
+ <MultiSelect
327
+ label={customFilter?.id ??
328
+ customFilter?.label ??
329
+ customFilter?.objectName ??
330
+ customFilter?.queryKey ??
331
+ def.id}
332
+ labelProp="label"
333
+ valueProp="value"
334
+ name={customFilter?.objectName ?? 'Item'}
335
+ {options}
336
+ valueAsObject={false}
337
+ onchange={(e: Array<unknown>) => {
338
+ // Phase 7.6: `valueAsObject={false}` returns
339
+ // primitives directly.
340
+ tableContext.paginationRepo?.filterBy(def.id, e);
341
+ }}
342
+ selectedValues={selectedPrimitives}
343
+ collapseSelection
344
+ />
345
+ {:catch e}
346
+ <span>Error retrieving options: {e.message}</span>
347
+ <Button
348
+ onclick={() => {
349
+ retryOptionsPromise();
350
+ }}>Retry</Button
351
+ >
352
+ {/await}
353
+ </div>
354
+ {:else if customFilter && isRemoteSelect}
355
+ <div class="contents">
356
+ {#await options then resolvedOptions}
357
+ {@const options = resolvedOptions as Array<SelectOption<unknown>>}
358
+ {#if hasQuery(customFilter)}
359
+ {#if customFilter.valueAsObject !== true}
360
+ {@const label = displayLabel}
361
+
362
+ <!-- TODO: multiselect -->
363
+
364
+ <QuerySelect
365
+ valueAsObject={false}
366
+ {label}
367
+ clearable
368
+ name={customFilter.objectName ?? 'Item'}
369
+ valueProp={'value' as never}
370
+ labelProp={'label' as never}
371
+ value={value as never}
372
+ query={customFilter.query}
373
+ bind:options={lastQueryOptions as never}
374
+ onchange={generateOnChangePrimitive(label)}
375
+ />
376
+ {:else}
377
+ {@const label = displayLabel}
378
+
379
+ <!-- TODO: multiselect -->
380
+
381
+ <QuerySelect
382
+ valueAsObject={true}
383
+ {label}
384
+ clearable
385
+ name={customFilter.objectName ?? 'Item'}
386
+ valueProp="value"
387
+ labelProp="label"
388
+ value={value as null}
389
+ query={customFilter.query}
390
+ bind:options={lastQueryOptions}
391
+ onchange={generateOnChangeObject(label)}
392
+ />
393
+ {/if}
394
+ {:else}
395
+ {@const selectedPrimitives = selectedOptions
396
+ .map((it) => it.value)
397
+ .filter(
398
+ (v): v is string | number => typeof v === 'string' || typeof v === 'number'
399
+ )}
400
+ <!--
401
+ With `valueAsObject={false}`, MultiSelect compares
402
+ its `selectedValues` against options by primitive
403
+ value, so we map `selectedOptions` to their
404
+ primitive `.value` rather than passing the
405
+ SelectOption objects directly.
406
+ -->
407
+ <MultiSelect
408
+ label={displayLabel}
409
+ labelProp="label"
410
+ valueProp="value"
411
+ name={customFilter.objectName ?? 'Item'}
412
+ {options}
413
+ valueAsObject={false}
414
+ onchange={(e: Array<unknown>) => {
415
+ // Phase 7.6: `valueAsObject={false}` returns
416
+ // primitives directly.
417
+ tableContext.paginationRepo?.filterBy(def.id, e);
418
+ }}
419
+ selectedValues={selectedPrimitives}
420
+ collapseSelection
421
+ />
422
+ {/if}
423
+ {/await}
424
+ </div>
425
+ {:else if def instanceof CheckboxDef}
426
+ <div class="inner-row">
427
+ <label>
428
+ <input
429
+ value={false}
430
+ type="radio"
431
+ bind:group={value}
432
+ onchange={() => {
433
+ tableContext.paginationRepo?.filterBy(def.id, value);
434
+ }}
435
+ />
436
+ No
437
+ </label>
438
+ <label>
439
+ <input
440
+ value={undefined}
441
+ type="radio"
442
+ bind:group={value}
443
+ onchange={() => {
444
+ tableContext.paginationRepo?.filterBy(def.id, value);
445
+ }}
446
+ />
447
+ Any
448
+ </label>
449
+ <label>
450
+ <input
451
+ value={true}
452
+ type="radio"
453
+ bind:group={value}
454
+ onchange={() => {
455
+ tableContext.paginationRepo?.filterBy(def.id, value);
456
+ }}
457
+ />
458
+ Yes
459
+ </label>
460
+ </div>
461
+ {:else if isNumeric}
462
+ {@const label = typeof def.header === 'string' ? def.header : undefined}
463
+ <div class="left">
464
+ <SingleSelect
465
+ {label}
466
+ name="Filter Type"
467
+ options={numberDateFilterTypes}
468
+ labelProp="label"
469
+ valueProp="value"
470
+ valueAsObject
471
+ value={selectedNumberFilterType}
472
+ onchange={(newValue: SelectOption<NumberDateFilterMode> | null) => {
473
+ const oldFilterType = selectedNumberFilterType;
474
+
475
+ const newFilterType = newValue?.value ?? NumberDateFilterMode.In;
476
+ tableContext.paginationRepo?.setFilterMode(def.id, newFilterType);
477
+
478
+ if (selectedNumberFilterType?.value !== oldFilterType?.value) {
479
+ if (oldFilterType?.value === NumberDateFilterMode.Between) {
480
+ valueMax = null;
481
+ tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMin]);
482
+ }
483
+
484
+ if (newFilterType === NumberDateFilterMode.In) {
485
+ tableContext.paginationRepo?.filterBy(def.id, [value]);
486
+ }
487
+
488
+ // TODO what about the other modes?
489
+ }
490
+ }}
491
+ onkeydown={(e: KeyboardEvent) => {
492
+ e.stopPropagation();
493
+ }}
494
+ />
495
+ </div>
496
+ <div class="right">
497
+ {#if selectedNumberFilterType?.value === NumberDateFilterMode.Between}
498
+ <NumberInput
499
+ id={inputId}
500
+ bind:value={valueMin}
501
+ oninput={() => {
502
+ debouncedFilterBy(def.id, [valueMin, valueMax]);
503
+ }}
504
+ onkeydown={(e: KeyboardEvent) => {
505
+ e.stopPropagation();
506
+ }}
507
+ />
508
+ <span>and</span>
509
+ <NumberInput
510
+ id={inputId}
511
+ bind:value={valueMax}
512
+ oninput={() => {
513
+ debouncedFilterBy(def.id, [valueMin, valueMax]);
514
+ }}
515
+ onkeydown={(e: KeyboardEvent) => {
516
+ e.stopPropagation();
517
+ }}
518
+ />
519
+ {:else if value == null || typeof value == 'string'}
520
+ <TextInput
521
+ id={inputId}
522
+ bind:value
523
+ oninput={() => {
524
+ debouncedFilterBy(def.id, [value]);
525
+ }}
526
+ onkeydown={(e: KeyboardEvent) => {
527
+ e.stopPropagation();
528
+ }}
529
+ />
530
+ {:else if dev}
531
+ error: {value?.toString()}
532
+ <!-- eslint-disable-next-line svelte/no-at-debug-tags -->
533
+ {@debug value}
534
+ {/if}
535
+ </div>
536
+ {:else}
537
+ {@const label = typeof def.header === 'string' ? def.header : undefined}
538
+ <div class="left">
539
+ <SingleSelect
540
+ name="Filter Type"
541
+ valueAsObject
542
+ {label}
543
+ options={stringFilterTypes}
544
+ labelProp="label"
545
+ valueProp="value"
546
+ value={selectedStringFilterType}
547
+ onchange={(newValue: SelectOption<StringFilterMode> | null) => {
548
+ const filterType = newValue?.value ?? StringFilterMode.Contains;
549
+ tableContext.paginationRepo?.setFilterMode(def.id, filterType);
550
+ }}
551
+ onkeydown={(e: KeyboardEvent) => {
552
+ e.stopPropagation();
553
+ }}
554
+ />
555
+ </div>
556
+ <div class="right">
557
+ {#if value == null || typeof value == 'string'}
558
+ <TextInput
559
+ id={inputId}
560
+ bind:value
561
+ oninput={() => {
562
+ debouncedFilterBy(def.id, value);
563
+ }}
564
+ onkeydown={(e: KeyboardEvent) => {
565
+ e.stopPropagation();
566
+ }}
567
+ />
568
+ {:else if dev}
569
+ error: {value?.toString()}
570
+ <!-- eslint-disable-next-line svelte/no-at-debug-tags -->
571
+ {@debug value}
572
+ {/if}
573
+ </div>
574
+ {/if}
575
+ </div>
576
+ {/if}
577
+
641
578
  <style>.contents {
642
579
  display: contents;
643
580
  --input-width: 100%;
@@ -708,4 +645,4 @@
708
645
  .inner-row label input {
709
646
  cursor: pointer;
710
647
  width: 2rem;
711
- }</style>
648
+ }</style>