@lavalogic/scoria 0.40.0 → 0.40.2
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/README.md +58 -58
- package/dist/Components/AccordionGroup.svelte +46 -46
- package/dist/Components/AccordionGroupButton.svelte +76 -76
- package/dist/Components/Action.svelte +136 -136
- package/dist/Components/AlertModal.svelte +195 -195
- package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +729 -729
- package/dist/Components/Base/BaseModal.svelte +125 -125
- package/dist/Components/Base/ContextWrapper.svelte +72 -72
- package/dist/Components/Base/Pagination.svelte +198 -198
- package/dist/Components/Base/Snippets.svelte +36 -36
- package/dist/Components/BigRadioSet.svelte +38 -38
- package/dist/Components/Bubble.svelte +101 -101
- package/dist/Components/Button.svelte +209 -209
- package/dist/Components/Checkbox.svelte +86 -86
- package/dist/Components/CollapsibleCard.svelte +78 -78
- package/dist/Components/Contexts/ContextMenu.svelte +164 -164
- package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
- package/dist/Components/Contexts/ContextMenuOption.svelte +60 -60
- package/dist/Components/Contexts/Toast.svelte +86 -86
- package/dist/Components/Contexts/ToastContainer.svelte +65 -65
- package/dist/Components/Contexts/Tooltip.svelte +37 -37
- package/dist/Components/DataMatrixIcon.svelte +112 -112
- package/dist/Components/DateInput.svelte +148 -148
- package/dist/Components/DatePicker.svelte +300 -300
- package/dist/Components/DesktopModal.svelte +90 -90
- package/dist/Components/Dial.svelte +204 -204
- package/dist/Components/DimensionInput.svelte +170 -170
- package/dist/Components/DragMenu/DragMenuHolder.svelte +119 -119
- package/dist/Components/DragMenu/DraggableCard.svelte +77 -77
- package/dist/Components/FileCard.svelte +24 -24
- package/dist/Components/FileUpload.svelte +236 -236
- package/dist/Components/GridInput.svelte +196 -196
- package/dist/Components/GridInputSet.svelte +29 -29
- package/dist/Components/HorizontalTabGroup.svelte +82 -82
- package/dist/Components/HorizontalTabGroupButton.svelte +44 -44
- package/dist/Components/Icon.svelte +128 -128
- package/dist/Components/Icons.js +357 -357
- package/dist/Components/InfoAlert.svelte +35 -35
- package/dist/Components/LoadingAnimation.svelte +29 -29
- package/dist/Components/LoadingModal.svelte +45 -45
- package/dist/Components/LoadingOverlay.svelte +25 -25
- package/dist/Components/Modal.svelte +258 -258
- package/dist/Components/MultiSelect.svelte +220 -220
- package/dist/Components/Nav/Nav.svelte +110 -110
- package/dist/Components/Nav/NavServiceMenuList.svelte +43 -43
- package/dist/Components/Nav/NavTab.svelte +62 -62
- package/dist/Components/Nav/ServiceMenuItem.svelte +112 -112
- package/dist/Components/Nav/ServicesNav.svelte +80 -80
- package/dist/Components/NumberInput.svelte +157 -157
- package/dist/Components/PageHeading.svelte +25 -25
- package/dist/Components/PasswordInput.svelte +88 -88
- package/dist/Components/Portal.svelte +36 -36
- package/dist/Components/ProgressBubble.svelte +107 -107
- package/dist/Components/QuerySelect.svelte +366 -366
- package/dist/Components/SingleSelect.svelte +250 -250
- package/dist/Components/StepButton.svelte +70 -70
- package/dist/Components/StepForm.svelte +140 -140
- package/dist/Components/Switch.svelte +69 -69
- package/dist/Components/Table/ARCHITECTURE.md +298 -298
- package/dist/Components/Table/Body/QuickSearchCell.svelte +679 -679
- package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +51 -51
- package/dist/Components/Table/Body/Rows/ColumnList.svelte +59 -59
- package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +454 -454
- package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +79 -79
- package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +185 -185
- package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +124 -124
- package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +131 -120
- package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +234 -234
- package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +105 -105
- package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +87 -87
- package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +126 -126
- package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +159 -159
- package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +246 -246
- package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +216 -216
- package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +252 -252
- package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +179 -179
- package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +221 -221
- package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +197 -197
- package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +190 -190
- package/dist/Components/Table/Body/Rows/TableRow.svelte +290 -290
- package/dist/Components/Table/Body/TableBody.svelte +155 -155
- package/dist/Components/Table/ColumnFactory.svelte.js +1 -0
- package/dist/Components/Table/Headers/HighlightAllHeader.svelte +41 -41
- package/dist/Components/Table/Headers/SelectAllHeader.svelte +30 -30
- package/dist/Components/Table/Headers/TableHead.svelte +415 -415
- package/dist/Components/Table/Misc/ColumnPanel.svelte +39 -39
- package/dist/Components/Table/Misc/ColumnPanelModal.svelte +39 -39
- package/dist/Components/Table/Misc/FilterInput.svelte +641 -641
- package/dist/Components/Table/Misc/FilterPanel.svelte +190 -190
- package/dist/Components/Table/Misc/FilterPanelModal.svelte +31 -31
- package/dist/Components/Table/Misc/ShareViewModal.svelte +291 -291
- package/dist/Components/Table/Misc/SidePanel.svelte +35 -35
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte +644 -644
- package/dist/Components/Table/Misc/TableFooter.svelte +340 -340
- package/dist/Components/Table/Misc/TableHorizontalBar.svelte +140 -140
- package/dist/Components/Table/Misc/TableSidebar.svelte +200 -200
- package/dist/Components/Table/Misc/TableViewDropdown.svelte +105 -105
- package/dist/Components/Table/Misc/ToolboxPanel.svelte +100 -100
- package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +107 -107
- package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +192 -192
- package/dist/Components/Table/NestedTable.svelte +130 -130
- package/dist/Components/Table/Table.svelte +692 -692
- package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.d.ts +5 -0
- package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.js +3 -0
- package/dist/Components/Table/Types/Public/ColumnOptions.d.ts +10 -0
- package/dist/Components/Test/TestContextConsumer.svelte +13 -13
- package/dist/Components/TextArea.svelte +130 -130
- package/dist/Components/TextInput.svelte +176 -176
- package/dist/Components/ThreeStateRadio.svelte +131 -131
- package/dist/Components/Touch/BoolCard.svelte +45 -45
- package/dist/Components/Touch/DateModal.svelte +209 -209
- package/dist/Components/Touch/EditCard.svelte +59 -59
- package/dist/Components/Touch/InfoCard.svelte +31 -31
- package/dist/Components/Touch/InfoTextCard.svelte +28 -28
- package/dist/Components/Touch/ModalExplanation.svelte +17 -17
- package/dist/Components/Touch/NumberKeyboard.svelte +225 -225
- package/dist/Components/Touch/NumberModal.svelte +79 -79
- package/dist/Components/Touch/PlusMinusCard.svelte +46 -46
- package/dist/Components/Touch/SelectModal.svelte +127 -127
- package/dist/Components/Touch/SummaryCard.svelte +93 -93
- package/dist/Components/Touch/TextAreaModal.svelte +95 -95
- package/dist/Components/Touch/TextModal.svelte +106 -106
- package/dist/Components/Touch/TouchCard.svelte +33 -33
- package/dist/Components/Touch/TouchModal.svelte +274 -274
- package/dist/Components/Touch/UtilityButton.svelte +109 -109
- package/dist/Components/VerticalTabGroup.svelte +79 -79
- package/dist/Components/VerticalTabGroupButton.svelte +58 -58
- package/dist/Helpers/Datamatrix.d.ts +19 -19
- package/dist/Helpers/Datamatrix.js +291 -291
- package/dist/Types/Examples/ExampleModal.svelte +35 -35
- package/dist/scss/_basics.scss +12 -12
- package/dist/scss/_colours.scss +134 -134
- package/dist/scss/_mixins.scss +3392 -3392
- package/dist/scss/_motion.scss +57 -57
- package/dist/scss/_sizing.scss +85 -85
- package/dist/scss/_transitions.scss +8 -8
- package/package.json +1 -1
|
@@ -1,643 +1,643 @@
|
|
|
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() : [];
|
|
103
|
-
}
|
|
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;
|
|
114
|
-
if (def.debug) {
|
|
115
|
-
console.debug(
|
|
116
|
-
`${def.id}: `,
|
|
117
|
-
'FILTER INPUT: filtering state value:',
|
|
118
|
-
$state.snapshot(filteringStateValue)
|
|
119
|
-
);
|
|
120
|
-
}
|
|
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);
|
|
155
|
-
if (def.debug) {
|
|
156
|
-
console.debug(`${def.id}: cached value:`, $state.snapshot(cached));
|
|
157
|
-
}
|
|
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
|
-
value = null;
|
|
171
|
-
}
|
|
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
|
-
}
|
|
197
|
-
}
|
|
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;
|
|
211
|
-
}
|
|
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;
|
|
218
|
-
} else {
|
|
219
|
-
value = valueMin = valueMax = null;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
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
|
-
}
|
|
233
|
-
}
|
|
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;
|
|
248
|
-
}
|
|
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
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
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);
|
|
293
|
-
}
|
|
294
|
-
typingDebounceTimer = setTimeout(() => {
|
|
295
|
-
typingDebounceTimer = undefined;
|
|
296
|
-
tableContext.paginationRepo?.filterBy(id, newValue);
|
|
297
|
-
}, FILTER_INPUT_DEBOUNCE_MS);
|
|
298
|
-
}
|
|
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;
|
|
307
|
-
}
|
|
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
|
-
};
|
|
317
|
-
}
|
|
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
|
-
};
|
|
326
|
-
}
|
|
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
|
-
|
|
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() : [];
|
|
103
|
+
}
|
|
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;
|
|
114
|
+
if (def.debug) {
|
|
115
|
+
console.debug(
|
|
116
|
+
`${def.id}: `,
|
|
117
|
+
'FILTER INPUT: filtering state value:',
|
|
118
|
+
$state.snapshot(filteringStateValue)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
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);
|
|
155
|
+
if (def.debug) {
|
|
156
|
+
console.debug(`${def.id}: cached value:`, $state.snapshot(cached));
|
|
157
|
+
}
|
|
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
|
+
value = null;
|
|
171
|
+
}
|
|
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
|
+
}
|
|
197
|
+
}
|
|
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;
|
|
211
|
+
}
|
|
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;
|
|
218
|
+
} else {
|
|
219
|
+
value = valueMin = valueMax = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
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
|
+
}
|
|
233
|
+
}
|
|
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;
|
|
248
|
+
}
|
|
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
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
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);
|
|
293
|
+
}
|
|
294
|
+
typingDebounceTimer = setTimeout(() => {
|
|
295
|
+
typingDebounceTimer = undefined;
|
|
296
|
+
tableContext.paginationRepo?.filterBy(id, newValue);
|
|
297
|
+
}, FILTER_INPUT_DEBOUNCE_MS);
|
|
298
|
+
}
|
|
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;
|
|
307
|
+
}
|
|
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
|
+
};
|
|
317
|
+
}
|
|
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
|
+
};
|
|
326
|
+
}
|
|
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
|
+
|
|
641
641
|
<style>.contents {
|
|
642
642
|
display: contents;
|
|
643
643
|
--input-width: 100%;
|
|
@@ -708,4 +708,4 @@
|
|
|
708
708
|
.inner-row label input {
|
|
709
709
|
cursor: pointer;
|
|
710
710
|
width: 2rem;
|
|
711
|
-
}</style>
|
|
711
|
+
}</style>
|