[Workplace Search] Bypass UnsavedChangesPrompt for tab changes in Display Settings (#97062)

* Move redirect logic into logic file

* Add logic to prevent prompt from triggering when changing tabs

The idea here is to set a boolean flag that sends false for unsavedChanges when switching between tabs and then sets it back after a successful tab change

* Keep sidebar nav item active for both tabs

* Add tests
This commit is contained in:
Scotty Bollinger 2021-04-14 12:28:00 -05:00 committed by GitHub
parent 096536647f
commit 3bc2952216
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 94 additions and 26 deletions

View file

@ -7,7 +7,6 @@
import '../../../../../__mocks__/shallow_useeffect.mock';
import { mockKibanaValues } from '../../../../../__mocks__';
import { setMockValues, setMockActions } from '../../../../../__mocks__';
import { exampleResult } from '../../../../__mocks__/content_sources.mock';
@ -25,11 +24,11 @@ import { DisplaySettings } from './display_settings';
import { FieldEditorModal } from './field_editor_modal';
describe('DisplaySettings', () => {
const { navigateToUrl } = mockKibanaValues;
const { exampleDocuments, searchResultConfig } = exampleResult;
const initializeDisplaySettings = jest.fn();
const setServerData = jest.fn();
const setColorField = jest.fn();
const handleSelectedTabChanged = jest.fn();
const values = {
isOrganization: true,
@ -46,6 +45,7 @@ describe('DisplaySettings', () => {
initializeDisplaySettings,
setServerData,
setColorField,
handleSelectedTabChanged,
});
setMockValues({ ...values });
});
@ -83,7 +83,7 @@ describe('DisplaySettings', () => {
const tabsEl = wrapper.find(EuiTabbedContent);
tabsEl.prop('onTabClick')!(tabs[0]);
expect(navigateToUrl).toHaveBeenCalledWith('/sources/123/display_settings/');
expect(handleSelectedTabChanged).toHaveBeenCalledWith('search_results');
});
it('handles second tab click', () => {
@ -91,7 +91,7 @@ describe('DisplaySettings', () => {
const tabsEl = wrapper.find(EuiTabbedContent);
tabsEl.prop('onTabClick')!(tabs[1]);
expect(navigateToUrl).toHaveBeenCalledWith('/sources/123/display_settings/result_detail');
expect(handleSelectedTabChanged).toHaveBeenCalledWith('result_detail');
});
});

View file

@ -20,19 +20,11 @@ import {
} from '@elastic/eui';
import { clearFlashMessages } from '../../../../../shared/flash_messages';
import { KibanaLogic } from '../../../../../shared/kibana';
import { Loading } from '../../../../../shared/loading';
import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt';
import { AppLogic } from '../../../../app_logic';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
import { SAVE_BUTTON } from '../../../../constants';
import {
DISPLAY_SETTINGS_RESULT_DETAIL_PATH,
DISPLAY_SETTINGS_SEARCH_RESULT_PATH,
getContentSourcePath,
} from '../../../../routes';
import {
UNSAVED_MESSAGE,
DISPLAY_SETTINGS_TITLE,
@ -42,7 +34,7 @@ import {
SEARCH_RESULTS_LABEL,
RESULT_DETAIL_LABEL,
} from './constants';
import { DisplaySettingsLogic } from './display_settings_logic';
import { DisplaySettingsLogic, TabId } from './display_settings_logic';
import { FieldEditorModal } from './field_editor_modal';
import { ResultDetail } from './result_detail';
import { SearchResults } from './search_results';
@ -52,19 +44,20 @@ interface DisplaySettingsProps {
}
export const DisplaySettings: React.FC<DisplaySettingsProps> = ({ tabId }) => {
const { initializeDisplaySettings, setServerData } = useActions(DisplaySettingsLogic);
const { initializeDisplaySettings, setServerData, handleSelectedTabChanged } = useActions(
DisplaySettingsLogic
);
const {
dataLoading,
sourceId,
addFieldModalVisible,
unsavedChanges,
exampleDocuments,
navigatingBetweenTabs,
} = useValues(DisplaySettingsLogic);
const { isOrganization } = useValues(AppLogic);
const hasDocuments = exampleDocuments.length > 0;
const hasUnsavedChanges = hasDocuments && unsavedChanges;
useEffect(() => {
initializeDisplaySettings();
@ -87,12 +80,7 @@ export const DisplaySettings: React.FC<DisplaySettingsProps> = ({ tabId }) => {
] as EuiTabbedContentTab[];
const onSelectedTabChanged = (tab: EuiTabbedContentTab) => {
const path =
tab.id === tabs[1].id
? getContentSourcePath(DISPLAY_SETTINGS_RESULT_DETAIL_PATH, sourceId, isOrganization)
: getContentSourcePath(DISPLAY_SETTINGS_SEARCH_RESULT_PATH, sourceId, isOrganization);
KibanaLogic.values.navigateToUrl(path);
handleSelectedTabChanged(tab.id as TabId);
};
const handleFormSubmit = (e: FormEvent) => {
@ -103,7 +91,7 @@ export const DisplaySettings: React.FC<DisplaySettingsProps> = ({ tabId }) => {
return (
<>
<UnsavedChangesPrompt
hasUnsavedChanges={hasDocuments && unsavedChanges}
hasUnsavedChanges={!navigatingBetweenTabs && hasUnsavedChanges}
messageText={UNSAVED_MESSAGE}
/>
<form onSubmit={handleFormSubmit}>

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__';
import {
LogicMounter,
mockFlashMessageHelpers,
mockHttpValues,
mockKibanaValues,
} from '../../../../../__mocks__';
import { exampleResult } from '../../../../__mocks__/content_sources.mock';
import { nextTick } from '@kbn/test/jest';
@ -25,6 +30,7 @@ import { DisplaySettingsLogic, defaultSearchResultConfig } from './display_setti
describe('DisplaySettingsLogic', () => {
const { http } = mockHttpValues;
const { navigateToUrl } = mockKibanaValues;
const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
const { mount } = new LogicMounter(DisplaySettingsLogic);
@ -40,6 +46,7 @@ describe('DisplaySettingsLogic', () => {
serverRoute: '',
editFieldIndex: null,
dataLoading: true,
navigatingBetweenTabs: false,
addFieldModalVisible: false,
titleFieldHover: false,
urlFieldHover: false,
@ -203,6 +210,12 @@ describe('DisplaySettingsLogic', () => {
});
});
it('setNavigatingBetweenTabs', () => {
DisplaySettingsLogic.actions.setNavigatingBetweenTabs(true);
expect(DisplaySettingsLogic.values.navigatingBetweenTabs).toEqual(true);
});
it('addDetailField', () => {
const newField = { label: 'Monkey', fieldName: 'primate' };
DisplaySettingsLogic.actions.setServerResponseData(serverProps);
@ -351,6 +364,31 @@ describe('DisplaySettingsLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
});
});
describe('handleSelectedTabChanged', () => {
beforeEach(() => {
DisplaySettingsLogic.actions.onInitializeDisplaySettings(serverProps);
});
it('calls sets navigatingBetweenTabs', async () => {
const setNavigatingBetweenTabsSpy = jest.spyOn(
DisplaySettingsLogic.actions,
'setNavigatingBetweenTabs'
);
DisplaySettingsLogic.actions.handleSelectedTabChanged('search_results');
await nextTick();
expect(setNavigatingBetweenTabsSpy).toHaveBeenCalledWith(true);
expect(navigateToUrl).toHaveBeenCalledWith('/p/sources/123/display_settings/');
});
it('calls calls correct route for "result_detail"', async () => {
DisplaySettingsLogic.actions.handleSelectedTabChanged('result_detail');
await nextTick();
expect(navigateToUrl).toHaveBeenCalledWith('/p/sources/123/display_settings/result_detail');
});
});
});
describe('selectors', () => {

View file

@ -16,7 +16,13 @@ import {
flashAPIErrors,
} from '../../../../../shared/flash_messages';
import { HttpLogic } from '../../../../../shared/http';
import { KibanaLogic } from '../../../../../shared/kibana';
import { AppLogic } from '../../../../app_logic';
import {
DISPLAY_SETTINGS_RESULT_DETAIL_PATH,
DISPLAY_SETTINGS_SEARCH_RESULT_PATH,
getContentSourcePath,
} from '../../../../routes';
import { DetailField, SearchResultConfig, OptionValue, Result } from '../../../../types';
import { SourceLogic } from '../../source_logic';
@ -34,6 +40,8 @@ export interface DisplaySettingsInitialData extends DisplaySettingsResponseProps
serverRoute: string;
}
export type TabId = 'search_results' | 'result_detail';
interface DisplaySettingsActions {
initializeDisplaySettings(): void;
setServerData(): void;
@ -51,6 +59,8 @@ interface DisplaySettingsActions {
setDetailFields(result: DropResult): { result: DropResult };
openEditDetailField(editFieldIndex: number | null): number | null;
removeDetailField(index: number): number;
setNavigatingBetweenTabs(navigatingBetweenTabs: boolean): boolean;
handleSelectedTabChanged(tabId: TabId): TabId;
addDetailField(newField: DetailField): DetailField;
updateDetailField(
updatedField: DetailField,
@ -73,6 +83,7 @@ interface DisplaySettingsValues {
serverRoute: string;
editFieldIndex: number | null;
dataLoading: boolean;
navigatingBetweenTabs: boolean;
addFieldModalVisible: boolean;
titleFieldHover: boolean;
urlFieldHover: boolean;
@ -109,6 +120,8 @@ export const DisplaySettingsLogic = kea<
setDetailFields: (result: DropResult) => ({ result }),
openEditDetailField: (editFieldIndex: number | null) => editFieldIndex,
removeDetailField: (index: number) => index,
setNavigatingBetweenTabs: (navigatingBetweenTabs: boolean) => navigatingBetweenTabs,
handleSelectedTabChanged: (tabId: TabId) => tabId,
addDetailField: (newField: DetailField) => newField,
updateDetailField: (updatedField: DetailField, index: number) => ({ updatedField, index }),
toggleFieldEditorModal: () => true,
@ -224,6 +237,12 @@ export const DisplaySettingsLogic = kea<
onInitializeDisplaySettings: () => false,
},
],
navigatingBetweenTabs: [
false,
{
setNavigatingBetweenTabs: (_, navigatingBetweenTabs) => navigatingBetweenTabs,
},
],
addFieldModalVisible: [
false,
{
@ -330,6 +349,26 @@ export const DisplaySettingsLogic = kea<
toggleFieldEditorModal: () => {
clearFlashMessages();
},
handleSelectedTabChanged: async (tabId, breakpoint) => {
const { isOrganization } = AppLogic.values;
const { sourceId } = values;
const path =
tabId === 'result_detail'
? getContentSourcePath(DISPLAY_SETTINGS_RESULT_DETAIL_PATH, sourceId, isOrganization)
: getContentSourcePath(DISPLAY_SETTINGS_SEARCH_RESULT_PATH, sourceId, isOrganization);
// This method is needed because the shared `UnsavedChangesPrompt` component is triggered
// when navigating between tabs. We set a boolean flag that tells the prompt there are no
// unsaved changes when navigating between the tabs and reset it one the transition is complete
// in order to restore the intended functionality when navigating away with unsaved changes.
actions.setNavigatingBetweenTabs(true);
await breakpoint();
KibanaLogic.values.navigateToUrl(path);
actions.setNavigatingBetweenTabs(false);
},
}),
});

View file

@ -45,7 +45,10 @@ export const SourceSubNav: React.FC = () => {
<SideNavLink to={getContentSourcePath(SOURCE_SCHEMAS_PATH, id, isOrganization)}>
{NAV.SCHEMA}
</SideNavLink>
<SideNavLink to={getContentSourcePath(SOURCE_DISPLAY_SETTINGS_PATH, id, isOrganization)}>
<SideNavLink
shouldShowActiveForSubroutes
to={getContentSourcePath(SOURCE_DISPLAY_SETTINGS_PATH, id, isOrganization)}
>
{NAV.DISPLAY_SETTINGS}
</SideNavLink>
</>