@lavalogic/scoria 0.37.44 → 0.37.46

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