@linzjs/step-ag-grid 2.4.3 → 2.4.5

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": "2.4.3",
5
+ "version": "2.4.5",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -46,6 +46,7 @@
46
46
  },
47
47
  "scripts": {
48
48
  "build": "run-s clean stylelint lint css bundle",
49
+ "yalc": "run-s clean css bundle && yalc publish",
49
50
  "clean": "rimraf dist && mkdirp ./dist",
50
51
  "bundle": "rollup -c",
51
52
  "stylelint": "stylelint src/**/*.scss src/**/*.css --fix",
@@ -107,7 +108,7 @@
107
108
  "babel-preset-react-app": "^10.0.1",
108
109
  "chromatic": "^6.11.4",
109
110
  "conventional-changelog-conventionalcommits": "^5.0.0",
110
- "eslint": "^8.26.0",
111
+ "eslint": "^8.27.0",
111
112
  "eslint-config-prettier": "^8.5.0",
112
113
  "eslint-config-react-app": "^7.0.1",
113
114
  "eslint-plugin-deprecation": "^1.3.2",
@@ -134,7 +135,7 @@
134
135
  "sass-loader": "10.3.1",
135
136
  "semantic-release": "^19.0.5",
136
137
  "storybook-addon-mock": "^2.4.1",
137
- "style-loader": "^2.0.0",
138
+ "style-loader": "2.0.0",
138
139
  "stylelint": "^14.14.1",
139
140
  "stylelint-config-prettier": "^9.0.3",
140
141
  "stylelint-config-standard": "^29.0.0",
@@ -1,6 +1,9 @@
1
- import { useCallback, useRef } from "react";
1
+ import { useCallback, useContext, useRef } from "react";
2
2
  import { PostSortRowsParams } from "ag-grid-community/dist/lib/entities/iCallbackParams";
3
3
  import { ColumnState } from "ag-grid-community/dist/lib/columns/columnModel";
4
+ import { isEmpty } from "lodash-es";
5
+ import { RowNode } from "ag-grid-community";
6
+ import { GridContext } from "../contexts/GridContext";
4
7
 
5
8
  interface PostSortRowsHookProps {
6
9
  setStaleGrid: (stale: boolean) => void;
@@ -12,6 +15,8 @@ interface PostSortRowsHookProps {
12
15
  * Handles stale sort, when you edit a row but don't want to re-sort.
13
16
  */
14
17
  export const usePostSortRowsHook = ({ setStaleGrid }: PostSortRowsHookProps) => {
18
+ const { redrawRows } = useContext(GridContext);
19
+
15
20
  // On first run we need to init the first backed up sort order
16
21
  const initialised = useRef(false);
17
22
 
@@ -19,7 +24,7 @@ export const usePostSortRowsHook = ({ setStaleGrid }: PostSortRowsHookProps) =>
19
24
  const lastSortOrderHash = useRef<string>("");
20
25
 
21
26
  // Used to detect if sort order was the same, has the direction changed
22
- const previousRowSortIndexRef = useRef<Record<number, number | undefined>>({});
27
+ const previousRowSortIndexRef = useRef<Record<string, { index: number; hash: string } | undefined>>({});
23
28
 
24
29
  // stale sort is when there's a sort and user edits a row
25
30
  // this applies a class to the div wrapping the grid which in turn adds a * beside the sort arrow
@@ -33,14 +38,21 @@ export const usePostSortRowsHook = ({ setStaleGrid }: PostSortRowsHookProps) =>
33
38
 
34
39
  return useCallback(
35
40
  ({ api, columnApi, nodes }: PostSortRowsParams) => {
41
+ // Grid is destroyed
42
+ if (!api || !columnApi) return;
43
+
36
44
  const previousRowSortIndex = previousRowSortIndexRef.current;
37
45
 
46
+ const hashNode = (node: RowNode | undefined) => {
47
+ return node ? JSON.stringify(node.data) : "";
48
+ };
49
+
38
50
  const copyCurrentSortSettings = (): ColumnState[] =>
39
51
  columnApi.getColumnState().map((row) => ({ colId: row.colId, sort: row.sort, sortIndex: row.sortIndex }));
40
52
 
41
53
  const backupSortOrder = () => {
42
54
  for (const x in previousRowSortIndex) delete previousRowSortIndex[x];
43
- nodes.forEach((row, index) => (previousRowSortIndex[row.data.id] = index));
55
+ nodes.forEach((node, index) => (previousRowSortIndex[`${node.data.id}`] = { index, hash: hashNode(node) }));
44
56
  };
45
57
 
46
58
  // Check if column is the first sorted column. Note: column is preconfigured to sort its sortIndex is null not 1
@@ -51,22 +63,13 @@ export const usePostSortRowsHook = ({ setStaleGrid }: PostSortRowsHookProps) =>
51
63
 
52
64
  const restorePreviousSortColumnState = () => columnApi.applyColumnState({ state: previousColSort.current });
53
65
 
54
- const hasNewRows = () => nodes.some((row) => previousRowSortIndex[row.data.id] == null);
55
-
56
- const sortIsStale = () => {
57
- // If there are new rows we want to them to be at the bottom of the grid, so we treat it as sort not stale
58
- if (hasNewRows()) return false;
59
-
60
- // Otherwise check if the stored sort index matches the new sort index
61
- return nodes.some((node, index) => previousRowSortIndex[node.data.id] != index);
62
- };
63
-
64
- const sortNodesByPreviousSort = () =>
66
+ const sortNodesByPreviousSort = () => {
65
67
  nodes.sort(
66
68
  (a, b) =>
67
- (previousRowSortIndex[a.data.id] ?? Number.MAX_SAFE_INTEGER) -
68
- (previousRowSortIndex[b.data.id] ?? Number.MAX_SAFE_INTEGER),
69
+ (previousRowSortIndex[`${a.data.id}`]?.index ?? Number.MAX_SAFE_INTEGER) -
70
+ (previousRowSortIndex[`${b.data.id}`]?.index ?? Number.MAX_SAFE_INTEGER),
69
71
  );
72
+ };
70
73
 
71
74
  // On first load copy the current sort
72
75
  if (!initialised.current) {
@@ -83,6 +86,10 @@ export const usePostSortRowsHook = ({ setStaleGrid }: PostSortRowsHookProps) =>
83
86
  sortOrderChanged = true;
84
87
  }
85
88
 
89
+ if (isEmpty(previousRowSortIndex)) {
90
+ backupSortOrder();
91
+ }
92
+
86
93
  if (sortOrderChanged) {
87
94
  const thisFirstCol = copyCurrentSortSettings().find(isFirstSortColumn);
88
95
  const previousFirstCol = previousColSort.current.find(isFirstSortColumn);
@@ -115,23 +122,69 @@ export const usePostSortRowsHook = ({ setStaleGrid }: PostSortRowsHookProps) =>
115
122
  lastSortOrderHash.current = newSortOrder;
116
123
  }
117
124
  } else {
118
- if (sortIsStale()) {
125
+ let firstChangedNodeIndex = -1;
126
+ let lastNewNode: RowNode | undefined = undefined;
127
+ let changedRowCount = 0;
128
+ let newRowCount = 0;
129
+ let index = 0;
130
+ for (const node of nodes) {
131
+ const psr = previousRowSortIndex[`${node.data.id}`];
132
+ if (psr) {
133
+ if (psr.hash != hashNode(node)) {
134
+ if (firstChangedNodeIndex === -1) firstChangedNodeIndex = index;
135
+ changedRowCount++;
136
+ }
137
+ } else {
138
+ lastNewNode = node;
139
+ newRowCount++;
140
+ }
141
+ index++;
142
+ }
143
+
144
+ let wasStale = false;
145
+ if (changedRowCount === 0 && newRowCount === 1) {
146
+ // insert new row at end
147
+ const newIndex = index - 1;
148
+ previousRowSortIndex[`${lastNewNode?.data.id}`] = { index: newIndex, hash: hashNode(lastNewNode) };
149
+ wasStale = true;
150
+ } else if (changedRowCount === 2 && newRowCount === 0) {
151
+ // This must be a swap rows
152
+ backupSortOrder();
153
+ wasStale = false;
154
+ } else if (changedRowCount > 1 && newRowCount === 1) {
155
+ // This must be a insert so, insert new row near the row that changed
156
+ previousRowSortIndex[`${lastNewNode?.data.id}`] = {
157
+ index: firstChangedNodeIndex + 0.5,
158
+ hash: hashNode(lastNewNode),
159
+ };
160
+ wasStale = true;
161
+ // For some reason AgGrid mis-positions the inserted row.
162
+ lastNewNode && redrawRows();
163
+ } else if (changedRowCount == 1 && newRowCount === 0) {
164
+ // User edited one row so, do nothing, retain sort
165
+ wasStale = true;
166
+ } else if (changedRowCount !== 0 || newRowCount != 0) {
167
+ // too many rows changed, resort
168
+ backupSortOrder();
169
+ }
170
+
171
+ if (wasStale) {
172
+ // Check if the sort order the aggrid passed matches our stale sort order
173
+ const stillStale =
174
+ Object.keys(previousRowSortIndex).length != nodes.length ||
175
+ nodes.some((node, index) => previousRowSortIndex[`${node.data.id}`]?.index !== index);
176
+
119
177
  // If we haven't already processed a stale sort then...
120
- if (!sortWasStale.current) {
178
+ if (stillStale && !sortWasStale.current) {
121
179
  // backup sort state, so we can restore it when sort is clicked on a stale column
122
180
  previousColSort.current = copyCurrentSortSettings();
123
- backupSortOrder();
124
181
  sortWasStale.current = true;
125
182
  setStaleGrid(true);
126
183
  }
127
-
128
- sortNodesByPreviousSort();
129
184
  }
130
- // secondary sort backup as there may be new nodes that didn't have their sort registered
131
- // which would cause two new rows to sort out of sequence
132
- backupSortOrder();
185
+ sortNodesByPreviousSort();
133
186
  }
134
187
  },
135
- [setStaleGrid],
188
+ [redrawRows, setStaleGrid],
136
189
  );
137
190
  };
@@ -16,8 +16,9 @@ export const GridPopoverMenu = <RowType extends GridBaseRow>(
16
16
  ): ColDefT<RowType> =>
17
17
  GridCell<RowType, GridFormPopoutMenuProps<RowType>>(
18
18
  {
19
- maxWidth: 64,
19
+ maxWidth: 40,
20
20
  editable: colDef.editable != null ? colDef.editable : true,
21
+ cellStyle: { "justify-content": "flex-end" },
21
22
  cellRenderer: GridRenderPopoutMenuCell,
22
23
  cellClass: custom?.multiEdit ? GenericMultiEditCellClass : undefined,
23
24
  ...colDef,
@@ -9,6 +9,7 @@ export const GridPopoverMessage = <RowType extends GridBaseRow>(
9
9
  ): ColDefT<RowType> =>
10
10
  GridCell(
11
11
  {
12
+ resizable: false,
12
13
  ...colDef,
13
14
  cellRendererParams: {
14
15
  singleClickEdit: true,
@@ -1,5 +1,5 @@
1
1
  import { createContext } from "react";
2
- import { GridApi } from "ag-grid-community";
2
+ import { GridApi, RowNode } from "ag-grid-community";
3
3
  import { GridBaseRow } from "../components/Grid";
4
4
 
5
5
  export interface GridContextType {
@@ -24,6 +24,7 @@ export interface GridContextType {
24
24
  fnUpdate: (selectedRows: any[]) => Promise<boolean>,
25
25
  setSaving?: (saving: boolean) => void,
26
26
  ) => Promise<boolean>;
27
+ redrawRows: (rowNodes?: RowNode[]) => void;
27
28
  }
28
29
 
29
30
  export const GridContext = createContext<GridContextType>({
@@ -83,4 +84,7 @@ export const GridContext = createContext<GridContextType>({
83
84
  console.error("no context provider for modifyUpdating");
84
85
  return false;
85
86
  },
87
+ redrawRows: () => {
88
+ console.error("no context provider for redrawRows");
89
+ },
86
90
  });
@@ -273,6 +273,12 @@ export const GridContextProvider = (props: GridContextProps): ReactElement => {
273
273
  });
274
274
  };
275
275
 
276
+ const redrawRows = (rowNodes?: RowNode[]) => {
277
+ gridApiOp((gridApi) => {
278
+ gridApi.redrawRows(rowNodes ? { rowNodes } : undefined);
279
+ });
280
+ };
281
+
276
282
  return (
277
283
  <GridContext.Provider
278
284
  value={{
@@ -293,6 +299,7 @@ export const GridContextProvider = (props: GridContextProps): ReactElement => {
293
299
  sizeColumnsToFit,
294
300
  stopEditing,
295
301
  updatingCells,
302
+ redrawRows,
296
303
  }}
297
304
  >
298
305
  {props.children}
@@ -6,18 +6,21 @@ import { LuiButton, LuiIcon, LuiMiniSpinner } from "@linzjs/lui";
6
6
  import { IconName } from "@linzjs/lui/dist/components/LuiIcon/LuiIcon";
7
7
  import { usePrevious } from "./reactUtils";
8
8
  import { useStateDeferred } from "./stateDeferredHook";
9
+ import { LuiButtonProps } from "@linzjs/lui/dist/components/LuiButton/LuiButton";
9
10
 
10
11
  export interface ActionButtonProps {
11
12
  icon: IconName;
12
- name: string;
13
+ name?: string;
14
+ "aria-label"?: string;
13
15
  inProgressName?: string;
14
16
  title?: string;
15
17
  dataTestId?: string;
16
18
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "ns";
17
19
  className?: string;
18
- onAction: () => Promise<void>;
20
+ onAction: () => Promise<void> | void;
19
21
  // Used for external code to get access to whether action is in progress
20
22
  externalSetInProgress?: () => void;
23
+ level?: LuiButtonProps["level"];
21
24
  }
22
25
 
23
26
  // Kept this less than one second, so I don't have issues with waitFor as it defaults to 1s
@@ -33,6 +36,8 @@ export const ActionButton = ({
33
36
  onAction,
34
37
  externalSetInProgress,
35
38
  size = "sm",
39
+ level = "tertiary",
40
+ "aria-label": ariaLabel,
36
41
  }: ActionButtonProps): JSX.Element => {
37
42
  const [inProgress, setInProgress] = useState(false);
38
43
  const lastInProgress = usePrevious(inProgress ?? false);
@@ -47,17 +52,22 @@ export const ActionButton = ({
47
52
  <LuiButton
48
53
  data-testid={dataTestId}
49
54
  type={"button"}
50
- level={"tertiary"}
51
- title={title ?? name}
52
- aria-label={name}
55
+ level={level}
56
+ title={title ?? ariaLabel ?? name}
57
+ aria-label={ariaLabel ?? name}
53
58
  className={clsx("lui-button-icon", "ActionButton", className, localInProgress && "ActionButton-inProgress")}
54
59
  size={"lg"}
60
+ style={name == null ? { padding: "8px 5px" } : {}}
55
61
  onClick={async () => {
56
- setInProgress(true);
57
- externalSetInProgress && setInProgress(true);
58
- await onAction();
59
- externalSetInProgress && setInProgress(false);
60
- setInProgress(false);
62
+ const promise = onAction();
63
+ const isPromise = typeof promise !== "undefined";
64
+ if (isPromise) {
65
+ setInProgress(true);
66
+ externalSetInProgress && setInProgress(true);
67
+ if (isPromise) await promise;
68
+ externalSetInProgress && setInProgress(false);
69
+ setInProgress(false);
70
+ }
61
71
  }}
62
72
  disabled={localInProgress}
63
73
  >
@@ -66,13 +76,13 @@ export const ActionButton = ({
66
76
  size={16}
67
77
  divProps={{
68
78
  "data-testid": "loading-spinner",
69
- style: { padding: 0, margin: 0, paddingRight: 8 },
79
+ style: { margin: 0, paddingRight: 5, paddingLeft: 3, paddingBottom: 0, paddingTop: 0 },
70
80
  role: "status",
71
- ["aria-label"]: "Loading",
81
+ "aria-label": "Loading",
72
82
  }}
73
83
  />
74
84
  ) : (
75
- <LuiIcon name={icon} alt={name} size={size} />
85
+ <LuiIcon name={icon} alt={ariaLabel ?? name ?? ""} size={size} />
76
86
  )}
77
87
  <span className={"ActionButton-minimalArea"}>
78
88
  <span className={"ActionButton-minimalAreaDisplay"}>{(localInProgress ? inProgressName : name) ?? name}</span>
@@ -16,7 +16,12 @@ const ActionButtonTemplate: ComponentStory<typeof ActionButton> = () => {
16
16
  const performAction = useCallback(async () => {
17
17
  await wait(1000);
18
18
  }, []);
19
- return <ActionButton icon={"ic_add"} name={"Add new row"} inProgressName={"Adding..."} onAction={performAction} />;
19
+ return (
20
+ <>
21
+ <ActionButton icon={"ic_add"} name={"Add new row"} inProgressName={"Adding..."} onAction={performAction} />
22
+ <ActionButton icon={"ic_add"} aria-label={"Add new row"} onAction={performAction} level={"primary"} />
23
+ </>
24
+ );
20
25
  };
21
26
 
22
27
  export const ActionButtonSimple = ActionButtonTemplate.bind({});
@@ -78,6 +78,7 @@ const GridReadOnlyTemplate: ComponentStory<typeof Grid> = (props: GridProps) =>
78
78
  GridPopoverMessage(
79
79
  {
80
80
  headerName: "Popout message",
81
+ maxWidth: 200,
81
82
  cellRenderer: () => <>Click me!</>,
82
83
  },
83
84
  {
@@ -91,9 +92,7 @@ const GridReadOnlyTemplate: ComponentStory<typeof Grid> = (props: GridProps) =>
91
92
  },
92
93
  ),
93
94
  GridPopoverMenu(
94
- {
95
- headerName: "Menu",
96
- },
95
+ {},
97
96
  {
98
97
  multiEdit: true,
99
98
  editorParams: {
@@ -136,7 +135,6 @@ const GridReadOnlyTemplate: ComponentStory<typeof Grid> = (props: GridProps) =>
136
135
  ),
137
136
  GridPopoverMenu(
138
137
  {
139
- headerName: "Menu disabled",
140
138
  editable: false,
141
139
  },
142
140
  {