@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.
Files changed (96) hide show
  1. package/README.md +4 -0
  2. package/dist/index.d.ts +32 -0
  3. package/dist/index.js +89342 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/components/ComponentLoadingWrapper.d.ts +9 -0
  6. package/dist/src/components/GenericCellClass.d.ts +2 -0
  7. package/dist/src/components/Grid.d.ts +24 -0
  8. package/dist/src/components/GridCell.d.ts +31 -0
  9. package/dist/src/components/GridIcon.d.ts +6 -0
  10. package/dist/src/components/GridLoadableCell.d.ts +6 -0
  11. package/dist/src/components/GridPopoutHook.d.ts +7 -0
  12. package/dist/src/components/GridSubComponentTextArea.d.ts +5 -0
  13. package/dist/src/components/PostSortRowsHook.d.ts +11 -0
  14. package/dist/src/components/gridForm/GridFormDropDown.d.ts +26 -0
  15. package/dist/src/components/gridForm/GridFormEditBearing.d.ts +9 -0
  16. package/dist/src/components/gridForm/GridFormMessage.d.ts +8 -0
  17. package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +26 -0
  18. package/dist/src/components/gridForm/GridFormPopoutMenu.d.ts +24 -0
  19. package/dist/src/components/gridForm/GridFormTextArea.d.ts +11 -0
  20. package/dist/src/components/gridForm/GridFormTextInput.d.ts +11 -0
  21. package/dist/src/components/gridHeader/GridHeaderSelect.d.ts +8 -0
  22. package/dist/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.d.ts +4 -0
  23. package/dist/src/components/gridPopoverEdit/GridPopoutMenu.d.ts +10 -0
  24. package/dist/src/components/gridPopoverEdit/GridPopoverEditBearing.d.ts +4 -0
  25. package/dist/src/components/gridPopoverEdit/GridPopoverEditDropDown.d.ts +4 -0
  26. package/dist/src/components/gridPopoverEdit/GridPopoverMessage.d.ts +4 -0
  27. package/dist/src/components/gridRender/GridRenderGenericCell.d.ts +15 -0
  28. package/dist/src/components/gridRender/GridRenderPopoutMenuCell.d.ts +3 -0
  29. package/dist/src/contexts/GridContext.d.ts +25 -0
  30. package/dist/src/contexts/GridContextProvider.d.ts +11 -0
  31. package/dist/src/contexts/UpdatingContext.d.ts +6 -0
  32. package/dist/src/contexts/UpdatingContextProvider.d.ts +7 -0
  33. package/dist/src/lui/TextAreaInput.d.ts +11 -0
  34. package/dist/src/lui/TextInputFormatted.d.ts +14 -0
  35. package/dist/src/setupTests.d.ts +1 -0
  36. package/dist/src/stories/components/FormTest.d.ts +12 -0
  37. package/dist/src/stories/components/GridPopoutBearing.stories.d.ts +9 -0
  38. package/dist/src/stories/components/GridPopoutEditDropDown.stories.d.ts +9 -0
  39. package/dist/src/stories/components/GridPopoutEditGeneric.stories.d.ts +9 -0
  40. package/dist/src/stories/components/GridPopoutEditGenericTextArea.stories.d.ts +9 -0
  41. package/dist/src/stories/components/GridPopoutEditMultiSelect.stories.d.ts +9 -0
  42. package/dist/src/stories/components/GridReadOnly.stories.d.ts +9 -0
  43. package/dist/src/utils/bearing.d.ts +5 -0
  44. package/dist/src/utils/util.d.ts +4 -0
  45. package/dist/step-ag-grid.esm.js +89297 -0
  46. package/dist/step-ag-grid.esm.js.map +1 -0
  47. package/package.json +126 -0
  48. package/src/components/ComponentLoadingWrapper.tsx +31 -0
  49. package/src/components/GenericCellClass.tsx +15 -0
  50. package/src/components/Grid.scss +38 -0
  51. package/src/components/Grid.tsx +262 -0
  52. package/src/components/GridCell.tsx +101 -0
  53. package/src/components/GridIcon.tsx +12 -0
  54. package/src/components/GridLoadableCell.tsx +22 -0
  55. package/src/components/GridPopoutHook.tsx +118 -0
  56. package/src/components/GridSubComponentTextArea.tsx +26 -0
  57. package/src/components/GridTheme.scss +94 -0
  58. package/src/components/PostSortRowsHook.ts +131 -0
  59. package/src/components/gridForm/GridFormDropDown.tsx +192 -0
  60. package/src/components/gridForm/GridFormEditBearing.scss +3 -0
  61. package/src/components/gridForm/GridFormEditBearing.tsx +62 -0
  62. package/src/components/gridForm/GridFormMessage.tsx +34 -0
  63. package/src/components/gridForm/GridFormMultiSelect.tsx +180 -0
  64. package/src/components/gridForm/GridFormPopoutMenu.tsx +90 -0
  65. package/src/components/gridForm/GridFormTextArea.tsx +61 -0
  66. package/src/components/gridForm/GridFormTextInput.tsx +66 -0
  67. package/src/components/gridHeader/GridHeaderSelect.tsx +54 -0
  68. package/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.ts +22 -0
  69. package/src/components/gridPopoverEdit/GridPopoutMenu.scss +9 -0
  70. package/src/components/gridPopoverEdit/GridPopoutMenu.tsx +35 -0
  71. package/src/components/gridPopoverEdit/GridPopoverEditBearing.ts +24 -0
  72. package/src/components/gridPopoverEdit/GridPopoverEditDropDown.ts +22 -0
  73. package/src/components/gridPopoverEdit/GridPopoverMessage.ts +22 -0
  74. package/src/components/gridRender/GridRenderGenericCell.scss +15 -0
  75. package/src/components/gridRender/GridRenderGenericCell.tsx +49 -0
  76. package/src/components/gridRender/GridRenderPopoutMenuCell.tsx +22 -0
  77. package/src/contexts/GridContext.tsx +82 -0
  78. package/src/contexts/GridContextProvider.tsx +292 -0
  79. package/src/contexts/UpdatingContext.tsx +16 -0
  80. package/src/contexts/UpdatingContextProvider.tsx +43 -0
  81. package/src/lui/TextAreaInput.tsx +50 -0
  82. package/src/lui/TextInputFormatted.scss +11 -0
  83. package/src/lui/TextInputFormatted.tsx +60 -0
  84. package/src/lui-overrides.scss +163 -0
  85. package/src/react-app-env.d.ts +1 -0
  86. package/src/setupTests.ts +5 -0
  87. package/src/stories/components/FormTest.scss +10 -0
  88. package/src/stories/components/FormTest.tsx +51 -0
  89. package/src/stories/components/GridPopoutBearing.stories.tsx +100 -0
  90. package/src/stories/components/GridPopoutEditDropDown.stories.tsx +174 -0
  91. package/src/stories/components/GridPopoutEditGeneric.stories.tsx +76 -0
  92. package/src/stories/components/GridPopoutEditGenericTextArea.stories.tsx +106 -0
  93. package/src/stories/components/GridPopoutEditMultiSelect.stories.tsx +111 -0
  94. package/src/stories/components/GridReadOnly.stories.tsx +151 -0
  95. package/src/utils/bearing.ts +72 -0
  96. 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,3 @@
1
+ .GridFormEditBearing-input {
2
+ width: 320px;
3
+ }
@@ -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
+ };