diff --git a/apps/dashboard/src/components/subscribers/subscriber-drawer.tsx b/apps/dashboard/src/components/subscribers/subscriber-drawer.tsx index c5ab115da4b..b4c8972ddd9 100644 --- a/apps/dashboard/src/components/subscribers/subscriber-drawer.tsx +++ b/apps/dashboard/src/components/subscribers/subscriber-drawer.tsx @@ -2,7 +2,7 @@ import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ import { VisuallyHidden } from '@/components/primitives/visually-hidden'; import { SubscriberTabs } from '@/components/subscribers/subscriber-tabs'; import { useCombinedRefs } from '@/hooks/use-combined-refs'; -import { useFormDialogProtection } from '@/hooks/use-form-dialog-protection'; +import { useFormProtection } from '@/hooks/use-form-protection'; import { cn } from '@/utils/ui'; import { forwardRef, useState } from 'react'; @@ -16,13 +16,19 @@ type SubscriberDrawerProps = { export const SubscriberDrawer = forwardRef((props, forwardedRef) => { const { open, onOpenChange, subscriberId, readOnly = false } = props; - const { protectedOnOpenChange, ProtectionAlert, ref: protectionRef } = useFormDialogProtection({ onOpenChange }); + const { + protectedOnValueChange, + ProtectionAlert, + ref: protectionRef, + } = useFormProtection({ + onValueChange: onOpenChange, + }); const combinedRef = useCombinedRefs(forwardedRef, protectionRef); return ( <> - + {/* Custom overlay since SheetOverlay does not work with modal={false} */}
+
@@ -85,6 +100,7 @@ export function SubscriberTabs(props: SubscriberTabsProps) { + ); } diff --git a/apps/dashboard/src/components/workflow-editor/steps/step-drawer.tsx b/apps/dashboard/src/components/workflow-editor/steps/step-drawer.tsx index 467c2931c7e..fef72862bc9 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/step-drawer.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/step-drawer.tsx @@ -5,7 +5,7 @@ import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ import { VisuallyHidden } from '@/components/primitives/visually-hidden'; import { useWorkflow } from '@/components/workflow-editor/workflow-provider'; import { useCombinedRefs } from '@/hooks/use-combined-refs'; -import { useFormDialogProtection } from '@/hooks/use-form-dialog-protection'; +import { useFormProtection } from '@/hooks/use-form-protection'; import { useOnElementUnmount } from '@/hooks/use-on-element-unmount'; import { cn } from '@/utils/ui'; import { StepTypeEnum } from '@novu/shared'; @@ -28,10 +28,12 @@ export const StepDrawer = ({ children, title }: { children: React.ReactNode; tit }); const { - protectedOnOpenChange, + protectedOnValueChange, ProtectionAlert, ref: protectionRef, - } = useFormDialogProtection({ onOpenChange: setIsOpen }); + } = useFormProtection({ + onValueChange: setIsOpen, + }); const combinedRef = useCombinedRefs(unmountRef, protectionRef); @@ -42,7 +44,7 @@ export const StepDrawer = ({ children, title }: { children: React.ReactNode; tit return ( <> - +
{ + if (!element) return; + + const checkDirty = () => { + const dirtyFound = element.querySelector('[data-dirty="true"]') !== null; + setIsDirty(dirtyFound); + }; + + checkDirty(); + + const observer = new MutationObserver((mutations) => { + const shouldCheck = mutations.some((mutation) => mutation.type === 'attributes' || mutation.type === 'childList'); + + if (shouldCheck) { + checkDirty(); + } + }); + + observer.observe(element, { + attributes: true, + childList: true, + subtree: true, + }); + + return () => observer.disconnect(); + }, []); + + return { isDirty, ref: setRef }; +} diff --git a/apps/dashboard/src/hooks/use-form-dialog-protection.tsx b/apps/dashboard/src/hooks/use-form-dialog-protection.tsx deleted file mode 100644 index 5a3c654894d..00000000000 --- a/apps/dashboard/src/hooks/use-form-dialog-protection.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { UnsavedChangesAlertDialog } from '@/components/unsaved-changes-alert-dialog'; -import { useBeforeUnload } from '@/hooks/use-before-unload'; -import { useCallback, useState } from 'react'; - -type UseFormDialogProtectionProps = { - onOpenChange?: (open: boolean) => void; // for drawers/sheets -}; -export function useFormDialogProtection(props: UseFormDialogProtectionProps) { - const { onOpenChange } = props; - const [isDirty, setIsDirty] = useState(false); - const [showAlert, setShowAlert] = useState(false); - - useBeforeUnload(isDirty); - - const setRef = useCallback((element: HTMLElement | null) => { - if (element) { - setupElementObserver(element); - } - }, []); - - function setupElementObserver(element: HTMLElement) { - const checkDirty = () => { - const dirtyFound = element.querySelector('[data-dirty="true"]') !== null; - setIsDirty(dirtyFound); - }; - - checkDirty(); - - const observer = new MutationObserver((mutations) => { - const shouldCheck = mutations.some((mutation) => mutation.type === 'attributes' || mutation.type === 'childList'); - - if (shouldCheck) { - checkDirty(); - } - }); - - observer.observe(element, { - attributes: true, - childList: true, - subtree: true, - }); - - return () => observer.disconnect(); - } - - const protectedOnOpenChange = (open: boolean) => { - if (isDirty) { - setShowAlert(true); - } else { - onOpenChange?.(open); - } - }; - - const ProtectionAlert = useCallback( - () => ( - { - setShowAlert(false); - }} - onProceed={() => { - setShowAlert(false); - onOpenChange?.(false); - }} - /> - ), - [onOpenChange, showAlert, setShowAlert] - ); - - return { isDirty, protectedOnOpenChange, ProtectionAlert, ref: setRef }; -} diff --git a/apps/dashboard/src/hooks/use-form-protection.tsx b/apps/dashboard/src/hooks/use-form-protection.tsx new file mode 100644 index 00000000000..b21a28a099d --- /dev/null +++ b/apps/dashboard/src/hooks/use-form-protection.tsx @@ -0,0 +1,48 @@ +import { UnsavedChangesAlertDialog } from '@/components/unsaved-changes-alert-dialog'; +import { useBeforeUnload } from '@/hooks/use-before-unload'; +import { useCallback, useState } from 'react'; +import { useFindDirtyForm } from './use-find-dirty-form'; + +type UseFormProtectionProps = { + onValueChange: (value: T) => void; +}; + +export function useFormProtection(props: UseFormProtectionProps) { + const { onValueChange } = props; + const [showAlert, setShowAlert] = useState(false); + const [pendingChange, setPendingChange] = useState<{ value: T } | null>(null); + const { isDirty, ref } = useFindDirtyForm(); + + useBeforeUnload(isDirty); + + const protectedOnValueChange = useCallback( + (value: T) => { + if (isDirty) { + setShowAlert(true); + setPendingChange({ value }); + } else { + onValueChange(value); + } + }, + [isDirty, onValueChange] + ); + + const ProtectionAlert = () => ( + { + setShowAlert(false); + setPendingChange(null); + }} + onProceed={() => { + if (pendingChange) { + onValueChange(pendingChange.value); + } + setShowAlert(false); + setPendingChange(null); + }} + /> + ); + + return { isDirty, protectedOnValueChange, ProtectionAlert, ref }; +} diff --git a/apps/dashboard/src/pages/create-subscriber.tsx b/apps/dashboard/src/pages/create-subscriber.tsx index 387e9669017..3c3afbd91e6 100644 --- a/apps/dashboard/src/pages/create-subscriber.tsx +++ b/apps/dashboard/src/pages/create-subscriber.tsx @@ -1,7 +1,7 @@ import { Sheet, SheetContent } from '@/components/primitives/sheet'; import { CreateSubscriberForm } from '@/components/subscribers/create-subscriber-form'; import { useCombinedRefs } from '@/hooks/use-combined-refs'; -import { useFormDialogProtection } from '@/hooks/use-form-dialog-protection'; +import { useFormProtection } from '@/hooks/use-form-protection'; import { useOnElementUnmount } from '@/hooks/use-on-element-unmount'; import { cn } from '@/utils/ui'; import { useState } from 'react'; @@ -12,11 +12,11 @@ export function CreateSubscriberPage() { const [open, setOpen] = useState(true); const { - protectedOnOpenChange, + protectedOnValueChange, ProtectionAlert, ref: protectionRef, - } = useFormDialogProtection({ - onOpenChange: setOpen, + } = useFormProtection({ + onValueChange: setOpen, }); const { ref: unmountRef } = useOnElementUnmount({ @@ -29,7 +29,7 @@ export function CreateSubscriberPage() { return ( <> - + {/* Custom overlay since SheetOverlay does not work with modal={false} */}