From 447f7c9c6b0f4228f86d923ad7856e51a2a7d7ae Mon Sep 17 00:00:00 2001 From: Johnson Mao <86179381+JohnsonMao@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:12:21 +0800 Subject: [PATCH 1/2] feat(group): check save confirm --- components/Group/Form/Fields/DateRadio.jsx | 23 +++++++------ components/Group/Form/useGroupForm.jsx | 34 +++++++++++++++++++ .../MarkdownEditor/MarkdownEditor.jsx | 2 +- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/components/Group/Form/Fields/DateRadio.jsx b/components/Group/Form/Fields/DateRadio.jsx index 40ff4167..c970f718 100644 --- a/components/Group/Form/Fields/DateRadio.jsx +++ b/components/Group/Form/Fields/DateRadio.jsx @@ -18,12 +18,15 @@ export default function DateRadio({ const [isCustomDate, setIsCustomDate] = useState(isCustomValue); const [date, setDate] = useState(value); - useEffect(() => { - control.onChange({ target: { name, value: date } }); - control.onChange({ - target: { name: customValueName, value: isCustomDate }, - }); - }, [name, date, customValueName, isCustomDate]); + const handleClickRadio = (isCustom) => { + setIsCustomDate(isCustom); + control.onChange({ target: { name: customValueName, value: isCustom } }); + }; + + const handleChange = (_date) => { + setDate(_date); + control.onChange({ target: { name, value: _date } }); + }; return ( @@ -35,15 +38,15 @@ export default function DateRadio({ }} > setIsCustomDate(true)} />} + control={ handleClickRadio(true)} />} label="自訂" checked={isCustomDate} /> setIsCustomDate(true)} + onChange={handleChange} + onAccept={() => handleClickRadio(true)} minDate={dayjs().add(1, 'day')} maxDate={dayjs().add(4, 'year')} renderInput={(params) => ( @@ -58,7 +61,7 @@ export default function DateRadio({
setIsCustomDate(false)} />} + control={ handleClickRadio(false)} />} label="不限" checked={!isCustomDate} /> diff --git a/components/Group/Form/useGroupForm.jsx b/components/Group/Form/useGroupForm.jsx index 1067cd20..9de6629e 100644 --- a/components/Group/Form/useGroupForm.jsx +++ b/components/Group/Form/useGroupForm.jsx @@ -1,6 +1,7 @@ import dayjs from 'dayjs'; import { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; +import { useRouter } from 'next/router'; import { ZodType, z } from 'zod'; import { useSnackbar } from '@/contexts/Snackbar'; import { CATEGORIES } from '@/constants/category'; @@ -84,6 +85,7 @@ const rules = { export default function useGroupForm(defaultValue) { const [isDirty, setIsDirty] = useState(false); + const router = useRouter(); const me = useSelector((state) => state.user); const notLogin = !me?._id; const [values, setValues] = useState(() => ({ @@ -218,6 +220,38 @@ export default function useGroupForm(defaultValue) { return () => clearTimeout(timer); }, [notLogin]); + useEffect(() => { + const confirmMessage = '尚未儲存,確認要離開此頁面?'; + + const handleBeforeUnload = (event) => { + if (!isDirty) return ''; + event.preventDefault(); + return confirmMessage; + }; + + const handleRouteChangeStart = () => { + if (!isDirty || confirm(confirmMessage)) return; + router.events.emit('routeChangeError'); + throw new Error(confirmMessage); + }; + + const handleUnhandledRejection = (event) => { + if (event?.reason?.message === confirmMessage) { + event.preventDefault(); + } + }; + + router.events.on('routeChangeStart', handleRouteChangeStart); + window.addEventListener('beforeunload', handleBeforeUnload); + window.addEventListener('unhandledrejection', handleUnhandledRejection); + + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart); + window.removeEventListener('beforeunload', handleBeforeUnload); + window.removeEventListener('unhandledrejection', handleUnhandledRejection); + }; + }, [isDirty, router.events]); + return { notLogin, control, diff --git a/shared/components/MarkdownEditor/MarkdownEditor.jsx b/shared/components/MarkdownEditor/MarkdownEditor.jsx index d0b8302a..6969a3f7 100644 --- a/shared/components/MarkdownEditor/MarkdownEditor.jsx +++ b/shared/components/MarkdownEditor/MarkdownEditor.jsx @@ -42,7 +42,7 @@ const toolbarContents = () => ( const generatePluginsSettings = ({ diffMarkdown = '' }) => ({ diffSource: diffSourcePlugin({ viewMode: 'rich-text', diffMarkdown }), - headings: headingsPlugin({ allowedHeadingLevels: [1, 2, 3] }), + headings: headingsPlugin({ allowedHeadingLevels: [2, 3] }), image: imagePlugin({ ImageDialog }), linkDialog: linkDialogPlugin(), link: linkPlugin(), From 9aaed940c0541ec729b58c33a4a82d8205edfed4 Mon Sep 17 00:00:00 2001 From: Johnson Mao <86179381+JohnsonMao@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:35:55 +0800 Subject: [PATCH 2/2] refactor: leave page confirm check --- components/Group/Form/useGroupForm.jsx | 35 +--------------- hooks/useLeaveConfirm.js | 55 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 hooks/useLeaveConfirm.js diff --git a/components/Group/Form/useGroupForm.jsx b/components/Group/Form/useGroupForm.jsx index 9de6629e..8f519b68 100644 --- a/components/Group/Form/useGroupForm.jsx +++ b/components/Group/Form/useGroupForm.jsx @@ -1,7 +1,6 @@ import dayjs from 'dayjs'; import { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { useRouter } from 'next/router'; import { ZodType, z } from 'zod'; import { useSnackbar } from '@/contexts/Snackbar'; import { CATEGORIES } from '@/constants/category'; @@ -10,6 +9,7 @@ import { EDUCATION_STEP } from '@/constants/member'; import { BASE_URL } from '@/constants/common'; import openLoginWindow from '@/utils/openLoginWindow'; import { activityCategoryList } from '@/constants/activityCategory'; +import useLeaveConfirm from '@/hooks/useLeaveConfirm'; const _eduOptions = EDUCATION_STEP.filter( (edu) => !['master', 'doctor', 'other'].includes(edu.value), @@ -85,7 +85,6 @@ const rules = { export default function useGroupForm(defaultValue) { const [isDirty, setIsDirty] = useState(false); - const router = useRouter(); const me = useSelector((state) => state.user); const notLogin = !me?._id; const [values, setValues] = useState(() => ({ @@ -220,37 +219,7 @@ export default function useGroupForm(defaultValue) { return () => clearTimeout(timer); }, [notLogin]); - useEffect(() => { - const confirmMessage = '尚未儲存,確認要離開此頁面?'; - - const handleBeforeUnload = (event) => { - if (!isDirty) return ''; - event.preventDefault(); - return confirmMessage; - }; - - const handleRouteChangeStart = () => { - if (!isDirty || confirm(confirmMessage)) return; - router.events.emit('routeChangeError'); - throw new Error(confirmMessage); - }; - - const handleUnhandledRejection = (event) => { - if (event?.reason?.message === confirmMessage) { - event.preventDefault(); - } - }; - - router.events.on('routeChangeStart', handleRouteChangeStart); - window.addEventListener('beforeunload', handleBeforeUnload); - window.addEventListener('unhandledrejection', handleUnhandledRejection); - - return () => { - router.events.off('routeChangeStart', handleRouteChangeStart); - window.removeEventListener('beforeunload', handleBeforeUnload); - window.removeEventListener('unhandledrejection', handleUnhandledRejection); - }; - }, [isDirty, router.events]); + useLeaveConfirm({ shouldConfirm: isDirty }); return { notLogin, diff --git a/hooks/useLeaveConfirm.js b/hooks/useLeaveConfirm.js new file mode 100644 index 00000000..f3db51b8 --- /dev/null +++ b/hooks/useLeaveConfirm.js @@ -0,0 +1,55 @@ +import { useRouter } from "next/router"; +import { useEffect } from "react"; + +export default function useLeaveConfirm({ + shouldConfirm = false, + confirmMessage = "資料未儲存,確定要離開此頁面?", +}) { + const router = useRouter(); + + useEffect(() => { + const handleRouteChange = (url) => { + if (!shouldConfirm || window.confirm(confirmMessage)) return; + router.events.emit("routeChangeError"); + throw new Error(confirmMessage); + }; + + router.events.on("routeChangeStart", handleRouteChange); + + return () => { + router.events.off("routeChangeStart", handleRouteChange); + }; + }, [shouldConfirm, confirmMessage, router.events]); + + useEffect(() => { + const handleBeforeUnload = (event) => { + if (!shouldConfirm) return ""; + event.preventDefault(); + return confirmMessage; + }; + window.addEventListener("beforeunload", handleBeforeUnload); + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, [shouldConfirm, confirmMessage]); + + useEffect(() => { + const handleUnhandledRejection = (event) => { + if (event?.reason?.message === confirmMessage) { + event.preventDefault(); + } + }; + + window.addEventListener("unhandledrejection", handleUnhandledRejection); + + return () => { + window.removeEventListener( + "unhandledrejection", + handleUnhandledRejection + ); + }; + }, [confirmMessage]); + + return null; +}