@olenbetong/appframe-ds 1.3.6 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +6 -6
- package/scripts/copyAssets.mjs +27 -27
- package/src/AfLookup/Lookup.css +130 -129
- package/src/AfLookup/Lookup.tsx +186 -186
- package/src/AfLookup/LookupCombobox.css +72 -78
- package/src/AfLookup/LookupCombobox.tsx +237 -237
- package/src/AfLookup/LookupEdit.tsx +130 -130
- package/src/AfLookup/LookupGrid.css +7 -12
- package/src/AfLookup/LookupGrid.tsx +124 -124
- package/src/AfLookup/index.ts +5 -5
- package/src/autocomplete/Autocomplete.css +256 -256
- package/src/autocomplete/Autocomplete.tsx +320 -320
- package/src/autocomplete/AutocompleteFrame.tsx +168 -168
- package/src/autocomplete/Combobox.tsx +286 -286
- package/src/autocomplete/index.ts +4 -4
- package/src/autocomplete/types.ts +204 -204
- package/src/autocomplete/utils.ts +183 -183
- package/src/binding/BoundNumericTextField.tsx +102 -102
- package/src/binding/BoundTextField.tsx +49 -49
- package/src/binding/CancelButton.tsx +30 -30
- package/src/binding/CancelIconButton.tsx +30 -30
- package/src/binding/DataEditToolbar.css +23 -23
- package/src/binding/DataEditToolbar.tsx +84 -84
- package/src/binding/DeleteButton.tsx +35 -35
- package/src/binding/DeleteIconButton.tsx +35 -35
- package/src/binding/RefreshButton.tsx +31 -31
- package/src/binding/RefreshIconButton.tsx +33 -33
- package/src/binding/RefreshRowIconButton.tsx +33 -33
- package/src/binding/SaveButton.tsx +37 -37
- package/src/binding/SaveIconButton.tsx +38 -38
- package/src/container/Container.tsx +50 -49
- package/src/container/index.ts +1 -1
- package/src/cutoff/CutOffPicker.css +12 -12
- package/src/cutoff/CutOffPicker.tsx +165 -165
- package/src/cutoff/context.tsx +194 -194
- package/src/cutoff/dsCutOffDates.ts +21 -21
- package/src/cutoff/index.ts +4 -4
- package/src/cutoff/utils.test.ts +59 -59
- package/src/cutoff/utils.ts +276 -276
- package/src/filter/ChipListInput.tsx +79 -79
- package/src/filter/FieldFilterPanel.css +150 -150
- package/src/filter/FieldFilterPanel.tsx +309 -309
- package/src/filter/FilterBuilder.css +152 -152
- package/src/filter/FilterBuilder.test.tsx +153 -153
- package/src/filter/FilterBuilder.tsx +435 -435
- package/src/filter/FilterEditor.css +231 -231
- package/src/filter/FilterEditor.test.tsx +285 -285
- package/src/filter/FilterEditor.tsx +75 -75
- package/src/filter/FilterGroupEditor.tsx +160 -160
- package/src/filter/FilterNameDialog.css +22 -22
- package/src/filter/FilterNameDialog.tsx +81 -81
- package/src/filter/FilterRow.tsx +117 -117
- package/src/filter/FilterShareDialog.css +28 -28
- package/src/filter/FilterShareDialog.tsx +201 -201
- package/src/filter/FilterStringField.tsx +78 -78
- package/src/filter/FilterValueEditor.tsx +239 -239
- package/src/filter/SavedFilterTree.css +107 -107
- package/src/filter/SavedFilterTree.test.tsx +94 -94
- package/src/filter/SavedFilterTree.tsx +218 -218
- package/src/filter/fieldFilter.test.ts +158 -158
- package/src/filter/fieldFilter.ts +139 -139
- package/src/filter/index.ts +27 -27
- package/src/filter/types.ts +38 -38
- package/src/filter/useDistinctValues.test.ts +102 -102
- package/src/filter/useDistinctValues.ts +230 -230
- package/src/global.d.ts +11 -11
- package/src/grid/AfGridColumnsPanel.tsx +118 -118
- package/src/grid/AfGridContext.tsx +51 -51
- package/src/grid/AfGridError.tsx +46 -46
- package/src/grid/AfHeaderFilterCell.tsx +147 -147
- package/src/grid/AfHeaderFilterPanel.tsx +111 -111
- package/src/grid/GridEditLookup.css +12 -12
- package/src/grid/GridEditLookup.tsx +121 -121
- package/src/grid/Toolbar.tsx +233 -233
- package/src/grid/editing.ts +100 -100
- package/src/grid/filter.ts +78 -78
- package/src/grid/formatters.ts +48 -48
- package/src/grid/index.css +245 -245
- package/src/grid/index.tsx +503 -503
- package/src/grid/license.ts +48 -48
- package/src/grid/localization.ts +369 -369
- package/src/grid/slots/index.tsx +334 -334
- package/src/grid/slots/slots.css +64 -64
- package/src/grid/slots/slots.test.tsx +39 -39
- package/src/grid/theme.tsx +138 -138
- package/src/grid/useAfColumns.tsx +371 -371
- package/src/grid/useAfCurrentIndex.ts +63 -63
- package/src/grid/useAfData.ts +20 -20
- package/src/grid/useAfFilter.ts +160 -160
- package/src/grid/useAfFilterFields.ts +55 -55
- package/src/grid/useAfGridApi.ts +71 -71
- package/src/grid/useAfKeyBindings.ts +36 -36
- package/src/grid/useAfNewItemRow.ts +258 -258
- package/src/grid/useAfPagination.ts +53 -53
- package/src/grid/useAfPersistedState.ts +230 -230
- package/src/grid/useAfRowEditModel.ts +145 -145
- package/src/grid/useAfRowGrouping.ts +47 -47
- package/src/grid/useAfServerAggregation.test.ts +68 -68
- package/src/grid/useAfServerAggregation.ts +158 -158
- package/src/grid/useAfSortModel.ts +84 -84
- package/src/i18n.ts +13 -13
- package/src/index.ts +14 -14
- package/src/input/InputAdornments.css +84 -84
- package/src/input/InputAdornments.tsx +102 -102
- package/src/input/index.ts +1 -1
- package/src/layout/AppMenu.tsx +74 -74
- package/src/layout/AppMenuDrawer.css +70 -70
- package/src/layout/AppMenuDrawer.tsx +122 -122
- package/src/layout/ErrorBoundary.tsx +162 -162
- package/src/layout/PageContainer.css +48 -48
- package/src/layout/PageContainer.tsx +41 -41
- package/src/layout/ResponsiveDialog.css +86 -86
- package/src/layout/ResponsiveDialog.tsx +55 -55
- package/src/layout/index.tsx +69 -69
- package/src/link/Link.tsx +23 -23
- package/src/link/index.ts +1 -1
- package/src/mdi/Mdi.css +138 -138
- package/src/mdi/Mdi.test.tsx +91 -91
- package/src/mdi/Mdi.tsx +141 -141
- package/src/mdi/MdiConfirmCloseDialog.tsx +47 -47
- package/src/mdi/MdiContext.tsx +59 -59
- package/src/mdi/MdiDocumentContext.tsx +60 -60
- package/src/mdi/MdiPanel.tsx +61 -61
- package/src/mdi/MdiRestore.test.tsx +86 -86
- package/src/mdi/MdiTab.tsx +30 -30
- package/src/mdi/controller.test.ts +382 -382
- package/src/mdi/controller.ts +656 -656
- package/src/mdi/dockviewHost.ts +132 -132
- package/src/mdi/fakeHost.ts +200 -200
- package/src/mdi/host.ts +44 -44
- package/src/mdi/ids.test.ts +20 -20
- package/src/mdi/ids.ts +18 -18
- package/src/mdi/index.ts +31 -31
- package/src/mdi/layoutRestore.test.ts +156 -156
- package/src/mdi/layoutRestore.ts +311 -311
- package/src/mdi/layoutSpec.test.ts +143 -143
- package/src/mdi/layoutSpec.ts +214 -214
- package/src/mdi/layoutStore.test.ts +93 -93
- package/src/mdi/layoutStore.ts +99 -99
- package/src/mdi/memorySettingsStore.ts +21 -21
- package/src/mdi/registry.ts +34 -34
- package/src/mdi/testTypes.ts +37 -37
- package/src/mdi/theme.ts +22 -22
- package/src/mdi/types.ts +214 -214
- package/src/page/Page.css +53 -53
- package/src/page/PageBlock.css +64 -64
- package/src/page/PageBlock.tsx +93 -92
- package/src/page/PageHeader.css +88 -88
- package/src/page/PageHeader.tsx +115 -115
- package/src/page/PageTitle.css +5 -5
- package/src/page/PageTitle.tsx +21 -21
- package/src/page/index.ts +4 -4
- package/src/paper/Paper.tsx +52 -52
- package/src/progress/LinearProgress.css +93 -93
- package/src/progress/LinearProgress.tsx +104 -104
- package/src/progress/index.ts +1 -1
- package/src/report-downloader/ReportDownloader.css +67 -67
- package/src/report-downloader/components/StatusItem.tsx +87 -87
- package/src/report-downloader/components/StatusList.tsx +28 -28
- package/src/report-downloader/index.ts +41 -41
- package/src/report-downloader/status.tsx +92 -92
- package/src/sortable/DropIndicator.css +51 -51
- package/src/sortable/DropIndicator.tsx +27 -27
- package/src/sortable/SortableItemList.css +21 -21
- package/src/sortable/SortableItemList.tsx +264 -264
- package/src/sortable/index.ts +8 -8
- package/src/sortable/reorderSortOrder.ts +126 -126
- package/src/sortable/sortOrder.ts +101 -101
- package/src/sortable/types.ts +9 -9
- package/src/sortable/useSortableContainer.ts +53 -53
- package/src/sortable/useSortableItem.ts +95 -95
- package/src/sortable/useSortableList.ts +117 -117
- package/src/test/happy-dom-document-class.ts +24 -24
- package/src/test/setup.ts +12 -12
- package/src/theme/useDsColorScheme.ts +53 -53
- package/tsconfig.build.json +9 -9
- package/tsconfig.json +8 -8
- package/vitest.config.ts +10 -10
|
@@ -1,239 +1,239 @@
|
|
|
1
|
-
import { Select, Textfield } from "@digdir/designsystemet-react";
|
|
2
|
-
import { PencilIcon } from "@navikt/aksel-icons";
|
|
3
|
-
import {
|
|
4
|
-
type DynamicValue,
|
|
5
|
-
type FieldMetadata,
|
|
6
|
-
type FilterExpression,
|
|
7
|
-
type FilterOperator,
|
|
8
|
-
getDynamicValueLabel,
|
|
9
|
-
getDynamicValuesForField,
|
|
10
|
-
getOperatorArity,
|
|
11
|
-
getValueType,
|
|
12
|
-
normalizeOperator,
|
|
13
|
-
} from "@olenbetong/appframe-core";
|
|
14
|
-
import { Combobox } from "../autocomplete/index.js";
|
|
15
|
-
|
|
16
|
-
import { ChipListInput } from "./ChipListInput.js";
|
|
17
|
-
import type { FilterEditorSize, FilterValueOption } from "./types.js";
|
|
18
|
-
import { getLocalizedString } from "../i18n.js";
|
|
19
|
-
|
|
20
|
-
export type FilterValueEditorProps = {
|
|
21
|
-
expression: FilterExpression;
|
|
22
|
-
field?: FieldMetadata;
|
|
23
|
-
options?: FilterValueOption[];
|
|
24
|
-
onChange: (expression: FilterExpression) => void;
|
|
25
|
-
readOnly?: boolean;
|
|
26
|
-
"data-size"?: FilterEditorSize;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/** The `input` type used for a field's scalar editor. */
|
|
30
|
-
function getInputType(field: FieldMetadata | undefined): "text" | "number" | "date" | "datetime-local" {
|
|
31
|
-
switch (field?.type) {
|
|
32
|
-
case "number":
|
|
33
|
-
return "number";
|
|
34
|
-
case "date":
|
|
35
|
-
return "date";
|
|
36
|
-
case "datetime":
|
|
37
|
-
return "datetime-local";
|
|
38
|
-
default:
|
|
39
|
-
return "text";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function toStringValue(value: FilterExpression["value"]): string {
|
|
44
|
-
if (value === null || value === undefined) return "";
|
|
45
|
-
if (value instanceof Date) return value.toISOString();
|
|
46
|
-
|
|
47
|
-
return String(value);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* A `datetime-local` input reports `2026-09-25T00:00`, leaving out the seconds
|
|
52
|
-
* when they are zero. The value reaches SQL Server as a string, and SQL Server
|
|
53
|
-
* only accepts the ISO 8601 form with a `T` when the seconds are present, so
|
|
54
|
-
* the filter fails with "Conversion failed when converting date and/or time
|
|
55
|
-
* from character string" unless they are added.
|
|
56
|
-
*/
|
|
57
|
-
function withSeconds(value: string): string {
|
|
58
|
-
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value) ? `${value}:00` : value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function toFilterValue(value: FilterExpression["value"], inputType: string): FilterExpression["value"] {
|
|
62
|
-
if (inputType !== "datetime-local") return value;
|
|
63
|
-
if (typeof value === "string") return withSeconds(value);
|
|
64
|
-
if (Array.isArray(value)) return value.map((item) => (typeof item === "string" ? withSeconds(item) : item));
|
|
65
|
-
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function toArrayValue(value: FilterExpression["value"], length?: number): string[] {
|
|
70
|
-
let array = Array.isArray(value) ? value.map(toStringValue) : [];
|
|
71
|
-
if (length === undefined) return array;
|
|
72
|
-
|
|
73
|
-
// Pair editors always render two inputs, even before both are filled in.
|
|
74
|
-
return Array.from({ length }, (_, index) => array[index] ?? "");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The value part of a filter row. Which editor is rendered depends on the
|
|
79
|
-
* operator's arity, the field's type, and whether the value is a dynamic
|
|
80
|
-
* ("special") value such as `Today` or `CurrentUser`.
|
|
81
|
-
*/
|
|
82
|
-
export function FilterValueEditor({
|
|
83
|
-
expression,
|
|
84
|
-
field,
|
|
85
|
-
options,
|
|
86
|
-
onChange,
|
|
87
|
-
readOnly,
|
|
88
|
-
"data-size": size = "sm",
|
|
89
|
-
}: FilterValueEditorProps) {
|
|
90
|
-
let operator = normalizeOperator(expression.operator);
|
|
91
|
-
let arity = getOperatorArity(operator);
|
|
92
|
-
let isDynamic = expression.valueType === "special";
|
|
93
|
-
let dynamicValues = field ? getDynamicValuesForField(field) : [];
|
|
94
|
-
let inputType = getInputType(field);
|
|
95
|
-
|
|
96
|
-
function setValue(value: FilterExpression["value"]) {
|
|
97
|
-
onChange({ ...expression, value: isDynamic ? value : toFilterValue(value, inputType) });
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function setDynamic(enabled: boolean) {
|
|
101
|
-
if (enabled) {
|
|
102
|
-
onChange({ ...expression, value: dynamicValues[0] ?? null, valueType: "special" });
|
|
103
|
-
} else {
|
|
104
|
-
onChange({
|
|
105
|
-
...expression,
|
|
106
|
-
value: arity === "pair" ? ["", ""] : arity === "list" ? [] : "",
|
|
107
|
-
valueType: field ? getValueType(field, operator as FilterOperator) : "string",
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (arity === "none") return null;
|
|
113
|
-
|
|
114
|
-
let editor: React.ReactNode;
|
|
115
|
-
|
|
116
|
-
if (isDynamic) {
|
|
117
|
-
editor = (
|
|
118
|
-
<Select
|
|
119
|
-
className="ObFilterEditor-value"
|
|
120
|
-
aria-label={getLocalizedString("Value")}
|
|
121
|
-
data-size={size}
|
|
122
|
-
disabled={readOnly}
|
|
123
|
-
value={toStringValue(expression.value)}
|
|
124
|
-
onChange={(event) => setValue(event.target.value)}
|
|
125
|
-
>
|
|
126
|
-
{dynamicValues.map((dynamicValue) => (
|
|
127
|
-
<Select.Option key={dynamicValue} value={dynamicValue}>
|
|
128
|
-
{getDynamicValueLabel(dynamicValue as DynamicValue)}
|
|
129
|
-
</Select.Option>
|
|
130
|
-
))}
|
|
131
|
-
</Select>
|
|
132
|
-
);
|
|
133
|
-
} else if (arity === "list") {
|
|
134
|
-
editor = options ? (
|
|
135
|
-
<Combobox
|
|
136
|
-
className="ObFilterEditor-value"
|
|
137
|
-
multiple
|
|
138
|
-
data-size={size}
|
|
139
|
-
readOnly={readOnly}
|
|
140
|
-
placeholder={getLocalizedString("Select values")}
|
|
141
|
-
aria-label={getLocalizedString("Value")}
|
|
142
|
-
options={options}
|
|
143
|
-
getOptionLabel={(option) => option.label ?? option.value}
|
|
144
|
-
isOptionEqualToValue={(a, b) => a.value === b.value}
|
|
145
|
-
value={toArrayValue(expression.value).map(
|
|
146
|
-
(item) => options.find((option) => option.value === item) ?? { value: item },
|
|
147
|
-
)}
|
|
148
|
-
onChange={(_event, selected) => setValue(selected.map((option) => option.value))}
|
|
149
|
-
/>
|
|
150
|
-
) : (
|
|
151
|
-
<ChipListInput
|
|
152
|
-
aria-label={getLocalizedString("Value")}
|
|
153
|
-
data-size={size}
|
|
154
|
-
readOnly={readOnly}
|
|
155
|
-
type={inputType}
|
|
156
|
-
value={toArrayValue(expression.value)}
|
|
157
|
-
onChange={setValue}
|
|
158
|
-
/>
|
|
159
|
-
);
|
|
160
|
-
} else if (arity === "pair") {
|
|
161
|
-
let [from, to] = toArrayValue(expression.value, 2);
|
|
162
|
-
|
|
163
|
-
editor = (
|
|
164
|
-
<div className="ObFilterEditor-pair">
|
|
165
|
-
<Textfield
|
|
166
|
-
className="ObFilterEditor-value"
|
|
167
|
-
aria-label={getLocalizedString("From")}
|
|
168
|
-
data-size={size}
|
|
169
|
-
readOnly={readOnly}
|
|
170
|
-
type={inputType}
|
|
171
|
-
value={from}
|
|
172
|
-
onChange={(event) => setValue([event.target.value, to])}
|
|
173
|
-
/>
|
|
174
|
-
<span className="ObFilterEditor-pairSeparator">{getLocalizedString("and")}</span>
|
|
175
|
-
<Textfield
|
|
176
|
-
className="ObFilterEditor-value"
|
|
177
|
-
aria-label={getLocalizedString("To")}
|
|
178
|
-
data-size={size}
|
|
179
|
-
readOnly={readOnly}
|
|
180
|
-
type={inputType}
|
|
181
|
-
value={to}
|
|
182
|
-
onChange={(event) => setValue([from, event.target.value])}
|
|
183
|
-
/>
|
|
184
|
-
</div>
|
|
185
|
-
);
|
|
186
|
-
} else if (options) {
|
|
187
|
-
let current = toStringValue(expression.value);
|
|
188
|
-
|
|
189
|
-
editor = (
|
|
190
|
-
<Combobox
|
|
191
|
-
className="ObFilterEditor-value"
|
|
192
|
-
data-size={size}
|
|
193
|
-
readOnly={readOnly}
|
|
194
|
-
placeholder={getLocalizedString("Select value")}
|
|
195
|
-
aria-label={getLocalizedString("Value")}
|
|
196
|
-
options={options}
|
|
197
|
-
getOptionLabel={(option) => option.label ?? option.value}
|
|
198
|
-
isOptionEqualToValue={(a, b) => a.value === b.value}
|
|
199
|
-
value={current ? (options.find((option) => option.value === current) ?? { value: current }) : null}
|
|
200
|
-
onChange={(_event, selected) => setValue(selected?.value ?? "")}
|
|
201
|
-
/>
|
|
202
|
-
);
|
|
203
|
-
} else {
|
|
204
|
-
editor = (
|
|
205
|
-
<Textfield
|
|
206
|
-
className="ObFilterEditor-value"
|
|
207
|
-
aria-label={getLocalizedString("Value")}
|
|
208
|
-
data-size={size}
|
|
209
|
-
readOnly={readOnly}
|
|
210
|
-
type={inputType}
|
|
211
|
-
value={toStringValue(expression.value)}
|
|
212
|
-
onChange={(event) => setValue(event.target.value)}
|
|
213
|
-
/>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Dynamic values are resolved server side, so they can only replace the whole
|
|
218
|
-
// value. Pairs would need one toggle per side, which the Windows editor does
|
|
219
|
-
// not offer either.
|
|
220
|
-
let canToggleDynamic = !readOnly && dynamicValues.length > 0 && arity === "single";
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<div className="ObFilterEditor-valueCell">
|
|
224
|
-
{editor}
|
|
225
|
-
{canToggleDynamic && (
|
|
226
|
-
<button
|
|
227
|
-
type="button"
|
|
228
|
-
className="ObFilterEditor-iconButton"
|
|
229
|
-
aria-pressed={isDynamic}
|
|
230
|
-
title={getLocalizedString("Use a relative value")}
|
|
231
|
-
aria-label={getLocalizedString("Use a relative value")}
|
|
232
|
-
onClick={() => setDynamic(!isDynamic)}
|
|
233
|
-
>
|
|
234
|
-
<PencilIcon aria-hidden />
|
|
235
|
-
</button>
|
|
236
|
-
)}
|
|
237
|
-
</div>
|
|
238
|
-
);
|
|
239
|
-
}
|
|
1
|
+
import { Select, Textfield } from "@digdir/designsystemet-react";
|
|
2
|
+
import { PencilIcon } from "@navikt/aksel-icons";
|
|
3
|
+
import {
|
|
4
|
+
type DynamicValue,
|
|
5
|
+
type FieldMetadata,
|
|
6
|
+
type FilterExpression,
|
|
7
|
+
type FilterOperator,
|
|
8
|
+
getDynamicValueLabel,
|
|
9
|
+
getDynamicValuesForField,
|
|
10
|
+
getOperatorArity,
|
|
11
|
+
getValueType,
|
|
12
|
+
normalizeOperator,
|
|
13
|
+
} from "@olenbetong/appframe-core";
|
|
14
|
+
import { Combobox } from "../autocomplete/index.js";
|
|
15
|
+
|
|
16
|
+
import { ChipListInput } from "./ChipListInput.js";
|
|
17
|
+
import type { FilterEditorSize, FilterValueOption } from "./types.js";
|
|
18
|
+
import { getLocalizedString } from "../i18n.js";
|
|
19
|
+
|
|
20
|
+
export type FilterValueEditorProps = {
|
|
21
|
+
expression: FilterExpression;
|
|
22
|
+
field?: FieldMetadata;
|
|
23
|
+
options?: FilterValueOption[];
|
|
24
|
+
onChange: (expression: FilterExpression) => void;
|
|
25
|
+
readOnly?: boolean;
|
|
26
|
+
"data-size"?: FilterEditorSize;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** The `input` type used for a field's scalar editor. */
|
|
30
|
+
function getInputType(field: FieldMetadata | undefined): "text" | "number" | "date" | "datetime-local" {
|
|
31
|
+
switch (field?.type) {
|
|
32
|
+
case "number":
|
|
33
|
+
return "number";
|
|
34
|
+
case "date":
|
|
35
|
+
return "date";
|
|
36
|
+
case "datetime":
|
|
37
|
+
return "datetime-local";
|
|
38
|
+
default:
|
|
39
|
+
return "text";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toStringValue(value: FilterExpression["value"]): string {
|
|
44
|
+
if (value === null || value === undefined) return "";
|
|
45
|
+
if (value instanceof Date) return value.toISOString();
|
|
46
|
+
|
|
47
|
+
return String(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A `datetime-local` input reports `2026-09-25T00:00`, leaving out the seconds
|
|
52
|
+
* when they are zero. The value reaches SQL Server as a string, and SQL Server
|
|
53
|
+
* only accepts the ISO 8601 form with a `T` when the seconds are present, so
|
|
54
|
+
* the filter fails with "Conversion failed when converting date and/or time
|
|
55
|
+
* from character string" unless they are added.
|
|
56
|
+
*/
|
|
57
|
+
function withSeconds(value: string): string {
|
|
58
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value) ? `${value}:00` : value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toFilterValue(value: FilterExpression["value"], inputType: string): FilterExpression["value"] {
|
|
62
|
+
if (inputType !== "datetime-local") return value;
|
|
63
|
+
if (typeof value === "string") return withSeconds(value);
|
|
64
|
+
if (Array.isArray(value)) return value.map((item) => (typeof item === "string" ? withSeconds(item) : item));
|
|
65
|
+
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toArrayValue(value: FilterExpression["value"], length?: number): string[] {
|
|
70
|
+
let array = Array.isArray(value) ? value.map(toStringValue) : [];
|
|
71
|
+
if (length === undefined) return array;
|
|
72
|
+
|
|
73
|
+
// Pair editors always render two inputs, even before both are filled in.
|
|
74
|
+
return Array.from({ length }, (_, index) => array[index] ?? "");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The value part of a filter row. Which editor is rendered depends on the
|
|
79
|
+
* operator's arity, the field's type, and whether the value is a dynamic
|
|
80
|
+
* ("special") value such as `Today` or `CurrentUser`.
|
|
81
|
+
*/
|
|
82
|
+
export function FilterValueEditor({
|
|
83
|
+
expression,
|
|
84
|
+
field,
|
|
85
|
+
options,
|
|
86
|
+
onChange,
|
|
87
|
+
readOnly,
|
|
88
|
+
"data-size": size = "sm",
|
|
89
|
+
}: FilterValueEditorProps) {
|
|
90
|
+
let operator = normalizeOperator(expression.operator);
|
|
91
|
+
let arity = getOperatorArity(operator);
|
|
92
|
+
let isDynamic = expression.valueType === "special";
|
|
93
|
+
let dynamicValues = field ? getDynamicValuesForField(field) : [];
|
|
94
|
+
let inputType = getInputType(field);
|
|
95
|
+
|
|
96
|
+
function setValue(value: FilterExpression["value"]) {
|
|
97
|
+
onChange({ ...expression, value: isDynamic ? value : toFilterValue(value, inputType) });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function setDynamic(enabled: boolean) {
|
|
101
|
+
if (enabled) {
|
|
102
|
+
onChange({ ...expression, value: dynamicValues[0] ?? null, valueType: "special" });
|
|
103
|
+
} else {
|
|
104
|
+
onChange({
|
|
105
|
+
...expression,
|
|
106
|
+
value: arity === "pair" ? ["", ""] : arity === "list" ? [] : "",
|
|
107
|
+
valueType: field ? getValueType(field, operator as FilterOperator) : "string",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (arity === "none") return null;
|
|
113
|
+
|
|
114
|
+
let editor: React.ReactNode;
|
|
115
|
+
|
|
116
|
+
if (isDynamic) {
|
|
117
|
+
editor = (
|
|
118
|
+
<Select
|
|
119
|
+
className="ObFilterEditor-value"
|
|
120
|
+
aria-label={getLocalizedString("Value")}
|
|
121
|
+
data-size={size}
|
|
122
|
+
disabled={readOnly}
|
|
123
|
+
value={toStringValue(expression.value)}
|
|
124
|
+
onChange={(event) => setValue(event.target.value)}
|
|
125
|
+
>
|
|
126
|
+
{dynamicValues.map((dynamicValue) => (
|
|
127
|
+
<Select.Option key={dynamicValue} value={dynamicValue}>
|
|
128
|
+
{getDynamicValueLabel(dynamicValue as DynamicValue)}
|
|
129
|
+
</Select.Option>
|
|
130
|
+
))}
|
|
131
|
+
</Select>
|
|
132
|
+
);
|
|
133
|
+
} else if (arity === "list") {
|
|
134
|
+
editor = options ? (
|
|
135
|
+
<Combobox
|
|
136
|
+
className="ObFilterEditor-value"
|
|
137
|
+
multiple
|
|
138
|
+
data-size={size}
|
|
139
|
+
readOnly={readOnly}
|
|
140
|
+
placeholder={getLocalizedString("Select values")}
|
|
141
|
+
aria-label={getLocalizedString("Value")}
|
|
142
|
+
options={options}
|
|
143
|
+
getOptionLabel={(option) => option.label ?? option.value}
|
|
144
|
+
isOptionEqualToValue={(a, b) => a.value === b.value}
|
|
145
|
+
value={toArrayValue(expression.value).map(
|
|
146
|
+
(item) => options.find((option) => option.value === item) ?? { value: item },
|
|
147
|
+
)}
|
|
148
|
+
onChange={(_event, selected) => setValue(selected.map((option) => option.value))}
|
|
149
|
+
/>
|
|
150
|
+
) : (
|
|
151
|
+
<ChipListInput
|
|
152
|
+
aria-label={getLocalizedString("Value")}
|
|
153
|
+
data-size={size}
|
|
154
|
+
readOnly={readOnly}
|
|
155
|
+
type={inputType}
|
|
156
|
+
value={toArrayValue(expression.value)}
|
|
157
|
+
onChange={setValue}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
} else if (arity === "pair") {
|
|
161
|
+
let [from, to] = toArrayValue(expression.value, 2);
|
|
162
|
+
|
|
163
|
+
editor = (
|
|
164
|
+
<div className="ObFilterEditor-pair">
|
|
165
|
+
<Textfield
|
|
166
|
+
className="ObFilterEditor-value"
|
|
167
|
+
aria-label={getLocalizedString("From")}
|
|
168
|
+
data-size={size}
|
|
169
|
+
readOnly={readOnly}
|
|
170
|
+
type={inputType}
|
|
171
|
+
value={from}
|
|
172
|
+
onChange={(event) => setValue([event.target.value, to])}
|
|
173
|
+
/>
|
|
174
|
+
<span className="ObFilterEditor-pairSeparator">{getLocalizedString("and")}</span>
|
|
175
|
+
<Textfield
|
|
176
|
+
className="ObFilterEditor-value"
|
|
177
|
+
aria-label={getLocalizedString("To")}
|
|
178
|
+
data-size={size}
|
|
179
|
+
readOnly={readOnly}
|
|
180
|
+
type={inputType}
|
|
181
|
+
value={to}
|
|
182
|
+
onChange={(event) => setValue([from, event.target.value])}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
} else if (options) {
|
|
187
|
+
let current = toStringValue(expression.value);
|
|
188
|
+
|
|
189
|
+
editor = (
|
|
190
|
+
<Combobox
|
|
191
|
+
className="ObFilterEditor-value"
|
|
192
|
+
data-size={size}
|
|
193
|
+
readOnly={readOnly}
|
|
194
|
+
placeholder={getLocalizedString("Select value")}
|
|
195
|
+
aria-label={getLocalizedString("Value")}
|
|
196
|
+
options={options}
|
|
197
|
+
getOptionLabel={(option) => option.label ?? option.value}
|
|
198
|
+
isOptionEqualToValue={(a, b) => a.value === b.value}
|
|
199
|
+
value={current ? (options.find((option) => option.value === current) ?? { value: current }) : null}
|
|
200
|
+
onChange={(_event, selected) => setValue(selected?.value ?? "")}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
} else {
|
|
204
|
+
editor = (
|
|
205
|
+
<Textfield
|
|
206
|
+
className="ObFilterEditor-value"
|
|
207
|
+
aria-label={getLocalizedString("Value")}
|
|
208
|
+
data-size={size}
|
|
209
|
+
readOnly={readOnly}
|
|
210
|
+
type={inputType}
|
|
211
|
+
value={toStringValue(expression.value)}
|
|
212
|
+
onChange={(event) => setValue(event.target.value)}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Dynamic values are resolved server side, so they can only replace the whole
|
|
218
|
+
// value. Pairs would need one toggle per side, which the Windows editor does
|
|
219
|
+
// not offer either.
|
|
220
|
+
let canToggleDynamic = !readOnly && dynamicValues.length > 0 && arity === "single";
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div className="ObFilterEditor-valueCell">
|
|
224
|
+
{editor}
|
|
225
|
+
{canToggleDynamic && (
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
className="ObFilterEditor-iconButton"
|
|
229
|
+
aria-pressed={isDynamic}
|
|
230
|
+
title={getLocalizedString("Use a relative value")}
|
|
231
|
+
aria-label={getLocalizedString("Use a relative value")}
|
|
232
|
+
onClick={() => setDynamic(!isDynamic)}
|
|
233
|
+
>
|
|
234
|
+
<PencilIcon aria-hidden />
|
|
235
|
+
</button>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|