@linzjs/step-ag-grid 6.1.0 → 7.0.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.
Files changed (44) hide show
  1. package/dist/index.css +12 -2
  2. package/dist/index.js +389 -213
  3. package/dist/index.js.map +1 -1
  4. package/dist/src/components/GridPopoverHook.d.ts +1 -1
  5. package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +17 -12
  6. package/dist/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.d.ts +1 -1
  7. package/dist/src/contexts/GridSubComponentContext.d.ts +1 -0
  8. package/dist/src/lui/ActionButton.d.ts +2 -1
  9. package/dist/src/lui/FormError.d.ts +2 -1
  10. package/dist/src/lui/TextAreaInput.d.ts +1 -1
  11. package/dist/src/lui/TextInputFormatted.d.ts +1 -1
  12. package/dist/src/react-menu3/utils/utils.d.ts +1 -0
  13. package/dist/src/utils/textMatcher.d.ts +13 -0
  14. package/dist/src/utils/textValidator.d.ts +3 -2
  15. package/dist/src/utils/util.d.ts +2 -1
  16. package/dist/step-ag-grid.esm.js +390 -215
  17. package/dist/step-ag-grid.esm.js.map +1 -1
  18. package/package.json +9 -8
  19. package/src/components/GridPopoverHook.tsx +7 -1
  20. package/src/components/gridForm/GridFormDropDown.tsx +15 -10
  21. package/src/components/gridForm/GridFormMultiSelect.tsx +290 -188
  22. package/src/components/gridForm/GridFormPopoverMenu.tsx +1 -0
  23. package/src/components/gridForm/GridFormSubComponentTextArea.tsx +2 -2
  24. package/src/components/gridForm/GridFormSubComponentTextInput.tsx +3 -5
  25. package/src/components/gridForm/GridFormTextArea.tsx +13 -13
  26. package/src/components/gridForm/GridFormTextInput.tsx +3 -8
  27. package/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.ts +3 -3
  28. package/src/contexts/GridSubComponentContext.ts +2 -0
  29. package/src/lui/ActionButton.tsx +26 -8
  30. package/src/lui/FormError.scss +7 -0
  31. package/src/lui/FormError.tsx +4 -13
  32. package/src/lui/TextAreaInput.tsx +1 -1
  33. package/src/lui/TextInputFormatted.tsx +1 -1
  34. package/src/react-menu3/components/ControlledMenu.tsx +3 -2
  35. package/src/react-menu3/components/MenuList.tsx +2 -12
  36. package/src/react-menu3/hooks/useItems.ts +5 -2
  37. package/src/react-menu3/utils/utils.ts +15 -0
  38. package/src/stories/components/ActionButton.stories.tsx +9 -0
  39. package/src/stories/grid/GridPopoutEditMultiSelect.stories.tsx +93 -17
  40. package/src/styles/Grid.scss +12 -0
  41. package/src/styles/lui-overrides.scss +0 -2
  42. package/src/utils/textMatcher.ts +31 -0
  43. package/src/utils/textValidator.ts +6 -3
  44. package/src/utils/util.ts +6 -7
@@ -19,7 +19,7 @@ export interface GridSubComponentTextAreaProps<RowType extends GridBaseRow>
19
19
  export const GridFormSubComponentTextArea = <RowType extends GridBaseRow>(
20
20
  props: GridSubComponentTextAreaProps<RowType>,
21
21
  ): JSX.Element => {
22
- const { value, data, setValue, setValid } = useContext(GridSubComponentContext);
22
+ const { value, data, setValue, setValid, context } = useContext(GridSubComponentContext);
23
23
 
24
24
  const helpText = props.helpText ?? "Press tab to save";
25
25
 
@@ -28,7 +28,7 @@ export const GridFormSubComponentTextArea = <RowType extends GridBaseRow>(
28
28
  if (value == null) setValue(props.defaultValue);
29
29
  }, [props.defaultValue, setValue, value]);
30
30
 
31
- const invalid = useCallback(() => TextInputValidator(props, value, data), [data, props, value]);
31
+ const invalid = useCallback(() => TextInputValidator(props, value, data, context), [data, props, value, context]);
32
32
 
