diff --git a/shesha-reactjs/src/components/formDesigner/componentPropertiesPanel/index.tsx b/shesha-reactjs/src/components/formDesigner/componentPropertiesPanel/index.tsx index 9ca633b2f6..80388351db 100644 --- a/shesha-reactjs/src/components/formDesigner/componentPropertiesPanel/index.tsx +++ b/shesha-reactjs/src/components/formDesigner/componentPropertiesPanel/index.tsx @@ -25,3 +25,4 @@ const ComponentPropertiesPanelInner: FC = () => { }; export const ComponentPropertiesPanel = React.memo(ComponentPropertiesPanelInner); + diff --git a/shesha-reactjs/src/components/formDesigner/designerMainArea/index.tsx b/shesha-reactjs/src/components/formDesigner/designerMainArea/index.tsx index daf614a865..9bf4f9fcfc 100644 --- a/shesha-reactjs/src/components/formDesigner/designerMainArea/index.tsx +++ b/shesha-reactjs/src/components/formDesigner/designerMainArea/index.tsx @@ -27,7 +27,7 @@ export const DesignerMainArea: FC<{ viewType?: IViewType }> = ({ viewType = 'con const shaForm = useShaFormInstance(); const { antdForm: form } = shaForm; const { styles } = useStyles(); - const { deleteComponent, settingsPanelRef } = useFormDesigner(); + const { deleteComponent, settingsPanelElement } = useFormDesigner(); const component = useFormDesignerSelectedComponent(); const showMarkup = false; @@ -63,7 +63,7 @@ export const DesignerMainArea: FC<{ viewType?: IViewType }> = ({ viewType = 'con if (isEditing) return; // Ignore if focus is inside the properties/settings panel - if (settingsPanelRef?.current && settingsPanelRef.current.contains(target)) { + if (settingsPanelElement && settingsPanelElement.contains(target)) { return; } @@ -71,7 +71,7 @@ export const DesignerMainArea: FC<{ viewType?: IViewType }> = ({ viewType = 'con event.preventDefault(); deleteComponent({ componentId: selectedComponentId }); } - }, [readOnly, formMode, selectedComponentId, deleteComponent, settingsPanelRef]); + }, [readOnly, formMode, selectedComponentId, deleteComponent, settingsPanelElement]); useEffect(() => { window.addEventListener('keydown', handleKeyDown); diff --git a/shesha-reactjs/src/designer-components/propertiesTabs/models.ts b/shesha-reactjs/src/designer-components/propertiesTabs/models.ts index 7377b19a00..08d960dffa 100644 --- a/shesha-reactjs/src/designer-components/propertiesTabs/models.ts +++ b/shesha-reactjs/src/designer-components/propertiesTabs/models.ts @@ -24,7 +24,6 @@ export interface ITabPaneProps export interface IPropertiesTabsComponentProps extends IConfigurableFormComponent { tabs: ITabPaneProps[]; size?: SizeType; - defaultActiveKey?: string; tabType?: 'line' | 'card'; hidden?: boolean; customVisibility?: string; diff --git a/shesha-reactjs/src/designer-components/propertiesTabs/searchableTabsComponent.tsx b/shesha-reactjs/src/designer-components/propertiesTabs/searchableTabsComponent.tsx index 8d5d3823e5..ca97c02262 100644 --- a/shesha-reactjs/src/designer-components/propertiesTabs/searchableTabsComponent.tsx +++ b/shesha-reactjs/src/designer-components/propertiesTabs/searchableTabsComponent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; import { Tabs, Input, Empty, InputRef } from 'antd'; import ParentProvider from '@/providers/parentProvider'; import ComponentsContainer from '@/components/formDesigner/containers/componentsContainer'; @@ -7,8 +7,9 @@ import { SearchOutlined } from '@ant-design/icons'; import { filterDynamicComponents } from './utils'; import { ITabPaneProps, IPropertiesTabsComponentProps } from './models'; import { IConfigurableFormComponent } from '@/interfaces'; -import { useFormState, useFormActions } from '@/providers/form'; +import { useFormStateOrUndefined, useFormActionsOrUndefined } from '@/providers/form'; import { useShaFormDataUpdate } from '@/providers/form/providers/shaFormProvider'; +import { useFormDesignerActiveSettingsTabKey, useFormDesigner } from '@/providers/formDesigner'; interface SearchableTabsProps { model: IPropertiesTabsComponentProps; @@ -17,11 +18,15 @@ interface SearchableTabsProps { const SearchableTabs: React.FC = ({ model }) => { const { tabs } = model; const [searchQuery, setSearchQuery] = useState(''); - const [activeTabKey, setActiveTabKey] = useState('1'); const { styles } = useStyles(); - const formState = useFormState(); - const formActions = useFormActions(); + const formState = useFormStateOrUndefined(); + const formActions = useFormActionsOrUndefined(); + const formDesigner = useFormDesigner(); + const persistedActiveTabKey = useFormDesignerActiveSettingsTabKey(); + + // Use persisted tab key if available, otherwise default to first tab + const activeTabKey = persistedActiveTabKey ?? '1'; useShaFormDataUpdate(); @@ -62,7 +67,7 @@ const SearchableTabs: React.FC = ({ model }) => { }; const handleTabChange = (newActiveKey: string): void => { - setActiveTabKey(newActiveKey); + formDesigner.setActiveSettingsTabKey(newActiveKey); requestAnimationFrame(() => searchInputRef.current?.focus()); }; @@ -133,18 +138,30 @@ const SearchableTabs: React.FC = ({ model }) => { Array.isArray(tab.components) ? tab.components.length > 0 : !!tab.components, ); if (firstVisibleTab && firstVisibleTab.key !== activeTabKey) { - setActiveTabKey(firstVisibleTab.key); + formDesigner.setActiveSettingsTabKey(firstVisibleTab.key); } } - }, [searchQuery, newFilteredTabs, activeTabKey]); + }, [searchQuery, newFilteredTabs, activeTabKey, formDesigner]); // Ensure we have a valid active tab key useEffect(() => { if (newFilteredTabs.length > 0 && !newFilteredTabs.find((tab) => tab.key === activeTabKey)) { - setActiveTabKey(newFilteredTabs[0].key); + formDesigner.setActiveSettingsTabKey(newFilteredTabs[0].key); } - }, [newFilteredTabs, activeTabKey]); - + }, [newFilteredTabs, activeTabKey, formDesigner]); + + const localTabs = useMemo(() => ( + + ), [activeTabKey, handleTabChange, model.size, model.tabType, newFilteredTabs, styles.content, model.position]); return ( <> @@ -155,17 +172,7 @@ const SearchableTabs: React.FC = ({ model }) => { })} {newFilteredTabs.length === 0 && searchQuery ? - : ( - - )} + : localTabs} ); }; diff --git a/shesha-reactjs/src/providers/formDesigner/contexts.tsx b/shesha-reactjs/src/providers/formDesigner/contexts.tsx index b4709f8477..496f27d18e 100644 --- a/shesha-reactjs/src/providers/formDesigner/contexts.tsx +++ b/shesha-reactjs/src/providers/formDesigner/contexts.tsx @@ -88,6 +88,7 @@ export type FormDesignerState = { isDebug: boolean; readOnly: boolean; formMode: FormMode; + activeSettingsTabKey: string | undefined; settingsPanelElement: HTMLDivElement | null; }; @@ -117,6 +118,7 @@ export type FormDesignerActions = { setReadOnly: (value: boolean) => void; setFormMode: (value: FormMode) => void; + setActiveSettingsTabKey: (key: string) => void; getCachedComponentEditor: (type: string, evaluator: () => ISettingsFormFactory) => ISettingsFormFactory; diff --git a/shesha-reactjs/src/providers/formDesigner/index.tsx b/shesha-reactjs/src/providers/formDesigner/index.tsx index daeb24060d..f19a9d0e88 100644 --- a/shesha-reactjs/src/providers/formDesigner/index.tsx +++ b/shesha-reactjs/src/providers/formDesigner/index.tsx @@ -132,6 +132,11 @@ const useFormDesignerUndoRedo = (): IUndoable => { }; }; +const useFormDesignerActiveSettingsTabKey = (): string | undefined => { + useFormDesignerSubscription('settings-tab'); + return useFormDesigner().activeSettingsTabKey; +}; + export { FormDesignerProvider, useFormDesignerOrUndefined, @@ -145,4 +150,5 @@ export { useFormDesignerFormMode, useFormDesignerUndoRedo, useFormDesignerIsModified, + useFormDesignerActiveSettingsTabKey, }; diff --git a/shesha-reactjs/src/providers/formDesigner/instance.ts b/shesha-reactjs/src/providers/formDesigner/instance.ts index f9276bd90e..f5e6d9f90b 100644 --- a/shesha-reactjs/src/providers/formDesigner/instance.ts +++ b/shesha-reactjs/src/providers/formDesigner/instance.ts @@ -81,6 +81,8 @@ export class FormDesignerInstance implements IFormDesignerInstance { formMode: FormMode; + activeSettingsTabKey: string | undefined; + get state(): FormDesignerFormState { return this.undoableState.getState(); } @@ -95,6 +97,7 @@ export class FormDesignerInstance implements IFormDesignerInstance { this.isDragging = false; this.hasDragged = false; this.isDataModified = false; + this.activeSettingsTabKey = undefined; this.subscriptions = new Map>(); // eslint-disable-next-line no-console @@ -720,6 +723,12 @@ export class FormDesignerInstance implements IFormDesignerInstance { this.notifySubscribers(['mode']); }; + setActiveSettingsTabKey = (key: string): void => { + if (this.activeSettingsTabKey === key) return; + this.activeSettingsTabKey = key; + this.notifySubscribers(['settings-tab']); + }; + componentEditors: IComponentSettingsEditorsCache = {}; getCachedComponentEditor = (type: string, evaluator: () => ISettingsFormFactory): ISettingsFormFactory => { diff --git a/shesha-reactjs/src/providers/formDesigner/models.ts b/shesha-reactjs/src/providers/formDesigner/models.ts index f883839cdd..04c858de6a 100644 --- a/shesha-reactjs/src/providers/formDesigner/models.ts +++ b/shesha-reactjs/src/providers/formDesigner/models.ts @@ -27,7 +27,7 @@ export type IComponentSettingsEditorsCache = Record void; export type FormDesignerSubscription = (designer: IFormDesignerInstance) => void; -export type FormDesignerSubscriptionType = 'markup' | 'selection' | 'readonly' | 'mode' | 'debug' | 'history' | 'data-modified'; +export type FormDesignerSubscriptionType = 'markup' | 'selection' | 'readonly' | 'mode' | 'debug' | 'history' | 'data-modified' | 'settings-tab'; export interface AddComponentPayloadBase { index: number;