@olenbetong/appframe-ds 1.0.3 → 1.2.0
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 +13 -0
- package/package.json +8 -7
- package/scripts/copyAssets.mjs +27 -55
- package/src/AfLookup/Lookup.css +129 -125
- package/src/AfLookup/Lookup.tsx +184 -162
- package/src/AfLookup/LookupCombobox.css +78 -78
- package/src/AfLookup/LookupCombobox.tsx +237 -237
- package/src/AfLookup/LookupEdit.tsx +130 -130
- 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.tsx +92 -92
- 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 +49 -49
- package/src/container/index.ts +1 -1
- package/src/cutoff/CutOffPicker.css +12 -0
- package/src/cutoff/CutOffPicker.tsx +165 -0
- package/src/cutoff/context.tsx +194 -0
- package/src/cutoff/dsCutOffDates.ts +21 -0
- package/src/cutoff/index.ts +4 -0
- package/src/cutoff/utils.test.ts +59 -0
- package/src/cutoff/utils.ts +276 -0
- 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 +259 -259
- package/src/filter/FilterEditor.tsx +75 -74
- 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 -77
- package/src/filter/FilterValueEditor.tsx +220 -220
- package/src/filter/SavedFilterTree.css +107 -107
- package/src/filter/SavedFilterTree.test.tsx +94 -94
- package/src/filter/SavedFilterTree.tsx +218 -222
- 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 -151
- package/src/grid/AfHeaderFilterPanel.tsx +111 -111
- package/src/grid/GridEditLookup.css +12 -0
- package/src/grid/GridEditLookup.tsx +121 -0
- 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 +489 -484
- package/src/grid/license.ts +48 -48
- package/src/grid/localization.ts +369 -369
- package/src/grid/slots/index.tsx +307 -307
- package/src/grid/slots/slots.css +64 -64
- 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.ts +145 -145
- package/src/grid/useAfSortModel.ts +84 -84
- package/src/i18n.ts +13 -0
- package/src/index.ts +14 -12
- package/src/input/InputAdornments.css +75 -75
- package/src/input/InputAdornments.tsx +102 -102
- package/src/input/index.ts +1 -1
- package/src/layout/AppMenu.tsx +74 -75
- package/src/layout/AppMenuDrawer.css +70 -70
- package/src/layout/AppMenuDrawer.tsx +122 -122
- package/src/layout/ErrorBoundary.tsx +162 -162
- package/src/layout/ResponsiveDialog.css +86 -0
- package/src/layout/ResponsiveDialog.tsx +55 -0
- package/src/layout/index.tsx +69 -68
- package/src/link/Link.tsx +23 -0
- package/src/link/index.ts +1 -0
- package/src/mdi/Mdi.css +138 -138
- package/src/mdi/Mdi.test.tsx +91 -87
- 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 -82
- 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 +92 -92
- package/src/page/PageHeader.css +88 -88
- package/src/page/PageHeader.tsx +115 -116
- package/src/page/PageTitle.css +5 -0
- package/src/page/PageTitle.tsx +21 -0
- package/src/page/index.ts +4 -3
- 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 -86
- 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,220 +1,220 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
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
|
-
function toArrayValue(value: FilterExpression["value"], length?: number): string[] {
|
|
51
|
-
let array = Array.isArray(value) ? value.map(toStringValue) : [];
|
|
52
|
-
if (length === undefined) return array;
|
|
53
|
-
|
|
54
|
-
// Pair editors always render two inputs, even before both are filled in.
|
|
55
|
-
return Array.from({ length }, (_, index) => array[index] ?? "");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The value part of a filter row. Which editor is rendered depends on the
|
|
60
|
-
* operator's arity, the field's type, and whether the value is a dynamic
|
|
61
|
-
* ("special") value such as `Today` or `CurrentUser`.
|
|
62
|
-
*/
|
|
63
|
-
export function FilterValueEditor({
|
|
64
|
-
expression,
|
|
65
|
-
field,
|
|
66
|
-
options,
|
|
67
|
-
onChange,
|
|
68
|
-
readOnly,
|
|
69
|
-
"data-size": size = "sm",
|
|
70
|
-
}: FilterValueEditorProps) {
|
|
71
|
-
let operator = normalizeOperator(expression.operator);
|
|
72
|
-
let arity = getOperatorArity(operator);
|
|
73
|
-
let isDynamic = expression.valueType === "special";
|
|
74
|
-
let dynamicValues = field ? getDynamicValuesForField(field) : [];
|
|
75
|
-
let inputType = getInputType(field);
|
|
76
|
-
|
|
77
|
-
function setValue(value: FilterExpression["value"]) {
|
|
78
|
-
onChange({ ...expression, value });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function setDynamic(enabled: boolean) {
|
|
82
|
-
if (enabled) {
|
|
83
|
-
onChange({ ...expression, value: dynamicValues[0] ?? null, valueType: "special" });
|
|
84
|
-
} else {
|
|
85
|
-
onChange({
|
|
86
|
-
...expression,
|
|
87
|
-
value: arity === "pair" ? ["", ""] : arity === "list" ? [] : "",
|
|
88
|
-
valueType: field ? getValueType(field, operator as FilterOperator) : "string",
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (arity === "none") return null;
|
|
94
|
-
|
|
95
|
-
let editor: React.ReactNode;
|
|
96
|
-
|
|
97
|
-
if (isDynamic) {
|
|
98
|
-
editor = (
|
|
99
|
-
<Select
|
|
100
|
-
className="ObFilterEditor-value"
|
|
101
|
-
aria-label={getLocalizedString("Value")}
|
|
102
|
-
data-size={size}
|
|
103
|
-
disabled={readOnly}
|
|
104
|
-
value={toStringValue(expression.value)}
|
|
105
|
-
onChange={(event) => setValue(event.target.value)}
|
|
106
|
-
>
|
|
107
|
-
{dynamicValues.map((dynamicValue) => (
|
|
108
|
-
<Select.Option key={dynamicValue} value={dynamicValue}>
|
|
109
|
-
{getDynamicValueLabel(dynamicValue as DynamicValue)}
|
|
110
|
-
</Select.Option>
|
|
111
|
-
))}
|
|
112
|
-
</Select>
|
|
113
|
-
);
|
|
114
|
-
} else if (arity === "list") {
|
|
115
|
-
editor = options ? (
|
|
116
|
-
<Combobox
|
|
117
|
-
className="ObFilterEditor-value"
|
|
118
|
-
multiple
|
|
119
|
-
data-size={size}
|
|
120
|
-
readOnly={readOnly}
|
|
121
|
-
placeholder={getLocalizedString("Select values")}
|
|
122
|
-
aria-label={getLocalizedString("Value")}
|
|
123
|
-
options={options}
|
|
124
|
-
getOptionLabel={(option) => option.label ?? option.value}
|
|
125
|
-
isOptionEqualToValue={(a, b) => a.value === b.value}
|
|
126
|
-
value={toArrayValue(expression.value).map(
|
|
127
|
-
(item) => options.find((option) => option.value === item) ?? { value: item },
|
|
128
|
-
)}
|
|
129
|
-
onChange={(_event, selected) => setValue(selected.map((option) => option.value))}
|
|
130
|
-
/>
|
|
131
|
-
) : (
|
|
132
|
-
<ChipListInput
|
|
133
|
-
aria-label={getLocalizedString("Value")}
|
|
134
|
-
data-size={size}
|
|
135
|
-
readOnly={readOnly}
|
|
136
|
-
type={inputType}
|
|
137
|
-
value={toArrayValue(expression.value)}
|
|
138
|
-
onChange={setValue}
|
|
139
|
-
/>
|
|
140
|
-
);
|
|
141
|
-
} else if (arity === "pair") {
|
|
142
|
-
let [from, to] = toArrayValue(expression.value, 2);
|
|
143
|
-
|
|
144
|
-
editor = (
|
|
145
|
-
<div className="ObFilterEditor-pair">
|
|
146
|
-
<Textfield
|
|
147
|
-
className="ObFilterEditor-value"
|
|
148
|
-
aria-label={getLocalizedString("From")}
|
|
149
|
-
data-size={size}
|
|
150
|
-
readOnly={readOnly}
|
|
151
|
-
type={inputType}
|
|
152
|
-
value={from}
|
|
153
|
-
onChange={(event) => setValue([event.target.value, to])}
|
|
154
|
-
/>
|
|
155
|
-
<span className="ObFilterEditor-pairSeparator">{getLocalizedString("and")}</span>
|
|
156
|
-
<Textfield
|
|
157
|
-
className="ObFilterEditor-value"
|
|
158
|
-
aria-label={getLocalizedString("To")}
|
|
159
|
-
data-size={size}
|
|
160
|
-
readOnly={readOnly}
|
|
161
|
-
type={inputType}
|
|
162
|
-
value={to}
|
|
163
|
-
onChange={(event) => setValue([from, event.target.value])}
|
|
164
|
-
/>
|
|
165
|
-
</div>
|
|
166
|
-
);
|
|
167
|
-
} else if (options) {
|
|
168
|
-
let current = toStringValue(expression.value);
|
|
169
|
-
|
|
170
|
-
editor = (
|
|
171
|
-
<Combobox
|
|
172
|
-
className="ObFilterEditor-value"
|
|
173
|
-
data-size={size}
|
|
174
|
-
readOnly={readOnly}
|
|
175
|
-
placeholder={getLocalizedString("Select value")}
|
|
176
|
-
aria-label={getLocalizedString("Value")}
|
|
177
|
-
options={options}
|
|
178
|
-
getOptionLabel={(option) => option.label ?? option.value}
|
|
179
|
-
isOptionEqualToValue={(a, b) => a.value === b.value}
|
|
180
|
-
value={current ? (options.find((option) => option.value === current) ?? { value: current }) : null}
|
|
181
|
-
onChange={(_event, selected) => setValue(selected?.value ?? "")}
|
|
182
|
-
/>
|
|
183
|
-
);
|
|
184
|
-
} else {
|
|
185
|
-
editor = (
|
|
186
|
-
<Textfield
|
|
187
|
-
className="ObFilterEditor-value"
|
|
188
|
-
aria-label={getLocalizedString("Value")}
|
|
189
|
-
data-size={size}
|
|
190
|
-
readOnly={readOnly}
|
|
191
|
-
type={inputType}
|
|
192
|
-
value={toStringValue(expression.value)}
|
|
193
|
-
onChange={(event) => setValue(event.target.value)}
|
|
194
|
-
/>
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Dynamic values are resolved server side, so they can only replace the whole
|
|
199
|
-
// value. Pairs would need one toggle per side, which the Windows editor does
|
|
200
|
-
// not offer either.
|
|
201
|
-
let canToggleDynamic = !readOnly && dynamicValues.length > 0 && arity === "single";
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<div className="ObFilterEditor-valueCell">
|
|
205
|
-
{editor}
|
|
206
|
-
{canToggleDynamic && (
|
|
207
|
-
<button
|
|
208
|
-
type="button"
|
|
209
|
-
className="ObFilterEditor-iconButton"
|
|
210
|
-
aria-pressed={isDynamic}
|
|
211
|
-
title={getLocalizedString("Use a relative value")}
|
|
212
|
-
aria-label={getLocalizedString("Use a relative value")}
|
|
213
|
-
onClick={() => setDynamic(!isDynamic)}
|
|
214
|
-
>
|
|
215
|
-
<PencilIcon aria-hidden />
|
|
216
|
-
</button>
|
|
217
|
-
)}
|
|
218
|
-
</div>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
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
|
+
function toArrayValue(value: FilterExpression["value"], length?: number): string[] {
|
|
51
|
+
let array = Array.isArray(value) ? value.map(toStringValue) : [];
|
|
52
|
+
if (length === undefined) return array;
|
|
53
|
+
|
|
54
|
+
// Pair editors always render two inputs, even before both are filled in.
|
|
55
|
+
return Array.from({ length }, (_, index) => array[index] ?? "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The value part of a filter row. Which editor is rendered depends on the
|
|
60
|
+
* operator's arity, the field's type, and whether the value is a dynamic
|
|
61
|
+
* ("special") value such as `Today` or `CurrentUser`.
|
|
62
|
+
*/
|
|
63
|
+
export function FilterValueEditor({
|
|
64
|
+
expression,
|
|
65
|
+
field,
|
|
66
|
+
options,
|
|
67
|
+
onChange,
|
|
68
|
+
readOnly,
|
|
69
|
+
"data-size": size = "sm",
|
|
70
|
+
}: FilterValueEditorProps) {
|
|
71
|
+
let operator = normalizeOperator(expression.operator);
|
|
72
|
+
let arity = getOperatorArity(operator);
|
|
73
|
+
let isDynamic = expression.valueType === "special";
|
|
74
|
+
let dynamicValues = field ? getDynamicValuesForField(field) : [];
|
|
75
|
+
let inputType = getInputType(field);
|
|
76
|
+
|
|
77
|
+
function setValue(value: FilterExpression["value"]) {
|
|
78
|
+
onChange({ ...expression, value });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setDynamic(enabled: boolean) {
|
|
82
|
+
if (enabled) {
|
|
83
|
+
onChange({ ...expression, value: dynamicValues[0] ?? null, valueType: "special" });
|
|
84
|
+
} else {
|
|
85
|
+
onChange({
|
|
86
|
+
...expression,
|
|
87
|
+
value: arity === "pair" ? ["", ""] : arity === "list" ? [] : "",
|
|
88
|
+
valueType: field ? getValueType(field, operator as FilterOperator) : "string",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (arity === "none") return null;
|
|
94
|
+
|
|
95
|
+
let editor: React.ReactNode;
|
|
96
|
+
|
|
97
|
+
if (isDynamic) {
|
|
98
|
+
editor = (
|
|
99
|
+
<Select
|
|
100
|
+
className="ObFilterEditor-value"
|
|
101
|
+
aria-label={getLocalizedString("Value")}
|
|
102
|
+
data-size={size}
|
|
103
|
+
disabled={readOnly}
|
|
104
|
+
value={toStringValue(expression.value)}
|
|
105
|
+
onChange={(event) => setValue(event.target.value)}
|
|
106
|
+
>
|
|
107
|
+
{dynamicValues.map((dynamicValue) => (
|
|
108
|
+
<Select.Option key={dynamicValue} value={dynamicValue}>
|
|
109
|
+
{getDynamicValueLabel(dynamicValue as DynamicValue)}
|
|
110
|
+
</Select.Option>
|
|
111
|
+
))}
|
|
112
|
+
</Select>
|
|
113
|
+
);
|
|
114
|
+
} else if (arity === "list") {
|
|
115
|
+
editor = options ? (
|
|
116
|
+
<Combobox
|
|
117
|
+
className="ObFilterEditor-value"
|
|
118
|
+
multiple
|
|
119
|
+
data-size={size}
|
|
120
|
+
readOnly={readOnly}
|
|
121
|
+
placeholder={getLocalizedString("Select values")}
|
|
122
|
+
aria-label={getLocalizedString("Value")}
|
|
123
|
+
options={options}
|
|
124
|
+
getOptionLabel={(option) => option.label ?? option.value}
|
|
125
|
+
isOptionEqualToValue={(a, b) => a.value === b.value}
|
|
126
|
+
value={toArrayValue(expression.value).map(
|
|
127
|
+
(item) => options.find((option) => option.value === item) ?? { value: item },
|
|
128
|
+
)}
|
|
129
|
+
onChange={(_event, selected) => setValue(selected.map((option) => option.value))}
|
|
130
|
+
/>
|
|
131
|
+
) : (
|
|
132
|
+
<ChipListInput
|
|
133
|
+
aria-label={getLocalizedString("Value")}
|
|
134
|
+
data-size={size}
|
|
135
|
+
readOnly={readOnly}
|
|
136
|
+
type={inputType}
|
|
137
|
+
value={toArrayValue(expression.value)}
|
|
138
|
+
onChange={setValue}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
} else if (arity === "pair") {
|
|
142
|
+
let [from, to] = toArrayValue(expression.value, 2);
|
|
143
|
+
|
|
144
|
+
editor = (
|
|
145
|
+
<div className="ObFilterEditor-pair">
|
|
146
|
+
<Textfield
|
|
147
|
+
className="ObFilterEditor-value"
|
|
148
|
+
aria-label={getLocalizedString("From")}
|
|
149
|
+
data-size={size}
|
|
150
|
+
readOnly={readOnly}
|
|
151
|
+
type={inputType}
|
|
152
|
+
value={from}
|
|
153
|
+
onChange={(event) => setValue([event.target.value, to])}
|
|
154
|
+
/>
|
|
155
|
+
<span className="ObFilterEditor-pairSeparator">{getLocalizedString("and")}</span>
|
|
156
|
+
<Textfield
|
|
157
|
+
className="ObFilterEditor-value"
|
|
158
|
+
aria-label={getLocalizedString("To")}
|
|
159
|
+
data-size={size}
|
|
160
|
+
readOnly={readOnly}
|
|
161
|
+
type={inputType}
|
|
162
|
+
value={to}
|
|
163
|
+
onChange={(event) => setValue([from, event.target.value])}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
} else if (options) {
|
|
168
|
+
let current = toStringValue(expression.value);
|
|
169
|
+
|
|
170
|
+
editor = (
|
|
171
|
+
<Combobox
|
|
172
|
+
className="ObFilterEditor-value"
|
|
173
|
+
data-size={size}
|
|
174
|
+
readOnly={readOnly}
|
|
175
|
+
placeholder={getLocalizedString("Select value")}
|
|
176
|
+
aria-label={getLocalizedString("Value")}
|
|
177
|
+
options={options}
|
|
178
|
+
getOptionLabel={(option) => option.label ?? option.value}
|
|
179
|
+
isOptionEqualToValue={(a, b) => a.value === b.value}
|
|
180
|
+
value={current ? (options.find((option) => option.value === current) ?? { value: current }) : null}
|
|
181
|
+
onChange={(_event, selected) => setValue(selected?.value ?? "")}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
editor = (
|
|
186
|
+
<Textfield
|
|
187
|
+
className="ObFilterEditor-value"
|
|
188
|
+
aria-label={getLocalizedString("Value")}
|
|
189
|
+
data-size={size}
|
|
190
|
+
readOnly={readOnly}
|
|
191
|
+
type={inputType}
|
|
192
|
+
value={toStringValue(expression.value)}
|
|
193
|
+
onChange={(event) => setValue(event.target.value)}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Dynamic values are resolved server side, so they can only replace the whole
|
|
199
|
+
// value. Pairs would need one toggle per side, which the Windows editor does
|
|
200
|
+
// not offer either.
|
|
201
|
+
let canToggleDynamic = !readOnly && dynamicValues.length > 0 && arity === "single";
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="ObFilterEditor-valueCell">
|
|
205
|
+
{editor}
|
|
206
|
+
{canToggleDynamic && (
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
className="ObFilterEditor-iconButton"
|
|
210
|
+
aria-pressed={isDynamic}
|
|
211
|
+
title={getLocalizedString("Use a relative value")}
|
|
212
|
+
aria-label={getLocalizedString("Use a relative value")}
|
|
213
|
+
onClick={() => setDynamic(!isDynamic)}
|
|
214
|
+
>
|
|
215
|
+
<PencilIcon aria-hidden />
|
|
216
|
+
</button>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Colours use the contextual `--ds-color-*` aliases so the tree follows the
|
|
3
|
-
* active colour scheme. Never add hex fallbacks — they only encode the light
|
|
4
|
-
* theme and silently mask misspelled tokens.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
.ObSavedFilterTree-root,
|
|
8
|
-
.ObSavedFilterTree-children {
|
|
9
|
-
list-style: none;
|
|
10
|
-
margin: 0;
|
|
11
|
-
padding: 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.ObSavedFilterTree-children {
|
|
15
|
-
/*
|
|
16
|
-
* A branch is chevron + icon, a child only an icon, so matching the branch
|
|
17
|
-
* indent would put a child's icon under its parent's icon and make the two
|
|
18
|
-
* levels look like one. Indent past the branch icon instead, so a child's
|
|
19
|
-
* icon starts where its parent's label does.
|
|
20
|
-
*/
|
|
21
|
-
margin-inline-start: calc(var(--ds-size-6) + var(--ds-size-1) + var(--ds-size-6));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.ObSavedFilterTree-item {
|
|
25
|
-
display: flex;
|
|
26
|
-
align-items: center;
|
|
27
|
-
gap: var(--ds-size-1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.ObSavedFilterTree-node {
|
|
31
|
-
display: flex;
|
|
32
|
-
align-items: center;
|
|
33
|
-
gap: var(--ds-size-1);
|
|
34
|
-
flex: 1;
|
|
35
|
-
min-inline-size: 0;
|
|
36
|
-
padding: var(--ds-size-1) var(--ds-size-2);
|
|
37
|
-
border: none;
|
|
38
|
-
border-radius: var(--ds-border-radius-sm);
|
|
39
|
-
background: none;
|
|
40
|
-
color: var(--ds-color-neutral-text-default);
|
|
41
|
-
font: inherit;
|
|
42
|
-
text-align: start;
|
|
43
|
-
cursor: pointer;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.ObSavedFilterTree-node:hover {
|
|
47
|
-
background: var(--ds-color-neutral-surface-hover);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.ObSavedFilterTree-node:focus-visible {
|
|
51
|
-
outline: var(--ds-border-width-focus) solid var(--ds-color-focus-outer);
|
|
52
|
-
outline-offset: var(--ds-border-width-focus);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.ObSavedFilterTree-selected {
|
|
56
|
-
background: var(--ds-color-neutral-surface-active);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.ObSavedFilterTree-dirty .ObSavedFilterTree-label {
|
|
60
|
-
font-weight: var(--ds-font-weight-semibold);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.ObSavedFilterTree-chevron,
|
|
64
|
-
.ObSavedFilterTree-icon {
|
|
65
|
-
display: inline-flex;
|
|
66
|
-
flex: none;
|
|
67
|
-
align-items: center;
|
|
68
|
-
justify-content: center;
|
|
69
|
-
inline-size: var(--ds-size-6);
|
|
70
|
-
block-size: var(--ds-size-6);
|
|
71
|
-
color: var(--ds-color-neutral-text-subtle);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.ObSavedFilterTree-chevron > *,
|
|
75
|
-
.ObSavedFilterTree-icon > * {
|
|
76
|
-
inline-size: 100%;
|
|
77
|
-
block-size: 100%;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.ObSavedFilterTree-label {
|
|
81
|
-
overflow: hidden;
|
|
82
|
-
text-overflow: ellipsis;
|
|
83
|
-
white-space: nowrap;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.ObSavedFilterTree-empty {
|
|
87
|
-
/* Lines up with the child labels, past their icon. */
|
|
88
|
-
padding: var(--ds-size-1) var(--ds-size-2) var(--ds-size-1)
|
|
89
|
-
calc(var(--ds-size-2) + var(--ds-size-6) + var(--ds-size-1));
|
|
90
|
-
color: var(--ds-color-neutral-text-subtle);
|
|
91
|
-
font-style: italic;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/*
|
|
95
|
-
* The menu button only appears on hover/focus on pointer devices; on touch it is
|
|
96
|
-
* always visible because there is no hover to reveal it.
|
|
97
|
-
*/
|
|
98
|
-
@media (hover: hover) {
|
|
99
|
-
.ObSavedFilterTree-menuButton {
|
|
100
|
-
visibility: hidden;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.ObSavedFilterTree-item:hover .ObSavedFilterTree-menuButton,
|
|
104
|
-
.ObSavedFilterTree-item:focus-within .ObSavedFilterTree-menuButton {
|
|
105
|
-
visibility: visible;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Colours use the contextual `--ds-color-*` aliases so the tree follows the
|
|
3
|
+
* active colour scheme. Never add hex fallbacks — they only encode the light
|
|
4
|
+
* theme and silently mask misspelled tokens.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.ObSavedFilterTree-root,
|
|
8
|
+
.ObSavedFilterTree-children {
|
|
9
|
+
list-style: none;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.ObSavedFilterTree-children {
|
|
15
|
+
/*
|
|
16
|
+
* A branch is chevron + icon, a child only an icon, so matching the branch
|
|
17
|
+
* indent would put a child's icon under its parent's icon and make the two
|
|
18
|
+
* levels look like one. Indent past the branch icon instead, so a child's
|
|
19
|
+
* icon starts where its parent's label does.
|
|
20
|
+
*/
|
|
21
|
+
margin-inline-start: calc(var(--ds-size-6) + var(--ds-size-1) + var(--ds-size-6));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ObSavedFilterTree-item {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: var(--ds-size-1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ObSavedFilterTree-node {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: var(--ds-size-1);
|
|
34
|
+
flex: 1;
|
|
35
|
+
min-inline-size: 0;
|
|
36
|
+
padding: var(--ds-size-1) var(--ds-size-2);
|
|
37
|
+
border: none;
|
|
38
|
+
border-radius: var(--ds-border-radius-sm);
|
|
39
|
+
background: none;
|
|
40
|
+
color: var(--ds-color-neutral-text-default);
|
|
41
|
+
font: inherit;
|
|
42
|
+
text-align: start;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ObSavedFilterTree-node:hover {
|
|
47
|
+
background: var(--ds-color-neutral-surface-hover);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ObSavedFilterTree-node:focus-visible {
|
|
51
|
+
outline: var(--ds-border-width-focus) solid var(--ds-color-focus-outer);
|
|
52
|
+
outline-offset: var(--ds-border-width-focus);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ObSavedFilterTree-selected {
|
|
56
|
+
background: var(--ds-color-neutral-surface-active);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ObSavedFilterTree-dirty .ObSavedFilterTree-label {
|
|
60
|
+
font-weight: var(--ds-font-weight-semibold);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ObSavedFilterTree-chevron,
|
|
64
|
+
.ObSavedFilterTree-icon {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
flex: none;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
inline-size: var(--ds-size-6);
|
|
70
|
+
block-size: var(--ds-size-6);
|
|
71
|
+
color: var(--ds-color-neutral-text-subtle);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ObSavedFilterTree-chevron > *,
|
|
75
|
+
.ObSavedFilterTree-icon > * {
|
|
76
|
+
inline-size: 100%;
|
|
77
|
+
block-size: 100%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ObSavedFilterTree-label {
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
text-overflow: ellipsis;
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ObSavedFilterTree-empty {
|
|
87
|
+
/* Lines up with the child labels, past their icon. */
|
|
88
|
+
padding: var(--ds-size-1) var(--ds-size-2) var(--ds-size-1)
|
|
89
|
+
calc(var(--ds-size-2) + var(--ds-size-6) + var(--ds-size-1));
|
|
90
|
+
color: var(--ds-color-neutral-text-subtle);
|
|
91
|
+
font-style: italic;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* The menu button only appears on hover/focus on pointer devices; on touch it is
|
|
96
|
+
* always visible because there is no hover to reveal it.
|
|
97
|
+
*/
|
|
98
|
+
@media (hover: hover) {
|
|
99
|
+
.ObSavedFilterTree-menuButton {
|
|
100
|
+
visibility: hidden;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ObSavedFilterTree-item:hover .ObSavedFilterTree-menuButton,
|
|
104
|
+
.ObSavedFilterTree-item:focus-within .ObSavedFilterTree-menuButton {
|
|
105
|
+
visibility: visible;
|
|
106
|
+
}
|
|
107
|
+
}
|