@homebound/beam 2.266.0 → 2.267.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/dist/components/Filters/TreeFilter.d.ts +14 -0
- package/dist/components/Filters/TreeFilter.js +16 -0
- package/dist/components/Filters/testDomain.d.ts +12 -0
- package/dist/forms/BoundTreeSelectField.d.ts +22 -0
- package/dist/forms/BoundTreeSelectField.js +29 -0
- package/dist/forms/FormStateApp.js +18 -1
- package/dist/forms/formStateDomain.d.ts +1 -0
- package/dist/inputs/TreeSelectField/TreeSelectField.js +23 -4
- package/dist/inputs/internal/ComboBoxBase.js +8 -6
- package/dist/inputs/internal/VirtualizedOptions.js +6 -5
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Key } from "react";
|
|
2
|
+
import { Filter } from "./types";
|
|
3
|
+
import { TreeSelectFieldProps, Value } from "../../inputs";
|
|
4
|
+
import { TreeSelectResponse } from "../../inputs/TreeSelectField/utils";
|
|
5
|
+
export type TreeFilterProps<O, V extends Value> = Omit<TreeSelectFieldProps<O, V>, "values" | "onSelect" | "label"> & {
|
|
6
|
+
defaultValue?: V[];
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Defines which of the tree values to use in the filter - "root", "leaf", or "all"
|
|
9
|
+
* @default "root" */
|
|
10
|
+
filterBy?: TreeFilterBy;
|
|
11
|
+
};
|
|
12
|
+
type TreeFilterBy = keyof TreeSelectResponse<any, any>;
|
|
13
|
+
export declare function treeFilter<O, V extends Key>(props: TreeFilterProps<O, V>): (key: string) => Filter<V[]>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.treeFilter = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
|
|
5
|
+
const BaseFilter_1 = require("./BaseFilter");
|
|
6
|
+
const inputs_1 = require("../../inputs");
|
|
7
|
+
function treeFilter(props) {
|
|
8
|
+
return (key) => new TreeFilter(key, props);
|
|
9
|
+
}
|
|
10
|
+
exports.treeFilter = treeFilter;
|
|
11
|
+
class TreeFilter extends BaseFilter_1.BaseFilter {
|
|
12
|
+
render(value, setValue, tid, inModal, vertical) {
|
|
13
|
+
const { defaultValue, nothingSelectedText, filterBy = "root", ...props } = this.props;
|
|
14
|
+
return ((0, jsx_runtime_1.jsx)(inputs_1.TreeSelectField, { ...props, label: this.label, values: value, compact: !vertical, labelStyle: inModal ? "hidden" : !inModal && !vertical ? "inline" : "above", sizeToContent: !inModal && !vertical, onSelect: (options) => setValue(options[filterBy].values), nothingSelectedText: nothingSelectedText !== null && nothingSelectedText !== void 0 ? nothingSelectedText : "All", ...this.testId(tid) }));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -20,6 +20,7 @@ export type Status = {
|
|
|
20
20
|
name: string;
|
|
21
21
|
};
|
|
22
22
|
export type Project = {
|
|
23
|
+
name: string;
|
|
23
24
|
id: string;
|
|
24
25
|
internalUser: InternalUser;
|
|
25
26
|
market: Market;
|
|
@@ -30,6 +31,16 @@ export type Project = {
|
|
|
30
31
|
doNotUse: boolean;
|
|
31
32
|
isStale: boolean;
|
|
32
33
|
};
|
|
34
|
+
export type Cohort = {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
projects: Project[];
|
|
38
|
+
};
|
|
39
|
+
export type Development = {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
cohorts: Cohort[];
|
|
43
|
+
};
|
|
33
44
|
export type ProjectFilter = {
|
|
34
45
|
marketId?: string[] | null;
|
|
35
46
|
internalUserId?: string | null;
|
|
@@ -43,6 +54,7 @@ export type ProjectFilter = {
|
|
|
43
54
|
dateRange?: DateRangeFilterValue<string>;
|
|
44
55
|
numberRange?: NumberRangeFilterValue;
|
|
45
56
|
isStale?: boolean | null;
|
|
57
|
+
projectCohortDevelopment?: string[];
|
|
46
58
|
};
|
|
47
59
|
export type StageFilter = NonNullable<FilterDefs<ProjectFilter>["stage"]>;
|
|
48
60
|
export type StageSingleFilter = NonNullable<FilterDefs<ProjectFilter>["stageSingle"]>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { FieldState } from "@homebound/form-state";
|
|
3
|
+
import { TreeSelectFieldProps, Value } from "../inputs";
|
|
4
|
+
import { TreeSelectResponse } from "../inputs/TreeSelectField/utils";
|
|
5
|
+
import { HasIdAndName, Optional } from "../types";
|
|
6
|
+
export type BoundTreeSelectFieldProps<O, V extends Value> = Omit<TreeSelectFieldProps<O, V>, "values" | "onSelect" | "label"> & {
|
|
7
|
+
onSelect?: (options: TreeSelectResponse<O, V>) => void;
|
|
8
|
+
field: FieldState<V[] | null | undefined>;
|
|
9
|
+
label?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Wraps `TreeSelectField` and binds it to a form field.
|
|
13
|
+
*
|
|
14
|
+
* To ease integration with "select this fooId" inputs, we can take a list
|
|
15
|
+
* of objects, `T` (i.e. `TradePartner[]`), but accept a field of type `V`
|
|
16
|
+
* (i.e. `string`).
|
|
17
|
+
*
|
|
18
|
+
* The caller has to tell us how to turn `T` into `V`, which is usually a
|
|
19
|
+
* lambda like `t => t.id`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function BoundTreeSelectField<T, V extends Value>(props: BoundTreeSelectFieldProps<T, V>): JSX.Element;
|
|
22
|
+
export declare function BoundTreeSelectField<T extends HasIdAndName<V>, V extends Value>(props: Optional<BoundTreeSelectFieldProps<T, V>, "getOptionLabel" | "getOptionValue">): JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BoundTreeSelectField = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
|
|
5
|
+
const mobx_react_1 = require("mobx-react");
|
|
6
|
+
const inputs_1 = require("../inputs");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
const defaultLabel_1 = require("../utils/defaultLabel");
|
|
9
|
+
const useTestIds_1 = require("../utils/useTestIds");
|
|
10
|
+
function BoundTreeSelectField(props) {
|
|
11
|
+
const { field, options, readOnly, getOptionValue = (opt) => opt.id, // if unset, assume O implements HasId
|
|
12
|
+
getOptionLabel = (opt) => opt.name, // if unset, assume O implements HasName
|
|
13
|
+
onSelect = (options) => field.set(options.all.values), label = (0, defaultLabel_1.defaultLabel)(field.key), onBlur, onFocus, ...others } = props;
|
|
14
|
+
const testId = (0, useTestIds_1.useTestIds)(props, field.key);
|
|
15
|
+
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => {
|
|
16
|
+
var _a;
|
|
17
|
+
return ((0, jsx_runtime_1.jsx)(inputs_1.TreeSelectField, { label: label, values: (_a = field.value) !== null && _a !== void 0 ? _a : undefined, onSelect: (options) => {
|
|
18
|
+
onSelect(options);
|
|
19
|
+
field.maybeAutoSave();
|
|
20
|
+
}, options: options, readOnly: readOnly !== null && readOnly !== void 0 ? readOnly : field.readOnly, errorMsg: field.touched ? field.errors.join(" ") : undefined, required: field.required, getOptionLabel: getOptionLabel, getOptionValue: getOptionValue, onBlur: () => {
|
|
21
|
+
field.blur();
|
|
22
|
+
(0, utils_1.maybeCall)(onBlur);
|
|
23
|
+
}, onFocus: () => {
|
|
24
|
+
field.focus();
|
|
25
|
+
(0, utils_1.maybeCall)(onFocus);
|
|
26
|
+
}, ...others, ...testId }));
|
|
27
|
+
} }));
|
|
28
|
+
}
|
|
29
|
+
exports.BoundTreeSelectField = BoundTreeSelectField;
|
|
@@ -9,6 +9,7 @@ const components_1 = require("../components");
|
|
|
9
9
|
const Css_1 = require("../Css");
|
|
10
10
|
const forms_1 = require("./");
|
|
11
11
|
const BoundCheckboxGroupField_1 = require("./BoundCheckboxGroupField");
|
|
12
|
+
const BoundTreeSelectField_1 = require("./BoundTreeSelectField");
|
|
12
13
|
const FormLines_1 = require("./FormLines");
|
|
13
14
|
const hooks_1 = require("../hooks");
|
|
14
15
|
function FormStateApp() {
|
|
@@ -56,7 +57,22 @@ function FormStateApp() {
|
|
|
56
57
|
{ value: "a:4", label: "Iguana" },
|
|
57
58
|
{ value: "a:5", label: "Turtle" },
|
|
58
59
|
];
|
|
59
|
-
|
|
60
|
+
const genres = [
|
|
61
|
+
{
|
|
62
|
+
id: "g:1",
|
|
63
|
+
name: "Action",
|
|
64
|
+
children: [
|
|
65
|
+
{
|
|
66
|
+
id: "g:2",
|
|
67
|
+
name: "Action Adventure",
|
|
68
|
+
children: [{ id: "g:3", name: "Action Adventure Comedy" }],
|
|
69
|
+
},
|
|
70
|
+
{ id: "g:4", name: "Action Comedy" },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{ id: "g:5", name: "Comedy", children: [{ id: "g:6", name: "Comedy Drama" }] },
|
|
74
|
+
];
|
|
75
|
+
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.$, children: [(0, jsx_runtime_1.jsxs)("header", { css: Css_1.Css.wPx(700).$, children: [(0, jsx_runtime_1.jsxs)(FormLines_1.FormLines, { labelSuffix: { required: "*", optional: "(Opt)" }, children: [(0, jsx_runtime_1.jsx)("b", { children: "Author" }), (0, jsx_runtime_1.jsx)(forms_1.BoundTextField, { field: formState.firstName }), (0, jsx_runtime_1.jsx)(forms_1.BoundTextField, { field: formState.middleInitial }), (0, jsx_runtime_1.jsx)(forms_1.BoundTextField, { field: formState.lastName }), (0, jsx_runtime_1.jsx)(forms_1.BoundDateField, { field: formState.birthday }), (0, jsx_runtime_1.jsxs)(forms_1.FieldGroup, { children: [(0, jsx_runtime_1.jsx)(forms_1.StaticField, { label: "Revenue", value: "$500" }), (0, jsx_runtime_1.jsx)(forms_1.StaticField, { label: "Website", children: (0, jsx_runtime_1.jsx)("a", { href: "https://google.com", children: "google.com" }) })] }), (0, jsx_runtime_1.jsx)(forms_1.BoundNumberField, { field: formState.heightInInches }), (0, jsx_runtime_1.jsx)(forms_1.FormDivider, {}), (0, jsx_runtime_1.jsx)(forms_1.BoundSelectField, { field: formState.favoriteSport, options: sports }), (0, jsx_runtime_1.jsx)(forms_1.BoundMultiSelectField, { field: formState.favoriteShapes, options: shapes }), (0, jsx_runtime_1.jsx)(BoundTreeSelectField_1.BoundTreeSelectField, { field: formState.favoriteGenres, options: genres }), (0, jsx_runtime_1.jsx)(forms_1.FormDivider, {}), (0, jsx_runtime_1.jsx)(BoundCheckboxGroupField_1.BoundCheckboxGroupField, { field: formState.favoriteColors, options: colors }), (0, jsx_runtime_1.jsx)(forms_1.BoundToggleChipGroupField, { field: formState.animals, options: animals }), (0, jsx_runtime_1.jsx)(forms_1.FormDivider, {}), (0, jsx_runtime_1.jsx)(forms_1.BoundSwitchField, { field: formState.isAvailable })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("strong", { children: ["Books", (0, jsx_runtime_1.jsx)(components_1.IconButton, { icon: "plus", onClick: () => formState.books.add({ id: String(formState.books.value.length) }) })] }), (0, jsx_runtime_1.jsx)(components_1.GridTable, { columns: columns, rows: rows })] }), (0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.gap1.$, children: [(0, jsx_runtime_1.jsx)(components_1.Button, { onClick: () => formState.revertChanges(), label: "Cancel" }), (0, jsx_runtime_1.jsx)(components_1.Button, { onClick: () => {
|
|
60
76
|
if (formState.canSave()) {
|
|
61
77
|
formState.commitChanges();
|
|
62
78
|
}
|
|
@@ -86,6 +102,7 @@ exports.formConfig = {
|
|
|
86
102
|
favoriteSport: { type: "value" },
|
|
87
103
|
favoriteColors: { type: "value", rules: [form_state_1.required] },
|
|
88
104
|
favoriteShapes: { type: "value", rules: [form_state_1.required] },
|
|
105
|
+
favoriteGenres: { type: "value", rules: [form_state_1.required] },
|
|
89
106
|
books: {
|
|
90
107
|
type: "list",
|
|
91
108
|
rules: [({ value }) => ((value || []).length === 0 ? "Empty" : undefined)],
|
|
@@ -111,7 +111,11 @@ function TreeSelectFieldBase(props) {
|
|
|
111
111
|
initialOptions.forEach(areAllChildrenSelected);
|
|
112
112
|
return {
|
|
113
113
|
selectedKeys,
|
|
114
|
-
inputValue: selectedOptions.length === 1
|
|
114
|
+
inputValue: selectedOptions.length === 1
|
|
115
|
+
? getOptionLabel(selectedOptions[0])
|
|
116
|
+
: selectedOptions.length === 0
|
|
117
|
+
? nothingSelectedText
|
|
118
|
+
: "",
|
|
115
119
|
filteredOptions,
|
|
116
120
|
selectedOptions,
|
|
117
121
|
allOptions: initialOptions,
|
|
@@ -166,6 +170,10 @@ function TreeSelectFieldBase(props) {
|
|
|
166
170
|
maybeInitLoad(options, fieldState, setFieldState);
|
|
167
171
|
firstOpen.current = false;
|
|
168
172
|
}
|
|
173
|
+
if (isOpen) {
|
|
174
|
+
// reset the input field to allow the user to start typing to filter
|
|
175
|
+
setFieldState((prevState) => ({ ...prevState, inputValue: "" }));
|
|
176
|
+
}
|
|
169
177
|
}
|
|
170
178
|
// This is _always_ going to appear new. Maybe `useMemo`?
|
|
171
179
|
const comboBoxProps = {
|
|
@@ -187,6 +195,7 @@ function TreeSelectFieldBase(props) {
|
|
|
187
195
|
const state = (0, react_stately_1.useComboBoxState)({
|
|
188
196
|
...comboBoxProps,
|
|
189
197
|
allowsEmptyCollection: true,
|
|
198
|
+
allowsCustomValue: true,
|
|
190
199
|
});
|
|
191
200
|
// @ts-ignore - `selectionManager.state` exists, but not according to the types. We are tricking the ComboBox state to support multiple selections.
|
|
192
201
|
state.selectionManager.state = (0, react_stately_1.useMultipleSelectionState)({
|
|
@@ -205,7 +214,12 @@ function TreeSelectFieldBase(props) {
|
|
|
205
214
|
if (addedKeys.size > 0 || removedKeys.size > 0) {
|
|
206
215
|
// Quickly return out of this if all selections are removed
|
|
207
216
|
if (newKeys.size === 0) {
|
|
208
|
-
setFieldState((prevState) => ({
|
|
217
|
+
setFieldState((prevState) => ({
|
|
218
|
+
...prevState,
|
|
219
|
+
inputValue: nothingSelectedText,
|
|
220
|
+
selectedKeys: [],
|
|
221
|
+
selectedOptions: [],
|
|
222
|
+
}));
|
|
209
223
|
onSelect({
|
|
210
224
|
all: { values: [], options: [] },
|
|
211
225
|
leaf: { values: [], options: [] },
|
|
@@ -298,10 +312,15 @@ function TreeSelectFieldBase(props) {
|
|
|
298
312
|
// Resets the TreeFieldState when the 'blur' event is triggered on the input.
|
|
299
313
|
function resetField() {
|
|
300
314
|
const { inputValue, selectedOptions } = fieldState;
|
|
301
|
-
if (inputValue !==
|
|
315
|
+
if (inputValue !== nothingSelectedText ||
|
|
316
|
+
(selectedOptions.length === 1 && inputValue !== getOptionLabel(selectedOptions[0]))) {
|
|
302
317
|
setFieldState((prevState) => ({
|
|
303
318
|
...prevState,
|
|
304
|
-
inputValue: selectedOptions.length === 1
|
|
319
|
+
inputValue: selectedOptions.length === 1
|
|
320
|
+
? getOptionLabel(selectedOptions[0])
|
|
321
|
+
: selectedOptions.length === 0
|
|
322
|
+
? nothingSelectedText
|
|
323
|
+
: "",
|
|
305
324
|
filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0)),
|
|
306
325
|
allowCollapsing: true,
|
|
307
326
|
}));
|
|
@@ -133,11 +133,10 @@ function ComboBoxBase(props) {
|
|
|
133
133
|
maybeInitLoad();
|
|
134
134
|
firstOpen.current = false;
|
|
135
135
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}));
|
|
136
|
+
// When using the multiselect field, always empty the input upon open.
|
|
137
|
+
if (multiselect && isOpen) {
|
|
138
|
+
setFieldState((prevState) => ({ ...prevState, inputValue: "" }));
|
|
139
|
+
}
|
|
141
140
|
}
|
|
142
141
|
// Used to calculate the rendered width of the combo box (input + button)
|
|
143
142
|
const comboBoxRef = (0, react_1.useRef)(null);
|
|
@@ -163,6 +162,9 @@ function ComboBoxBase(props) {
|
|
|
163
162
|
const state = (0, react_stately_1.useComboBoxState)({
|
|
164
163
|
...comboBoxProps,
|
|
165
164
|
allowsEmptyCollection: true,
|
|
165
|
+
// We don't really allow custom values, as we reset the input value once a user `blur`s the input field.
|
|
166
|
+
// Though, setting `allowsCustomValue: true` prevents React-Aria/Stately from attempting to reset the input field's value when the menu closes.
|
|
167
|
+
allowsCustomValue: true,
|
|
166
168
|
// useComboBoxState.onSelectionChange will be executed if a keyboard interaction (Enter key) is used to select an item
|
|
167
169
|
onSelectionChange: (key) => {
|
|
168
170
|
// ignore undefined/null keys - `null` can happen if input field's value is completely deleted after having a value assigned.
|
|
@@ -250,7 +252,7 @@ function ComboBoxBase(props) {
|
|
|
250
252
|
// Ensures the menu never gets too small.
|
|
251
253
|
minWidth: 200,
|
|
252
254
|
};
|
|
253
|
-
return ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.fdc.w100.maxw((0, Css_1.px)(550)).if(labelStyle === "left").maxw100.$, ref: comboBoxRef, children: [(0, jsx_runtime_1.jsx)(ComboBoxInput_1.ComboBoxInput, { ...otherProps, buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, state: state, labelProps: labelProps, selectedOptions: fieldState.selectedOptions, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, contrast: contrast, nothingSelectedText: nothingSelectedText, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly), resetField: resetField,
|
|
255
|
+
return ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.fdc.w100.maxw((0, Css_1.px)(550)).if(labelStyle === "left").maxw100.$, ref: comboBoxRef, children: [(0, jsx_runtime_1.jsx)(ComboBoxInput_1.ComboBoxInput, { ...otherProps, buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, listBoxRef: listBoxRef, state: state, labelProps: labelProps, selectedOptions: fieldState.selectedOptions, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, contrast: contrast, nothingSelectedText: nothingSelectedText, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly), resetField: resetField,
|
|
254
256
|
// If there are 10 or fewer options and it is not the multiselect, then we disable the typeahead filter for a better UX.
|
|
255
257
|
typeToFilter: !(!multiselect && Array.isArray(options) && options.length <= 10) }), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, { triggerRef: triggerRef, popoverRef: popoverRef, positionProps: positionProps, onClose: () => state.close(), isOpen: state.isOpen, minWidth: 200, children: (0, jsx_runtime_1.jsx)(ListBox_1.ListBox, { ...listBoxProps, positionProps: positionProps, state: state, listBoxRef: listBoxRef, selectedOptions: fieldState.selectedOptions, getOptionLabel: getOptionLabel, getOptionValue: (o) => (0, Value_1.valueToKey)(getOptionValue(o)), contrast: contrast, horizontalLayout: labelStyle === "left", loading: fieldState.optionsLoading, disabledOptionsWithReasons: disabledOptionsWithReasons }) }))] }));
|
|
256
258
|
}
|
|
@@ -24,11 +24,12 @@ function VirtualizedOptions(props) {
|
|
|
24
24
|
}, [focusedItem]);
|
|
25
25
|
return ((0, jsx_runtime_1.jsx)(react_virtuoso_1.Virtuoso, { ref: virtuosoRef, totalListHeightChanged: onListHeightChange, totalCount: items.length,
|
|
26
26
|
// Ensure the selected item is visible when the list renders
|
|
27
|
-
initialTopMostItemIndex: selectedItem ? selectedItem.index : 0,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
initialTopMostItemIndex: selectedItem ? selectedItem.index : 0, ...(process.env.NODE_ENV === "test"
|
|
28
|
+
? {
|
|
29
|
+
initialItemCount: items.length,
|
|
30
|
+
key: items.length,
|
|
31
|
+
}
|
|
32
|
+
: {}), itemContent: (idx) => {
|
|
32
33
|
var _a;
|
|
33
34
|
const item = items[idx];
|
|
34
35
|
if (item) {
|