Skip to content

Commit

Permalink
feat: Simplify forms (#29)
Browse files Browse the repository at this point in the history
* Simplify forms

* Fix null string exception

* Refactoring formprops generics

* Fix dropdown multiedit

* Fix value not populated

* Tidy

* Lint
  • Loading branch information
matttdawson committed Oct 26, 2022
1 parent c056bee commit 5ab4992
Show file tree
Hide file tree
Showing 23 changed files with 214 additions and 201 deletions.
2 changes: 1 addition & 1 deletion web/src/components/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { isNotEmpty } from "../utils/util";
import { GridHeaderSelect } from "./gridHeader/GridHeaderSelect";
import { UpdatingContext } from "../contexts/UpdatingContext";

export interface BaseGridRow {
export interface GridBaseRow {
id: string | number;
}

Expand Down
94 changes: 51 additions & 43 deletions web/src/components/GridCell.tsx
Original file line number Diff line number Diff line change
@@ -1,93 +1,101 @@
import { MutableRefObject, useCallback, useContext, useState } from "react";
import { BaseGridRow } from "./Grid";
import { useCallback, useContext, useMemo, useState } from "react";
import { GridBaseRow } from "./Grid";
import { GridContext } from "../contexts/GridContext";
import { GenericMultiEditCellClass } from "./GenericCellClass";
import { GenericCellRendererParams, GridGenericCellRendererComponent } from "./gridRender/GridRenderGenericCell";
import { ColDef, ICellEditorParams } from "ag-grid-community";

type SaveFn = (selectedRows: any[]) => Promise<boolean>;

export interface GridFormProps {
export interface GridFormProps<RowType extends GridBaseRow> {
cellEditorParams: ICellEditorParams;
updateValue: (saveFn: (selectedRows: any[]) => Promise<boolean>) => Promise<boolean>;
updateValue: (saveFn: (selectedRows: RowType[]) => Promise<boolean>) => Promise<boolean>;
saving: boolean;
value: any;
field: string | undefined;
selectedRows: RowType[];
formProps: Record<string, any>;
}

export interface GenericCellEditorParams {
export interface GenericCellEditorParams<RowType extends GridBaseRow> {
multiEdit?: boolean;
form?: (props: GridFormProps) => JSX.Element;
form?: (props: GridFormProps<RowType>) => JSX.Element;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface GenericCellEditorColDef<RowType, FormProps extends Record<string, any>> extends ColDef {
cellEditorParams?: GenericCellEditorParams & FormProps;
export interface GenericCellEditorColDef<
RowType extends GridBaseRow,
FormProps extends GenericCellEditorParams<RowType>,
> extends ColDef {
cellEditorParams?: FormProps;
cellRendererParams?: GenericCellRendererParams;
}

/**
* For editing a text area.
*/
export const GridCell = <RowType extends BaseGridRow, FormProps extends Record<string, any>>(
export const GridCell = <RowType extends GridBaseRow, FormProps extends GenericCellEditorParams<RowType>>(
props: GenericCellEditorColDef<RowType, FormProps>,
): ColDef => {
return props.cellEditorParams
? {
cellRenderer: props.cellRenderer ?? GridGenericCellRendererComponent,
...props,
editable: props.editable ?? true,
sortable: !!(props?.field || props?.valueGetter),
resizable: true,
cellEditor: GenericCellEditorComponent,
cellClass: props?.cellEditorParams?.multiEdit ? GenericMultiEditCellClass : undefined,
}
: {
cellRenderer: props.cellRenderer ?? GridGenericCellRendererComponent,
sortable: !!(props?.field || props?.valueGetter),
resizable: true,
...props,
};
return {
cellRenderer: props.cellRenderer ?? GridGenericCellRendererComponent,
sortable: !!(props?.field || props?.valueGetter),
resizable: true,
...(props.cellEditorParams && {
cellClass: props?.cellEditorParams?.multiEdit ? GenericMultiEditCellClass : undefined,
editable: true,
cellEditor: GenericCellEditorComponent,
}),
...props,
};
};

interface GenericCellEditorICellEditorParams<RowType extends BaseGridRow, FormProps extends Record<string, any>>
interface GenericCellEditorICellEditorParams<RowType extends GridBaseRow, FormProps extends Record<string, any>>
extends ICellEditorParams {
data: RowType;
colDef: GenericCellEditorColDef<RowType, FormProps>;
}

export interface GridGenericCellEditorFormContextParams {
cellEditorParamsRef: MutableRefObject<ICellEditorParams>;
saveRef: MutableRefObject<SaveFn>;
triggerSave: () => Promise<void>;
}

export const GenericCellEditorComponent = <RowType extends BaseGridRow, FormProps extends Record<string, any>>(
export const GenericCellEditorComponent = <RowType extends GridBaseRow, FormProps extends Record<string, any>>(
props: GenericCellEditorICellEditorParams<RowType, FormProps>,
) => {
const { updatingCells } = useContext(GridContext);
const { updatingCells, getSelectedRows } = useContext(GridContext);

const { colDef, data } = props;
const { cellEditorParams } = props.colDef;
const multiEdit = cellEditorParams?.multiEdit ?? false;
const field = props.colDef.field ?? "";

const formProps = colDef.cellEditorParams ?? {};
const value = props.value;

const selectedRows = useMemo(
() => (multiEdit ? getSelectedRows<RowType>() : [data]),
[data, getSelectedRows, multiEdit],
);

const [saving, setSaving] = useState(false);

const updateValue = useCallback(
async (saveFn: (selectedRows: any[]) => Promise<boolean>): Promise<boolean> => {
return !saving && (await updatingCells({ data, multiEdit, field }, saveFn, setSaving));
return !saving && (await updatingCells({ selectedRows, field }, saveFn, setSaving));
},
[data, field, multiEdit, saving, updatingCells],
[field, saving, selectedRows, updatingCells],
);

if (cellEditorParams == null) return <></>;

// The key=${saving} ensures the cell re-renders when the updatingContext redraws.
return (
<div>
<>
<div>{colDef.cellRenderer ? <colDef.cellRenderer {...props} saving={saving} /> : props.value}</div>
{cellEditorParams?.form && (
<cellEditorParams.form cellEditorParams={props} updateValue={updateValue} saving={saving} />
<cellEditorParams.form
cellEditorParams={props}
updateValue={updateValue}
saving={saving}
formProps={formProps}
value={value}
field={field}
selectedRows={selectedRows}
/>
)}
</div>
</>
);
};
6 changes: 5 additions & 1 deletion web/src/components/GridPopoutHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { GridContext } from "../contexts/GridContext";
import { ControlledMenu } from "@szhsin/react-menu";
import { GridFormProps } from "./GridCell";
import { hasParentClass } from "../utils/util";
import { GridBaseRow } from "./Grid";

export const useGridPopoutHook = (props: GridFormProps, save?: (selectedRows: any[]) => Promise<boolean>) => {
export const useGridPopoutHook = <RowType extends GridBaseRow>(
props: GridFormProps<RowType>,
save?: (selectedRows: any[]) => Promise<boolean>,
) => {
const { cellEditorParams, saving, updateValue } = props;
const { eGridCell } = cellEditorParams as ICellEditorParams;
const { stopEditing } = useContext(GridContext);
Expand Down
26 changes: 11 additions & 15 deletions web/src/components/gridForm/GridFormDropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import "@szhsin/react-menu/dist/index.css";

import { MenuItem, MenuDivider, FocusableItem } from "@szhsin/react-menu";
import { useCallback, useContext, useEffect, useRef, useState, KeyboardEvent } from "react";
import { BaseGridRow } from "../Grid";
import { GridBaseRow } from "../Grid";
import { ComponentLoadingWrapper } from "../ComponentLoadingWrapper";
import { GridContext } from "../../contexts/GridContext";
import { delay } from "lodash-es";
import debounce from "debounce-promise";
import { GridFormProps } from "../GridCell";
import { GenericCellEditorParams, GridFormProps } from "../GridCell";
import { useGridPopoutHook } from "../GridPopoutHook";

export interface GridPopoutEditDropDownSelectedItem<RowType, ValueType> {
Expand All @@ -25,7 +25,8 @@ export const MenuSeparator = Object.freeze({ value: MenuSeparatorString });

export type SelectOption<ValueType> = ValueType | FinalSelectOption<ValueType>;

export interface GridFormPopoutDropDownProps<RowType, ValueType> {
export interface GridFormPopoutDropDownProps<RowType extends GridBaseRow, ValueType>
extends GenericCellEditorParams<RowType> {
filtered?: "local" | "reload";
filterPlaceholder?: string;
onSelectedItem?: (props: GridPopoutEditDropDownSelectedItem<RowType, ValueType>) => Promise<void>;
Expand All @@ -35,15 +36,9 @@ export interface GridFormPopoutDropDownProps<RowType, ValueType> {
optionsRequestCancel?: () => void;
}

export const GridFormDropDown = <RowType extends BaseGridRow, ValueType>(props: GridFormProps) => {
const { getSelectedRows } = useContext(GridContext);
export const GridFormDropDown = <RowType extends GridBaseRow, ValueType>(props: GridFormProps<RowType>) => {
const { popoutWrapper } = useGridPopoutHook(props);

const { cellEditorParams } = props;
const { data, colDef } = cellEditorParams;
const formProps: GridFormPopoutDropDownProps<RowType, ValueType> = colDef.cellEditorParams;
const field = colDef.field ?? colDef.colId ?? "";
const { multiEdit } = colDef.cellEditorParams;
const formProps = props.formProps as GridFormPopoutDropDownProps<RowType, ValueType>;

const { updatingCells, stopEditing } = useContext(GridContext);

Expand All @@ -54,7 +49,8 @@ export const GridFormDropDown = <RowType extends BaseGridRow, ValueType>(props:

const selectItemHandler = useCallback(
async (value: ValueType): Promise<boolean> => {
return await updatingCells({ data, field, multiEdit }, async (selectedRows) => {
const field = props.field;
return await updatingCells({ selectedRows: props.selectedRows, field }, async (selectedRows) => {
const hasChanged = selectedRows.some((row) => row[field as keyof RowType] !== value);
if (hasChanged) {
if (formProps.onSelectedItem) {
Expand All @@ -66,7 +62,7 @@ export const GridFormDropDown = <RowType extends BaseGridRow, ValueType>(props:
return true;
});
},
[data, field, formProps, multiEdit, updatingCells],
[formProps, props.field, props.selectedRows, updatingCells],
);

// Load up options list if it's async function
Expand All @@ -77,7 +73,7 @@ export const GridFormDropDown = <RowType extends BaseGridRow, ValueType>(props:

(async () => {
if (typeof optionsConf == "function") {
optionsConf = await optionsConf(getSelectedRows(), filter);
optionsConf = await optionsConf(props.selectedRows, filter);
}

const optionsList = optionsConf?.map((item) => {
Expand All @@ -96,7 +92,7 @@ export const GridFormDropDown = <RowType extends BaseGridRow, ValueType>(props:
}
optionsInitialising.current = false;
})();
}, [filter, getSelectedRows, options, formProps.filtered, formProps.options]);
}, [filter, options, formProps.filtered, formProps.options, props.selectedRows]);

// Local filtering
useEffect(() => {
Expand Down
20 changes: 9 additions & 11 deletions web/src/components/gridForm/GridFormEditBearing.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import "./GridFormEditBearing.scss";

import { useCallback, useState } from "react";
import { BaseGridRow } from "../Grid";
import { GridBaseRow } from "../Grid";
import { TextInputFormatted } from "../../lui/TextInputFormatted";
import { bearingNumberParser, bearingStringValidator, convertDDToDMS } from "../../utils/bearing";
import { GridFormProps } from "../GridCell";
import { GenericCellEditorParams, GridFormProps } from "../GridCell";
import { useGridPopoutHook } from "../GridPopoutHook";

export interface GridFormEditBearingProps<RowType extends BaseGridRow> {
export interface GridFormEditBearingProps<RowType extends GridBaseRow> extends GenericCellEditorParams<RowType> {
placeHolder: string;
onSave?: (selectedRows: RowType[], value: number | null) => Promise<boolean>;
}

export const GridFormEditBearing = <RowType extends BaseGridRow>(props: GridFormProps) => {
const { colDef } = props.cellEditorParams;
const formProps: GridFormEditBearingProps<RowType> = colDef.cellEditorParams;
const field = colDef.field;
const originalValue = props.cellEditorParams?.value;
const [value, setValue] = useState<string>(`${originalValue ?? ""}`);
export const GridFormEditBearing = <RowType extends GridBaseRow>(props: GridFormProps<RowType>) => {
const formProps = props.formProps as GridFormEditBearingProps<RowType>;
const [value, setValue] = useState<string>(`${props.value ?? ""}`);

const save = useCallback(
async (selectedRows: RowType[]): Promise<boolean> => {
if (bearingStringValidator(value)) return false;
const parsedValue = bearingNumberParser(value);
// Value didn't change so don't save just cancel
if (parsedValue === originalValue) {
if (parsedValue === props.value) {
return true;
}
if (formProps.onSave) {
return await formProps.onSave(selectedRows, parsedValue);
} else {
const field = props.field;
if (field == null) {
console.error("field is not defined in ColDef");
} else {
Expand All @@ -38,7 +36,7 @@ export const GridFormEditBearing = <RowType extends BaseGridRow>(props: GridForm
}
return true;
},
[field, formProps, originalValue, value],
[formProps, props.field, props.value, value],
);
const { popoutWrapper, triggerSave } = useGridPopoutHook(props, save);

Expand Down
18 changes: 8 additions & 10 deletions web/src/components/gridForm/GridFormMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
import { useContext, useEffect, useState } from "react";
import { GridFormProps } from "../GridCell";
import { useEffect, useState } from "react";
import { GenericCellEditorParams, GridFormProps } from "../GridCell";
import { ICellEditorParams } from "ag-grid-community";
import { ComponentLoadingWrapper } from "../ComponentLoadingWrapper";
import { BaseGridRow } from "../Grid";
import { GridContext } from "../../contexts/GridContext";
import { GridBaseRow } from "../Grid";
import { useGridPopoutHook } from "../GridPopoutHook";

export interface GridFormMessageProps<RowType extends BaseGridRow> {
export interface GridFormMessageProps<RowType extends GridBaseRow> extends GenericCellEditorParams<RowType> {
message: (
selectedRows: RowType[],
cellEditorParams: ICellEditorParams,
) => Promise<string | JSX.Element> | string | JSX.Element;
}

export const GridFormMessage = <RowType extends BaseGridRow>(props: GridFormProps) => {
const formProps: GridFormMessageProps<RowType> = props.cellEditorParams.colDef.cellEditorParams;
const { getSelectedRows } = useContext(GridContext);
export const GridFormMessage = <RowType extends GridBaseRow>(props: GridFormProps<RowType>) => {
const formProps = props.formProps as GridFormMessageProps<RowType>;

const [message, setMessage] = useState<string | JSX.Element | null>(null);
const { popoutWrapper } = useGridPopoutHook(props);

useEffect(() => {
(async () => {
setMessage(await formProps.message(getSelectedRows(), props.cellEditorParams));
setMessage(await formProps.message(props.selectedRows, props.cellEditorParams));
})().then();
}, [formProps, getSelectedRows, props]);
}, [formProps, props.selectedRows, props]);

return popoutWrapper(
<ComponentLoadingWrapper loading={message === null}>
Expand Down
Loading

0 comments on commit 5ab4992

Please sign in to comment.