33
33
  useEffect(() => {
34
34
  setValid(value != null && invalid() == null);
@@ -17,7 +17,7 @@ export interface GridFormSubComponentTextInputProps<RowType extends GridBaseRow>
17
17
  export const GridFormSubComponentTextInput = <RowType extends GridBaseRow>(
18
18
  props: GridFormSubComponentTextInputProps<RowType>,
19
19
  ): JSX.Element => {
20
- const { value, setValue, setValid, data } = useContext(GridSubComponentContext);
20
+ const { value, setValue, setValid, data, context } = useContext(GridSubComponentContext);
21
21
 
22
22
  const helpText = props.helpText ?? "Press enter or tab to save";
23
23
 
@@ -26,7 +26,7 @@ export const GridFormSubComponentTextInput = <RowType extends GridBaseRow>(
26
26
  if (value == null) setValue(props.defaultValue);
27
27
  }, [props.defaultValue, setValue, value]);
28
28
 
29
- const invalid = useCallback(() => TextInputValidator(props, value, data), [data, props, value]);
29
+ const invalid = useCallback(() => TextInputValidator(props, value, data, context), [context, data, props, value]);
30
30
 
31
31
  useEffect(() => {
32
32
  setValid(value != null && invalid() == null);
@@ -40,9 +40,7 @@ export const GridFormSubComponentTextInput = <RowType extends GridBaseRow>(
40
40
  helpText={helpText}
41
41
  autoFocus={true}
42
42
  placeholder={props.placeholder}
43
- style={{
44
- width: "100%",
45
- }}
43
+ style={{ width: "100%" }}
46
44
  />
47
45
  );
48
46
  };
@@ -1,4 +1,4 @@
1
- import { useCallback, useState } from "react";
1
+ import { useCallback, useMemo, useState } from "react";
2
2
  import { TextAreaInput } from "../../lui/TextAreaInput";
3
3
  import { useGridPopoverHook } from "../GridPopoverHook";
4
4
  import { GridBaseRow } from "../Grid";
@@ -17,30 +17,30 @@ export interface GridFormTextAreaProps<RowType extends GridBaseRow>
17
17
 
18
18
  export const GridFormTextArea = <RowType extends GridBaseRow>(props: GridFormTextAreaProps<RowType>) => {
19
19
  const { field, value: initialVale, data } = useGridPopoverContext<RowType>();
20
- const [value, setValue] = useState(initialVale != null ? `${initialVale}` : "");
20
+
21
+ const initValue = useMemo(() => (initialVale == null ? "" : `${initialVale}`), [initialVale]);
22
+ const [value, setValue] = useState(initValue);
21
23
 
22
24
  const helpText = props.helpText ?? "Press tab to save";
23
25
 
24
- const invalid = useCallback(() => TextInputValidator(props, value, data), [props, value, data]);
26
+ const invalid = useCallback(() => TextInputValidator(props, value, data, {}), [props, value, data]);
25
27
 
26
28
  const save = useCallback(
27
29
  async (selectedRows: RowType[]): Promise<boolean> => {
28
- if (initialVale === (value ?? "")) return true;
30
+ if (invalid()) return false;
31
+
32
+ const trimmedValue = value.trim();
33
+ // No change, so don't save
34
+ if (initValue === trimmedValue) return true;
29
35
 
30
36
  if (props.onSave) {
31
- return await props.onSave(selectedRows, value);
37
+ return await props.onSave(selectedRows, trimmedValue);
32
38
  }
33
39
 
34
- if (field == null) {
35
- console.error("ColDef has no field set");
36
- return false;
37
- }
38
- selectedRows.forEach((row) => {
39
- row[field] = value as any;
40
- });
40
+ selectedRows.forEach((row) => (row[field] = trimmedValue as any));
41
41
  return true;
42
42
  },
43
- [initialVale, value, props, field],
43
+ [invalid, value, initValue, props, field],
44
44
  );
45
45
 
46
46
  const { popoverWrapper } = useGridPopoverHook({
@@ -24,26 +24,21 @@ export const GridFormTextInput = <RowType extends GridBaseRow>(props: GridFormTe
24
24
  const initValue = useMemo(() => (initialVale == null ? "" : `${initialVale}`), [initialVale]);
25
25
  const [value, setValue] = useState(initValue);
26
26
 
27
- const invalid = useCallback(() => TextInputValidator<RowType>(props, value, data), [data, props, value]);
27
+ const invalid = useCallback(() => TextInputValidator<RowType>(props, value, data, {}), [data, props, value]);
28
28
 
29
29
  const save = useCallback(
30
30
  async (selectedRows: RowType[]): Promise<boolean> => {
31
31
  if (invalid()) return false;
32
32
 
33
33
  const trimmedValue = value.trim();
34
+ // No change, so don't save
34
35
  if (initValue === trimmedValue) return true;
35
36
 
36
37
  if (props.onSave) {
37
38
  return await props.onSave(selectedRows, trimmedValue);
38
39
  }
39
40
 
40
- if (field == null) {
41
- console.error("ColDef has no field set");
42
- return false;
43
- }
44
- selectedRows.forEach((row) => {
45
- row[field] = trimmedValue as any;
46
- });
41
+ selectedRows.forEach((row) => (row[field] = trimmedValue as any));
47
42
  return true;
48
43
  },
49
44
  [invalid, value, initValue, props, field],
@@ -3,15 +3,15 @@ import { GridBaseRow } from "../Grid";
3
3
  import { GridFormMultiSelect, GridFormMultiSelectProps } from "../gridForm/GridFormMultiSelect";
4
4
  import { GenericCellColDef } from "../gridRender/GridRenderGenericCell";
5
5
 
6
- export const GridPopoutEditMultiSelect = <RowType extends GridBaseRow, ValueType>(
6
+ export const GridPopoutEditMultiSelect = <RowType extends GridBaseRow>(
7
7
  colDef: GenericCellColDef<RowType>,
8
- props: GenericCellEditorProps<GridFormMultiSelectProps<RowType, ValueType>>,
8
+ props: GenericCellEditorProps<GridFormMultiSelectProps<RowType>>,
9
9
  ): ColDefT<RowType> =>
10
10
  GridCell(colDef, {
11
11
  editor: GridFormMultiSelect,
12
12
  ...props,
13
13
  editorParams: {
14
14
  className: "GridMultiSelect-containerMedium",
15
- ...(props.editorParams as GridFormMultiSelectProps<RowType, ValueType>),
15
+ ...(props.editorParams as GridFormMultiSelectProps<RowType>),
16
16
  },
17
17
  });
@@ -6,6 +6,7 @@ export interface GridSubComponentContextType {
6
6
  setValue: (value: string) => void;
7
7
  setValid: (valid: boolean) => void;
8
8
  triggerSave: () => Promise<void>;
9
+ context: any;
9
10
  }
10
11
 
11
12
  export const GridSubComponentContext = createContext<GridSubComponentContextType>({
@@ -20,4 +21,5 @@ export const GridSubComponentContext = createContext<GridSubComponentContextType
20
21
  triggerSave: async () => {
21
22
  console.error("GridSubComponentContext triggerSave no context");
22
23
  },
24
+ context: null,
23
25
  });
@@ -16,6 +16,7 @@ export interface ActionButtonProps {
16
16
  title?: string;
17
17
  dataTestId?: string;
18
18
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "ns";
19
+ iconPosition?: "left" | "right";
19
20
  className?: string;
20
21
  onAction: () => Promise<void> | void;
21
22
  // Used for external code to get access to whether action is in progress
@@ -36,6 +37,7 @@ export const ActionButton = ({
36
37
  onAction,
37
38
  externalSetInProgress,
38
39
  size = "sm",
40
+ iconPosition = "left",
39
41
  level = "tertiary",
40
42
  "aria-label": ariaLabel,
41
43
  }: ActionButtonProps): JSX.Element => {
@@ -48,6 +50,14 @@ export const ActionButton = ({
48
50
  inProgress ? setLocalInProgress(true) : setLocalInProgressDeferred(false, minimumInProgressTimeMs);
49
51
  }, [inProgress, lastInProgress, setLocalInProgress, setLocalInProgressDeferred]);
50
52
 
53
+ const buttonText = (
54
+ <span className={"ActionButton-minimalArea"}>
55
+ <span className={"ActionButton-minimalAreaDisplay"}>{(localInProgress ? inProgressName : name) ?? name}</span>
56
+ {/* This makes sure the button expands to fill maximum length text at all times */}
57
+ <span className={"ActionButton-minimalAreaExpand"}>{name}</span>
58
+ </span>
59
+ );
60
+
51
61
  return (
52
62
  <LuiButton
53
63
  data-testid={dataTestId}
@@ -55,7 +65,7 @@ export const ActionButton = ({
55
65
  level={level}
56
66
  title={title ?? ariaLabel ?? name}
57
67
  aria-label={ariaLabel ?? name}
58
- className={clsx("lui-button-icon", "ActionButton", className, localInProgress && "ActionButton-inProgress")}
68
+ className={clsx("lui-button-icon-right", "ActionButton", className, localInProgress && "ActionButton-inProgress")}
59
69
  size={"lg"}
60
70
  style={name == null ? { padding: "8px 5px" } : {}}
61
71
  onClick={async () => {
@@ -71,24 +81,32 @@ export const ActionButton = ({
71
81
  }}
72
82
  disabled={localInProgress}
73
83
  >
84
+ {iconPosition === "right" && buttonText}
74
85
  {localInProgress ? (
75
86
  <LuiMiniSpinner
76
87
  size={16}
77
88
  divProps={{
78
89
  "data-testid": "loading-spinner",
79
- style: { margin: 0, paddingRight: 5, paddingLeft: 3, paddingBottom: 0, paddingTop: 0 },
90
+ style: {
91
+ margin: 0,
92
+ paddingRight: 5,
93
+ paddingLeft: 3,
94
+ paddingBottom: 0,
95
+ paddingTop: 0,
96
+ },
80
97
  role: "status",
81
98
  "aria-label": "Loading",
82
99
  }}
83
100
  />
84
101
  ) : (
85
- <LuiIcon name={icon} alt={ariaLabel ?? name ?? ""} size={size} />
102
+ <LuiIcon
103
+ name={icon}
104
+ alt={ariaLabel ?? name ?? ""}
105
+ size={size}
106
+ spanProps={{ style: iconPosition === "right" ? { transform: "scaleX(-1)" } : {} }}
107
+ />
86
108
  )}
87
- <span className={"ActionButton-minimalArea"}>
88
- <span className={"ActionButton-minimalAreaDisplay"}>{(localInProgress ? inProgressName : name) ?? name}</span>
89
- {/* This makes sure the button expands to fill maximum length text at all times */}
90
- <span className={"ActionButton-minimalAreaExpand"}>{name}</span>
91
- </span>
109
+ {iconPosition === "left" && buttonText}
92
110
  </LuiButton>
93
111
  );
94
112
  };
@@ -0,0 +1,7 @@
1
+ @use "../../node_modules/@linzjs/lui/dist/scss/Foundation/Variables/ColorVars" as colors;
2
+
3
+ .helpText {
4
+ font-size: 0.75rem;
5
+ color: colors.$fuscous;
6
+ font-weight: 400;
7
+ }
@@ -1,29 +1,20 @@
1
- import { LuiIcon } from "@linzjs/lui";
1
+ import "./FormError.scss";
2
2
 
3
3
  export interface FormErrorProps {
4
4
  helpText?: string;
5
- error?: string | boolean | null;
5
+ error?: JSX.Element | string | boolean | null;
6
6
  }
7
7
 
8
8
  export const FormError = (props: FormErrorProps) => {
9
9
  return (
10
10
  <>
11
11
  {props.error && (
12
- <span className="LuiTextInput-error">
13
- <LuiIcon alt="error" name="ic_error" className="LuiTextInput-error-icon" size="sm" status="error" />
12
+ <span className="LuiTextInput-error" style={{ paddingLeft: 0 }}>
14
13
  {props.error}
15
14
  </span>
16
15
  )}
17
16
 
18
- {props.helpText && !props.error && (
19
- <span
20
- style={{
21
- fontSize: "0.7rem",
22
- }}
23
- >
24
- {props.helpText}
25
- </span>
26
- )}
17
+ {props.helpText && !props.error && <span className={"helpText"}>{props.helpText}</span>}
27
18
  </>
28
19
  );
29
20
  };
@@ -17,7 +17,7 @@ export interface LuiTextAreaInputProps extends InputHTMLAttributes<HTMLTextAreaE
17
17
  label?: JSX.Element | string;
18
18
  mandatory?: boolean;
19
19
  helpText?: string;
20
- error?: string | boolean | null;
20
+ error?: JSX.Element | string | boolean | null;
21
21
  }
22
22
 
23
23
  export const TextAreaInput = (props: LuiTextAreaInputProps) => {
@@ -12,7 +12,7 @@ export interface LuiTextInputProps extends DetailedHTMLProps<InputHTMLAttributes
12
12
 
13
13
  // Custom fields
14
14
  helpText?: string;
15
- error?: string | boolean | null;
15
+ error?: JSX.Element | string | boolean | null;
16
16
  formatted?: string;
17
17
  }
18
18
 
@@ -137,7 +137,8 @@ export const ControlledMenuFr = (
137
137
  if (activeElement !== firstInputEl && activeElement !== lastInputEl) return;
138
138
 
139
139
  const isTextArea = activeElement.nodeName === "TEXTAREA";
140
- const suppressEnterAutoSave = activeElement.getAttribute("data-disableEnterAutoSave") || isTextArea;
140
+ const suppressEnterAutoSave = activeElement.getAttribute("data-disableenterautoSave") || isTextArea;
141
+ const allowTabToSave = activeElement.getAttribute("data-allowtabtoSave");
141
142
  const invokeSave = (reason: string) => {
142
143
  if (!saveButtonRef?.current) return;
143
144
  saveButtonRef.current?.setAttribute("data-reason", reason);
@@ -147,7 +148,7 @@ export const ControlledMenuFr = (
147
148
  switch (activeElement.nodeName) {
148
149
  case "TEXTAREA":
149
150
  case "INPUT": {
150
- if (activeElement === lastInputEl && activeElement === firstInputEl) {
151
+ if ((activeElement === lastInputEl && activeElement === firstInputEl) || allowTabToSave) {
151
152
  if (ev.key === "Tab") {
152
153
  // Can't forward/backwards tab out of popup
153
154
  ev.preventDefault();
@@ -7,6 +7,7 @@ import {
7
7
  CloseReason,
8
8
  commonProps,
9
9
  floatEqual,
10
+ focusFirstInput,
10
11
  FocusPositions,
11
12
  getScrollAncestor,
12
13
  getTransition,
@@ -380,18 +381,7 @@ export const MenuList = ({
380
381
  if (!menuRef.current.contains(document.activeElement)) {
381
382
  // Handle popover portal focus
382
383
  const popupElement = focusRef.current?.nextSibling;
383
- if (popupElement instanceof Element) {
384
- const input = popupElement.querySelectorAll("input,textarea")[0] as HTMLElement;
385
- if (input) {
386
- input.focus();
387
- // Text areas should start at end
388
- if (input instanceof HTMLTextAreaElement) {
389
- input.selectionStart = input.value.length;
390
- }
391
- } else {
392
- focusRef.current?.focus();
393
- }
394
- } else {
384
+ if (!focusFirstInput(popupElement)) {
395
385
  focusRef.current?.focus();
396
386
  }
397
387
  setItemFocus();
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useRef, MutableRefObject } from "react";
2
- import { HoverActionTypes, indexOfNode } from "../utils";
2
+ import { focusFirstInput, HoverActionTypes, indexOfNode } from "../utils";
3
3
  import { FocusPosition } from "../types";
4
4
 
5
5
  export const useItems = (menuRef: MutableRefObject<any>, focusRef: MutableRefObject<any> | undefined) => {
@@ -84,16 +84,19 @@ export const useItems = (menuRef: MutableRefObject<any>, focusRef: MutableRefObj
84
84
  index++;
85
85
  if (index >= items.length) index = 0;
86
86
  newItem = items[index];
87
+ focusFirstInput(newItem);
87
88
  break;
88
89
 
89
- case HoverActionTypes.DECREASE:
90
+ case HoverActionTypes.DECREASE: {
90
91
  sortItems();
91
92
  index = hoverIndex;
92
93
  if (index < 0) index = items.indexOf(item);
93
94
  index--;
94
95
  if (index < 0) index = items.length - 1;
95
96
  newItem = items[index];
97
+ focusFirstInput(newItem);
96
98
  break;
99
+ }
97
100
 
98
101
  default:
99
102
  if (process.env.NODE_ENV !== "production")
@@ -86,3 +86,18 @@ export function commonProps(isDisabled?: boolean, isHovering?: boolean) {
86
86
  }
87
87
 
88
88
  export const indexOfNode = (nodeList: NodeListOf<Node>, node: Node) => findIndex(nodeList, node);
89
+
90
+ export const focusFirstInput = (container: any) => {
91
+ if (!(container instanceof Element)) return false;
92
+ const inputs = container.querySelectorAll("input[type='text'],textarea");
93
+ const input = inputs[0];
94
+ if (input instanceof HTMLElement) {
95
+ input.focus();
96
+ // Text areas should start at end
97
+ if (input instanceof HTMLTextAreaElement) {
98
+ input.selectionStart = input.value.length;
99
+ }
100
+ return true;
101
+ }
102
+ return false;
103
+ };
@@ -19,7 +19,16 @@ const ActionButtonTemplate: ComponentStory<typeof ActionButton> = () => {
19
19
  return (
20
20
  <>
21
21
  <ActionButton icon={"ic_add"} name={"Add new row"} inProgressName={"Adding..."} onAction={performAction} />
22
+ <br />
22
23
  <ActionButton icon={"ic_add"} aria-label={"Add new row"} onAction={performAction} level={"primary"} />
24
+ <br />
25
+ <ActionButton
26
+ icon={"ic_arrow_back"}
27
+ name={"Continue"}
28
+ onAction={performAction}
29
+ iconPosition={"right"}
30
+ level={"secondary"}
31
+ />
23
32
  </>
24
33
  );
25
34
  };
@@ -8,12 +8,13 @@ import { GridUpdatingContextProvider } from "../../contexts/GridUpdatingContextP
8
8
  import { GridContextProvider } from "../../contexts/GridContextProvider";
9
9
  import { Grid, GridProps } from "../../components/Grid";
10
10
  import { useMemo, useState } from "react";
11
- import { MenuSeparator } from "../../components/gridForm/GridFormDropDown";
12
- import { wait } from "../../utils/util";
11
+ import { MenuHeaderItem, MenuSeparator } from "../../components/gridForm/GridFormDropDown";
13
12
  import { GridFormSubComponentTextArea } from "../../components/gridForm/GridFormSubComponentTextArea";
14
13
  import { ColDefT, GridCell } from "../../components/GridCell";
15
14
  import { GridPopoutEditMultiSelect } from "../../components/gridPopoverEdit/GridPopoutEditMultiSelect";
16
- import { partition } from "lodash-es";
15
+ import { isEmpty, partition } from "lodash-es";
16
+ import { wait } from "../../utils/util";
17
+ import { MultiSelectOption } from "../../components/gridForm/GridFormMultiSelect";
17
18
 
18
19
  export default {
19
20
  title: "Components / Grids",
@@ -37,7 +38,7 @@ export default {
37
38
 
38
39
  interface ITestRow {
39
40
  id: number;
40
- position: string | null;
41
+ position: string[] | null;
41
42
  position2: string | null;
42
43
  position3: string | null;
43
44
  }
@@ -46,6 +47,15 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
46
47
  const [externalSelectedItems, setExternalSelectedItems] = useState<any[]>([]);
47
48
 
48
49
  const columnDefs: ColDefT<ITestRow>[] = useMemo(() => {
50
+ const positionMap: Record<string, string> = {
51
+ lot1: "Lot 1",
52
+ lot2: "Lot 2",
53
+ lot3: "Lot 3",
54
+ lot4: "Lot A 482392",
55
+ appA: "A",
56
+ appB: "B",
57
+ other: "Other",
58
+ };
49
59
  const positionTwoMap: Record<string, string> = {
50
60
  "1": "One",
51
61
  "2": "Two",
@@ -62,8 +72,12 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
62
72
  {
63
73
  field: "position",
64
74
  initialWidth: 65,
65
- maxWidth: 300,
75
+ maxWidth: 250,
66
76
  headerName: "Position",
77
+ valueFormatter: ({ value }) => {
78
+ if (value == null) return "";
79
+ return value.map((v: string) => positionMap[v] ?? v).join(", ");
80
+ },
67
81
  },
68
82
  {
69
83
  multiEdit: true,
@@ -72,11 +86,14 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
72
86
  filterPlaceholder: "Filter position",
73
87
  className: "GridMultiSelect-containerUnlimited",
74
88
  options: [
75
- { value: "a", label: "Architect" },
76
- { value: "b", label: "Developer" },
77
- { value: "c", label: "Product Owner" },
78
- { value: "d", label: "Scrum Master" },
79
- { value: "e", label: "Tester" },
89
+ MenuHeaderItem("Header item"),
90
+ { value: "lot1", label: "Lot 1" },
91
+ { value: "lot2", label: "Lot 2" },
92
+ { value: "lot3", label: "Lot 3" },
93
+ { value: "lot11", label: "Lot 11" },
94
+ { value: "lot4", label: "Lot A 482392" },
95
+ { value: "appA", label: "A" },
96
+ { value: "appB", label: "B" },
80
97
  MenuSeparator,
81
98
  {
82
99
  value: "other",
@@ -84,16 +101,76 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
84
101
  subComponent: () => <GridFormSubComponentTextArea required={true} maxLength={5} defaultValue={""} />,
85
102
  },
86
103
  ],
87
- initialSelectedValues: () => ({
88
- other: "Hello",
89
- }),
90
104
  onSave: async (selectedRows, selectedOptions) => {
91
105
  // eslint-disable-next-line no-console
92
106
  console.log("multiSelect result", { selectedRows, selectedOptions });
93
107
 
94
108
  await wait(1000);
95
109
  const [subValues, normalValues] = partition(selectedOptions, (o) => o.subComponent);
96
- const newValue = [...normalValues.map((o) => o.label), ...subValues.map((o) => o.subValue)].join(", ");
110
+ const newValue = [...normalValues.map((o) => o.value), ...subValues.map((o) => o.subValue)];
111
+ selectedRows.forEach((row) => (row.position = newValue));
112
+ return true;
113
+ },
114
+ },
115
+ },
116
+ ),
117
+ GridPopoutEditMultiSelect(
118
+ {
119
+ field: "position",
120
+ initialWidth: 65,
121
+ maxWidth: 250,
122
+ headerName: "Parcel picker",
123
+ valueFormatter: ({ value }) => {
124
+ if (value == null) return "";
125
+ return value.map((v: string) => positionMap[v] ?? v).join(", ");
126
+ },
127
+ },
128
+ {
129
+ multiEdit: true,
130
+ editorParams: {
131
+ filtered: true,
132
+ filterPlaceholder: "Filter/add custom parcel...",
133
+ filterHelpText: (filter, options) =>
134
+ isEmpty(filter) || options.find((o) => o.label && o.label.toLowerCase() === filter.toLowerCase())
135
+ ? undefined
136
+ : "Press enter to add free text",
137
+ onSelectFilter: async (filter, options) => {
138
+ if (isEmpty(filter) || options.find((o) => o.label && o.label.toLowerCase() === filter.toLowerCase()))
139
+ return;
140
+ options.push({ value: filter, label: filter, filter: "freeText", checked: true });
141
+ },
142
+ className: "GridMultiSelect-containerLarge",
143
+ headers: [{ header: "Free text", filter: "freeText" }, { header: "Parcels" }],
144
+ options: (selectedRows) => {
145
+ const firstRow = selectedRows[0];
146
+ const r: MultiSelectOption[] = [
147
+ { value: "lot1", label: "Lot 1" },
148
+ { value: "lot2", label: "Lot 2" },
149
+ { value: "lot3", label: "Lot 3" },
150
+ { value: "lot11", label: "Lot 11" },
151
+ { value: "lot4", label: "Lot A 482392" },
152
+ { value: "appA", label: "A" },
153
+ { value: "appB", label: "B" },
154
+ ].map((r) => ({ ...r, checked: firstRow.position?.includes(r.value) }));
155
+ firstRow.position?.forEach(
156
+ (p) =>
157
+ !(p in positionMap) &&
158
+ r.push({
159
+ value: p,
160
+ label: p,
161
+ checked: true,
162
+ filter: "freeText",
163
+ }),
164
+ );
165
+ return r;
166
+ },
167
+ onSave: async (selectedRows, selectedOptions) => {
168
+ // eslint-disable-next-line no-console
169
+ console.log("multiSelect result", { selectedRows, selectedOptions });
170
+
171
+ await wait(1000);
172
+ const [subValues, normalValues] = partition(selectedOptions, (o) => o.subComponent);
173
+ const newValue = [...normalValues.map((o) => o.value), ...subValues.map((o) => o.subValue)];
97
174
  selectedRows.forEach((row) => (row.position = newValue));
98
175
  return true;
99
176
  },
@@ -113,7 +190,6 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
113
190
  editorParams: {
114
191
  filtered: true,
115
192
  filterPlaceholder: "Filter position",
116
- initialSelectedValues: (selectedRows) => [selectedRows[0].position2],
117
193
  options: Object.entries(positionTwoMap).map(([k, v]) => ({ value: k, label: v })),
118
194
  onSave: async (selectedRows, selectedOptions) => {
119
195
  // eslint-disable-next-line no-console
@@ -128,8 +204,8 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
128
204
  }, []);
129
205
 
130
206
  const [rowData] = useState([
131
- { id: 1000, position: "Tester", position2: "1", position3: "Tester" },
132
- { id: 1001, position: "Developer", position2: "2", position3: "Developer" },
207
+ { id: 1000, position: ["lot1", "lot2"], position2: "lot1", position3: "Tester" },
208
+ { id: 1001, position: ["appA"], position2: "lot2", position3: "Developer" },
133
209
  ] as ITestRow[]);
134
210
 
135
211
  return (
@@ -39,3 +39,15 @@
39
39
  padding: 4px 8px;
40
40
  max-width: 400px;
41
41
  }
42
+
43
+ .ag-cell:not(.ag-cell-focus) .Grid-displayWhenCellFocused {
44
+ visibility: hidden;
45
+ }
46
+
47
+ .ag-cell:hover .Grid-displayWhenCellFocused {
48
+ visibility: inherit;
49
+ }
50
+
51
+ .ag-cell-wrapper {
52
+ width: 100%;
53
+ }
@@ -32,8 +32,6 @@
32
32
 
33
33
  /** Free text input */
34
34
  .free-text-input {
35
- border: transparent;
36
- outline: transparent;
37
35
  line-height: 33px;
38
36
  padding-left: 4px;
39
37
  }
@@ -0,0 +1,31 @@
1
+ import { isEmpty } from "lodash-es";
2
+ import { isMatch } from "matcher";
3
+
4
+ /**
5
+ * Text matching with wildcards and multiple matchers.
6
+ *
7
+ * "L" => L*
8
+ * "L*" => L*
9
+ * "*L*" => *L*
10
+ * "*L" => *L
11
+ * "A B" => A* and B*
12
+ * "A B, C" => (A* and B*) or C*
13
+ *
14
+ * Returns ture if there's a text match.
15
+ */
16
+ export const textMatch = (text: string | undefined | null, filter: string): boolean => {
17
+ if (text == null) return true;
18
+
19
+ const superFilters = filter
20
+ .split(",")
21
+ .map((sf) => sf.trim())
22
+ .filter((sf) => sf);
23
+ const values = text.replaceAll(",", " ").trim().split(/\s+/);
24
+ return (
25
+ isEmpty(superFilters) || // Not filtered
26
+ superFilters.some((superFilter) => {
27
+ const subFilters = superFilter.split(/\s+/).map((s) => s.replaceAll(/([!?])/g, "\\$1"));
28
+ return subFilters.every((subFilter) => values.some((value) => isMatch(value, subFilter)));
29
+ })
30
+ );
31
+ };