@lavalogic/scoria 0.37.44 → 0.37.45

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 (237) 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 +83 -83
  22. package/dist/Components/Contexts/ToastContainer.svelte +65 -65
  23. package/dist/Components/Contexts/Tooltip.svelte +37 -37
  24. package/dist/Components/DataMatrixIcon.svelte +112 -112
  25. package/dist/Components/DataMatrixIconProps.d.ts +3 -13
  26. package/dist/Components/DateInput.svelte +148 -148
  27. package/dist/Components/DatePicker.svelte +300 -300
  28. package/dist/Components/DesktopModal.svelte +90 -90
  29. package/dist/Components/Dial.svelte +204 -204
  30. package/dist/Components/DialProps.d.ts +1 -18
  31. package/dist/Components/DimensionInput.svelte +170 -170
  32. package/dist/Components/DragMenu/DragMenuHolder.svelte +119 -119
  33. package/dist/Components/DragMenu/DraggableCard.svelte +77 -77
  34. package/dist/Components/FileCard.svelte +24 -24
  35. package/dist/Components/FileUpload.svelte +236 -236
  36. package/dist/Components/GridInput.svelte +196 -200
  37. package/dist/Components/GridInputSet.svelte +29 -29
  38. package/dist/Components/HorizontalTabGroup.svelte +82 -82
  39. package/dist/Components/HorizontalTabGroupButton.svelte +44 -44
  40. package/dist/Components/Icon.svelte +128 -128
  41. package/dist/Components/IconProps.d.ts +2 -7
  42. package/dist/Components/Icons.js +357 -357
  43. package/dist/Components/LoadingAnimation.svelte +29 -29
  44. package/dist/Components/LoadingModal.svelte +45 -45
  45. package/dist/Components/LoadingOverlay.svelte +25 -25
  46. package/dist/Components/LoadingOverlayProps.d.ts +3 -7
  47. package/dist/Components/Modal.svelte +258 -258
  48. package/dist/Components/ModalProps.d.ts +0 -3
  49. package/dist/Components/MultiSelect.svelte +220 -220
  50. package/dist/Components/MultiSelectCommonProps.d.ts +6 -17
  51. package/dist/Components/Nav/Nav.svelte +99 -99
  52. package/dist/Components/Nav/NavProps.d.ts +4 -11
  53. package/dist/Components/Nav/NavServiceMenuList.svelte +43 -43
  54. package/dist/Components/Nav/NavTab.svelte +62 -62
  55. package/dist/Components/Nav/ServiceMenuItem.svelte +100 -100
  56. package/dist/Components/Nav/ServicesNav.svelte +80 -80
  57. package/dist/Components/NumberInput.svelte +157 -157
  58. package/dist/Components/PageHeading.svelte +25 -25
  59. package/dist/Components/PasswordInput.svelte +88 -88
  60. package/dist/Components/Portal.svelte +36 -36
  61. package/dist/Components/ProgressBubble.svelte +107 -108
  62. package/dist/Components/ProgressBubbleProps.d.ts +3 -3
  63. package/dist/Components/QuerySelect.svelte +366 -366
  64. package/dist/Components/QuerySelectProps.d.ts +4 -14
  65. package/dist/Components/SingleSelect.svelte +250 -250
  66. package/dist/Components/SingleSelectCommonProps.d.ts +3 -19
  67. package/dist/Components/StepButton.svelte +70 -70
  68. package/dist/Components/StepForm.svelte +140 -140
  69. package/dist/Components/Switch.svelte +69 -69
  70. package/dist/Components/Table/ARCHITECTURE.md +298 -0
  71. package/dist/Components/Table/Body/QuickSearchCell.svelte +679 -694
  72. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +51 -51
  73. package/dist/Components/Table/Body/Rows/ColumnList.svelte +59 -59
  74. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +456 -478
  75. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +79 -79
  76. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +185 -188
  77. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +124 -124
  78. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +120 -120
  79. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +230 -230
  80. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +105 -105
  81. package/dist/Components/Table/Body/Rows/Columns/SelectColumn.d.ts +12 -9
  82. package/dist/Components/Table/Body/Rows/Columns/SelectTableCell.d.ts +5 -5
  83. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +87 -87
  84. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +126 -126
  85. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +159 -159
  86. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +246 -246
  87. package/dist/Components/Table/Body/Rows/Columns/TableDatePickerProps.d.ts +4 -4
  88. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +216 -216
  89. package/dist/Components/Table/Body/Rows/Columns/TableQueryCell.d.ts +6 -5
  90. package/dist/Components/Table/Body/Rows/Columns/TableQueryColumn.d.ts +7 -6
  91. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +252 -257
  92. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +179 -179
  93. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +221 -229
  94. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte.d.ts +11 -11
  95. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +197 -197
  96. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +190 -190
  97. package/dist/Components/Table/Body/Rows/TableRow.svelte +290 -290
  98. package/dist/Components/Table/Body/TableBody.svelte +155 -155
  99. package/dist/Components/Table/ColumnDefinitions/ExampleItemColumnDefRepository.svelte.js +2 -3
  100. package/dist/Components/Table/ColumnFactory.svelte.d.ts +9 -11
  101. package/dist/Components/Table/ColumnFactory.svelte.js +101 -118
  102. package/dist/Components/Table/ColumnKindRegistry.d.ts +112 -0
  103. package/dist/Components/Table/ColumnKindRegistry.js +63 -0
  104. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +41 -41
  105. package/dist/Components/Table/Headers/SelectAllHeader.svelte +30 -30
  106. package/dist/Components/Table/Headers/TableHead.svelte +415 -413
  107. package/dist/Components/Table/Misc/ColumnPanel.svelte +94 -94
  108. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +112 -112
  109. package/dist/Components/Table/Misc/FilterInput.svelte +620 -644
  110. package/dist/Components/Table/Misc/FilterInput.svelte.d.ts +12 -12
  111. package/dist/Components/Table/Misc/FilterInputProps.d.ts +2 -2
  112. package/dist/Components/Table/Misc/FilterPanel.svelte +257 -257
  113. package/dist/Components/Table/Misc/FilterPanelModal.svelte +30 -30
  114. package/dist/Components/Table/Misc/SidePanel.svelte +35 -35
  115. package/dist/Components/Table/Misc/TableFooter.svelte +311 -311
  116. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +125 -125
  117. package/dist/Components/Table/Misc/TableSidebar.svelte +157 -157
  118. package/dist/Components/Table/Misc/ToolboxPanel.svelte +100 -100
  119. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +107 -107
  120. package/dist/Components/Table/NestedTable.svelte +130 -133
  121. package/dist/Components/Table/SubApis.svelte.d.ts +1 -5
  122. package/dist/Components/Table/SubApis.svelte.js +33 -32
  123. package/dist/Components/Table/Table.svelte +692 -745
  124. package/dist/Components/Table/TableHelpers.svelte.d.ts +6 -19
  125. package/dist/Components/Table/TableHelpers.svelte.js +6 -14
  126. package/dist/Components/Table/Types/Columns/Column.d.ts +2 -2
  127. package/dist/Components/Table/Types/Columns/DateColumn.d.ts +3 -3
  128. package/dist/Components/Table/Types/Columns/DateTableCell.d.ts +7 -7
  129. package/dist/Components/Table/Types/Columns/Definitions/Accessors/AccessorDef.svelte.d.ts +7 -8
  130. package/dist/Components/Table/Types/Columns/Definitions/Accessors/AccessorDef.svelte.js +4 -5
  131. package/dist/Components/Table/Types/Columns/Definitions/Accessors/CheckboxDef.svelte.d.ts +11 -14
  132. package/dist/Components/Table/Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js +3 -4
  133. package/dist/Components/Table/Types/Columns/Definitions/Accessors/DateInputDef.svelte.d.ts +7 -11
  134. package/dist/Components/Table/Types/Columns/Definitions/Accessors/DateInputDef.svelte.js +3 -3
  135. package/dist/Components/Table/Types/Columns/Definitions/Accessors/NumberInputDef.svelte.d.ts +13 -18
  136. package/dist/Components/Table/Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js +4 -5
  137. package/dist/Components/Table/Types/Columns/Definitions/Accessors/QueryDef.svelte.d.ts +5 -5
  138. package/dist/Components/Table/Types/Columns/Definitions/Accessors/QueryDef.svelte.js +2 -2
  139. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.d.ts +15 -12
  140. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.js +4 -5
  141. package/dist/Components/Table/Types/Columns/Definitions/Accessors/TextInputDef.svelte.d.ts +8 -12
  142. package/dist/Components/Table/Types/Columns/Definitions/Accessors/TextInputDef.svelte.js +3 -3
  143. package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.d.ts +6 -6
  144. package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.js +3 -3
  145. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.d.ts +49 -16
  146. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.js +27 -5
  147. package/dist/Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.d.ts +7 -7
  148. package/dist/Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js +4 -4
  149. package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.d.ts +5 -5
  150. package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.js +2 -2
  151. package/dist/Components/Table/Types/Columns/Definitions/Display/DisplayDef.svelte.d.ts +5 -5
  152. package/dist/Components/Table/Types/Columns/Definitions/Display/DisplayDef.svelte.js +2 -2
  153. package/dist/Components/Table/Types/Columns/Definitions/Display/ProgressBarDef.svelte.d.ts +5 -5
  154. package/dist/Components/Table/Types/Columns/Definitions/Display/ProgressBarDef.svelte.js +2 -2
  155. package/dist/Components/Table/Types/Columns/Definitions/Display/ValidityDef.svelte.d.ts +5 -5
  156. package/dist/Components/Table/Types/Columns/Definitions/Display/ValidityDef.svelte.js +2 -2
  157. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.d.ts +11 -19
  158. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.js +4 -8
  159. package/dist/Components/Table/Types/Columns/Definitions/FilterOnly/FilterOnlyDef.svelte.d.ts +6 -7
  160. package/dist/Components/Table/Types/Columns/Definitions/FilterOnly/FilterOnlyDef.svelte.js +3 -4
  161. package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.d.ts +9 -9
  162. package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.js +4 -4
  163. package/dist/Components/Table/Types/Columns/ExpandTableCell.d.ts +5 -5
  164. package/dist/Components/Table/Types/Columns/ProgressBarTableCell.d.ts +6 -6
  165. package/dist/Components/Table/Types/Columns/TableCell.d.ts +7 -6
  166. package/dist/Components/Table/Types/Context/ClipboardState.svelte.d.ts +98 -0
  167. package/dist/Components/Table/Types/Context/ClipboardState.svelte.js +290 -0
  168. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +17 -45
  169. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +17 -45
  170. package/dist/Components/Table/Types/Context/FilteringState.svelte.d.ts +9 -20
  171. package/dist/Components/Table/Types/Context/FilteringState.svelte.js +9 -20
  172. package/dist/Components/Table/Types/Context/FocusState.svelte.d.ts +23 -42
  173. package/dist/Components/Table/Types/Context/FocusState.svelte.js +32 -59
  174. package/dist/Components/Table/Types/Context/ITable.d.ts +5 -8
  175. package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +29 -56
  176. package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +28 -55
  177. package/dist/Components/Table/Types/Context/QueryValuesState.svelte.d.ts +50 -0
  178. package/dist/Components/Table/Types/Context/QueryValuesState.svelte.js +62 -0
  179. package/dist/Components/Table/Types/Context/SelectionState.svelte.d.ts +17 -28
  180. package/dist/Components/Table/Types/Context/SelectionState.svelte.js +17 -28
  181. package/dist/Components/Table/Types/Context/SortingState.svelte.d.ts +15 -29
  182. package/dist/Components/Table/Types/Context/SortingState.svelte.js +17 -31
  183. package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +212 -325
  184. package/dist/Components/Table/Types/Context/TableContext.svelte.js +292 -706
  185. package/dist/Components/Table/Types/Context/VirtualisationState.svelte.d.ts +62 -79
  186. package/dist/Components/Table/Types/Context/VirtualisationState.svelte.js +54 -73
  187. package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.d.ts +16 -27
  188. package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.js +16 -27
  189. package/dist/Components/Table/Types/DataRepository/IDataRepository.d.ts +12 -15
  190. package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.d.ts +15 -30
  191. package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.js +16 -28
  192. package/dist/Components/Table/Types/Header.d.ts +4 -4
  193. package/dist/Components/Table/Types/HeaderGroup.d.ts +5 -5
  194. package/dist/Components/Table/Types/Public/index.d.ts +2 -4
  195. package/dist/Components/Table/Types/Public/index.js +2 -4
  196. package/dist/Components/Table/clipboardEscape.d.ts +32 -0
  197. package/dist/Components/Table/clipboardEscape.js +45 -0
  198. package/dist/Components/Table/createTable.svelte.d.ts +8 -18
  199. package/dist/Components/Table/createTable.svelte.js +19 -47
  200. package/dist/Components/Test/TestContextConsumer.svelte +13 -13
  201. package/dist/Components/TextArea.svelte +130 -130
  202. package/dist/Components/TextInput.svelte +176 -176
  203. package/dist/Components/ThreeStateRadio.svelte +131 -131
  204. package/dist/Components/Touch/BoolCard.svelte +45 -45
  205. package/dist/Components/Touch/DateModal.svelte +209 -209
  206. package/dist/Components/Touch/EditCard.svelte +59 -59
  207. package/dist/Components/Touch/InfoCard.svelte +31 -31
  208. package/dist/Components/Touch/InfoTextCard.svelte +28 -28
  209. package/dist/Components/Touch/ModalExplanation.svelte +17 -17
  210. package/dist/Components/Touch/NumberKeyboard.svelte +225 -225
  211. package/dist/Components/Touch/NumberModal.svelte +79 -79
  212. package/dist/Components/Touch/PlusMinusCard.svelte +46 -46
  213. package/dist/Components/Touch/SelectModal.svelte +123 -123
  214. package/dist/Components/Touch/SummaryCard.svelte +93 -93
  215. package/dist/Components/Touch/TextAreaModal.svelte +95 -95
  216. package/dist/Components/Touch/TextModal.svelte +106 -106
  217. package/dist/Components/Touch/TouchCard.svelte +33 -33
  218. package/dist/Components/Touch/TouchModal.svelte +274 -274
  219. package/dist/Components/Touch/UtilityButton.svelte +109 -109
  220. package/dist/Components/VerticalTabGroup.svelte +79 -79
  221. package/dist/Components/VerticalTabGroupButton.svelte +58 -58
  222. package/dist/Contexts/ToastContext.svelte.js +3 -4
  223. package/dist/Helpers/Datamatrix.d.ts +19 -19
  224. package/dist/Helpers/Datamatrix.js +291 -291
  225. package/dist/Helpers/Helpers.svelte.d.ts +2 -4
  226. package/dist/Helpers/Helpers.svelte.js +2 -4
  227. package/dist/Helpers/dropdownPortal.d.ts +26 -41
  228. package/dist/Helpers/dropdownPortal.js +62 -116
  229. package/dist/Types/Examples/ExampleModal.svelte +35 -35
  230. package/dist/index.d.ts +0 -1
  231. package/dist/scss/_basics.scss +12 -12
  232. package/dist/scss/_colours.scss +134 -134
  233. package/dist/scss/_mixins.scss +3348 -3360
  234. package/dist/scss/_motion.scss +57 -57
  235. package/dist/scss/_sizing.scss +85 -85
  236. package/dist/scss/_transitions.scss +8 -8
  237. package/package.json +2 -1
@@ -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>