@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,681 +1,681 @@
|
|
|
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 Action from '../../Action.svelte';
|
|
9
|
-
import Button from '../../Button.svelte';
|
|
10
|
-
import MultiSelect from '../../MultiSelect.svelte';
|
|
11
|
-
import NumberInput from '../../NumberInput.svelte';
|
|
12
|
-
import SingleSelect from '../../SingleSelect.svelte';
|
|
13
|
-
import TextInput from '../../TextInput.svelte';
|
|
14
|
-
import ThreeStateRadio from '../../ThreeStateRadio.svelte';
|
|
15
|
-
import DateModal from '../../Touch/DateModal.svelte';
|
|
16
|
-
import { hasQuery } from '../TableHelpers.svelte.js';
|
|
17
|
-
import { devCatch, isDateTuple, isNumericArray } from '../../../Helpers/Helpers.svelte.js';
|
|
18
|
-
import { Colour, ColourSet } from '../../../scss/colours.js';
|
|
19
|
-
import { InputVariant } from '../../../Types/Internal/InputVariant.js';
|
|
20
|
-
import type { SelectOption } from '../../../Types/Internal/SelectOption.js';
|
|
21
|
-
import { Size } from '../../../Types/Internal/Size.js';
|
|
22
|
-
import { getContext, onDestroy, onMount, untrack } from 'svelte';
|
|
23
|
-
import { cubicOut } from 'svelte/easing';
|
|
24
|
-
import { fade } from 'svelte/transition';
|
|
25
|
-
|
|
26
|
-
import QuerySelect from '../../QuerySelect.svelte';
|
|
27
|
-
import { Ternary } from '../../../Types/Internal/Ternary.js';
|
|
28
|
-
import { numberDateFilterTypes } from '../Misc/numberDateFilterTypes.js';
|
|
29
|
-
import { stringFilterTypes } from '../Misc/stringFilterTypes.js';
|
|
30
|
-
import { quickFilterContextIdentifier } from '../Table.svelte';
|
|
31
|
-
import { CheckboxDef } from '../Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js';
|
|
32
|
-
import { DateInputDef } from '../Types/Columns/Definitions/Accessors/DateInputDef.svelte.js';
|
|
33
|
-
import { NumberInputDef } from '../Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js';
|
|
34
|
-
import { SelectDef } from '../Types/Columns/Definitions/Accessors/SelectDef.svelte.js';
|
|
35
|
-
import { TableContext } from '../Types/Context/TableContext.svelte.js';
|
|
36
|
-
import type { Primitive } from '../Types/Context/TableInitOptions.js';
|
|
37
|
-
import { CustomFilterType } from '../Types/Filtering/CustomFilterType.js';
|
|
38
|
-
import { NumberDateFilterMode } from '../Types/Filtering/NumberDateFilterMode.js';
|
|
39
|
-
import { StringFilterMode } from '../Types/Filtering/StringFilterMode.js';
|
|
40
|
-
import type { QuickSearchCellProps } from './QuickSearchCellProps.js';
|
|
41
|
-
|
|
42
|
-
const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
|
|
43
|
-
|
|
44
|
-
const { column, customFilters }: QuickSearchCellProps<T, FilterValueType> = $props();
|
|
45
|
-
|
|
46
|
-
const def = $derived(column.columnDef);
|
|
47
|
-
|
|
48
|
-
const customFilter = $derived(customFilters?.get(def.id));
|
|
49
|
-
|
|
50
|
-
const isSelect = $derived(
|
|
51
|
-
def instanceof SelectDef ||
|
|
52
|
-
def.filterValueType == CustomFilterType.Select ||
|
|
53
|
-
customFilter?.type === CustomFilterType.Select
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const hasFilterFn = $derived(!!def.filterFn && typeof def.filterFn === 'function');
|
|
57
|
-
|
|
58
|
-
// A column is filterable when it has any of: a local `filterFn`, a
|
|
59
|
-
// declared `filterValueType` (server-side filtering — required for
|
|
60
|
-
// remote-pagination tables whose data repo never assigns `filterFn`),
|
|
61
|
-
// or a `customFilters` entry from `defsWrapper.remoteFilters`.
|
|
62
|
-
const hasDeclaredFilterType = $derived(def.filterValueType != null);
|
|
63
|
-
|
|
64
|
-
const isSearchable = $derived(
|
|
65
|
-
def.allowFiltering &&
|
|
66
|
-
(hasFilterFn || hasDeclaredFilterType || customFilters?.has(def.id) == true)
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const isNumeric = $derived(
|
|
70
|
-
def instanceof NumberInputDef ||
|
|
71
|
-
def.filterValueType == 'Number' ||
|
|
72
|
-
customFilter?.type === CustomFilterType.Number
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
function emptyOptsAndWarn(): Array<SelectOption<unknown>> {
|
|
76
|
-
console.warn(`def ${def.id} is declared as a select filter but has no getOptions() function`);
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
const options = $derived(
|
|
80
|
-
isSelect ? (def.getOptions?.() ?? customFilter?.options ?? emptyOptsAndWarn()) : []
|
|
81
|
-
);
|
|
82
|
-
const isDate = $derived(def instanceof DateInputDef);
|
|
83
|
-
const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
|
|
84
|
-
|
|
85
|
-
const selectSymbol = Symbol();
|
|
86
|
-
|
|
87
|
-
const openedSelect = getContext<{ value: symbol | null } | null | undefined>(
|
|
88
|
-
quickFilterContextIdentifier
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const isSelectOpen = $derived(openedSelect?.value === selectSymbol);
|
|
92
|
-
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
94
|
-
let value: unknown | null = $state(null);
|
|
95
|
-
let valueMin: number | null = $state(null);
|
|
96
|
-
let valueMax: number | null = $state(null);
|
|
97
|
-
|
|
98
|
-
let selectedOptions: Array<SelectOption<unknown>> = $state([]);
|
|
99
|
-
let lastQueryOptions: Array<SelectOption<unknown>> = $state([]);
|
|
100
|
-
|
|
101
|
-
const inputId = $derived(`${tableContext.tableName}-cell-quicksearch-${column.id}`);
|
|
102
|
-
|
|
103
|
-
const filterMode = $derived(tableContext.paginationRepo?.filterModes.get(def.id));
|
|
104
|
-
|
|
105
|
-
const selectedNumberFilterType: SelectOption<NumberDateFilterMode> | null = $derived.by(() => {
|
|
106
|
-
const value = (filterMode as NumberDateFilterMode | undefined) ?? NumberDateFilterMode.In;
|
|
107
|
-
return numberDateFilterTypes.find((it) => it.value === value) ?? null;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const selectedStringFilterType: SelectOption<StringFilterMode> | null = $derived.by(() => {
|
|
111
|
-
const value = (filterMode as StringFilterMode | undefined) ?? StringFilterMode.Contains;
|
|
112
|
-
return stringFilterTypes.find((it) => it.value === value) ?? null;
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const filteringStateValue = $derived(
|
|
116
|
-
tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
$effect(() => {
|
|
120
|
-
if (isRemoteSelect && !tableContext.paginationRepo?.filtering) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
void filteringStateValue;
|
|
124
|
-
|
|
125
|
-
if (hasQuery(customFilter)) {
|
|
126
|
-
if (def.debug) {
|
|
127
|
-
console.debug(`${def.id}: `, 'has query');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
untrack(async () => {
|
|
131
|
-
const gotValue = filteringStateValue as Array<unknown> | undefined;
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
// as unknown as string | number | undefined;
|
|
135
|
-
|
|
136
|
-
if (gotValue != undefined) {
|
|
137
|
-
if (def.debug) {
|
|
138
|
-
console.debug(`${def.id}: query values:`, tableContext.queryValues.snapshot());
|
|
139
|
-
}
|
|
140
|
-
const x = tableContext.queryValues.get(def);
|
|
141
|
-
|
|
142
|
-
if (def.debug) {
|
|
143
|
-
console.debug(`${def.id}: `, 'value:', $state.snapshot(x));
|
|
144
|
-
}
|
|
145
|
-
value = x ?? null;
|
|
146
|
-
} else if (def.debug) {
|
|
147
|
-
console.debug(
|
|
148
|
-
`${def.id}: `,
|
|
149
|
-
'filtering state value was undefined',
|
|
150
|
-
$state.snapshot(tableContext.paginationRepo?.filtering)
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
} catch (e) {
|
|
154
|
-
console.warn(e);
|
|
155
|
-
value = null;
|
|
156
|
-
}
|
|
157
|
-
}).catch(devCatch);
|
|
158
|
-
} else if (isSelect) {
|
|
159
|
-
if (def.debug) {
|
|
160
|
-
console.debug(`${def.id}: `, 'is select (not query)');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!tableContext.paginationRepo?.filtering) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const gotValue = filteringStateValue as Array<unknown> | undefined;
|
|
168
|
-
|
|
169
|
-
function isSelectOption(it: unknown): it is SelectOption<unknown> {
|
|
170
|
-
return it != null && typeof it == 'object';
|
|
171
|
-
}
|
|
172
|
-
untrack(async () => {
|
|
173
|
-
try {
|
|
174
|
-
const opts = await options;
|
|
175
|
-
if (def.debug) {
|
|
176
|
-
console.debug(`${def.id}: possible options:`, $state.snapshot(opts));
|
|
177
|
-
}
|
|
178
|
-
const x = opts.filter((it) => {
|
|
179
|
-
if (!isSelectOption(it)) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
if (it.value == undefined) {
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (typeof it.value == 'string' || typeof it.value == 'number') {
|
|
187
|
-
return (gotValue as unknown as Array<string | number> | undefined)?.includes(
|
|
188
|
-
it.value
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return false;
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
if (def.debug) {
|
|
196
|
-
console.debug(`${def.id}: `, 'selected options:', x);
|
|
197
|
-
}
|
|
198
|
-
selectedOptions = x;
|
|
199
|
-
} catch (e) {
|
|
200
|
-
if (dev || def.debug) {
|
|
201
|
-
console.debug(`${def.id}: `, e);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}).catch(devCatch);
|
|
205
|
-
} else if (isNumeric) {
|
|
206
|
-
if (
|
|
207
|
-
selectedNumberFilterType?.value === NumberDateFilterMode.Between &&
|
|
208
|
-
tableContext.paginationRepo?.filtering
|
|
209
|
-
) {
|
|
210
|
-
const gotValue = filteringStateValue;
|
|
211
|
-
if (gotValue != undefined) {
|
|
212
|
-
const [min, max] = gotValue as [number, number | undefined];
|
|
213
|
-
value = min;
|
|
214
|
-
valueMin = min;
|
|
215
|
-
valueMax = max ?? null;
|
|
216
|
-
} else {
|
|
217
|
-
value = valueMin = valueMax = null;
|
|
218
|
-
}
|
|
219
|
-
} else if (tableContext.paginationRepo?.filtering) {
|
|
220
|
-
const gotValue = filteringStateValue;
|
|
221
|
-
|
|
222
|
-
if (gotValue != undefined) {
|
|
223
|
-
const [min] = gotValue as [number, number | undefined];
|
|
224
|
-
value = min;
|
|
225
|
-
valueMin = min;
|
|
226
|
-
valueMax = min;
|
|
227
|
-
} else {
|
|
228
|
-
value = valueMin = valueMax = null;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
} else if (tableContext.paginationRepo?.filtering) {
|
|
232
|
-
const gotValue = filteringStateValue;
|
|
233
|
-
if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
|
|
234
|
-
assertNotPromise(gotValue);
|
|
235
|
-
value = gotValue;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Dev-only invariant: a Promise must never reach the synchronous primitive
|
|
242
|
-
* sink (we should always have awaited it upstream). Throws in dev to make
|
|
243
|
-
* any regression loud; no-ops in production builds.
|
|
244
|
-
*/
|
|
245
|
-
function assertNotPromise(gotValue: unknown) {
|
|
246
|
-
if (dev && gotValue != null && typeof gotValue == 'object' && gotValue instanceof Promise) {
|
|
247
|
-
throw new Error(`Unexpected Promise`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
onMount(() => {
|
|
251
|
-
const filterOption =
|
|
252
|
-
tableContext.paginationRepo?.filtering &&
|
|
253
|
-
tableContext.paginationRepo.filtering.find((it) => it.id === def.id);
|
|
254
|
-
|
|
255
|
-
if (filterOption) {
|
|
256
|
-
if (isNumericArray(filterOption.value)) {
|
|
257
|
-
[valueMin, valueMax] = filterOption.value;
|
|
258
|
-
if (valueMax == undefined) {
|
|
259
|
-
valueMax = valueMin;
|
|
260
|
-
value = valueMin?.toString();
|
|
261
|
-
} else if (valueMin == undefined) {
|
|
262
|
-
valueMin = valueMax;
|
|
263
|
-
value = valueMax.toString();
|
|
264
|
-
} else {
|
|
265
|
-
value = valueMin.toString();
|
|
266
|
-
}
|
|
267
|
-
} else if (!hasQuery(customFilter)) {
|
|
268
|
-
const gotValue = filterOption.value;
|
|
269
|
-
if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
|
|
270
|
-
assertNotPromise(gotValue);
|
|
271
|
-
value = gotValue;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
function closeOnCtrlF(e: KeyboardEvent) {
|
|
278
|
-
if (e.key.toLocaleUpperCase() === 'F' && e.ctrlKey) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
e.stopPropagation();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
let showFromDateModal = $state(false);
|
|
285
|
-
let showToDateModal = $state(false);
|
|
286
|
-
|
|
287
|
-
function generateOnChangeObject(_label: string) {
|
|
288
|
-
return (newValue: SelectOption<unknown> | null) => {
|
|
289
|
-
tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
|
|
290
|
-
tableContext.queryValues.set(def, newValue);
|
|
291
|
-
assertNotPromise(newValue);
|
|
292
|
-
value = newValue;
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function generateOnChangePrimitive(_label: string) {
|
|
297
|
-
// AsPrimitive QuerySelect hands us the primitive value directly.
|
|
298
|
-
return (newValue: unknown) => {
|
|
299
|
-
tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
|
|
300
|
-
tableContext.queryValues.set(def, newValue ?? null);
|
|
301
|
-
assertNotPromise(newValue);
|
|
302
|
-
value = newValue ?? null;
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let isNullFilter = $derived(selectedNumberFilterType?.value == NumberDateFilterMode.null);
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Debounce window for typed filter inputs. For a 10k-row table each
|
|
310
|
-
* keystroke would otherwise kick off a full filter pass; coalescing
|
|
311
|
-
* adjacent keystrokes within `FILTER_DEBOUNCE_MS` cuts the cost to
|
|
312
|
-
* roughly one filter pass per pause-in-typing.
|
|
313
|
-
*/
|
|
314
|
-
const FILTER_DEBOUNCE_MS = 300;
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Sequence counter used to discard the result of a stale debounce
|
|
318
|
-
* timer. When the user types again before the timer fires, the
|
|
319
|
-
* `pendingSeq` increments; only the timer whose captured `seq`
|
|
320
|
-
* matches `pendingSeq` is allowed to call `filterBy`.
|
|
321
|
-
*/
|
|
322
|
-
let pendingSeq = 0;
|
|
323
|
-
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
324
|
-
|
|
325
|
-
function debouncedFilterBy(value: unknown): void {
|
|
326
|
-
pendingSeq += 1;
|
|
327
|
-
const seq = pendingSeq;
|
|
328
|
-
if (debounceTimer !== undefined) {
|
|
329
|
-
clearTimeout(debounceTimer);
|
|
330
|
-
}
|
|
331
|
-
debounceTimer = setTimeout(() => {
|
|
332
|
-
debounceTimer = undefined;
|
|
333
|
-
if (seq !== pendingSeq) {
|
|
334
|
-
// A newer keystroke superseded this one before the timer
|
|
335
|
-
// fired; the newer timer will handle the filter call.
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
tableContext.paginationRepo?.filterBy(def.id, value);
|
|
339
|
-
}, FILTER_DEBOUNCE_MS);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
onDestroy(() => {
|
|
343
|
-
if (debounceTimer !== undefined) {
|
|
344
|
-
clearTimeout(debounceTimer);
|
|
345
|
-
debounceTimer = undefined;
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
</script>
|
|
349
|
-
|
|
350
|
-
<div class="input-cell {inputId}">
|
|
351
|
-
{#if isSearchable}
|
|
352
|
-
<div class="input-wrapper-parent">
|
|
353
|
-
{#if isDate}
|
|
354
|
-
{@const dateValues = tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)
|
|
355
|
-
?.value ?? [null, null]}
|
|
356
|
-
{#if isDateTuple(dateValues)}
|
|
357
|
-
{@const [fromDate, toDate] = dateValues}
|
|
358
|
-
<div class="spaced-around">
|
|
359
|
-
<Button
|
|
360
|
-
size={Size.FillHeight}
|
|
361
|
-
fullWidth
|
|
362
|
-
onclick={() => {
|
|
363
|
-
showFromDateModal = true;
|
|
364
|
-
}}>From {fromDate?.toLocaleDateString() ?? 'Any Date'}</Button
|
|
365
|
-
>
|
|
366
|
-
|
|
367
|
-
<Button
|
|
368
|
-
size={Size.FillHeight}
|
|
369
|
-
fullWidth
|
|
370
|
-
onclick={() => {
|
|
371
|
-
showToDateModal = true;
|
|
372
|
-
}}>To {toDate?.toLocaleDateString() ?? 'Any Date'}</Button
|
|
373
|
-
>
|
|
374
|
-
</div>
|
|
375
|
-
|
|
376
|
-
{#if showFromDateModal}
|
|
377
|
-
<DateModal
|
|
378
|
-
header="Edit Date From"
|
|
379
|
-
value={fromDate}
|
|
380
|
-
cancelEditing={() => {
|
|
381
|
-
showFromDateModal = false;
|
|
382
|
-
}}
|
|
383
|
-
confirm={(e) => {
|
|
384
|
-
tableContext.paginationRepo?.filterBy(def.id, [e, toDate]);
|
|
385
|
-
showFromDateModal = false;
|
|
386
|
-
}}
|
|
387
|
-
/>
|
|
388
|
-
{/if}
|
|
389
|
-
|
|
390
|
-
{#if showToDateModal}
|
|
391
|
-
<DateModal
|
|
392
|
-
inclusive
|
|
393
|
-
header="Edit Date To"
|
|
394
|
-
value={toDate}
|
|
395
|
-
cancelEditing={() => {
|
|
396
|
-
showToDateModal = false;
|
|
397
|
-
}}
|
|
398
|
-
confirm={(e) => {
|
|
399
|
-
tableContext.paginationRepo?.filterBy(def.id, [fromDate, e]);
|
|
400
|
-
showToDateModal = false;
|
|
401
|
-
}}
|
|
402
|
-
/>
|
|
403
|
-
{/if}
|
|
404
|
-
{/if}
|
|
405
|
-
{:else if isSelect}
|
|
406
|
-
{#if hasQuery(customFilter)}
|
|
407
|
-
{#if customFilter.valueAsObject !== true}
|
|
408
|
-
{@const label = customFilter.id}
|
|
409
|
-
|
|
410
|
-
<!-- TODO: multiselect -->
|
|
411
|
-
|
|
412
|
-
<!-- FIXME: Most likely Unsafe -->
|
|
413
|
-
<QuerySelect
|
|
414
|
-
valueAsObject={false}
|
|
415
|
-
clearable
|
|
416
|
-
name={customFilter.objectName ?? 'Item'}
|
|
417
|
-
valueProp="value"
|
|
418
|
-
labelProp="label"
|
|
419
|
-
value={value as null}
|
|
420
|
-
query={customFilter.query}
|
|
421
|
-
bind:options={lastQueryOptions}
|
|
422
|
-
onchange={generateOnChangePrimitive(label)}
|
|
423
|
-
/>
|
|
424
|
-
{:else}
|
|
425
|
-
{@const label = customFilter.id}
|
|
426
|
-
|
|
427
|
-
<!-- TODO: multiselect -->
|
|
428
|
-
|
|
429
|
-
<QuerySelect
|
|
430
|
-
valueAsObject={true}
|
|
431
|
-
clearable
|
|
432
|
-
name={customFilter.objectName ?? 'Item'}
|
|
433
|
-
valueProp="value"
|
|
434
|
-
labelProp="label"
|
|
435
|
-
value={value as null}
|
|
436
|
-
query={customFilter.query}
|
|
437
|
-
bind:options={lastQueryOptions}
|
|
438
|
-
onchange={generateOnChangeObject(label)}
|
|
439
|
-
/>
|
|
440
|
-
{/if}
|
|
441
|
-
{:else}
|
|
442
|
-
{#await options then options}
|
|
443
|
-
<MultiSelect
|
|
444
|
-
labelProp="label"
|
|
445
|
-
valueProp="value"
|
|
446
|
-
{options}
|
|
447
|
-
valueAsObject
|
|
448
|
-
onchange={(e: Array<SelectOption<unknown>>) => {
|
|
449
|
-
tableContext.paginationRepo?.filterBy(
|
|
450
|
-
def.id,
|
|
451
|
-
e.map((it) => it.value)
|
|
452
|
-
);
|
|
453
|
-
}}
|
|
454
|
-
selectedValues={selectedOptions}
|
|
455
|
-
collapseSelection
|
|
456
|
-
/>
|
|
457
|
-
{/await}
|
|
458
|
-
{/if}
|
|
459
|
-
{:else if def instanceof CheckboxDef}
|
|
460
|
-
{@const value = (() => {
|
|
461
|
-
switch (tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value) {
|
|
462
|
-
case true: {
|
|
463
|
-
return Ternary.True;
|
|
464
|
-
}
|
|
465
|
-
case false: {
|
|
466
|
-
return Ternary.False;
|
|
467
|
-
}
|
|
468
|
-
default: {
|
|
469
|
-
return Ternary.Neutral;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
})()}
|
|
473
|
-
<div class="centered">
|
|
474
|
-
<ThreeStateRadio
|
|
475
|
-
{value}
|
|
476
|
-
yesLabel="Selected"
|
|
477
|
-
noLabel="Not Selected"
|
|
478
|
-
neutralLabel="Any"
|
|
479
|
-
neutralIcon={{ name: 'circle-minus' }}
|
|
480
|
-
onchange={(v) => {
|
|
481
|
-
switch (v) {
|
|
482
|
-
case Ternary.False: {
|
|
483
|
-
tableContext.paginationRepo?.filterBy(def.id, false);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
case Ternary.True: {
|
|
487
|
-
tableContext.paginationRepo?.filterBy(def.id, true);
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
case Ternary.Neutral:
|
|
491
|
-
default: {
|
|
492
|
-
tableContext.paginationRepo?.filterBy(def.id, null);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}}
|
|
497
|
-
/>
|
|
498
|
-
</div>
|
|
499
|
-
{:else if isNumeric}
|
|
500
|
-
{#if selectedNumberFilterType?.value === NumberDateFilterMode.Between}
|
|
501
|
-
<div class="input-wrapper left">
|
|
502
|
-
<NumberInput
|
|
503
|
-
data-type="quicksearch"
|
|
504
|
-
id={inputId}
|
|
505
|
-
bind:value={valueMin}
|
|
506
|
-
variant={InputVariant.Table}
|
|
507
|
-
placeholder="Min…"
|
|
508
|
-
oninput={() => {
|
|
509
|
-
debouncedFilterBy([valueMin, valueMax]);
|
|
510
|
-
}}
|
|
511
|
-
onkeydown={closeOnCtrlF}
|
|
512
|
-
/>
|
|
513
|
-
</div>
|
|
514
|
-
<div class="input-wrapper right">
|
|
515
|
-
<NumberInput
|
|
516
|
-
data-type="quicksearch"
|
|
517
|
-
id={inputId}
|
|
518
|
-
bind:value={valueMax}
|
|
519
|
-
placeholder="Max…"
|
|
520
|
-
variant={InputVariant.Table}
|
|
521
|
-
oninput={() => {
|
|
522
|
-
debouncedFilterBy([valueMin, valueMax]);
|
|
523
|
-
}}
|
|
524
|
-
onkeydown={closeOnCtrlF}
|
|
525
|
-
/>
|
|
526
|
-
</div>
|
|
527
|
-
{:else}
|
|
528
|
-
<div class="input-wrapper right">
|
|
529
|
-
{#await isNullFilter}
|
|
530
|
-
loading
|
|
531
|
-
{:then isNullFilter}
|
|
532
|
-
{#if isNullFilter}
|
|
533
|
-
<TextInput
|
|
534
|
-
data-type="quicksearch"
|
|
535
|
-
id={inputId}
|
|
536
|
-
value="is Null"
|
|
537
|
-
disabled
|
|
538
|
-
variant={InputVariant.Table}
|
|
539
|
-
/>
|
|
540
|
-
{:else}
|
|
541
|
-
<!-- NOTE: This is a text input in order to support the __in functionality, which is a comma separated list of numbers -->
|
|
542
|
-
<TextInput
|
|
543
|
-
data-type="quicksearch"
|
|
544
|
-
id={inputId}
|
|
545
|
-
bind:value={value as string | null}
|
|
546
|
-
placeholder="Filter…"
|
|
547
|
-
variant={InputVariant.Table}
|
|
548
|
-
oninput={() => {
|
|
549
|
-
debouncedFilterBy([value || null]);
|
|
550
|
-
}}
|
|
551
|
-
onkeydown={closeOnCtrlF}
|
|
552
|
-
/>
|
|
553
|
-
{/if}
|
|
554
|
-
{:catch e}
|
|
555
|
-
<!-- eslint-disable-next-line svelte/no-at-debug-tags -->
|
|
556
|
-
{@debug e}
|
|
557
|
-
{/await}
|
|
558
|
-
</div>
|
|
559
|
-
{/if}
|
|
560
|
-
{:else if value == null || typeof value == 'string'}
|
|
561
|
-
<div class="input-wrapper right">
|
|
562
|
-
{#if selectedStringFilterType?.value == StringFilterMode.null}
|
|
563
|
-
<TextInput
|
|
564
|
-
data-type="quicksearch"
|
|
565
|
-
id={inputId}
|
|
566
|
-
value="is Null"
|
|
567
|
-
disabled
|
|
568
|
-
variant={InputVariant.Table}
|
|
569
|
-
/>
|
|
570
|
-
{:else if selectedStringFilterType?.value == StringFilterMode.Empty}
|
|
571
|
-
<TextInput
|
|
572
|
-
data-type="quicksearch"
|
|
573
|
-
id={inputId}
|
|
574
|
-
value="is Empty"
|
|
575
|
-
disabled
|
|
576
|
-
variant={InputVariant.Table}
|
|
577
|
-
/>
|
|
578
|
-
{:else}
|
|
579
|
-
<TextInput
|
|
580
|
-
data-type="quicksearch"
|
|
581
|
-
id={inputId}
|
|
582
|
-
oninput={() => {
|
|
583
|
-
debouncedFilterBy(value);
|
|
584
|
-
}}
|
|
585
|
-
variant={InputVariant.Table}
|
|
586
|
-
bind:value={value as string | null}
|
|
587
|
-
placeholder="Filter…"
|
|
588
|
-
/>
|
|
589
|
-
{/if}
|
|
590
|
-
</div>
|
|
591
|
-
{/if}
|
|
592
|
-
|
|
593
|
-
<!-- TODO button for date, checkbox or select for checkbox -->
|
|
594
|
-
</div>
|
|
595
|
-
|
|
596
|
-
{#if !isSelect && !isDate && !(def instanceof CheckboxDef)}
|
|
597
|
-
<div class="stick-right">
|
|
598
|
-
<Action
|
|
599
|
-
data-type="quicksearch"
|
|
600
|
-
size={Size.FillHeight}
|
|
601
|
-
colourSet={ColourSet.Auxiliary}
|
|
602
|
-
icon={{
|
|
603
|
-
name: 'filter-2',
|
|
604
|
-
size: Size.MediumLarge,
|
|
605
|
-
colour: Colour['$ui-blue-5'],
|
|
606
|
-
}}
|
|
607
|
-
onclick={() => {
|
|
608
|
-
if (!openedSelect) {
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
if (openedSelect.value === selectSymbol) {
|
|
612
|
-
openedSelect.value = null;
|
|
613
|
-
} else {
|
|
614
|
-
openedSelect.value = selectSymbol;
|
|
615
|
-
}
|
|
616
|
-
}}
|
|
617
|
-
/>
|
|
618
|
-
</div>
|
|
619
|
-
{/if}
|
|
620
|
-
{/if}
|
|
621
|
-
|
|
622
|
-
{#if isSelectOpen}
|
|
623
|
-
<div
|
|
624
|
-
class="floating"
|
|
625
|
-
transition:fade={{ easing: cubicOut, duration: 100 }}
|
|
626
|
-
>
|
|
627
|
-
<div class="select-wrapper">
|
|
628
|
-
{#if isNumeric}
|
|
629
|
-
<SingleSelect
|
|
630
|
-
options={numberDateFilterTypes}
|
|
631
|
-
labelProp="label"
|
|
632
|
-
valueProp="value"
|
|
633
|
-
name="Filter Type"
|
|
634
|
-
valueAsObject
|
|
635
|
-
value={selectedNumberFilterType}
|
|
636
|
-
onchange={(newValue: SelectOption<NumberDateFilterMode> | null) => {
|
|
637
|
-
const oldFilterType = selectedNumberFilterType;
|
|
638
|
-
|
|
639
|
-
const newFilterType = newValue?.value ?? NumberDateFilterMode.In;
|
|
640
|
-
|
|
641
|
-
if (newFilterType !== oldFilterType?.value) {
|
|
642
|
-
tableContext.paginationRepo?.setFilterMode(def.id, newFilterType);
|
|
643
|
-
|
|
644
|
-
if (newFilterType === NumberDateFilterMode.Between) {
|
|
645
|
-
tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMin]);
|
|
646
|
-
} else if (newFilterType === NumberDateFilterMode.In) {
|
|
647
|
-
tableContext.paginationRepo?.filterBy(def.id, [value]);
|
|
648
|
-
} else {
|
|
649
|
-
tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMax]);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}}
|
|
653
|
-
onkeydown={(e: KeyboardEvent) => {
|
|
654
|
-
e.stopPropagation();
|
|
655
|
-
}}
|
|
656
|
-
/>
|
|
657
|
-
{:else}
|
|
658
|
-
<SingleSelect
|
|
659
|
-
options={stringFilterTypes}
|
|
660
|
-
labelProp="label"
|
|
661
|
-
valueProp="value"
|
|
662
|
-
name="Filter Type"
|
|
663
|
-
valueAsObject
|
|
664
|
-
value={selectedStringFilterType}
|
|
665
|
-
onchange={(newValue: SelectOption<StringFilterMode> | null) => {
|
|
666
|
-
const filterType = newValue?.value ?? StringFilterMode.Contains;
|
|
667
|
-
tableContext.paginationRepo?.setFilterMode(def.id, filterType);
|
|
668
|
-
}}
|
|
669
|
-
onkeydown={(e: KeyboardEvent) => {
|
|
670
|
-
e.stopPropagation();
|
|
671
|
-
}}
|
|
672
|
-
/>
|
|
673
|
-
{/if}
|
|
674
|
-
</div>
|
|
675
|
-
</div>
|
|
676
|
-
{/if}
|
|
677
|
-
</div>
|
|
678
|
-
|
|
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 Action from '../../Action.svelte';
|
|
9
|
+
import Button from '../../Button.svelte';
|
|
10
|
+
import MultiSelect from '../../MultiSelect.svelte';
|
|
11
|
+
import NumberInput from '../../NumberInput.svelte';
|
|
12
|
+
import SingleSelect from '../../SingleSelect.svelte';
|
|
13
|
+
import TextInput from '../../TextInput.svelte';
|
|
14
|
+
import ThreeStateRadio from '../../ThreeStateRadio.svelte';
|
|
15
|
+
import DateModal from '../../Touch/DateModal.svelte';
|
|
16
|
+
import { hasQuery } from '../TableHelpers.svelte.js';
|
|
17
|
+
import { devCatch, isDateTuple, isNumericArray } from '../../../Helpers/Helpers.svelte.js';
|
|
18
|
+
import { Colour, ColourSet } from '../../../scss/colours.js';
|
|
19
|
+
import { InputVariant } from '../../../Types/Internal/InputVariant.js';
|
|
20
|
+
import type { SelectOption } from '../../../Types/Internal/SelectOption.js';
|
|
21
|
+
import { Size } from '../../../Types/Internal/Size.js';
|
|
22
|
+
import { getContext, onDestroy, onMount, untrack } from 'svelte';
|
|
23
|
+
import { cubicOut } from 'svelte/easing';
|
|
24
|
+
import { fade } from 'svelte/transition';
|
|
25
|
+
|
|
26
|
+
import QuerySelect from '../../QuerySelect.svelte';
|
|
27
|
+
import { Ternary } from '../../../Types/Internal/Ternary.js';
|
|
28
|
+
import { numberDateFilterTypes } from '../Misc/numberDateFilterTypes.js';
|
|
29
|
+
import { stringFilterTypes } from '../Misc/stringFilterTypes.js';
|
|
30
|
+
import { quickFilterContextIdentifier } from '../Table.svelte';
|
|
31
|
+
import { CheckboxDef } from '../Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js';
|
|
32
|
+
import { DateInputDef } from '../Types/Columns/Definitions/Accessors/DateInputDef.svelte.js';
|
|
33
|
+
import { NumberInputDef } from '../Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js';
|
|
34
|
+
import { SelectDef } from '../Types/Columns/Definitions/Accessors/SelectDef.svelte.js';
|
|
35
|
+
import { TableContext } from '../Types/Context/TableContext.svelte.js';
|
|
36
|
+
import type { Primitive } from '../Types/Context/TableInitOptions.js';
|
|
37
|
+
import { CustomFilterType } from '../Types/Filtering/CustomFilterType.js';
|
|
38
|
+
import { NumberDateFilterMode } from '../Types/Filtering/NumberDateFilterMode.js';
|
|
39
|
+
import { StringFilterMode } from '../Types/Filtering/StringFilterMode.js';
|
|
40
|
+
import type { QuickSearchCellProps } from './QuickSearchCellProps.js';
|
|
41
|
+
|
|
42
|
+
const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
|
|
43
|
+
|
|
44
|
+
const { column, customFilters }: QuickSearchCellProps<T, FilterValueType> = $props();
|
|
45
|
+
|
|
46
|
+
const def = $derived(column.columnDef);
|
|
47
|
+
|
|
48
|
+
const customFilter = $derived(customFilters?.get(def.id));
|
|
49
|
+
|
|
50
|
+
const isSelect = $derived(
|
|
51
|
+
def instanceof SelectDef ||
|
|
52
|
+
def.filterValueType == CustomFilterType.Select ||
|
|
53
|
+
customFilter?.type === CustomFilterType.Select
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const hasFilterFn = $derived(!!def.filterFn && typeof def.filterFn === 'function');
|
|
57
|
+
|
|
58
|
+
// A column is filterable when it has any of: a local `filterFn`, a
|
|
59
|
+
// declared `filterValueType` (server-side filtering — required for
|
|
60
|
+
// remote-pagination tables whose data repo never assigns `filterFn`),
|
|
61
|
+
// or a `customFilters` entry from `defsWrapper.remoteFilters`.
|
|
62
|
+
const hasDeclaredFilterType = $derived(def.filterValueType != null);
|
|
63
|
+
|
|
64
|
+
const isSearchable = $derived(
|
|
65
|
+
def.allowFiltering &&
|
|
66
|
+
(hasFilterFn || hasDeclaredFilterType || customFilters?.has(def.id) == true)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const isNumeric = $derived(
|
|
70
|
+
def instanceof NumberInputDef ||
|
|
71
|
+
def.filterValueType == 'Number' ||
|
|
72
|
+
customFilter?.type === CustomFilterType.Number
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
function emptyOptsAndWarn(): Array<SelectOption<unknown>> {
|
|
76
|
+
console.warn(`def ${def.id} is declared as a select filter but has no getOptions() function`);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const options = $derived(
|
|
80
|
+
isSelect ? (def.getOptions?.() ?? customFilter?.options ?? emptyOptsAndWarn()) : []
|
|
81
|
+
);
|
|
82
|
+
const isDate = $derived(def instanceof DateInputDef);
|
|
83
|
+
const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
|
|
84
|
+
|
|
85
|
+
const selectSymbol = Symbol();
|
|
86
|
+
|
|
87
|
+
const openedSelect = getContext<{ value: symbol | null } | null | undefined>(
|
|
88
|
+
quickFilterContextIdentifier
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const isSelectOpen = $derived(openedSelect?.value === selectSymbol);
|
|
92
|
+
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
94
|
+
let value: unknown | null = $state(null);
|
|
95
|
+
let valueMin: number | null = $state(null);
|
|
96
|
+
let valueMax: number | null = $state(null);
|
|
97
|
+
|
|
98
|
+
let selectedOptions: Array<SelectOption<unknown>> = $state([]);
|
|
99
|
+
let lastQueryOptions: Array<SelectOption<unknown>> = $state([]);
|
|
100
|
+
|
|
101
|
+
const inputId = $derived(`${tableContext.tableName}-cell-quicksearch-${column.id}`);
|
|
102
|
+
|
|
103
|
+
const filterMode = $derived(tableContext.paginationRepo?.filterModes.get(def.id));
|
|
104
|
+
|
|
105
|
+
const selectedNumberFilterType: SelectOption<NumberDateFilterMode> | null = $derived.by(() => {
|
|
106
|
+
const value = (filterMode as NumberDateFilterMode | undefined) ?? NumberDateFilterMode.In;
|
|
107
|
+
return numberDateFilterTypes.find((it) => it.value === value) ?? null;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const selectedStringFilterType: SelectOption<StringFilterMode> | null = $derived.by(() => {
|
|
111
|
+
const value = (filterMode as StringFilterMode | undefined) ?? StringFilterMode.Contains;
|
|
112
|
+
return stringFilterTypes.find((it) => it.value === value) ?? null;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const filteringStateValue = $derived(
|
|
116
|
+
tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
$effect(() => {
|
|
120
|
+
if (isRemoteSelect && !tableContext.paginationRepo?.filtering) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
void filteringStateValue;
|
|
124
|
+
|
|
125
|
+
if (hasQuery(customFilter)) {
|
|
126
|
+
if (def.debug) {
|
|
127
|
+
console.debug(`${def.id}: `, 'has query');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
untrack(async () => {
|
|
131
|
+
const gotValue = filteringStateValue as Array<unknown> | undefined;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// as unknown as string | number | undefined;
|
|
135
|
+
|
|
136
|
+
if (gotValue != undefined) {
|
|
137
|
+
if (def.debug) {
|
|
138
|
+
console.debug(`${def.id}: query values:`, tableContext.queryValues.snapshot());
|
|
139
|
+
}
|
|
140
|
+
const x = tableContext.queryValues.get(def);
|
|
141
|
+
|
|
142
|
+
if (def.debug) {
|
|
143
|
+
console.debug(`${def.id}: `, 'value:', $state.snapshot(x));
|
|
144
|
+
}
|
|
145
|
+
value = x ?? null;
|
|
146
|
+
} else if (def.debug) {
|
|
147
|
+
console.debug(
|
|
148
|
+
`${def.id}: `,
|
|
149
|
+
'filtering state value was undefined',
|
|
150
|
+
$state.snapshot(tableContext.paginationRepo?.filtering)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.warn(e);
|
|
155
|
+
value = null;
|
|
156
|
+
}
|
|
157
|
+
}).catch(devCatch);
|
|
158
|
+
} else if (isSelect) {
|
|
159
|
+
if (def.debug) {
|
|
160
|
+
console.debug(`${def.id}: `, 'is select (not query)');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!tableContext.paginationRepo?.filtering) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const gotValue = filteringStateValue as Array<unknown> | undefined;
|
|
168
|
+
|
|
169
|
+
function isSelectOption(it: unknown): it is SelectOption<unknown> {
|
|
170
|
+
return it != null && typeof it == 'object';
|
|
171
|
+
}
|
|
172
|
+
untrack(async () => {
|
|
173
|
+
try {
|
|
174
|
+
const opts = await options;
|
|
175
|
+
if (def.debug) {
|
|
176
|
+
console.debug(`${def.id}: possible options:`, $state.snapshot(opts));
|
|
177
|
+
}
|
|
178
|
+
const x = opts.filter((it) => {
|
|
179
|
+
if (!isSelectOption(it)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (it.value == undefined) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (typeof it.value == 'string' || typeof it.value == 'number') {
|
|
187
|
+
return (gotValue as unknown as Array<string | number> | undefined)?.includes(
|
|
188
|
+
it.value
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return false;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (def.debug) {
|
|
196
|
+
console.debug(`${def.id}: `, 'selected options:', x);
|
|
197
|
+
}
|
|
198
|
+
selectedOptions = x;
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (dev || def.debug) {
|
|
201
|
+
console.debug(`${def.id}: `, e);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}).catch(devCatch);
|
|
205
|
+
} else if (isNumeric) {
|
|
206
|
+
if (
|
|
207
|
+
selectedNumberFilterType?.value === NumberDateFilterMode.Between &&
|
|
208
|
+
tableContext.paginationRepo?.filtering
|
|
209
|
+
) {
|
|
210
|
+
const gotValue = filteringStateValue;
|
|
211
|
+
if (gotValue != undefined) {
|
|
212
|
+
const [min, max] = gotValue as [number, number | undefined];
|
|
213
|
+
value = min;
|
|
214
|
+
valueMin = min;
|
|
215
|
+
valueMax = max ?? null;
|
|
216
|
+
} else {
|
|
217
|
+
value = valueMin = valueMax = null;
|
|
218
|
+
}
|
|
219
|
+
} else if (tableContext.paginationRepo?.filtering) {
|
|
220
|
+
const gotValue = filteringStateValue;
|
|
221
|
+
|
|
222
|
+
if (gotValue != undefined) {
|
|
223
|
+
const [min] = gotValue as [number, number | undefined];
|
|
224
|
+
value = min;
|
|
225
|
+
valueMin = min;
|
|
226
|
+
valueMax = min;
|
|
227
|
+
} else {
|
|
228
|
+
value = valueMin = valueMax = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else if (tableContext.paginationRepo?.filtering) {
|
|
232
|
+
const gotValue = filteringStateValue;
|
|
233
|
+
if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
|
|
234
|
+
assertNotPromise(gotValue);
|
|
235
|
+
value = gotValue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Dev-only invariant: a Promise must never reach the synchronous primitive
|
|
242
|
+
* sink (we should always have awaited it upstream). Throws in dev to make
|
|
243
|
+
* any regression loud; no-ops in production builds.
|
|
244
|
+
*/
|
|
245
|
+
function assertNotPromise(gotValue: unknown) {
|
|
246
|
+
if (dev && gotValue != null && typeof gotValue == 'object' && gotValue instanceof Promise) {
|
|
247
|
+
throw new Error(`Unexpected Promise`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
onMount(() => {
|
|
251
|
+
const filterOption =
|
|
252
|
+
tableContext.paginationRepo?.filtering &&
|
|
253
|
+
tableContext.paginationRepo.filtering.find((it) => it.id === def.id);
|
|
254
|
+
|
|
255
|
+
if (filterOption) {
|
|
256
|
+
if (isNumericArray(filterOption.value)) {
|
|
257
|
+
[valueMin, valueMax] = filterOption.value;
|
|
258
|
+
if (valueMax == undefined) {
|
|
259
|
+
valueMax = valueMin;
|
|
260
|
+
value = valueMin?.toString();
|
|
261
|
+
} else if (valueMin == undefined) {
|
|
262
|
+
valueMin = valueMax;
|
|
263
|
+
value = valueMax.toString();
|
|
264
|
+
} else {
|
|
265
|
+
value = valueMin.toString();
|
|
266
|
+
}
|
|
267
|
+
} else if (!hasQuery(customFilter)) {
|
|
268
|
+
const gotValue = filterOption.value;
|
|
269
|
+
if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
|
|
270
|
+
assertNotPromise(gotValue);
|
|
271
|
+
value = gotValue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
function closeOnCtrlF(e: KeyboardEvent) {
|
|
278
|
+
if (e.key.toLocaleUpperCase() === 'F' && e.ctrlKey) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
e.stopPropagation();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let showFromDateModal = $state(false);
|
|
285
|
+
let showToDateModal = $state(false);
|
|
286
|
+
|
|
287
|
+
function generateOnChangeObject(_label: string) {
|
|
288
|
+
return (newValue: SelectOption<unknown> | null) => {
|
|
289
|
+
tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
|
|
290
|
+
tableContext.queryValues.set(def, newValue);
|
|
291
|
+
assertNotPromise(newValue);
|
|
292
|
+
value = newValue;
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function generateOnChangePrimitive(_label: string) {
|
|
297
|
+
// AsPrimitive QuerySelect hands us the primitive value directly.
|
|
298
|
+
return (newValue: unknown) => {
|
|
299
|
+
tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
|
|
300
|
+
tableContext.queryValues.set(def, newValue ?? null);
|
|
301
|
+
assertNotPromise(newValue);
|
|
302
|
+
value = newValue ?? null;
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let isNullFilter = $derived(selectedNumberFilterType?.value == NumberDateFilterMode.null);
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Debounce window for typed filter inputs. For a 10k-row table each
|
|
310
|
+
* keystroke would otherwise kick off a full filter pass; coalescing
|
|
311
|
+
* adjacent keystrokes within `FILTER_DEBOUNCE_MS` cuts the cost to
|
|
312
|
+
* roughly one filter pass per pause-in-typing.
|
|
313
|
+
*/
|
|
314
|
+
const FILTER_DEBOUNCE_MS = 300;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Sequence counter used to discard the result of a stale debounce
|
|
318
|
+
* timer. When the user types again before the timer fires, the
|
|
319
|
+
* `pendingSeq` increments; only the timer whose captured `seq`
|
|
320
|
+
* matches `pendingSeq` is allowed to call `filterBy`.
|
|
321
|
+
*/
|
|
322
|
+
let pendingSeq = 0;
|
|
323
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
324
|
+
|
|
325
|
+
function debouncedFilterBy(value: unknown): void {
|
|
326
|
+
pendingSeq += 1;
|
|
327
|
+
const seq = pendingSeq;
|
|
328
|
+
if (debounceTimer !== undefined) {
|
|
329
|
+
clearTimeout(debounceTimer);
|
|
330
|
+
}
|
|
331
|
+
debounceTimer = setTimeout(() => {
|
|
332
|
+
debounceTimer = undefined;
|
|
333
|
+
if (seq !== pendingSeq) {
|
|
334
|
+
// A newer keystroke superseded this one before the timer
|
|
335
|
+
// fired; the newer timer will handle the filter call.
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
tableContext.paginationRepo?.filterBy(def.id, value);
|
|
339
|
+
}, FILTER_DEBOUNCE_MS);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
onDestroy(() => {
|
|
343
|
+
if (debounceTimer !== undefined) {
|
|
344
|
+
clearTimeout(debounceTimer);
|
|
345
|
+
debounceTimer = undefined;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
</script>
|
|
349
|
+
|
|
350
|
+
<div class="input-cell {inputId}">
|
|
351
|
+
{#if isSearchable}
|
|
352
|
+
<div class="input-wrapper-parent">
|
|
353
|
+
{#if isDate}
|
|
354
|
+
{@const dateValues = tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)
|
|
355
|
+
?.value ?? [null, null]}
|
|
356
|
+
{#if isDateTuple(dateValues)}
|
|
357
|
+
{@const [fromDate, toDate] = dateValues}
|
|
358
|
+
<div class="spaced-around">
|
|
359
|
+
<Button
|
|
360
|
+
size={Size.FillHeight}
|
|
361
|
+
fullWidth
|
|
362
|
+
onclick={() => {
|
|
363
|
+
showFromDateModal = true;
|
|
364
|
+
}}>From {fromDate?.toLocaleDateString() ?? 'Any Date'}</Button
|
|
365
|
+
>
|
|
366
|
+
|
|
367
|
+
<Button
|
|
368
|
+
size={Size.FillHeight}
|
|
369
|
+
fullWidth
|
|
370
|
+
onclick={() => {
|
|
371
|
+
showToDateModal = true;
|
|
372
|
+
}}>To {toDate?.toLocaleDateString() ?? 'Any Date'}</Button
|
|
373
|
+
>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
{#if showFromDateModal}
|
|
377
|
+
<DateModal
|
|
378
|
+
header="Edit Date From"
|
|
379
|
+
value={fromDate}
|
|
380
|
+
cancelEditing={() => {
|
|
381
|
+
showFromDateModal = false;
|
|
382
|
+
}}
|
|
383
|
+
confirm={(e) => {
|
|
384
|
+
tableContext.paginationRepo?.filterBy(def.id, [e, toDate]);
|
|
385
|
+
showFromDateModal = false;
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
388
|
+
{/if}
|
|
389
|
+
|
|
390
|
+
{#if showToDateModal}
|
|
391
|
+
<DateModal
|
|
392
|
+
inclusive
|
|
393
|
+
header="Edit Date To"
|
|
394
|
+
value={toDate}
|
|
395
|
+
cancelEditing={() => {
|
|
396
|
+
showToDateModal = false;
|
|
397
|
+
}}
|
|
398
|
+
confirm={(e) => {
|
|
399
|
+
tableContext.paginationRepo?.filterBy(def.id, [fromDate, e]);
|
|
400
|
+
showToDateModal = false;
|
|
401
|
+
}}
|
|
402
|
+
/>
|
|
403
|
+
{/if}
|
|
404
|
+
{/if}
|
|
405
|
+
{:else if isSelect}
|
|
406
|
+
{#if hasQuery(customFilter)}
|
|
407
|
+
{#if customFilter.valueAsObject !== true}
|
|
408
|
+
{@const label = customFilter.id}
|
|
409
|
+
|
|
410
|
+
<!-- TODO: multiselect -->
|
|
411
|
+
|
|
412
|
+
<!-- FIXME: Most likely Unsafe -->
|
|
413
|
+
<QuerySelect
|
|
414
|
+
valueAsObject={false}
|
|
415
|
+
clearable
|
|
416
|
+
name={customFilter.objectName ?? 'Item'}
|
|
417
|
+
valueProp="value"
|
|
418
|
+
labelProp="label"
|
|
419
|
+
value={value as null}
|
|
420
|
+
query={customFilter.query}
|
|
421
|
+
bind:options={lastQueryOptions}
|
|
422
|
+
onchange={generateOnChangePrimitive(label)}
|
|
423
|
+
/>
|
|
424
|
+
{:else}
|
|
425
|
+
{@const label = customFilter.id}
|
|
426
|
+
|
|
427
|
+
<!-- TODO: multiselect -->
|
|
428
|
+
|
|
429
|
+
<QuerySelect
|
|
430
|
+
valueAsObject={true}
|
|
431
|
+
clearable
|
|
432
|
+
name={customFilter.objectName ?? 'Item'}
|
|
433
|
+
valueProp="value"
|
|
434
|
+
labelProp="label"
|
|
435
|
+
value={value as null}
|
|
436
|
+
query={customFilter.query}
|
|
437
|
+
bind:options={lastQueryOptions}
|
|
438
|
+
onchange={generateOnChangeObject(label)}
|
|
439
|
+
/>
|
|
440
|
+
{/if}
|
|
441
|
+
{:else}
|
|
442
|
+
{#await options then options}
|
|
443
|
+
<MultiSelect
|
|
444
|
+
labelProp="label"
|
|
445
|
+
valueProp="value"
|
|
446
|
+
{options}
|
|
447
|
+
valueAsObject
|
|
448
|
+
onchange={(e: Array<SelectOption<unknown>>) => {
|
|
449
|
+
tableContext.paginationRepo?.filterBy(
|
|
450
|
+
def.id,
|
|
451
|
+
e.map((it) => it.value)
|
|
452
|
+
);
|
|
453
|
+
}}
|
|
454
|
+
selectedValues={selectedOptions}
|
|
455
|
+
collapseSelection
|
|
456
|
+
/>
|
|
457
|
+
{/await}
|
|
458
|
+
{/if}
|
|
459
|
+
{:else if def instanceof CheckboxDef}
|
|
460
|
+
{@const value = (() => {
|
|
461
|
+
switch (tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value) {
|
|
462
|
+
case true: {
|
|
463
|
+
return Ternary.True;
|
|
464
|
+
}
|
|
465
|
+
case false: {
|
|
466
|
+
return Ternary.False;
|
|
467
|
+
}
|
|
468
|
+
default: {
|
|
469
|
+
return Ternary.Neutral;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
})()}
|
|
473
|
+
<div class="centered">
|
|
474
|
+
<ThreeStateRadio
|
|
475
|
+
{value}
|
|
476
|
+
yesLabel="Selected"
|
|
477
|
+
noLabel="Not Selected"
|
|
478
|
+
neutralLabel="Any"
|
|
479
|
+
neutralIcon={{ name: 'circle-minus' }}
|
|
480
|
+
onchange={(v) => {
|
|
481
|
+
switch (v) {
|
|
482
|
+
case Ternary.False: {
|
|
483
|
+
tableContext.paginationRepo?.filterBy(def.id, false);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
case Ternary.True: {
|
|
487
|
+
tableContext.paginationRepo?.filterBy(def.id, true);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
case Ternary.Neutral:
|
|
491
|
+
default: {
|
|
492
|
+
tableContext.paginationRepo?.filterBy(def.id, null);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}}
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
499
|
+
{:else if isNumeric}
|
|
500
|
+
{#if selectedNumberFilterType?.value === NumberDateFilterMode.Between}
|
|
501
|
+
<div class="input-wrapper left">
|
|
502
|
+
<NumberInput
|
|
503
|
+
data-type="quicksearch"
|
|
504
|
+
id={inputId}
|
|
505
|
+
bind:value={valueMin}
|
|
506
|
+
variant={InputVariant.Table}
|
|
507
|
+
placeholder="Min…"
|
|
508
|
+
oninput={() => {
|
|
509
|
+
debouncedFilterBy([valueMin, valueMax]);
|
|
510
|
+
}}
|
|
511
|
+
onkeydown={closeOnCtrlF}
|
|
512
|
+
/>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="input-wrapper right">
|
|
515
|
+
<NumberInput
|
|
516
|
+
data-type="quicksearch"
|
|
517
|
+
id={inputId}
|
|
518
|
+
bind:value={valueMax}
|
|
519
|
+
placeholder="Max…"
|
|
520
|
+
variant={InputVariant.Table}
|
|
521
|
+
oninput={() => {
|
|
522
|
+
debouncedFilterBy([valueMin, valueMax]);
|
|
523
|
+
}}
|
|
524
|
+
onkeydown={closeOnCtrlF}
|
|
525
|
+
/>
|
|
526
|
+
</div>
|
|
527
|
+
{:else}
|
|
528
|
+
<div class="input-wrapper right">
|
|
529
|
+
{#await isNullFilter}
|
|
530
|
+
loading
|
|
531
|
+
{:then isNullFilter}
|
|
532
|
+
{#if isNullFilter}
|
|
533
|
+
<TextInput
|
|
534
|
+
data-type="quicksearch"
|
|
535
|
+
id={inputId}
|
|
536
|
+
value="is Null"
|
|
537
|
+
disabled
|
|
538
|
+
variant={InputVariant.Table}
|
|
539
|
+
/>
|
|
540
|
+
{:else}
|
|
541
|
+
<!-- NOTE: This is a text input in order to support the __in functionality, which is a comma separated list of numbers -->
|
|
542
|
+
<TextInput
|
|
543
|
+
data-type="quicksearch"
|
|
544
|
+
id={inputId}
|
|
545
|
+
bind:value={value as string | null}
|
|
546
|
+
placeholder="Filter…"
|
|
547
|
+
variant={InputVariant.Table}
|
|
548
|
+
oninput={() => {
|
|
549
|
+
debouncedFilterBy([value || null]);
|
|
550
|
+
}}
|
|
551
|
+
onkeydown={closeOnCtrlF}
|
|
552
|
+
/>
|
|
553
|
+
{/if}
|
|
554
|
+
{:catch e}
|
|
555
|
+
<!-- eslint-disable-next-line svelte/no-at-debug-tags -->
|
|
556
|
+
{@debug e}
|
|
557
|
+
{/await}
|
|
558
|
+
</div>
|
|
559
|
+
{/if}
|
|
560
|
+
{:else if value == null || typeof value == 'string'}
|
|
561
|
+
<div class="input-wrapper right">
|
|
562
|
+
{#if selectedStringFilterType?.value == StringFilterMode.null}
|
|
563
|
+
<TextInput
|
|
564
|
+
data-type="quicksearch"
|
|
565
|
+
id={inputId}
|
|
566
|
+
value="is Null"
|
|
567
|
+
disabled
|
|
568
|
+
variant={InputVariant.Table}
|
|
569
|
+
/>
|
|
570
|
+
{:else if selectedStringFilterType?.value == StringFilterMode.Empty}
|
|
571
|
+
<TextInput
|
|
572
|
+
data-type="quicksearch"
|
|
573
|
+
id={inputId}
|
|
574
|
+
value="is Empty"
|
|
575
|
+
disabled
|
|
576
|
+
variant={InputVariant.Table}
|
|
577
|
+
/>
|
|
578
|
+
{:else}
|
|
579
|
+
<TextInput
|
|
580
|
+
data-type="quicksearch"
|
|
581
|
+
id={inputId}
|
|
582
|
+
oninput={() => {
|
|
583
|
+
debouncedFilterBy(value);
|
|
584
|
+
}}
|
|
585
|
+
variant={InputVariant.Table}
|
|
586
|
+
bind:value={value as string | null}
|
|
587
|
+
placeholder="Filter…"
|
|
588
|
+
/>
|
|
589
|
+
{/if}
|
|
590
|
+
</div>
|
|
591
|
+
{/if}
|
|
592
|
+
|
|
593
|
+
<!-- TODO button for date, checkbox or select for checkbox -->
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
{#if !isSelect && !isDate && !(def instanceof CheckboxDef)}
|
|
597
|
+
<div class="stick-right">
|
|
598
|
+
<Action
|
|
599
|
+
data-type="quicksearch"
|
|
600
|
+
size={Size.FillHeight}
|
|
601
|
+
colourSet={ColourSet.Auxiliary}
|
|
602
|
+
icon={{
|
|
603
|
+
name: 'filter-2',
|
|
604
|
+
size: Size.MediumLarge,
|
|
605
|
+
colour: Colour['$ui-blue-5'],
|
|
606
|
+
}}
|
|
607
|
+
onclick={() => {
|
|
608
|
+
if (!openedSelect) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (openedSelect.value === selectSymbol) {
|
|
612
|
+
openedSelect.value = null;
|
|
613
|
+
} else {
|
|
614
|
+
openedSelect.value = selectSymbol;
|
|
615
|
+
}
|
|
616
|
+
}}
|
|
617
|
+
/>
|
|
618
|
+
</div>
|
|
619
|
+
{/if}
|
|
620
|
+
{/if}
|
|
621
|
+
|
|
622
|
+
{#if isSelectOpen}
|
|
623
|
+
<div
|
|
624
|
+
class="floating"
|
|
625
|
+
transition:fade={{ easing: cubicOut, duration: 100 }}
|
|
626
|
+
>
|
|
627
|
+
<div class="select-wrapper">
|
|
628
|
+
{#if isNumeric}
|
|
629
|
+
<SingleSelect
|
|
630
|
+
options={numberDateFilterTypes}
|
|
631
|
+
labelProp="label"
|
|
632
|
+
valueProp="value"
|
|
633
|
+
name="Filter Type"
|
|
634
|
+
valueAsObject
|
|
635
|
+
value={selectedNumberFilterType}
|
|
636
|
+
onchange={(newValue: SelectOption<NumberDateFilterMode> | null) => {
|
|
637
|
+
const oldFilterType = selectedNumberFilterType;
|
|
638
|
+
|
|
639
|
+
const newFilterType = newValue?.value ?? NumberDateFilterMode.In;
|
|
640
|
+
|
|
641
|
+
if (newFilterType !== oldFilterType?.value) {
|
|
642
|
+
tableContext.paginationRepo?.setFilterMode(def.id, newFilterType);
|
|
643
|
+
|
|
644
|
+
if (newFilterType === NumberDateFilterMode.Between) {
|
|
645
|
+
tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMin]);
|
|
646
|
+
} else if (newFilterType === NumberDateFilterMode.In) {
|
|
647
|
+
tableContext.paginationRepo?.filterBy(def.id, [value]);
|
|
648
|
+
} else {
|
|
649
|
+
tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMax]);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}}
|
|
653
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
654
|
+
e.stopPropagation();
|
|
655
|
+
}}
|
|
656
|
+
/>
|
|
657
|
+
{:else}
|
|
658
|
+
<SingleSelect
|
|
659
|
+
options={stringFilterTypes}
|
|
660
|
+
labelProp="label"
|
|
661
|
+
valueProp="value"
|
|
662
|
+
name="Filter Type"
|
|
663
|
+
valueAsObject
|
|
664
|
+
value={selectedStringFilterType}
|
|
665
|
+
onchange={(newValue: SelectOption<StringFilterMode> | null) => {
|
|
666
|
+
const filterType = newValue?.value ?? StringFilterMode.Contains;
|
|
667
|
+
tableContext.paginationRepo?.setFilterMode(def.id, filterType);
|
|
668
|
+
}}
|
|
669
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
670
|
+
e.stopPropagation();
|
|
671
|
+
}}
|
|
672
|
+
/>
|
|
673
|
+
{/if}
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
{/if}
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
679
|
<style>.focusable,
|
|
680
680
|
.checkbox-cell,
|
|
681
681
|
.input-cell,
|
|
@@ -815,4 +815,4 @@ div.input-wrapper-parent > :global(.select-wrapper) {
|
|
|
815
815
|
height: 60px;
|
|
816
816
|
--input-height: 60px;
|
|
817
817
|
background-color: #ffffff;
|
|
818
|
-
}</style>
|
|
818
|
+
}</style>
|