diff --git a/app/components/SectionList/DeleteSectionDialog.tsx b/app/components/SectionList/DeleteSectionDialog.tsx new file mode 100644 index 0000000..4d050e6 --- /dev/null +++ b/app/components/SectionList/DeleteSectionDialog.tsx @@ -0,0 +1,76 @@ +import {StateDialog} from '@components/Dialog/StateDialog' +import {Loader} from '@components/Loader/Loader' +import {ICreateSectionResponse} from '@controllers/sections.controller' +import {useFetcher, useParams} from '@remix-run/react' +import {API} from '@route/utils/api' +import {Button} from '@ui/button' +import {DialogClose, DialogHeader, DialogTitle} from '@ui/dialog' +import {toast} from '@ui/use-toast' +import React, {useEffect} from 'react' + +export const DeleteSectionDialog = (param: { + state: boolean + setState: React.Dispatch> + sectionId: number + sectionData: ICreateSectionResponse[] + reloadSections: () => void +}) => { + const params = useParams() + const projectId = +(params['projectId'] ?? 0) + const deleteSectionFetcher = useFetcher() + + const deleteSectionButtonClicked = () => { + param.setState(false) + const data = { + projectId: projectId, + sectionId: param.sectionId, + } + + deleteSectionFetcher.submit(data, { + method: 'DELETE', + action: `/${API.DeleteSection}`, + encType: 'application/json', + }) + } + + useEffect(() => { + if (deleteSectionFetcher.data) { + deleteSectionFetcher.data?.data + ? toast({ + variant: 'success', + description: `Section deleted successfully`, + }) + : toast({ + variant: 'destructive', + description: + deleteSectionFetcher.data?.error ?? 'Error while deleting', + }) + param.reloadSections() + } + }, [deleteSectionFetcher.data]) + + if (deleteSectionFetcher.state === 'submitting') return + + return ( + + Delete Section + + } + contentComponent={ +
+ Are you sure you want to delete this section? +
+ } + footerComponent={ + + + + } + /> + ) +} diff --git a/app/components/SectionList/RenderSections.tsx b/app/components/SectionList/RenderSections.tsx index f269797..3e0edd6 100644 --- a/app/components/SectionList/RenderSections.tsx +++ b/app/components/SectionList/RenderSections.tsx @@ -3,7 +3,13 @@ import {Tooltip} from '@components/Tooltip/Tooltip' import {ICreateSectionResponse} from '@controllers/sections.controller' import {Checkbox} from '@ui/checkbox' import {cn} from '@ui/utils' -import {ChevronDown, ChevronRight, CirclePlus, Pencil} from 'lucide-react' +import { + ChevronDown, + ChevronRight, + CirclePlus, + Pencil, + Trash2, +} from 'lucide-react' import React, {memo, useCallback} from 'react' import {useParams} from 'react-router' import SectionSkeleton from './SectionSkeleton' @@ -24,6 +30,7 @@ interface IRenderSection { ) => void addSubsectionClicked: (sectionId: number | null) => void editSubsectionClicked: (sectionId: number) => void + deleteSubsectionClicked: (sectionId: number) => void } const RenderSections = memo( @@ -36,6 +43,7 @@ const RenderSections = memo( applySectionFilter, addSubsectionClicked, editSubsectionClicked, + deleteSubsectionClicked, sectionData, }: IRenderSection) => { sections = sections @@ -61,6 +69,7 @@ const RenderSections = memo( applySectionFilter={applySectionFilter} addSubsectionClicked={addSubsectionClicked} editSubsectionClicked={editSubsectionClicked} + deleteSubsectionClicked={deleteSubsectionClicked} sectionData={sectionData} /> @@ -75,6 +84,8 @@ const RenderSections = memo( selectedSections, applySectionFilter, addSubsectionClicked, + editSubsectionClicked, + deleteSubsectionClicked, ], ) @@ -154,31 +165,42 @@ const RenderSections = memo( /> {!runId && ( -
+
addSubsectionClicked(section.sectionId) } - size={16} /> } - content="Add SubSection" + content={
Add Sub-Section
} /> - { + size={14} + color="orange" + onClick={() => editSubsectionClicked(section.sectionId) - }} - color="#2d7071" + } + /> + } + content={
Edit Section
} + /> + + deleteSubsectionClicked(section.sectionId) + } /> } - content="Edit Section" + content={
Delete Section
} />
)} diff --git a/app/components/SectionList/SectionList.tsx b/app/components/SectionList/SectionList.tsx index df81acc..1386ab7 100644 --- a/app/components/SectionList/SectionList.tsx +++ b/app/components/SectionList/SectionList.tsx @@ -17,9 +17,11 @@ import {CirclePlus, ListRestart} from 'lucide-react' import React, {useEffect, useState} from 'react' import {API} from '~/routes/utilities/api' import {AddSectionDialog} from './AddSectionDialog' + import {EditSectionDialog} from './EditSectionDialog' import RenderSections from './RenderSections' import {SectionInfoBox} from './SectionInfoBox' +import {DeleteSectionDialog} from './DeleteSectionDialog' export const SectionList = () => { const [searchParams, setSearchParams] = useSearchParams([]) @@ -28,6 +30,7 @@ export const SectionList = () => { ) const [addSectionDialog, setAddSectionDialog] = useState(false) const [editSectionDialog, setEditSectionDialog] = useState(false) + const [deleteSectionDialog, setDeleteSectionDialog] = useState(false) const [sectionAPIData, setSectionAPIData] = useState< ICreateSectionResponse[] >([]) @@ -168,6 +171,11 @@ export const SectionList = () => { setEditSectionDialog(true) } + const deleteSubsectionClicked = (sectionId: number) => { + setSectionId(sectionId) + setDeleteSectionDialog(true) + } + const reloadSections = () => { sectionFetcher.load(`/${API.GetSections}?projectId=${projectId}`) } @@ -201,6 +209,7 @@ export const SectionList = () => { toggleSection={toggleSection} sectionData={sectionFetcher?.data?.data} editSubsectionClicked={editSubsectionClicked} + deleteSubsectionClicked={deleteSubsectionClicked} /> {!runId && (
) } diff --git a/app/dataController/sections.controller.ts b/app/dataController/sections.controller.ts index 7adde68..06c2f43 100644 --- a/app/dataController/sections.controller.ts +++ b/app/dataController/sections.controller.ts @@ -1,4 +1,5 @@ import SectionsDao from '~/db/dao/sections.dao' +import TestsDao from '~/db/dao/test.dao' export interface IGetSectionIdByHierarcy { sectionName: string @@ -63,6 +64,12 @@ export interface IEditSection { parentId?: number | null } +export interface IDeleteSection { + sectionId: number + projectId: number + userId: number +} + const SectionsController = { getAllSections: ( param: IGetAllSections, @@ -123,6 +130,22 @@ const SectionsController = { }, editSection: async (param: IEditSection) => SectionsDao.editSection(param), + + deleteSection: async (param: IDeleteSection) => { + const activeTests = await TestsDao.getTestsCount({ + projectId: param.projectId, + sectionIds: [param.sectionId], + status: 'Active', + }) + + if (activeTests && activeTests.count > 0) { + throw new Error( + `Section has ${activeTests.count} active test(s) associated with it, cannot delete section`, + ) + } + + return SectionsDao.deleteSection(param) + }, } export default SectionsController diff --git a/app/db/dao/sections.dao.ts b/app/db/dao/sections.dao.ts index d29aeaf..86a087d 100644 --- a/app/db/dao/sections.dao.ts +++ b/app/db/dao/sections.dao.ts @@ -14,7 +14,7 @@ import {errorHandling} from './utils' const SectionsDao = { getAllSections: async ({projectId, runId}: IGetAllSections) => { try { - const whereClauses = [] + const whereClauses = [eq(sections.status, 'Active')] if (projectId) { whereClauses.push(eq(sections.projectId, projectId)) @@ -208,6 +208,35 @@ const SectionsDao = { errorHandling(error) } }, + deleteSection: async ({ + sectionId, + projectId, + userId, + }: { + sectionId: number + projectId: number + userId: number + }) => { + try { + const data = await dbClient + .update(sections) + .set({status: 'Deleted', updatedBy: userId}) + .where( + and( + eq(sections.sectionId, sectionId), + eq(sections.projectId, projectId), + ), + ) + return data + } catch (error: any) { + logger({ + type: LogType.SQL_ERROR, + tag: 'Error while deleting section', + message: error, + }) + errorHandling(error) + } + }, } export default SectionsDao diff --git a/app/db/schema/tests.ts b/app/db/schema/tests.ts index 1cf9c8d..1118e69 100644 --- a/app/db/schema/tests.ts +++ b/app/db/schema/tests.ts @@ -58,6 +58,9 @@ export const sections = mysqlTable( updatedBy: int('updatedBy').references(() => users.userId, { onDelete: 'set null', }), + status: mysqlEnum('status', ['Active', 'Deleted']) + .default('Active') + .notNull(), }, (sections) => { return { diff --git a/app/routes/api/v1/deleteSection.ts b/app/routes/api/v1/deleteSection.ts new file mode 100644 index 0000000..bc8a52f --- /dev/null +++ b/app/routes/api/v1/deleteSection.ts @@ -0,0 +1,50 @@ +import SectionsController from '@controllers/sections.controller' +import {ActionFunctionArgs} from '@remix-run/node' +import {z} from 'zod' +import {API} from '~/routes/utilities/api' +import {getUserAndCheckAccess} from '~/routes/utilities/checkForUserAndAccess' +import { + errorResponseHandler, + responseHandler, +} from '~/routes/utilities/responseHandler' +import {getRequestParams} from '../../utilities/utils' + +const DeleteSectionRequestSchema = z.object({ + sectionId: z.number().gt(0, 'Valid SectionId is required'), + projectId: z.number().gt(0, 'Valid ProjectId is required'), +}) + +type DeleteSectionRequestAPIType = z.infer + +export const action = async ({request}: ActionFunctionArgs) => { + try { + const user = await getUserAndCheckAccess({ + request, + resource: API.DeleteSection, + }) + const data = await getRequestParams( + request, + DeleteSectionRequestSchema, + ) + + const deleteSectionData: any = await SectionsController.deleteSection({ + ...data, + userId: user?.userId ?? 0, + }) + + if (deleteSectionData[0]?.affectedRows === 0) { + return responseHandler({ + error: `No section found for sectionId: ${data.sectionId}`, + status: 400, + }) + } + return responseHandler({ + data: { + message: `SectionId: ${data.sectionId} deleted successfully`, + }, + status: 200, + }) + } catch (error: any) { + return errorResponseHandler(error) + } +} diff --git a/app/routes/utilities/api.ts b/app/routes/utilities/api.ts index 6bf35ad..0ed035c 100644 --- a/app/routes/utilities/api.ts +++ b/app/routes/utilities/api.ts @@ -61,6 +61,7 @@ export enum API { UpdateUserRole = 'api/v1/user/update-role', AddSection = 'api/v1/project/add-section', EditSection = 'api/v1/project/edit-section', + DeleteSection = 'api/v1/project/delete-section', } export const API_RESOLUTION_PATHS = { @@ -113,6 +114,7 @@ export const API_RESOLUTION_PATHS = { [API.UpdateUserRole]: 'routes/api/v1/updateUserType.ts', [API.AddSection]: 'routes/api/v1/addSection.ts', [API.EditSection]: 'routes/api/v1/editSection.ts', + [API.DeleteSection]: 'routes/api/v1/deleteSection.ts', } export const CLOSED_API = { @@ -171,4 +173,5 @@ export const ApiToTypeMap: { [API.UpdateUserRole]: ApiTypes.PUT, [API.AddSection]: ApiTypes.POST, [API.EditSection]: ApiTypes.PUT, + [API.DeleteSection]: ApiTypes.DELETE, }