From 84ada72890db22e3a1407aa0a6776dc533d282b4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 9 Sep 2025 22:55:31 +0500 Subject: [PATCH 1/2] #1949 add different tab modes in PropertyView --- .../comps/comps/tabs/tabbedContainerComp.tsx | 167 ++++++++++-------- 1 file changed, 92 insertions(+), 75 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index 86124c921..a4781363b 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -15,7 +15,7 @@ import { NameGenerator } from "comps/utils"; import { ScrollBar, Section, sectionNames } from "lowcoder-design"; import { HintPlaceHolder } from "lowcoder-design"; import _ from "lodash"; -import React, { useCallback, useContext, useEffect } from "react"; +import React, {useContext, useEffect,useState } from "react"; import styled, { css } from "styled-components"; import { IContainer } from "../containerBase/iContainer"; import { SimpleContainerComp } from "../containerBase/simpleContainerComp"; @@ -34,7 +34,7 @@ import { EditorContext } from "comps/editorState"; import { checkIsMobile } from "util/commonUtils"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { BoolControl } from "comps/controls/boolControl"; -import { PositionControl } from "comps/controls/dropdownControl"; +import { PositionControl,dropdownControl } from "comps/controls/dropdownControl"; import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl"; import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; @@ -46,6 +46,15 @@ const EVENT_OPTIONS = [ }, ] as const; +const TAB_BEHAVIOR_OPTIONS = [ + { label: "Lazy Loading", value: "lazy" }, + { label: "Remember State", value: "remember" }, + { label: "Destroy Inactive", value: "destroy" }, + { label: "Keep Alive (render all)", value: "keep-alive" }, +] as const; + +const TabBehaviorControl = dropdownControl(TAB_BEHAVIOR_OPTIONS, "lazy"); + const childrenMap = { tabs: TabsOptionControl, selectedTabKey: stringExposingStateControl("key", "Tab1"), @@ -61,7 +70,7 @@ const childrenMap = { onEvent: eventHandlerControl(EVENT_OPTIONS), disabled: BoolCodeControl, showHeader: withDefault(BoolControl, true), - destroyInactiveTab: withDefault(BoolControl, false), + tabBehavior: withDefault(TabBehaviorControl, "lazy"), style: styleControl(TabContainerStyle , 'style'), headerStyle: styleControl(ContainerHeaderStyle , 'headerStyle'), bodyStyle: styleControl(TabBodyStyle , 'bodyStyle'), @@ -72,7 +81,7 @@ const childrenMap = { type ViewProps = RecordConstructorToView; type TabbedContainerProps = ViewProps & { dispatch: DispatchType }; - + const getStyle = ( style: TabContainerStyleType, headerStyle: ContainerHeaderStyleType, @@ -138,11 +147,11 @@ const getStyle = ( `; }; -const StyledTabs = styled(Tabs)<{ +const StyledTabs = styled(Tabs)<{ $style: TabContainerStyleType; $headerStyle: ContainerHeaderStyleType; $bodyStyle: TabBodyStyleType; - $isMobile?: boolean; + $isMobile?: boolean; $showHeader?: boolean; $animationStyle:AnimationStyleType }>` @@ -157,13 +166,12 @@ const StyledTabs = styled(Tabs)<{ .ant-tabs-content { height: 100%; - // margin-top: -16px; + } .ant-tabs-nav { display: ${(props) => (props.$showHeader ? "block" : "none")}; padding: 0 ${(props) => (props.$isMobile ? 16 : 24)}px; - // background: white; margin: 0px; } @@ -197,27 +205,20 @@ const TabbedContainer = (props: TabbedContainerProps) => { headerStyle, bodyStyle, horizontalGridCells, - destroyInactiveTab, + tabBehavior, } = props; const visibleTabs = tabs.filter((tab) => !tab.hidden); const selectedTab = visibleTabs.find((tab) => tab.key === props.selectedTabKey.value); - const activeKey = selectedTab - ? selectedTab.key - : visibleTabs.length > 0 - ? visibleTabs[0].key - : undefined; - - const onTabClick = useCallback( - (key: string, event: React.KeyboardEvent | React.MouseEvent) => { - // log.debug("onTabClick. event: ", event); - const target = event.target; - (target as any).parentNode.click - ? (target as any).parentNode.click() - : (target as any).parentNode.parentNode.click(); - }, - [] - ); + const activeKey = selectedTab? selectedTab.key: visibleTabs.length > 0 ? visibleTabs[0].key : undefined; + + // Placeholder-based lazy loading — only for "lazy" mode + const [loadedTabs, setLoadedTabs] = useState>(new Set()); + useEffect(() => { + if (tabBehavior === "lazy" && activeKey) { + setLoadedTabs((prev: Set) => new Set([...prev, activeKey])); + } + }, [tabBehavior, activeKey]); const editorState = useContext(EditorContext); const maxWidth = editorState.getAppSettings().maxWidth; @@ -230,23 +231,38 @@ const TabbedContainer = (props: TabbedContainerProps) => { const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id); const containerProps = containers[id].children; const hasIcon = tab.icon.props.value; + const label = ( <> - {tab.iconPosition === "left" && hasIcon && ( - {tab.icon} - )} + {tab.iconPosition === "left" && hasIcon && {tab.icon}} {tab.label} - {tab.iconPosition === "right" && hasIcon && ( - {tab.icon} - )} + {tab.iconPosition === "right" && hasIcon && {tab.icon}} ); - return { - label, - key: tab.key, - forceRender: !destroyInactiveTab, - destroyInactiveTab: destroyInactiveTab, - children: ( + + // Item-level forceRender mapping + const forceRender: boolean = tabBehavior === "keep-alive"; + + // Render content (placeholder only for "lazy" & not yet opened) + const renderTabContent = () => { + if (tabBehavior === "lazy" && !loadedTabs.has(tab.key)) { + return ( +
+ Click to load tab content +
+ ); + } + + return ( { /> - ) - } - }) + ); + }; + + return { + label, + key: tab.key, + forceRender, // true only for keep-alive + children: renderTabContent(), + }; + }); return (
- - { - if (key !== props.selectedTabKey.value) { - props.selectedTabKey.onChange(key); - props.onEvent("change"); - } - }} - // onTabClick={onTabClick} - animated - $isMobile={isMobile} - items={tabItems} - tabBarGutter={props.tabsGutter} - centered={props.tabsCentered} - > - - -
+ + { + if (key !== props.selectedTabKey.value) { + props.selectedTabKey.onChange(key); + props.onEvent("change"); + if (tabBehavior === "lazy") { + setLoadedTabs((prev: Set) => new Set([...prev, key])); + } + } + }} + animated + $isMobile={isMobile} + items={tabItems} + tabBarGutter={props.tabsGutter} + centered={props.tabsCentered} + /> + + ); }; - export const TabbedContainerBaseComp = (function () { return new UICompBuilder(childrenMap, (props, dispatch) => { return ( @@ -313,14 +337,14 @@ export const TabbedContainerBaseComp = (function () { })} {children.selectedTabKey.propertyView({ label: trans("prop.defaultValue") })} - + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && (
{children.onEvent.getPropertyView()} {disabledPropertyView(children)} {hiddenPropertyView(children)} {children.showHeader.propertyView({ label: trans("tabbedContainer.showTabs") })} - {children.destroyInactiveTab.propertyView({ label: trans("tabbedContainer.destroyInactiveTab") })} + {children.tabBehavior.propertyView({ label: "Tab Behavior" })}
)} @@ -371,21 +395,18 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai const actions: CompAction[] = []; Object.keys(containers).forEach((id) => { if (!ids.has(id)) { - // log.debug("syncContainers delete. ids=", ids, " id=", id); actions.push(wrapChildAction("containers", wrapChildAction(id, deleteCompAction()))); } }); // new ids.forEach((id) => { if (!containers.hasOwnProperty(id)) { - // log.debug("syncContainers new containers: ", containers, " id: ", id); actions.push( wrapChildAction("containers", addMapChildAction(id, { layout: {}, items: {} })) ); } }); - // log.debug("syncContainers. actions: ", actions); let instance = this; actions.forEach((action) => { instance = instance.reduce(action); @@ -414,13 +435,11 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai return this; } } - // log.debug("before super reduce. action: ", action); let newInstance = super.reduce(action); if (action.type === CompActionTypes.UPDATE_NODES_V2) { // Need eval to get the value in StringControl newInstance = newInstance.syncContainers(); } - // log.debug("reduce. instance: ", this, " newInstance: ", newInstance); return newInstance; } @@ -464,8 +483,6 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai override autoHeight(): boolean { return this.children.autoHeight.getView(); } - - } export const TabbedContainerComp = withExposingConfigs(TabbedContainerImplComp, [ From 289ee7ac5979b37ff89bf12c96e765a776d63407 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 10 Sep 2025 22:53:45 +0500 Subject: [PATCH 2/2] #1949 add tab modes feature --- .../comps/comps/tabs/tabbedContainerComp.tsx | 170 +++++++++++------- .../packages/lowcoder/src/i18n/locales/en.ts | 9 +- 2 files changed, 112 insertions(+), 67 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index a4781363b..7f1ef53cf 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -15,7 +15,7 @@ import { NameGenerator } from "comps/utils"; import { ScrollBar, Section, sectionNames } from "lowcoder-design"; import { HintPlaceHolder } from "lowcoder-design"; import _ from "lodash"; -import React, {useContext, useEffect,useState } from "react"; +import React, {useContext, useMemo } from "react"; import styled, { css } from "styled-components"; import { IContainer } from "../containerBase/iContainer"; import { SimpleContainerComp } from "../containerBase/simpleContainerComp"; @@ -47,10 +47,9 @@ const EVENT_OPTIONS = [ ] as const; const TAB_BEHAVIOR_OPTIONS = [ - { label: "Lazy Loading", value: "lazy" }, - { label: "Remember State", value: "remember" }, - { label: "Destroy Inactive", value: "destroy" }, - { label: "Keep Alive (render all)", value: "keep-alive" }, + { label: trans("tabbedContainer.tabBehaviorLazy"), value: "lazy" }, + { label: trans("tabbedContainer.tabBehaviorKeepAlive"), value: "keep-alive" }, + { label: trans("tabbedContainer.tabBehaviorDestroy"), value: "destroy" }, ] as const; const TabBehaviorControl = dropdownControl(TAB_BEHAVIOR_OPTIONS, "lazy"); @@ -153,7 +152,8 @@ const StyledTabs = styled(Tabs)<{ $bodyStyle: TabBodyStyleType; $isMobile?: boolean; $showHeader?: boolean; - $animationStyle:AnimationStyleType + $animationStyle:AnimationStyleType; + $isDestroyPane?: boolean; }>` &.ant-tabs { height: 100%; @@ -166,7 +166,6 @@ const StyledTabs = styled(Tabs)<{ .ant-tabs-content { height: 100%; - } .ant-tabs-nav { @@ -183,16 +182,71 @@ const StyledTabs = styled(Tabs)<{ margin-right: -24px; } - ${(props) => props.$style && getStyle( - props.$style, - props.$headerStyle, - props.$bodyStyle, - )} + ${(props) => + props.$style && getStyle(props.$style, props.$headerStyle, props.$bodyStyle)} + + /* Conditional styling for all modes except Destroy Inactive Pane */ + ${(props) => !props.$isDestroyPane && ` + .ant-tabs-content-holder { position: relative; } + + .ant-tabs-tabpane[aria-hidden="true"], + .ant-tabs-tabpane-hidden { + display: block !important; + visibility: hidden !important; + position: absolute !important; + inset: 0; + pointer-events: none; + } + `} `; const ContainerInTab = (props: ContainerBaseProps) => { + return ; +}; + +type TabPaneContentProps = { + autoHeight: boolean; + showVerticalScrollbar: boolean; + paddingWidth: number; + horizontalGridCells: number; + bodyBackground: string; + layoutView: any; + itemsView: any; + positionParamsView: any; + dispatch: DispatchType; +}; + +const TabPaneContent: React.FC = ({ + autoHeight, + showVerticalScrollbar, + paddingWidth, + horizontalGridCells, + bodyBackground, + layoutView, + itemsView, + positionParamsView, + dispatch, +}) => { + const gridItems = useMemo(() => gridItemCompToGridItems(itemsView), [itemsView]); + return ( - + + + + + ); }; @@ -212,13 +266,6 @@ const TabbedContainer = (props: TabbedContainerProps) => { const selectedTab = visibleTabs.find((tab) => tab.key === props.selectedTabKey.value); const activeKey = selectedTab? selectedTab.key: visibleTabs.length > 0 ? visibleTabs[0].key : undefined; - // Placeholder-based lazy loading — only for "lazy" mode - const [loadedTabs, setLoadedTabs] = useState>(new Set()); - useEffect(() => { - if (tabBehavior === "lazy" && activeKey) { - setLoadedTabs((prev: Set) => new Set([...prev, activeKey])); - } - }, [tabBehavior, activeKey]); const editorState = useContext(EditorContext); const maxWidth = editorState.getAppSettings().maxWidth; @@ -229,7 +276,7 @@ const TabbedContainer = (props: TabbedContainerProps) => { const tabItems = visibleTabs.map((tab) => { const id = String(tab.id); const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id); - const containerProps = containers[id].children; + const containerChildren = containers[id].children; const hasIcon = tab.icon.props.value; const label = ( @@ -240,50 +287,25 @@ const TabbedContainer = (props: TabbedContainerProps) => { ); - // Item-level forceRender mapping - const forceRender: boolean = tabBehavior === "keep-alive"; - - // Render content (placeholder only for "lazy" & not yet opened) - const renderTabContent = () => { - if (tabBehavior === "lazy" && !loadedTabs.has(tab.key)) { - return ( -
- Click to load tab content -
- ); - } - - return ( - - - - - - ); - }; + const forceRender = tabBehavior === "keep-alive"; return { label, key: tab.key, - forceRender, // true only for keep-alive - children: renderTabContent(), + forceRender, + children: ( + + ), }; }); @@ -299,13 +321,11 @@ const TabbedContainer = (props: TabbedContainerProps) => { $headerStyle={headerStyle} $bodyStyle={bodyStyle} $showHeader={showHeader} + $isDestroyPane={tabBehavior === "destroy"} onChange={(key) => { if (key !== props.selectedTabKey.value) { props.selectedTabKey.onChange(key); props.onEvent("change"); - if (tabBehavior === "lazy") { - setLoadedTabs((prev: Set) => new Set([...prev, key])); - } } }} animated @@ -344,7 +364,25 @@ export const TabbedContainerBaseComp = (function () { {disabledPropertyView(children)} {hiddenPropertyView(children)} {children.showHeader.propertyView({ label: trans("tabbedContainer.showTabs") })} - {children.tabBehavior.propertyView({ label: "Tab Behavior" })} + {children.tabBehavior.propertyView({ + label: trans("tabbedContainer.tabBehavior"), + tooltip: ( +
+
+ {trans("tabbedContainer.tabBehaviorLazy")}: +  {trans("tabbedContainer.tabBehaviorLazyTooltip")} +
+
+ {trans("tabbedContainer.tabBehaviorKeepAlive")}: +  {trans("tabbedContainer.tabBehaviorKeepAliveTooltip")} +
+
+ {trans("tabbedContainer.tabBehaviorDestroy")}: +  {trans("tabbedContainer.tabBehaviorDestroyTooltip")} +
+
+ ), + })} )} @@ -435,6 +473,7 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai return this; } } + let newInstance = super.reduce(action); if (action.type === CompActionTypes.UPDATE_NODES_V2) { // Need eval to get the value in StringControl @@ -489,4 +528,3 @@ export const TabbedContainerComp = withExposingConfigs(TabbedContainerImplComp, new NameConfig("selectedTabKey", trans("tabbedContainer.selectedTabKeyDesc")), NameConfigHidden, ]); - diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 80f667288..f9b83b8d8 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3102,7 +3102,14 @@ export const en = { "gutter" : "Gap", "gutterTooltip" : "The distance between tabs in px", "tabsCentered" : "Centered Tabs", - "destroyInactiveTab": "Destroy Inactive TabPane" + "destroyInactiveTab": "Destroy Inactive TabPane", + "tabBehavior": "Tab Behavior", + "tabBehaviorLazy": "Lazy", + "tabBehaviorKeepAlive": "Keep Alive", + "tabBehaviorDestroy": "Destroy Inactive", + "tabBehaviorLazyTooltip": "Render tabs only when they are first activated. Hidden tabs are not rendered until selected.", + "tabBehaviorKeepAliveTooltip": "Keep all tab contents mounted and initialized. Hidden tabs remain mounted but are visually hidden.", + "tabBehaviorDestroyTooltip": "Unmount contents of inactive tabs to free resources. Hidden tabs are destroyed until selected again." }, "formComp": { "containerPlaceholder": "Drag Components from the Right Pane or",