From d1d1b88450197ddff25d05a4e75614220f73d5b8 Mon Sep 17 00:00:00 2001 From: paola-p <> Date: Tue, 12 Dec 2023 08:24:24 +0100 Subject: [PATCH] feat(packages/components): export TotalLayout + add logic for the total layout to know when the form is dirty --- .../src/layout/TotalLayout/TotalLayout.js | 47 ++-------- .../layout/TotalLayout/TotalLayout.stories.js | 85 ++++++++----------- .../layout/TotalLayout/TotalLayoutFooter.js | 3 +- .../TotalLayoutHeader.constants.js | 1 + .../TotalLayoutHeader/TotalLayoutHeader.js | 12 +-- 5 files changed, 52 insertions(+), 96 deletions(-) diff --git a/packages/components/src/layout/TotalLayout/TotalLayout.js b/packages/components/src/layout/TotalLayout/TotalLayout.js index 8edc38b2b..d3dad789b 100644 --- a/packages/components/src/layout/TotalLayout/TotalLayout.js +++ b/packages/components/src/layout/TotalLayout/TotalLayout.js @@ -6,15 +6,12 @@ import Footer from './TotalLayoutFooter'; import Body from './TotalLayoutBody/TotalLayoutBody'; import { TotalLayoutStyles } from './TotalLayout.styles'; import { TOTAL_LAYOUT_DEFAULT_PROPS, TOTAL_LAYOUT_PROP_TYPES } from './TotalLayout.constants'; -import { Modal } from '../../overlay'; -import { Button } from '../../form'; -import { Paragraph } from '../../typography'; const useTotalLayout = () => { const [activeStep, setActiveStep] = React.useState(0); const [completedSteps, setCompletedSteps] = React.useState([]); const [stepsInfo, setStepsInfo] = React.useState([]); - const [openCancelModal, setOpenCancelModal] = React.useState(false); + const formIsDirty = React.useRef(false); return { activeStep, setActiveStep, @@ -22,8 +19,7 @@ const useTotalLayout = () => { setCompletedSteps, stepsInfo, setStepsInfo, - openCancelModal, - setOpenCancelModal, + formIsDirty, }; }; @@ -41,9 +37,8 @@ const TotalLayout = ({ footerFinalActions, stepsInfo, setStepsInfo, - openCancelModal, - setOpenCancelModal, - onCancel, + formIsDirty, + isLoading, }) => { const form = useFormContext(); const [topScroll, setTopScroll] = React.useState(false); @@ -124,11 +119,15 @@ const TotalLayout = ({ }; const handleNext = async () => { + formIsDirty.current = + formIsDirty.current || Object.keys(form.formState.touchedFields).length > 0; setCompletedSteps((prevCompletedSteps) => [...prevCompletedSteps, activeStep]); setActiveStep((prevActiveStep) => getNextValidStep(prevActiveStep + 1)); window.scrollTo(0, 0, { behavior: 'smooth' }); }; const handlePrev = async () => { + formIsDirty.current = + formIsDirty.current || Object.keys(form.formState.touchedFields).length > 0; const isValidStep = await form.trigger(); if (!isValidStep && completedSteps.includes(activeStep)) { setCompletedSteps((prevCompletedSteps) => @@ -156,26 +155,6 @@ const TotalLayout = ({ return ( - {/* Cancel Modal */} - setOpenCancelModal(false)}> - - {'Seguro que desea salir? Todos sus cambios serán descartados.'} - - - - - - -
@@ -204,6 +183,7 @@ const TotalLayout = ({ minStepNumberForDraftSave={minStepNumberForDraftSave} onSave={() => validateAndAct(onSave)} isLastStep={lastValidStep === activeStep} + isLoading={isLoading} /> @@ -217,16 +197,7 @@ TotalLayout.propTypes = TOTAL_LAYOUT_PROP_TYPES; export { TotalLayout, useTotalLayout }; /* -DONE: -- arreglar footer en pequeñito -- save draft es dinámico. Pasar el stepNumberForDraftSave -- Pasar labels dinámicamente -> para los botones de footer -- Vertical Stepper! Crear un TotalLayoutStepper a partir del VerticalStepper TODO: -- Steps se muestran dinámicamente en el stepper -- onCancel para el header y Modal. Genérica, reutilizable. -- Toast de confirmación al guardar borrador. -- aplicar efecto a las sombras del header - Tidy: una carpeta para cada componente con sus constantes y estilos, etc. - Al final: P r o p s d e t o d o */ diff --git a/packages/components/src/layout/TotalLayout/TotalLayout.stories.js b/packages/components/src/layout/TotalLayout/TotalLayout.stories.js index e29e641e4..2182890bb 100644 --- a/packages/components/src/layout/TotalLayout/TotalLayout.stories.js +++ b/packages/components/src/layout/TotalLayout/TotalLayout.stories.js @@ -11,10 +11,6 @@ import mdx from './TotalLayout.mdx'; import BasicDataForm from './mock/BasicDataForm'; import ContentForm from './mock/ContentForm'; import OptionalForm from './mock/OptionalForm'; -import { Modal } from '../../overlay'; -import { Paragraph } from '../../typography'; -import { Stack } from '../Stack'; -import { Button } from '../../form'; export default { title: 'Molecules/Layout/TotalLayout', @@ -37,8 +33,6 @@ export default { // The page must take care of wrapping the TotalLayout within a FormProvider const Template = () => { const totalLayoutProps = useTotalLayout(); - const [openModal, setOpenModal] = React.useState(false); - const [modalContent, setModalContent] = React.useState({ title: '', body: '', action: null }); const initialStepsInfo = [ { @@ -53,6 +47,13 @@ const Template = () => { tags: z.string().optional(), color: z.string().optional(), }), + initialValues: { + title: '', + description: '', + program: '', + tags: '', + color: '', + }, // Steps can use the any form methods with the useFormContext hook (from react-hook-form) stepComponent: , }, @@ -65,6 +66,10 @@ const Template = () => { instructions: z.string({ required_error: 'Instructions are required' }).min(1), deliverables: z.boolean().optional(), }), + initialValues: { + instructions: '', + deliverables: false, + }, stepComponent: , }, { @@ -88,6 +93,7 @@ const Template = () => { // Prepare Form const formMethods = useForm({ + defaultValues: initialStepsInfo[totalLayoutProps.activeStep]?.initialValues, resolver: zodResolver(initialStepsInfo[totalLayoutProps.activeStep]?.validationSchema), }); const formValues = formMethods.watch(); @@ -102,39 +108,25 @@ const Template = () => { } }, [formValues.addOptionalStep]); - // Prepare dynamic modal. Total Layout only handles the "global" cancel modal - const renderActionModal = ({ title, body, action }) => ( - setOpenModal(false)}> - - {body} - - - - - - - ); - // Prepare Header. It is necesary to pass the setOpenCancelModal function to the header const onCancel = () => { console.log('Redirecting after cancel'); }; + const handleOnCancel = () => { + if (totalLayoutProps.formIsDirty) { + // Usar openConfirmationModal del plugin Layout(leemons) + return; + } + onCancel(); + }; + const buildHeader = () => ( } formTitlePlaceholder={'Título de la tarea'} - setOpenCancelModal={totalLayoutProps.setOpenCancelModal} + onCancel={handleOnCancel} /> ); @@ -186,25 +178,22 @@ const Template = () => { // const footerFinalAction = [{ label: 'Finalize with single action', action: handleFinalize }]; return ( - <> - {/* Modal - Implemented at the page level */} - {renderActionModal(modalContent)} - - - buildHeader()} - showStepper - footerActionsLabels={footerActionsLabels} - footerFinalActions={footerFinalActions} - minStepNumberForDraftSave={1} - onSave={handleOnSave} - initialStepsInfo={initialStepsInfo} - onCancel={onCancel} - /> - - - + + + buildHeader()} + showStepper + footerActionsLabels={footerActionsLabels} + footerFinalActions={footerFinalActions} + minStepNumberForDraftSave={1} + onSave={handleOnSave} + initialStepsInfo={initialStepsInfo} + onCancel={onCancel} + isLoading={false} + /> + + ); }; diff --git a/packages/components/src/layout/TotalLayout/TotalLayoutFooter.js b/packages/components/src/layout/TotalLayout/TotalLayoutFooter.js index a54411d81..35fb82c4c 100644 --- a/packages/components/src/layout/TotalLayout/TotalLayoutFooter.js +++ b/packages/components/src/layout/TotalLayout/TotalLayoutFooter.js @@ -17,6 +17,7 @@ const TotalLayoutFooter = ({ minStepNumberForDraftSave, onSave, isLastStep, + isLoading, }) => { const { classes } = TotalLayoutFooterStyles( { showFooterShadow, leftOffset }, @@ -30,7 +31,7 @@ const TotalLayoutFooter = ({ // It's the final step and there's more than one final action if (finalActions?.length > 1) { return ( - + {footerActionsLabels.dropdownLabel || 'Finalizar'} ); diff --git a/packages/components/src/layout/TotalLayout/TotalLayoutHeader/TotalLayoutHeader.constants.js b/packages/components/src/layout/TotalLayout/TotalLayoutHeader/TotalLayoutHeader.constants.js index 7f4492a55..aef8cddd6 100644 --- a/packages/components/src/layout/TotalLayout/TotalLayoutHeader/TotalLayoutHeader.constants.js +++ b/packages/components/src/layout/TotalLayout/TotalLayoutHeader/TotalLayoutHeader.constants.js @@ -1,3 +1,4 @@ +import React from 'react'; import PropTypes from 'prop-types'; import { PluginAssignmentsIcon as Icon } from '@bubbles-ui/icons/solid'; diff --git a/packages/components/src/layout/TotalLayout/TotalLayoutHeader/TotalLayoutHeader.js b/packages/components/src/layout/TotalLayout/TotalLayoutHeader/TotalLayoutHeader.js index a3c545c9e..e4fa0d7b7 100644 --- a/packages/components/src/layout/TotalLayout/TotalLayoutHeader/TotalLayoutHeader.js +++ b/packages/components/src/layout/TotalLayout/TotalLayoutHeader/TotalLayoutHeader.js @@ -1,7 +1,7 @@ import { useFormContext } from 'react-hook-form'; import { Stack } from '../../Stack'; import { Text } from '../../../typography'; -import { Button } from '../../../form'; +import { Button } from '../../../form/Button'; import { Box } from '../../Box'; import { TOTAL_LAYOUT_HEADER_PROP_TYPES, @@ -9,14 +9,13 @@ import { } from './TotalLayoutHeader.constants'; import { TotalLayoutHeaderStyles } from './TotalLayoutHeader.styles'; import CrossIcon from './crossIcon'; -import { useTotalLayout } from '../TotalLayout'; const TotalLayoutHeader = ({ title, icon, formTitlePlaceholder, children, - setOpenCancelModal, + onCancel, compact = false, }) => { const { watch } = useFormContext(); @@ -54,12 +53,7 @@ const TotalLayoutHeader = ({ {/* CANCEL BUTTON */} -