@linzjs/step-ag-grid 1.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/README.md +4 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +89342 -0
- package/dist/index.js.map +1 -0
- package/dist/src/components/ComponentLoadingWrapper.d.ts +9 -0
- package/dist/src/components/GenericCellClass.d.ts +2 -0
- package/dist/src/components/Grid.d.ts +24 -0
- package/dist/src/components/GridCell.d.ts +31 -0
- package/dist/src/components/GridIcon.d.ts +6 -0
- package/dist/src/components/GridLoadableCell.d.ts +6 -0
- package/dist/src/components/GridPopoutHook.d.ts +7 -0
- package/dist/src/components/GridSubComponentTextArea.d.ts +5 -0
- package/dist/src/components/PostSortRowsHook.d.ts +11 -0
- package/dist/src/components/gridForm/GridFormDropDown.d.ts +26 -0
- package/dist/src/components/gridForm/GridFormEditBearing.d.ts +9 -0
- package/dist/src/components/gridForm/GridFormMessage.d.ts +8 -0
- package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +26 -0
- package/dist/src/components/gridForm/GridFormPopoutMenu.d.ts +24 -0
- package/dist/src/components/gridForm/GridFormTextArea.d.ts +11 -0
- package/dist/src/components/gridForm/GridFormTextInput.d.ts +11 -0
- package/dist/src/components/gridHeader/GridHeaderSelect.d.ts +8 -0
- package/dist/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.d.ts +4 -0
- package/dist/src/components/gridPopoverEdit/GridPopoutMenu.d.ts +10 -0
- package/dist/src/components/gridPopoverEdit/GridPopoverEditBearing.d.ts +4 -0
- package/dist/src/components/gridPopoverEdit/GridPopoverEditDropDown.d.ts +4 -0
- package/dist/src/components/gridPopoverEdit/GridPopoverMessage.d.ts +4 -0
- package/dist/src/components/gridRender/GridRenderGenericCell.d.ts +15 -0
- package/dist/src/components/gridRender/GridRenderPopoutMenuCell.d.ts +3 -0
- package/dist/src/contexts/GridContext.d.ts +25 -0
- package/dist/src/contexts/GridContextProvider.d.ts +11 -0
- package/dist/src/contexts/UpdatingContext.d.ts +6 -0
- package/dist/src/contexts/UpdatingContextProvider.d.ts +7 -0
- package/dist/src/lui/TextAreaInput.d.ts +11 -0
- package/dist/src/lui/TextInputFormatted.d.ts +14 -0
- package/dist/src/setupTests.d.ts +1 -0
- package/dist/src/stories/components/FormTest.d.ts +12 -0
- package/dist/src/stories/components/GridPopoutBearing.stories.d.ts +9 -0
- package/dist/src/stories/components/GridPopoutEditDropDown.stories.d.ts +9 -0
- package/dist/src/stories/components/GridPopoutEditGeneric.stories.d.ts +9 -0
- package/dist/src/stories/components/GridPopoutEditGenericTextArea.stories.d.ts +9 -0
- package/dist/src/stories/components/GridPopoutEditMultiSelect.stories.d.ts +9 -0
- package/dist/src/stories/components/GridReadOnly.stories.d.ts +9 -0
- package/dist/src/utils/bearing.d.ts +5 -0
- package/dist/src/utils/util.d.ts +4 -0
- package/dist/step-ag-grid.esm.js +89297 -0
- package/dist/step-ag-grid.esm.js.map +1 -0
- package/package.json +126 -0
- package/src/components/ComponentLoadingWrapper.tsx +31 -0
- package/src/components/GenericCellClass.tsx +15 -0
- package/src/components/Grid.scss +38 -0
- package/src/components/Grid.tsx +262 -0
- package/src/components/GridCell.tsx +101 -0
- package/src/components/GridIcon.tsx +12 -0
- package/src/components/GridLoadableCell.tsx +22 -0
- package/src/components/GridPopoutHook.tsx +118 -0
- package/src/components/GridSubComponentTextArea.tsx +26 -0
- package/src/components/GridTheme.scss +94 -0
- package/src/components/PostSortRowsHook.ts +131 -0
- package/src/components/gridForm/GridFormDropDown.tsx +192 -0
- package/src/components/gridForm/GridFormEditBearing.scss +3 -0
- package/src/components/gridForm/GridFormEditBearing.tsx +62 -0
- package/src/components/gridForm/GridFormMessage.tsx +34 -0
- package/src/components/gridForm/GridFormMultiSelect.tsx +180 -0
- package/src/components/gridForm/GridFormPopoutMenu.tsx +90 -0
- package/src/components/gridForm/GridFormTextArea.tsx +61 -0
- package/src/components/gridForm/GridFormTextInput.tsx +66 -0
- package/src/components/gridHeader/GridHeaderSelect.tsx +54 -0
- package/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.ts +22 -0
- package/src/components/gridPopoverEdit/GridPopoutMenu.scss +9 -0
- package/src/components/gridPopoverEdit/GridPopoutMenu.tsx +35 -0
- package/src/components/gridPopoverEdit/GridPopoverEditBearing.ts +24 -0
- package/src/components/gridPopoverEdit/GridPopoverEditDropDown.ts +22 -0
- package/src/components/gridPopoverEdit/GridPopoverMessage.ts +22 -0
- package/src/components/gridRender/GridRenderGenericCell.scss +15 -0
- package/src/components/gridRender/GridRenderGenericCell.tsx +49 -0
- package/src/components/gridRender/GridRenderPopoutMenuCell.tsx +22 -0
- package/src/contexts/GridContext.tsx +82 -0
- package/src/contexts/GridContextProvider.tsx +292 -0
- package/src/contexts/UpdatingContext.tsx +16 -0
- package/src/contexts/UpdatingContextProvider.tsx +43 -0
- package/src/lui/TextAreaInput.tsx +50 -0
- package/src/lui/TextInputFormatted.scss +11 -0
- package/src/lui/TextInputFormatted.tsx +60 -0
- package/src/lui-overrides.scss +163 -0
- package/src/react-app-env.d.ts +1 -0
- package/src/setupTests.ts +5 -0
- package/src/stories/components/FormTest.scss +10 -0
- package/src/stories/components/FormTest.tsx +51 -0
- package/src/stories/components/GridPopoutBearing.stories.tsx +100 -0
- package/src/stories/components/GridPopoutEditDropDown.stories.tsx +174 -0
- package/src/stories/components/GridPopoutEditGeneric.stories.tsx +76 -0
- package/src/stories/components/GridPopoutEditGenericTextArea.stories.tsx +106 -0
- package/src/stories/components/GridPopoutEditMultiSelect.stories.tsx +111 -0
- package/src/stories/components/GridReadOnly.stories.tsx +151 -0
- package/src/utils/bearing.ts +72 -0
- package/src/utils/util.ts +26 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ICellEditorParams } from "ag-grid-community";
|
|
2
|
+
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { GridContext } from "../contexts/GridContext";
|
|
4
|
+
import { ControlledMenu } from "@szhsin/react-menu";
|
|
5
|
+
import { GridFormProps } from "./GridCell";
|
|
6
|
+
import { hasParentClass } from "../utils/util";
|
|
7
|
+
import { GridBaseRow } from "./Grid";
|
|
8
|
+
|
|
9
|
+
export const useGridPopoutHook = <RowType extends GridBaseRow>(
|
|
10
|
+
props: GridFormProps<RowType>,
|
|
11
|
+
save?: (selectedRows: any[]) => Promise<boolean>,
|
|
12
|
+
) => {
|
|
13
|
+
const { cellEditorParams, saving, updateValue } = props;
|
|
14
|
+
const { eGridCell } = cellEditorParams as ICellEditorParams;
|
|
15
|
+
const { stopEditing } = useContext(GridContext);
|
|
16
|
+
const saveButtonRef = useRef<HTMLButtonElement>(null);
|
|
17
|
+
const anchorRef = useRef(eGridCell);
|
|
18
|
+
anchorRef.current = eGridCell;
|
|
19
|
+
const [isOpen, setOpen] = useState(false);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setOpen(true);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const triggerSave = useCallback(
|
|
26
|
+
async (reason?: string) => {
|
|
27
|
+
if (reason == "cancel" || !save || (await updateValue(save))) {
|
|
28
|
+
setOpen(false);
|
|
29
|
+
stopEditing();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
[save, stopEditing, updateValue],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const clickIsWithinMenu = useCallback((ev: MouseEvent) => {
|
|
36
|
+
return hasParentClass("szh-menu--state-open", ev.target as Node);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const handleScreenMouseDown = useCallback(
|
|
40
|
+
(ev: MouseEvent) => {
|
|
41
|
+
if (!clickIsWithinMenu(ev)) {
|
|
42
|
+
ev.preventDefault();
|
|
43
|
+
ev.stopPropagation();
|
|
44
|
+
// There's an issue in React17
|
|
45
|
+
// the cell doesn't refresh during update if save is invoked from a native event
|
|
46
|
+
// This doesn't happen in React18
|
|
47
|
+
// To work around it, I invoke the save by clicking on an invisible button in the dropdown
|
|
48
|
+
saveButtonRef.current?.click();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[clickIsWithinMenu],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const handleScreenMouseEvent = useCallback(
|
|
55
|
+
(ev: MouseEvent) => {
|
|
56
|
+
if (!clickIsWithinMenu(ev)) {
|
|
57
|
+
ev.preventDefault();
|
|
58
|
+
ev.stopPropagation();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[clickIsWithinMenu],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (isOpen) {
|
|
66
|
+
document.addEventListener("mousedown", handleScreenMouseDown, true);
|
|
67
|
+
document.addEventListener("mouseup", handleScreenMouseEvent, true);
|
|
68
|
+
document.addEventListener("click", handleScreenMouseEvent, true);
|
|
69
|
+
return () => {
|
|
70
|
+
document.removeEventListener("mousedown", handleScreenMouseDown, true);
|
|
71
|
+
document.removeEventListener("mouseup", handleScreenMouseEvent, true);
|
|
72
|
+
document.removeEventListener("click", handleScreenMouseEvent, true);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return () => {};
|
|
76
|
+
}, [handleScreenMouseDown, handleScreenMouseEvent, isOpen]);
|
|
77
|
+
|
|
78
|
+
const popoutWrapper = useCallback(
|
|
79
|
+
(children: JSX.Element) => {
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{anchorRef.current && (
|
|
83
|
+
<ControlledMenu
|
|
84
|
+
state={isOpen ? "open" : "closed"}
|
|
85
|
+
portal={true}
|
|
86
|
+
unmountOnClose={true}
|
|
87
|
+
anchorRef={anchorRef}
|
|
88
|
+
menuClassName={"lui-menu"}
|
|
89
|
+
onClose={(event) => triggerSave(event.reason).then()}
|
|
90
|
+
>
|
|
91
|
+
{saving && (
|
|
92
|
+
<div
|
|
93
|
+
style={{
|
|
94
|
+
position: "absolute",
|
|
95
|
+
left: 0,
|
|
96
|
+
top: 0,
|
|
97
|
+
bottom: 0,
|
|
98
|
+
right: 0,
|
|
99
|
+
backgroundColor: "rgba(64,64,64,0.1)",
|
|
100
|
+
zIndex: 1000,
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
{children}
|
|
105
|
+
<button ref={saveButtonRef} onClick={() => triggerSave().then()} style={{ display: "none" }} />
|
|
106
|
+
</ControlledMenu>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
[isOpen, saving, triggerSave],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
popoutWrapper,
|
|
116
|
+
triggerSave,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface GridSubComponentTextAreaProps {
|
|
4
|
+
setValue: (value: string) => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const GridSubComponentTextArea = (props: GridSubComponentTextAreaProps) => {
|
|
8
|
+
const [value, setValue] = useState("");
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
props.setValue(value);
|
|
12
|
+
}, [props, value]);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={"FreeTextInput LuiDeprecatedForms"}>
|
|
16
|
+
<textarea
|
|
17
|
+
autoFocus
|
|
18
|
+
maxLength={10000}
|
|
19
|
+
spellCheck={true}
|
|
20
|
+
className={"free-text-input"}
|
|
21
|
+
onChange={(e) => setValue(e.target.value)}
|
|
22
|
+
defaultValue={value}
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
@use '~@linzjs/lui/dist/scss/Core' as lui;
|
|
2
|
+
|
|
3
|
+
@import "node_modules/ag-grid-community/src/styles/ag-grid.scss";
|
|
4
|
+
@import "node_modules/ag-grid-community/src/styles/ag-theme-alpine/sass/ag-theme-alpine-mixin";
|
|
5
|
+
|
|
6
|
+
.ag-theme-alpine {
|
|
7
|
+
// AG Grid themes can be customized using theme parameters, which are arguments that change its appearance. Refer to
|
|
8
|
+
// https://www.ag-grid.com/javascript-data-grid/themes-customising/#full-list-of-theme-parameters for a full list of
|
|
9
|
+
// the available theme parameters, which should be used whenever possible.
|
|
10
|
+
@include ag-theme-alpine((
|
|
11
|
+
alpine-active-color: lui.$sea,
|
|
12
|
+
foreground-color: lui.$grey-80,
|
|
13
|
+
data-color: lui.$grey-80,
|
|
14
|
+
header-foreground-color: lui.$surfie,
|
|
15
|
+
background-color: lui.$white,
|
|
16
|
+
odd-row-background-color: lui.$white,
|
|
17
|
+
header-background-color: lui.$white,
|
|
18
|
+
border-color: lui.$lily,
|
|
19
|
+
secondary-border-color: lui.$lily,
|
|
20
|
+
row-hover-color: lui.$lily,
|
|
21
|
+
font-family: ("Open Sans", system-ui, sans-serif),
|
|
22
|
+
font-size: 0.875rem !important,
|
|
23
|
+
borders: false,
|
|
24
|
+
borders-critical: true,
|
|
25
|
+
borders-secondary: false,
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// Fix that when you click below checkbox it doesn't process a click
|
|
29
|
+
.ag-cell[col-id="selection"] {
|
|
30
|
+
display: flex;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fix that when you click below checkbox it doesn't process a click
|
|
34
|
+
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
|
35
|
+
height: auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ag-row:last-of-type {
|
|
39
|
+
border-bottom: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// You can set a background _color_ with theme parameters, but for something like a gradient, you need to override CSS.
|
|
43
|
+
.ag-row-selected {
|
|
44
|
+
background-color: lui.$polar;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ag-header-cell {
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
padding: 7px 0 7px 8px;
|
|
50
|
+
|
|
51
|
+
.LuiIcon {
|
|
52
|
+
fill: lui.$surfie;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.ag-cell {
|
|
57
|
+
padding-left: 7px;
|
|
58
|
+
padding-right: 4px;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ag-cell-more {
|
|
64
|
+
padding: 0;
|
|
65
|
+
display: flex;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ag-cell-wrap-text {
|
|
70
|
+
word-break: break-word;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ag-cell-popup-editing, .ag-selected-for-edit, .ag-cell-inline-editing {
|
|
74
|
+
background: rgba(72, 160, 244, 0.2);
|
|
75
|
+
// These are important, it needs to override ag-grid to work
|
|
76
|
+
border: 3px solid #48a0f4 !important;
|
|
77
|
+
border-right: 3px solid #48a0f4 !important;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ag-row .ag-cell-data-changed {
|
|
81
|
+
// ag-grid natively has !important on this style so we have to use it here :(
|
|
82
|
+
background-color: lightgoldenrodyellow !important;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ag-center-cols-clipper {
|
|
86
|
+
// when using domLayout={"autoHeight"}, ag grid has a default min-height set to 150px so the !important is necessary here
|
|
87
|
+
min-height: 40px !important;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.ag-body-horizontal-scroll-viewport {
|
|
91
|
+
// It's set to scroll by default, but this causes issues with selecting the last row
|
|
92
|
+
overflow-x: auto;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { PostSortRowsParams } from "ag-grid-community/dist/lib/entities/iCallbackParams";
|
|
3
|
+
import { ColumnState } from "ag-grid-community/dist/lib/columns/columnModel";
|
|
4
|
+
|
|
5
|
+
interface PostSortRowsHookProps {
|
|
6
|
+
setStaleGrid: (stale: boolean) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retains last sort order from via <AgGrid postRowSort>.
|
|
11
|
+
* Then applies this sort until next sort column change.
|
|
12
|
+
* Handles stale sort, when you edit a row but don't want to re-sort.
|
|
13
|
+
*/
|
|
14
|
+
export const usePostSortRowsHook = ({ setStaleGrid }: PostSortRowsHookProps) => {
|
|
15
|
+
// On first run we need to init the first backed up sort order
|
|
16
|
+
const initialised = useRef(false);
|
|
17
|
+
|
|
18
|
+
// Used to detect sort order has changed since last run
|
|
19
|
+
const lastSortOrderHash = useRef<string>("");
|
|
20
|
+
|
|
21
|
+
// Used to detect if sort order was the same, has the direction changed
|
|
22
|
+
const previousRowSortIndexRef = useRef<Record<number, number | undefined>>({});
|
|
23
|
+
|
|
24
|
+
// stale sort is when there's a sort and user edits a row
|
|
25
|
+
// this applies a class to the div wrapping the grid which in turn adds a * beside the sort arrow
|
|
26
|
+
const sortWasStale = useRef(false);
|
|
27
|
+
|
|
28
|
+
// Previous col sort is stored and reapplied if the sort direction changes on the same column and the sort was stale
|
|
29
|
+
// As a sort on a stale row should just resort, not change sort direction
|
|
30
|
+
const previousColSort = useRef<ColumnState[]>([]);
|
|
31
|
+
|
|
32
|
+
const previousQuickFilter = useRef("");
|
|
33
|
+
|
|
34
|
+
return useCallback(
|
|
35
|
+
({ api, columnApi, nodes }: PostSortRowsParams) => {
|
|
36
|
+
const previousRowSortIndex = previousRowSortIndexRef.current;
|
|
37
|
+
|
|
38
|
+
const copyCurrentSortSettings = (): ColumnState[] =>
|
|
39
|
+
columnApi.getColumnState().map((row) => ({ colId: row.colId, sort: row.sort, sortIndex: row.sortIndex }));
|
|
40
|
+
|
|
41
|
+
const backupSortOrder = () => {
|
|
42
|
+
for (const x in previousRowSortIndex) delete previousRowSortIndex[x];
|
|
43
|
+
nodes.forEach((row, index) => (previousRowSortIndex[row.data.id] = index));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Check if column is the first sorted column. Note: column is preconfigured to sort its sortIndex is null not 1
|
|
47
|
+
const isFirstSortColumn = (row: ColumnState) => row.sort && [0, null].includes(row.sortIndex ?? null);
|
|
48
|
+
|
|
49
|
+
const isSameColumnAndDifferentSort = (col1: ColumnState | undefined, col2: ColumnState | undefined) =>
|
|
50
|
+
col1 && col2 && col1.colId === col2.colId && col1.sort != col2.sort;
|
|
51
|
+
|
|
52
|
+
const restorePreviousSortColumnState = () => columnApi.applyColumnState({ state: previousColSort.current });
|
|
53
|
+
|
|
54
|
+
const hasNewRows = () => nodes.some((row) => previousRowSortIndex[row.data.id] == null);
|
|
55
|
+
|
|
56
|
+
const sortIsStale = () =>
|
|
57
|
+
!hasNewRows() && nodes.some((row, index) => ![null, index].includes(previousRowSortIndex[row.data.id] ?? null));
|
|
58
|
+
|
|
59
|
+
const sortNodesByPreviousSort = () =>
|
|
60
|
+
nodes.sort(
|
|
61
|
+
(a, b) =>
|
|
62
|
+
(previousRowSortIndex[a.data.id] ?? Number.MAX_SAFE_INTEGER) -
|
|
63
|
+
(previousRowSortIndex[b.data.id] ?? Number.MAX_SAFE_INTEGER),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// On first load copy the current sort
|
|
67
|
+
if (!initialised.current) {
|
|
68
|
+
initialised.current = true;
|
|
69
|
+
previousColSort.current = copyCurrentSortSettings();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const newSortOrder = JSON.stringify(copyCurrentSortSettings());
|
|
73
|
+
let sortOrderChanged = newSortOrder != lastSortOrderHash.current;
|
|
74
|
+
|
|
75
|
+
const quickFilter = (api as any).filterManager.quickFilter;
|
|
76
|
+
if (previousQuickFilter.current != quickFilter) {
|
|
77
|
+
previousQuickFilter.current = quickFilter;
|
|
78
|
+
sortOrderChanged = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (sortOrderChanged) {
|
|
82
|
+
const thisFirstCol = copyCurrentSortSettings().find(isFirstSortColumn);
|
|
83
|
+
const previousFirstCol = previousColSort.current.find(isFirstSortColumn);
|
|
84
|
+
|
|
85
|
+
// Change to sort can be: (in the context of stale sort do...)
|
|
86
|
+
// - no sort to sorted (do nothing)
|
|
87
|
+
// - asc to desc (resort as asc, and remove stale sort)
|
|
88
|
+
// - desc to no sort (resort as desc, and remove stale sort)
|
|
89
|
+
const wasSortChangedOnSameColumn =
|
|
90
|
+
previousFirstCol && (!thisFirstCol || isSameColumnAndDifferentSort(previousFirstCol, thisFirstCol));
|
|
91
|
+
|
|
92
|
+
// if sort was stale, and we're here, someone has clicked a stale sort order (^*) in the column header
|
|
93
|
+
if (sortWasStale.current && wasSortChangedOnSameColumn) {
|
|
94
|
+
// Sort was an existing priority 1 column
|
|
95
|
+
// We want to re-sort with the old sort direction and clear stale sort
|
|
96
|
+
sortWasStale.current = false;
|
|
97
|
+
setStaleGrid(false);
|
|
98
|
+
|
|
99
|
+
// trigger a recapture of sort order
|
|
100
|
+
lastSortOrderHash.current = "";
|
|
101
|
+
restorePreviousSortColumnState();
|
|
102
|
+
} else {
|
|
103
|
+
// Sort was on a different column so clear sort and sort like normal
|
|
104
|
+
if (sortWasStale.current) {
|
|
105
|
+
sortWasStale.current = false;
|
|
106
|
+
setStaleGrid(false);
|
|
107
|
+
}
|
|
108
|
+
// Columns were sorted so retain the sort
|
|
109
|
+
backupSortOrder();
|
|
110
|
+
lastSortOrderHash.current = newSortOrder;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
if (sortIsStale()) {
|
|
114
|
+
// If we haven't already processed a stale sort then...
|
|
115
|
+
if (!sortWasStale.current) {
|
|
116
|
+
// backup sort state, so we can restore it when sort is clicked on a stale column
|
|
117
|
+
previousColSort.current = copyCurrentSortSettings();
|
|
118
|
+
sortWasStale.current = true;
|
|
119
|
+
setStaleGrid(true);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
sortNodesByPreviousSort();
|
|
123
|
+
}
|
|
124
|
+
// secondary sort backup as there may be new nodes that didn't have their sort registered
|
|
125
|
+
// which would cause two new rows to sort out of sequence
|
|
126
|
+
backupSortOrder();
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[setStaleGrid],
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import "@szhsin/react-menu/dist/index.css";
|
|
2
|
+
|
|
3
|
+
import { MenuItem, MenuDivider, FocusableItem } from "@szhsin/react-menu";
|
|
4
|
+
import { useCallback, useContext, useEffect, useRef, useState, KeyboardEvent } from "react";
|
|
5
|
+
import { GridBaseRow } from "../Grid";
|
|
6
|
+
import { ComponentLoadingWrapper } from "../ComponentLoadingWrapper";
|
|
7
|
+
import { GridContext } from "../../contexts/GridContext";
|
|
8
|
+
import { delay } from "lodash-es";
|
|
9
|
+
import debounce from "debounce-promise";
|
|
10
|
+
import { GenericCellEditorParams, GridFormProps } from "../GridCell";
|
|
11
|
+
import { useGridPopoutHook } from "../GridPopoutHook";
|
|
12
|
+
|
|
13
|
+
export interface GridPopoutEditDropDownSelectedItem<RowType, ValueType> {
|
|
14
|
+
selectedRows: RowType[];
|
|
15
|
+
value: ValueType;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface FinalSelectOption<ValueType> {
|
|
19
|
+
value: ValueType;
|
|
20
|
+
label?: JSX.Element | string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const MenuSeparatorString = "_____MENU_SEPARATOR_____";
|
|
24
|
+
export const MenuSeparator = Object.freeze({ value: MenuSeparatorString });
|
|
25
|
+
|
|
26
|
+
export type SelectOption<ValueType> = ValueType | FinalSelectOption<ValueType>;
|
|
27
|
+
|
|
28
|
+
export interface GridFormPopoutDropDownProps<RowType extends GridBaseRow, ValueType>
|
|
29
|
+
extends GenericCellEditorParams<RowType> {
|
|
30
|
+
filtered?: "local" | "reload";
|
|
31
|
+
filterPlaceholder?: string;
|
|
32
|
+
onSelectedItem?: (props: GridPopoutEditDropDownSelectedItem<RowType, ValueType>) => Promise<void>;
|
|
33
|
+
options:
|
|
34
|
+
| SelectOption<ValueType>[]
|
|
35
|
+
| ((selectedRows: RowType[], filter?: string) => Promise<SelectOption<ValueType>[]> | SelectOption<ValueType>[]);
|
|
36
|
+
optionsRequestCancel?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const GridFormDropDown = <RowType extends GridBaseRow, ValueType>(props: GridFormProps<RowType>) => {
|
|
40
|
+
const { popoutWrapper } = useGridPopoutHook(props);
|
|
41
|
+
const formProps = props.formProps as GridFormPopoutDropDownProps<RowType, ValueType>;
|
|
42
|
+
|
|
43
|
+
const { updatingCells, stopEditing } = useContext(GridContext);
|
|
44
|
+
|
|
45
|
+
const [filter, setFilter] = useState("");
|
|
46
|
+
const [filteredValues, setFilteredValues] = useState<any[]>([]);
|
|
47
|
+
const optionsInitialising = useRef(false);
|
|
48
|
+
const [options, setOptions] = useState<FinalSelectOption<ValueType>[] | null>(null);
|
|
49
|
+
|
|
50
|
+
const selectItemHandler = useCallback(
|
|
51
|
+
async (value: ValueType): Promise<boolean> => {
|
|
52
|
+
const field = props.field;
|
|
53
|
+
return await updatingCells({ selectedRows: props.selectedRows, field }, async (selectedRows) => {
|
|
54
|
+
const hasChanged = selectedRows.some((row) => row[field as keyof RowType] !== value);
|
|
55
|
+
if (hasChanged) {
|
|
56
|
+
if (formProps.onSelectedItem) {
|
|
57
|
+
await formProps.onSelectedItem({ selectedRows, value });
|
|
58
|
+
} else {
|
|
59
|
+
selectedRows.forEach((row) => (row[field as keyof RowType] = value));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
[formProps, props.field, props.selectedRows, updatingCells],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Load up options list if it's async function
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (options || optionsInitialising.current) return;
|
|
71
|
+
optionsInitialising.current = true;
|
|
72
|
+
let optionsConf = formProps.options ?? [];
|
|
73
|
+
|
|
74
|
+
(async () => {
|
|
75
|
+
if (typeof optionsConf == "function") {
|
|
76
|
+
optionsConf = await optionsConf(props.selectedRows, filter);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const optionsList = optionsConf?.map((item) => {
|
|
80
|
+
if (item == null || typeof item == "string" || typeof item == "number") {
|
|
81
|
+
item = { value: item as ValueType, label: item } as FinalSelectOption<ValueType>;
|
|
82
|
+
}
|
|
83
|
+
return item;
|
|
84
|
+
}) as any as FinalSelectOption<ValueType>[];
|
|
85
|
+
|
|
86
|
+
if (formProps.filtered) {
|
|
87
|
+
// This is needed otherwise when filter input is rendered and sets autofocus
|
|
88
|
+
// the mouse up of the double click edit triggers the cell to cancel editing
|
|
89
|
+
delay(() => setOptions(optionsList), 100);
|
|
90
|
+
} else {
|
|
91
|
+
setOptions(optionsList);
|
|
92
|
+
}
|
|
93
|
+
optionsInitialising.current = false;
|
|
94
|
+
})();
|
|
95
|
+
}, [filter, options, formProps.filtered, formProps.options, props.selectedRows]);
|
|
96
|
+
|
|
97
|
+
// Local filtering
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (formProps.filtered == "local") {
|
|
100
|
+
if (options == null) return;
|
|
101
|
+
setFilteredValues(
|
|
102
|
+
options
|
|
103
|
+
.map((option) => {
|
|
104
|
+
if (option.label != null && typeof option.label !== "string") {
|
|
105
|
+
console.error("Cannot filter non-string labels", option);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const str = (option.label as string) || "";
|
|
109
|
+
return str.toLowerCase().indexOf(filter) === -1 ? option.value : undefined;
|
|
110
|
+
})
|
|
111
|
+
.filter((r) => r !== undefined),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}, [formProps.filtered, filter, options]);
|
|
115
|
+
|
|
116
|
+
const researchOnFilterChange = debounce(
|
|
117
|
+
useCallback(() => {
|
|
118
|
+
setOptions(null);
|
|
119
|
+
}, []),
|
|
120
|
+
500,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const previousFilter = useRef<string>(filter);
|
|
124
|
+
|
|
125
|
+
// Reload filtering
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (previousFilter.current != filter && formProps.filtered == "reload") {
|
|
128
|
+
previousFilter.current = filter;
|
|
129
|
+
formProps.optionsRequestCancel && formProps.optionsRequestCancel();
|
|
130
|
+
researchOnFilterChange().then();
|
|
131
|
+
}
|
|
132
|
+
}, [filter, formProps, props, researchOnFilterChange]);
|
|
133
|
+
|
|
134
|
+
const onFilterKeyDown = useCallback(
|
|
135
|
+
async (e: KeyboardEvent) => {
|
|
136
|
+
if (!options) return;
|
|
137
|
+
if (e.key == "Enter" || e.key == "Tab") {
|
|
138
|
+
const activeOptions = options.filter((option) => !filteredValues.includes(option.value));
|
|
139
|
+
if (activeOptions.length == 1) {
|
|
140
|
+
await selectItemHandler(activeOptions[0].value);
|
|
141
|
+
stopEditing();
|
|
142
|
+
} else {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
[filteredValues, options, selectItemHandler, stopEditing],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return popoutWrapper(
|
|
152
|
+
<>
|
|
153
|
+
{formProps.filtered && (
|
|
154
|
+
<>
|
|
155
|
+
<FocusableItem className={"filter-item"} index={-1}>
|
|
156
|
+
{({ ref }: any) => (
|
|
157
|
+
<div style={{ display: "flex", width: "100%" }}>
|
|
158
|
+
<input
|
|
159
|
+
autoFocus
|
|
160
|
+
className={"free-text-input"}
|
|
161
|
+
style={{ border: "0px" }}
|
|
162
|
+
ref={ref}
|
|
163
|
+
type="text"
|
|
164
|
+
placeholder={formProps.filterPlaceholder ?? "Placeholder"}
|
|
165
|
+
data-testid={"filteredMenu-free-text-input"}
|
|
166
|
+
defaultValue={filter}
|
|
167
|
+
onChange={(e) => setFilter(e.target.value.toLowerCase())}
|
|
168
|
+
onKeyDown={(e) => onFilterKeyDown(e)}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</FocusableItem>
|
|
173
|
+
<MenuDivider key={`$$divider_filter`} />
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
<ComponentLoadingWrapper loading={!options}>
|
|
177
|
+
<>
|
|
178
|
+
{options && options.length == filteredValues?.length && <MenuItem>[Empty]</MenuItem>}
|
|
179
|
+
{options?.map((item, index) =>
|
|
180
|
+
item.value === MenuSeparatorString ? (
|
|
181
|
+
<MenuDivider key={`$$divider_${index}`} />
|
|
182
|
+
) : filteredValues.includes(item.value) ? null : (
|
|
183
|
+
<MenuItem key={`${item.value}`} value={item.value} onClick={() => selectItemHandler(item.value)}>
|
|
184
|
+
{item.label ?? (item.value == null ? `<${item.value}>` : `${item.value}`)}
|
|
185
|
+
</MenuItem>
|
|
186
|
+
),
|
|
187
|
+
)}
|
|
188
|
+
</>
|
|
189
|
+
</ComponentLoadingWrapper>
|
|
190
|
+
</>,
|
|
191
|
+
);
|
|
192
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import "./GridFormEditBearing.scss";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { GridBaseRow } from "../Grid";
|
|
5
|
+
import { TextInputFormatted } from "../../lui/TextInputFormatted";
|
|
6
|
+
import { bearingNumberParser, bearingStringValidator, convertDDToDMS } from "../../utils/bearing";
|
|
7
|
+
import { GenericCellEditorParams, GridFormProps } from "../GridCell";
|
|
8
|
+
import { useGridPopoutHook } from "../GridPopoutHook";
|
|
9
|
+
|
|
10
|
+
export interface GridFormEditBearingProps<RowType extends GridBaseRow> extends GenericCellEditorParams<RowType> {
|
|
11
|
+
placeHolder: string;
|
|
12
|
+
onSave?: (selectedRows: RowType[], value: number | null) => Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GridFormEditBearing = <RowType extends GridBaseRow>(props: GridFormProps<RowType>) => {
|
|
16
|
+
const formProps = props.formProps as GridFormEditBearingProps<RowType>;
|
|
17
|
+
const [value, setValue] = useState<string>(`${props.value ?? ""}`);
|
|
18
|
+
|
|
19
|
+
const save = useCallback(
|
|
20
|
+
async (selectedRows: RowType[]): Promise<boolean> => {
|
|
21
|
+
if (bearingStringValidator(value)) return false;
|
|
22
|
+
const parsedValue = bearingNumberParser(value);
|
|
23
|
+
// Value didn't change so don't save just cancel
|
|
24
|
+
if (parsedValue === props.value) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (formProps.onSave) {
|
|
28
|
+
return await formProps.onSave(selectedRows, parsedValue);
|
|
29
|
+
} else {
|
|
30
|
+
const field = props.field;
|
|
31
|
+
if (field == null) {
|
|
32
|
+
console.error("field is not defined in ColDef");
|
|
33
|
+
} else {
|
|
34
|
+
selectedRows.forEach((row) => ((row as Record<string, any>)[field] = parsedValue));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
[formProps, props.field, props.value, value],
|
|
40
|
+
);
|
|
41
|
+
const { popoutWrapper, triggerSave } = useGridPopoutHook(props, save);
|
|
42
|
+
|
|
43
|
+
return popoutWrapper(
|
|
44
|
+
<div className={"GridFormEditBearing-input Grid-popoverContainer"}>
|
|
45
|
+
<TextInputFormatted
|
|
46
|
+
value={value ?? ""}
|
|
47
|
+
onChange={(e) => {
|
|
48
|
+
setValue(e.target.value.trim());
|
|
49
|
+
}}
|
|
50
|
+
inputProps={{
|
|
51
|
+
autoFocus: true,
|
|
52
|
+
placeholder: formProps.placeHolder,
|
|
53
|
+
disabled: false,
|
|
54
|
+
maxLength: 16,
|
|
55
|
+
onKeyDown: async (e) => e.key === "Enter" && triggerSave().then(),
|
|
56
|
+
}}
|
|
57
|
+
formatted={bearingStringValidator(value) ? "?" : convertDDToDMS(bearingNumberParser(value))}
|
|
58
|
+
error={bearingStringValidator(value)}
|
|
59
|
+
/>
|
|
60
|
+
</div>,
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { GenericCellEditorParams, GridFormProps } from "../GridCell";
|
|
3
|
+
import { ICellEditorParams } from "ag-grid-community";
|
|
4
|
+
import { ComponentLoadingWrapper } from "../ComponentLoadingWrapper";
|
|
5
|
+
import { GridBaseRow } from "../Grid";
|
|
6
|
+
import { useGridPopoutHook } from "../GridPopoutHook";
|
|
7
|
+
|
|
8
|
+
export interface GridFormMessageProps<RowType extends GridBaseRow> extends GenericCellEditorParams<RowType> {
|
|
9
|
+
message: (
|
|
10
|
+
selectedRows: RowType[],
|
|
11
|
+
cellEditorParams: ICellEditorParams,
|
|
12
|
+
) => Promise<string | JSX.Element> | string | JSX.Element;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GridFormMessage = <RowType extends GridBaseRow>(props: GridFormProps<RowType>) => {
|
|
16
|
+
const formProps = props.formProps as GridFormMessageProps<RowType>;
|
|
17
|
+
|
|
18
|
+
const [message, setMessage] = useState<string | JSX.Element | null>(null);
|
|
19
|
+
const { popoutWrapper } = useGridPopoutHook(props);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
(async () => {
|
|
23
|
+
setMessage(await formProps.message(props.selectedRows, props.cellEditorParams));
|
|
24
|
+
})().then();
|
|
25
|
+
}, [formProps, props.selectedRows, props]);
|
|
26
|
+
|
|
27
|
+
return popoutWrapper(
|
|
28
|
+
<ComponentLoadingWrapper loading={message === null}>
|
|
29
|
+
<div style={{ maxWidth: 400 }} className={"Grid-popoverContainer"}>
|
|
30
|
+
{message}
|
|
31
|
+
</div>
|
|
32
|
+
</ComponentLoadingWrapper>,
|
|
33
|
+
);
|
|
34
|
+
};
|