Add ability to properly cast a string, number, null to an integer (#990)
This commit is contained in:
@ -6,6 +6,10 @@ import { IconUsers } from '@/ui/icon';
|
|||||||
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
|
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
|
||||||
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
||||||
import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql';
|
import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql';
|
||||||
|
import {
|
||||||
|
canBeCastAsIntegerOrNull,
|
||||||
|
castAsIntegerOrNull,
|
||||||
|
} from '~/utils/cast-as-integer-or-null';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
company: Pick<Company, 'id' | 'employees'>;
|
company: Pick<Company, 'id' | 'employees'>;
|
||||||
@ -27,30 +31,25 @@ export function CompanyEmployeesEditableField({ company }: OwnProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!internalValue) return;
|
if (!canBeCastAsIntegerOrNull(internalValue)) {
|
||||||
|
|
||||||
try {
|
|
||||||
const numberValue = parseInt(internalValue);
|
|
||||||
|
|
||||||
if (isNaN(numberValue)) {
|
|
||||||
throw new Error('Not a number');
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateCompany({
|
|
||||||
variables: {
|
|
||||||
where: {
|
|
||||||
id: company.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
employees: numberValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setInternalValue(numberValue.toString());
|
|
||||||
} catch {
|
|
||||||
handleCancel();
|
handleCancel();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const valueCastedAsNumberOrNull = castAsIntegerOrNull(internalValue);
|
||||||
|
|
||||||
|
await updateCompany({
|
||||||
|
variables: {
|
||||||
|
where: {
|
||||||
|
id: company.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
employees: valueCastedAsNumberOrNull,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setInternalValue(valueCastedAsNumberOrNull?.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCancel() {
|
async function handleCancel() {
|
||||||
|
|||||||
@ -4,12 +4,16 @@ import { EditableField } from '@/ui/editable-field/components/EditableField';
|
|||||||
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
|
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
|
||||||
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
|
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
|
||||||
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
||||||
|
import {
|
||||||
|
canBeCastAsIntegerOrNull,
|
||||||
|
castAsIntegerOrNull,
|
||||||
|
} from '~/utils/cast-as-integer-or-null';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value: number | null | undefined;
|
value: number | null | undefined;
|
||||||
onSubmit?: (newValue: number) => void;
|
onSubmit?: (newValue: number | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function NumberEditableField({
|
export function NumberEditableField({
|
||||||
@ -29,26 +33,16 @@ export function NumberEditableField({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!internalValue) return;
|
if (!canBeCastAsIntegerOrNull(internalValue)) {
|
||||||
|
|
||||||
try {
|
|
||||||
const numberValue = parseInt(internalValue);
|
|
||||||
|
|
||||||
if (isNaN(numberValue)) {
|
|
||||||
throw new Error('Not a number');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: find a way to store this better in DB
|
|
||||||
if (numberValue > 2000000000) {
|
|
||||||
throw new Error('Number too big');
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit?.(numberValue);
|
|
||||||
|
|
||||||
setInternalValue(numberValue.toString());
|
|
||||||
} catch {
|
|
||||||
handleCancel();
|
handleCancel();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const valueCastedAsNumberOrNull = castAsIntegerOrNull(internalValue);
|
||||||
|
|
||||||
|
onSubmit?.(valueCastedAsNumberOrNull);
|
||||||
|
|
||||||
|
setInternalValue(valueCastedAsNumberOrNull?.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCancel() {
|
async function handleCancel() {
|
||||||
|
|||||||
72
front/src/utils/__tests__/cast-as-integer-or-null.test.ts
Normal file
72
front/src/utils/__tests__/cast-as-integer-or-null.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
58
front/src/utils/cast-as-integer-or-null.ts
Normal file
58
front/src/utils/cast-as-integer-or-null.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user