diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bd074fa3db..2e7f91228a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -86,7 +86,7 @@ "stripe": "^11.1.0", "timezone-mock": "^1.3.6", "type-fest": "^4.17.0", - "typescript": "^4.5.3", + "typescript": "^5.4.5", "use-debounce": "^7.0.1", "use-draggable-scroll": "^0.1.0", "validator": "^13.7.0", @@ -24282,15 +24282,15 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ufo": { @@ -43043,9 +43043,9 @@ } }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==" }, "ufo": { "version": "1.5.3", diff --git a/frontend/package.json b/frontend/package.json index a6fb0623e1..fe5a8df560 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -82,7 +82,7 @@ "stripe": "^11.1.0", "timezone-mock": "^1.3.6", "type-fest": "^4.17.0", - "typescript": "^4.5.3", + "typescript": "^5.4.5", "use-debounce": "^7.0.1", "use-draggable-scroll": "^0.1.0", "validator": "^13.7.0", @@ -94,7 +94,7 @@ "postinstall": "npm run gen:theme-typings && npm --prefix ../shared install", "gen:theme-typings": "chakra-cli tokens --template augmentation src/theme/index.ts || true", "start": "vite", - "build": "vite build", + "build": "npx tsc && vite build", "test": "cross-env SKIP_PREFLIGHT_CHECK=true vitest", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx index faa404076f..417e71fcc2 100644 --- a/frontend/src/components/Button/Button.tsx +++ b/frontend/src/components/Button/Button.tsx @@ -5,8 +5,6 @@ import { IconProps, } from '@chakra-ui/react' -import { ThemeColorScheme } from '~theme/foundations/colours' - import Spinner from '../Spinner' export interface ButtonProps extends ChakraButtonProps { @@ -14,11 +12,6 @@ export interface ButtonProps extends ChakraButtonProps { * Loading spinner font size. Defaults to `1.5rem`. */ spinnerFontSize?: IconProps['fontSize'] - - /** - * Color scheme of button. - */ - colorScheme?: ThemeColorScheme /** * Base color intensity of button. */ diff --git a/frontend/src/components/Calendar/Calendar.tsx b/frontend/src/components/Calendar/Calendar.tsx index e0c3182eae..955287fc41 100644 --- a/frontend/src/components/Calendar/Calendar.tsx +++ b/frontend/src/components/Calendar/Calendar.tsx @@ -1,12 +1,11 @@ import { Box, forwardRef, + ThemingProps, useControllableState, useMultiStyleConfig, } from '@chakra-ui/react' -import { ThemeColorScheme } from '~theme/foundations/colours' - import { CalendarStylesProvider } from './CalendarBase/CalendarStyleProvider' import { CalendarAria, @@ -33,7 +32,7 @@ export interface CalendarProps extends CalendarBaseProps { /** Function to determine whether a date should be made unavailable. */ isDateUnavailable?: (d: Date) => boolean /** Color scheme for component */ - colorScheme?: ThemeColorScheme + colorScheme?: ThemingProps<'Calendar'>['colorScheme'] } export const Calendar = forwardRef( diff --git a/frontend/src/components/Calendar/CalendarBase/CalendarContext.tsx b/frontend/src/components/Calendar/CalendarBase/CalendarContext.tsx index c554d8d562..ecf2dd641a 100644 --- a/frontend/src/components/Calendar/CalendarBase/CalendarContext.tsx +++ b/frontend/src/components/Calendar/CalendarBase/CalendarContext.tsx @@ -7,6 +7,7 @@ import { useMemo, useState, } from 'react' +import { ThemingProps } from '@chakra-ui/react' import cuid from 'cuid' import { addMonths, @@ -18,8 +19,6 @@ import { Props as DayzedProps, RenderProps, useDayzed } from 'dayzed' import { inRange } from 'lodash' import { useKey } from 'rooks' -import { ThemeColorScheme } from '~theme/foundations/colours' - import { CalendarProps } from '../Calendar' import { DateRangeValue } from './types' @@ -71,7 +70,10 @@ type PassthroughProps = { /** * Color scheme of date input */ - colorScheme?: ThemeColorScheme + colorScheme?: ThemingProps<'Calendar'>['colorScheme'] + + /** Size of the component */ + size?: ThemingProps<'Calendar'>['size'] } export type UseProvideCalendarProps = Pick & PassthroughProps diff --git a/frontend/src/components/Calendar/CalendarBase/types.ts b/frontend/src/components/Calendar/CalendarBase/types.ts index 7ad7abaab5..f9e3ae1bc8 100644 --- a/frontend/src/components/Calendar/CalendarBase/types.ts +++ b/frontend/src/components/Calendar/CalendarBase/types.ts @@ -2,7 +2,7 @@ import { UseProvideCalendarProps } from './CalendarContext' export type CalendarBaseProps = Pick< UseProvideCalendarProps, - 'colorScheme' | 'isDateUnavailable' | 'monthsToDisplay' + 'colorScheme' | 'isDateUnavailable' | 'monthsToDisplay' | 'size' > export type DateRangeValue = [null, null] | [Date, null] | [Date, Date] diff --git a/frontend/src/components/DatePicker/DatePickerContext.tsx b/frontend/src/components/DatePicker/DatePickerContext.tsx index 37f3975b78..9381f73c2b 100644 --- a/frontend/src/components/DatePicker/DatePickerContext.tsx +++ b/frontend/src/components/DatePicker/DatePickerContext.tsx @@ -13,6 +13,7 @@ import React, { import { CSSObject, FormControlProps, + ThemingProps, useControllableState, useDisclosure, UseDisclosureReturn, @@ -21,7 +22,6 @@ import { } from '@chakra-ui/react' import { format, isValid, parse } from 'date-fns' -import { ThemeColorScheme } from '~theme/foundations/colours' import { useIsMobile } from '~hooks/useIsMobile' import { DatePickerProps } from './DatePicker' @@ -42,7 +42,7 @@ interface DatePickerContextReturn { closeCalendarOnChange: boolean placeholder: string allowManualInput: boolean - colorScheme: ThemeColorScheme + colorScheme?: ThemingProps<'DatePicker'>['colorScheme'] isDateUnavailable?: (date: Date) => boolean disclosureProps: UseDisclosureReturn monthsToDisplay?: number diff --git a/frontend/src/components/DatePicker/types.ts b/frontend/src/components/DatePicker/types.ts index 37754857c6..08be2d686e 100644 --- a/frontend/src/components/DatePicker/types.ts +++ b/frontend/src/components/DatePicker/types.ts @@ -3,7 +3,7 @@ import { InputProps } from '@chakra-ui/react' export interface DatePickerBaseProps extends Omit< InputProps, - 'value' | 'defaultValue' | 'onChange' | 'colorScheme' + 'value' | 'defaultValue' | 'onChange' | 'colorScheme' | 'size' > { /** * The `date-fns` format to display the date. diff --git a/frontend/src/components/DateRangePicker/DateRangePickerContext.tsx b/frontend/src/components/DateRangePicker/DateRangePickerContext.tsx index 5fd63ec28b..41b34ad409 100644 --- a/frontend/src/components/DateRangePicker/DateRangePickerContext.tsx +++ b/frontend/src/components/DateRangePicker/DateRangePickerContext.tsx @@ -14,6 +14,7 @@ import { import { CSSObject, FormControlProps, + ThemingProps, useControllableState, useDisclosure, UseDisclosureReturn, @@ -22,7 +23,6 @@ import { } from '@chakra-ui/react' import { format, isValid, parse } from 'date-fns' -import { ThemeColorScheme } from '~theme/foundations/colours' import { useIsMobile } from '~hooks/useIsMobile' import { DateRangeValue } from '~components/Calendar' @@ -51,7 +51,7 @@ interface DateRangePickerContextReturn { isDateUnavailable?: (date: Date) => boolean disclosureProps: UseDisclosureReturn labelSeparator: string - colorScheme: ThemeColorScheme + colorScheme?: ThemingProps<'DatePicker'>['colorScheme'] monthsToDisplay?: number } diff --git a/frontend/src/components/Field/YesNo/YesNoOption.tsx b/frontend/src/components/Field/YesNo/YesNoOption.tsx index 0966066b03..fa8be0ba96 100644 --- a/frontend/src/components/Field/YesNo/YesNoOption.tsx +++ b/frontend/src/components/Field/YesNo/YesNoOption.tsx @@ -4,6 +4,7 @@ import { Box, forwardRef, Icon, + ThemingProps, useMultiStyleConfig, useRadio, UseRadioGroupReturn, @@ -40,6 +41,8 @@ interface YesNoOptionProps extends UseRadioProps { * instead of the default event-only argument. */ onChange?: UseRadioGroupReturn['onChange'] + + size?: ThemingProps<'Radio'>['size'] } /** diff --git a/frontend/src/components/NumberInput/NumberInput.tsx b/frontend/src/components/NumberInput/NumberInput.tsx index 6b1e914819..6d7ffdd68c 100644 --- a/frontend/src/components/NumberInput/NumberInput.tsx +++ b/frontend/src/components/NumberInput/NumberInput.tsx @@ -31,6 +31,8 @@ export interface NumberInputProps extends ChakraNumberInputProps { * Whether to prevent default on user pressing the 'Enter' key. */ preventDefaultOnEnter?: boolean + + placeholder?: string } export const NumberInput = forwardRef( diff --git a/frontend/src/components/Toast/Toast.tsx b/frontend/src/components/Toast/Toast.tsx index a329a38f8d..88458f2a4e 100644 --- a/frontend/src/components/Toast/Toast.tsx +++ b/frontend/src/components/Toast/Toast.tsx @@ -15,7 +15,13 @@ import { BxsCheckCircle, BxsErrorCircle } from '~assets/icons' import { useMdComponents } from '~hooks/useMdComponents' import { MarkdownText } from '~components/MarkdownText' -export type ToastStatus = 'danger' | 'success' | 'warning' +export type ToastStatus = + | 'info' + | 'danger' + | 'success' + | 'warning' + | 'error' + | 'loading' export interface ToastProps extends Omit< diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditTable/EditTable.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditTable/EditTable.tsx index 8f65f1863f..dd9fe65da8 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditTable/EditTable.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditTable/EditTable.tsx @@ -181,7 +181,7 @@ export const EditTable = ({ field }: EditTableProps): JSX.Element => { // Must be greater than minimum rows validate: (value) => !value || - value > getValues('minimumRows') || + value > (getValues('minimumRows') || 0) || 'Maximum rows must be greater than minimum rows', }} control={control} diff --git a/frontend/src/features/admin-form/create/logic/components/LogicContent/EditLogicBlock/EditCondition/EditConditionBlock.tsx b/frontend/src/features/admin-form/create/logic/components/LogicContent/EditLogicBlock/EditCondition/EditConditionBlock.tsx index 57dc561539..a2d3223694 100644 --- a/frontend/src/features/admin-form/create/logic/components/LogicContent/EditLogicBlock/EditCondition/EditConditionBlock.tsx +++ b/frontend/src/features/admin-form/create/logic/components/LogicContent/EditLogicBlock/EditCondition/EditConditionBlock.tsx @@ -3,6 +3,7 @@ import { Controller, ControllerRenderProps, UseFormReturn, + Validate, } from 'react-hook-form' import { BiTrash } from 'react-icons/bi' import { @@ -185,7 +186,9 @@ export const EditConditionBlock = ({ return ifValueTypeValue === LogicIfValue.MultiSelect ? '0px' : 'auto' }, [ifValueTypeValue]) - const validateValueInputComponent = useCallback( + const validateValueInputComponent: Validate< + string | number | string[] | number[] + > = useCallback( (val) => { switch (ifValueTypeValue) { case LogicIfValue.Number: { diff --git a/frontend/src/features/admin-form/preview/PreviewFormProvider.tsx b/frontend/src/features/admin-form/preview/PreviewFormProvider.tsx index 0244c66437..3dea0c61a7 100644 --- a/frontend/src/features/admin-form/preview/PreviewFormProvider.tsx +++ b/frontend/src/features/admin-form/preview/PreviewFormProvider.tsx @@ -21,6 +21,7 @@ import { HttpError } from '~services/ApiService' import { FormFieldValues } from '~templates/Field' import NotFoundErrorPage from '~pages/NotFoundError' +import { SubmitEmailFormArgs } from '~features/public-form/PublicFormService' import { useEnv } from '../../env/queries' import { axiosDebugFlow } from '../../public-form/utils' @@ -51,7 +52,7 @@ export const PreviewFormProvider = ({ useCommonFormProvider(formId) const showErrorToast = useCallback( - (error) => { + (error: unknown) => { toast({ status: 'danger', description: @@ -119,11 +120,10 @@ export const PreviewFormProvider = ({ async (formInputs) => { const { form } = data ?? {} if (!form) return - - const formData = { + const formData: Omit = { formFields: form.form_fields, formLogics: form.form_logics, - formInputs, + formInputs: formInputs, } const logMeta = { diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx index eca1d4a971..2fded6fec6 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx @@ -169,7 +169,7 @@ export const ResponsesTable = () => { }, [currentPage, gotoPage]) const handleRowClick = useCallback( - (submissionId: string, responseNumber) => { + (submissionId: string, responseNumber: number) => { onRowClick() return navigate(submissionId, { state: { diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/utils/ndjsonStream.ts b/frontend/src/features/admin-form/responses/ResponsesPage/storage/utils/ndjsonStream.ts index 85050e6346..91d94ac413 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/utils/ndjsonStream.ts +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/utils/ndjsonStream.ts @@ -18,7 +18,7 @@ export const ndjsonStream = ( let data_buf = '' const processResult = ( - result?: ReadableStreamDefaultReadResult, + result?: ReadableStreamReadResult, ): Promise | undefined => { if (!result || (result.done && shouldCancel)) { return diff --git a/frontend/src/features/public-form/PublicFormProvider.tsx b/frontend/src/features/public-form/PublicFormProvider.tsx index 0d4604e011..6c8b369228 100644 --- a/frontend/src/features/public-form/PublicFormProvider.tsx +++ b/frontend/src/features/public-form/PublicFormProvider.tsx @@ -354,7 +354,7 @@ export const PublicFormProvider = ({ }, [data, toast, t]) const showErrorToast = useCallback( - (error, form: PublicFormDto) => { + (error: unknown, form: PublicFormDto) => { toast({ status: 'danger', description: diff --git a/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentPage.stories.tsx b/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentPage.stories.tsx index 63dd0af16e..d0406425f7 100644 --- a/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentPage.stories.tsx +++ b/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentPage.stories.tsx @@ -94,7 +94,6 @@ export const CompleteWithoutReceipt = Template( min_amount: 50, max_amount: 200, }} - paymentDate={new Date(Date.now())} />, ).bind({}) CompleteWithoutReceipt.parameters = { @@ -119,7 +118,6 @@ export const CompleteWithReceipt = Template( min_amount: 50, max_amount: 200, }} - paymentDate={new Date(Date.now())} />, ).bind({}) CompleteWithReceipt.parameters = { diff --git a/frontend/src/features/public-form/components/FormPaymentPage/components/PaymentQuantityModal.tsx b/frontend/src/features/public-form/components/FormPaymentPage/components/PaymentQuantityModal.tsx index 4298376442..ad6199249a 100644 --- a/frontend/src/features/public-form/components/FormPaymentPage/components/PaymentQuantityModal.tsx +++ b/frontend/src/features/public-form/components/FormPaymentPage/components/PaymentQuantityModal.tsx @@ -101,7 +101,7 @@ const PaymentQuantityModal = ({ variant="clear" aria-label="Decrement" colorScheme="secondary" - isDisabled={quantity <= minQty} + isDisabled={(quantity || 0) <= minQty} onClick={() => { setValue('quantity', quantity ? quantity - 1 : minQty) trigger('quantity') @@ -133,7 +133,7 @@ const PaymentQuantityModal = ({ variant="clear" aria-label="Increment" colorScheme="secondary" - isDisabled={quantity >= maxQty} + isDisabled={(quantity || 0) >= maxQty} onClick={() => { setValue('quantity', quantity ? quantity + 1 : minQty) trigger('quantity') diff --git a/frontend/src/features/user/billing/BillCharges/components/BillingTable.tsx b/frontend/src/features/user/billing/BillCharges/components/BillingTable.tsx index 76ccb40e53..a228d41c74 100644 --- a/frontend/src/features/user/billing/BillCharges/components/BillingTable.tsx +++ b/frontend/src/features/user/billing/BillCharges/components/BillingTable.tsx @@ -19,7 +19,7 @@ type BillingColumnData = Pick< 'formName' | 'adminEmail' | 'authType' | 'total' > -const AUTHTYPE_TO_TEXT = { +const AUTHTYPE_TO_TEXT: { [K in FormAuthType]?: string } = { [FormAuthType.NIL]: '-', [FormAuthType.SP]: 'Singpass', [FormAuthType.SGID]: 'sgID', diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModal.stories.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModal.stories.tsx index 070a0faef7..67023c784e 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModal.stories.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModal.stories.tsx @@ -98,6 +98,10 @@ export const StorageModeAckScreen = () => { Promise.resolve(console.log('create storage mode form')), secretKey, register, + handleCreateStorageModeOrMultirespondentForm: () => + Promise.resolve( + console.log('create storage mode or multirespondent form'), + ), } }, [handleCopyKey, hasCopiedKey, register]) diff --git a/frontend/src/features/workspace/components/WorkspaceFormRow/WorkspaceRowsContext.tsx b/frontend/src/features/workspace/components/WorkspaceFormRow/WorkspaceRowsContext.tsx index dcf170c43b..f712f2e059 100644 --- a/frontend/src/features/workspace/components/WorkspaceFormRow/WorkspaceRowsContext.tsx +++ b/frontend/src/features/workspace/components/WorkspaceFormRow/WorkspaceRowsContext.tsx @@ -76,7 +76,6 @@ export const WorkspaceRowsProvider = ({ }} > diff --git a/frontend/src/hooks/useToast.ts b/frontend/src/hooks/useToast.ts index 7d7abab819..29e8e81545 100644 --- a/frontend/src/hooks/useToast.ts +++ b/frontend/src/hooks/useToast.ts @@ -1,6 +1,5 @@ import React, { useMemo } from 'react' import { - RenderProps, useToast as useChakraToast, UseToastOptions as ChakraUseToastOptions, } from '@chakra-ui/react' @@ -54,19 +53,20 @@ export const useToast = ({ duration, position, ...rest, - render: (props: RenderProps) => - // NOTE: Because chakra expects this to be JSX, this has to be called with createElement. - // Omitting the createElement causes a visual bug, where our own theme providers are not used. - // Using createElement also allows the file to be pure ts rather than tsx. + render: render ?? - React.createElement(() => - Toast({ - status: status ?? initialStatus, - isClosable: initialProps.isClosable, - ...rest, - ...props, - }), - ), + ((props) => + // NOTE: Because chakra expects this to be JSX, this has to be called with createElement. + // Omitting the createElement causes a visual bug, where our own theme providers are not used. + // Using createElement also allows the file to be pure ts rather than tsx. + React.createElement(() => + Toast({ + status: status ?? initialStatus, + isClosable: initialProps.isClosable, + ...rest, + ...props, + }), + )), }) impl.close = toast.close diff --git a/frontend/src/mocks/msw/handlers/types.ts b/frontend/src/mocks/msw/handlers/types.ts index f79ca57ec7..48812ac88e 100644 --- a/frontend/src/mocks/msw/handlers/types.ts +++ b/frontend/src/mocks/msw/handlers/types.ts @@ -1,9 +1,7 @@ -import { DefaultRequestBody, DelayMode, MockedRequest, RestHandler } from 'msw' +import { DefaultBodyType, DelayMode, MockedRequest, RestHandler } from 'msw' export type WithDelayProps = { delay?: number | DelayMode } -export type DefaultRequestReturn = RestHandler< - MockedRequest -> +export type DefaultRequestReturn = RestHandler> diff --git a/frontend/src/templates/Field/ChildrenCompound/ChildrenCompoundField.tsx b/frontend/src/templates/Field/ChildrenCompound/ChildrenCompoundField.tsx index 987d94f478..a983a8fde0 100644 --- a/frontend/src/templates/Field/ChildrenCompound/ChildrenCompoundField.tsx +++ b/frontend/src/templates/Field/ChildrenCompound/ChildrenCompoundField.tsx @@ -339,6 +339,8 @@ const ChildrenBody = ({ onChange={(name) => { // This is bad practice but we have no choice because our // custom Select doesn't forward the event. + // FIXME: Fix types + // @ts-expect-error type inference issue setValue(childNamePath, name, { shouldValidate: true }) }} ref={(e) => { @@ -388,6 +390,8 @@ const ChildrenBody = ({ // We need to do this as the underlying data is not updated // by the field's value, but rather by onChange, which we did // not trigger via prefill. + // FIXME: Fix types + // @ts-expect-error type inference issue setValue(fieldPath, myInfoFormattedValue, { shouldValidate: true }) } const isDisabled = isSubmitting || !!myInfoValue @@ -436,6 +440,8 @@ const ChildrenBody = ({ onChange={(option) => // This is bad practice but we have no choice because our // custom Select doesn't forward the event. + // FIXME: Fix types + // @ts-expect-error type inference issue setValue(fieldPath, option, { shouldValidate: true }) } /> @@ -462,6 +468,8 @@ const ChildrenBody = ({ displayFormat={DATE_DISPLAY_FORMAT} inputValue={value} onInputValueChange={(date) => { + // FIXME: Fix types + // @ts-expect-error type inference issue setValue(fieldPath, date, { shouldValidate: true }) }} colorScheme={`theme-${colorTheme}`} diff --git a/frontend/src/templates/Field/Radio/RadioField.tsx b/frontend/src/templates/Field/Radio/RadioField.tsx index 33f00baf7d..011625d277 100644 --- a/frontend/src/templates/Field/Radio/RadioField.tsx +++ b/frontend/src/templates/Field/Radio/RadioField.tsx @@ -123,6 +123,7 @@ export const RadioField = ({ ml={styles.othersInput?.ml as string} mb={0} > + {/* @ts-expect-error FIXME: type inference */} {get(errors, `${othersInputName}.message`)} diff --git a/frontend/src/templates/Field/Table/ColumnCell.tsx b/frontend/src/templates/Field/Table/ColumnCell.tsx index 8749b22ff8..91f81d346e 100644 --- a/frontend/src/templates/Field/Table/ColumnCell.tsx +++ b/frontend/src/templates/Field/Table/ColumnCell.tsx @@ -180,6 +180,7 @@ export const ColumnCell = ({ // be shown in the individual column cells. isMobile ? ( + {/* @ts-expect-error FIXME: type inference */} {get(errors, `${inputName}.message`)} ) : null diff --git a/frontend/src/templates/Field/Table/TableField.tsx b/frontend/src/templates/Field/Table/TableField.tsx index 7c2ea634f2..d58693957b 100644 --- a/frontend/src/templates/Field/Table/TableField.tsx +++ b/frontend/src/templates/Field/Table/TableField.tsx @@ -87,14 +87,15 @@ export const TableField = ({ useEffect(() => { // Update field array when min rows changes. if (hasMinRowsChanged) { + const minRows = schema.minimumRows || 0 const prevRowLength = fields.length - if (schema.minimumRows > prevRowLength) { - for (let i = prevRowLength; i < schema.minimumRows; i++) { + if (minRows > prevRowLength) { + for (let i = prevRowLength; i < minRows; i++) { appendTableRow() } } else { // Remove rows from field array - for (let i = prevRowLength; i > schema.minimumRows; i--) { + for (let i = prevRowLength; i > minRows; i--) { remove(i - 1) } } @@ -102,7 +103,11 @@ export const TableField = ({ }, [appendTableRow, fields.length, hasMinRowsChanged, remove, schema]) const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = - useTable({ columns: columnsData, data: fields }) + useTable({ + // @ts-expect-error Loose types, cell props will be passed during render, but will be fixed if upgrade to v8. + columns: columnsData, + data: fields, + }) const handleAddRow = useCallback(() => { if ( @@ -115,7 +120,8 @@ export const TableField = ({ const handleRemoveRow = useCallback( (rowIndex: number) => { - if (fields.length <= schema.minimumRows || rowIndex >= fields.length) { + const minRows = schema.minimumRows || 0 + if (fields.length <= minRows || rowIndex >= fields.length) { return } return remove(rowIndex) @@ -228,7 +234,8 @@ export const TableField = ({ > & { [PAYMENT_CONTACT_FIELD_ID]?: { value: string } [PAYMENT_VARIABLE_INPUT_AMOUNT_FIELD_ID]?: string - [PAYMENT_PRODUCT_FIELD_ID]?: Array + [PAYMENT_PRODUCT_FIELD_ID]?: ProductItemInput[] } export type AttachmentFieldInput = FieldInput diff --git a/frontend/src/templates/PublicHeader/PublicHeader.stories.tsx b/frontend/src/templates/PublicHeader/PublicHeader.stories.tsx index 6bf16b5d68..7fc9a079b8 100644 --- a/frontend/src/templates/PublicHeader/PublicHeader.stories.tsx +++ b/frontend/src/templates/PublicHeader/PublicHeader.stories.tsx @@ -2,6 +2,7 @@ import { Button } from '@chakra-ui/react' import { Meta, StoryFn } from '@storybook/react' import { BxsHelpCircle } from '~assets/icons/BxsHelpCircle' +import { FORM_GUIDE } from '~constants/links' import { getMobileViewParameters, getTabletViewParameters, @@ -16,13 +17,9 @@ const DEFAULT_ARGS: PublicHeaderProps = { ), publicHeaderLinks: [ - { - label: 'Products', - href: '', - }, { label: 'Help', - href: 'https://guide.form.gov.sg', + href: FORM_GUIDE, showOnMobile: true, MobileIcon: BxsHelpCircle, }, diff --git a/frontend/src/templates/PublicHeader/PublicHeader.tsx b/frontend/src/templates/PublicHeader/PublicHeader.tsx index 092fa379da..5f9e5f6d41 100644 --- a/frontend/src/templates/PublicHeader/PublicHeader.tsx +++ b/frontend/src/templates/PublicHeader/PublicHeader.tsx @@ -28,7 +28,7 @@ export interface PublicHeaderProps { /** Header links to display, if provided. */ publicHeaderLinks?: PublicHeaderLinkProps[] /** Call to action element to render, if any. */ - ctaElement?: React.ReactChild + ctaElement?: React.ReactNode /** Background colour to use for the header, if specified. */ bg?: string } diff --git a/frontend/src/theme/components/NumberInput.ts b/frontend/src/theme/components/NumberInput.ts index d9428b52ed..c473fac154 100644 --- a/frontend/src/theme/components/NumberInput.ts +++ b/frontend/src/theme/components/NumberInput.ts @@ -75,5 +75,6 @@ export const NumberInput = defineMultiStyleConfig({ baseStyle, sizes, variants: variants, + // @ts-expect-error type inference defaultProps: Input.defaultProps, }) diff --git a/package-lock.json b/package-lock.json index 11fd0e9ec4..f3fe6c878c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -197,7 +197,7 @@ "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "type-fest": "^4.17.0", - "typescript": "^4.9.4", + "typescript": "^5.4.5", "webpack": "^4.46.0", "webpack-cli": "^4.10.0", "worker-loader": "^2.0.0" @@ -28657,16 +28657,16 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uglify-js": { diff --git a/package.json b/package.json index e232d06592..0efab29f10 100644 --- a/package.json +++ b/package.json @@ -243,7 +243,7 @@ "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "type-fest": "^4.17.0", - "typescript": "^4.9.4", + "typescript": "^5.4.5", "webpack": "^4.46.0", "webpack-cli": "^4.10.0", "worker-loader": "^2.0.0" diff --git a/serverless/virus-scanner/package-lock.json b/serverless/virus-scanner/package-lock.json index 5bcc6381d6..68073e86b4 100644 --- a/serverless/virus-scanner/package-lock.json +++ b/serverless/virus-scanner/package-lock.json @@ -41,7 +41,7 @@ "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "type-fest": "^4.17.0", - "typescript": "^4.9.4" + "typescript": "^5.4.5" } }, "node_modules/@ampproject/remapping": { @@ -13844,16 +13844,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbzip2-stream": { @@ -25409,9 +25409,9 @@ "dev": true }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true }, "unbzip2-stream": { diff --git a/serverless/virus-scanner/package.json b/serverless/virus-scanner/package.json index 564617dad9..635735ce96 100644 --- a/serverless/virus-scanner/package.json +++ b/serverless/virus-scanner/package.json @@ -45,6 +45,6 @@ "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "type-fest": "^4.17.0", - "typescript": "^4.9.4" + "typescript": "^5.4.5" } } diff --git a/shared/types/form/product.ts b/shared/types/form/product.ts index 755c6f2a9f..0a504701fd 100644 --- a/shared/types/form/product.ts +++ b/shared/types/form/product.ts @@ -1,21 +1,27 @@ -import { Opaque } from 'type-fest' +import { z } from 'zod' -export type ProductId = Opaque -export type Product = { - name: string - description: string - multi_qty: boolean - min_qty: number - max_qty: number - amount_cents: number - _id: ProductId -} +export const ProductId = z.string().brand<'ProductId'>() +export type ProductId = z.infer -export type ProductItem = { - data: Product - selected: boolean - quantity: number -} +export const ProductDto = z.object({ + _id: ProductId, + name: z.string(), + description: z.string(), + multi_qty: z.boolean(), + min_qty: z.number().nonnegative(), + max_qty: z.number().nonnegative(), + amount_cents: z.number().nonnegative(), +}) +export type Product = z.infer + +export const ProductItemDto = z.object({ + data: ProductDto, + selected: z.boolean(), + quantity: z.number().nonnegative(), +}) + +export type ProductItem = z.infer +export type ProductItemInput = z.input export type ProductItemForReceipt = { name: string diff --git a/src/app/models/field/tableField.ts b/src/app/models/field/tableField.ts index 53309c6d32..35fb1f7797 100644 --- a/src/app/models/field/tableField.ts +++ b/src/app/models/field/tableField.ts @@ -48,7 +48,7 @@ const createTableFieldSchema = () => { min: 2, validate: { validator: function (this: ITableFieldSchema, v?: number) { - return !v || v > this.minimumRows + return !v || v > (this.minimumRows || 0) }, message: 'Maximum number of rows must be greater than minimum.', }, diff --git a/src/app/modules/payments/__tests__/payments.service.spec.ts b/src/app/modules/payments/__tests__/payments.service.spec.ts index f736981135..0079865b53 100644 --- a/src/app/modules/payments/__tests__/payments.service.spec.ts +++ b/src/app/modules/payments/__tests__/payments.service.spec.ts @@ -131,7 +131,7 @@ describe('payments.service', () => { { data: { ...mockValidProduct, - _id: new ObjectId() as unknown as ProductId, + _id: new ObjectId().toString() as ProductId, }, quantity: 1, selected: true, diff --git a/src/app/utils/field-validation/validators/tableValidator.ts b/src/app/utils/field-validation/validators/tableValidator.ts index 644a782a26..0ccdd4ac0f 100644 --- a/src/app/utils/field-validation/validators/tableValidator.ts +++ b/src/app/utils/field-validation/validators/tableValidator.ts @@ -30,7 +30,7 @@ const makeMinimumRowsValidator: TableValidatorConstructor = const { answerArray } = response const { minimumRows } = tableField - return answerArray.length >= minimumRows + return answerArray.length >= (minimumRows || 0) ? right(response) : left(`TableValidator:\tanswer has less than the minimum number of rows`) } diff --git a/src/app/utils/field-validation/validators/textValidator.ts b/src/app/utils/field-validation/validators/textValidator.ts index c547b8c1b7..69e9111283 100644 --- a/src/app/utils/field-validation/validators/textValidator.ts +++ b/src/app/utils/field-validation/validators/textValidator.ts @@ -26,7 +26,7 @@ const minLengthValidator: TextFieldValidatorConstructor = (textField) => (response) => { const { customVal: min } = textField.ValidationOptions if (min === null) return right(response) - return response.answer.length >= min + return response.answer.length >= (min || 0) ? right(response) : left(`TextValidator.minLength:\tanswer is less than minimum of ${min}`) } @@ -39,7 +39,7 @@ const maxLengthValidator: TextFieldValidatorConstructor = (textField) => (response) => { const { customVal: max } = textField.ValidationOptions if (max === null) return right(response) - return response.answer.length <= max + return response.answer.length <= (max || 0) ? right(response) : left( `TextValidator.maxLength:\tanswer is greater than maximum of ${max}`,