From b0419c4b968ddd897f07d758de22e6c22a81ca3e Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sat, 29 Jul 2023 20:51:34 -0700 Subject: [PATCH] Add ability to properly cast a string, number, null to an integer --- .../CompanyEmployeesEditableField.tsx | 39 +++++----- .../components/NumberEditableField.tsx | 30 ++++---- .../__tests__/cast-as-integer-or-null.test.ts | 72 +++++++++++++++++++ front/src/utils/cast-as-integer-or-null.ts | 58 +++++++++++++++ 4 files changed, 161 insertions(+), 38 deletions(-) create mode 100644 front/src/utils/__tests__/cast-as-integer-or-null.test.ts create mode 100644 front/src/utils/cast-as-integer-or-null.ts diff --git a/front/src/modules/companies/editable-field/components/CompanyEmployeesEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyEmployeesEditableField.tsx index 8983adc8c294..bb83f0e74a55 100644 --- a/front/src/modules/companies/editable-field/components/CompanyEmployeesEditableField.tsx +++ b/front/src/modules/companies/editable-field/components/CompanyEmployeesEditableField.tsx @@ -6,6 +6,10 @@ import { IconUsers } from '@/ui/icon'; import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText'; import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; +import { + canBeCastAsIntegerOrNull, + castAsIntegerOrNull, +} from '~/utils/cast-as-integer-or-null'; type OwnProps = { company: Pick; @@ -27,30 +31,25 @@ export function CompanyEmployeesEditableField({ company }: OwnProps) { } async function handleSubmit() { - if (!internalValue) return; - - try { - const numberValue = parseInt(internalValue); + if (!canBeCastAsIntegerOrNull(internalValue)) { + handleCancel(); + return; + } - if (isNaN(numberValue)) { - throw new Error('Not a number'); - } + const valueCastedAsNumberOrNull = castAsIntegerOrNull(internalValue); - await updateCompany({ - variables: { - where: { - id: company.id, - }, - data: { - employees: numberValue, - }, + await updateCompany({ + variables: { + where: { + id: company.id, }, - }); + data: { + employees: valueCastedAsNumberOrNull, + }, + }, + }); - setInternalValue(numberValue.toString()); - } catch { - handleCancel(); - } + setInternalValue(valueCastedAsNumberOrNull?.toString()); } async function handleCancel() { diff --git a/front/src/modules/ui/editable-field/variants/components/NumberEditableField.tsx b/front/src/modules/ui/editable-field/variants/components/NumberEditableField.tsx index 70dfed65bb50..f2944230c62b 100644 --- a/front/src/modules/ui/editable-field/variants/components/NumberEditableField.tsx +++ b/front/src/modules/ui/editable-field/variants/components/NumberEditableField.tsx @@ -4,12 +4,16 @@ import { EditableField } from '@/ui/editable-field/components/EditableField'; import { FieldContext } from '@/ui/editable-field/states/FieldContext'; import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText'; import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; +import { + canBeCastAsIntegerOrNull, + castAsIntegerOrNull, +} from '~/utils/cast-as-integer-or-null'; type OwnProps = { icon?: React.ReactNode; placeholder?: string; value: number | null | undefined; - onSubmit?: (newValue: number) => void; + onSubmit?: (newValue: number | null) => void; }; export function NumberEditableField({ @@ -29,26 +33,16 @@ export function NumberEditableField({ } async function handleSubmit() { - if (!internalValue) return; - - try { - const numberValue = parseInt(internalValue); - - if (isNaN(numberValue)) { - throw new Error('Not a number'); - } + if (!canBeCastAsIntegerOrNull(internalValue)) { + handleCancel(); + return; + } - // TODO: find a way to store this better in DB - if (numberValue > 2000000000) { - throw new Error('Number too big'); - } + const valueCastedAsNumberOrNull = castAsIntegerOrNull(internalValue); - onSubmit?.(numberValue); + onSubmit?.(valueCastedAsNumberOrNull); - setInternalValue(numberValue.toString()); - } catch { - handleCancel(); - } + setInternalValue(valueCastedAsNumberOrNull?.toString()); } async function handleCancel() { diff --git a/front/src/utils/__tests__/cast-as-integer-or-null.test.ts b/front/src/utils/__tests__/cast-as-integer-or-null.test.ts new file mode 100644 index 000000000000..cc077afdb27c --- /dev/null +++ b/front/src/utils/__tests__/cast-as-integer-or-null.test.ts @@ -0,0 +1,72 @@ +import { + canBeCastAsIntegerOrNull, + castAsIntegerOrNull, +} from '../cast-as-integer-or-null'; + +describe('canBeCastAsIntegerOrNull', () => { + it(`should return true if null`, () => { + expect(canBeCastAsIntegerOrNull(null)).toBeTruthy(); + }); + + it(`should return true if number`, () => { + expect(canBeCastAsIntegerOrNull(9)).toBeTruthy(); + }); + + it(`should return true if empty string`, () => { + expect(canBeCastAsIntegerOrNull('')).toBeTruthy(); + }); + + it(`should return true if integer string`, () => { + expect(canBeCastAsIntegerOrNull('9')).toBeTruthy(); + }); + + it(`should return false if undefined`, () => { + expect(canBeCastAsIntegerOrNull(undefined)).toBeFalsy(); + }); + + it(`should return false if non numeric string`, () => { + expect(canBeCastAsIntegerOrNull('9a')).toBeFalsy(); + }); + + it(`should return false if non numeric string #2`, () => { + expect(canBeCastAsIntegerOrNull('a9a')).toBeFalsy(); + }); + + it(`should return false if float`, () => { + expect(canBeCastAsIntegerOrNull(0.9)).toBeFalsy(); + }); + + it(`should return false if float string`, () => { + expect(canBeCastAsIntegerOrNull('0.9')).toBeFalsy(); + }); +}); + +describe('castAsIntegerOrNull', () => { + it(`should cast null to null`, () => { + expect(castAsIntegerOrNull(null)).toBe(null); + }); + + it(`should cast empty string to null`, () => { + expect(castAsIntegerOrNull('')).toBe(null); + }); + + it(`should cast an integer to an integer`, () => { + expect(castAsIntegerOrNull(9)).toBe(9); + }); + + it(`should cast an integer string to an integer`, () => { + expect(castAsIntegerOrNull('9')).toBe(9); + }); + + it(`should throw if trying to cast a float string to an integer`, () => { + expect(() => castAsIntegerOrNull('9.9')).toThrow(Error); + }); + + it(`should throw if trying to cast a non numeric string to an integer`, () => { + expect(() => castAsIntegerOrNull('9.9a')).toThrow(Error); + }); + + it(`should throw if trying to cast an undefined to an integer`, () => { + expect(() => castAsIntegerOrNull(undefined)).toThrow(Error); + }); +}); diff --git a/front/src/utils/cast-as-integer-or-null.ts b/front/src/utils/cast-as-integer-or-null.ts new file mode 100644 index 000000000000..ee706f257197 --- /dev/null +++ b/front/src/utils/cast-as-integer-or-null.ts @@ -0,0 +1,58 @@ +export function canBeCastAsIntegerOrNull( + probableNumberOrNull: string | undefined | number | null, +): probableNumberOrNull is number | null { + if (probableNumberOrNull === undefined) { + return false; + } + + if (typeof probableNumberOrNull === 'number') { + return Number.isInteger(probableNumberOrNull); + } + + if (probableNumberOrNull === null) { + return true; + } + + if (probableNumberOrNull === '') { + return true; + } + + if (typeof probableNumberOrNull === 'string') { + const stringAsNumber = +probableNumberOrNull; + + if (isNaN(stringAsNumber)) { + return false; + } + if (Number.isInteger(stringAsNumber)) { + return true; + } + } + + return false; +} + +export function castAsIntegerOrNull( + probableNumberOrNull: string | undefined | number | null, +): number | null { + if (canBeCastAsIntegerOrNull(probableNumberOrNull) === false) { + throw new Error('Cannot cast to number or null'); + } + + if (probableNumberOrNull === null) { + return null; + } + + if (probableNumberOrNull === '') { + return null; + } + + if (typeof probableNumberOrNull === 'number') { + return probableNumberOrNull; + } + + if (typeof probableNumberOrNull === 'string') { + return +probableNumberOrNull; + } + + return null; +}