feat(explore): Show confirmation modal if user exits Explore without saving changes (#19993)

* feat(explore): Show confirmation modal if user exits Explore without saving changes

* Fix calling cleanup func unnecessarily

* Fix comparing AdhocMetric instance with JSON object

* Replace sliceFormData with the initial form data
This commit is contained in:
Kamil Gabryjelski
2022-05-12 12:24:29 +02:00
committed by GitHub
parent 26c81a70e7
commit ca9766c109
5 changed files with 119 additions and 76 deletions

View File

@@ -17,12 +17,18 @@
* under the License.
*/
/* eslint camelcase: 0 */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { styled, t, css, useTheme, logging } from '@superset-ui/core';
import { debounce, pick } from 'lodash';
import { debounce, pick, isEmpty } from 'lodash';
import { Resizable } from 're-resizable';
import { useChangeEffect } from 'src/hooks/useChangeEffect';
import { usePluginContext } from 'src/components/DynamicPlugins';
@@ -43,7 +49,11 @@ import * as chartActions from 'src/components/Chart/chartAction';
import { fetchDatasourceMetadata } from 'src/dashboard/actions/datasources';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import { mergeExtraFormData } from 'src/dashboard/components/nativeFilters/utils';
import { postFormData, putFormData } from 'src/explore/exploreUtils/formData';
import {
getFormDataDiffs,
postFormData,
putFormData,
} from 'src/explore/exploreUtils/formData';
import { useTabId } from 'src/hooks/useTabId';
import ExploreChartPanel from '../ExploreChartPanel';
import ConnectedControlPanelsContainer from '../ControlPanelsContainer';
@@ -216,6 +226,11 @@ const updateHistory = debounce(
1000,
);
const handleUnloadEvent = e => {
e.preventDefault();
e.returnValue = 'Controls changed';
};
function ExploreViewContainer(props) {
const dynamicPluginContext = usePluginContext();
const dynamicPlugin = dynamicPluginContext.dynamicPlugins[props.vizType];
@@ -236,6 +251,9 @@ function ExploreViewContainer(props) {
const theme = useTheme();
const isBeforeUnloadActive = useRef(false);
const initialFormData = useRef(props.form_data);
const defaultSidebarsWidth = {
controls_width: 320,
datasource_width: 300,
@@ -365,6 +383,27 @@ function ExploreViewContainer(props) {
};
}, [handleKeydown, previousHandleKeyDown]);
useEffect(() => {
const formDataChanged = !isEmpty(
getFormDataDiffs(initialFormData.current, props.form_data),
);
if (formDataChanged && !isBeforeUnloadActive.current) {
window.addEventListener('beforeunload', handleUnloadEvent);
isBeforeUnloadActive.current = true;
}
if (!formDataChanged && isBeforeUnloadActive.current) {
window.removeEventListener('beforeunload', handleUnloadEvent);
isBeforeUnloadActive.current = false;
}
}, [props.form_data]);
// cleanup beforeunload event listener
// we use separate useEffect to call it only on component unmount instead of on every form data change
useEffect(
() => () => window.removeEventListener('beforeunload', handleUnloadEvent),
[],
);
useEffect(() => {
if (wasDynamicPluginLoading && !isDynamicPluginLoading) {
// reload the controls now that we actually have the control config