diff --git a/webapp/packages/core-blocks/src/useExecutor.ts b/webapp/packages/core-blocks/src/useExecutor.ts index 3a43fffd1b..884deaf7b2 100644 --- a/webapp/packages/core-blocks/src/useExecutor.ts +++ b/webapp/packages/core-blocks/src/useExecutor.ts @@ -49,12 +49,12 @@ export function useExecutor(options: IUseExecutorOptions): void { } } - if (props.next) { - executor.next(props.next); + if (props.before) { + executor.before(props.before); } - if (props.before) { - executor.next(props.before); + if (props.next) { + executor.next(props.next); } return () => { @@ -74,5 +74,9 @@ export function useExecutor(options: IUseExecutorOptions): void { executor.removeBefore(before); } }; - }, [executor, props.handlers?.length, props.postHandlers?.length]); + }, [ + executor, props.handlers?.length, props.postHandlers?.length, + props.next, props.before, props.handlers, + props.postHandlers, + ]); } diff --git a/webapp/packages/core-ui/src/Tabs/TabPanel.tsx b/webapp/packages/core-ui/src/Tabs/TabPanel.tsx index 812ba14bf8..a207688279 100644 --- a/webapp/packages/core-ui/src/Tabs/TabPanel.tsx +++ b/webapp/packages/core-ui/src/Tabs/TabPanel.tsx @@ -9,44 +9,45 @@ import { observer } from 'mobx-react-lite'; import { useContext, useEffect, useMemo, useRef } from 'react'; import { TabPanel as BaseTabPanel, TabStateReturn } from 'reakit/Tab'; -import { FormContext, getComputed, Loader } from '@cloudbeaver/core-blocks'; +import { getComputed, Loader } from '@cloudbeaver/core-blocks'; import { TabContext } from './TabContext'; import type { TabPanelProps } from './TabPanelProps'; import { TabsContext } from './TabsContext'; +import { TabPanelValidationHandlerContext } from './TabPanelValidationHandlerContext'; export const TabPanel: React.FC = observer(function TabPanel({ tabId, children, className, lazy }) { - const state = useContext(TabsContext); - const formContext = useContext(FormContext); + const tabContextState = useContext(TabsContext); const panelRef = useRef(null); + const tabPanelValidationHandlerContext = useContext(TabPanelValidationHandlerContext); - if (!state) { + if (!tabContextState) { throw new Error('Tabs context was not provided'); } + if (!tabPanelValidationHandlerContext) { + throw new Error('TabPanelValidationHandlerContext context was not provided'); + } + const tabContext = useMemo(() => ({ tabId }), [tabId]); - const selected = getComputed(() => state.state.selectedId === tabId); - const enabled = getComputed(() => (lazy || state.lazy) && !selected); + const selected = getComputed(() => tabContextState.state.selectedId === tabId); + const enabled = getComputed(() => (lazy || tabContextState.lazy) && !selected); useEffect(() => { - if (formContext === null) { + if (tabPanelValidationHandlerContext === null) { return; } async function trackValidity(event: Event) { - if (selected) { - return; - } - - await state?.open(tabId); - formContext?.reportValidity(); + tabPanelValidationHandlerContext?.addInvalidTab(tabId); } + panelRef.current?.removeEventListener('invalid', trackValidity, true); panelRef.current?.addEventListener('invalid', trackValidity, true); return () => { panelRef.current?.removeEventListener('invalid', trackValidity, true); }; - }, [formContext, selected, state, tabId]); + }, [tabId]); if (enabled) { return null; @@ -55,8 +56,8 @@ export const TabPanel: React.FC = observer(function TabPanel({ ta if (typeof children === 'function') { return ( - - {(children as (state: TabStateReturn) => React.ReactNode)(state.state)} + + {(children as (state: TabStateReturn) => React.ReactNode)(tabContextState.state)} ); @@ -64,7 +65,7 @@ export const TabPanel: React.FC = observer(function TabPanel({ ta return ( - + {children} diff --git a/webapp/packages/core-ui/src/Tabs/TabPanelList.tsx b/webapp/packages/core-ui/src/Tabs/TabPanelList.tsx index a5d9baf241..2c23fb25e1 100644 --- a/webapp/packages/core-ui/src/Tabs/TabPanelList.tsx +++ b/webapp/packages/core-ui/src/Tabs/TabPanelList.tsx @@ -17,6 +17,7 @@ import { BASE_TAB_STYLES } from './Tab/BASE_TAB_STYLES'; import { TabPanel } from './TabPanel'; import type { ITabInfo } from './TabsContainer/ITabsContainer'; import { TabsContext } from './TabsContext'; +import { TabPanelValidationHandler } from './TabPanelValidationHandler'; interface Props extends React.PropsWithChildren { style?: ComponentStyle; @@ -42,7 +43,7 @@ export const TabPanelList = observer(function TabPanelList({ style, child const displayed = state.container.getDisplayed(state.props); return styled(styles)( - <> + {displayed .map( generateTabElement( @@ -56,6 +57,6 @@ export const TabPanelList = observer(function TabPanelList({ style, child ) .flat()} {children} - , + , ); }); diff --git a/webapp/packages/core-ui/src/Tabs/TabPanelValidationHandler.tsx b/webapp/packages/core-ui/src/Tabs/TabPanelValidationHandler.tsx new file mode 100644 index 0000000000..0e9b5e0a1c --- /dev/null +++ b/webapp/packages/core-ui/src/Tabs/TabPanelValidationHandler.tsx @@ -0,0 +1,76 @@ +import { observer } from 'mobx-react-lite'; +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { TabsContext } from './TabsContext'; +import { FormContext, useExecutor } from '@cloudbeaver/core-blocks'; +import { TabPanelValidationHandlerContext } from './TabPanelValidationHandlerContext'; +import { SyncExecutor } from '@cloudbeaver/core-executor'; + +export interface ITabPanelHandlerProps { + children: React.ReactNode; +} + +export const TabPanelValidationHandler = observer(function TabPanelValidationHandler({ children }: ITabPanelHandlerProps) { + const state = useContext(TabsContext); + const containerRef = useRef(null); + const formContext = useContext(FormContext); + const selectedTab = state?.state.selectedId; + const invalidTabs = useRef>(new Set()); + const [resetExecutor] = useState(() => new SyncExecutor()); + + function addInvalidTab(tabId: string) { + invalidTabs.current.add(tabId); + }; + + function resetInvalidTabs() { + invalidTabs.current = new Set(); + } + + useExecutor({ + executor: resetExecutor, + handlers: [resetInvalidTabs], + }); + + useExecutor({ + executor: formContext?.onValidate, + before: resetExecutor, + }); + + useExecutor({ + executor: formContext?.onValidate, + postHandlers: [() => formContext?.reportValidity()], + }); + + useEffect(() => { + async function goNextTab() { + const firstInvalidTab = invalidTabs.current.values().next().value; + + if ( + (selectedTab && invalidTabs.current.has(selectedTab)) || + invalidTabs.current.size === 0 || + !firstInvalidTab + ) { + return; + } + + await state?.open(firstInvalidTab); + resetInvalidTabs(); + } + + formContext?.parent?.ref?.removeEventListener('invalid', goNextTab, true); + formContext?.parent?.ref?.addEventListener('invalid', goNextTab, true); + + return () => { + formContext?.parent?.ref?.removeEventListener('invalid', goNextTab, true); + }; + }, []); + + const value = useMemo(() => ({ addInvalidTab, invalidTabs }), []); + + return ( + +
+ {children} +
+
+ ); +}); \ No newline at end of file diff --git a/webapp/packages/core-ui/src/Tabs/TabPanelValidationHandlerContext.ts b/webapp/packages/core-ui/src/Tabs/TabPanelValidationHandlerContext.ts new file mode 100644 index 0000000000..60737cb081 --- /dev/null +++ b/webapp/packages/core-ui/src/Tabs/TabPanelValidationHandlerContext.ts @@ -0,0 +1,8 @@ +import { MutableRefObject, createContext } from 'react'; + +export interface ITabPanelValidationHandlerContext { + invalidTabs: MutableRefObject>; + addInvalidTab: (tabId: string) => void; +} + +export const TabPanelValidationHandlerContext = createContext(null); \ No newline at end of file