diff --git a/src/components/DataTable/ActionButton/index.tsx b/src/components/DataTable/ActionButton/index.tsx index 096c391a..a6b3351f 100644 --- a/src/components/DataTable/ActionButton/index.tsx +++ b/src/components/DataTable/ActionButton/index.tsx @@ -24,6 +24,7 @@ export function ActionButton({ color="gray.600" onClick={action} isDisabled={disabled} + aria-label={label} > {icon} diff --git a/src/pages/Stages/EditionModal/index.tsx b/src/pages/Stages/EditionModal/index.tsx new file mode 100644 index 00000000..8a5c7be5 --- /dev/null +++ b/src/pages/Stages/EditionModal/index.tsx @@ -0,0 +1,130 @@ +import { useEffect } from "react"; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + chakra, + Button, + useToast, +} from "@chakra-ui/react"; +import * as yup from "yup"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; + +import { updateStage } from "services/stages"; +import { Input } from "components/FormFields"; +import { useLoading } from "hooks/useLoading"; + +type FormValues = { + name: string; + duration: number; +}; + +const validationSchema = yup.object({ + name: yup.string().required("Dê um nome à etapa"), + duration: yup.number().required().typeError("Dê uma duração para esta etapa"), +}); + +interface EditionModalProps { + stage: Stage; + isOpen: boolean; + onClose: () => void; + afterSubmission: () => void; +} + +export function EditionModal({ + stage, + isOpen, + onClose, + afterSubmission, +}: EditionModalProps) { + const toast = useToast(); + const { handleLoading } = useLoading(); + + const { + register, + handleSubmit, + formState: { errors }, + reset, + } = useForm({ + resolver: yupResolver(validationSchema), + reValidateMode: "onChange", + }); + + const onSubmit = handleSubmit(async (formData) => { + handleLoading(true); + const res = await updateStage({ + ...formData, + idStage: stage.idStage, + }); + + onClose(); + afterSubmission(); + + if (res.type === "success") { + handleLoading(false); + + toast({ + id: "edit-stage-success", + title: "Sucesso!", + description: "A etapa foi editada.", + status: "success", + }); + return; + } + + handleLoading(false); + toast({ + id: "edit-stage-error", + title: "Erro ao editar etapa", + description: res.error?.message, + status: "error", + isClosable: true, + }); + }); + + useEffect(() => { + reset(); + }, [isOpen]); + + return ( + + + + Editar etapa + + + + + + + + + + + + + + ); +} diff --git a/src/pages/Stages/index.tsx b/src/pages/Stages/index.tsx index 1879e0c1..55beda31 100644 --- a/src/pages/Stages/index.tsx +++ b/src/pages/Stages/index.tsx @@ -9,7 +9,7 @@ import { chakra, } from "@chakra-ui/react"; import { AddIcon, Icon, SearchIcon } from "@chakra-ui/icons"; -import { MdDelete } from "react-icons/md"; +import { MdDelete, MdEdit } from "react-icons/md"; import { createColumnHelper } from "@tanstack/react-table"; import { useAuth } from "hooks/useAuth"; import { PrivateLayout } from "layouts/Private"; @@ -20,6 +20,7 @@ import { Pagination } from "components/Pagination"; import { getStages } from "../../services/stages"; import { CreationModal } from "./CreationModal"; import { DeletionModal } from "./DeletionModal"; +import { EditionModal } from "./EditionModal"; function Stages() { const toast = useToast(); @@ -36,6 +37,11 @@ function Stages() { onOpen: onDeletionOpen, onClose: onDeletionClose, } = useDisclosure(); + const { + isOpen: isEditionOpen, + onOpen: onEditionOpen, + onClose: onEditionClose, + } = useDisclosure(); const [currentPage, setCurrentPage] = useState(0); const handlePageChange = (selectedPage: { selected: number }) => { setCurrentPage(selectedPage.selected); @@ -74,6 +80,19 @@ function Stages() { const tableActions = useMemo( () => [ + { + label: "Editar Etapa", + icon: , + action: ({ stage }: { stage: Stage }) => { + selectStage(stage); + onEditionOpen(); + }, + actionName: "edit-stage", + disabled: !isActionAllowedToUser( + userData?.value?.allowedActions || [], + "edit-stage" + ), + }, { label: "Excluir Etapa", icon: , @@ -224,6 +243,14 @@ function Stages() { refetchStages={refetchStages} /> )} + {selectedStage ? ( + + ) : null} ); } diff --git a/src/pages/__tests__/Stages.test.tsx b/src/pages/__tests__/Stages.test.tsx index 4dcb083c..6f50f10a 100644 --- a/src/pages/__tests__/Stages.test.tsx +++ b/src/pages/__tests__/Stages.test.tsx @@ -1,5 +1,11 @@ import { describe, expect } from "vitest"; -import { act, render, screen, fireEvent } from "@testing-library/react"; +import { + act, + render, + screen, + fireEvent, + waitFor, +} from "@testing-library/react"; import { BrowserRouter } from "react-router-dom"; import { rest } from "msw"; @@ -107,31 +113,51 @@ describe("Stages page", () => { expect(await screen.queryByText("j")).toBe(null); }); + it("open and closes the edition modal correctly", async () => { + const editStageButton = screen.getAllByLabelText("Editar Etapa"); + + await act(async () => { + await fireEvent.click(editStageButton[0]); + }); + + expect(await screen.findAllByText("Editar etapa")).not.toBeNull(); + + const closeModalButton = await screen.getByText("Cancelar"); + + await act(async () => { + await fireEvent.click(closeModalButton); + }); + + await waitFor(() => { + expect(screen.queryByText("Editar etapa")).toBeNull(); + }); + }); + it("filters stages correctly", async () => { - const input = screen.getByPlaceholderText("Pesquisar etapas"); + const searchStagesButton = screen.getByPlaceholderText("Pesquisar etapas"); - expect(input).not.toBe(null); + expect(searchStagesButton).not.toBe(null); await act(async () => { - await fireEvent.change(input, { + await fireEvent.change(searchStagesButton, { target: { value: "a" }, }); - await fireEvent.submit(input); + await fireEvent.submit(searchStagesButton); }); expect(await screen.queryByText("a")).not.toBe(null); expect(await screen.queryByText("b")).toBe(null); expect(await screen.queryByText("c")).toBe(null); - const button = screen.getByLabelText("botão de busca"); + const searchBarButton = screen.getByLabelText("botão de busca"); - expect(button).not.toBe(null); + expect(searchBarButton).not.toBe(null); await act(async () => { - await fireEvent.change(input, { + await fireEvent.change(searchStagesButton, { target: { value: "c" }, }); - await fireEvent.click(button); + await fireEvent.click(searchBarButton); }); expect(await screen.queryByText("a")).toBe(null); diff --git a/src/services/stages/index.ts b/src/services/stages/index.ts index 4c619f14..4a6f9708 100644 --- a/src/services/stages/index.ts +++ b/src/services/stages/index.ts @@ -57,6 +57,29 @@ export const createStage = async (data: { } }; +export const updateStage = async (data: { + idStage: number; + name: string; +}): Promise> => { + try { + const res = await api.stages.put("/updateStage", data); + + return { + type: "success", + value: res.data, + }; + } catch (error) { + if (error instanceof Error) + return { type: "error", error, value: undefined }; + + return { + type: "error", + error: new Error("Erro desconhecido"), + value: undefined, + }; + } +}; + export const deleteStage = async (idStage: number): Promise> => { try { const res = await api.stages.delete(`/deleteStage/${idStage}`);