diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx
index 659608b191..39a9e27514 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx
@@ -20,6 +20,7 @@ import IconButton from '~components/IconButton'
import {
AttachmentField,
CheckboxField,
+ DateField,
DecimalField,
DropdownField,
EmailField,
@@ -316,6 +317,8 @@ const MemoFieldRow = memo(({ field, ...rest }: MemoFieldRowProps) => {
return
case BasicField.Decimal:
return
+ case BasicField.Date:
+ return
case BasicField.Dropdown:
return
case BasicField.Statement:
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/EditFieldDrawer.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/EditFieldDrawer.tsx
index ce4e30abce..2906169c58 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/EditFieldDrawer.tsx
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/EditFieldDrawer.tsx
@@ -17,9 +17,10 @@ import {
} from '../../useBuilderAndDesignStore'
import { CreatePageDrawerCloseButton } from '../CreatePageDrawerCloseButton'
-import { EditAttachment } from './edit-fieldtype/EditAttachment'
import {
+ EditAttachment,
EditCheckbox,
+ EditDate,
EditDecimal,
EditDropdown,
EditEmail,
@@ -149,6 +150,8 @@ export const MemoFieldDrawerContent = memo(
return
case BasicField.Number:
return
+ case BasicField.Date:
+ return
case BasicField.Decimal:
return
case BasicField.Section:
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/EditDate.stories.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/EditDate.stories.tsx
new file mode 100644
index 0000000000..4d2b34bb9c
--- /dev/null
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/EditDate.stories.tsx
@@ -0,0 +1,78 @@
+import { Meta, Story } from '@storybook/react'
+
+import {
+ BasicField,
+ DateFieldBase,
+ DateSelectedValidation,
+} from '~shared/types'
+
+import { EditFieldDrawerDecorator, StoryRouter } from '~utils/storybook'
+
+import { EditDate } from './EditDate'
+
+const DEFAULT_DATE_FIELD: DateFieldBase = {
+ title: 'Storybook Date',
+ description: 'Some description',
+ dateValidation: {
+ selectedDateValidation: null,
+ customMaxDate: null,
+ customMinDate: null,
+ },
+ required: true,
+ disabled: false,
+ fieldType: BasicField.Date,
+ globalId: 'unused',
+}
+
+export default {
+ title: 'Features/AdminForm/EditFieldDrawer/EditDate',
+ component: EditDate,
+ decorators: [
+ StoryRouter({
+ initialEntries: ['/61540ece3d4a6e50ac0cc6ff'],
+ path: '/:formId',
+ }),
+ EditFieldDrawerDecorator,
+ ],
+ parameters: {
+ // Required so skeleton "animation" does not hide content.
+ chromatic: { pauseAnimationAtEnd: true },
+ },
+ args: {
+ field: DEFAULT_DATE_FIELD,
+ },
+} as Meta
+
+interface StoryArgs {
+ field: DateFieldBase
+}
+
+const Template: Story = ({ field }) => {
+ return
+}
+
+export const Default = Template.bind({})
+
+export const WithNoFutureDates = Template.bind({})
+WithNoFutureDates.args = {
+ field: {
+ ...DEFAULT_DATE_FIELD,
+ dateValidation: {
+ selectedDateValidation: DateSelectedValidation.NoFuture,
+ customMaxDate: null,
+ customMinDate: null,
+ },
+ },
+}
+
+export const WithCustomDateRange = Template.bind({})
+WithCustomDateRange.args = {
+ field: {
+ ...DEFAULT_DATE_FIELD,
+ dateValidation: {
+ selectedDateValidation: DateSelectedValidation.Custom,
+ customMinDate: new Date('2020-01-01T00:00:00Z'),
+ customMaxDate: new Date('2020-01-12T00:00:00Z'),
+ },
+ },
+}
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/EditDate.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/EditDate.tsx
new file mode 100644
index 0000000000..6a9188be2a
--- /dev/null
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/EditDate.tsx
@@ -0,0 +1,228 @@
+import { useMemo } from 'react'
+import { Controller, RegisterOptions } from 'react-hook-form'
+import { Box, FormControl, SimpleGrid } from '@chakra-ui/react'
+import { isBefore, isDate, isEqual } from 'date-fns'
+import { extend, get, isEmpty, pick } from 'lodash'
+
+import {
+ DateFieldBase,
+ DateSelectedValidation,
+ DateValidationOptions,
+} from '~shared/types/field'
+
+import {
+ transformDateToShortIsoString,
+ transformShortIsoStringToDate,
+} from '~utils/date'
+import { createBaseValidationRules } from '~utils/fieldValidation'
+import DateInput from '~components/DatePicker'
+import { SingleSelect } from '~components/Dropdown'
+import FormErrorMessage from '~components/FormControl/FormErrorMessage'
+import FormLabel from '~components/FormControl/FormLabel'
+import Input from '~components/Input'
+import Textarea from '~components/Textarea'
+import Toggle from '~components/Toggle'
+
+import { DrawerContentContainer } from '../common/DrawerContentContainer'
+import { FormFieldDrawerActions } from '../common/FormFieldDrawerActions'
+import { EditFieldProps } from '../common/types'
+import { useEditFieldForm } from '../common/useEditFieldForm'
+
+type EditDateProps = EditFieldProps
+
+const EDIT_DATE_FIELD_KEYS = ['title', 'description', 'required'] as const
+
+type EditDateInputs = Pick<
+ DateFieldBase,
+ typeof EDIT_DATE_FIELD_KEYS[number]
+> & {
+ dateValidation: {
+ selectedDateValidation: DateSelectedValidation | ''
+ customMaxDate: string
+ customMinDate: string
+ }
+}
+
+const transformDateFieldToEditForm = (field: DateFieldBase): EditDateInputs => {
+ const nextValidationOptions = {
+ selectedDateValidation:
+ field.dateValidation.selectedDateValidation ?? ('' as const),
+ customMaxDate: field.dateValidation.selectedDateValidation
+ ? transformDateToShortIsoString(field.dateValidation.customMaxDate) ?? ''
+ : ('' as const),
+ customMinDate: field.dateValidation.selectedDateValidation
+ ? transformDateToShortIsoString(field.dateValidation.customMinDate) ?? ''
+ : ('' as const),
+ }
+ return {
+ ...pick(field, EDIT_DATE_FIELD_KEYS),
+ dateValidation: nextValidationOptions,
+ }
+}
+
+const transformDateEditFormToField = (
+ inputs: EditDateInputs,
+ originalField: DateFieldBase,
+): DateFieldBase => {
+ let nextValidationOptions: DateValidationOptions
+ switch (inputs.dateValidation.selectedDateValidation) {
+ case '':
+ nextValidationOptions = {
+ selectedDateValidation: null,
+ customMinDate: null,
+ customMaxDate: null,
+ }
+ break
+ case DateSelectedValidation.NoFuture:
+ case DateSelectedValidation.NoPast:
+ nextValidationOptions = {
+ selectedDateValidation: inputs.dateValidation.selectedDateValidation,
+ customMinDate: null,
+ customMaxDate: null,
+ }
+ break
+ case DateSelectedValidation.Custom: {
+ nextValidationOptions = {
+ selectedDateValidation: inputs.dateValidation.selectedDateValidation,
+ customMinDate: transformShortIsoStringToDate(
+ inputs.dateValidation.customMinDate,
+ ),
+ customMaxDate: transformShortIsoStringToDate(
+ inputs.dateValidation.customMaxDate,
+ ),
+ }
+ }
+ }
+
+ return extend({}, originalField, inputs, {
+ dateValidation: nextValidationOptions,
+ })
+}
+
+export const EditDate = ({ field }: EditDateProps): JSX.Element => {
+ const {
+ register,
+ formState: { errors },
+ getValues,
+ isSaveEnabled,
+ control,
+ buttonText,
+ handleUpdateField,
+ isLoading,
+ handleCancel,
+ } = useEditFieldForm({
+ field,
+ transform: {
+ input: transformDateFieldToEditForm,
+ output: transformDateEditFormToField,
+ },
+ })
+
+ const requiredValidationRule = useMemo(
+ () => createBaseValidationRules({ required: true }),
+ [],
+ )
+
+ const customMinValidationOptions: RegisterOptions<
+ EditDateInputs,
+ 'dateValidation.customMinDate'
+ > = useMemo(
+ () => ({
+ // customMin is required if there is selected validation.
+ validate: {
+ hasValidation: (val) => {
+ const hasMaxValue =
+ getValues('dateValidation.selectedDateValidation') ===
+ DateSelectedValidation.Custom &&
+ !!getValues('dateValidation.customMaxDate')
+ return !!val || hasMaxValue || 'You must specify at least one date.'
+ },
+ validDate: (val) =>
+ !val || isDate(new Date(val)) || 'Please enter a valid date',
+ inRange: (val) => {
+ const date = new Date(val)
+ const maxDate = new Date(getValues('dateValidation.customMaxDate'))
+ return (
+ isEqual(date, maxDate) ||
+ isBefore(date, maxDate) ||
+ 'Max date cannot be less than min date.'
+ )
+ },
+ },
+ }),
+ [getValues],
+ )
+
+ return (
+
+
+ Question
+
+ {errors?.title?.message}
+
+
+ Description
+
+ {errors?.description?.message}
+
+
+
+
+
+ Date validation
+
+
+ (
+
+ )}
+ />
+
+ {getValues('dateValidation.selectedDateValidation') ===
+ DateSelectedValidation.Custom ? (
+ <>
+ }
+ />
+ }
+ />
+ >
+ ) : null}
+
+
+ {get(errors, 'dateValidation.customMinDate.message')}
+
+
+
+
+ )
+}
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/index.ts b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/index.ts
new file mode 100644
index 0000000000..809901c528
--- /dev/null
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditDate/index.ts
@@ -0,0 +1 @@
+export { EditDate } from './EditDate'
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/index.ts b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/index.ts
index 0facfd4eab..1b23b9297f 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/index.ts
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/index.ts
@@ -1,4 +1,6 @@
+export * from './EditAttachment'
export * from './EditCheckbox'
+export * from './EditDate'
export * from './EditDecimal'
export * from './EditDropdown'
export * from './EditEmail'
diff --git a/frontend/src/features/admin-form/create/builder-and-design/UpdateFormFieldService.ts b/frontend/src/features/admin-form/create/builder-and-design/UpdateFormFieldService.ts
index bdf15500e4..53ff0d9349 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/UpdateFormFieldService.ts
+++ b/frontend/src/features/admin-form/create/builder-and-design/UpdateFormFieldService.ts
@@ -1,5 +1,6 @@
import { FieldCreateDto, FormFieldDto } from '~shared/types/field'
+import { transformAllIsoStringsToDate } from '~utils/date'
import { ApiService } from '~services/ApiService'
import { ADMIN_FORM_ENDPOINT } from '~features/admin-form/common/AdminViewFormService'
@@ -24,7 +25,9 @@ export const createSingleFormField = async ({
`${ADMIN_FORM_ENDPOINT}/${formId}/fields`,
createFieldBody,
{ params: { to: insertionIndex } },
- ).then(({ data }) => data)
+ )
+ .then(({ data }) => data)
+ .then(transformAllIsoStringsToDate)
}
export const updateSingleFormField = async ({
@@ -37,7 +40,9 @@ export const updateSingleFormField = async ({
return ApiService.put(
`${ADMIN_FORM_ENDPOINT}/${formId}/fields/${updateFieldBody._id}`,
updateFieldBody,
- ).then(({ data }) => data)
+ )
+ .then(({ data }) => data)
+ .then(transformAllIsoStringsToDate)
}
/**
@@ -60,7 +65,9 @@ export const reorderSingleFormField = async ({
`${ADMIN_FORM_ENDPOINT}/${formId}/fields/${fieldId}/reorder`,
{},
{ params: { to: newPosition } },
- ).then(({ data }) => data)
+ )
+ .then(({ data }) => data)
+ .then(transformAllIsoStringsToDate)
}
/**
@@ -79,7 +86,9 @@ export const duplicateSingleFormField = async ({
}): Promise => {
return ApiService.post(
`${ADMIN_FORM_ENDPOINT}/${formId}/fields/${fieldId}/duplicate`,
- ).then(({ data }) => data)
+ )
+ .then(({ data }) => data)
+ .then(transformAllIsoStringsToDate)
}
/**
diff --git a/frontend/src/features/admin-form/create/builder-and-design/utils/fieldCreation.ts b/frontend/src/features/admin-form/create/builder-and-design/utils/fieldCreation.ts
index cbbcbfc881..79e3532f92 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/utils/fieldCreation.ts
+++ b/frontend/src/features/admin-form/create/builder-and-design/utils/fieldCreation.ts
@@ -152,6 +152,17 @@ export const getFieldCreationMeta = (fieldType: BasicField): FieldCreateDto => {
},
}
}
+ case BasicField.Date: {
+ return {
+ fieldType,
+ ...baseMeta,
+ dateValidation: {
+ customMaxDate: null,
+ customMinDate: null,
+ selectedDateValidation: null,
+ },
+ }
+ }
default: {
return {
fieldType: BasicField.Section,
diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts
index f8c9790e07..4a386d3097 100644
--- a/frontend/src/utils/date.ts
+++ b/frontend/src/utils/date.ts
@@ -1,8 +1,10 @@
import {
addDays,
endOfToday,
+ format,
isAfter,
isBefore,
+ isDate,
parseISO,
startOfToday,
} from 'date-fns'
@@ -83,6 +85,16 @@ export const isIsoDateString = (value: unknown): value is JsonDate => {
)
}
+export const SHORT_ISO_DATE_FORMAT_REGEX = /^\d{4}-\d{2}-\d{2}$/
+
+export const isShortIsoDateString = (value: unknown): value is JsonDate => {
+ return (
+ typeof value === 'string' &&
+ SHORT_ISO_DATE_FORMAT_REGEX.test(value) &&
+ !isNaN(new Date(value).getTime())
+ )
+}
+
/**
* This function mutates given @param body, and transforms all ISO date strings
* in the body object to Date objects.
@@ -114,3 +126,17 @@ export const transformAllIsoStringsToDate = (body: T): T => {
mutableTransformAllIsoStringsToDate(body)
return body
}
+
+/** Transforms YYYY-MM-DD strings to date, otherwise null */
+export const transformShortIsoStringToDate = (
+ isoString: unknown,
+): Date | null => {
+ return isShortIsoDateString(isoString)
+ ? // Set to UTC time regardless.
+ parseISO(`${isoString}T00:00:00Z`)
+ : null
+}
+
+export const transformDateToShortIsoString = (date: unknown): string | null => {
+ return isDate(date) ? format(date as Date, 'yyyy-MM-dd') : null
+}