@lavalogic/scoria 0.37.43 → 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 -686
  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 -635
  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 -747
  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 -36
  228. package/dist/Helpers/dropdownPortal.js +146 -76
  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,637 +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
- tableContext.dataRepo.__queryValues.set(label, newValue ?? null);
301
- value = newValue ?? null;
302
- };
303
- }
304
-
305
- function generateOnChangePrimitive(label: string) {
306
- // Phase 7.6: AsPrimitive QuerySelect now hands us the primitive
307
- // directly. The previous code typed this as `SelectOption<unknown>`
308
- // and read `.value` off it; with the discriminated typing the
309
- // primitive flows straight through.
310
- return (newValue: unknown) => {
311
- tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
312
- tableContext.dataRepo.__queryValues.set(label, newValue ?? null);
313
- value = newValue ?? null;
314
- };
315
- }
316
- </script>
317
-
318
- {#if isSearchable}
319
- <!-- TODO remove this check once datePicker is supported -->
320
- {#if def instanceof DateInputDef || def instanceof CheckboxDef}
321
- <label for={inputId}>{displayLabel}</label>
322
- {/if}
323
- <div class="row">
324
- {#if def instanceof DateInputDef}
325
- {@const dateValues =
326
- tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value ?? []}
327
- {#if isDateTuple(dateValues)}
328
- {@const [fromDate, toDate] = dateValues}
329
- <div class="spaced-around">
330
- <Button
331
- onclick={() => {
332
- showFromDateModal = true;
333
- }}>From {fromDate?.toLocaleDateString() ?? 'Any Date'}</Button
334
- >
335
-
336
- <Button
337
- onclick={() => {
338
- showToDateModal = true;
339
- }}>To {toDate?.toLocaleDateString() ?? 'Any Date'}</Button
340
- >
341
- </div>
342
-
343
- {#if showFromDateModal}
344
- <DateModal
345
- header="Edit Date From"
346
- value={fromDate}
347
- cancelEditing={() => {
348
- showFromDateModal = false;
349
- }}
350
- confirm={(e) => {
351
- tableContext.paginationRepo?.filterBy(def.id, [e, toDate]);
352
- showFromDateModal = false;
353
- }}
354
- />
355
- {/if}
356
-
357
- {#if showToDateModal}
358
- <DateModal
359
- inclusive
360
- header="Edit Date To"
361
- value={toDate}
362
- cancelEditing={() => {
363
- showToDateModal = false;
364
- }}
365
- confirm={(e) => {
366
- tableContext.paginationRepo?.filterBy(def.id, [fromDate, e]);
367
- showToDateModal = false;
368
- }}
369
- />
370
- {/if}
371
- {/if}
372
- {:else if def instanceof SelectDef && !isRemoteSelect}
373
- <div class="maxwidth">
374
- {#await optionsPromise then options}
375
- {@const selectedPrimitives = selectedOptions
376
- .map((it) => it.value)
377
- .filter((v): v is string | number => typeof v === 'string' || typeof v === 'number')}
378
- <MultiSelect
379
- label={customFilter?.id ??
380
- customFilter?.label ??
381
- customFilter?.objectName ??
382
- customFilter?.queryKey ??
383
- def.id}
384
- labelProp="label"
385
- valueProp="value"
386
- name={customFilter?.objectName ?? 'Item'}
387
- {options}
388
- valueAsObject={false}
389
- onchange={(e: Array<unknown>) => {
390
- // Phase 7.6: `valueAsObject={false}` returns
391
- // primitives directly.
392
- tableContext.paginationRepo?.filterBy(def.id, e);
393
- }}
394
- selectedValues={selectedPrimitives}
395
- collapseSelection
396
- />
397
- {:catch e}
398
- <span>Error retrieving options: {e.message}</span>
399
- <Button
400
- onclick={() => {
401
- retryOptionsPromise();
402
- }}>Retry</Button
403
- >
404
- {/await}
405
- </div>
406
- {:else if customFilter && isRemoteSelect}
407
- <div class="contents">
408
- {#await options then options}
409
- {#if hasQuery(customFilter)}
410
- {#if customFilter.valueAsObject !== true}
411
- {@const label = displayLabel}
412
-
413
- <!-- TODO: multiselect -->
414
-
415
- <QuerySelect
416
- valueAsObject={false}
417
- {label}
418
- clearable
419
- name={customFilter.objectName ?? 'Item'}
420
- valueProp={'value' as never}
421
- labelProp={'label' as never}
422
- value={value as never}
423
- query={customFilter.query}
424
- bind:options={lastQueryOptions as never}
425
- onchange={generateOnChangePrimitive(label)}
426
- />
427
- {:else}
428
- {@const label = displayLabel}
429
-
430
- <!-- TODO: multiselect -->
431
-
432
- <QuerySelect
433
- valueAsObject={true}
434
- {label}
435
- clearable
436
- name={customFilter.objectName ?? 'Item'}
437
- valueProp="value"
438
- labelProp="label"
439
- value={value as null}
440
- query={customFilter.query}
441
- bind:options={lastQueryOptions}
442
- onchange={generateOnChangeObject(label)}
443
- />
444
- {/if}
445
- {:else}
446
- {@const selectedPrimitives = selectedOptions
447
- .map((it) => it.value)
448
- .filter(
449
- (v): v is string | number => typeof v === 'string' || typeof v === 'number'
450
- )}
451
- <!--
452
- MultiSelect with `valueAsObject={false}` matches
453
- options against the primitive value, not the whole
454
- SelectOption shape. Previously the remote-select
455
- branch passed the SelectOption objects directly
456
- (the `selectedOptions` $state), meaning the inner
457
- `selectedKeys` derived saw
458
- `[{label, value}, ...]` rows where it expected
459
- `[id1, id2, ...]`, so no chips ever rendered even
460
- though the filter was active server-side. Pull the
461
- primitive `value` off each option to match the
462
- local-select branch above.
463
- -->
464
- <MultiSelect
465
- label={displayLabel}
466
- labelProp="label"
467
- valueProp="value"
468
- name={customFilter.objectName ?? 'Item'}
469
- {options}
470
- valueAsObject={false}
471
- onchange={(e: Array<unknown>) => {
472
- // Phase 7.6: `valueAsObject={false}` returns
473
- // primitives directly.
474
- tableContext.paginationRepo?.filterBy(def.id, e);
475
- }}
476
- selectedValues={selectedPrimitives}
477
- collapseSelection
478
- />
479
- {/if}
480
- {/await}
481
- </div>
482
- {:else if def instanceof CheckboxDef}
483
- <div class="inner-row">
484
- <label>
485
- <input
486
- value={false}
487
- type="radio"
488
- bind:group={value}
489
- onchange={() => {
490
- tableContext.paginationRepo?.filterBy(def.id, value);
491
- }}
492
- />
493
- No
494
- </label>
495
- <label>
496
- <input
497
- value={undefined}
498
- type="radio"
499
- bind:group={value}
500
- onchange={() => {
501
- tableContext.paginationRepo?.filterBy(def.id, value);
502
- }}
503
- />
504
- Any
505
- </label>
506
- <label>
507
- <input
508
- value={true}
509
- type="radio"
510
- bind:group={value}
511
- onchange={() => {
512
- tableContext.paginationRepo?.filterBy(def.id, value);
513
- }}
514
- />
515
- Yes
516
- </label>
517
- </div>
518
- {:else if isNumeric}
519
- {@const label = typeof def.header === 'string' ? def.header : undefined}
520
- <div class="left">
521
- <SingleSelect
522
- {label}
523
- name="Filter Type"
524
- options={numberDateFilterTypes}
525
- labelProp="label"
526
- valueProp="value"
527
- valueAsObject
528
- value={selectedNumberFilterType}
529
- onchange={(newValue: SelectOption<NumberDateFilterMode> | null) => {
530
- const oldFilterType = selectedNumberFilterType;
531
-
532
- const newFilterType = newValue?.value ?? NumberDateFilterMode.In;
533
- tableContext.paginationRepo?.setFilterMode(def.id, newFilterType);
534
-
535
- if (selectedNumberFilterType?.value !== oldFilterType?.value) {
536
- if (oldFilterType?.value === NumberDateFilterMode.Between) {
537
- valueMax = null;
538
- tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMin]);
539
- }
540
-
541
- if (newFilterType === NumberDateFilterMode.In) {
542
- tableContext.paginationRepo?.filterBy(def.id, [value]);
543
- }
544
-
545
- // TODO what about the other modes?
546
- }
547
- }}
548
- onkeydown={(e: KeyboardEvent) => {
549
- e.stopPropagation();
550
- }}
551
- />
552
- </div>
553
- <div class="right">
554
- {#if selectedNumberFilterType?.value === NumberDateFilterMode.Between}
555
- <NumberInput
556
- id={inputId}
557
- bind:value={valueMin}
558
- oninput={() => {
559
- debouncedFilterBy(def.id, [valueMin, valueMax]);
560
- }}
561
- onkeydown={(e: KeyboardEvent) => {
562
- e.stopPropagation();
563
- }}
564
- />
565
- <span>and</span>
566
- <NumberInput
567
- id={inputId}
568
- bind:value={valueMax}
569
- oninput={() => {
570
- debouncedFilterBy(def.id, [valueMin, valueMax]);
571
- }}
572
- onkeydown={(e: KeyboardEvent) => {
573
- e.stopPropagation();
574
- }}
575
- />
576
- {:else if value == null || typeof value == 'string'}
577
- <TextInput
578
- id={inputId}
579
- bind:value
580
- oninput={() => {
581
- debouncedFilterBy(def.id, [value]);
582
- }}
583
- onkeydown={(e: KeyboardEvent) => {
584
- e.stopPropagation();
585
- }}
586
- />
587
- {:else if dev}
588
- error: {value?.toString()}
589
- <!-- eslint-disable-next-line svelte/no-at-debug-tags -->
590
- {@debug value}
591
- {/if}
592
- </div>
593
- {:else}
594
- {@const label = typeof def.header === 'string' ? def.header : undefined}
595
- <div class="left">
596
- <SingleSelect
597
- name="Filter Type"
598
- valueAsObject
599
- {label}
600
- options={stringFilterTypes}
601
- labelProp="label"
602
- valueProp="value"
603
- value={selectedStringFilterType}
604
- onchange={(newValue: SelectOption<StringFilterMode> | null) => {
605
- const filterType = newValue?.value ?? StringFilterMode.Contains;
606
- tableContext.paginationRepo?.setFilterMode(def.id, filterType);
607
- }}
608
- onkeydown={(e: KeyboardEvent) => {
609
- e.stopPropagation();
610
- }}
611
- />
612
- </div>
613
- <div class="right">
614
- {#if value == null || typeof value == 'string'}
615
- <TextInput
616
- id={inputId}
617
- bind:value
618
- oninput={() => {
619
- debouncedFilterBy(def.id, value);
620
- }}
621
- onkeydown={(e: KeyboardEvent) => {
622
- e.stopPropagation();
623
- }}
624
- />
625
- {:else if dev}
626
- error: {value?.toString()}
627
- <!-- eslint-disable-next-line svelte/no-at-debug-tags -->
628
- {@debug value}
629
- {/if}
630
- </div>
631
- {/if}
632
- </div>
633
- {/if}
634
-
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
+
635
620
  <style>.contents {
636
621
  display: contents;
637
622
  --input-width: 100%;
@@ -702,4 +687,4 @@
702
687
  .inner-row label input {
703
688
  cursor: pointer;
704
689
  width: 2rem;
705
- }</style>
690
+ }</style>