@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.
@@ -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
- 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)(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
+ 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)],
@@ -23,6 +23,7 @@ export interface AuthorInput {
23
23
  animals?: string[] | null;
24
24
  bio?: string | null;
25
25
  saleDates?: DateRange | null;
26
+ favoriteGenres?: string[] | null;
26
27
  }
27
28
  export interface AuthorAddress {
28
29
  street?: string | null;
@@ -111,7 +111,11 @@ function TreeSelectFieldBase(props) {
111
111
  initialOptions.forEach(areAllChildrenSelected);
112
112
  return {
113
113
  selectedKeys,
114
- inputValue: selectedOptions.length === 1 ? getOptionLabel(selectedOptions[0]) : "",
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) => ({ ...prevState, inputValue: "", selectedKeys: [], selectedOptions: [] }));
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 !== "" || (selectedOptions.length === 1 && inputValue !== getOptionLabel(selectedOptions[0]))) {
315
+ if (inputValue !== nothingSelectedText ||
316
+ (selectedOptions.length === 1 && inputValue !== getOptionLabel(selectedOptions[0]))) {
302
317
  setFieldState((prevState) => ({
303
318
  ...prevState,
304
- inputValue: selectedOptions.length === 1 ? getOptionLabel(selectedOptions[0]) : "",
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
- setFieldState((prevState) => ({
137
- ...prevState,
138
- // When using the multiselect field, always empty the input upon open.
139
- inputValue: multiselect && isOpen ? "" : prevState.inputValue,
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
- // We don't really need to set this, but it's handy for tests, which would
29
- // otherwise render just 1 row. A better way to do this would be to jest.mock
30
- // out Virtuoso with an impl that just rendered everything, but doing this for now.
31
- initialItemCount: 10, itemContent: (idx) => {
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.266.0",
3
+ "version": "2.267.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",