@linzjs/step-ag-grid 15.0.2 → 15.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": "15.0.2",
5
+ "version": "15.1.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -14,6 +14,7 @@ import { fnOrVar, isNotEmpty } from "../utils/util";
14
14
  import { GridNoRowsOverlay } from "./GridNoRowsOverlay";
15
15
  import { usePostSortRowsHook } from "./PostSortRowsHook";
16
16
  import { GridHeaderSelect } from "./gridHeader";
17
+ import { GridContextMenuComponent, useGridContextMenu } from "./gridHook";
17
18
 
18
19
  export interface GridBaseRow {
19
20
  id: string | number;
@@ -67,6 +68,16 @@ export interface GridProps {
67
68
  * Once the last cell to edit closes this callback is called.
68
69
  */
69
70
  onCellEditingComplete?: () => void;
71
+
72
+ /**
73
+ * Context menu definition if required.
74
+ */
75
+ contextMenu?: GridContextMenuComponent<any>;
76
+
77
+ /**
78
+ * Whether to select row on context menu.
79
+ */
80
+ contextMenuSelectRow?: boolean;
70
81
  }
71
82
 
72
83
  /**
@@ -79,6 +90,7 @@ export const Grid = ({
79
90
  theme = "ag-theme-alpine",
80
91
  sizeColumns = "auto",
81
92
  selectColumnPinned = null,
93
+ contextMenuSelectRow = false,
82
94
  ...params
83
95
  }: GridProps): JSX.Element => {
84
96
  const {
@@ -569,6 +581,9 @@ export const Grid = ({
569
581
  }
570
582
  }, []);
571
583
 
584
+ const gridContextMenu = useGridContextMenu({ contextMenu: params.contextMenu, contextMenuSelectRow });
585
+
586
+ // This is setting a ref in the GridContext so won't be triggering an update loop
572
587
  setOnCellEditingComplete(params.onCellEditingComplete);
573
588
 
574
589
  return (
@@ -581,6 +596,7 @@ export const Grid = ({
581
596
  gridReady && params.rowData && "Grid-ready",
582
597
  )}
583
598
  >
599
+ {gridContextMenu.component}
584
600
  <div style={{ flex: 1 }} ref={gridDivRef}>
585
601
  <AgGridReact
586
602
  rowHeight={params.rowHeight}
@@ -621,6 +637,8 @@ export const Grid = ({
621
637
  isExternalFilterPresent={isExternalFilterPresent}
622
638
  doesExternalFilterPass={doesExternalFilterPass}
623
639
  maintainColumnOrder={true}
640
+ preventDefaultOnContextMenu={true}
641
+ onCellContextMenu={gridContextMenu.cellContextMenu}
624
642
  />
625
643
  </div>
626
644
  </div>
@@ -0,0 +1 @@
1
+ export * from "./useGridContextMenu";
@@ -0,0 +1,92 @@
1
+ import { ColDef } from "ag-grid-community";
2
+ import { CellContextMenuEvent } from "ag-grid-community/dist/lib/events";
3
+ import { ReactElement, useCallback, useContext, useRef, useState } from "react";
4
+
5
+ import { GridContext } from "../../contexts/GridContext";
6
+ import { ControlledMenu } from "../../react-menu3";
7
+ import { GridBaseRow } from "../Grid";
8
+
9
+ export interface GridContextMenuComponentProps<RowType extends GridBaseRow> {
10
+ selectedRows: RowType[];
11
+ clickedRow: RowType;
12
+ colDef: ColDef;
13
+ close: () => void;
14
+ }
15
+
16
+ export type GridContextMenuComponent<RowType extends GridBaseRow> = (
17
+ props: GridContextMenuComponentProps<RowType>,
18
+ ) => ReactElement | null;
19
+
20
+ export const useGridContextMenu = <RowType extends GridBaseRow>({
21
+ contextMenuSelectRow,
22
+ contextMenu: ContextMenu,
23
+ }: {
24
+ contextMenuSelectRow: boolean;
25
+ contextMenu?: GridContextMenuComponent<RowType>;
26
+ }) => {
27
+ const { redrawRows, prePopupOps, postPopupOps, getSelectedRows } = useContext(GridContext);
28
+
29
+ const [isOpen, setOpen] = useState(false);
30
+ const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
31
+
32
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
33
+ const clickedColDefRef = useRef<ColDef>(null!);
34
+ const selectedRowsRef = useRef<any[]>([]);
35
+ const clickedRowRef = useRef<any>(null);
36
+
37
+ const openMenu = useCallback(
38
+ (e: PointerEvent | null | undefined) => {
39
+ if (!e) return;
40
+ prePopupOps();
41
+ setAnchorPoint({ x: e.clientX, y: e.clientY });
42
+ setOpen(true);
43
+ },
44
+ [prePopupOps],
45
+ );
46
+
47
+ const closeMenu = useCallback(() => {
48
+ setOpen(false);
49
+ redrawRows();
50
+ postPopupOps();
51
+ }, [postPopupOps, redrawRows]);
52
+
53
+ const cellContextMenu = useCallback(
54
+ (event: CellContextMenuEvent) => {
55
+ if (contextMenuSelectRow && !event.node.isSelected()) {
56
+ event.node.setSelected(true, true);
57
+ }
58
+
59
+ clickedColDefRef.current = event.colDef;
60
+ selectedRowsRef.current = getSelectedRows();
61
+ clickedRowRef.current = event.data;
62
+
63
+ // This is actually a pointer event
64
+ openMenu(event.event as PointerEvent);
65
+ },
66
+ [contextMenuSelectRow, getSelectedRows, openMenu],
67
+ );
68
+
69
+ return {
70
+ openMenu,
71
+ cellContextMenu,
72
+ component: ContextMenu ? (
73
+ <>
74
+ <ControlledMenu
75
+ anchorPoint={anchorPoint}
76
+ state={isOpen ? "open" : "closed"}
77
+ direction="right"
78
+ onClose={closeMenu}
79
+ >
80
+ {isOpen && (
81
+ <ContextMenu
82
+ selectedRows={selectedRowsRef.current}
83
+ clickedRow={clickedRowRef.current}
84
+ colDef={clickedColDefRef.current}
85
+ close={closeMenu}
86
+ />
87
+ )}
88
+ </ControlledMenu>
89
+ </>
90
+ ) : null,
91
+ };
92
+ };
@@ -7,6 +7,7 @@ export * from "./GridCellMultiSelectClassRules";
7
7
  export * from "./gridFilter";
8
8
  export * from "./gridForm";
9
9
  export * from "./gridHeader";
10
+ export * from "./gridHook";
10
11
  export * from "./GridIcon";
11
12
  export * from "./GridLoadableCell";
12
13
  export * from "./GridNoRowsOverlay";
@@ -23,6 +23,7 @@ export interface GridContextType<RowType extends GridBaseRow> {
23
23
  getColumnIds: (filter?: keyof ColDef | ((r: ColDef) => boolean | undefined | null | number | string)) => string[];
24
24
  setApis: (gridApi: GridApi | undefined, columnApi: ColumnApi | undefined, dataTestId?: string) => void;
25
25
  prePopupOps: () => void;
26
+ postPopupOps: () => void;
26
27
  setQuickFilter: (quickFilter: string) => void;
27
28
  editingCells: () => boolean;
28
29
  getSelectedRows: <T extends GridBaseRow>() => T[];
@@ -85,6 +86,9 @@ export const GridContext = createContext<GridContextType<any>>({
85
86
  prePopupOps: () => {
86
87
  console.error("no context provider for prePopupOps");
87
88
  },
89
+ postPopupOps: () => {
90
+ console.error("no context provider for prePopupOps");
91
+ },
88
92
  externallySelectedItemsAreInSync: false,
89
93
  setApis: () => {
90
94
  console.error("no context provider for setApis");
@@ -132,6 +132,16 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
132
132
  prePopupFocusedCell.current = gridApi?.getFocusedCell() ?? undefined;
133
133
  }, [gridApi]);
134
134
 
135
+ /**
136
+ * After a popup refocus the cell.
137
+ */
138
+ const postPopupOps = useCallback(() => {
139
+ if (!gridApi) return;
140
+ if (prePopupFocusedCell.current) {
141
+ gridApi?.setFocusedCell(prePopupFocusedCell.current.rowIndex, prePopupFocusedCell.current.column);
142
+ }
143
+ }, [gridApi]);
144
+
135
145
  /**
136
146
  * Get all row id's in grid.
137
147
  */
@@ -663,6 +673,7 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
663
673
  setInvisibleColumnIds,
664
674
  gridReady,
665
675
  prePopupOps,
676
+ postPopupOps,
666
677
  setApis,
667
678
  setQuickFilter,
668
679
  selectRowsById,
@@ -0,0 +1,136 @@
1
+ import { ComponentMeta, ComponentStory } from "@storybook/react/dist/ts3.9/client/preview/types-6-3";
2
+ import { ReactElement, useCallback, useContext, useMemo, useState } from "react";
3
+
4
+ import "@linzjs/lui/dist/fonts";
5
+ import "@linzjs/lui/dist/scss/base.scss";
6
+
7
+ import {
8
+ ActionButton,
9
+ ColDefT,
10
+ Grid,
11
+ GridCell,
12
+ GridContext,
13
+ GridContextMenuComponentProps,
14
+ GridContextProvider,
15
+ GridProps,
16
+ GridUpdatingContextProvider,
17
+ MenuItem,
18
+ wait,
19
+ } from "../..";
20
+ import "../../styles/GridTheme.scss";
21
+ import "../../styles/index.scss";
22
+ import { IFormTestRow } from "./FormTest";
23
+
24
+ export default {
25
+ title: "Components / Grids",
26
+ component: Grid,
27
+ args: {
28
+ quickFilterValue: "",
29
+ selectable: true,
30
+ },
31
+ decorators: [
32
+ (Story) => (
33
+ <div style={{ width: 1024, height: 400 }}>
34
+ <GridUpdatingContextProvider>
35
+ <GridContextProvider>
36
+ <Story />
37
+ </GridContextProvider>
38
+ </GridUpdatingContextProvider>
39
+ </div>
40
+ ),
41
+ ],
42
+ } as ComponentMeta<typeof Grid>;
43
+
44
+ const ContextMenu = ({ clickedRow, colDef, close }: GridContextMenuComponentProps<IFormTestRow>): ReactElement => {
45
+ const onClick = useCallback(() => {
46
+ switch (colDef.field) {
47
+ case "name":
48
+ clickedRow.name = "";
49
+ break;
50
+ case "distance":
51
+ clickedRow.distance = null;
52
+ break;
53
+ }
54
+ close();
55
+ }, [close, colDef.field, clickedRow]);
56
+
57
+ return (
58
+ <>
59
+ <button onClick={onClick}>Button - Clear cell</button>
60
+ <MenuItem onClick={onClick}>Menu Item - Clear cell</MenuItem>
61
+ </>
62
+ );
63
+ };
64
+
65
+ const GridPopoutContextMenuTemplate: ComponentStory<typeof Grid> = (props: GridProps) => {
66
+ const { selectRowsWithFlashDiff } = useContext(GridContext);
67
+ const [externalSelectedItems, setExternalSelectedItems] = useState<any[]>([]);
68
+ const [rowData, setRowData] = useState([
69
+ { id: 1000, name: "IS IS DP12345", nameType: "IS", numba: "IX", plan: "DP 12345", distance: 10 },
70
+ { id: 1001, name: "PEG V SD523", nameType: "PEG", numba: "V", plan: "SD 523", distance: null },
71
+ ] as IFormTestRow[]);
72
+
73
+ const columnDefs: ColDefT<IFormTestRow>[] = useMemo(
74
+ () => [
75
+ GridCell({
76
+ field: "id",
77
+ headerName: "Id",
78
+ }),
79
+ GridCell({
80
+ field: "name",
81
+ headerName: "Name",
82
+ }),
83
+ GridCell({
84
+ field: "distance",
85
+ headerName: "Number input",
86
+ valueFormatter: (params) => {
87
+ const v = params.data.distance;
88
+ return v != null ? `${v}m` : "–";
89
+ },
90
+ }),
91
+ ],
92
+ [],
93
+ );
94
+
95
+ const addRowAction = useCallback(async () => {
96
+ await wait(1000);
97
+
98
+ const lastRow = rowData[rowData.length - 1];
99
+ await selectRowsWithFlashDiff(async () => {
100
+ setRowData([
101
+ ...rowData,
102
+ {
103
+ id: (lastRow?.id ?? 0) + 1,
104
+ name: "?",
105
+ nameType: "?",
106
+ numba: "?",
107
+ plan: "",
108
+ distance: null,
109
+ },
110
+ ]);
111
+ });
112
+ }, [rowData, selectRowsWithFlashDiff]);
113
+
114
+ return (
115
+ <>
116
+ <Grid
117
+ {...props}
118
+ externalSelectedItems={externalSelectedItems}
119
+ setExternalSelectedItems={setExternalSelectedItems}
120
+ columnDefs={columnDefs}
121
+ rowData={rowData}
122
+ domLayout={"autoHeight"}
123
+ defaultColDef={{ minWidth: 70 }}
124
+ sizeColumns={"auto"}
125
+ onCellEditingComplete={() => {
126
+ /* eslint-disable-next-line no-console */
127
+ console.log("Cell editing complete");
128
+ }}
129
+ contextMenu={ContextMenu}
130
+ />
131
+ <ActionButton icon={"ic_add"} name={"Add new row"} inProgressName={"Adding..."} onClick={addRowAction} />
132
+ </>
133
+ );
134
+ };
135
+
136
+ export const EditContextMenu = GridPopoutContextMenuTemplate.bind({});