@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.
- package/dist/index.css +12 -2
- package/dist/index.js +389 -213
- package/dist/index.js.map +1 -1
- package/dist/src/components/GridPopoverHook.d.ts +1 -1
- package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +17 -12
- package/dist/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.d.ts +1 -1
- package/dist/src/contexts/GridSubComponentContext.d.ts +1 -0
- package/dist/src/lui/ActionButton.d.ts +2 -1
- package/dist/src/lui/FormError.d.ts +2 -1
- package/dist/src/lui/TextAreaInput.d.ts +1 -1
- package/dist/src/lui/TextInputFormatted.d.ts +1 -1
- package/dist/src/react-menu3/utils/utils.d.ts +1 -0
- package/dist/src/utils/textMatcher.d.ts +13 -0
- package/dist/src/utils/textValidator.d.ts +3 -2
- package/dist/src/utils/util.d.ts +2 -1
- package/dist/step-ag-grid.esm.js +390 -215
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +9 -8
- package/src/components/GridPopoverHook.tsx +7 -1
- package/src/components/gridForm/GridFormDropDown.tsx +15 -10
- package/src/components/gridForm/GridFormMultiSelect.tsx +290 -188
- package/src/components/gridForm/GridFormPopoverMenu.tsx +1 -0
- package/src/components/gridForm/GridFormSubComponentTextArea.tsx +2 -2
- package/src/components/gridForm/GridFormSubComponentTextInput.tsx +3 -5
- package/src/components/gridForm/GridFormTextArea.tsx +13 -13
- package/src/components/gridForm/GridFormTextInput.tsx +3 -8
- package/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.ts +3 -3
- package/src/contexts/GridSubComponentContext.ts +2 -0
- package/src/lui/ActionButton.tsx +26 -8
- package/src/lui/FormError.scss +7 -0
- package/src/lui/FormError.tsx +4 -13
- package/src/lui/TextAreaInput.tsx +1 -1
- package/src/lui/TextInputFormatted.tsx +1 -1
- package/src/react-menu3/components/ControlledMenu.tsx +3 -2
- package/src/react-menu3/components/MenuList.tsx +2 -12
- package/src/react-menu3/hooks/useItems.ts +5 -2
- package/src/react-menu3/utils/utils.ts +15 -0
- package/src/stories/components/ActionButton.stories.tsx +9 -0
- package/src/stories/grid/GridPopoutEditMultiSelect.stories.tsx +93 -17
- package/src/styles/Grid.scss +12 -0
- package/src/styles/lui-overrides.scss +0 -2
- package/src/utils/textMatcher.ts +31 -0
- package/src/utils/textValidator.ts +6 -3
- 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
|
-
|
|
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 (
|
|
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,
|
|
37
|
+
return await props.onSave(selectedRows, trimmedValue);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
|
6
|
+
export const GridPopoutEditMultiSelect = <RowType extends GridBaseRow>(
|
|
7
7
|
colDef: GenericCellColDef<RowType>,
|
|
8
|
-
props: GenericCellEditorProps<GridFormMultiSelectProps<RowType
|
|
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
|
|
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
|
});
|
package/src/lui/ActionButton.tsx
CHANGED
|
@@ -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: {
|
|
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
|
|
102
|
+
<LuiIcon
|
|
103
|
+
name={icon}
|
|
104
|
+
alt={ariaLabel ?? name ?? ""}
|
|
105
|
+
size={size}
|
|
106
|
+
spanProps={{ style: iconPosition === "right" ? { transform: "scaleX(-1)" } : {} }}
|
|
107
|
+
/>
|
|
86
108
|
)}
|
|
87
|
-
|
|
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
|
};
|
package/src/lui/FormError.tsx
CHANGED
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import
|
|
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) => {
|
|
@@ -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-
|
|
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
|
|
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:
|
|
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
|
-
|
|
76
|
-
{ value: "
|
|
77
|
-
{ value: "
|
|
78
|
-
{ value: "
|
|
79
|
-
{ value: "
|
|
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.
|
|
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: "
|
|
132
|
-
{ id: 1001, position: "
|
|
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 (
|
package/src/styles/Grid.scss
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
+
};
|