mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-29 10:24:20 +01:00
(feat) alert unsaved changes
if the user tries to quit the product form, he will be alerted about unsaved changes, if any
This commit is contained in:
parent
c381c985d2
commit
6b224d7db1
@ -23,6 +23,7 @@ interface FabModalProps {
|
||||
customHeader?: ReactNode,
|
||||
customFooter?: ReactNode,
|
||||
onConfirm?: (event: BaseSyntheticEvent) => void,
|
||||
onClose?: (event: BaseSyntheticEvent) => void,
|
||||
preventConfirm?: boolean,
|
||||
onCreation?: () => void,
|
||||
onConfirmSendFormId?: string,
|
||||
@ -31,7 +32,7 @@ interface FabModalProps {
|
||||
/**
|
||||
* This component is a template for a modal dialog that wraps the application style
|
||||
*/
|
||||
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton, className, width = 'sm', closeButton, customHeader, customFooter, onConfirm, preventConfirm, onCreation, onConfirmSendFormId }) => {
|
||||
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton, className, width = 'sm', closeButton, customHeader, customFooter, onConfirm, onClose, preventConfirm, onCreation, onConfirmSendFormId }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
useEffect(() => {
|
||||
@ -40,12 +41,20 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
/**
|
||||
* Callback triggered when the user request to close the modal without confirming.
|
||||
*/
|
||||
const handleClose = (event) => {
|
||||
if (typeof onClose === 'function') onClose(event);
|
||||
toggleModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}
|
||||
className={`fab-modal fab-modal-${width} ${className || ''}`}
|
||||
overlayClassName="fab-modal-overlay"
|
||||
onRequestClose={toggleModal}>
|
||||
{closeButton && <FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.fab_modal.close')}</FabButton>}
|
||||
onRequestClose={handleClose}>
|
||||
{closeButton && <FabButton className="modal-btn--close" onClick={handleClose}>{t('app.shared.fab_modal.close')}</FabButton>}
|
||||
<div className="fab-modal-header">
|
||||
{!customHeader && <h1>{ title }</h1>}
|
||||
{customHeader && customHeader}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import React, { PropsWithChildren, useEffect } from 'react';
|
||||
import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';
|
||||
import { UIRouter } from '@uirouter/angularjs';
|
||||
import { FormState } from 'react-hook-form/dist/types/form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import Deferred from '../../lib/deferred';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface UnsavedFormAlertProps<TFieldValues> {
|
||||
uiRouter: UIRouter,
|
||||
@ -9,20 +12,101 @@ interface UnsavedFormAlertProps<TFieldValues> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert the user about unsaved changes in the given form, before leaving the current page
|
||||
* Alert the user about unsaved changes in the given form, before leaving the current page.
|
||||
* This component is highly dependent of these external libraries:
|
||||
* - [react-hook-form](https://react-hook-form.com/)
|
||||
* - [ui-router](https://ui-router.github.io/)
|
||||
*/
|
||||
export const UnsavedFormAlert = <TFieldValues extends FieldValues>({ uiRouter, formState, children }: PropsWithChildren<UnsavedFormAlertProps<TFieldValues>>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [showAlertModal, setShowAlertModal] = useState<boolean>(false);
|
||||
const [promise, setPromise] = useState<Deferred<boolean>>(null);
|
||||
const [dirty, setDirty] = useState<boolean>(formState.isDirty);
|
||||
|
||||
useEffect(() => {
|
||||
const submitStatus = (!formState.isSubmitting && (!formState.isSubmitted || !formState.isSubmitSuccessful));
|
||||
setDirty(submitStatus && Object.keys(formState.dirtyFields).length > 0);
|
||||
}, [formState]);
|
||||
|
||||
/**
|
||||
* Check if the current form is dirty. If so, show the confirmation modal and return a promise
|
||||
*/
|
||||
const alertOnDirtyForm = (isDirty: boolean): Promise<boolean>|void => {
|
||||
if (isDirty) {
|
||||
toggleAlertModal();
|
||||
const userChoicePromise = new Deferred<boolean>();
|
||||
setPromise(userChoicePromise);
|
||||
return userChoicePromise.promise;
|
||||
}
|
||||
};
|
||||
|
||||
// memoised version of the alertOnDirtyForm function, will be updated only when the form becames dirty
|
||||
const alertDirty = useCallback<() => Promise<boolean>|void>(() => alertOnDirtyForm(dirty), [dirty]);
|
||||
|
||||
// we should place this useEffect after the useCallback declaration (because it's a scoped variable)
|
||||
useEffect(() => {
|
||||
const { transitionService, globals: { current } } = uiRouter;
|
||||
transitionService.onBefore({ from: current.name }, () => {
|
||||
const { isDirty } = formState;
|
||||
console.log('transition start', isDirty);
|
||||
});
|
||||
}, []);
|
||||
const deregisters = transitionService.onBefore({ from: current.name }, alertDirty);
|
||||
return () => {
|
||||
deregisters();
|
||||
};
|
||||
}, [alertDirty]);
|
||||
|
||||
/**
|
||||
* When the user tries to close the current page (tab/window), we alert him about unsaved changes
|
||||
*/
|
||||
const alertOnExit = (event: BeforeUnloadEvent, isDirty: boolean) => {
|
||||
if (isDirty) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
};
|
||||
|
||||
// memoised version of the alertOnExit function, will be updated only when the form becames dirty
|
||||
const alertExit = useCallback<(event: BeforeUnloadEvent) => void>((event) => alertOnExit(event, dirty), [dirty]);
|
||||
|
||||
// we should place this useEffect after the useCallback declaration (because it's a scoped variable)
|
||||
useEffect(() => {
|
||||
window.addEventListener('beforeunload', alertExit);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', alertExit);
|
||||
};
|
||||
}, [alertExit]);
|
||||
|
||||
/**
|
||||
* Hide/show the alert modal "you have some unsaved content, are you sure you want to leave?"
|
||||
*/
|
||||
const toggleAlertModal = () => {
|
||||
setShowAlertModal(!showAlertModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has choosen: continue and exit
|
||||
*/
|
||||
const handleConfirmation = () => {
|
||||
promise.resolve(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has choosen: cancel and stay
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
promise.resolve(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="unsaved-form-alert">
|
||||
{children}
|
||||
<FabModal isOpen={showAlertModal}
|
||||
toggleModal={toggleAlertModal}
|
||||
confirmButton={t('app.shared.unsaved_form_alert.confirmation_button')}
|
||||
title={t('app.shared.unsaved_form_alert.modal_title')}
|
||||
onConfirm={handleConfirmation}
|
||||
onClose={handleCancel}
|
||||
closeButton>
|
||||
{t('app.shared.unsaved_form_alert.confirmation_message')}
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -227,8 +227,8 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
<FabButton className="main-action-btn" onClick={handleSubmit(saveProduct)}>{t('app.admin.store.product_form.save')}</FabButton>
|
||||
</div>
|
||||
</header>
|
||||
<UnsavedFormAlert uiRouter={uiRouter} formState={formState} />
|
||||
<form className="product-form" onSubmit={handleSubmit(onSubmit)}>
|
||||
<UnsavedFormAlert uiRouter={uiRouter} formState={formState} />
|
||||
<div className='tabs'>
|
||||
<p className={!stockTab ? 'is-active' : ''} onClick={() => setStockTab(false)}>{t('app.admin.store.product_form.product_parameters')}</p>
|
||||
<p className={stockTab ? 'is-active' : ''} onClick={() => setStockTab(true)}>{t('app.admin.store.product_form.stock_management')}</p>
|
||||
|
22
app/frontend/src/javascript/lib/deferred.ts
Normal file
22
app/frontend/src/javascript/lib/deferred.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// This is a kind of promise you can resolve from outside the function callback.
|
||||
// Credits to https://stackoverflow.com/a/71158892/1039377
|
||||
export default class Deferred<T> {
|
||||
public readonly promise: Promise<T>;
|
||||
private resolveFn!: (value: T | PromiseLike<T>) => void;
|
||||
private rejectFn!: (reason?: unknown) => void;
|
||||
|
||||
public constructor () {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolveFn = resolve;
|
||||
this.rejectFn = reject;
|
||||
});
|
||||
}
|
||||
|
||||
public reject (reason?: unknown): void {
|
||||
this.rejectFn(reason);
|
||||
}
|
||||
|
||||
public resolve (param: T): void {
|
||||
this.resolveFn(param);
|
||||
}
|
||||
}
|
@ -564,3 +564,7 @@ en:
|
||||
order_item:
|
||||
total: "Total"
|
||||
client: "Client"
|
||||
unsaved_form_alert:
|
||||
modal_title: "You have some unsaved changes"
|
||||
confirmation_message: "If you leave this page, your changes will be lost. Are you sure you want to continue?"
|
||||
confirmation_button: "Yes, don't save"
|
||||
|
Loading…
Reference in New Issue
Block a user