From 64e926dd556b0dcb620432116ba681360e572073 Mon Sep 17 00:00:00 2001 From: matttdawson <89495499+matttdawson@users.noreply.github.com> Date: Thu, 27 Oct 2022 19:18:13 +1300 Subject: [PATCH] feat: bearing ranges, disableds in dropdowns and menus * feat: bearing ranges plus tests * feat: added disabled option and disabled title to dropdown * Added disabled option to drop downs --- package.json | 2 +- src/components/gridForm/GridFormDropDown.tsx | 11 +++++-- .../gridForm/GridFormEditBearing.tsx | 3 +- .../gridForm/GridFormPopoutMenu.tsx | 8 +++-- src/components/gridForm/GridFormTextArea.tsx | 2 +- src/components/gridForm/GridFormTextInput.tsx | 2 +- .../gridPopoverEdit/GridPopoverEditBearing.ts | 30 +++++++++++++++++-- .../components/GridPopoutBearing.stories.tsx | 23 +++++++------- .../GridPopoutEditDropDown.stories.tsx | 7 +++-- .../components/GridReadOnly.stories.tsx | 5 ++++ src/utils/bearing.test.ts | 30 +++++++++++++++++++ src/utils/bearing.ts | 27 ++++++++++++----- 12 files changed, 118 insertions(+), 32 deletions(-) create mode 100644 src/utils/bearing.test.ts diff --git a/package.json b/package.json index 5dd2a126..b2035fe9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test": "react-scripts test --passWithNoTests", "eject": "react-scripts eject", "lint": "eslint ./src --ext .js,.ts,.tsx --fix --cache", - "storybook": "start-storybook --no-manager-cache -p 6006 -s public", + "storybook": "start-storybook -p 6006 -s public", "build-storybook": "build-storybook -s public", "deploy-storybook": "npx --yes -p @storybook/storybook-deployer storybook-to-ghpages", "chromatic": "chromatic --exit-zero-on-changes", diff --git a/src/components/gridForm/GridFormDropDown.tsx b/src/components/gridForm/GridFormDropDown.tsx index fee80c58..25245b66 100644 --- a/src/components/gridForm/GridFormDropDown.tsx +++ b/src/components/gridForm/GridFormDropDown.tsx @@ -18,6 +18,7 @@ export interface GridPopoutEditDropDownSelectedItem { interface FinalSelectOption { value: ValueType; label?: JSX.Element | string; + disabled?: boolean | string; } export const MenuSeparatorString = "_____MENU_SEPARATOR_____"; @@ -78,7 +79,7 @@ export const GridFormDropDown = (props: const optionsList = optionsConf?.map((item) => { if (item == null || typeof item == "string" || typeof item == "number") { - item = { value: item as ValueType, label: item } as FinalSelectOption; + item = { value: item as ValueType, label: item, disabled: false } as FinalSelectOption; } return item; }) as any as FinalSelectOption[]; @@ -180,7 +181,13 @@ export const GridFormDropDown = (props: item.value === MenuSeparatorString ? ( ) : filteredValues.includes(item.value) ? null : ( - selectItemHandler(item.value)}> + selectItemHandler(item.value)} + > {item.label ?? (item.value == null ? `<${item.value}>` : `${item.value}`)} ), diff --git a/src/components/gridForm/GridFormEditBearing.tsx b/src/components/gridForm/GridFormEditBearing.tsx index b3fce0e1..f938c23f 100644 --- a/src/components/gridForm/GridFormEditBearing.tsx +++ b/src/components/gridForm/GridFormEditBearing.tsx @@ -9,6 +9,7 @@ import { useGridPopoverHook } from "../GridPopoverHook"; export interface GridFormEditBearingProps extends GenericCellEditorParams { placeHolder: string; + range?: (value: number | null) => string | null; onSave?: (selectedRows: RowType[], value: number | null) => Promise; } @@ -55,7 +56,7 @@ export const GridFormEditBearing = (props: GridForm onKeyDown: async (e) => e.key === "Enter" && triggerSave().then(), }} formatted={bearingStringValidator(value) ? "?" : convertDDToDMS(bearingNumberParser(value))} - error={bearingStringValidator(value)} + error={bearingStringValidator(value, formProps.range)} /> , ); diff --git a/src/components/gridForm/GridFormPopoutMenu.tsx b/src/components/gridForm/GridFormPopoutMenu.tsx index d4016234..2d379931 100644 --- a/src/components/gridForm/GridFormPopoutMenu.tsx +++ b/src/components/gridForm/GridFormPopoutMenu.tsx @@ -19,7 +19,8 @@ interface MenuSeparatorType { export interface MenuOption { label: JSX.Element | string | MenuSeparatorType; - action: (selectedRows: RowType[]) => Promise; + action?: (selectedRows: RowType[]) => Promise; + disabled?: string | boolean; multiEdit: boolean; } @@ -54,7 +55,7 @@ export const GridFormPopoutMenu = (props: GridFormP const actionClick = useCallback( async (menuOption: MenuOption) => { return await updatingCells({ selectedRows: props.selectedRows, field: props.field }, async (selectedRows) => { - await menuOption.action(selectedRows); + menuOption.action && (await menuOption.action(selectedRows)); return true; }); }, @@ -78,7 +79,8 @@ export const GridFormPopoutMenu = (props: GridFormP actionClick(item)} - disabled={!filteredOptions?.includes(item)} + disabled={!!item.disabled || !filteredOptions?.includes(item)} + title={item.disabled && typeof item.disabled !== "boolean" ? item.disabled : ""} > {item.label as JSX.Element | string} diff --git a/src/components/gridForm/GridFormTextArea.tsx b/src/components/gridForm/GridFormTextArea.tsx index 253a55cb..27228101 100644 --- a/src/components/gridForm/GridFormTextArea.tsx +++ b/src/components/gridForm/GridFormTextArea.tsx @@ -28,7 +28,7 @@ export const GridFormTextArea = (props: GridFormPro return formProps.validate(value); } return null; - }, [formProps.maxlength, formProps.required, value.length]); + }, [formProps, value]); const save = useCallback( async (selectedRows: any[]): Promise => { diff --git a/src/components/gridForm/GridFormTextInput.tsx b/src/components/gridForm/GridFormTextInput.tsx index 217a9334..38797ef4 100644 --- a/src/components/gridForm/GridFormTextInput.tsx +++ b/src/components/gridForm/GridFormTextInput.tsx @@ -28,7 +28,7 @@ export const GridFormTextInput = (props: GridFormPr return formProps.validate(value); } return null; - }, [formProps.maxlength, formProps.required, value.length]); + }, [formProps, value]); const save = useCallback( async (selectedRows: any[]): Promise => { diff --git a/src/components/gridPopoverEdit/GridPopoverEditBearing.ts b/src/components/gridPopoverEdit/GridPopoverEditBearing.ts index bfbd3d15..efb2e7a0 100644 --- a/src/components/gridPopoverEdit/GridPopoverEditBearing.ts +++ b/src/components/gridPopoverEdit/GridPopoverEditBearing.ts @@ -1,11 +1,11 @@ import { GenericMultiEditCellClass } from "../GenericCellClass"; import { GenericCellColDef } from "../gridRender/GridRenderGenericCell"; -import { bearingValueFormatter } from "@utils/bearing"; +import { bearingCorrectionValueFormatter, bearingValueFormatter } from "@utils/bearing"; import { GridCell } from "../GridCell"; import { GridFormEditBearing, GridFormEditBearingProps } from "../gridForm/GridFormEditBearing"; import { GridBaseRow } from "../Grid"; -export const GridPopoverEditBearing = ( +export const GridPopoverEditBearingLike = ( colDef: GenericCellColDef>, ) => GridCell>({ @@ -21,3 +21,29 @@ export const GridPopoverEditBearing = ( }, }), }); + +export const GridPopoverEditBearing = ( + colDef: GenericCellColDef>, +) => ({ + ...GridPopoverEditBearingLike(colDef), + valueFormatter: bearingValueFormatter, + range: (value: number | null) => { + if (value === null) return "Bearing is required"; + if (value >= 360) return "Bearing must be less than 360 degrees"; + if (value < 0) return "Bearing must not be negative"; + return null; + }, +}); + +export const GridPopoverEditBearingCorrection = ( + colDef: GenericCellColDef>, +) => ({ + ...GridPopoverEditBearingLike(colDef), + valueFormatter: bearingCorrectionValueFormatter, + range: (value: number | null) => { + if (value === null) return "Bearing is required"; + if (value >= 360) return "Bearing must be less than 360 degrees"; + if (value <= -180) return "Bearing must be greater then -180 degrees"; + return null; + }, +}); diff --git a/src/stories/components/GridPopoutBearing.stories.tsx b/src/stories/components/GridPopoutBearing.stories.tsx index a28d7eca..81ee07fb 100644 --- a/src/stories/components/GridPopoutBearing.stories.tsx +++ b/src/stories/components/GridPopoutBearing.stories.tsx @@ -8,7 +8,10 @@ import { UpdatingContextProvider } from "@contexts/UpdatingContextProvider"; import { GridContextProvider } from "@contexts/GridContextProvider"; import { Grid, GridProps } from "@components/Grid"; import { GridCell } from "@components/GridCell"; -import { GridPopoverEditBearing } from "@components/gridPopoverEdit/GridPopoverEditBearing"; +import { + GridPopoverEditBearing, + GridPopoverEditBearingCorrection, +} from "@components/gridPopoverEdit/GridPopoverEditBearing"; import { wait } from "@utils/util"; export default { @@ -34,7 +37,7 @@ export default { interface ITestRow { id: number; bearing1: number | null; - bearing2: number | null; + bearingCorrection: number | null; } const GridReadOnlyTemplate: ComponentStory = (props: GridProps) => { @@ -59,15 +62,15 @@ const GridReadOnlyTemplate: ComponentStory = (props: GridProps) => placeHolder: "Enter Bearing", }, }), - GridPopoverEditBearing({ - field: "bearing2", - headerName: "Bearing onSave", + GridPopoverEditBearingCorrection({ + field: "bearingCorrection", + headerName: "Bearing Correction", cellEditorParams: { multiEdit: true, placeHolder: "Enter Bearing", - onSave: async (selectedRows: ITestRow[], value: ITestRow["bearing2"]) => { + onSave: async (selectedRows: ITestRow[], value: ITestRow["bearingCorrection"]) => { await wait(1000); - selectedRows.forEach((row) => (row["bearing2"] = value)); + selectedRows.forEach((row) => (row["bearingCorrection"] = value)); return true; }, }, @@ -79,9 +82,9 @@ const GridReadOnlyTemplate: ComponentStory = (props: GridProps) => const rowData = useMemo( () => [ - { id: 1000, bearing1: 1.234, bearing2: 90 }, - { id: 1001, bearing1: 1.565, bearing2: 240 }, - { id: 1002, bearing1: null, bearing2: 355.1 }, + { id: 1000, bearing1: 1.234, bearingCorrection: 90 }, + { id: 1001, bearing1: 1.565, bearingCorrection: 240 }, + { id: 1002, bearing1: null, bearingCorrection: 355.1 }, ] as ITestRow[], [], ); diff --git a/src/stories/components/GridPopoutEditDropDown.stories.tsx b/src/stories/components/GridPopoutEditDropDown.stories.tsx index 2dadef38..11b1ffdc 100644 --- a/src/stories/components/GridPopoutEditDropDown.stories.tsx +++ b/src/stories/components/GridPopoutEditDropDown.stories.tsx @@ -94,11 +94,12 @@ const GridEditDropDownTemplate: ComponentStory = (props: GridProps) options: [ { value: "1", - label: One, + label: "One", + disabled: "Disabled for test", }, - { value: "2", label: Two }, + { value: "2", label: "Two" }, MenuSeparator, - { value: "3", label: Three }, + { value: "3", label: "Three" }, ], }, }), diff --git a/src/stories/components/GridReadOnly.stories.tsx b/src/stories/components/GridReadOnly.stories.tsx index ae92c787..384d2418 100644 --- a/src/stories/components/GridReadOnly.stories.tsx +++ b/src/stories/components/GridReadOnly.stories.tsx @@ -109,6 +109,11 @@ const GridReadOnlyTemplate: ComponentStory = (props: GridProps) => }, multiEdit: true, }, + { + label: "Disabled item", + disabled: "Disabled for test", + multiEdit: true, + }, ]; }, }, diff --git a/src/utils/bearing.test.ts b/src/utils/bearing.test.ts new file mode 100644 index 00000000..d101b2c1 --- /dev/null +++ b/src/utils/bearing.test.ts @@ -0,0 +1,30 @@ +import { convertDDToDMS } from "./bearing"; + +describe("convertDDToDMS", () => { + test("converts decimal-ish degrees to DMS", () => { + expect(convertDDToDMS(-0.001, false, false)).toBe("-0° 00' 10\""); + expect(convertDDToDMS(-10.001, false, false)).toBe("-10° 00' 10\""); + expect(convertDDToDMS(-370.001, false, false)).toBe("-10° 00' 10\""); + expect(convertDDToDMS(359.595999, false, false)).toBe("0° 00'"); + expect(convertDDToDMS(369.696999, false, false)).toBe("10° 10' 10\""); + expect(convertDDToDMS(221.555999, false, false)).toBe("221° 56'"); + expect(convertDDToDMS(221.555999, false, true)).toBe("221° 56' 00.0\""); + expect(convertDDToDMS(5)).toBe("+5° 00' 00.0\""); + expect(convertDDToDMS(5.0)).toBe("+5° 00' 00.0\""); + expect(convertDDToDMS(5.00001)).toBe("+5° 00' 00.1\""); + expect(convertDDToDMS(5.1)).toBe("+5° 10' 00.0\""); + expect(convertDDToDMS(5.12345)).toBe("+5° 12' 34.5\""); + expect(convertDDToDMS(5.12345, false)).toBe("5° 12' 34.5\""); + + expect(convertDDToDMS(300)).toBe("+300° 00' 00.0\""); + expect(convertDDToDMS(300.0)).toBe("+300° 00' 00.0\""); + expect(convertDDToDMS(300.00001)).toBe("+300° 00' 00.1\""); + expect(convertDDToDMS(300.1)).toBe("+300° 10' 00.0\""); + expect(convertDDToDMS(300.12345)).toBe("+300° 12' 34.5\""); + expect(convertDDToDMS(300.12345, false)).toBe("300° 12' 34.5\""); + + expect(convertDDToDMS(300, false, false)).toBe("300° 00'"); + expect(convertDDToDMS(300.1, false, false)).toBe("300° 10'"); + expect(convertDDToDMS(0, false)).toBe("0° 00'"); + }); +}); diff --git a/src/utils/bearing.ts b/src/utils/bearing.ts index 3ae3559a..59139a64 100644 --- a/src/utils/bearing.ts +++ b/src/utils/bearing.ts @@ -5,7 +5,15 @@ export const bearingValueFormatter = (params: ValueFormatterParams): string => { if (value == null) { return "-"; } - return convertDDToDMS(value); + return convertDDToDMS(value, false, false); +}; + +export const bearingCorrectionValueFormatter = (params: ValueFormatterParams): string => { + const value = params.value; + if (value == null) { + return "-"; + } + return convertDDToDMS(value, true, true); }; export const bearingNumberParser = (value: string): number | null => { @@ -13,27 +21,30 @@ export const bearingNumberParser = (value: string): number | null => { return parseFloat(value); }; -const validMaskForDmsBearing = /^(\d+)?(\.([0-5](\d([0-5](\d(\d+)?)?)?)?)?)?$/; -export const bearingStringValidator = (value: string): string | null => { +const validMaskForDmsBearing = /^((-)?\d+)?(\.([0-5](\d([0-5](\d(\d+)?)?)?)?)?)?$/; +export const bearingStringValidator = ( + value: string, + customValidate?: (value: number | null) => string | null, +): string | null => { value = value.trim(); if (value === "") return null; const match = value.match(validMaskForDmsBearing); if (!match) return "Bearing must be a positive number in D.MMSSS format"; - const decimalPart = match[3]; + const decimalPart = match[4]; if (decimalPart != null && decimalPart.length > 5) { return "Bearing has a maximum of 5 decimal places"; } const bearing = parseFloat(value); - if (bearing >= 360) return "Bearing must be between 0 and 360 inclusive"; - return null; + + return customValidate ? customValidate(bearing) : null; }; // Decimal-ish degrees to Degrees Minutes Seconds converter export const convertDDToDMS = (dd: number | null, showPositiveSymbol = true, addTrailingZeros = true): string => { if (dd == null) return "–"; - if (dd === 0) addTrailingZeros = true; + if (dd === 0) addTrailingZeros = false; // toFixed rounds parts up greater than 60, which has to be corrected below const [bearingWholeString, beringDecimalString] = dd.toFixed(5).split("."); @@ -64,7 +75,7 @@ export const convertDDToDMS = (dd: number | null, showPositiveSymbol = true, add dmsString += `\xa0${minString}'\xa0${secString}.${deciSecString}"`; // "\xa0" is here for non-breaking space } else if (secNumeric != 0) { dmsString += `\xa0${minString}'\xa0${secString}"`; - } else if (minNumeric != 0) { + } else { dmsString += `\xa0${minString}'`; }