From f2fcd7d787093cd5d5ab2319c711acd5ea4d6c16 Mon Sep 17 00:00:00 2001 From: "skarawalsatishrao@hawk.iit.edu" Date: Sun, 25 Oct 2026 19:49:49 -0500 Subject: [PATCH] fix: add confirmation modal --- .../Collaboration/JobFormbuilder.jsx | 35 ++++++- .../JobFormBuilder.deletion.test.jsx | 95 +++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/components/Collaboration/__tests__/JobFormBuilder.deletion.test.jsx diff --git a/src/components/Collaboration/JobFormbuilder.jsx b/src/components/Collaboration/JobFormbuilder.jsx index 769d6f1ecd4..40d8451d765 100644 --- a/src/components/Collaboration/JobFormbuilder.jsx +++ b/src/components/Collaboration/JobFormbuilder.jsx @@ -2,6 +2,7 @@ /* eslint-disable no-console */ import { useState, useEffect, useMemo } from 'react'; import axios from 'axios'; +import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { useDispatch, useSelector } from 'react-redux'; import { Prompt } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; @@ -75,6 +76,7 @@ function JobFormBuilder() { const [editModalOpen, setEditModalOpen] = useState(false); const [editingQuestion, setEditingQuestion] = useState(null); const [editingIndex, setEditingIndex] = useState(null); + const [pendingDeleteIndex, setPendingDeleteIndex] = useState(null); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const markAsSaved = (fields, savedJobTitle) => { @@ -187,6 +189,7 @@ function JobFormBuilder() { }; const deleteField = async index => { + setPendingDeleteIndex(null); // Update local state immediately const newFields = [...formFields]; newFields.splice(index, 1); @@ -471,7 +474,7 @@ function JobFormBuilder() { index={index} totalFields={formFields.length} onMove={moveField} - onDelete={deleteField} + onDelete={setPendingDeleteIndex} onEdit={editField} visible={field.visible} onVisibilityChange={event => changeVisiblity(event, field)} @@ -675,6 +678,36 @@ function JobFormBuilder() { )} + setPendingDeleteIndex(null)} + labelledBy="delete-question-title" + contentClassName={darkMode ? 'bg-dark text-light' : ''} + returnFocusAfterClose + > + setPendingDeleteIndex(null)}> + Delete question? + + +

Are you sure you want to delete this question?

+

+ {pendingDeleteIndex !== null && + stripLeadingQuestionNumber(formFields[pendingDeleteIndex]?.questionText || '')} +

+

+ This will remove it from the active form and delete all of its choices. + {currentFormId && ' This change is saved immediately.'} +

+
+ + + + +
setShowPreviewModal(false)} diff --git a/src/components/Collaboration/__tests__/JobFormBuilder.deletion.test.jsx b/src/components/Collaboration/__tests__/JobFormBuilder.deletion.test.jsx new file mode 100644 index 00000000000..9962f0c64b7 --- /dev/null +++ b/src/components/Collaboration/__tests__/JobFormBuilder.deletion.test.jsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import axios from 'axios'; +import JobFormBuilder from '../JobFormbuilder'; +import { ENDPOINTS } from '~/utils/URL'; + +vi.mock('axios'); +vi.mock('../QuestionSetManager', () => ({ default: () => null })); +vi.mock('../QuestionEditModal', () => ({ default: () => null })); +vi.mock('../FormPreviewModal', () => ({ default: () => null })); + +const store = { + getState: () => ({ theme: { darkMode: false }, auth: { user: {} } }), + subscribe: () => () => {}, + dispatch: () => true, +}; + +const renderBuilder = () => + render( + + + + + , + ); + +const questionLabels = () => screen.getAllByText(/^\d+\.\) /).map(element => element.textContent); + +describe('question deletion confirmation', () => { + beforeEach(() => { + vi.clearAllMocks(); + axios.get.mockResolvedValue({ + data: { + forms: [ + { + _id: 'form-1', + title: 'Software Engineer', + questions: [ + { questionText: 'First question', questionType: 'textbox', visible: true }, + { + questionText: 'Choose a skill', + questionType: 'dropdown', + options: ['React', 'Node'], + visible: true, + }, + { questionText: 'Last question', questionType: 'textbox', visible: true }, + ], + }, + ], + }, + }); + axios.delete.mockResolvedValue({ data: {} }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('preserves the question, its choices and order when canceled', async () => { + renderBuilder(); + await screen.findByText(/Choose a skill/); + const before = questionLabels(); + + fireEvent.click(screen.getAllByTitle('Delete question')[1]); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(axios.delete).not.toHaveBeenCalled(); + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(axios.delete).not.toHaveBeenCalled(); + expect(questionLabels()).toEqual(before); + expect(screen.getByRole('option', { name: 'React' })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'Node' })).toBeInTheDocument(); + }); + + it('deletes only the selected question after confirmation', async () => { + renderBuilder(); + await screen.findByText(/Choose a skill/); + + fireEvent.click(screen.getAllByTitle('Delete question')[1]); + + expect(axios.delete).not.toHaveBeenCalled(); + fireEvent.click(screen.getByRole('button', { name: 'Delete question' })); + + await waitFor(() => expect(axios.delete).toHaveBeenCalledTimes(1)); + expect(axios.delete).toHaveBeenCalledWith( + ENDPOINTS.DELETE_QUESTION('form-1', 1), + expect.any(Object), + ); + expect(screen.queryByText(/Choose a skill/)).not.toBeInTheDocument(); + expect(questionLabels()).toEqual(['1.) First question', '2.) Last question']); + }); +});