diff --git a/CHANGELOG.md b/CHANGELOG.md index 436ca101e9..d414f609f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,30 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v6.168.0](https://github.com/opengovsg/FormSG/compare/v6.168.0...v6.168.0) + +- fix: search fields drag and drop mismatch [`#7972`](https://github.com/opengovsg/FormSG/pull/7972) + +#### [v6.168.0](https://github.com/opengovsg/FormSG/compare/v6.167.0...v6.168.0) + +> 3 December 2024 + +- build: merge release v6.167.0 to develop [`#7964`](https://github.com/opengovsg/FormSG/pull/7964) +- feat: implement field search functionality [`#7958`](https://github.com/opengovsg/FormSG/pull/7958) +- feat(mrf be validation): enable mrf response validation hard block [`#7963`](https://github.com/opengovsg/FormSG/pull/7963) +- build: release v6.167.0 [`#7962`](https://github.com/opengovsg/FormSG/pull/7962) +- chore: bump version to v6.168.0 [`c69a28b`](https://github.com/opengovsg/FormSG/commit/c69a28b58d1e7f3f8faeb58624f408b830d115f7) + #### [v6.167.0](https://github.com/opengovsg/FormSG/compare/v6.166.1...v6.167.0) +> 2 December 2024 + - build: merge release v6.166.1 to develop [`#7961`](https://github.com/opengovsg/FormSG/pull/7961) - feat(twilio): remove twilio be [`#7870`](https://github.com/opengovsg/FormSG/pull/7870) - feat: update form create modal response options [`#7957`](https://github.com/opengovsg/FormSG/pull/7957) - chore: include latest features in whats new page [`#7956`](https://github.com/opengovsg/FormSG/pull/7956) - build: release v6.166.1 [`#7950`](https://github.com/opengovsg/FormSG/pull/7950) +- chore: bump version to v6.167.0 [`9f97c52`](https://github.com/opengovsg/FormSG/commit/9f97c52fa05aa990beb143139ef36b027e0b3874) #### [v6.166.1](https://github.com/opengovsg/FormSG/compare/v6.166.0...v6.166.1) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c5a9c15a06..d16ed630fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "form-frontend", - "version": "6.167.0", + "version": "6.168.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "form-frontend", - "version": "6.167.0", + "version": "6.168.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^2.8.2", diff --git a/frontend/package.json b/frontend/package.json index 683f7293be..3855cd6773 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "form-frontend", - "version": "6.167.0", + "version": "6.168.0", "homepage": ".", "type": "module", "private": true, diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListDrawer.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListDrawer.tsx index 1a767c7924..447887f94a 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListDrawer.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListDrawer.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Box, @@ -23,11 +24,13 @@ import { MyInfoFieldPanel, PaymentsInputPanel, } from './field-panels' +import { FieldSearchBar } from './FieldSearchBar' export const FieldListDrawer = (): JSX.Element => { const { t } = useTranslation() const { fieldListTabIndex, setFieldListTabIndex } = useCreatePageSidebar() const { isLoading } = useCreateTabForm() + const [searchValue, setSearchValue] = useState('') const tabsDataList = [ { @@ -51,7 +54,12 @@ export const FieldListDrawer = (): JSX.Element => { isDisabled: isLoading, key: FieldListTabIndex.Payments, }, - ].filter((tab) => !tab.isHidden) + ].filter((tab) => !tab.isHidden) as { + header: string + component: (props: { searchValue?: string }) => JSX.Element + isDisabled: boolean + key: FieldListTabIndex + }[] return ( { - + setSearchValue(e.target.value)} + /> + {tabsDataList.map((tab) => ( {tab.header} @@ -82,7 +94,7 @@ export const FieldListDrawer = (): JSX.Element => { {tabsDataList.map((tab) => ( - + ))} diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldSearchBar.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldSearchBar.tsx new file mode 100644 index 0000000000..cffb6c6b14 --- /dev/null +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldSearchBar.tsx @@ -0,0 +1,21 @@ +import { BiSearch } from 'react-icons/bi' +import { Icon, Input, InputGroup, InputLeftElement } from '@chakra-ui/react' + +export const FieldSearchBar = ({ + searchValue, + onChange, +}: { + searchValue: string + onChange: (e: React.ChangeEvent) => void +}) => ( + + + + + + +) diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/BasicFieldPanel.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/BasicFieldPanel.tsx index be4cc78c6c..a4ee0087c5 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/BasicFieldPanel.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/BasicFieldPanel.tsx @@ -5,32 +5,41 @@ import { BASIC_FIELDS_ORDERED, CREATE_FIELD_DROP_ID, } from '~features/admin-form/create/builder-and-design/constants' +import { BASICFIELD_TO_DRAWER_META } from '~features/admin-form/create/constants' import { useCreateTabForm } from '../../../../builder-and-design/useCreateTabForm' import { DraggableBasicFieldListOption } from '../FieldListOption' import { FieldSection } from './FieldSection' +import { filterFieldsBySearchValue } from './utils' -export const BasicFieldPanel = () => { +export const BasicFieldPanel = ({ searchValue }: { searchValue: string }) => { const { isLoading } = useCreateTabForm() + const filteredCreateBasicFields = filterFieldsBySearchValue( + searchValue, + BASIC_FIELDS_ORDERED, + BASICFIELD_TO_DRAWER_META, + ) + return ( {(provided) => ( - {BASIC_FIELDS_ORDERED.map((fieldType, index) => { - const shouldDisableField = isLoading - - return ( - - ) - })} + + {filteredCreateBasicFields.map(({ fieldType, originalIndex }) => { + const shouldDisableField = isLoading + return ( + + ) + })} + {provided.placeholder} diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx index f45cf447fd..ed86c2db7c 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx @@ -28,6 +28,7 @@ import { CREATE_MYINFO_PERSONAL_DROP_ID, CREATE_MYINFO_PERSONAL_FIELDS_ORDERED, } from '~features/admin-form/create/builder-and-design/constants' +import { MYINFO_FIELD_TO_DRAWER_META } from '~features/admin-form/create/constants' import { isMyInfo } from '~features/myinfo/utils' import { useUser } from '~features/user/queries' @@ -35,6 +36,7 @@ import { useCreateTabForm } from '../../../../builder-and-design/useCreateTabFor import { DraggableMyInfoFieldListOption } from '../FieldListOption' import { FieldSection } from './FieldSection' +import { filterFieldsBySearchValue } from './utils' const SGID_SUPPORTED_V1 = [ MyInfoAttribute.Name, @@ -70,7 +72,7 @@ const SGID_SUPPORTED_V2 = [ MyInfoAttribute.DivorceDate, ] -export const MyInfoFieldPanel = () => { +export const MyInfoFieldPanel = ({ searchValue }: { searchValue: string }) => { const { data: form, isLoading } = useCreateTabForm() const { user } = useUser() @@ -125,22 +127,56 @@ export const MyInfoFieldPanel = () => { [form, isDisabled, sgIDUnSupported], ) + const filteredCreateMyInfoPersonalFields = filterFieldsBySearchValue( + searchValue, + CREATE_MYINFO_PERSONAL_FIELDS_ORDERED, + MYINFO_FIELD_TO_DRAWER_META, + ) + + const filteredCreateMyInfoContactFields = filterFieldsBySearchValue( + searchValue, + CREATE_MYINFO_CONTACT_FIELDS_ORDERED, + MYINFO_FIELD_TO_DRAWER_META, + ) + + const filteredCreateMyInfoParticularsFields = filterFieldsBySearchValue( + searchValue, + CREATE_MYINFO_PARTICULARS_FIELDS_ORDERED, + MYINFO_FIELD_TO_DRAWER_META, + ) + + const filteredCreateMyInfoMarriageFields = filterFieldsBySearchValue( + searchValue, + CREATE_MYINFO_MARRIAGE_FIELDS_ORDERED, + MYINFO_FIELD_TO_DRAWER_META, + ) + + const filteredCreateMyInfoChildrenFields = filterFieldsBySearchValue( + searchValue, + CREATE_MYINFO_CHILDREN_FIELDS_ORDERED, + MYINFO_FIELD_TO_DRAWER_META, + ) + return ( <> {(provided) => ( - - {CREATE_MYINFO_PERSONAL_FIELDS_ORDERED.map((fieldType, index) => ( - - ))} - + {filteredCreateMyInfoPersonalFields.length > 0 && ( + + {filteredCreateMyInfoPersonalFields.map( + ({ fieldType, originalIndex }) => ( + + ), + )} + + )} {provided.placeholder} )} @@ -148,16 +184,20 @@ export const MyInfoFieldPanel = () => { {(provided) => ( - - {CREATE_MYINFO_CONTACT_FIELDS_ORDERED.map((fieldType, index) => ( - - ))} - + {filteredCreateMyInfoContactFields.length > 0 && ( + + {filteredCreateMyInfoContactFields.map( + ({ fieldType, originalIndex }) => ( + + ), + )} + + )} {provided.placeholder} )} @@ -165,18 +205,20 @@ export const MyInfoFieldPanel = () => { {(provided) => ( - - {CREATE_MYINFO_PARTICULARS_FIELDS_ORDERED.map( - (fieldType, index) => ( - - ), - )} - + {filteredCreateMyInfoParticularsFields.length > 0 && ( + + {filteredCreateMyInfoParticularsFields.map( + ({ fieldType, originalIndex }) => ( + + ), + )} + + )} {provided.placeholder} )} @@ -184,16 +226,20 @@ export const MyInfoFieldPanel = () => { {(provided) => ( - - {CREATE_MYINFO_MARRIAGE_FIELDS_ORDERED.map((fieldType, index) => ( - - ))} - + {filteredCreateMyInfoMarriageFields.length > 0 && ( + + {filteredCreateMyInfoMarriageFields.map( + ({ fieldType, originalIndex }) => ( + + ), + )} + + )} {provided.placeholder} )} @@ -203,18 +249,20 @@ export const MyInfoFieldPanel = () => { {(provided) => ( - - {CREATE_MYINFO_CHILDREN_FIELDS_ORDERED.map( - (fieldType, index) => ( - - ), - )} - + {filteredCreateMyInfoChildrenFields.length > 0 && ( + + {filteredCreateMyInfoChildrenFields.map( + ({ fieldType, originalIndex }) => ( + + ), + )} + + )} {provided.placeholder} )} diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/utils.ts b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/utils.ts new file mode 100644 index 0000000000..9881b42ff7 --- /dev/null +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/utils.ts @@ -0,0 +1,45 @@ +import { BasicField, MyInfoAttribute } from '~shared/types' + +import { BuilderSidebarFieldMeta } from '~features/admin-form/create/constants' + +const checkSearchValueMatchesFieldMeta = ( + searchValue: string, + fieldMeta: BuilderSidebarFieldMeta, +): boolean => { + const lowerCaseSearchValue = searchValue.toLowerCase() + return !!( + fieldMeta.label.toLowerCase().includes(lowerCaseSearchValue) || + fieldMeta.searchAliases?.some((searchAlias) => + searchAlias.toLowerCase().includes(lowerCaseSearchValue), + ) + ) +} + +/** + * Filter field types by search value. + * @param searchValue - The search value to filter fields by + * @param orderedFields - The ordered fields to filter + * @param fieldsMeta - The fields meta to filter by + * @returns The filtered fields and their original indices based on the ordered fields. + * which follow the shape { fieldType: T; originalIndex: number representing the index of the field in the orderedFields array }[] + * NOTE: The originalIndex is necessary for the draggable component to maintain the order of the fields when dropped into the BuilderAndDesignTab. + */ +export const filterFieldsBySearchValue = < + T extends BasicField | MyInfoAttribute, +>( + searchValue: string, + orderedFields: T[], + fieldsMeta: { + [key in T]: BuilderSidebarFieldMeta + }, +): { fieldType: T; originalIndex: number }[] => { + return orderedFields + .map((fieldType, originalIndex) => ({ + fieldType, + originalIndex, + })) + .filter(({ fieldType }) => { + const fieldMeta = fieldsMeta[fieldType] + return checkSearchValueMatchesFieldMeta(searchValue, fieldMeta) + }) +} diff --git a/frontend/src/features/admin-form/create/constants.ts b/frontend/src/features/admin-form/create/constants.ts index 8ceca71882..61b25c6783 100644 --- a/frontend/src/features/admin-form/create/constants.ts +++ b/frontend/src/features/admin-form/create/constants.ts @@ -47,11 +47,12 @@ import { As } from '@chakra-ui/react' import { BasicField, MyInfoAttribute } from '~shared/types/field' -type BuilderSidebarFieldMeta = { +export type BuilderSidebarFieldMeta = { label: string icon: As // Is this fieldType included in submissions? isSubmitted: boolean + searchAliases?: string[] } // !!! Do not use this to reference field titles for MyInfo fields. !!! @@ -63,126 +64,201 @@ export const BASICFIELD_TO_DRAWER_META: { label: 'Image', icon: BiImage, isSubmitted: false, + searchAliases: ['photo', 'picture'], }, [BasicField.Statement]: { label: 'Paragraph', icon: BiText, isSubmitted: false, + searchAliases: ['description'], }, [BasicField.Section]: { label: 'Heading', icon: BiHeading, isSubmitted: false, + searchAliases: ['header', 'title', 'section'], }, [BasicField.Attachment]: { label: 'Attachment', icon: BiCloudUpload, isSubmitted: true, + searchAliases: ['supporting', 'screenshot', 'document', 'file', 'upload'], }, [BasicField.Checkbox]: { label: 'Checkbox', icon: BiSelectMultiple, isSubmitted: true, + searchAliases: [ + 'choice', + 'options', + 'multiple', + 'declaration', + 'acknowledgement', + ], }, [BasicField.Date]: { label: 'Date', icon: BiCalendarEvent, isSubmitted: true, + searchAliases: [ + 'birthdate', + 'dob', + 'date of birth', + 'event date', + 'start date', + 'end date', + 'time', + ], }, [BasicField.Decimal]: { label: 'Decimal', icon: BiCalculator, isSubmitted: true, + searchAliases: ['price', 'amount', 'cost'], }, [BasicField.Dropdown]: { label: 'Dropdown', icon: BiCaretDownSquare, isSubmitted: true, + searchAliases: ['choice', 'options', 'category', 'type', 'status'], }, [BasicField.CountryRegion]: { label: 'Country/Region', icon: BiFlag, isSubmitted: true, + searchAliases: ['country', 'region', 'location', 'nationality'], }, [BasicField.Email]: { label: 'Email', icon: BiMailSend, isSubmitted: true, + searchAliases: ['contact'], }, [BasicField.HomeNo]: { label: 'Home number', icon: BiPhone, isSubmitted: true, + searchAliases: ['phone', 'contact', 'telephone'], }, [BasicField.LongText]: { label: 'Long answer', icon: BiAlignLeft, isSubmitted: true, + searchAliases: [ + 'text', + 'description', + 'comments', + 'remarks', + 'feedback', + 'notes', + 'details', + 'explanation', + 'paragraph', + ], }, [BasicField.Mobile]: { label: 'Mobile number', icon: BiMobile, isSubmitted: true, + searchAliases: ['phone', 'contact', 'telephone', 'sms'], }, [BasicField.Nric]: { label: 'NRIC/FIN', icon: BiUser, isSubmitted: true, + searchAliases: [ + 'id', + 'identification', + 'national', + 'singpass', + 'ic number', + ], }, [BasicField.Number]: { label: 'Number', icon: BiHash, isSubmitted: true, + searchAliases: ['age', 'quantity', 'count'], }, [BasicField.Radio]: { label: 'Radio', icon: BiRadioCircleMarked, isSubmitted: true, + searchAliases: ['choice', 'options', 'mcq', 'multiple'], }, [BasicField.Rating]: { label: 'Rating', icon: BiStar, isSubmitted: true, + searchAliases: ['satisfaction', 'quality', 'performance'], }, [BasicField.ShortText]: { label: 'Short answer', icon: BiRename, isSubmitted: true, + searchAliases: ['name', 'text'], }, [BasicField.Table]: { label: 'Table', icon: BiTable, isSubmitted: true, + searchAliases: [ + 'grid', + 'spreadsheet', + 'list', + 'collection', + 'entries', + 'records', + 'items', + 'multiple', + ], }, [BasicField.Uen]: { label: 'UEN', icon: BiBuilding, isSubmitted: true, + searchAliases: [ + 'id', + 'business', + 'company registration', + 'organization', + 'corporation', + 'unique entity', + 'number', + ], }, [BasicField.YesNo]: { label: 'Yes/No', icon: BiToggleLeft, isSubmitted: true, + searchAliases: [ + 'consent', + 'agreement', + 'confirmation', + 'approve', + 'approval', + 'accept', + ], }, [BasicField.Children]: { @@ -206,6 +282,7 @@ export const MYINFO_FIELD_TO_DRAWER_META: { label: 'Sex', icon: BiInfinite, isSubmitted: true, + searchAliases: ['gender'], }, [MyInfoAttribute.DateOfBirth]: { label: 'Date of Birth', diff --git a/package-lock.json b/package-lock.json index 56d6a9aef0..fcee8bc948 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "FormSG", - "version": "6.167.0", + "version": "6.168.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "FormSG", - "version": "6.167.0", + "version": "6.168.0", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.536.0", diff --git a/package.json b/package.json index e11a1a2157..39dd0e51df 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "6.167.0", + "version": "6.168.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG " diff --git a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts index 728f5b6841..bc1f243a13 100644 --- a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts +++ b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts @@ -531,16 +531,13 @@ export const validateMultirespondentSubmission = async ( }) }) .andThen((previousResponses) => { - // TODO: (FRM-1688) Set to block after sure that validation logic works as expected. - validateMrfFieldResponses({ + return validateMrfFieldResponses({ formId, visibleFieldIds, formFields: form_fields, responses: req.body.responses, previousResponses, }) - - return ok(req.body.responses) }), ) }, diff --git a/src/app/modules/submission/submission.utils.ts b/src/app/modules/submission/submission.utils.ts index 24bc51ee6a..495fc5f91d 100644 --- a/src/app/modules/submission/submission.utils.ts +++ b/src/app/modules/submission/submission.utils.ts @@ -135,6 +135,7 @@ import { SubmissionSaveError, UnsupportedSettingsError, ValidateFieldError, + ValidateFieldErrorV3, VirusScanFailedError, } from './submission.errors' import { @@ -309,6 +310,7 @@ const errorMapper: MapRouteError = ( 'Submission too large to be saved. Please reduce the size of your submission and try again.', } case ValidateFieldError: + case ValidateFieldErrorV3: case DatabaseValidationError: case InvalidFileExtensionError: case AttachmentTooLargeError: