@linzjs/step-ag-grid 7.0.3 → 7.1.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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@linzjs/step-ag-grid",
3
3
  "repository": "github:linz/step-ag-grid.git",
4
4
  "license": "MIT",
5
- "version": "7.0.3",
5
+ "version": "7.1.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -78,7 +78,7 @@ export const GridCell = <RowType extends GridBaseRow, Props extends CellEditorCo
78
78
  }),
79
79
  // Default value formatter, otherwise react freaks out on objects
80
80
  valueFormatter: (params: ValueFormatterParams) => {
81
- if (params.value == null) return "-";
81
+ if (params.value == null) return "";
82
82
  const types = ["number", "boolean", "string"];
83
83
  if (types.includes(typeof params.value)) return `${params.value}`;
84
84
  else return JSON.stringify(params.value);
@@ -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-allowtabtoSave={true}
250
+ data-allowtabtosave={true}
251
251
  onChange={(e) => setFilter(e.target.value)}
252
252
  onKeyDown={handleKeyDown}
253
253
  onKeyUp={handleKeyUp}
@@ -3,7 +3,7 @@ import "../../styles/GridFormEditBearing.scss";
3
3
  import { useCallback, useMemo, useState } from "react";
4
4
  import { GridBaseRow } from "../Grid";
5
5
  import { TextInputFormatted } from "../../lui/TextInputFormatted";
6
- import { bearingNumberParser, bearingStringValidator, convertDDToDMS } from "../../utils/bearing";
6
+ import { bearingNumberParser, bearingStringValidator } from "../../utils/bearing";
7
7
  import { useGridPopoverHook } from "../GridPopoverHook";
8
8
  import { CellEditorCommon } from "../GridCell";
9
9
  import { useGridPopoverContext } from "../../contexts/GridPopoverContext";
@@ -15,7 +15,7 @@ export interface GridFormEditBearingProps<RowType extends GridBaseRow> extends C
15
15
  }
16
16
 
