Skip to content

Commit

Permalink
feat: add submission limit error handling and checks
Browse files Browse the repository at this point in the history
  • Loading branch information
karrui committed Sep 30, 2021
1 parent 09a78c3 commit 78b0295
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SubmissionCountQueryDto } from '~shared/types/submission'

import { ApiService } from '~services/ApiService'

import { ADMIN_FORM_ENDPOINT } from '../common/AdminViewFormService'

/**
* Counts the number of submissions for a given form
* @param urlParameters Mapping of the url parameters to values
* @returns The number of form submissions
*/
export const countFormSubmissions = async ({
formId,
dates,
}: {
formId: string
dates?: SubmissionCountQueryDto
}): Promise<number> => {
const queryUrl = `${ADMIN_FORM_ENDPOINT}/${formId}/submissions/count`
if (dates) {
return ApiService.get(queryUrl, {
params: { ...dates },
}).then(({ data }) => data)
}
return ApiService.get(queryUrl).then(({ data }) => data)
}
23 changes: 23 additions & 0 deletions frontend/src/features/admin-form/responses/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useQuery, UseQueryResult } from 'react-query'
import { useParams } from 'react-router-dom'

import { adminFormKeys } from '../common/queries'

import { countFormSubmissions } from './AdminSubmissionsService'

export const adminFormResponsesKeys = {
base: [...adminFormKeys.base, 'responses'] as const,
id: (id: string) => [...adminFormResponsesKeys.base, id] as const,
}

/**
* @precondition Must be wrapped in a Router as `useParam` is used.
*/
export const useFormResponsesCount = (): UseQueryResult<number> => {
const { formId } = useParams<{ formId: string }>()
return useQuery(
adminFormResponsesKeys.id(formId),
() => countFormSubmissions({ formId }),
{ staleTime: 10 * 60 * 1000 },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,65 @@ import {
} from 'react'
import { FormControl, Skeleton } from '@chakra-ui/react'

import FormErrorMessage from '~components/FormControl/FormErrorMessage'
import FormLabel from '~components/FormControl/FormLabel'
import NumberInput from '~components/NumberInput'
import Toggle from '~components/Toggle'

import { useFormResponsesCount } from '~features/admin-form/responses/queries'

import { useMutateFormSettings } from '../mutations'
import { useAdminFormSettings } from '../queries'

const DEFAULT_SUBMISSION_LIMIT = 1000

interface FormLimitBlockProps {
initialLimit: string
currentResponseCount: number
}
const FormLimitBlock = ({ initialLimit }: FormLimitBlockProps): JSX.Element => {
const FormLimitBlock = ({
initialLimit,
currentResponseCount,
}: FormLimitBlockProps): JSX.Element => {
const [value, setValue] = useState(initialLimit)
const [error, setError] = useState<string>()

const inputRef = useRef<HTMLInputElement>(null)
const { mutateFormLimit } = useMutateFormSettings()

// TODO: Show error when given value is below current submission counts.
const handleValueChange = useCallback((val: string) => {
// Only allow numeric inputs
setValue(val.replace(/\D/g, ''))
}, [])
const handleValueChange = useCallback(
(val: string) => {
// Only allow numeric inputs and remove leading zeroes.
const nextVal = val.replace(/^0+|\D/g, '')
setValue(nextVal)
if (parseInt(nextVal, 10) <= currentResponseCount) {
setError(
`Submission limit must be greater than current submission count (${currentResponseCount})`,
)
} else if (error) {
setError(undefined)
}
},
[currentResponseCount, error],
)

const handleBlur = useCallback(() => {
if (error) {
setError(undefined)
}
if (value === initialLimit) return
if (value === '') {
const valueInt = parseInt(value, 10)
if (value === '' || valueInt <= currentResponseCount) {
return setValue(initialLimit)
}

return mutateFormLimit.mutate(parseInt(value, 10), {
return mutateFormLimit.mutate(valueInt, {
onError: () => {
setValue(initialLimit)
},
})
}, [initialLimit, mutateFormLimit, value])
}, [currentResponseCount, error, initialLimit, mutateFormLimit, value])

const handleKeydown: KeyboardEventHandler<HTMLInputElement> = useCallback(
(e) => {
Expand All @@ -53,7 +78,7 @@ const FormLimitBlock = ({ initialLimit }: FormLimitBlockProps): JSX.Element => {
)

return (
<FormControl mt="2rem">
<FormControl mt="2rem" isInvalid={!!error}>
<FormLabel
isRequired
description="Your form will automatically close once it reaches the set limit. Enable
Expand All @@ -64,15 +89,17 @@ const FormLimitBlock = ({ initialLimit }: FormLimitBlockProps): JSX.Element => {
<NumberInput
maxW="16rem"
ref={inputRef}
min={0}
// min={currentResponseCount + 1}
inputMode="numeric"
allowMouseWheel
clampValueOnBlur
precision={0}
value={value}
onChange={handleValueChange}
onKeyDown={handleKeydown}
onBlur={handleBlur}
/>
<FormErrorMessage>{error}</FormErrorMessage>
</FormControl>
)
}
Expand All @@ -81,6 +108,9 @@ export const FormLimitToggle = (): JSX.Element => {
const { data: settings, isLoading: isLoadingSettings } =
useAdminFormSettings()

const { data: responseCount, isLoading: isLoadingCount } =
useFormResponsesCount()

const isLimit = useMemo(
() => settings && settings?.submissionLimit !== null,
[settings],
Expand All @@ -89,10 +119,34 @@ export const FormLimitToggle = (): JSX.Element => {
const { mutateFormLimit } = useMutateFormSettings()

const handleToggleLimit = useCallback(() => {
if (!settings || isLoadingSettings || mutateFormLimit.isLoading) return
const nextLimit = settings.submissionLimit === null ? 1000 : null
if (
!settings ||
isLoadingSettings ||
isLoadingCount ||
responseCount === undefined ||
mutateFormLimit.isLoading
)
return

// Case toggling submissionLimit off.
if (settings.submissionLimit !== null) {
return mutateFormLimit.mutate(null)
}

// Case toggling submissionLimit on.
// Allow 1 more response if default submission limit is hit.
const nextLimit =
responseCount > DEFAULT_SUBMISSION_LIMIT
? responseCount + 1
: DEFAULT_SUBMISSION_LIMIT
return mutateFormLimit.mutate(nextLimit)
}, [isLoadingSettings, mutateFormLimit, settings])
}, [
isLoadingCount,
isLoadingSettings,
mutateFormLimit,
responseCount,
settings,
])

return (
<Skeleton isLoaded={!isLoadingSettings && !!settings}>
Expand All @@ -103,7 +157,12 @@ export const FormLimitToggle = (): JSX.Element => {
onChange={() => handleToggleLimit()}
/>
{settings && settings?.submissionLimit !== null && (
<FormLimitBlock initialLimit={String(settings.submissionLimit)} />
<Skeleton isLoaded={!isLoadingCount}>
<FormLimitBlock
initialLimit={String(settings.submissionLimit)}
currentResponseCount={responseCount ?? 0}
/>
</Skeleton>
)}
</Skeleton>
)
Expand Down

0 comments on commit 78b0295

Please sign in to comment.