Skip to content

Commit

Permalink
feat: add delete functionality for deny
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin9foong committed Dec 24, 2024
1 parent 23f0581 commit 9af405a
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { FormErrorMessage } from '~components/FormControl/FormErrorMessage/FormE

import { useAssistanceMutations } from '~features/admin-form/assistance/mutations'

import { useMagicFormBuilderStore } from '../../useMagicFormBuilderStore'
import { useDeleteFormField } from '../../mutations/useDeleteFormField'
import {
recentlyCreatedFieldIdsSelector,
useMagicFormBuilderStore,
} from '../../useMagicFormBuilderStore'

const GENERATE_FORM_PLACEHOLDER =
'Describe form, fields and sections to create...'
Expand Down Expand Up @@ -176,13 +180,18 @@ const MagicFormBuilderPopover = ({
const clearRecentlyCreatedFieldIds = useMagicFormBuilderStore(
(state) => state.clearRecentlyCreatedFieldIds,
)
const recentlyCreatedFieldIds = useMagicFormBuilderStore(
recentlyCreatedFieldIdsSelector,
)

const onClickDefaults = () => {
clearRecentlyCreatedFieldIds()
setIsOpen(false)
setTimeout(() => setIsAcceptDenyOpen(false), 100) // delay to allow popover to close before updating state
}

const { deleteMultipleFormFieldsMutation } = useDeleteFormField()

return (
<Popover isLazy placement="right" isOpen={isOpen}>
<PopoverAnchor>
Expand All @@ -200,7 +209,9 @@ const MagicFormBuilderPopover = ({
<MagicFormBuilderAcceptDeny
onAccept={onClickDefaults}
onDeny={() => {
// trigger deletion of fields with field ids in recentlyCreatedFieldIds
deleteMultipleFormFieldsMutation.mutate(
Array.from(recentlyCreatedFieldIds),
)
onClickDefaults()
}}
onClose={() => setIsOpen(false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,15 @@ export const deleteSingleFormField = async ({
}): Promise<void> => {
return ApiService.delete(`${ADMIN_FORM_ENDPOINT}/${formId}/fields/${fieldId}`)
}

export const deleteMultipleFormFields = async ({
formId,
fieldIds,
}: {
formId: string
fieldIds: string[]
}): Promise<void> => {
return ApiService.post(`${ADMIN_FORM_ENDPOINT}/${formId}/fields/delete`, {
fieldIds,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import {
stateSelector,
usePaymentStore,
} from '../BuilderAndDesignDrawer/FieldListDrawer/field-panels/usePaymentStore'
import { deleteSingleFormField } from '../UpdateFormFieldService'
import {
deleteMultipleFormFields,
deleteSingleFormField,
} from '../UpdateFormFieldService'
import {
FieldBuilderState,
setToInactiveSelector,
Expand Down Expand Up @@ -137,6 +140,38 @@ export const useDeleteFormField = () => {
},
)

const handleDeleteMultipleFieldsSuccess = useCallback(
(_data: unknown, fieldIds: string[]) => {
queryClient.setQueryData<AdminFormDto>(adminFormKey, (oldForm) => {
// Should not happen, should not be able to update field if there is no
// existing data.
if (!oldForm) throw new Error('Query should have been set')
const deletedFieldIndices = fieldIds
.map((fieldId) =>
oldForm.form_fields.findIndex((ff) => ff._id === fieldId),
)
.filter((index) => index >= 0)
oldForm.form_fields = oldForm.form_fields.filter(
(_field, index) => !deletedFieldIndices.includes(index),
)
return oldForm
})
setToInactive()
},
[adminFormKey, queryClient, setToInactive],
)

const handleDeleteMultipleFieldsError = useCallback(
(error: Error) => {
toast.closeAll()
toast({
description: error.message,
status: 'danger',
})
},
[toast],
)

return {
deleteFieldMutation: useMutation(
(fieldId: string) =>
Expand All @@ -149,6 +184,17 @@ export const useDeleteFormField = () => {
onError: handleError,
},
),
deleteMultipleFormFieldsMutation: useMutation(
(fieldIds: string[]) =>
deleteMultipleFormFields({
formId,
fieldIds,
}),
{
onSuccess: handleDeleteMultipleFieldsSuccess,
onError: handleDeleteMultipleFieldsError,
},
),
deletePaymentFieldMutation,
}
}
11 changes: 11 additions & 0 deletions src/app/models/form.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,17 @@ const compileFormModel = (db: Mongoose): IFormModel => {
).exec()
}

FormSchema.statics.deleteFormFieldsByIds = async function (
formId: string,
fieldIds: string[],
): Promise<IFormSchema | null> {
return this.findByIdAndUpdate(
formId,
{ $pull: { form_fields: { _id: { $in: fieldIds } } } },
{ new: true, runValidators: true },
).exec()
}

// Updates specified form logic.
FormSchema.statics.updateFormLogic = async function (
formId: string,
Expand Down
52 changes: 52 additions & 0 deletions src/app/modules/form/admin-form/admin-form.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2785,6 +2785,58 @@ export const handleDeleteFormField: ControllerHandler<
)
}

/**
* Handler for PATCH /forms/:formId/fields/
* @security session
*
* @returns 204 when deletion is successful
* @returns 403 when current user does not have permissions to delete form fields
* @returns 404 when form cannot be found
* @returns 410 when deleting fields of an archived form
* @returns 422 when user in session cannot be retrieved from the database
* @returns 500 when database error occurs during deletion
*/
export const handleDeleteFormFields: ControllerHandler<
{ formId: string },
{ message: string } | ErrorDto,
{ fieldIds: string[] }
> = (req, res) => {
const { formId } = req.params
const { fieldIds } = req.body
const sessionUserId = (req.session as AuthedSessionData).user._id

return (
// Step 1: Retrieve currently logged in user.
UserService.getPopulatedUserById(sessionUserId)
.andThen((user) =>
// Step 2: Retrieve form with write permission check.
AuthService.getFormAfterPermissionChecks({
user,
formId,
level: PermissionLevel.Write,
}),
)
// Step 3: Delete form fields.
.andThen((form) => AdminFormService.deleteFormFields(form, fieldIds))
.map(() => res.sendStatus(StatusCodes.NO_CONTENT))
.mapErr((error) => {
logger.error({
message: 'Error occurred when deleting form fields',
meta: {
action: 'handleDeleteFormFields',
...createReqMeta(req),
userId: sessionUserId,
formId,
fieldIds,
},
error,
})
const { errorMessage, statusCode } = mapRouteError(error)
return res.status(statusCode).json({ message: errorMessage })
})
)
}

/**
* NOTE: Exported for testing.
* Private handler for PUT /forms/:formId/end-page
Expand Down
37 changes: 37 additions & 0 deletions src/app/modules/form/admin-form/admin-form.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2157,6 +2157,43 @@ export const deleteFormField = <T extends IFormSchema>(
})
}

/**
* Deletes multiple form fields from the given form by their ids.
* If any of the fieldIds does not exist, it will be ignored.
* @param form The form to delete the specified form fields for
* @param fieldIds the ids of the form fields to delete
* @returns ok(updated form) on success
* @returns err(PossibleDatabaseError) if db error is thrown during the deletion of form fields
* @returns err(FormNotFoundError) if the form cannot be found
*/
export const deleteFormFields = (
form: IPopulatedForm,
fieldIds: string[],
): ResultAsync<IFormSchema, PossibleDatabaseError | FormNotFoundError> => {
const logMeta = {
action: 'deleteFormFields',
formId: form._id,
fieldIds,
}

return ResultAsync.fromPromise(
FormModel.deleteFormFieldsByIds(form._id, fieldIds),
(error) => {
logger.error({
message: 'Error occurred when deleting form fields',
meta: logMeta,
error,
})
return transformMongoError(error)
},
).andThen((updatedForm) => {
if (!updatedForm) {
return errAsync(new FormNotFoundError())
}
return okAsync(updatedForm)
})
}

/**
* Update the end page of the given form
* @param formId the id of the form to update the end page for
Expand Down
17 changes: 17 additions & 0 deletions src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,23 @@ AdminFormsFormRouter.put(
AdminFormController.handleUpdateOptionsToRecipientsMap,
)

/**
* Deletes multiple form fields from the specified form.
* Uses POST for bulk deletion. See: https://stackoverflow.com/questions/21863326/delete-multiple-records-using-rest
* @security session
*
* @returns 204 when deletion is successful
* @returns 403 when current user does not have permissions to delete form fields
* @returns 404 when form cannot be found
* @returns 410 when deleting fields of an archived form
* @returns 422 when user in session cannot be retrieved from the database
* @returns 500 when database error occurs during deletion
*/
AdminFormsFormRouter.post(
'/:formId([a-fA-F0-9]{24})/fields/delete',
AdminFormController.handleDeleteFormFields,
)

/**
* Duplicates the form field with the fieldId from the specified form
* @security session
Expand Down
5 changes: 5 additions & 0 deletions src/types/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@ export interface IFormModel extends Model<IFormSchema> {
fieldId: string,
): Promise<IFormSchema | null>

deleteFormFieldsByIds(
formId: string,
fieldIds: string[],
): Promise<IFormSchema | null>

deactivateById(formId: string): Promise<IFormSchema | null>

getMetaByUserIdOrEmail(
Expand Down

0 comments on commit 9af405a

Please sign in to comment.