Skip to content

Commit c0e3d8b

Browse files
pa-lembilalabbad
andauthored
Prevent forms from being closed if there are unsaved changes (#4419)
* add context and functions to display confirm modal * disable rule * add fragment * update modal * update typo * fix function name * avoid re render aftter filter update for form * fix set prevent from form data * update changelog * revert import * re arrange submit * removed file not changed --------- Co-authored-by: bilalabbad <[email protected]>
1 parent d176b60 commit c0e3d8b

File tree

6 files changed

+94
-50
lines changed

6 files changed

+94
-50
lines changed

changelog/4419.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Prevent the form from being closed if there are unsaved changes.

frontend/app/.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"tsconfig.json"
2626
],
2727
"rules": {
28+
"react/prop-types": "off",
2829
"quotes": [
2930
"error",
3031
"double",
@@ -74,4 +75,4 @@
7475
"semi": "error",
7576
"no-trailing-spaces": "error"
7677
}
77-
}
78+
}

frontend/app/src/components/display/slide-over.tsx

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Dialog, Transition } from "@headlessui/react";
2-
import React, { Fragment, useRef } from "react";
2+
import React, { Fragment, useRef, useState } from "react";
33
import { Badge } from "@/components/ui/badge";
44
import { Icon } from "@iconify-icon/react";
55
import { ObjectHelpButton } from "@/components/menu/object-help-button";
66
import { useAtomValue } from "jotai/index";
77
import { currentBranchAtom } from "@/state/atoms/branches.atom";
88
import { IModelSchema } from "@/state/atoms/schema.atom";
9+
import ModalDelete from "../modals/modal-delete";
910