17
17
  export const GridFormEditBearing = <RowType extends GridBaseRow>(props: GridFormEditBearingProps<RowType>) => {
18
- const { field, value: initialValue } = useGridPopoverContext<RowType>();
18
+ const { field, value: initialValue, formatValue } = useGridPopoverContext<RowType>();
19
19
 
20
20
  // This clears out any scientific precision
21
21
  const defaultValue = useMemo(
@@ -65,7 +65,7 @@ export const GridFormEditBearing = <RowType extends GridBaseRow>(props: GridForm
65
65
  }}
66
66
  autoFocus={true}
67
67
  placeholder={props.placeHolder}
68
- formatted={bearingStringValidator(value, props.range) ? "?" : convertDDToDMS(bearingNumberParser(value))}
68
+ formatted={bearingStringValidator(value, props.range) ? "?" : formatValue(bearingNumberParser(value))}
69
69
  error={bearingStringValidator(value, props.range)}
70
70
  helpText={"Press enter or tab to save"}
71
71
  />
@@ -250,7 +250,7 @@ const FilterInput = (props: {
250
250
  data-testid={"filteredMenu-free-text-input"}
251
251
  value={filter}
252
252
  data-disableenterautosave={true}
253
- data-allowtabtoSave={true}
253
+ data-allowtabtosave={true}
254
254
  onChange={(e) => setFilter(e.target.value)}
255
255
  onKeyUp={handleKeyUp}
256
256
  />
@@ -12,7 +12,6 @@ export const GridPopoverEditBearingLike = <RowType extends GridBaseRow>(
12
12
  {
13
13
  initialWidth: 65,
14
14
  maxWidth: 150,
15
- valueFormatter: bearingValueFormatter,
16
15
  ...colDef,
17
16
  },
18
17
  {
@@ -5,7 +5,6 @@ import { GridUpdatingContext } from "../../contexts/GridUpdatingContext";
5
5
  import { GridLoadableCell } from "../GridLoadableCell";
6
6
  import { GridIcon } from "../GridIcon";
7
7
  import { ColDef, ICellRendererParams } from "ag-grid-community";
8
- import { ValueFormatterParams } from "ag-grid-community/dist/lib/entities/colDef";
9
8
  import { GridBaseRow } from "../Grid";
10
9
  import { ColDefT } from "../GridCell";
11
10
 
@@ -33,13 +32,9 @@ export const GridRendererGenericCell = <RowType extends GridBaseRow>(props: ICel
33
32
  const infoFn = cellRendererParams?.info;
34
33
  const infoText = infoFn ? infoFn(props) : undefined;
35
34
 
36
- const defaultFormatter = (props: ValueFormatterParams): string => props.value;
37
- const formatter = colDef.valueFormatter ?? defaultFormatter;
38
- if (typeof formatter === "string") {
39
- console.error("valueFormatter must be a function");
40
- return <span>valueFormatter must be a function</span>;
41
- }
42
- const formatted = formatter(props as ValueFormatterParams);
35
+ const defaultFormatter = (value: any): string => value;
36
+ const formatter = props.formatValue ?? defaultFormatter;
37
+ const formatted = formatter(props.value);
43
38
 
44
39
  return (
45
40
  <GridLoadableCell isLoading={checkUpdating(colDef.field ?? colDef.colId ?? "", props.data.id)}>
@@ -1,14 +1,6 @@
1
1
  import { createContext, RefObject, useContext } from "react";
2
2
  import { GridBaseRow } from "../components/Grid";
3
3
 
4
- export interface PropsType {
5
- value: any;
6
- data: any;
7
- field: string;
8
- selectedRows: any[];
9
- updateValue: (saveFn: (selectedRows: any[]) => Promise<boolean>) => Promise<boolean>;
10
- }
11
-
12
4
  export interface GridPopoverContextType<RowType extends GridBaseRow> {
13
5
  anchorRef: RefObject<Element>;
14
6
  saving: boolean;
@@ -18,6 +10,7 @@ export interface GridPopoverContextType<RowType extends GridBaseRow> {
18
10
  data: RowType;
19
11
  selectedRows: RowType[];
20
12
  updateValue: (saveFn: (selectedRows: any[]) => Promise<boolean>, tabDirection: 1 | 0 | -1) => Promise<boolean>;
13
+ formatValue: (value: any) => any;
21
14
  }
22
15
 
23
16
  export const GridPopoverContext = createContext<GridPopoverContextType<any>>({
@@ -29,6 +22,7 @@ export const GridPopoverContext = createContext<GridPopoverContextType<any>>({
29
22
  data: {} as GridBaseRow,
30
23
  selectedRows: [],
31
24
  updateValue: async () => false,
25
+ formatValue: () => "! No gridPopoverContextProvider !",
32
26
  });
33
27
 
34
28
  export const useGridPopoverContext = <RowType extends GridBaseRow>() =>
@@ -49,6 +49,7 @@ export const GridPopoverContextProvider = ({ props, children }: GridPopoverConte
49
49
  data: props.data,
50
50
  value: props.value,
51
51
  updateValue,
52
+ formatValue: props.formatValue,
52
53
  }}
53
54
  >
54
55
  {children}
package/src/index.ts CHANGED
@@ -45,3 +45,4 @@ export * from "./lui/ActionButton";
45
45
 
46
46
  export * from "./utils/bearing";
47
47
  export * from "./utils/util";
48
+ export * from "./utils/testUtil";
@@ -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-allowtabtoSave");
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);
@@ -37,8 +37,8 @@ export default {
37
37
 
38
38
  interface ITestRow {
39
39
  id: number;
40
- bearing1: string | number | null;
41
40
  bearingCorrection: number | null;
41
+ bearing: string | number | null;
42
42
  }
43
43
 
44
44
  const GridReadOnlyTemplate: ComponentStory<typeof Grid> = (props: GridProps) => {
@@ -51,29 +51,29 @@ const GridReadOnlyTemplate: ComponentStory<typeof Grid> = (props: GridProps) =>
51
51
  initialWidth: 65,
52
52
  maxWidth: 85,
53
53
  }),
54
- GridPopoverEditBearing(
54
+ GridPopoverEditBearingCorrection(
55
55
  {
56
- field: "bearing1",
57
- headerName: "Bearing GCE",
56
+ field: "bearingCorrection",
57
+ headerName: "Bearing correction",
58
58
  cellRendererParams: {
59
- warning: (props: any) => props.data.id == 1002 && "Testers are testing",
60
- info: (props: any) => props.data.id == 1001 && "Developers are developing",
59
+ warning: (props) => props.data.id == 1002 && "Testers are testing",
60
+ info: (props) => props.data.id == 1001 && "Developers are developing",
61
61
  },
62
62
  },
63
63
  {
64
64
  multiEdit: false,
65
65
  },
66
66
  ),
67
- GridPopoverEditBearingCorrection(
67
+ GridPopoverEditBearing(
68
68
  {
69
- field: "bearingCorrection",
70
- headerName: "Bearing Correction",
69
+ field: "bearing",
70
+ headerName: "Bearing",
71
71
  },
72
72
  {
73
73
  editorParams: {
74
- onSave: async (selectedRows, value: ITestRow["bearingCorrection"]) => {
74
+ onSave: async (selectedRows, value: ITestRow["bearing"]) => {
75
75
  await wait(1000);
76
- selectedRows.forEach((row) => (row["bearingCorrection"] = value));
76
+ selectedRows.forEach((row) => (row["bearing"] = value));
77
77
  return true;
78
78
  },
79
79
  },
@@ -84,10 +84,10 @@ const GridReadOnlyTemplate: ComponentStory<typeof Grid> = (props: GridProps) =>
84
84
  );
85
85
 
86
86
  const [rowData] = useState([
87
- { id: 1000, bearing1: 1.234, bearingCorrection: 90 },
88
- { id: 1001, bearing1: "0E-12", bearingCorrection: 240 },
89
- { id: 1002, bearing1: null, bearingCorrection: 355.1 },
90
- { id: 1003, bearing1: null, bearingCorrection: 0 },
87
+ { id: 1000, bearing: 1.234, bearingCorrection: null },
88
+ { id: 1001, bearing: "0E-12", bearingCorrection: 240 },
89
+ { id: 1002, bearing: null, bearingCorrection: 355.1 },
90
+ { id: 1003, bearing: null, bearingCorrection: 0 },
91
91
  ] as ITestRow[]);
92
92
 
93
93
  return (
@@ -3,7 +3,7 @@ import { ValueFormatterParams } from "ag-grid-community/dist/lib/entities/colDef
3
3
  export const bearingValueFormatter = (params: ValueFormatterParams): string => {
4
4
  const value = typeof params.value == "string" ? parseFloat(params.value) : params.value;
5
5
  if (value == null) {
6
- return "-";
6
+ return "";
7
7
  }
8
8
  return convertDDToDMS(value, false, false);
9
9
  };
@@ -11,7 +11,7 @@ export const bearingValueFormatter = (params: ValueFormatterParams): string => {
11
11
  export const bearingCorrectionValueFormatter = (params: ValueFormatterParams): string => {
12
12
  const value = params.value;
13
13
  if (value == null) {
14
- return "-";
14
+ return "";
15
15
  }
16
16
  if (typeof value === "string") {
17
17
  return convertDDToDMS(bearingNumberParser(value), true, true);
@@ -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
+ };