Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Simplify forms #29

Merged
merged 7 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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