1011
interface Props {
1112
open: boolean;
@@ -15,9 +16,16 @@ interface Props {
1516
offset?: number;
1617
}
1718

19+
interface SlideOverContextProps {
20+
setPreventClose?: (value: boolean) => void;
21+
}
22+
23+
export const SlideOverContext = React.createContext<SlideOverContextProps>({});
24+
1825
export default function SlideOver(props: Props) {
1926
const { open, setOpen, title, offset = 0 } = props;
2027
const initialFocusRef = useRef(null);
28+
const [preventClose, setPreventClose] = useState(false);
2129

2230
// Need to define full classes so tailwind can compile the css
2331
const panelWidth = "w-[400px]";
@@ -27,52 +35,69 @@ export default function SlideOver(props: Props) {
2735
1: "-translate-x-[400px]",
2836
};
2937

38+
const context = {
39+
isOpen: open || (!open && preventClose),
40+
setPreventClose: (value: boolean) => setPreventClose(value),
41+
};
42+
3043
return (
31-
<Transition.Root show={open} as={Fragment}>
32-
<Dialog as="div" className="relative z-10" onClose={setOpen} initialFocus={initialFocusRef}>
33-
<Transition.Child
34-
as={Fragment}
35-
enter="ease-in-out duration-500"
36-
enterFrom="opacity-0"
37-
enterTo="opacity-100"
38-
leave="ease-in-out duration-500"
39-
leaveFrom="opacity-100"
40-
leaveTo="opacity-0">
41-
<div
42-
className="fixed inset-0 bg-black bg-opacity-40 transition-opacity"
43-
data-cy="side-panel-background"
44-
data-testid="side-panel-background"
45-
/>
46-
</Transition.Child>
47-
48-
<div className="before:fixed inset-0 overflow-hidden">
49-
<div className="absolute inset-0 overflow-hidden">
50-
<div className="pointer-events-none fixed inset-y-0 right-0 flex">
51-
<button type="button" tabIndex={-1} ref={initialFocusRef} />
52-
<Transition.Child
53-
as={Fragment}
54-
enter="transform transition ease-in-out duration-500"
55-
enterFrom="translate-x-full"
56-
enterTo={`${offestWidth[offset]}`}
57-
leave="transform transition ease-in-out duration-500"
58-
leaveFrom={`${offestWidth[offset]}`}
59-
leaveTo="translate-x-full">
60-
<Dialog.Panel
61-
className={`bg-custom-white pointer-events-auto shadow-xl flex flex-col ${panelWidth} ${offestWidth[offset]}`}
62-
data-testid="side-panel-container">
63-
<div className="px-4 py-4 sm:px-4 bg-gray-50 border-b">
64-
<div className="w-full">
65-
<Dialog.Title className="text-base leading-6">{title}</Dialog.Title>
44+
<SlideOverContext.Provider value={context}>
45+
<Transition.Root show={open || (!open && preventClose)} as={Fragment}>
46+
<Dialog as="div" className="relative z-10" onClose={setOpen} initialFocus={initialFocusRef}>
47+
<Transition.Child
48+
as={Fragment}
49+
enter="ease-in-out duration-500"
50+
enterFrom="opacity-0"
51+
enterTo="opacity-100"
52+
leave="ease-in-out duration-500"
53+
leaveFrom="opacity-100"
54+
leaveTo="opacity-0">
55+
<div
56+
className="fixed inset-0 bg-black bg-opacity-40 transition-opacity"
57+
data-cy="side-panel-background"
58+
data-testid="side-panel-background"
59+
/>
60+
</Transition.Child>
61+
62+
<div className="before:fixed inset-0 overflow-hidden">
63+
<div className="absolute inset-0 overflow-hidden">
64+
<div className="pointer-events-none fixed inset-y-0 right-0 flex">
65+
<button type="button" tabIndex={-1} ref={initialFocusRef} />
66+
<Transition.Child
67+
as={Fragment}
68+
enter="transform transition ease-in-out duration-500"
69+
enterFrom="translate-x-full"
70+
enterTo={`${offestWidth[offset]}`}
71+
leave="transform transition ease-in-out duration-500"
72+
leaveFrom={`${offestWidth[offset]}`}
73+
leaveTo="translate-x-full">
74+
<Dialog.Panel
75+
className={`bg-custom-white pointer-events-auto shadow-xl flex flex-col ${panelWidth} ${offestWidth[offset]}`}
76+
data-testid="side-panel-container">
77+
<div className="px-4 py-4 sm:px-4 bg-gray-50 border-b">
78+
<div className="w-full">
79+
<Dialog.Title className="text-base leading-6">{title}</Dialog.Title>
80+
</div>
6681
</div>
67-
</div>
68-
{props.children}
69-
</Dialog.Panel>
70-
</Transition.Child>
82+
{props.children}
83+
</Dialog.Panel>
84+
</Transition.Child>
85+
</div>
7186
</div>
7287
</div>
73-
</div>
74-
</Dialog>
75-
</Transition.Root>
88+
</Dialog>
89+
</Transition.Root>
90+
91+
<ModalDelete
92+
title="Closing form"
93+
description={"Are you sure you want to close this form? All unsaved changes will be lost."}
94+
onCancel={() => setOpen(true)}
95+
onDelete={() => setPreventClose(false)}
96+
open={!open && preventClose}
97+
setOpen={() => setPreventClose(false)}
98+
confirmLabel="Close"
99+
/>
100+
</SlideOverContext.Provider>
76101
);
77102
}
78103

frontend/app/src/components/modals/modal-confirm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ interface iProps {
99
isLoading?: boolean;
1010
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
1111
title: string;
12-
description: string | React.ReactNode;
12+
description?: string | React.ReactNode;
1313
onConfirm: Function;
1414
onCancel: Function;
15-
children: ReactNode;
15+
children?: ReactNode;
1616
icon?: string;
1717
}
1818

frontend/app/src/components/ui/form.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "react-hook-form";
2121
import { Spinner } from "@/components/ui/spinner";
2222
import Label, { LabelProps } from "@/components/ui/label";
23+
import { SlideOverContext } from "../display/slide-over";
2324

2425
export type FormRef = ReturnType<typeof useForm>;
2526

@@ -33,12 +34,23 @@ export const Form = React.forwardRef<FormRef, FormProps>(
3334
({ form, defaultValues, className, children, onSubmit, ...props }: FormProps, ref) => {
3435
const currentForm = form ?? useForm({ defaultValues });
3536

37+
const slideOverContext = useContext(SlideOverContext);
38+
3639
useImperativeHandle(ref, () => currentForm);
3740

3841
useEffect(() => {
3942
currentForm.reset(defaultValues);
4043
}, [JSON.stringify(defaultValues)]);
4144

45+
useEffect(() => {
46+
// Stop logic if there is no context to prevent the slide over close
47+
if (!slideOverContext?.setPreventClose) return;
48+
49+
if (!currentForm.formState.isDirty) return;
50+
51+
slideOverContext?.setPreventClose(true);
52+
}, [currentForm.formState.isDirty]);
53+
4254
return (
4355
<FormProvider {...currentForm}>
4456
<form
@@ -47,9 +59,11 @@ export const Form = React.forwardRef<FormRef, FormProps>(
4759
event.stopPropagation();
4860
}
4961

50-
if (!onSubmit) return;
62+
if (onSubmit) currentForm.handleSubmit(onSubmit)(event);
5163

52-
currentForm.handleSubmit(onSubmit)(event);
64+
if (slideOverContext?.setPreventClose) {
65+
slideOverContext?.setPreventClose(false);
66+
}
5367
}}
5468
className={classNames("space-y-4", className)}
5569
{...props}>

frontend/app/src/hooks/usePagination.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,12 @@ const usePagination = (): [tPagination, Function] => {
5353

5454
// Set the pagination in the QSP
5555
const setPagination = (newPagination: tPagination) => {
56+
const newLimit = getVerifiedLimit(newPagination?.limit, config);
57+
const newOffset = getVerifiedOffset(newPagination?.offset, config);
58+
5659
const newValidatedPagination = {
57-
limit: getVerifiedLimit(newPagination?.limit, config),
58-
offset: getVerifiedOffset(newPagination?.offset, config),
60+
limit: newLimit,
61+
offset: newOffset,
5962
};
6063

6164
setPaginationInQueryString(JSON.stringify(newValidatedPagination));

0 commit comments

Comments
 (0)