From 6765ea3a201e1d06d015496742bf501f86f61f2e Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 11 Jul 2025 15:25:23 +0200 Subject: [PATCH 001/197] feat: Add save template modal --- src/assets/icons/template.svg | 3 + src/locales/en/translation.json | 16 +- src/locales/it/translation.json | 16 +- src/pages/Plan/Controls.tsx | 37 ++- src/pages/Plan/PlanBody.tsx | 4 +- .../Plan/common/ModulesBottomNavigation.tsx | 4 +- src/pages/Plan/context/planContext.tsx | 20 +- src/pages/Plan/index.tsx | 4 +- src/pages/Plan/modals/SaveAsTemplateModal.tsx | 212 ++++++++++++++++++ .../Plan/navigation/aside/AddBlockButton.tsx | 4 +- src/pages/Plan/navigation/aside/NavBody.tsx | 4 +- .../navigation/aside/modal/AddBlockModal.tsx | 4 +- .../Plan/navigation/header/BreadCrumbTabs.tsx | 4 +- src/pages/Plan/summary/index.tsx | 4 +- 14 files changed, 303 insertions(+), 33 deletions(-) create mode 100644 src/assets/icons/template.svg create mode 100644 src/pages/Plan/modals/SaveAsTemplateModal.tsx diff --git a/src/assets/icons/template.svg b/src/assets/icons/template.svg new file mode 100644 index 000000000..870b325f5 --- /dev/null +++ b/src/assets/icons/template.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ba310e231..41d70cf22 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -25,7 +25,6 @@ "__ASIDE_NAVIGATION_MODULE_ADDITIONAL_TARGET_SUBTITLE": " ", "__ASIDE_NAVIGATION_MODULE_AGE_SUBTITLE": "Participant age groups", "__ASIDE_NAVIGATION_MODULE_BROWSER_SUBTITLE": "Compatible browsers", - "__ASIDE_NAVIGATION_MODULE_DATES_BLOCK_SUBTITLE": " ", "__ASIDE_NAVIGATION_MODULE_DIGITAL_LITERACY_ACCORDION_SUBTITLE": "Participant technical proficiency", "__ASIDE_NAVIGATION_MODULE_GENDER_ACCORDION_SUBTITLE": "Participant gender criteria", "__ASIDE_NAVIGATION_MODULE_GOAL_SUBTITLE": "Activity objective", @@ -34,7 +33,6 @@ "__ASIDE_NAVIGATION_MODULE_LOCALITY_SUBTITLE": "Participant location criteria", "__ASIDE_NAVIGATION_MODULE_OUT_OF_SCOPE_SUBTITLE": " ", "__ASIDE_NAVIGATION_MODULE_SETUP_NOTE_BLOCK_SUBTITLE": " ", - "__ASIDE_NAVIGATION_MODULE_SUBTITLE_BLOCK_SUBTITLE": " ", "__ASIDE_NAVIGATION_MODULE_TARGET_NOTE_BLOCK_SUBTITLE": " ", "__ASIDE_NAVIGATION_MODULE_TARGET_SUBTITLE": "Participant number", "__ASIDE_NAVIGATION_MODULE_TASKS_SUBTITLE": " ", @@ -808,7 +806,6 @@ "__PLAN_PAGE_MODULE_BROWSER_LABEL": "Browser selection", "__PLAN_PAGE_MODULE_BROWSER_REMOVE_BUTTON": "Delete", "__PLAN_PAGE_MODULE_BROWSER_TITLE": "Choose the browser you want to include in the activity", - "__PLAN_PAGE_MODULE_DATES_BLOCK_TITLE": "Dates item", "__PLAN_PAGE_MODULE_DIGITAL_LITERACY_ACCORDION_LABEL": "Digital skills", "__PLAN_PAGE_MODULE_DIGITAL_LITERACY_ALL_LABEL": "All levels", "__PLAN_PAGE_MODULE_DIGITAL_LITERACY_ALL_LABEL_HINT": "Participants equally divided by literacy level", @@ -984,7 +981,6 @@ "__PLAN_PAGE_MODULE_TASKS_TASK_TITLE_PLACEHOLDER_EMPTY": "Task title", "__PLAN_PAGE_MODULE_TASKS_TASK_URL_LINK_ERROR_INVALID_URL": "Task link should be a valid URL", "__PLAN_PAGE_MODULE_TASKS_TITLE": "Tasks", - "__PLAN_PAGE_MODULE_TITLE_BLOCK_TITLE": "Title item", "__PLAN_PAGE_MODULE_TOUCHPOINTS_ADD_TOUCHPOINT_APP_TAB": "App based", "__PLAN_PAGE_MODULE_TOUCHPOINTS_ADD_TOUCHPOINT_BUTTON": "Add touchpoint", "__PLAN_PAGE_MODULE_TOUCHPOINTS_ADD_TOUCHPOINT_MODAL_DEFAULT_TAB": "All", @@ -1044,6 +1040,13 @@ "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_WEB_LINK_LABEL": "Test link", "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_WEB_LINK_PLACEHOLDER": "https://example.com", "__PLAN_PAGE_NAV_GENERIC_MODULE_ERROR": "This item has some errors", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_BUTTON_CANCEL": "Cancel", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_BUTTON_CONFIRM": "Save template", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_DESCRIPTION_MAX": "This description is a bit long. We advise you to stay within 512 characters including spaces.", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_HEADER": "Did you find the perfect match?This template will help you launch similar activities more quickly.", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_NAME_MAX": "This name is a bit long. We advise you to stay within 64 characters including spaces.", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_NAME_REQUIRED": "Choose a name before saving the template", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_TITLE": "Save as template", "__PLAN_PAGE_SUMMARY_TAB_ACTIVITY_INFO_APPROVED_DESCRIPTION": "The quotation are currently being processed. Some details may vary or need confirmation", "__PLAN_PAGE_SUMMARY_TAB_ACTIVITY_INFO_AWAITING_DESCRIPTION": "Your request has been processed. Please review and confirm these details.", "__PLAN_PAGE_SUMMARY_TAB_ACTIVITY_INFO_DATE_LABEL": "Start date", @@ -1077,6 +1080,7 @@ "__PLAN_SAVE_CONFIGURATION_CTA": "Save Draft", "__PLAN_SAVE_DRAFT_TOAST_ERROR": "We couldn't save your draft: please try again later.", "__PLAN_SAVE_DRAFT_TOAST_SUCCESS": "Activity draft saved! You can safely continue editing.", + "__PLAN_SAVE_TEMPLATE_CTA": "Save as template", "__PLAN_SETUP_NOTE_SIZE_ERROR_EMPTY": "Required: please enter a text to continue", "__PLAN_TARGET_NOTE_SIZE_ERROR_EMPTY": "Please enter a text to continue", "__PLAN_TARGET_SIZE_ERROR_REQUIRED": "Please enter at least one user to include in the activity", @@ -1291,6 +1295,10 @@ "PLAN_GLOBAL_ALERT_AWATING_STATE_TITLE": "Your quotation is ready!", "PLAN_GLOBAL_ALERT_SUBMITTED_STATE_MESSAGE": "You'll receive a notification once your quotation is available.", "PLAN_GLOBAL_ALERT_SUBMITTED_STATE_TITLE": "Activity submitted for review", + "SAVE_AS_TEMPLATE_FORM_DESCRIPTION": "Template description (optional)", + "SAVE_AS_TEMPLATE_FORM_DESCRIPTION_PLACEHOLDER": "Write the template description", + "SAVE_AS_TEMPLATE_FORM_TITLE": "Template name", + "SAVE_AS_TEMPLATE_FORM_TITLE_PLACEHOLDER": "Write the template name", "Severity ({{count}})_one": "Severity ({{count}})", "Severity ({{count}})_other": "Severities ({{count}})", "SIGNUP_FORM_CTA_RETURN_TO_UNGUESS_LANDING": "Go to the UNGUESS website", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index e21a5a7e7..b5c7b3e71 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -25,7 +25,6 @@ "__ASIDE_NAVIGATION_MODULE_ADDITIONAL_TARGET_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_AGE_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_BROWSER_SUBTITLE": "", - "__ASIDE_NAVIGATION_MODULE_DATES_BLOCK_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_DIGITAL_LITERACY_ACCORDION_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_GENDER_ACCORDION_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_GOAL_SUBTITLE": "", @@ -34,7 +33,6 @@ "__ASIDE_NAVIGATION_MODULE_LOCALITY_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_OUT_OF_SCOPE_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_SETUP_NOTE_BLOCK_SUBTITLE": "", - "__ASIDE_NAVIGATION_MODULE_SUBTITLE_BLOCK_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_TARGET_NOTE_BLOCK_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_TARGET_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_TASKS_SUBTITLE": "", @@ -838,7 +836,6 @@ "__PLAN_PAGE_MODULE_BROWSER_LABEL": "", "__PLAN_PAGE_MODULE_BROWSER_REMOVE_BUTTON": "", "__PLAN_PAGE_MODULE_BROWSER_TITLE": "", - "__PLAN_PAGE_MODULE_DATES_BLOCK_TITLE": "", "__PLAN_PAGE_MODULE_DIGITAL_LITERACY_ACCORDION_LABEL": "", "__PLAN_PAGE_MODULE_DIGITAL_LITERACY_ALL_LABEL": "", "__PLAN_PAGE_MODULE_DIGITAL_LITERACY_ALL_LABEL_HINT": "", @@ -1014,7 +1011,6 @@ "__PLAN_PAGE_MODULE_TASKS_TASK_TITLE_PLACEHOLDER_EMPTY": "", "__PLAN_PAGE_MODULE_TASKS_TASK_URL_LINK_ERROR_INVALID_URL": "", "__PLAN_PAGE_MODULE_TASKS_TITLE": "", - "__PLAN_PAGE_MODULE_TITLE_BLOCK_TITLE": "", "__PLAN_PAGE_MODULE_TOUCHPOINTS_ADD_TOUCHPOINT_APP_TAB": "", "__PLAN_PAGE_MODULE_TOUCHPOINTS_ADD_TOUCHPOINT_BUTTON": "", "__PLAN_PAGE_MODULE_TOUCHPOINTS_ADD_TOUCHPOINT_MODAL_DEFAULT_TAB": "", @@ -1074,6 +1070,13 @@ "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_WEB_LINK_LABEL": "", "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_WEB_LINK_PLACEHOLDER": "", "__PLAN_PAGE_NAV_GENERIC_MODULE_ERROR": "", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_BUTTON_CANCEL": "", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_BUTTON_CONFIRM": "", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_DESCRIPTION_MAX": "", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_HEADER": "", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_NAME_MAX": "", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_NAME_REQUIRED": "", + "__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_TITLE": "", "__PLAN_PAGE_SUMMARY_TAB_ACTIVITY_INFO_APPROVED_DESCRIPTION": "", "__PLAN_PAGE_SUMMARY_TAB_ACTIVITY_INFO_AWAITING_DESCRIPTION": "", "__PLAN_PAGE_SUMMARY_TAB_ACTIVITY_INFO_DATE_LABEL": "", @@ -1107,6 +1110,7 @@ "__PLAN_SAVE_CONFIGURATION_CTA": "", "__PLAN_SAVE_DRAFT_TOAST_ERROR": "", "__PLAN_SAVE_DRAFT_TOAST_SUCCESS": "", + "__PLAN_SAVE_TEMPLATE_CTA": "", "__PLAN_SETUP_NOTE_SIZE_ERROR_EMPTY": "", "__PLAN_TARGET_NOTE_SIZE_ERROR_EMPTY": "", "__PLAN_TARGET_SIZE_ERROR_REQUIRED": "", @@ -1329,6 +1333,10 @@ "PLAN_GLOBAL_ALERT_AWATING_STATE_TITLE": "", "PLAN_GLOBAL_ALERT_SUBMITTED_STATE_MESSAGE": "", "PLAN_GLOBAL_ALERT_SUBMITTED_STATE_TITLE": "", + "SAVE_AS_TEMPLATE_FORM_DESCRIPTION": "", + "SAVE_AS_TEMPLATE_FORM_DESCRIPTION_PLACEHOLDER": "", + "SAVE_AS_TEMPLATE_FORM_TITLE": "", + "SAVE_AS_TEMPLATE_FORM_TITLE_PLACEHOLDER": "", "Severity ({{count}})_one": "Gravità ({{count}})", "Severity ({{count}})_many": "", "Severity ({{count}})_other": "Gravità ({{count}})", diff --git a/src/pages/Plan/Controls.tsx b/src/pages/Plan/Controls.tsx index 1fb34eeca..eb20dac73 100644 --- a/src/pages/Plan/Controls.tsx +++ b/src/pages/Plan/Controls.tsx @@ -2,26 +2,31 @@ import { Button, ButtonMenu, IconButton, - useToast, Notification, + useToast, } from '@appquality/unguess-design-system'; +import { ReactComponent as DotsIcon } from '@zendeskgarden/svg-icons/src/16/overflow-vertical-stroke.svg'; +import { ReactComponent as TrashIcon } from '@zendeskgarden/svg-icons/src/16/trash-stroke.svg'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; +import { ReactComponent as SaveTemplateIcon } from 'src/assets/icons/template.svg'; +import { Divider } from 'src/common/components/divider'; import { Pipe } from 'src/common/components/Pipe'; -import { ReactComponent as DotsIcon } from '@zendeskgarden/svg-icons/src/16/overflow-vertical-stroke.svg'; import { usePatchPlansByPidStatusMutation } from 'src/features/api'; +import { useModule } from 'src/features/modules/useModule'; import { useSubmit } from 'src/features/modules/useModuleConfiguration'; import { useRequestQuotation } from 'src/features/modules/useRequestQuotation'; import { useValidateForm } from 'src/features/planModules'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import styled from 'styled-components'; -import { useModule } from 'src/features/modules/useModule'; import { getPlanStatus } from '../Dashboard/hooks/getPlanStatus'; +import { usePlanContext } from './context/planContext'; import { usePlan } from './hooks/usePlan'; -import { SendRequestModal } from './modals/SendRequestModal'; import { DeletePlanModal } from './modals/DeletePlanModal'; +import { SaveAsTemplateModal } from './modals/SaveAsTemplateModal'; +import { SendRequestModal } from './modals/SendRequestModal'; const StyledPipe = styled(Pipe)` display: inline; @@ -35,6 +40,8 @@ export const Controls = () => { const { addToast } = useToast(); const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const { isSaveTemplateModalOpen, setIsSaveTemplateModalOpen } = + usePlanContext(); const { planId } = useParams(); const { plan } = usePlan(planId); const { handleSubmit: submitModuleConfiguration, isLoading: isSubmitting } = @@ -92,6 +99,9 @@ export const Controls = () => { if (value === 'delete') { setIsDeleteModalOpen(true); } + if (value === 'save_template') { + setIsSaveTemplateModalOpen(true); + } }; if (!plan) return null; @@ -174,10 +184,19 @@ export const Controls = () => { )} > + } + > + {t('__PLAN_SAVE_TEMPLATE_CTA')} + + } > {t('__PLAN_DELETE_PLAN_CTA')} @@ -191,6 +210,16 @@ export const Controls = () => { /> )} + {isSaveTemplateModalOpen && planId && ( + { + setIsSaveTemplateModalOpen(false); + }} + /> + )} + {isModalOpen && setIsModalOpen(false)} />} ); diff --git a/src/pages/Plan/PlanBody.tsx b/src/pages/Plan/PlanBody.tsx index 9fb9082af..c3bc93fc1 100644 --- a/src/pages/Plan/PlanBody.tsx +++ b/src/pages/Plan/PlanBody.tsx @@ -2,14 +2,14 @@ import { Col, Grid, Row } from '@appquality/unguess-design-system'; import { appTheme } from 'src/app/theme'; import { LayoutWrapper } from 'src/common/components/LayoutWrapper'; import { StickyCol } from './common/StickyCol'; -import { usePlanTab } from './context/planContext'; +import { usePlanContext } from './context/planContext'; import { ModulesList } from './ModulesList'; import { Nav } from './navigation/aside'; import { PlanDetails } from './navigation/header/PlanDetails'; import SummaryBody from './summary'; export const PlanBody = () => { - const { activeTab } = usePlanTab(); + const { activeTab } = usePlanContext(); // Debug info const params = new URLSearchParams(window.location.search); diff --git a/src/pages/Plan/common/ModulesBottomNavigation.tsx b/src/pages/Plan/common/ModulesBottomNavigation.tsx index 237afab03..2a3d05d1c 100644 --- a/src/pages/Plan/common/ModulesBottomNavigation.tsx +++ b/src/pages/Plan/common/ModulesBottomNavigation.tsx @@ -5,10 +5,10 @@ import { useTranslation } from 'react-i18next'; import { appTheme } from 'src/app/theme'; import { MODULE_TABS_ORDER } from 'src/constants'; import { useModuleConfiguration } from 'src/features/modules/useModuleConfiguration'; -import { PlanTab, usePlanTab } from '../context/planContext'; +import { PlanTab, usePlanContext } from '../context/planContext'; export const ModulesBottomNavigation = ({ tabId }: { tabId: PlanTab }) => { - const { setActiveTab } = usePlanTab(); + const { setActiveTab } = usePlanContext(); const { t } = useTranslation(); const { getPlanStatus } = useModuleConfiguration(); let leftLabel = ''; diff --git a/src/pages/Plan/context/planContext.tsx b/src/pages/Plan/context/planContext.tsx index ec756dd05..c892bad6e 100644 --- a/src/pages/Plan/context/planContext.tsx +++ b/src/pages/Plan/context/planContext.tsx @@ -1,23 +1,33 @@ -import { createContext, useContext, useState, ReactNode, useMemo } from 'react'; +import { createContext, ReactNode, useContext, useMemo, useState } from 'react'; export type PlanTab = 'setup' | 'target' | 'instructions' | 'summary'; -interface TabContextProps { +interface PlanContextProps { activeTab: PlanTab; setActiveTab: (tab: PlanTab) => void; + setIsSaveTemplateModalOpen: (isOpen: boolean) => void; + isSaveTemplateModalOpen: boolean; } -const PlanContext = createContext(null); +const PlanContext = createContext(null); export const PlanProvider = ({ children }: { children: ReactNode }) => { const [activeTab, setActiveTab] = useState('setup'); + const [isSaveTemplateModalOpen, setIsSaveTemplateModalOpen] = useState(false); const planContextValue = useMemo( () => ({ activeTab, setActiveTab, + setIsSaveTemplateModalOpen, + isSaveTemplateModalOpen, }), - [activeTab, setActiveTab] + [ + activeTab, + setActiveTab, + setIsSaveTemplateModalOpen, + isSaveTemplateModalOpen, + ] ); return ( @@ -27,7 +37,7 @@ export const PlanProvider = ({ children }: { children: ReactNode }) => { ); }; -export const usePlanTab = () => { +export const usePlanContext = () => { const context = useContext(PlanContext); if (!context) { throw new Error('useTab must be used within a PlanProvider'); diff --git a/src/pages/Plan/index.tsx b/src/pages/Plan/index.tsx index 02a9c0a05..15d3e7a20 100644 --- a/src/pages/Plan/index.tsx +++ b/src/pages/Plan/index.tsx @@ -14,14 +14,14 @@ import { setWorkspace } from 'src/features/navigation/navigationSlice'; import { Page } from 'src/features/templates/Page'; import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; -import { PlanProvider, usePlanTab } from './context/planContext'; +import { PlanProvider, usePlanContext } from './context/planContext'; import PlanPageHeader from './navigation/header/Header'; import { PlanBody } from './PlanBody'; import { formatModuleDate } from './utils/formatModuleDate'; const PlanPage = ({ plan }: { plan: GetPlansByPidApiResponse | undefined }) => { const { t } = useTranslation(); - const { activeTab, setActiveTab } = usePlanTab(); + const { activeTab, setActiveTab } = usePlanContext(); useEffect(() => { if (!plan) return; diff --git a/src/pages/Plan/modals/SaveAsTemplateModal.tsx b/src/pages/Plan/modals/SaveAsTemplateModal.tsx new file mode 100644 index 000000000..d222fa6aa --- /dev/null +++ b/src/pages/Plan/modals/SaveAsTemplateModal.tsx @@ -0,0 +1,212 @@ +import { + Button, + Dots, + FooterItem, + FormField, + Input, + Label, + MD, + Message, + Modal, + ModalClose, + Notification, + Span, + Textarea, + useToast, + XXL, +} from '@appquality/unguess-design-system'; +import { Field, FieldProps, Formik } from 'formik'; +import { Trans, useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { appTheme } from 'src/app/theme'; +import { useDeletePlansByPidMutation } from 'src/features/api'; +import { styled } from 'styled-components'; +import * as yup from 'yup'; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.sm}; +`; + +const FormProvider = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => { + const { t } = useTranslation(); + const initialValues = { + templateName: `${title} - Copy`, + templateDescription: '', + }; + const validationSchema = yup.object().shape({ + templateName: yup + .string() + .max(64, t('__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_NAME_MAX')) + .required(t('__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_NAME_REQUIRED')), + templateDescription: yup + .string() + .max(200, t('__PLAN_PAGE_SAVE_AS_TEMPLATE_MODAL_DESCRIPTION_MAX')), + }); + + return ( + {}} + > + {() => children} + + ); +}; + +const Form = () => { + const { t } = useTranslation(); + return ( + <> + + {({ field, meta }: FieldProps) => { + const hasError = Boolean(meta.touched && meta.error); + return ( + + + + {hasError && {meta.error}} + + ); + }} + + + {({ field, meta }: FieldProps) => { + const hasError = Boolean(meta.touched && meta.error); + return ( + + +