diff --git a/src/command-pallette/CommandPalletteUI/CommandPaletteUI.tsx b/src/command-pallette/CommandPalletteUI/CommandPaletteUI.tsx index da271e378f..5c0bf224f3 100644 --- a/src/command-pallette/CommandPalletteUI/CommandPaletteUI.tsx +++ b/src/command-pallette/CommandPalletteUI/CommandPaletteUI.tsx @@ -1,5 +1,5 @@ import { ReactNode, useEffect, useRef, useState } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { useEventListener } from 'hooks/useEventListener'; import { addHistoryEntry, getHistoryEntries } from './search-history'; import { activeNamespaceIdState } from 'state/activeNamespaceIdAtom'; @@ -14,11 +14,9 @@ import { useSearchResults } from './useSearchResults'; import { K8sResource } from 'types'; import { Button, Icon, Input } from '@ui5/webcomponents-react'; import './CommandPaletteUI.scss'; -import { handleActionIfFormOpen } from 'shared/components/UnsavedMessageBox/helpers'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; import { showKymaCompanionState } from 'state/companion/showKymaCompanionAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; import { SCREEN_SIZE_BREAKPOINT_M } from './types'; +import { useFormNavigation } from 'shared/hooks/useFormNavigation'; function Background({ hide, @@ -58,10 +56,7 @@ export function CommandPaletteUI({ shellbarWidth, }: CommandPaletteProps) { const namespace = useRecoilValue(activeNamespaceIdState); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); + const { navigateSafely } = useFormNavigation(); const [query, setQuery] = useState(''); const [originalQuery, setOriginalQuery] = useState(''); @@ -153,17 +148,10 @@ export function CommandPaletteUI({ if (key === 'Enter' && results[0]) { // choose current entry e.preventDefault(); - - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - addHistoryEntry(results[0].query); - results[0].onActivate(); - }, - ); + navigateSafely(() => { + addHistoryEntry(results[0].query); + results[0].onActivate(); + }); } else if (key === 'Tab') { e.preventDefault(); // fill search with active history entry diff --git a/src/command-pallette/CommandPalletteUI/ResultsList/ResultsList.tsx b/src/command-pallette/CommandPalletteUI/ResultsList/ResultsList.tsx index 0230d86c18..18850c1202 100644 --- a/src/command-pallette/CommandPalletteUI/ResultsList/ResultsList.tsx +++ b/src/command-pallette/CommandPalletteUI/ResultsList/ResultsList.tsx @@ -6,10 +6,7 @@ import { addHistoryEntry } from '../search-history'; import './ResultsList.scss'; import { useTranslation } from 'react-i18next'; import { LOADING_INDICATOR } from '../types'; -import { useRecoilState } from 'recoil'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; -import { handleActionIfFormOpen } from 'shared/components/UnsavedMessageBox/helpers'; +import { useFormNavigation } from 'shared/hooks/useFormNavigation'; function scrollInto(element: Element) { element.scrollIntoView({ @@ -36,10 +33,7 @@ export function ResultsList({ }: ResultsListProps) { const listRef = useRef(null); const { t } = useTranslation(); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); + const { navigateSafely } = useFormNavigation(); //todo 2 const isLoading = results.find((r: any) => r.type === LOADING_INDICATOR); @@ -71,16 +65,10 @@ export function ResultsList({ scrollInto(listRef.current!.children[activeIndex - 1]); } else if (key === 'Enter' && results?.[activeIndex]) { e.preventDefault(); - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - addHistoryEntry(results[activeIndex].query); - results[activeIndex].onActivate(); - }, - ); + navigateSafely(() => { + addHistoryEntry(results[activeIndex].query); + results[activeIndex].onActivate(); + }); } }, [activeIndex, results, isHistoryMode], @@ -97,16 +85,10 @@ export function ResultsList({ activeIndex={activeIndex} setActiveIndex={setActiveIndex} onItemClick={() => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - addHistoryEntry(result.query); - result.onActivate(); - }, - ); + navigateSafely(() => { + addHistoryEntry(result.query); + result.onActivate(); + }); }} /> )) diff --git a/src/header/Header.tsx b/src/header/Header.tsx index c44cb9cf31..a68c922c66 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from 'react'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Avatar, ShellBar, @@ -9,6 +9,7 @@ import { import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; +import { useFormNavigation } from 'shared/hooks/useFormNavigation'; import { useFeature } from 'hooks/useFeature'; import { useAvailableNamespaces } from 'hooks/useAvailableNamespaces'; import { useCheckSAPUser } from 'hooks/useCheckSAPUser'; @@ -16,8 +17,6 @@ import { useCheckSAPUser } from 'hooks/useCheckSAPUser'; import { clustersState } from 'state/clustersAtom'; import { clusterState } from 'state/clusterAtom'; import { showKymaCompanionState } from 'state/companion/showKymaCompanionAtom'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; import { Logo } from './Logo/Logo'; import { SidebarSwitcher } from './SidebarSwitcher/SidebarSwitcher'; @@ -25,7 +24,6 @@ import { HeaderMenu } from './HeaderMenu'; import { CommandPaletteSearchBar } from 'command-pallette/CommandPalletteUI/CommandPaletteSearchBar'; import { SnowFeature } from './SnowFeature'; -import { handleActionIfFormOpen } from 'shared/components/UnsavedMessageBox/helpers'; import { configFeaturesNames } from 'state/types'; import './Header.scss'; @@ -38,16 +36,13 @@ export function Header() { const { t } = useTranslation(); const navigate = useNavigate(); + const { navigateSafely } = useFormNavigation(); const { isEnabled: isFeedbackEnabled, link: feedbackLink } = useFeature( configFeaturesNames.FEEDBACK, ); const cluster = useRecoilValue(clusterState); const clusters = useRecoilValue(clustersState); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); const { isEnabled: isKymaCompanionEnabled } = useFeature('KYMA_COMPANION'); const setShowCompanion = useSetRecoilState(showKymaCompanionState); @@ -83,13 +78,7 @@ export function Header() { window.location.pathname !== '/clusters' && } onLogoClick={() => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => navigate('/clusters'), - ); + navigateSafely(() => navigate('/clusters')); setShowCompanion({ show: false, fullScreen: false, @@ -103,22 +92,16 @@ export function Header() { } menuItems={window.location.pathname !== '/clusters' ? clustersList : []} onMenuItemClick={e => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - e.detail.item.textContent === - t('clusters.overview.title-all-clusters') - ? navigate('/clusters') - : navigate( - `/cluster/${encodeURIComponent( - e.detail.item?.textContent ?? '', - )}`, - ); - }, - ); + navigateSafely(() => { + e.detail.item.textContent === + t('clusters.overview.title-all-clusters') + ? navigate('/clusters') + : navigate( + `/cluster/${encodeURIComponent( + e.detail.item?.textContent ?? '', + )}`, + ); + }); setShowCompanion({ show: false, fullScreen: false, diff --git a/src/header/NamespaceChooser/NamespaceChooser.tsx b/src/header/NamespaceChooser/NamespaceChooser.tsx index fd10e3e5da..0d4b9e3bc1 100644 --- a/src/header/NamespaceChooser/NamespaceChooser.tsx +++ b/src/header/NamespaceChooser/NamespaceChooser.tsx @@ -1,23 +1,18 @@ -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { useTranslation } from 'react-i18next'; import { useUrl } from 'hooks/useUrl'; import { useMatch, useNavigate } from 'react-router'; import { namespacesState } from 'state/namespacesAtom'; import { SideNavigationSubItem } from '@ui5/webcomponents-react'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; -import { handleActionIfFormOpen } from 'shared/components/UnsavedMessageBox/helpers'; +import { useFormNavigation } from 'shared/hooks/useFormNavigation'; export function NamespaceChooser() { const { t } = useTranslation(); const navigate = useNavigate(); const { namespaceUrl } = useUrl(); const allNamespaces = useRecoilValue(namespacesState); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); + const { navigateSafely } = useFormNavigation(); const { resourceType = '' } = useMatch({ @@ -31,12 +26,8 @@ export function NamespaceChooser() { text={t('navigation.all-namespaces')} data-key="all-namespaces" onClick={() => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => navigate(namespaceUrl(resourceType, { namespace: '-all-' })), + navigateSafely(() => + navigate(namespaceUrl(resourceType, { namespace: '-all-' })), ); }} />, @@ -49,17 +40,12 @@ export function NamespaceChooser() { key={ns} data-key={ns} onClick={e => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => - navigate( - namespaceUrl(resourceType, { - namespace: e.target.dataset.key ?? undefined, - }), - ), + navigateSafely(() => + navigate( + namespaceUrl(resourceType, { + namespace: e.target.dataset.key ?? undefined, + }), + ), ); }} />, diff --git a/src/shared/ResourceForm/components/ResourceForm.js b/src/shared/ResourceForm/components/ResourceForm.js index f0abd51f4c..8480e00e0e 100644 --- a/src/shared/ResourceForm/components/ResourceForm.js +++ b/src/shared/ResourceForm/components/ResourceForm.js @@ -14,25 +14,15 @@ import jp from 'jsonpath'; import { Form, FormItem } from '@ui5/webcomponents-react'; import { UI5Panel } from 'shared/components/UI5Panel/UI5Panel'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { editViewModeState } from 'state/preferences/editViewModeAtom'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; import { createPortal } from 'react-dom'; import { UnsavedMessageBox } from 'shared/components/UnsavedMessageBox/UnsavedMessageBox'; -import { cloneDeep } from 'lodash'; import { getDescription, SchemaContext } from 'shared/helpers/schema'; -import './ResourceForm.scss'; import { columnLayoutState } from 'state/columnLayoutAtom'; - -export const excludeStatus = resource => { - const modifiedResource = cloneDeep(resource); - delete modifiedResource.status; - delete modifiedResource.metadata?.resourceVersion; - delete modifiedResource.metadata?.managedFields; - return modifiedResource; -}; +import { useFormEditTracking } from 'shared/hooks/useFormEditTracking'; +import './ResourceForm.scss'; export function ResourceForm({ pluralKind, // used for the request path @@ -101,37 +91,9 @@ export function ResourceForm({ } const editViewMode = useRecoilValue(editViewModeState); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); - const { leavingForm } = isFormOpen; const [editorError, setEditorError] = useState(null); - useEffect(() => { - // Check if form is opened based on width - if (leavingForm && formElementRef?.current?.clientWidth !== 0) { - if ( - JSON.stringify(excludeStatus(resource)) !== - JSON.stringify(excludeStatus(initialResource)) || - editorError - ) { - setIsResourceEdited({ ...isResourceEdited, isEdited: true }); - } - - if ( - JSON.stringify(excludeStatus(resource)) === - JSON.stringify(excludeStatus(initialResource)) && - !editorError - ) { - setIsResourceEdited({ isEdited: false }); - setIsFormOpen({ formOpen: false }); - if (isResourceEdited.discardAction) isResourceEdited.discardAction(); - } - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [leavingForm]); + useFormEditTracking(resource, initialResource, editorError); const { t } = useTranslation(); const createResource = useCreateResource({ diff --git a/src/shared/ResourceForm/components/Single.js b/src/shared/ResourceForm/components/Single.js index 8a373307ce..bd4a271911 100644 --- a/src/shared/ResourceForm/components/Single.js +++ b/src/shared/ResourceForm/components/Single.js @@ -2,12 +2,9 @@ import { useEffect, useRef } from 'react'; import classnames from 'classnames'; import { ResourceFormWrapper } from './Wrapper'; -import { excludeStatus } from './ResourceForm'; -import { useRecoilState } from 'recoil'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; import { createPortal } from 'react-dom'; import { UnsavedMessageBox } from 'shared/components/UnsavedMessageBox/UnsavedMessageBox'; +import { useFormEditTracking } from 'shared/hooks/useFormEditTracking'; export function SingleForm({ formElementRef, @@ -21,11 +18,6 @@ export function SingleForm({ ...props }) { const validationRef = useRef(true); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); - const { leavingForm } = isFormOpen; useEffect(() => { if (setCustomValid) { @@ -34,26 +26,7 @@ export function SingleForm({ validationRef.current = true; }, [resource, children, setCustomValid]); - useEffect(() => { - if (initialResource && leavingForm) { - if ( - JSON.stringify(excludeStatus(resource)) !== - JSON.stringify(excludeStatus(initialResource)) - ) { - setIsResourceEdited({ ...isResourceEdited, isEdited: true }); - } - - if ( - JSON.stringify(excludeStatus(resource)) === - JSON.stringify(excludeStatus(initialResource)) - ) { - setIsResourceEdited({ isEdited: false }); - setIsFormOpen({ formOpen: false }); - if (isResourceEdited.discardAction) isResourceEdited.discardAction(); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [leavingForm]); + useFormEditTracking(resource, initialResource); return (
diff --git a/src/shared/ResourceForm/useCreateResource.js b/src/shared/ResourceForm/useCreateResource.js index 63de86c5f5..4726e383cd 100644 --- a/src/shared/ResourceForm/useCreateResource.js +++ b/src/shared/ResourceForm/useCreateResource.js @@ -13,7 +13,6 @@ import { useUrl } from 'hooks/useUrl'; import { usePrepareLayout } from 'shared/hooks/usePrepareLayout'; import { columnLayoutState } from 'state/columnLayoutAtom'; import { isResourceEditedState } from 'state/resourceEditedAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; import { extractApiGroupVersion } from 'resources/Roles/helpers'; import { useNavigate } from 'react-router'; @@ -39,7 +38,6 @@ export function useCreateResource({ const { scopedUrl } = useUrl(); const [layoutColumn, setLayoutColumn] = useRecoilState(columnLayoutState); const setIsResourceEdited = useSetRecoilState(isResourceEditedState); - const setIsFormOpen = useSetRecoilState(isFormOpenState); const { nextQuery, nextLayout } = usePrepareLayout(layoutNumber); @@ -133,10 +131,6 @@ export function useCreateResource({ setIsResourceEdited({ isEdited: false, }); - - setIsFormOpen({ - formOpen: false, - }); }; const handleCreate = async () => { diff --git a/src/shared/components/DynamicPageComponent/DynamicPageComponent.js b/src/shared/components/DynamicPageComponent/DynamicPageComponent.js index 5d63080ced..037c909d78 100644 --- a/src/shared/components/DynamicPageComponent/DynamicPageComponent.js +++ b/src/shared/components/DynamicPageComponent/DynamicPageComponent.js @@ -21,8 +21,8 @@ import { columnLayoutState } from 'state/columnLayoutAtom'; import { HintButton } from '../DescriptionHint/DescriptionHint'; import { isResourceEditedState } from 'state/resourceEditedAtom'; import { isFormOpenState } from 'state/formOpenAtom'; -import { handleActionIfFormOpen } from '../UnsavedMessageBox/helpers'; import { useNavigate, useSearchParams } from 'react-router'; +import { useFormNavigation } from 'shared/hooks/useFormNavigation'; const useGetHeaderHeight = (dynamicPageRef, tabContainerRef) => { const [headerHeight, setHeaderHeight] = useState(undefined); @@ -119,6 +119,7 @@ export const DynamicPageComponent = ({ isResourceEditedState, ); const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); + const { navigateSafely } = useFormNavigation(); const [searchParams] = useSearchParams(); const showEdit = searchParams.get('showEdit'); const editColumn = searchParams.get('editColumn'); @@ -256,13 +257,7 @@ export const DynamicPageComponent = ({ design="Transparent" icon="decline" onClick={() => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => handleColumnClose(), - ); + navigateSafely(() => handleColumnClose()); }} /> @@ -371,51 +366,42 @@ export const DynamicPageComponent = ({ } const newTabName = e.detail.tab.getAttribute('data-mode'); - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - setSelectedSectionIdState(newTabName); - setIsResourceEdited({ - isEdited: false, - }); - - if (newTabName === 'edit') { - const params = new URLSearchParams(); - if (layoutColumn.layout !== 'OneColumn') { - params.set('layout', layoutColumn.layout); - if (title === 'Modules') { - params.set('editColumn', 'startColumn'); - } + navigateSafely(() => { + setSelectedSectionIdState(newTabName); + + if (newTabName === 'edit') { + const params = new URLSearchParams(); + if (layoutColumn.layout !== 'OneColumn') { + params.set('layout', layoutColumn.layout); + if (title === 'Modules') { + params.set('editColumn', 'startColumn'); } - params.set('showEdit', 'true'); - - setLayoutColumn({ - ...layoutColumn, - showEdit: { - ...currColumnInfo, - resource: null, - }, - }); - setIsFormOpen({ formOpen: true }); - navigate(`${window.location.pathname}?${params.toString()}`); - } else { - setLayoutColumn({ - ...layoutColumn, - showEdit: null, - }); - navigate( - `${window.location.pathname}${ - layoutColumn.layout === 'OneColumn' - ? '' - : '?layout=' + layoutColumn.layout - }`, - ); } - }, - ); + params.set('showEdit', 'true'); + + setLayoutColumn({ + ...layoutColumn, + showEdit: { + ...currColumnInfo, + resource: null, + }, + }); + setIsFormOpen({ formOpen: true }); + navigate(`${window.location.pathname}?${params.toString()}`); + } else { + setLayoutColumn({ + ...layoutColumn, + showEdit: null, + }); + navigate( + `${window.location.pathname}${ + layoutColumn.layout === 'OneColumn' + ? '' + : '?layout=' + layoutColumn.layout + }`, + ); + } + }); }} > { const overrides = namespace === '-all-' ? { namespace } : {}; @@ -421,13 +416,7 @@ export const GenericList = ({ onRowClick={e => { const selection = window.getSelection().toString(); if (!hasDetailsView || selection.length > 0) return; - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => handleRowClick(e), - ); + navigateSafely(() => handleRowClick(e)); }} headerRow={ {}, @@ -27,10 +26,8 @@ export const ModalWithForm = ({ }) => { const { t } = useTranslation(); const [isOpen, setOpen] = useState(false); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); + const setIsFormOpen = useSetRecoilState(isFormOpenState); + const { navigateSafely } = useFormNavigation(); const { isValid, @@ -132,13 +129,7 @@ export const ModalWithForm = ({ , - , + , ]} > {t('common.messages.discard-changes-warning')} diff --git a/src/shared/components/UnsavedMessageBox/helpers.ts b/src/shared/components/UnsavedMessageBox/helpers.ts deleted file mode 100644 index 1e7d400d7c..0000000000 --- a/src/shared/components/UnsavedMessageBox/helpers.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IsResourceEditedState } from 'state/resourceEditedAtom'; -import { SetterOrUpdater } from 'recoil'; -import { IsFormOpenState } from 'state/formOpenAtom'; - -export function handleActionIfFormOpen( - isResourceEdited: IsResourceEditedState, - setIsResourceEdited: SetterOrUpdater, - isFormOpen: IsFormOpenState, - setIsFormOpen: SetterOrUpdater, - action: Function, -) { - if (isFormOpen.formOpen) { - setIsResourceEdited({ - ...isResourceEdited, - discardAction: () => action(), - }); - setIsFormOpen({ formOpen: true, leavingForm: true }); - return; - } - action(); -} diff --git a/src/shared/hooks/useFormEditTracking.js b/src/shared/hooks/useFormEditTracking.js new file mode 100644 index 0000000000..c5d2a66f83 --- /dev/null +++ b/src/shared/hooks/useFormEditTracking.js @@ -0,0 +1,41 @@ +import { cloneDeep, isEqual } from 'lodash'; +import { useEffect, useMemo } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isResourceEditedState } from 'state/resourceEditedAtom'; + +const excludeStatus = resource => { + if (!resource) return null; + const modifiedResource = cloneDeep(resource); + delete modifiedResource.status; + delete modifiedResource.metadata?.resourceVersion; + delete modifiedResource.metadata?.managedFields; + return modifiedResource; +}; + +export function useFormEditTracking( + resource, + initialResource, + editorError = false, +) { + const setIsResourceEdited = useSetRecoilState(isResourceEditedState); + + const excludedResource = useMemo(() => excludeStatus(resource), [resource]); + + const excludedInitialResource = useMemo( + () => excludeStatus(initialResource), + [initialResource], + ); + + const isEdited = useMemo(() => { + if (!excludedResource || !excludedInitialResource) return false; + return !isEqual(excludedResource, excludedInitialResource) || editorError; + }, [excludedResource, excludedInitialResource, editorError]); + + useEffect(() => { + if (isEdited) { + setIsResourceEdited(prevState => ({ ...prevState, isEdited: true })); + } else { + setIsResourceEdited({ isEdited: false }); + } + }, [isEdited, setIsResourceEdited]); +} diff --git a/src/shared/hooks/useFormNavigation.ts b/src/shared/hooks/useFormNavigation.ts new file mode 100644 index 0000000000..68da0d3143 --- /dev/null +++ b/src/shared/hooks/useFormNavigation.ts @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import { useRecoilState } from 'recoil'; +import { isResourceEditedState } from 'state/resourceEditedAtom'; +import { isFormOpenState } from 'state/formOpenAtom'; + +export function useFormNavigation() { + const [isResourceEdited, setIsResourceEdited] = useRecoilState( + isResourceEditedState, + ); + const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); + + const navigateSafely = useCallback( + (action: Function) => { + // Store the navigation action for later use if the user confirms + setIsResourceEdited(prevState => ({ + ...prevState, + discardAction: () => action(), + })); + + // Check if we should show the confirmation dialog + if (isFormOpen.formOpen && isResourceEdited.isEdited) { + setIsFormOpen({ formOpen: true, leavingForm: true }); + return; + } + + // Otherwise, perform the action immediately + setIsResourceEdited({ isEdited: false }); + action(); + }, + [isFormOpen, isResourceEdited, setIsFormOpen, setIsResourceEdited], + ); + + const confirmDiscard = useCallback(() => { + if (isResourceEdited.discardAction) { + isResourceEdited.discardAction(); + } + + // Reset states + setIsFormOpen({ formOpen: false, leavingForm: false }); + setIsResourceEdited({ isEdited: false }); + }, [isResourceEdited, setIsFormOpen, setIsResourceEdited]); + + const cancelDiscard = useCallback(() => { + setIsFormOpen(prev => ({ ...prev, leavingForm: false })); + }, [setIsFormOpen]); + + return { + navigateSafely, + confirmDiscard, + cancelDiscard, + }; +} diff --git a/src/sidebar/NavItem.tsx b/src/sidebar/NavItem.tsx index 80beed4a03..29b1f1ed36 100644 --- a/src/sidebar/NavItem.tsx +++ b/src/sidebar/NavItem.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { NavNode } from 'state/types'; import { useUrl } from 'hooks/useUrl'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { activeNamespaceIdState } from 'state/activeNamespaceIdAtom'; import { clusterState } from 'state/clusterAtom'; import { columnLayoutState } from 'state/columnLayoutAtom'; @@ -12,12 +12,9 @@ import { SideNavigationItem, SideNavigationSubItem, } from '@ui5/webcomponents-react'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; - -import { isFormOpenState } from 'state/formOpenAtom'; -import { handleActionIfFormOpen } from 'shared/components/UnsavedMessageBox/helpers'; import { useJsonata } from 'components/Extensibility/hooks/useJsonata'; import { Resource } from 'components/Extensibility/contexts/DataSources'; +import { useFormNavigation } from 'shared/hooks/useFormNavigation'; type NavItemProps = { node: NavNode; @@ -30,10 +27,6 @@ export function NavItem({ node, subItem = false }: NavItemProps) { const navigate = useNavigate(); const location = useLocation(); const setLayoutColumn = useSetRecoilState(columnLayoutState); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); const { scopedUrl } = urlGenerators; const namespaceId = useRecoilValue(activeNamespaceIdState); @@ -41,6 +34,7 @@ export function NavItem({ node, subItem = false }: NavItemProps) { const jsonata = useJsonata({ resource: {} as Resource }); const [jsonataLink, jsonataError] = jsonata(node.externalUrl || ''); + const { navigateSafely } = useFormNavigation(); const isNodeSelected = (node: NavNode) => { if (node.externalUrl) return false; @@ -70,32 +64,26 @@ export function NavItem({ node, subItem = false }: NavItemProps) { const newWindow = window.open(link, 'noopener, noreferrer'); if (newWindow) newWindow.opener = null; } else { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - const url = node.createUrlFn - ? node.createUrlFn(urlGenerators) - : scopedUrl(node.pathSegment); - if (location?.pathname !== url) { - setLayoutColumn({ - startColumn: { - resourceType: node?.resourceTypeCased, - resourceName: null, - namespaceId: namespaceId, - apiGroup: node?.apiGroup, - apiVersion: node?.apiVersion, - }, - midColumn: null, - endColumn: null, - layout: 'OneColumn', - }); - navigate(url); - } - }, - ); + navigateSafely(() => { + const url = node.createUrlFn + ? node.createUrlFn(urlGenerators) + : scopedUrl(node.pathSegment); + if (location?.pathname !== url) { + setLayoutColumn({ + startColumn: { + resourceType: node?.resourceTypeCased, + resourceName: null, + namespaceId: namespaceId, + apiGroup: node?.apiGroup, + apiVersion: node?.apiVersion, + }, + midColumn: null, + endColumn: null, + layout: 'OneColumn', + }); + navigate(url); + } + }); } }; diff --git a/src/sidebar/SidebarNavigation.tsx b/src/sidebar/SidebarNavigation.tsx index 664f8f0e5e..d03c43e735 100644 --- a/src/sidebar/SidebarNavigation.tsx +++ b/src/sidebar/SidebarNavigation.tsx @@ -19,9 +19,7 @@ import { useTranslation } from 'react-i18next'; import { useMatch, useNavigate } from 'react-router'; import { useUrl } from 'hooks/useUrl'; import { NamespaceChooser } from 'header/NamespaceChooser/NamespaceChooser'; -import { isResourceEditedState } from 'state/resourceEditedAtom'; -import { isFormOpenState } from 'state/formOpenAtom'; -import { handleActionIfFormOpen } from 'shared/components/UnsavedMessageBox/helpers'; +import { useFormNavigation } from 'shared/hooks/useFormNavigation'; export function SidebarNavigation() { const navigationNodes = useRecoilValue(sidebarNavigationNodesSelector); @@ -29,11 +27,8 @@ export function SidebarNavigation() { const namespace = useRecoilValue(activeNamespaceIdState); const { t } = useTranslation(); const navigate = useNavigate(); + const { navigateSafely } = useFormNavigation(); const setLayoutColumn = useSetRecoilState(columnLayoutState); - const [isResourceEdited, setIsResourceEdited] = useRecoilState( - isResourceEditedState, - ); - const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState); const { clusterUrl, namespaceUrl } = useUrl(); const { resourceType = '' } = @@ -98,16 +93,10 @@ export function SidebarNavigation() { icon={'slim-arrow-left'} text={'Back To Cluster Details'} onClick={() => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - setDefaultColumnLayout(); - return navigate(clusterUrl(`overview`)); - }, - ); + navigateSafely(() => { + setDefaultColumnLayout(); + return navigate(clusterUrl(`overview`)); + }); }} selected={isClusterOverviewSelected()} /> @@ -130,38 +119,31 @@ export function SidebarNavigation() { id="NamespaceComboBox" className="combobox-with-dimension-icon" onSelectionChange={e => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - const newNamespace = - e.target.value === t('navigation.all-namespaces') - ? '-all-' - : e.target.value; - setLayoutColumn(prevState => ({ - startColumn: { - resourceType: - prevState.startColumn?.resourceType ?? null, - resourceName: - prevState.startColumn?.resourceName ?? null, - apiGroup: prevState.startColumn?.apiGroup ?? null, - apiVersion: - prevState.startColumn?.apiVersion ?? null, - namespaceId: newNamespace, - }, - midColumn: null, - endColumn: null, - layout: 'OneColumn', - })); - return navigate( - namespaceUrl(resourceType, { - namespace: newNamespace, - }), - ); - }, - ); + navigateSafely(() => { + const newNamespace = + e.target.value === t('navigation.all-namespaces') + ? '-all-' + : e.target.value; + setLayoutColumn(prevState => ({ + startColumn: { + resourceType: + prevState.startColumn?.resourceType ?? null, + resourceName: + prevState.startColumn?.resourceName ?? null, + apiGroup: prevState.startColumn?.apiGroup ?? null, + apiVersion: prevState.startColumn?.apiVersion ?? null, + namespaceId: newNamespace, + }, + midColumn: null, + endColumn: null, + layout: 'OneColumn', + })); + return navigate( + namespaceUrl(resourceType, { + namespace: newNamespace, + }), + ); + }); }} value={getNamespaceLabel()} > @@ -183,13 +165,7 @@ export function SidebarNavigation() { icon={namespace ? 'slim-arrow-left' : 'bbyd-dashboard'} text={namespace ? 'Back To Cluster Details' : 'Cluster Details'} onClick={() => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => navigate(clusterUrl(`overview`)), - ); + navigateSafely(() => navigate(clusterUrl(`overview`))); }} selected={isClusterOverviewSelected()} /> @@ -209,16 +185,10 @@ export function SidebarNavigation() { icon={'bbyd-dashboard'} text={'Cluster Details'} onClick={() => { - handleActionIfFormOpen( - isResourceEdited, - setIsResourceEdited, - isFormOpen, - setIsFormOpen, - () => { - setDefaultColumnLayout(); - return navigate(clusterUrl(`overview`)); - }, - ); + navigateSafely(() => { + setDefaultColumnLayout(); + return navigate(clusterUrl(`overview`)); + }); }} selected={isClusterOverviewSelected()} />