@tensor_1/react 2.47.0 → 2.48.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/README.md +470 -1
- package/dist/{SchedulingCalendar-B6TakO2h.d.ts → SchedulingCalendar-WWyFL3Eo.d.ts} +51 -2
- package/dist/announce.d.ts +10 -0
- package/dist/announce.js +30 -0
- package/dist/chart-analytic.cjs +33 -25
- package/dist/chart-preset.cjs +23 -15
- package/dist/chart.cjs +23 -15
- package/dist/combobox-model.d.ts +10 -1
- package/dist/combobox-model.js +15 -0
- package/dist/command-palette-model.d.ts +44 -0
- package/dist/command-palette-model.js +36 -0
- package/dist/components/AccountMenu.d.ts +44 -0
- package/dist/components/AccountMenu.js +43 -0
- package/dist/components/AppShell.d.ts +45 -2
- package/dist/components/AppShell.js +116 -5
- package/dist/components/AuditLog.d.ts +128 -0
- package/dist/components/AuditLog.js +196 -0
- package/dist/components/BrandProfiles.js +4 -3
- package/dist/components/Breadcrumbs.d.ts +9 -2
- package/dist/components/Breadcrumbs.js +3 -2
- package/dist/components/Button.d.ts +12 -5
- package/dist/components/Button.js +7 -3
- package/dist/components/Card.d.ts +18 -1
- package/dist/components/Card.js +5 -1
- package/dist/components/ChangeHighlight.d.ts +30 -0
- package/dist/components/ChangeHighlight.js +21 -0
- package/dist/components/ChartCore.js +2 -1
- package/dist/components/Checkbox.d.ts +27 -1
- package/dist/components/Checkbox.js +31 -2
- package/dist/components/CodeDisplay.d.ts +65 -0
- package/dist/components/CodeDisplay.js +124 -0
- package/dist/components/Combobox.d.ts +27 -6
- package/dist/components/Combobox.js +195 -58
- package/dist/components/CommandPalette.d.ts +36 -1
- package/dist/components/CommandPalette.js +254 -2
- package/dist/components/ContactCard.d.ts +66 -0
- package/dist/components/ContactCard.js +74 -0
- package/dist/components/Count.d.ts +24 -0
- package/dist/components/Count.js +32 -0
- package/dist/components/DataWorkspace.d.ts +167 -0
- package/dist/components/DataWorkspace.js +383 -0
- package/dist/components/DateFieldParts.d.ts +2 -1
- package/dist/components/DateFieldParts.js +7 -0
- package/dist/components/DatePicker.d.ts +9 -2
- package/dist/components/DatePicker.js +67 -28
- package/dist/components/DateRangePicker.d.ts +12 -2
- package/dist/components/DateRangePicker.js +13 -12
- package/dist/components/DetailPage.d.ts +7 -1
- package/dist/components/DetailPage.js +2 -1
- package/dist/components/Dialogs.d.ts +71 -10
- package/dist/components/Dialogs.js +130 -30
- package/dist/components/DiffViewer.d.ts +23 -3
- package/dist/components/DiffViewer.js +26 -12
- package/dist/components/DiscardPrompt.js +16 -0
- package/dist/components/DisplayPreferences.d.ts +83 -0
- package/dist/components/DisplayPreferences.js +185 -0
- package/dist/components/EditorialHome.d.ts +23 -3
- package/dist/components/EditorialHome.js +16 -11
- package/dist/components/EntityPicker.d.ts +20 -3
- package/dist/components/EntityPicker.js +32 -13
- package/dist/components/FilterEditor.d.ts +18 -4
- package/dist/components/FilterEditor.js +141 -19
- package/dist/components/InfoTip.js +11 -12
- package/dist/components/InlineEdit.d.ts +87 -20
- package/dist/components/InlineEdit.js +197 -54
- package/dist/components/Input.d.ts +12 -2
- package/dist/components/Input.js +5 -3
- package/dist/components/Kbd.d.ts +6 -1
- package/dist/components/Kbd.js +6 -6
- package/dist/components/Lightbox.d.ts +41 -5
- package/dist/components/Lightbox.js +95 -9
- package/dist/components/MatchReview.d.ts +40 -1
- package/dist/components/MatchReview.js +130 -4
- package/dist/components/MetricInput.js +3 -2
- package/dist/components/Modal.d.ts +2 -1
- package/dist/components/Modal.js +13 -4
- package/dist/components/NavFavourites.d.ts +5 -0
- package/dist/components/NavFavourites.js +7 -7
- package/dist/components/OtpInput.d.ts +33 -0
- package/dist/components/OtpInput.js +123 -0
- package/dist/components/PageHeader.d.ts +7 -1
- package/dist/components/PageHeader.js +2 -2
- package/dist/components/Pagination.js +6 -5
- package/dist/components/PasswordInput.d.ts +34 -0
- package/dist/components/PasswordInput.js +101 -0
- package/dist/components/Peek.js +3 -3
- package/dist/components/Popover.js +2 -0
- package/dist/components/PrintLayout.d.ts +63 -0
- package/dist/components/PrintLayout.js +114 -0
- package/dist/components/QrCode.d.ts +47 -0
- package/dist/components/QrCode.js +55 -0
- package/dist/components/QrScanner.d.ts +95 -0
- package/dist/components/QrScanner.js +266 -0
- package/dist/components/RelationshipGraph.d.ts +32 -1
- package/dist/components/RelationshipGraph.js +18 -5
- package/dist/components/RelationshipGraphLayered.d.ts +1 -1
- package/dist/components/RelationshipGraphLayered.js +190 -115
- package/dist/components/RelationshipGraphList.js +33 -6
- package/dist/components/RuleBuilder.js +39 -7
- package/dist/components/SchedulingAgenda.d.ts +3 -1
- package/dist/components/SchedulingCalendar.d.ts +3 -1
- package/dist/components/SchedulingCalendar.js +43 -4
- package/dist/components/SchedulingResourceDay.d.ts +57 -0
- package/dist/components/SchedulingResourceDay.js +287 -0
- package/dist/components/SchedulingTimeGrid.d.ts +3 -1
- package/dist/components/SchedulingTimeline.d.ts +3 -1
- package/dist/components/SearchSuggest.d.ts +23 -3
- package/dist/components/SearchSuggest.js +48 -17
- package/dist/components/Segmented.d.ts +39 -4
- package/dist/components/Segmented.js +31 -3
- package/dist/components/Select.d.ts +3 -2
- package/dist/components/Select.js +1 -1
- package/dist/components/ShortcutHint.d.ts +11 -10
- package/dist/components/ShortcutHint.js +44 -38
- package/dist/components/Sidebar.d.ts +36 -4
- package/dist/components/Sidebar.js +37 -12
- package/dist/components/SortableList.d.ts +45 -0
- package/dist/components/SortableList.js +134 -0
- package/dist/components/Stat.d.ts +28 -3
- package/dist/components/Stat.js +13 -3
- package/dist/components/StatusPill.d.ts +14 -3
- package/dist/components/StatusPill.js +11 -3
- package/dist/components/Steps.js +2 -1
- package/dist/components/Table.d.ts +9 -2
- package/dist/components/Table.js +4 -2
- package/dist/components/TableControls.js +18 -58
- package/dist/components/TableOverflowProbe.d.ts +12 -0
- package/dist/components/TableOverflowProbe.js +27 -0
- package/dist/components/Tabs.d.ts +2 -1
- package/dist/components/Tabs.js +1 -0
- package/dist/components/TagInput.d.ts +67 -0
- package/dist/components/TagInput.js +342 -0
- package/dist/components/Textarea.d.ts +5 -0
- package/dist/components/Textarea.js +2 -2
- package/dist/components/TimePicker.d.ts +80 -0
- package/dist/components/TimePicker.js +347 -0
- package/dist/components/Toast.js +3 -2
- package/dist/components/Tooltip.d.ts +8 -4
- package/dist/components/Tooltip.js +24 -21
- package/dist/components/Tree.d.ts +8 -0
- package/dist/components/Tree.js +11 -5
- package/dist/display-preferences-model.d.ts +53 -0
- package/dist/display-preferences-model.js +64 -0
- package/dist/emoji.cjs +28 -35
- package/dist/filter-model.d.ts +59 -3
- package/dist/filter-model.js +67 -2
- package/dist/hooks/useChangeHighlight.d.ts +33 -0
- package/dist/hooks/useChangeHighlight.js +44 -0
- package/dist/hooks/useDialog.d.ts +5 -0
- package/dist/hooks/useDialog.js +3 -2
- package/dist/hooks/useOptimisticValue.d.ts +51 -0
- package/dist/hooks/useOptimisticValue.js +61 -0
- package/dist/hooks/useReducedMotion.d.ts +4 -1
- package/dist/hooks/useReducedMotion.js +7 -1
- package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
- package/dist/hooks/useResolvedFilterLabels.js +34 -0
- package/dist/hooks/useVirtualRows.d.ts +21 -2
- package/dist/hooks/useVirtualRows.js +30 -3
- package/dist/icons/Icon.d.ts +1 -1
- package/dist/index.cjs +18644 -13210
- package/dist/index.d.cts +3003 -699
- package/dist/index.d.ts +42 -16
- package/dist/index.js +139 -7
- package/dist/labels-context.d.ts +90 -3
- package/dist/overlay-stack.d.ts +6 -0
- package/dist/overlay-stack.js +13 -0
- package/dist/qr-model.d.ts +65 -0
- package/dist/qr-model.js +323 -0
- package/dist/relationship-graph-model.d.ts +78 -3
- package/dist/relationship-graph-model.js +112 -5
- package/dist/reorder.d.ts +30 -0
- package/dist/reorder.js +74 -0
- package/dist/rich-text.cjs +4 -2
- package/dist/scheduling-resource-model.d.ts +76 -0
- package/dist/scheduling-resource-model.js +74 -0
- package/dist/search-suggest-model.js +3 -1
- package/dist/shell-context.d.ts +11 -0
- package/dist/shell-context.js +7 -0
- package/dist/time-field-model.d.ts +46 -0
- package/dist/time-field-model.js +152 -0
- package/dist/toast-store.d.ts +17 -1
- package/dist/toast-store.js +23 -7
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { TextFilterOperator, NumberFilterOperator, FilterField, FilterValue } from '../filter-model.js';
|
|
2
|
+
import { TextFilterOperator, NumberFilterOperator, DateFilterOperator, FilterField, FilterValue } from '../filter-model.js';
|
|
3
3
|
|
|
4
4
|
interface FilterEditorLabels {
|
|
5
5
|
apply: string;
|
|
@@ -26,7 +26,15 @@ interface FilterEditorLabels {
|
|
|
26
26
|
remove: string;
|
|
27
27
|
/** Names the row of `field.presets` chips. Optional so existing label objects still type-check; default "Presets". */
|
|
28
28
|
presets?: string;
|
|
29
|
+
/** `tags` (HAR-1832): the field that takes a new value, and its hint. Optional, like `presets`. */
|
|
30
|
+
addTag?: string;
|
|
31
|
+
/** `tags`: how to add one, read with the field. */
|
|
32
|
+
addTagHint?: string;
|
|
33
|
+
/** `date` (HAR-1832): the date of on / before / after. Optional, like `presets`. */
|
|
34
|
+
date?: string;
|
|
29
35
|
operators: Record<TextFilterOperator | NumberFilterOperator, string>;
|
|
36
|
+
/** `date` conditions (HAR-1832). Optional, like `presets`. */
|
|
37
|
+
dateOperators?: Partial<Record<DateFilterOperator, string>>;
|
|
30
38
|
}
|
|
31
39
|
declare const DEFAULT_FILTER_EDITOR_LABELS: FilterEditorLabels;
|
|
32
40
|
interface FilterEditorProps {
|
|
@@ -34,7 +42,12 @@ interface FilterEditorProps {
|
|
|
34
42
|
/** Undefined means "not filtered yet". */
|
|
35
43
|
value: FilterValue | undefined;
|
|
36
44
|
onValueChange: (value: FilterValue) => void;
|
|
37
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Apply the filter: the Apply button, or Enter in any field (HAR-1832). Gets the value to
|
|
47
|
+
* apply, which already holds a tag still being typed: read it rather than your state, which
|
|
48
|
+
* has not caught up when Enter applies in the same event.
|
|
49
|
+
*/
|
|
50
|
+
onApply: (value: FilterValue) => void;
|
|
38
51
|
onClear: () => void;
|
|
39
52
|
labels?: Partial<FilterEditorLabels>;
|
|
40
53
|
/** Show an option filter above an enum list once it has more options than this. Default 8. */
|
|
@@ -44,8 +57,9 @@ interface FilterEditorProps {
|
|
|
44
57
|
}
|
|
45
58
|
/**
|
|
46
59
|
* Editor content for one typed column filter (HAR-1365; TENSOR C6, MOTUS C-1): an enum
|
|
47
|
-
* multi-select, text or number with a condition, a date range,
|
|
48
|
-
*
|
|
60
|
+
* multi-select, text or number with a condition, a date range, a date with a condition
|
|
61
|
+
* (on / before / after / between), yes/no, free-text tags, or records found by an async search
|
|
62
|
+
* (restored ids get their labels from `field.resolveLabels`). Enter applies in every kind. The product owns the popover trigger and the query; `summarizeFilter`
|
|
49
63
|
* turns the applied value into the chip text. `FilterPopover` stays for the one-string case.
|
|
50
64
|
* An enum field's `presets` (HAR-1505) render as toggle chips above its options. Words come
|
|
51
65
|
* from `labels`, or from `UixLabelsProvider`'s `filterEditor` entry.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Fragment as Fragment2, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { Button } from "./Button.js";
|
|
5
5
|
import { Checkbox } from "./Checkbox.js";
|
|
6
6
|
import { Input } from "./Input.js";
|
|
@@ -8,9 +8,11 @@ import { Radio, RadioGroup } from "./Radio.js";
|
|
|
8
8
|
import { Select } from "./Select.js";
|
|
9
9
|
import { Chip } from "./Chip.js";
|
|
10
10
|
import { fillLabel } from "../fill-label.js";
|
|
11
|
+
import { cx } from "../cx.js";
|
|
11
12
|
import { useUixLabels } from "../labels-context.js";
|
|
13
|
+
import { useDelayedPending } from "../hooks/useDelayedPending.js";
|
|
12
14
|
import { foldForSearch } from "../search-suggest-model.js";
|
|
13
|
-
import { NUMBER_FILTER_OPERATORS, TEXT_FILTER_OPERATORS, emptyFilterValue, matchFilterPreset } from "../filter-model.js";
|
|
15
|
+
import { DATE_FILTER_OPERATORS, NUMBER_FILTER_OPERATORS, TEXT_FILTER_OPERATORS, emptyFilterValue, isUnresolvedChoice, matchFilterPreset, orderFilterRange } from "../filter-model.js";
|
|
14
16
|
const DEFAULT_FILTER_EDITOR_LABELS = {
|
|
15
17
|
apply: "Apply",
|
|
16
18
|
clear: "Clear",
|
|
@@ -32,6 +34,10 @@ const DEFAULT_FILTER_EDITOR_LABELS = {
|
|
|
32
34
|
selected: "{count} selected",
|
|
33
35
|
remove: "Remove {label}",
|
|
34
36
|
presets: "Presets",
|
|
37
|
+
addTag: "Add a value",
|
|
38
|
+
addTagHint: "Enter or a comma adds it.",
|
|
39
|
+
date: "Date",
|
|
40
|
+
dateOperators: { on: "on", before: "before", after: "after", between: "between" },
|
|
35
41
|
operators: {
|
|
36
42
|
contains: "contains",
|
|
37
43
|
"not-contains": "does not contain",
|
|
@@ -53,10 +59,21 @@ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear
|
|
|
53
59
|
...DEFAULT_FILTER_EDITOR_LABELS,
|
|
54
60
|
...provided,
|
|
55
61
|
...labelOverrides,
|
|
56
|
-
operators: { ...DEFAULT_FILTER_EDITOR_LABELS.operators, ...provided?.operators, ...labelOverrides?.operators }
|
|
62
|
+
operators: { ...DEFAULT_FILTER_EDITOR_LABELS.operators, ...provided?.operators, ...labelOverrides?.operators },
|
|
63
|
+
dateOperators: { ...DEFAULT_FILTER_EDITOR_LABELS.dateOperators, ...provided?.dateOperators, ...labelOverrides?.dateOperators }
|
|
57
64
|
};
|
|
58
65
|
const id = useId();
|
|
59
|
-
|
|
66
|
+
let value = valueProp?.kind === field.kind ? valueProp : emptyFilterValue(field.kind);
|
|
67
|
+
if (value.kind === "date" && field.dateOperators?.length && !field.dateOperators.includes(value.operator)) value = { ...value, operator: field.dateOperators[0] };
|
|
68
|
+
const pendingTag = useRef(() => void 0);
|
|
69
|
+
const apply = () => onApply(orderFilterRange((value.kind === "tags" ? pendingTag.current() : void 0) ?? value));
|
|
70
|
+
const onKeyDown = (event) => {
|
|
71
|
+
if (event.key !== "Enter" || event.defaultPrevented || event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
72
|
+
const target = event.target;
|
|
73
|
+
if (target.closest('button, a, textarea, [role="option"], [role="listbox"], [aria-expanded="true"]')) return;
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
apply();
|
|
76
|
+
};
|
|
60
77
|
let body = null;
|
|
61
78
|
switch (value.kind) {
|
|
62
79
|
case "enum":
|
|
@@ -65,13 +82,31 @@ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear
|
|
|
65
82
|
case "reference":
|
|
66
83
|
body = /* @__PURE__ */ jsx(ReferenceBody, { field, value, onChange: onValueChange, labels, delay: searchDelay });
|
|
67
84
|
break;
|
|
85
|
+
case "tags":
|
|
86
|
+
body = /* @__PURE__ */ jsx(TagsBody, { value, onChange: onValueChange, labels, pending: pendingTag, id });
|
|
87
|
+
break;
|
|
88
|
+
case "date": {
|
|
89
|
+
const ops = field.dateOperators ?? DATE_FILTER_OPERATORS;
|
|
90
|
+
const words = labels.dateOperators ?? {};
|
|
91
|
+
const between = value.operator === "between";
|
|
92
|
+
body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
|
|
93
|
+
ops.length > 1 && /* @__PURE__ */ jsx(Select, { size: "sm", "aria-label": labels.operator, value: value.operator, onChange: (e) => onValueChange({ ...value, operator: e.currentTarget.value, ...e.currentTarget.value === "between" ? {} : { to: void 0 } }), children: ops.map((op) => /* @__PURE__ */ jsx("option", { value: op, children: words[op] ?? DEFAULT_FILTER_EDITOR_LABELS.dateOperators[op] }, op)) }),
|
|
94
|
+
/* @__PURE__ */ jsxs("label", { className: "uix-filter-editor__date", children: [
|
|
95
|
+
/* @__PURE__ */ jsx("span", { children: between ? labels.from : labels.date ?? DEFAULT_FILTER_EDITOR_LABELS.date }),
|
|
96
|
+
/* @__PURE__ */ jsx(Input, { type: "date", value: value.from ?? "", max: between ? value.to : void 0, onChange: (e) => onValueChange({ ...value, from: e.currentTarget.value || void 0 }), autoFocus: true })
|
|
97
|
+
] }),
|
|
98
|
+
between && /* @__PURE__ */ jsxs("label", { className: "uix-filter-editor__date", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("span", { children: labels.to }),
|
|
100
|
+
/* @__PURE__ */ jsx(Input, { type: "date", value: value.to ?? "", min: value.from, onChange: (e) => onValueChange({ ...value, to: e.currentTarget.value || void 0 }) })
|
|
101
|
+
] })
|
|
102
|
+
] });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
68
105
|
case "text": {
|
|
69
106
|
const ops = field.operators ?? TEXT_FILTER_OPERATORS;
|
|
70
107
|
body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
|
|
71
108
|
ops.length > 1 && /* @__PURE__ */ jsx(Select, { size: "sm", "aria-label": labels.operator, value: value.operator, onChange: (e) => onValueChange({ ...value, operator: e.currentTarget.value }), children: ops.map((op) => /* @__PURE__ */ jsx("option", { value: op, children: labels.operators[op] }, op)) }),
|
|
72
|
-
/* @__PURE__ */ jsx(Input, { "aria-label": labels.value, value: value.text, onChange: (e) => onValueChange({ ...value, text: e.currentTarget.value }),
|
|
73
|
-
if (e.key === "Enter") onApply();
|
|
74
|
-
}, autoFocus: true })
|
|
109
|
+
/* @__PURE__ */ jsx(Input, { "aria-label": labels.value, value: value.text, onChange: (e) => onValueChange({ ...value, text: e.currentTarget.value }), autoFocus: true })
|
|
75
110
|
] });
|
|
76
111
|
break;
|
|
77
112
|
}
|
|
@@ -111,12 +146,12 @@ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear
|
|
|
111
146
|
break;
|
|
112
147
|
}
|
|
113
148
|
}
|
|
114
|
-
return /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover uix-filter-editor", role: "group", "aria-labelledby": `${id}-label`, "data-kind": field.kind, children: [
|
|
149
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover uix-filter-editor", role: "group", "aria-labelledby": `${id}-label`, "data-kind": field.kind, onKeyDown, children: [
|
|
115
150
|
/* @__PURE__ */ jsx("div", { id: `${id}-label`, className: "uix-filter-editor__label", children: field.label }),
|
|
116
|
-
body,
|
|
151
|
+
/* @__PURE__ */ jsx(Fragment2, { children: body }, `${field.kind}:${field.id}`),
|
|
117
152
|
/* @__PURE__ */ jsxs("div", { className: "uix-filter-popover__actions", children: [
|
|
118
153
|
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: onClear, children: labels.clear }),
|
|
119
|
-
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", onClick:
|
|
154
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", onClick: apply, children: labels.apply })
|
|
120
155
|
] })
|
|
121
156
|
] });
|
|
122
157
|
}
|
|
@@ -146,13 +181,18 @@ function ReferenceBody({ field, value, onChange, labels, delay }) {
|
|
|
146
181
|
const [query, setQuery] = useState("");
|
|
147
182
|
const [state, setState] = useState({ status: "idle", results: [] });
|
|
148
183
|
const [attempt, setAttempt] = useState(0);
|
|
184
|
+
const slow = useDelayedPending(state.status === "loading");
|
|
149
185
|
const latest = useRef(0);
|
|
186
|
+
const search = useRef(field.onSearch);
|
|
187
|
+
search.current = field.onSearch;
|
|
188
|
+
const resolve = useRef(field.resolveLabels);
|
|
189
|
+
resolve.current = field.resolveLabels;
|
|
150
190
|
useEffect(() => {
|
|
151
|
-
if (!
|
|
191
|
+
if (!search.current) return;
|
|
152
192
|
const run = ++latest.current;
|
|
153
193
|
const timer = window.setTimeout(() => {
|
|
154
194
|
setState((s) => ({ ...s, status: "loading" }));
|
|
155
|
-
|
|
195
|
+
search.current(query.trim()).then(
|
|
156
196
|
(results) => {
|
|
157
197
|
if (run === latest.current) setState({ status: "done", results });
|
|
158
198
|
},
|
|
@@ -162,22 +202,104 @@ function ReferenceBody({ field, value, onChange, labels, delay }) {
|
|
|
162
202
|
);
|
|
163
203
|
}, delay);
|
|
164
204
|
return () => window.clearTimeout(timer);
|
|
165
|
-
}, [query, attempt, field, delay]);
|
|
166
|
-
const
|
|
167
|
-
const
|
|
205
|
+
}, [query, attempt, field.id, delay]);
|
|
206
|
+
const [known, setKnown] = useState(/* @__PURE__ */ new Map());
|
|
207
|
+
const answered = useRef(/* @__PURE__ */ new Set());
|
|
208
|
+
const requests = useRef(/* @__PURE__ */ new Map());
|
|
209
|
+
const unresolved = value.values.filter((v) => isUnresolvedChoice(v) && !known.has(v.value) && !answered.current.has(v.value)).map((v) => v.value);
|
|
210
|
+
const unresolvedKey = unresolved.join("\0");
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!unresolvedKey || !resolve.current) return;
|
|
213
|
+
let live = true;
|
|
214
|
+
const ids = unresolvedKey.split("\0");
|
|
215
|
+
let request = requests.current.get(unresolvedKey);
|
|
216
|
+
if (!request) {
|
|
217
|
+
request = resolve.current(ids);
|
|
218
|
+
requests.current.set(unresolvedKey, request);
|
|
219
|
+
}
|
|
220
|
+
const settle = (choices) => {
|
|
221
|
+
for (const id of ids) answered.current.add(id);
|
|
222
|
+
if (live) setKnown((prev) => new Map([...prev, ...choices.map((c) => [c.value, c.label])]));
|
|
223
|
+
};
|
|
224
|
+
request.then(settle, () => settle([]));
|
|
225
|
+
return () => {
|
|
226
|
+
live = false;
|
|
227
|
+
};
|
|
228
|
+
}, [unresolvedKey, field.id]);
|
|
229
|
+
const labelled = (v) => isUnresolvedChoice(v) && known.has(v.value) ? { value: v.value, label: known.get(v.value) } : v;
|
|
230
|
+
const values = value.values.map(labelled);
|
|
231
|
+
const chosen = new Map(values.map((v) => [v.value, v]));
|
|
232
|
+
const toggle = (choice, on) => onChange({ kind: "reference", values: on ? [...values, choice] : values.filter((v) => v.value !== choice.value) });
|
|
233
|
+
const showResults = state.status === "done" || state.status === "loading" && state.results.length > 0;
|
|
168
234
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
169
|
-
|
|
235
|
+
values.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-chip-group", role: "group", "aria-label": fillLabel(labels.selected, { count: values.length }), children: values.map((v) => /* @__PURE__ */ jsx(Chip, { removeLabel: fillLabel(labels.remove, { label: v.label }), onRemove: () => toggle(v, false), children: v.label }, v.value)) }),
|
|
170
236
|
/* @__PURE__ */ jsx(Input, { type: "search", size: "sm", "aria-label": labels.search, placeholder: labels.search, value: query, onChange: (e) => setQuery(e.currentTarget.value), autoFocus: true }),
|
|
171
237
|
/* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__list", "aria-busy": state.status === "loading" || void 0, children: [
|
|
172
|
-
state.status === "loading" && /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", role: "status", children: labels.searching }),
|
|
173
238
|
state.status === "error" && /* @__PURE__ */ jsxs("p", { className: "uix-filter-editor__note", role: "alert", children: [
|
|
174
239
|
labels.searchFailed,
|
|
175
240
|
" ",
|
|
176
241
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => setAttempt((n) => n + 1), children: labels.retry })
|
|
177
242
|
] }),
|
|
178
243
|
state.status === "done" && state.results.length === 0 && /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", children: labels.noMatches }),
|
|
179
|
-
|
|
180
|
-
] })
|
|
244
|
+
showResults && state.results.map((r) => /* @__PURE__ */ jsx(Checkbox, { label: r.label, checked: chosen.has(r.value), onChange: (e) => toggle(r, e.currentTarget.checked) }, r.value))
|
|
245
|
+
] }),
|
|
246
|
+
/* @__PURE__ */ jsx("p", { className: cx("uix-filter-editor__note", "uix-filter-editor__searching", !slow && "uix-visually-hidden"), role: "status", "data-shown": slow || void 0, children: slow ? labels.searching : "" })
|
|
247
|
+
] });
|
|
248
|
+
}
|
|
249
|
+
const splitTags = (text) => text.split(/[,\n]/).map((t) => t.trim()).filter(Boolean);
|
|
250
|
+
function TagsBody({ value, onChange, labels, pending, id }) {
|
|
251
|
+
const [draft, setDraft] = useState("");
|
|
252
|
+
const field = useRef(null);
|
|
253
|
+
const withTags = (tags) => ({ kind: "tags", values: [...value.values, ...tags.filter((t, i) => !value.values.includes(t) && tags.indexOf(t) === i)] });
|
|
254
|
+
pending.current = () => splitTags(draft).length ? withTags(splitTags(draft)) : void 0;
|
|
255
|
+
const commit = (text) => {
|
|
256
|
+
const tags = splitTags(text);
|
|
257
|
+
if (tags.length) onChange(withTags(tags));
|
|
258
|
+
setDraft("");
|
|
259
|
+
};
|
|
260
|
+
const remove = (tag) => {
|
|
261
|
+
onChange({ kind: "tags", values: value.values.filter((t) => t !== tag) });
|
|
262
|
+
field.current?.focus();
|
|
263
|
+
};
|
|
264
|
+
const onKeyDown = (event) => {
|
|
265
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
266
|
+
if ((event.key === "Enter" || event.key === ",") && draft.trim()) {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
commit(draft);
|
|
269
|
+
} else if (event.key === ",") {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
} else if (event.key === "Backspace" && draft === "" && value.values.length > 0) {
|
|
272
|
+
onChange({ kind: "tags", values: value.values.slice(0, -1) });
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const onPaste = (event) => {
|
|
276
|
+
const text = event.clipboardData.getData("text");
|
|
277
|
+
if (!/[,\n]/.test(text)) return;
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
commit(`${draft}${text}`);
|
|
280
|
+
};
|
|
281
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-taginput uix-filter-editor__tags", children: [
|
|
282
|
+
value.values.map((tag) => /* @__PURE__ */ jsxs("span", { className: "uix-tag", children: [
|
|
283
|
+
tag,
|
|
284
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-tag__remove", "aria-label": fillLabel(labels.remove, { label: tag }), onClick: () => remove(tag), children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) })
|
|
285
|
+
] }, tag)),
|
|
286
|
+
/* @__PURE__ */ jsx(
|
|
287
|
+
"input",
|
|
288
|
+
{
|
|
289
|
+
ref: field,
|
|
290
|
+
className: "uix-taginput__field",
|
|
291
|
+
"aria-label": labels.addTag ?? DEFAULT_FILTER_EDITOR_LABELS.addTag,
|
|
292
|
+
"aria-describedby": `${id}-tags-hint`,
|
|
293
|
+
placeholder: labels.addTag ?? DEFAULT_FILTER_EDITOR_LABELS.addTag,
|
|
294
|
+
value: draft,
|
|
295
|
+
enterKeyHint: "enter",
|
|
296
|
+
onChange: (e) => setDraft(e.currentTarget.value),
|
|
297
|
+
onKeyDown,
|
|
298
|
+
onPaste,
|
|
299
|
+
autoFocus: true
|
|
300
|
+
}
|
|
301
|
+
),
|
|
302
|
+
/* @__PURE__ */ jsx("span", { id: `${id}-tags-hint`, className: "uix-visually-hidden", children: labels.addTagHint ?? DEFAULT_FILTER_EDITOR_LABELS.addTagHint })
|
|
181
303
|
] });
|
|
182
304
|
}
|
|
183
305
|
export {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { helpParagraphs } from "../info-tip-model.js";
|
|
6
6
|
import { Popover } from "./Popover.js";
|
|
7
7
|
const OPEN_DELAY = 300;
|
|
8
8
|
const CLOSE_DELAY = 100;
|
|
9
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
9
10
|
const QuestionMark = () => /* @__PURE__ */ jsx("svg", { viewBox: "262 -840 436 760", fill: "currentColor", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M424-320q0-81 14.5-116.5T500-514q41-36 62.5-62.5T584-637q0-41-27.5-68T480-732q-51 0-77.5 31T365-638l-103-44q21-64 77-111t141-47q105 0 161.5 58.5T698-641q0 50-21.5 85.5T609-475q-49 47-59.5 71.5T539-320H424Zm56 240q-33 0-56.5-23.5T400-160q0-33 23.5-56.5T480-240q33 0 56.5 23.5T560-160q0 33-23.5 56.5T480-80Z" }) });
|
|
10
11
|
const isShown = (el) => {
|
|
11
12
|
try {
|
|
@@ -93,18 +94,21 @@ function InfoTipControl({
|
|
|
93
94
|
refocusing.current = false;
|
|
94
95
|
}
|
|
95
96
|
};
|
|
96
|
-
|
|
97
|
-
if (!open) return;
|
|
97
|
+
useIsomorphicLayoutEffect(() => {
|
|
98
|
+
if (!open) return void 0;
|
|
98
99
|
const onKey = (e) => {
|
|
99
|
-
if (e.key
|
|
100
|
+
if (e.key !== "Escape" || !openRef.current) return;
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
escape();
|
|
100
104
|
};
|
|
101
105
|
const onPointer = (e) => {
|
|
102
106
|
if (!wrapRef.current?.contains(e.target)) hide();
|
|
103
107
|
};
|
|
104
|
-
document.addEventListener("keydown", onKey);
|
|
108
|
+
document.addEventListener("keydown", onKey, true);
|
|
105
109
|
document.addEventListener("pointerdown", onPointer, true);
|
|
106
110
|
return () => {
|
|
107
|
-
document.removeEventListener("keydown", onKey);
|
|
111
|
+
document.removeEventListener("keydown", onKey, true);
|
|
108
112
|
document.removeEventListener("pointerdown", onPointer, true);
|
|
109
113
|
};
|
|
110
114
|
}, [open]);
|
|
@@ -148,13 +152,8 @@ function InfoTipControl({
|
|
|
148
152
|
pinned.current = true;
|
|
149
153
|
}
|
|
150
154
|
},
|
|
151
|
-
onKeyDown: (
|
|
155
|
+
onKeyDown: () => {
|
|
152
156
|
pressing.current = false;
|
|
153
|
-
if (e.key === "Escape" && openRef.current) {
|
|
154
|
-
e.preventDefault();
|
|
155
|
-
e.stopPropagation();
|
|
156
|
-
hide();
|
|
157
|
-
}
|
|
158
157
|
},
|
|
159
158
|
children: /* @__PURE__ */ jsx(QuestionMark, {})
|
|
160
159
|
}
|
|
@@ -3,11 +3,11 @@ import { ReactNode } from 'react';
|
|
|
3
3
|
|
|
4
4
|
/** Every word `InlineEdit` renders or announces. `{label}` is the field's name. */
|
|
5
5
|
interface InlineEditLabels {
|
|
6
|
-
/** Added to the view button's name after the value: "…, Edit {label}". */
|
|
6
|
+
/** Added to the view button's name after the value: "…, Edit {label}"; the icon trigger's whole name. */
|
|
7
7
|
edit: string;
|
|
8
8
|
save: string;
|
|
9
9
|
cancel: string;
|
|
10
|
-
/** Polite status while `onSave` runs. */
|
|
10
|
+
/** Polite status while `onSave` runs (shown after 300 ms, so a fast save does not flash it). */
|
|
11
11
|
saving: string;
|
|
12
12
|
/** Polite status after a save. */
|
|
13
13
|
saved: string;
|
|
@@ -15,50 +15,114 @@ interface InlineEditLabels {
|
|
|
15
15
|
failed: string;
|
|
16
16
|
/** What the view shows when the value is empty and no `placeholder` was given. */
|
|
17
17
|
empty: string;
|
|
18
|
+
/** Visually hidden before a `readOnlyReason`, so it is read as one: "Read-only: …". */
|
|
19
|
+
readOnly: string;
|
|
18
20
|
}
|
|
19
21
|
declare const DEFAULT_INLINE_EDIT_LABELS: InlineEditLabels;
|
|
22
|
+
/** Where an `InlineEdit` is (HAR-1833). `saved` holds until the field is opened again. */
|
|
23
|
+
type InlineEditState = 'idle' | 'editing' | 'saving' | 'saved' | 'error';
|
|
20
24
|
/** What a custom editor receives. Spread the field props onto your control. */
|
|
21
|
-
interface InlineEditEditorProps {
|
|
25
|
+
interface InlineEditEditorProps<T = string> {
|
|
22
26
|
/** The draft. */
|
|
23
|
-
value:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
value: T;
|
|
28
|
+
/** Change the draft. With `commit="change"` this also saves it. Ignored while a save runs. */
|
|
29
|
+
onChange: (next: T) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Save the draft (what Enter does in the default editor). Pass the value to save one you
|
|
32
|
+
* just chose: `save(next)` does not wait for the draft to update, so it never saves a stale one.
|
|
33
|
+
*/
|
|
34
|
+
save: (next?: T) => void;
|
|
27
35
|
cancel: () => void;
|
|
28
|
-
/** `id`, the accessible name, the error wiring and
|
|
36
|
+
/** `id`, the accessible name, the error wiring, and the busy state while saving. */
|
|
29
37
|
field: {
|
|
30
38
|
id: string;
|
|
31
39
|
'aria-label': string;
|
|
32
40
|
'aria-describedby': string | undefined;
|
|
33
41
|
'aria-invalid': true | undefined;
|
|
42
|
+
/** True once a save has run for 300 ms (changes are ignored from its start, so a fast save never greys the control). */
|
|
34
43
|
disabled: boolean;
|
|
44
|
+
/** The same moment as `disabled`, for a text control that should keep focus and its text selectable. */
|
|
45
|
+
readOnly: boolean;
|
|
35
46
|
};
|
|
36
47
|
}
|
|
37
|
-
|
|
48
|
+
/** What `renderError` receives besides the rejection itself. */
|
|
49
|
+
interface InlineEditErrorContext<T = string> {
|
|
50
|
+
/** The rejection's message, else `labels.failed`: what the default rendering shows. */
|
|
51
|
+
message: ReactNode;
|
|
52
|
+
/** The draft that failed. */
|
|
53
|
+
draft: T;
|
|
54
|
+
/** Send the draft again. Show it when a retry can help (a timeout, not a refusal). */
|
|
55
|
+
retry: () => void;
|
|
56
|
+
/** Close the editor and drop the draft, e.g. after "Reload latest" on a version conflict. */
|
|
57
|
+
cancel: () => void;
|
|
58
|
+
}
|
|
59
|
+
interface InlineEditProps<T = string> {
|
|
38
60
|
/** The saved value. */
|
|
39
|
-
value:
|
|
61
|
+
value: T;
|
|
40
62
|
/**
|
|
41
63
|
* Save the new value. Return a promise to show the pending state; a rejection keeps the
|
|
42
64
|
* editor open with the error under it (the rejection's message, else `labels.failed`).
|
|
65
|
+
* The editor waits for it: the value shows as saved only once it resolves.
|
|
43
66
|
*/
|
|
44
|
-
onSave: (next:
|
|
67
|
+
onSave: (next: T) => void | Promise<unknown>;
|
|
45
68
|
/** The field's name: "Title", "Due date". It names the edit button and the editor. */
|
|
46
69
|
label: string;
|
|
47
|
-
/**
|
|
48
|
-
|
|
70
|
+
/**
|
|
71
|
+
* How the saved value shows. Default: the text itself (`String(value)`); pass it for any
|
|
72
|
+
* value that is not a string (an option, a person, a time window).
|
|
73
|
+
*/
|
|
74
|
+
renderView?: (value: T) => ReactNode;
|
|
49
75
|
/**
|
|
50
76
|
* A custom editor: a `Select`, a `Combobox`, a date field. Spread `field` onto the control.
|
|
51
|
-
* Default: an `Input`, or a `Textarea` with `multiline
|
|
77
|
+
* Default: an `Input`, or a `Textarea` with `multiline` (string values only).
|
|
52
78
|
*/
|
|
53
|
-
renderEditor?: (props: InlineEditEditorProps) => ReactNode;
|
|
79
|
+
renderEditor?: (props: InlineEditEditorProps<T>) => ReactNode;
|
|
54
80
|
/** Edit in a textarea: Enter adds a line, Ctrl/⌘+Enter saves. */
|
|
55
81
|
multiline?: boolean;
|
|
56
82
|
/** Return a message to refuse the draft; nothing is saved while it returns one. */
|
|
57
|
-
validate?: (next:
|
|
83
|
+
validate?: (next: T) => ReactNode | undefined;
|
|
84
|
+
/** Whether two values are the same (an unchanged draft saves nothing). Default `Object.is`. */
|
|
85
|
+
equals?: (a: T, b: T) => boolean;
|
|
86
|
+
/** Whether a value counts as empty (the view then shows `placeholder`). Default: `null`, `undefined` or `''`. */
|
|
87
|
+
isEmpty?: (value: T) => boolean;
|
|
88
|
+
/**
|
|
89
|
+
* When the draft is saved (HAR-1833).
|
|
90
|
+
* `'explicit'` (default): Enter or Save only; leaving the field keeps the draft open.
|
|
91
|
+
* `'blur'`: also when focus leaves the editor (text fields of a record).
|
|
92
|
+
* `'change'`: as soon as the editor reports a change (a select, a toggle, a date), with no
|
|
93
|
+
* Save button; leaving it unchanged closes it.
|
|
94
|
+
*/
|
|
95
|
+
commit?: 'explicit' | 'blur' | 'change';
|
|
58
96
|
/** Shown in the view while the value is empty, e.g. "Add a description". */
|
|
59
97
|
placeholder?: ReactNode;
|
|
60
|
-
/** Read-only: the value shows as plain text, with no edit
|
|
98
|
+
/** Read-only: the value shows as plain text, with no edit control. */
|
|
61
99
|
disabled?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Why the field cannot be edited here ("Set by the integration"). Makes it read-only and
|
|
102
|
+
* shows the reason under the value, which is described by it.
|
|
103
|
+
*/
|
|
104
|
+
readOnlyReason?: ReactNode;
|
|
105
|
+
/**
|
|
106
|
+
* `'value'` (default): the whole value is the button, named "{value}, Edit {label}".
|
|
107
|
+
* `'icon'`: the value is plain text and a pencil button after it is named only "Edit {label}"
|
|
108
|
+
* (for a value that is itself a link, a chip or long text).
|
|
109
|
+
*/
|
|
110
|
+
trigger?: 'value' | 'icon';
|
|
111
|
+
/** Controlled open state, e.g. one field of a record open at a time. */
|
|
112
|
+
editing?: boolean;
|
|
113
|
+
/** Called when the field asks to open (`true`) or close (`false`). */
|
|
114
|
+
onEditingChange?: (editing: boolean) => void;
|
|
115
|
+
/** Controlled draft, e.g. to report unsaved input. Reset to `value` on open. */
|
|
116
|
+
draft?: T;
|
|
117
|
+
/** Called with every change of the draft, and with `value` when the field opens. */
|
|
118
|
+
onDraftChange?: (draft: T) => void;
|
|
119
|
+
/**
|
|
120
|
+
* Renders a failed save under the editor, in place of the message: a Try again button that
|
|
121
|
+
* calls `retry`, or a Reload latest after a version conflict.
|
|
122
|
+
*/
|
|
123
|
+
renderError?: (error: unknown, context: InlineEditErrorContext<T>) => ReactNode;
|
|
124
|
+
/** Called on every change of `InlineEditState`. */
|
|
125
|
+
onStateChange?: (state: InlineEditState) => void;
|
|
62
126
|
/** Each word defaults to `UixLabelsProvider` `inlineEdit`, then to English. */
|
|
63
127
|
labels?: Partial<InlineEditLabels>;
|
|
64
128
|
className?: string;
|
|
@@ -67,8 +131,11 @@ interface InlineEditProps {
|
|
|
67
131
|
* A value that is edited in place (HAR-1381; TENSOR C16 `editable-record.tsx`): the value is
|
|
68
132
|
* a button that says what it edits; activating it swaps in an editor with Save and Cancel.
|
|
69
133
|
* Enter saves and Escape cancels; while `onSave` runs the editor is busy; a failed save keeps
|
|
70
|
-
* the draft and shows why; afterwards focus is back on the value
|
|
134
|
+
* the draft and shows why; afterwards focus is back on the value, which shows what was saved
|
|
135
|
+
* until `value` catches up. HAR-1833: any value type (`equals`, `renderView`, `renderEditor`),
|
|
136
|
+
* saving on blur or on change (`commit`), controllable `editing` and `draft`, `renderError`
|
|
137
|
+
* with retry, `onStateChange`, an icon trigger, and a `readOnlyReason`.
|
|
71
138
|
*/
|
|
72
|
-
declare function InlineEdit({ value, onSave, label, renderView, renderEditor, multiline, validate, placeholder, disabled, labels: labelOverrides, className, }: InlineEditProps): react.JSX.Element;
|
|
139
|
+
declare function InlineEdit<T = string>({ value, onSave, label, renderView, renderEditor, multiline, validate, equals, isEmpty, commit, placeholder, disabled, readOnlyReason, trigger, editing: editingProp, onEditingChange, draft: draftProp, onDraftChange, renderError, onStateChange, labels: labelOverrides, className, }: InlineEditProps<T>): react.JSX.Element;
|
|
73
140
|
|
|
74
|
-
export { DEFAULT_INLINE_EDIT_LABELS, InlineEdit, type InlineEditEditorProps, type InlineEditLabels, type InlineEditProps };
|
|
141
|
+
export { DEFAULT_INLINE_EDIT_LABELS, InlineEdit, type InlineEditEditorProps, type InlineEditErrorContext, type InlineEditLabels, type InlineEditProps, type InlineEditState };
|