@linzjs/step-ag-grid 7.0.4 → 7.1.1
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.js +20597 -286
- package/dist/index.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/utils/testQuick.d.ts +62 -0
- package/dist/src/utils/testUtil.d.ts +22 -0
- package/dist/src/utils/textMatcher.d.ts +2 -2
- package/dist/step-ag-grid.esm.js +20301 -28
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/gridForm/GridFormDropDown.tsx +1 -1
- package/src/components/gridForm/GridFormMultiSelect.tsx +84 -22
- package/src/components/gridPopoverEdit/GridPopoverEditDropDown.ts +2 -2
- package/src/index.ts +1 -0
- package/src/react-menu3/components/ControlledMenu.tsx +1 -1
- package/src/stories/grid/GridPopoutEditMultiSelect.stories.tsx +2 -2
- package/src/utils/testQuick.ts +146 -0
- package/src/utils/testUtil.ts +135 -0
- package/src/utils/textMatcher.ts +11 -8
package/package.json
CHANGED
|
@@ -247,7 +247,7 @@ export const GridFormDropDown = <RowType extends GridBaseRow>(props: GridFormPop
|
|
|
247
247
|
data-testid={"filteredMenu-free-text-input"}
|
|
248
248
|
defaultValue={filter}
|
|
249
249
|
data-disableenterautosave={true}
|
|
250
|
-
data-
|
|
250
|
+
data-allowtabtosave={true}
|
|
251
251
|
onChange={(e) => setFilter(e.target.value)}
|
|
252
252
|
onKeyDown={handleKeyDown}
|
|
253
253
|
onKeyUp={handleKeyUp}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "react";
|
|
15
15
|
import { GridBaseRow } from "../Grid";
|
|
16
16
|
import { ComponentLoadingWrapper } from "../ComponentLoadingWrapper";
|
|
17
|
-
import { groupBy, isEmpty, pick, toPairs } from "lodash-es";
|
|
17
|
+
import { fromPairs, groupBy, isEmpty, pick, toPairs } from "lodash-es";
|
|
18
18
|
import { LuiCheckboxInput } from "@linzjs/lui";
|
|
19
19
|
import { useGridPopoverHook } from "../GridPopoverHook";
|
|
20
20
|
import { MenuSeparatorString } from "./GridFormDropDown";
|
|
@@ -25,6 +25,8 @@ import { useGridPopoverContext } from "../../contexts/GridPopoverContext";
|
|
|
25
25
|
import { FormError } from "../../lui/FormError";
|
|
26
26
|
import { textMatch } from "../../utils/textMatcher";
|
|
27
27
|
|
|
28
|
+
type HeaderGroupType = Record<string, MultiSelectOption[]> | undefined;
|
|
29
|
+
|
|
28
30
|
export interface MultiSelectOption {
|
|
29
31
|
value: any;
|
|
30
32
|
label?: string;
|
|
@@ -112,15 +114,34 @@ export const GridFormMultiSelect = <RowType extends GridBaseRow>(props: GridForm
|
|
|
112
114
|
/**
|
|
113
115
|
* Groups options into their header groups
|
|
114
116
|
*/
|
|
115
|
-
const headerGroups = useMemo(
|
|
116
|
-
()
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
const headerGroups = useMemo(() => {
|
|
118
|
+
if (!options) return undefined;
|
|
119
|
+
const result = groupBy(
|
|
120
|
+
options.filter((o) => textMatch(o.label, filter) && o.value),
|
|
121
|
+
"filter",
|
|
122
|
+
);
|
|
123
|
+
// remove leading/trailing/duplicate dividers
|
|
124
|
+
return fromPairs(
|
|
125
|
+
toPairs(result).map(([key, arr]) => {
|
|
126
|
+
let lastWasDivider = true;
|
|
127
|
+
return [
|
|
128
|
+
key,
|
|
129
|
+
arr
|
|
130
|
+
.map((row, index) => {
|
|
131
|
+
if (row.value === MenuSeparatorString) {
|
|
132
|
+
if (lastWasDivider) return null;
|
|
133
|
+
if (index === arr.length - 1) return null;
|
|
134
|
+
lastWasDivider = true;
|
|
135
|
+
} else {
|
|
136
|
+
lastWasDivider = false;
|
|
137
|
+
}
|
|
138
|
+
return row;
|
|
139
|
+
})
|
|
140
|
+
.filter((r) => r),
|
|
141
|
+
];
|
|
142
|
+
}),
|
|
143
|
+
) as HeaderGroupType;
|
|
144
|
+
}, [filter, options]);
|
|
124
145
|
|
|
125
146
|
const headers: GridFormMultiSelectGroup[] = useMemo(() => props.headers ?? [{ header: "" }], [props.headers]);
|
|
126
147
|
|
|
@@ -136,7 +157,7 @@ export const GridFormMultiSelect = <RowType extends GridBaseRow>(props: GridForm
|
|
|
136
157
|
<>
|
|
137
158
|
{props.filtered && (
|
|
138
159
|
<FilterInput
|
|
139
|
-
{...{ headerGroups, options, setOptions, filter, setFilter }}
|
|
160
|
+
{...{ headerGroups, options, setOptions, filter, setFilter, triggerSave }}
|
|
140
161
|
filterHelpText={props.filterHelpText}
|
|
141
162
|
onSelectFilter={props.onSelectFilter}
|
|
142
163
|
filterPlaceholder={props.filterPlaceholder}
|
|
@@ -185,12 +206,25 @@ const FilterInput = (props: {
|
|
|
185
206
|
onSelectFilter?: (filter: string, options: MultiSelectOption[]) => void;
|
|
186
207
|
filter: string;
|
|
187
208
|
setFilter: Dispatch<SetStateAction<string>>;
|
|
188
|
-
headerGroups:
|
|
209
|
+
headerGroups: HeaderGroupType;
|
|
189
210
|
filterPlaceholder?: string;
|
|
190
211
|
filterHelpText?: string | ((filter: string, options: MultiSelectOption[]) => string | undefined);
|
|
212
|
+
triggerSave: () => Promise<void>;
|
|
191
213
|
}) => {
|
|
192
|
-
const {
|
|
193
|
-
|
|
214
|
+
const {
|
|
215
|
+
options,
|
|
216
|
+
setOptions,
|
|
217
|
+
onSelectFilter,
|
|
218
|
+
filter,
|
|
219
|
+
setFilter,
|
|
220
|
+
headerGroups,
|
|
221
|
+
filterPlaceholder,
|
|
222
|
+
filterHelpText,
|
|
223
|
+
triggerSave,
|
|
224
|
+
} = props;
|
|
225
|
+
|
|
226
|
+
const enterHasBeenPressed = useRef(false);
|
|
227
|
+
const lastKeyWasEnter = useRef(false);
|
|
194
228
|
|
|
195
229
|
const toggleSelectAllVisible = useCallback(() => {
|
|
196
230
|
if (!options || !headerGroups) return;
|
|
@@ -198,7 +232,9 @@ const FilterInput = (props: {
|
|
|
198
232
|
if (isEmpty(filter.trim())) {
|
|
199
233
|
// Toggle off if any items are checked otherwise on
|
|
200
234
|
const anyChecked = options.some((o) => o.checked);
|
|
201
|
-
options.forEach((o) =>
|
|
235
|
+
options.forEach((o) => {
|
|
236
|
+
if (o.label !== undefined) o.checked = !anyChecked;
|
|
237
|
+
});
|
|
202
238
|
} else {
|
|
203
239
|
// Toggle on if any filtered items are checked otherwise off
|
|
204
240
|
const anyChecked = Object.values(headerGroups).some((headerOptions) =>
|
|
@@ -206,7 +242,7 @@ const FilterInput = (props: {
|
|
|
206
242
|
);
|
|
207
243
|
Object.values(headerGroups).forEach((headerOptions) => {
|
|
208
244
|
headerOptions.forEach((o) => {
|
|
209
|
-
if (o.
|
|
245
|
+
if (o.label !== undefined) o.checked = anyChecked;
|
|
210
246
|
});
|
|
211
247
|
});
|
|
212
248
|
}
|
|
@@ -216,14 +252,26 @@ const FilterInput = (props: {
|
|
|
216
252
|
const addCustomFilterValue = useCallback(() => {
|
|
217
253
|
if (!options || !onSelectFilter) return;
|
|
218
254
|
|
|
255
|
+
const filterTrimmed = filter.trim();
|
|
256
|
+
if (isEmpty(filterTrimmed)) {
|
|
257
|
+
triggerSave().then();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
const preFilterOptions = JSON.stringify(options);
|
|
220
|
-
onSelectFilter(
|
|
262
|
+
onSelectFilter(filterTrimmed, options);
|
|
221
263
|
// Detect if options list changed and update
|
|
222
264
|
if (preFilterOptions === JSON.stringify(options)) return;
|
|
223
265
|
|
|
224
266
|
setOptions([...options]);
|
|
225
267
|
setFilter("");
|
|
226
|
-
}, [filter, onSelectFilter, options, setFilter, setOptions]);
|
|
268
|
+
}, [filter, onSelectFilter, options, setFilter, setOptions, triggerSave]);
|
|
269
|
+
|
|
270
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
271
|
+
if (e.key === "Enter") {
|
|
272
|
+
enterHasBeenPressed.current = true;
|
|
273
|
+
}
|
|
274
|
+
}, []);
|
|
227
275
|
|
|
228
276
|
const handleKeyUp = useCallback(
|
|
229
277
|
(e: KeyboardEvent) => {
|
|
@@ -232,10 +280,23 @@ const FilterInput = (props: {
|
|
|
232
280
|
e.preventDefault();
|
|
233
281
|
|
|
234
282
|
if (e.ctrlKey) toggleSelectAllVisible();
|
|
235
|
-
else if (
|
|
283
|
+
else if (enterHasBeenPressed.current) {
|
|
284
|
+
const filterTrimmed = filter.trim();
|
|
285
|
+
if (isEmpty(filterTrimmed)) {
|
|
286
|
+
triggerSave().then();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
onSelectFilter && addCustomFilterValue();
|
|
290
|
+
}
|
|
291
|
+
lastKeyWasEnter.current = true;
|
|
292
|
+
} else if (e.key === "Control") {
|
|
293
|
+
lastKeyWasEnter.current && setFilter("");
|
|
294
|
+
lastKeyWasEnter.current = false;
|
|
295
|
+
} else {
|
|
296
|
+
lastKeyWasEnter.current = false;
|
|
236
297
|
}
|
|
237
298
|
},
|
|
238
|
-
[addCustomFilterValue, onSelectFilter, toggleSelectAllVisible],
|
|
299
|
+
[addCustomFilterValue, filter, onSelectFilter, setFilter, toggleSelectAllVisible, triggerSave],
|
|
239
300
|
);
|
|
240
301
|
|
|
241
302
|
return (
|
|
@@ -250,8 +311,9 @@ const FilterInput = (props: {
|
|
|
250
311
|
data-testid={"filteredMenu-free-text-input"}
|
|
251
312
|
value={filter}
|
|
252
313
|
data-disableenterautosave={true}
|
|
253
|
-
data-
|
|
314
|
+
data-allowtabtosave={true}
|
|
254
315
|
onChange={(e) => setFilter(e.target.value)}
|
|
316
|
+
onKeyDown={handleKeyDown}
|
|
255
317
|
onKeyUp={handleKeyUp}
|
|
256
318
|
/>
|
|
257
319
|
{filterHelpText && (
|
|
@@ -267,7 +329,7 @@ const FilterInput = (props: {
|
|
|
267
329
|
</FocusableItem>
|
|
268
330
|
<MenuDivider key={`$$divider_filter`} />
|
|
269
331
|
{headerGroups && !toPairs(headerGroups).some(([_, options]) => !isEmpty(options)) && (
|
|
270
|
-
<div className={"szh-menu__item"}>
|
|
332
|
+
<div className={"szh-menu__item GridPopoverEditDropDown-noOptions"}>No Options</div>
|
|
271
333
|
)}
|
|
272
334
|
</>
|
|
273
335
|
);
|
|
@@ -11,8 +11,8 @@ export const GridPopoverEditDropDown = <RowType extends GridBaseRow>(
|
|
|
11
11
|
editor: GridFormDropDown,
|
|
12
12
|
...props,
|
|
13
13
|
editorParams: {
|
|
14
|
-
// Defaults to
|
|
15
|
-
className: "GridPopoverEditDropDown-
|
|
14
|
+
// Defaults to large size container
|
|
15
|
+
className: "GridPopoverEditDropDown-containerLarge",
|
|
16
16
|
...(props.editorParams as GridFormPopoutDropDownProps<RowType>),
|
|
17
17
|
},
|
|
18
18
|
});
|
package/src/index.ts
CHANGED
|
@@ -138,7 +138,7 @@ export const ControlledMenuFr = (
|
|
|
138
138
|
|
|
139
139
|
const isTextArea = activeElement.nodeName === "TEXTAREA";
|
|
140
140
|
const suppressEnterAutoSave = activeElement.getAttribute("data-disableenterautosave") || isTextArea;
|
|
141
|
-
const allowTabToSave = activeElement.getAttribute("data-
|
|
141
|
+
const allowTabToSave = activeElement.getAttribute("data-allowtabtosave");
|
|
142
142
|
const invokeSave = (reason: string) => {
|
|
143
143
|
if (!saveButtonRef?.current) return;
|
|
144
144
|
saveButtonRef.current?.setAttribute("data-reason", reason);
|
|
@@ -8,7 +8,7 @@ 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 {
|
|
11
|
+
import { MenuSeparator } from "../../components/gridForm/GridFormDropDown";
|
|
12
12
|
import { GridFormSubComponentTextArea } from "../../components/gridForm/GridFormSubComponentTextArea";
|
|
13
13
|
import { ColDefT, GridCell } from "../../components/GridCell";
|
|
14
14
|
import { GridPopoutEditMultiSelect } from "../../components/gridPopoverEdit/GridPopoutEditMultiSelect";
|
|
@@ -85,8 +85,8 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
|
|
|
85
85
|
filtered: true,
|
|
86
86
|
filterPlaceholder: "Filter position",
|
|
87
87
|
className: "GridMultiSelect-containerUnlimited",
|
|
88
|
+
headers: [{ header: "Header item" }],
|
|
88
89
|
options: [
|
|
89
|
-
MenuHeaderItem("Header item"),
|
|
90
90
|
{ value: "lot1", label: "Lot 1" },
|
|
91
91
|
{ value: "lot2", label: "Lot 2" },
|
|
92
92
|
{ value: "lot3", label: "Lot 3" },
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General query all by selected. Internal use, use quick operations instead.
|
|
3
|
+
*
|
|
4
|
+
* @param selector Selector to use
|
|
5
|
+
* @param container Optional container
|
|
6
|
+
* @return HTMLElement array
|
|
7
|
+
*/
|
|
8
|
+
import { IconName } from "@linzjs/lui/dist/components/LuiIcon/LuiIcon";
|
|
9
|
+
import { wait } from "./util";
|
|
10
|
+
|
|
11
|
+
const queryAllBySelector = <T extends HTMLElement>(selector: string, container: HTMLElement = document.body): T[] =>
|
|
12
|
+
Array.from(container.querySelectorAll<T>(selector));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Filters for query quick operations.
|
|
16
|
+
*/
|
|
17
|
+
export interface IQueryQuick {
|
|
18
|
+
testId?: string;
|
|
19
|
+
tagName?: "button" | "span" | "div" | "input" | "textarea" | any;
|
|
20
|
+
textEquals?: string;
|
|
21
|
+
textContains?: string;
|
|
22
|
+
icon?: IconName;
|
|
23
|
+
ariaLabel?: string;
|
|
24
|
+
role?: string;
|
|
25
|
+
classes?: string;
|
|
26
|
+
child?: IQueryQuick;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const escapeSelectorParam = (param: string): string => param.replace(/["\\]/g, "\\$&");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build selector for quick operations.
|
|
33
|
+
*
|
|
34
|
+
* @param props Filters to convert to selector.
|
|
35
|
+
* @param container Optional container to search in.
|
|
36
|
+
*/
|
|
37
|
+
const quickSelector = <T extends HTMLElement>(
|
|
38
|
+
props: IQueryQuick,
|
|
39
|
+
container?: HTMLElement,
|
|
40
|
+
): { selector: string; els: T[] } => {
|
|
41
|
+
let selector = "";
|
|
42
|
+
let lastIQueryQuick = props;
|
|
43
|
+
for (let loop: IQueryQuick | undefined = props; loop; loop = loop.child) {
|
|
44
|
+
lastIQueryQuick = loop;
|
|
45
|
+
loop.tagName && (selector += loop.tagName);
|
|
46
|
+
loop.ariaLabel && (selector += `[aria-label='${escapeSelectorParam(loop.ariaLabel)}']`);
|
|
47
|
+
loop.role && (selector += `[role="${escapeSelectorParam(loop.role)}"]`);
|
|
48
|
+
loop.testId && (selector += `[data-testid="${escapeSelectorParam(loop.testId)}"]`);
|
|
49
|
+
loop.icon && (selector += `[data-icon='${loop.icon}']`);
|
|
50
|
+
loop.classes && (selector += loop.classes);
|
|
51
|
+
selector += " ";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (selector.trim() == "") {
|
|
55
|
+
throw "get/query/findQuick needs at least one defined parameter";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let els = queryAllBySelector<T>(selector, container);
|
|
59
|
+
|
|
60
|
+
const textMatch = lastIQueryQuick.textEquals?.toLowerCase();
|
|
61
|
+
if (textMatch != null) {
|
|
62
|
+
els = els.filter((el) => el.innerHTML.toLowerCase() === textMatch || el.innerText.toLowerCase() === textMatch);
|
|
63
|
+
}
|
|
64
|
+
const textContains = lastIQueryQuick.textContains?.toLowerCase();
|
|
65
|
+
if (textContains != null) {
|
|
66
|
+
els = els.filter(
|
|
67
|
+
(el) =>
|
|
68
|
+
el.innerHTML.toLowerCase().indexOf(textContains) != -1 ||
|
|
69
|
+
el.innerText.toLowerCase().indexOf(textContains) != -1,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { selector, els };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Query by filter.
|
|
78
|
+
*
|
|
79
|
+
* @param filter Filter
|
|
80
|
+
* @param container Optional container to look in
|
|
81
|
+
* @return HTMLElement if found else null
|
|
82
|
+
*/
|
|
83
|
+
export const queryQuick = <T extends HTMLElement>(filter: IQueryQuick, container?: HTMLElement): T | null => {
|
|
84
|
+
const { els, selector } = quickSelector<T>(filter, container);
|
|
85
|
+
if (els.length > 1) {
|
|
86
|
+
throw `Found multiple(${els.length}) elements by selector ${selector}`;
|
|
87
|
+
}
|
|
88
|
+
return els[0] ?? null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Query all by filter.
|
|
93
|
+
*
|
|
94
|
+
* @param filter Filter
|
|
95
|
+
* @param container Optional container to look in
|
|
96
|
+
* @return HTMLElement array
|
|
97
|
+
*/
|
|
98
|
+
export const queryAllQuick = <T extends HTMLElement>(filter: IQueryQuick, container?: HTMLElement): T[] =>
|
|
99
|
+
quickSelector<T>(filter, container)?.els;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get all by filter.
|
|
103
|
+
*
|
|
104
|
+
* @param filter Filter
|
|
105
|
+
* @param container Optional container to look in
|
|
106
|
+
* @return HTMLElement array. Throws exception if nothing is found.
|
|
107
|
+
*/
|
|
108
|
+
export const getAllQuick = <T extends HTMLElement>(filter: IQueryQuick, container?: HTMLElement): T[] => {
|
|
109
|
+
const els = queryAllQuick(filter, container);
|
|
110
|
+
if (els.length == 0) {
|
|
111
|
+
throw Error(`getAllQuick not found, selector: ${quickSelector(filter)}`);
|
|
112
|
+
}
|
|
113
|
+
return els as T[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get by filter.
|
|
118
|
+
*
|
|
119
|
+
* @param filter Filter
|
|
120
|
+
* @param container Optional container to look in
|
|
121
|
+
* @return HTMLElement. Throws exception if not found.
|
|
122
|
+
*/
|
|
123
|
+
export const getQuick = (filter: IQueryQuick, container?: HTMLElement): HTMLElement => {
|
|
124
|
+
const el = queryQuick(filter, container);
|
|
125
|
+
if (el == null) {
|
|
126
|
+
throw Error(`getQuick not found, selector: ${quickSelector(filter).selector}`);
|
|
127
|
+
}
|
|
128
|
+
return el;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find by filter. Waits up to 4 seconds to find filter.
|
|
133
|
+
*
|
|
134
|
+
* @param filter Filter
|
|
135
|
+
* @param container Optional container to look in
|
|
136
|
+
* @return HTMLElement. Throws exception if not found.
|
|
137
|
+
*/
|
|
138
|
+
export const findQuick = async <T extends HTMLElement>(filter: IQueryQuick, container?: HTMLElement): Promise<T> => {
|
|
139
|
+
const endTime = Date.now() + 4000;
|
|
140
|
+
while (Date.now() < endTime) {
|
|
141
|
+
const el = queryQuick<T>(filter, container);
|
|
142
|
+
if (el) return el;
|
|
143
|
+
await wait(50);
|
|
144
|
+
}
|
|
145
|
+
throw Error(`findQuick not found, selector: ${quickSelector(filter).selector}`);
|
|
146
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { act, within } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { findQuick, getAllQuick, getQuick, queryQuick } from "./testQuick";
|
|
4
|
+
|
|
5
|
+
export const findRow = async (rowId: number | string, within?: HTMLElement): Promise<HTMLDivElement> => {
|
|
6
|
+
return findQuick<HTMLDivElement>({ tagName: `div[row-id='${rowId}']:not(:empty)` }, within);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const queryRow = async (rowId: number | string, within?: HTMLElement): Promise<HTMLDivElement | null> => {
|
|
10
|
+
return queryQuick<HTMLDivElement>({ tagName: `div[row-id='${rowId}']:not(:empty)` }, within);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const _selectRow = async (
|
|
14
|
+
select: "select" | "deselect" | "toggle",
|
|
15
|
+
rowId: string | number,
|
|
16
|
+
within?: HTMLElement,
|
|
17
|
+
): Promise<void> => {
|
|
18
|
+
await act(async () => {
|
|
19
|
+
const row = await findRow(rowId, within);
|
|
20
|
+
const isSelected = !row.className.includes("ag-row-selected");
|
|
21
|
+
if (select === "toggle" || (select === "select" && !isSelected) || (select === "deselect" && isSelected)) {
|
|
22
|
+
userEvent.click(row);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const selectRow = async (rowId: string | number, within?: HTMLElement): Promise<void> =>
|
|
28
|
+
_selectRow("select", rowId, within);
|
|
29
|
+
|
|
30
|
+
export const deselectRow = async (rowId: string | number, within?: HTMLElement): Promise<void> =>
|
|
31
|
+
_selectRow("deselect", rowId, within);
|
|
32
|
+
|
|
33
|
+
export const findCell = async (rowId: number | string, colId: string, within?: HTMLElement): Promise<HTMLElement> => {
|
|
34
|
+
const row = await findRow(rowId, within);
|
|
35
|
+
return await findQuick({ tagName: `[col-id='${colId}']` }, row);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const selectCell = async (rowId: string | number, colId: string, within?: HTMLElement): Promise<void> => {
|
|
39
|
+
await act(async () => {
|
|
40
|
+
const cell = await findCell(rowId, colId, within);
|
|
41
|
+
userEvent.click(cell);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const editCell = async (rowId: number | string, colId: string, within?: HTMLElement): Promise<void> => {
|
|
46
|
+
await act(async () => {
|
|
47
|
+
const cell = await findCell(rowId, colId, within);
|
|
48
|
+
userEvent.dblClick(cell);
|
|
49
|
+
await findOpenMenu();
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const findOpenMenu = async (): Promise<HTMLElement> => findQuick({ classes: ".szh-menu--state-open" });
|
|
54
|
+
|
|
55
|
+
export const queryMenuOption = async (menuOptionText: string): Promise<HTMLElement | null> => {
|
|
56
|
+
const openMenu = await findOpenMenu();
|
|
57
|
+
const els = await within(openMenu).findAllByRole("menuitem");
|
|
58
|
+
const result = els.find(
|
|
59
|
+
(e: HTMLElement) => e.innerHTML?.trim() === menuOptionText || e.innerText?.trim() === menuOptionText,
|
|
60
|
+
);
|
|
61
|
+
return result ?? null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const findMenuOption = async (menuOptionText: string): Promise<HTMLElement> => {
|
|
65
|
+
const menuOption = await queryMenuOption(menuOptionText);
|
|
66
|
+
if (menuOption == null) {
|
|
67
|
+
throw Error(`Unable to find menu option ${menuOptionText}`);
|
|
68
|
+
}
|
|
69
|
+
return menuOption;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const clickMenuOption = async (menuOptionText: string): Promise<void> => {
|
|
73
|
+
await act(async () => {
|
|
74
|
+
const menuOption = await findMenuOption(menuOptionText);
|
|
75
|
+
menuOption && userEvent.click(menuOption);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const openAndClickMenuOption = async (
|
|
80
|
+
rowId: number | string,
|
|
81
|
+
colId: string,
|
|
82
|
+
menuOptionText: string,
|
|
83
|
+
within?: HTMLElement,
|
|
84
|
+
): Promise<void> => {
|
|
85
|
+
await editCell(rowId, colId, within);
|
|
86
|
+
await clickMenuOption(menuOptionText);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const getMultiSelectOptions = async () => {
|
|
90
|
+
const openMenu = await findOpenMenu();
|
|
91
|
+
return getAllQuick<HTMLInputElement>({ role: "menuitem", child: { tagName: "input,textarea" } }, openMenu).map(
|
|
92
|
+
(input) => {
|
|
93
|
+
return {
|
|
94
|
+
v: input.value,
|
|
95
|
+
c: input.checked ?? true,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const findMultiSelectOption = async (value: string): Promise<HTMLElement> => {
|
|
102
|
+
const openMenu = await findOpenMenu();
|
|
103
|
+
return getQuick({ role: "menuitem", child: { tagName: `input[value='${value}']` } }, openMenu);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const clickMultiSelectOption = async (value: string): Promise<void> => {
|
|
107
|
+
const menuItem = await findMultiSelectOption(value);
|
|
108
|
+
menuItem.parentElement && userEvent.click(menuItem.parentElement);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const typeOtherInput = async (value: string): Promise<void> => {
|
|
112
|
+
const openMenu = await findOpenMenu();
|
|
113
|
+
const otherInput = await findQuick({ tagName: "input[type='text']" }, openMenu);
|
|
114
|
+
userEvent.type(otherInput, value);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const typeOtherTextArea = async (value: string): Promise<void> => {
|
|
118
|
+
const openMenu = await findOpenMenu();
|
|
119
|
+
const otherTextArea = await findQuick({ tagName: "textarea" }, openMenu);
|
|
120
|
+
userEvent.type(otherTextArea, value);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const closeMenu = (): void => {
|
|
124
|
+
userEvent.click(document.body);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const findActionButton = (text: string, container: HTMLElement): Promise<HTMLElement> =>
|
|
128
|
+
findQuick({ tagName: "button", child: { classes: ".ActionButton-minimalAreaDisplay", textEquals: text } }, container);
|
|
129
|
+
|
|
130
|
+
export const clickActionButton = async (text: string, container: HTMLElement): Promise<void> => {
|
|
131
|
+
await act(async () => {
|
|
132
|
+
const button = await findActionButton(text, container);
|
|
133
|
+
userEvent.click(button);
|
|
134
|
+
});
|
|
135
|
+
};
|
package/src/utils/textMatcher.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEmpty } from "lodash-es";
|
|
1
|
+
import { isEmpty, partition } from "lodash-es";
|
|
2
2
|
import { isMatch } from "matcher";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -10,8 +10,8 @@ import { isMatch } from "matcher";
|
|
|
10
10
|
* "*L" => *L
|
|
11
11
|
* "A B" => A* and B*
|
|
12
12
|
* "A B, C" => (A* and B*) or C*
|
|
13
|
-
*
|
|
14
|
-
* Returns
|
|
13
|
+
* "!A" => all values must not match A
|
|
14
|
+
* Returns true if there's a text match.
|
|
15
15
|
*/
|
|
16
16
|
export const textMatch = (text: string | undefined | null, filter: string): boolean => {
|
|
17
17
|
if (text == null) return true;
|
|
@@ -20,12 +20,15 @@ export const textMatch = (text: string | undefined | null, filter: string): bool
|
|
|
20
20
|
.split(",")
|
|
21
21
|
.map((sf) => sf.trim())
|
|
22
22
|
.filter((sf) => sf);
|
|
23
|
+
const [negativeFilters, positiveFilters] = partition(superFilters, (superFilters) => superFilters.startsWith("!"));
|
|
23
24
|
const values = text.replaceAll(",", " ").trim().split(/\s+/);
|
|
24
25
|
return (
|
|
25
|
-
isEmpty(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
(isEmpty(positiveFilters) || // Not filtered
|
|
27
|
+
positiveFilters.some((superFilter) => {
|
|
28
|
+
const subFilters = superFilter.split(/\s+/).map((s) => s.replaceAll(/([!?])/g, "\\$1"));
|
|
29
|
+
return subFilters.every((subFilter) => values.some((value) => isMatch(value, subFilter)));
|
|
30
|
+
})) &&
|
|
31
|
+
(isEmpty(negativeFilters) ||
|
|
32
|
+
negativeFilters.every((superFilter) => values.every((value) => isMatch(value, superFilter))))
|
|
30
33
|
);
|
|
31
34
|
};
|