From 30f29233447cb0f56a0e1b58768fb32ba7a79d22 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 2 Oct 2025 18:19:51 +0500 Subject: [PATCH 01/97] [Fix]: #2021 add map mode for NAV --- .../src/comps/comps/navComp/navComp.tsx | 134 +++++++++++++++--- 1 file changed, 111 insertions(+), 23 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index e3727508e..5842fcc92 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -1,9 +1,10 @@ import { NameConfig, NameConfigHidden, withExposingConfigs } from "comps/generators/withExposing"; +import { MultiCompBuilder } from "comps/generators/multi"; import { UICompBuilder, withDefault } from "comps/generators"; import { Section, sectionNames } from "lowcoder-design"; import styled from "styled-components"; import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; -import { StringControl } from "comps/controls/codeControl"; +import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; import { alignWithJustifyControl } from "comps/controls/alignControl"; import { navListComp } from "./navItemComp"; import { menuPropertyView } from "./components/MenuItemList"; @@ -22,6 +23,9 @@ import { trans } from "i18n"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import { controlItem } from "lowcoder-design"; +import { mapOptionsControl } from "comps/controls/optionsControl"; type IProps = { $justify: boolean; @@ -137,35 +141,50 @@ const childrenMap = { horizontalAlignment: alignWithJustifyControl(), style: migrateOldData(styleControl(NavigationStyle, 'style'), fixOldStyleData), animationStyle: styleControl(AnimationStyle, 'animationStyle'), - items: withDefault(navListComp(), [ - { - label: trans("menuItem") + " 1", - }, - ]), + items: withDefault(createNavItemsControl(), { + optionType: "manual", + manual: [ + { + label: trans("menuItem") + " 1", + }, + ], + }), }; const NavCompBase = new UICompBuilder(childrenMap, (props) => { const data = props.items; const items = ( <> - {data.map((menuItem, idx) => { - const { hidden, label, items, active, onEvent } = menuItem.getView(); + {data.map((menuItem: any, idx: number) => { + const isCompItem = typeof menuItem?.getView === "function"; + const view = isCompItem ? menuItem.getView() : menuItem; + const hidden = !!view?.hidden; if (hidden) { return null; } + + const label = view?.label; + const active = !!view?.active; + const onEvent = view?.onEvent; + const subItems = isCompItem ? view?.items : []; + const subMenuItems: Array<{ key: string; label: string }> = []; const subMenuSelectedKeys: Array = []; - items.forEach((subItem, originalIndex) => { - if (subItem.children.hidden.getView()) { - return; - } - const key = originalIndex + ""; - subItem.children.active.getView() && subMenuSelectedKeys.push(key); - subMenuItems.push({ - key: key, - label: subItem.children.label.getView(), + + if (Array.isArray(subItems)) { + subItems.forEach((subItem: any, originalIndex: number) => { + if (subItem.children.hidden.getView()) { + return; + } + const key = originalIndex + ""; + subItem.children.active.getView() && subMenuSelectedKeys.push(key); + subMenuItems.push({ + key: key, + label: subItem.children.label.getView(), + }); }); - }); + } + const item = ( { $textTransform={props.style.textTransform} $textDecoration={props.style.textDecoration} $margin={props.style.margin} - onClick={() => onEvent("click")} + onClick={() => onEvent && onEvent("click")} > {label} - {items.length > 0 && } + {Array.isArray(subItems) && subItems.length > 0 && } ); if (subMenuItems.length > 0) { const subMenu = ( { - const { onEvent: onSubEvent } = items[Number(e.key)]?.getView(); - onSubEvent("click"); + const subItem = subItems[Number(e.key)]; + const onSubEvent = subItem?.getView()?.onEvent; + onSubEvent && onSubEvent("click"); }} selectedKeys={subMenuSelectedKeys} items={subMenuItems} @@ -237,7 +257,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { return ( <>
- {menuPropertyView(children.items)} + {children.items.propertyView()}
{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( @@ -285,3 +305,71 @@ export const NavComp = withExposingConfigs(NavCompBase, [ NameConfigHidden, new NameConfig("items", trans("navigation.itemsDesc")), ]); + +// ---------------------------------------- +// Nav Items Control (Manual / Map modes) +// ---------------------------------------- +function createNavItemsControl() { + const OptionTypes = [ + { label: trans("prop.manual"), value: "manual" }, + { label: trans("prop.map"), value: "map" }, + ] as const; + + // Variant used in Map mode + const NavMapOption = new MultiCompBuilder( + { + label: StringControl, + hidden: BoolCodeControl, + active: BoolCodeControl, + onEvent: eventHandlerControl([clickEvent]), + }, + (props) => props + ) + .setPropertyViewFn((children) => ( + <> + {children.label.propertyView({ label: trans("label"), placeholder: "{{item}}" })} + {children.active.propertyView({ label: trans("navItemComp.active") })} + {children.hidden.propertyView({ label: trans("hidden") })} + {children.onEvent.propertyView({ inline: true })} + + )) + .build(); + + const TmpNavItemsControl = new MultiCompBuilder( + { + optionType: dropdownControl(OptionTypes, "manual"), + manual: navListComp(), + mapData: mapOptionsControl(NavMapOption), + }, + (props) => { + return props.optionType === "manual" ? props.manual : props.mapData; + } + ) + .setPropertyViewFn(() => { + throw new Error("Method not implemented."); + }) + .build(); + + return class NavItemsControl extends TmpNavItemsControl { + exposingNode() { + return this.children.optionType.getView() === "manual" + ? (this.children.manual as any).exposingNode() + : (this.children.mapData as any).exposingNode(); + } + + propertyView() { + const isManual = this.children.optionType.getView() === "manual"; + const content = isManual + ? menuPropertyView(this.children.manual as any) + : this.children.mapData.getPropertyView(); + + return controlItem( + { searchChild: true }, + <> + {this.children.optionType.propertyView({ radioButton: true, type: "oneline" })} + {content} + + ); + } + }; +} From 6741bf545b6659a90d5bdffa2213b79e6102fedd Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 2 Oct 2025 18:24:04 +0500 Subject: [PATCH 02/97] update Menu Items name --- client/packages/lowcoder/src/i18n/locales/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 60095c146..9ec7f365e 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3204,7 +3204,7 @@ export const en = { "logoURL": "Navigation Logo URL", "horizontalAlignment": "Horizontal Alignment", "logoURLDesc": "You can display a Logo on the left side by entering URI Value or Base64 String like data:image/png;base64,AAA... CCC", - "itemsDesc": "Hierarchical Navigation Menu Items" + "itemsDesc": "Menu Items" }, "droppadbleMenuItem": { "subMenu": "Submenu {number}" From bd23748840e5738f3e0e49e264c4930974af12ac Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 2 Oct 2025 19:05:06 +0500 Subject: [PATCH 03/97] [Feat]: #2021 add disabled property for nav --- .../src/comps/comps/navComp/navComp.tsx | 17 ++++++++++++----- .../src/comps/comps/navComp/navItemComp.tsx | 7 ++++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 5842fcc92..6be3f9ce7 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -67,11 +67,12 @@ const Item = styled.div<{ $padding: string; $textTransform:string; $textDecoration:string; + $disabled?: boolean; }>` height: 30px; line-height: 30px; padding: ${(props) => props.$padding ? props.$padding : '0 16px'}; - color: ${(props) => (props.$active ? props.$activeColor : props.$color)}; + color: ${(props) => props.$disabled ? `${props.$color}80` : (props.$active ? props.$activeColor : props.$color)}; font-weight: ${(props) => (props.$textWeight ? props.$textWeight : 500)}; font-family:${(props) => (props.$fontFamily ? props.$fontFamily : 'sans-serif')}; font-style:${(props) => (props.$fontStyle ? props.$fontStyle : 'normal')}; @@ -81,8 +82,8 @@ const Item = styled.div<{ margin:${(props) => props.$margin ? props.$margin : '0px'}; &:hover { - color: ${(props) => props.$activeColor}; - cursor: pointer; + color: ${(props) => props.$disabled ? (props.$active ? props.$activeColor : props.$color) : props.$activeColor}; + cursor: ${(props) => props.$disabled ? 'not-allowed' : 'pointer'}; } .anticon { @@ -166,6 +167,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { const label = view?.label; const active = !!view?.active; const onEvent = view?.onEvent; + const disabled = !!view?.disabled; const subItems = isCompItem ? view?.items : []; const subMenuItems: Array<{ key: string; label: string }> = []; @@ -199,7 +201,8 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { $textTransform={props.style.textTransform} $textDecoration={props.style.textDecoration} $margin={props.style.margin} - onClick={() => onEvent && onEvent("click")} + $disabled={disabled} + onClick={() => { if (!disabled && onEvent) onEvent("click"); }} > {label} {Array.isArray(subItems) && subItems.length > 0 && } @@ -209,6 +212,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { const subMenu = ( { + if (disabled) return; const subItem = subItems[Number(e.key)]; const onSubEvent = subItem?.getView()?.onEvent; onSubEvent && onSubEvent("click"); @@ -221,6 +225,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { subMenu} + disabled={disabled} > {item} @@ -320,6 +325,7 @@ function createNavItemsControl() { { label: StringControl, hidden: BoolCodeControl, + disabled: BoolCodeControl, active: BoolCodeControl, onEvent: eventHandlerControl([clickEvent]), }, @@ -330,7 +336,8 @@ function createNavItemsControl() { {children.label.propertyView({ label: trans("label"), placeholder: "{{item}}" })} {children.active.propertyView({ label: trans("navItemComp.active") })} {children.hidden.propertyView({ label: trans("hidden") })} - {children.onEvent.propertyView({ inline: true })} + {children.disabled.propertyView({ label: trans("disabled") })} + {children.onEvent.getPropertyView()} )) .build(); diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx index 565013ab4..c0b0695d7 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx @@ -3,7 +3,7 @@ import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerCont import { list } from "comps/generators/list"; import { parseChildrenFromValueAndChildrenMap, ToViewReturn } from "comps/generators/multi"; import { withDefault } from "comps/generators/simpleGenerators"; -import { hiddenPropertyView } from "comps/utils/propertyUtils"; +import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; import _ from "lodash"; import { fromRecord, MultiBaseComp, Node, RecordNode, RecordNodeToValue } from "lowcoder-core"; @@ -14,6 +14,7 @@ const events = [clickEvent]; const childrenMap = { label: StringControl, hidden: BoolCodeControl, + disabled: BoolCodeControl, active: BoolCodeControl, onEvent: withDefault(eventHandlerControl(events), [ { @@ -29,6 +30,7 @@ const childrenMap = { type ChildrenType = { label: InstanceType; hidden: InstanceType; + disabled: InstanceType; active: InstanceType; onEvent: InstanceType>; items: InstanceType>; @@ -43,6 +45,7 @@ export class NavItemComp extends MultiBaseComp { return ( <> {this.children.label.propertyView({ label: trans("label") })} + {disabledPropertyView(this.children)} {hiddenPropertyView(this.children)} {this.children.active.propertyView({ label: trans("navItemComp.active") })} {this.children.onEvent.propertyView({ inline: true })} @@ -69,6 +72,7 @@ export class NavItemComp extends MultiBaseComp { return fromRecord({ label: this.children.label.exposingNode(), hidden: this.children.hidden.exposingNode(), + disabled: this.children.disabled.exposingNode(), active: this.children.active.exposingNode(), items: this.children.items.exposingNode(), }); @@ -78,6 +82,7 @@ export class NavItemComp extends MultiBaseComp { type NavItemExposing = { label: Node; hidden: Node; + disabled: Node; active: Node; items: Node[]>; }; From 8dc2af3bd574a51f70cdfd2a6a56c330d7a6b652 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 3 Oct 2025 16:08:29 +0500 Subject: [PATCH 04/97] [Fix]: #2021 import nav app data issue --- .../src/comps/comps/navComp/navComp.tsx | 19 +++++++++++++++++-- .../packages/lowcoder/src/i18n/locales/en.ts | 2 ++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 6be3f9ce7..b752f414e 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -136,13 +136,29 @@ function fixOldStyleData(oldData: any) { return oldData; } +function fixOldItemsData(oldData: any) { + if (Array.isArray(oldData)) { + return { + optionType: "manual", + manual: oldData, + }; + } + if (oldData && !oldData.optionType && Array.isArray(oldData.manual)) { + return { + optionType: "manual", + manual: oldData.manual, + }; + } + return oldData; +} + const childrenMap = { logoUrl: StringControl, logoEvent: withDefault(eventHandlerControl(logoEventHandlers), [{ name: "click" }]), horizontalAlignment: alignWithJustifyControl(), style: migrateOldData(styleControl(NavigationStyle, 'style'), fixOldStyleData), animationStyle: styleControl(AnimationStyle, 'animationStyle'), - items: withDefault(createNavItemsControl(), { + items: withDefault(migrateOldData(createNavItemsControl(), fixOldItemsData), { optionType: "manual", manual: [ { @@ -320,7 +336,6 @@ function createNavItemsControl() { { label: trans("prop.map"), value: "map" }, ] as const; - // Variant used in Map mode const NavMapOption = new MultiCompBuilder( { label: StringControl, diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 9ec7f365e..c607a218b 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -26,6 +26,8 @@ export const en = { "text": "Text", "basic": "Basic", "label": "Label", + "hidden": "Hidden", + "disabled": "Disabled", "layout": "Layout", "color": "Color", "form": "Form", From 18fadd684b48352febf5c202944ecfb2cd091c55 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 3 Oct 2025 16:22:54 +0500 Subject: [PATCH 05/97] [Fix]: #2021 disabled for the submenu item --- client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index b752f414e..e14dc94f6 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -186,7 +186,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { const disabled = !!view?.disabled; const subItems = isCompItem ? view?.items : []; - const subMenuItems: Array<{ key: string; label: string }> = []; + const subMenuItems: Array<{ key: string; label: any; disabled?: boolean }> = []; const subMenuSelectedKeys: Array = []; if (Array.isArray(subItems)) { @@ -199,6 +199,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { subMenuItems.push({ key: key, label: subItem.children.label.getView(), + disabled: !!subItem.children.disabled.getView(), }); }); } @@ -230,6 +231,8 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { onClick={(e) => { if (disabled) return; const subItem = subItems[Number(e.key)]; + const isSubDisabled = !!subItem?.children?.disabled?.getView?.(); + if (isSubDisabled) return; const onSubEvent = subItem?.getView()?.onEvent; onSubEvent && onSubEvent("click"); }} From 1d8656248f8ad6e724c572c3d871dc3b3bb61da9 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 3 Oct 2025 18:32:13 +0500 Subject: [PATCH 06/97] refactor navComp --- .../navComp/components/NavItemsControl.tsx | 77 +++++++++++++++++++ .../src/comps/comps/navComp/navComp.tsx | 72 +---------------- .../src/comps/comps/navComp/navItemComp.tsx | 2 +- 3 files changed, 79 insertions(+), 72 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx new file mode 100644 index 000000000..ee0817b49 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx @@ -0,0 +1,77 @@ +import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; +import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; +import { MultiCompBuilder } from "comps/generators/multi"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import { mapOptionsControl } from "comps/controls/optionsControl"; +import { trans } from "i18n"; +import { navListComp } from "../navItemComp"; +import { controlItem } from "lowcoder-design"; +import { menuPropertyView } from "./MenuItemList"; + +export function createNavItemsControl() { + const OptionTypes = [ + { label: trans("prop.manual"), value: "manual" }, + { label: trans("prop.map"), value: "map" }, + ] as const; + + const NavMapOption = new MultiCompBuilder( + { + label: StringControl, + hidden: BoolCodeControl, + disabled: BoolCodeControl, + active: BoolCodeControl, + onEvent: eventHandlerControl([clickEvent]), + }, + (props) => props + ) + .setPropertyViewFn((children) => ( + <> + {children.label.propertyView({ label: trans("label"), placeholder: "{{item}}" })} + {children.active.propertyView({ label: trans("navItemComp.active") })} + {children.hidden.propertyView({ label: trans("hidden") })} + {children.disabled.propertyView({ label: trans("disabled") })} + {children.onEvent.getPropertyView()} + + )) + .build(); + + const TmpNavItemsControl = new MultiCompBuilder( + { + optionType: dropdownControl(OptionTypes, "manual"), + manual: navListComp(), + mapData: mapOptionsControl(NavMapOption), + }, + (props) => { + return props.optionType === "manual" ? props.manual : props.mapData; + } + ) + .setPropertyViewFn(() => { + throw new Error("Method not implemented."); + }) + .build(); + + return class NavItemsControl extends TmpNavItemsControl { + exposingNode() { + return this.children.optionType.getView() === "manual" + ? (this.children.manual as any).exposingNode() + : (this.children.mapData as any).exposingNode(); + } + + propertyView() { + const isManual = this.children.optionType.getView() === "manual"; + const content = isManual + ? menuPropertyView(this.children.manual as any) + : this.children.mapData.getPropertyView(); + + return controlItem( + { searchChild: true }, + <> + {this.children.optionType.propertyView({ radioButton: true, type: "oneline" })} + {content} + + ); + } + }; +} + + diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index e14dc94f6..670f4bba9 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -23,9 +23,8 @@ import { trans } from "i18n"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; -import { dropdownControl } from "comps/controls/dropdownControl"; import { controlItem } from "lowcoder-design"; -import { mapOptionsControl } from "comps/controls/optionsControl"; +import { createNavItemsControl } from "./components/NavItemsControl"; type IProps = { $justify: boolean; @@ -329,72 +328,3 @@ export const NavComp = withExposingConfigs(NavCompBase, [ NameConfigHidden, new NameConfig("items", trans("navigation.itemsDesc")), ]); - -// ---------------------------------------- -// Nav Items Control (Manual / Map modes) -// ---------------------------------------- -function createNavItemsControl() { - const OptionTypes = [ - { label: trans("prop.manual"), value: "manual" }, - { label: trans("prop.map"), value: "map" }, - ] as const; - - const NavMapOption = new MultiCompBuilder( - { - label: StringControl, - hidden: BoolCodeControl, - disabled: BoolCodeControl, - active: BoolCodeControl, - onEvent: eventHandlerControl([clickEvent]), - }, - (props) => props - ) - .setPropertyViewFn((children) => ( - <> - {children.label.propertyView({ label: trans("label"), placeholder: "{{item}}" })} - {children.active.propertyView({ label: trans("navItemComp.active") })} - {children.hidden.propertyView({ label: trans("hidden") })} - {children.disabled.propertyView({ label: trans("disabled") })} - {children.onEvent.getPropertyView()} - - )) - .build(); - - const TmpNavItemsControl = new MultiCompBuilder( - { - optionType: dropdownControl(OptionTypes, "manual"), - manual: navListComp(), - mapData: mapOptionsControl(NavMapOption), - }, - (props) => { - return props.optionType === "manual" ? props.manual : props.mapData; - } - ) - .setPropertyViewFn(() => { - throw new Error("Method not implemented."); - }) - .build(); - - return class NavItemsControl extends TmpNavItemsControl { - exposingNode() { - return this.children.optionType.getView() === "manual" - ? (this.children.manual as any).exposingNode() - : (this.children.mapData as any).exposingNode(); - } - - propertyView() { - const isManual = this.children.optionType.getView() === "manual"; - const content = isManual - ? menuPropertyView(this.children.manual as any) - : this.children.mapData.getPropertyView(); - - return controlItem( - { searchChild: true }, - <> - {this.children.optionType.propertyView({ radioButton: true, type: "oneline" })} - {content} - - ); - } - }; -} diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx index c0b0695d7..e8ce0f011 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx @@ -45,9 +45,9 @@ export class NavItemComp extends MultiBaseComp { return ( <> {this.children.label.propertyView({ label: trans("label") })} - {disabledPropertyView(this.children)} {hiddenPropertyView(this.children)} {this.children.active.propertyView({ label: trans("navItemComp.active") })} + {disabledPropertyView(this.children)} {this.children.onEvent.propertyView({ inline: true })} ); From adccc1621db4db065a981f5799cc630963b0c77e Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 3 Oct 2025 22:15:44 +0500 Subject: [PATCH 07/97] [Feat]: #1979 expose hide/showcolumns methods --- .../src/comps/comps/tableComp/tableComp.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index 725543eff..f050997a0 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx @@ -822,6 +822,55 @@ TableTmpComp = withMethodExposing(TableTmpComp, [ } }, } + , + { + method: { + name: "hideColumns", + description: "Hide specified columns by dataIndex or title", + params: [ + { name: "columns", type: "arrayString" }, + ], + }, + execute: (comp, values) => { + const columns = values[0]; + if (!isArray(columns)) { + return Promise.reject("hideColumns expects an array of strings, e.g. ['id','name']"); + } + const targets = new Set((columns as any[]).map((c) => String(c))); + comp.children.columns.getView().forEach((c) => { + const view = c.getView(); + if (targets.has(view.dataIndex) || targets.has(view.title)) { + // Ensure both persistent and temporary flags are updated + c.children.hide.dispatchChangeValueAction(true); + c.children.tempHide.dispatchChangeValueAction(true); + } + }); + }, + } + , + { + method: { + name: "showColumns", + description: "Show specified columns by dataIndex or title", + params: [ + { name: "columns", type: "arrayString" }, + ], + }, + execute: (comp, values) => { + const columns = values[0]; + if (!isArray(columns)) { + return Promise.reject("showColumns expects an array of strings, e.g. ['id','name']"); + } + const targets = new Set((columns as any[]).map((c) => String(c))); + comp.children.columns.getView().forEach((c) => { + const view = c.getView(); + if (targets.has(view.dataIndex) || targets.has(view.title)) { + c.children.hide.dispatchChangeValueAction(false); + c.children.tempHide.dispatchChangeValueAction(false); + } + }); + }, + } ]); // exposing data @@ -1052,6 +1101,30 @@ export const TableComp = withExposingConfigs(TableTmpComp, [ }, trans("table.displayDataDesc") ), + new CompDepsConfig( + "hiddenColumns", + (comp) => { + return { + dataIndexes: comp.children.columns.getColumnsNode("dataIndex"), + hides: comp.children.columns.getColumnsNode("hide"), + tempHides: comp.children.columns.getColumnsNode("tempHide"), + columnSetting: comp.children.toolbar.children.columnSetting.node(), + }; + }, + (input) => { + const hidden: string[] = []; + _.forEach(input.dataIndexes, (dataIndex, idx) => { + const isHidden = columnHide({ + hide: input.hides[idx].value, + tempHide: input.tempHides[idx], + enableColumnSetting: input.columnSetting.value, + }); + if (isHidden) hidden.push(dataIndex); + }); + return hidden; + }, + trans("table.displayDataDesc") + ), new DepsConfig( "filter", (children) => { From 46599c4f64ca1de44450b1cfba845bfd60551212 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 6 Oct 2025 16:58:55 +0500 Subject: [PATCH 08/97] [Fix]: #1065 duplicates in continuous mode --- .../src/comps/comps/buttonComp/scannerComp.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx index 8b061cb4c..c4387f55c 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx @@ -150,11 +150,19 @@ const ScannerTmpComp = (function () { }, [success, showModal]); const continuousValue = useRef([]); + const seenSetRef = useRef>(new Set()); const handleUpdate = (err: any, result: any) => { if (result) { if (props.continuous) { - continuousValue.current = [...continuousValue.current, result.text]; + const scannedText: string = result.text; + if (props.uniqueData && seenSetRef.current.has(scannedText)) { + return; + } + continuousValue.current = [...continuousValue.current, scannedText]; + if (props.uniqueData) { + seenSetRef.current.add(scannedText); + } const val = props.uniqueData ? [...new Set(continuousValue.current)] : continuousValue.current; @@ -205,6 +213,7 @@ const ScannerTmpComp = (function () { props.onEvent("click"); setShowModal(true); continuousValue.current = []; + seenSetRef.current = new Set(); }} > {props.text} From 022cd512b3217af6f11c832e84ae84432ac72550 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 6 Oct 2025 19:35:48 +0500 Subject: [PATCH 09/97] [Fix]: #1060 exposed .value to open correct URL --- .../lowcoder/src/comps/comps/buttonComp/scannerComp.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx index c4387f55c..8900ea914 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx @@ -30,7 +30,7 @@ import React, { useState, useContext, } from "react"; -import { arrayStringExposingStateControl } from "comps/controls/codeStateControl"; +import { arrayStringExposingStateControl, stringExposingStateControl } from "comps/controls/codeStateControl"; import { BoolControl } from "comps/controls/boolControl"; import { RefControl } from "comps/controls/refControl"; import { EditorContext } from "comps/editorState"; @@ -120,6 +120,7 @@ const BarcodeScannerComponent = React.lazy( const ScannerTmpComp = (function () { const childrenMap = { data: arrayStringExposingStateControl("data"), + value: stringExposingStateControl("value"), text: withDefault(StringControl, trans("scanner.text")), continuous: BoolControl, uniqueData: withDefault(BoolControl, true), @@ -166,9 +167,11 @@ const ScannerTmpComp = (function () { const val = props.uniqueData ? [...new Set(continuousValue.current)] : continuousValue.current; + props.value.onChange(scannedText); props.data.onChange(val); props.onEvent("success"); } else { + props.value.onChange(result.text); props.data.onChange([result.text]); setShowModal(false); setSuccess(true); @@ -326,6 +329,7 @@ const ScannerTmpComp = (function () { export const ScannerComp = withExposingConfigs(ScannerTmpComp, [ new NameConfig("data", trans("data")), + new NameConfig("value", trans("value")), new NameConfig("text", trans("button.textDesc")), ...CommonNameConfig, ]); From 7cfa888cf8f908bea025eb8f07421757eb46aca5 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 7 Oct 2025 18:29:00 +0500 Subject: [PATCH 10/97] [Fix]: #2039 copyToClipboard utility content --- client/packages/lowcoder/src/comps/hooks/utilsComp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/hooks/utilsComp.ts b/client/packages/lowcoder/src/comps/hooks/utilsComp.ts index 2a8689efc..a7190b1f3 100644 --- a/client/packages/lowcoder/src/comps/hooks/utilsComp.ts +++ b/client/packages/lowcoder/src/comps/hooks/utilsComp.ts @@ -87,12 +87,12 @@ UtilsComp = withMethodExposing(UtilsComp, [ method: { name: "copyToClipboard", description: trans("utilsComp.copyToClipboard"), - params: [{ name: "url", type: "string" }], + params: [{ name: "text", type: "string" }], }, execute: (comp, params) => { const text = params?.[0]; if (typeof text === "string" && !isEmpty(text)) { - copy(text); + copy(text, { format: "text/plain" }); } }, }, From a4980bb0eab0ac9624f7cc49ddea355bcdfb4cf8 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 7 Oct 2025 21:55:58 +0500 Subject: [PATCH 11/97] [Fix]: #2041 form content default values --- .../src/comps/comps/formComp/formComp.tsx | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/formComp/formComp.tsx b/client/packages/lowcoder/src/comps/comps/formComp/formComp.tsx index 825581393..bece24fe2 100644 --- a/client/packages/lowcoder/src/comps/comps/formComp/formComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/formComp/formComp.tsx @@ -390,15 +390,24 @@ let FormTmpComp = class extends FormBaseComp implements IForm { if (ret.children.initialData !== this.children.initialData) { // FIXME: kill setTimeout ? setTimeout(() => { - this.dispatch( - customAction( - { - type: "setData", - initialData: (action.value["initialData"] as ValueAndMsg).value || {}, - }, - false - ) - ); + const newInitialData = (action.value["initialData"] as ValueAndMsg) + .value; + // only setData when initialData has explicit keys. + if ( + newInitialData && + typeof newInitialData === "object" && + Object.keys(newInitialData).length > 0 + ) { + this.dispatch( + customAction( + { + type: "setData", + initialData: newInitialData, + }, + false + ) + ); + } }, 1000); } return ret; From 32b706f205649258e20271c5c477c9a3ca31c149 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 8 Oct 2025 20:52:11 +0500 Subject: [PATCH 12/97] [Feat]: #1867 add styles for the button column --- .../tableComp/column/simpleColumnTypeComps.tsx | 13 +++++++++---- .../lowcoder/src/comps/controls/styleControl.tsx | 6 ++++-- .../src/comps/controls/styleControlConstants.tsx | 13 +++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx index f9bedc754..eaac53327 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx @@ -4,8 +4,8 @@ import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { disabledPropertyView, loadingPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; -import { useStyle } from "comps/controls/styleControl"; -import { ButtonStyle } from "comps/controls/styleControlConstants"; +import { styleControl, useStyle } from "comps/controls/styleControl"; +import { ButtonStyle, TableColumnButtonStyle } from "comps/controls/styleControlConstants"; import { Button100 } from "comps/comps/buttonComp/buttonCompConstants"; import { IconControl } from "comps/controls/iconControl"; import { hasIcon } from "comps/utils"; @@ -58,10 +58,11 @@ const childrenMap = { disabled: BoolCodeControl, prefixIcon: IconControl, suffixIcon: IconControl, + style: styleControl(TableColumnButtonStyle, 'style', { boldTitle: true }), }; const ButtonStyled = React.memo(({ props }: { props: ToViewReturn>}) => { - const style = useStyle(ButtonStyle); + const themeButtonStyle = useStyle(ButtonStyle); const hasText = !!props.text; const hasPrefixIcon = hasIcon(props.prefixIcon); const hasSuffixIcon = hasIcon(props.suffixIcon); @@ -85,7 +86,10 @@ const ButtonStyled = React.memo(({ props }: { props: ToViewReturn @@ -120,6 +124,7 @@ const ButtonCompTmp = (function () { })} {loadingPropertyView(children)} {disabledPropertyView(children)} + {children.style.getPropertyView()} {children.onClick.propertyView()} )) diff --git a/client/packages/lowcoder/src/comps/controls/styleControl.tsx b/client/packages/lowcoder/src/comps/controls/styleControl.tsx index 5d40a2db4..a743d466d 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControl.tsx @@ -937,11 +937,12 @@ function calcColors>( return res as ColorMap; } -const TitleDiv = styled.div` +const TitleDiv = styled.div<{ $boldTitle?: boolean }>` display: flex; justify-content: space-between; font-size: 13px; line-height: 1; + font-weight: ${(props) => (props.$boldTitle ? 600 : 400)}; span:nth-of-type(2) { cursor: pointer; @@ -1149,6 +1150,7 @@ const useThemeStyles = ( export function styleControl( colorConfigs: T, styleKey: string = '', + options?: { boldTitle?: boolean }, ) { type ColorMap = { [K in Names]: string }; const childrenMap: any = {}; @@ -1268,7 +1270,7 @@ export function styleControl( return ( <> - + {label} {showReset && ( ; export type TimeLineStyleType = StyleConfigType; export type AvatarStyleType = StyleConfigType; @@ -2437,6 +2449,7 @@ export type TableColumnStyleType = StyleConfigType; export type TableColumnLinkStyleType = StyleConfigType< typeof TableColumnLinkStyle >; +export type TableColumnButtonStyleType = StyleConfigType; export type TableSummaryRowStyleType = StyleConfigType; export type FileStyleType = StyleConfigType; export type FileViewerStyleType = StyleConfigType; From e8bc8815a9b2e1252bfc3df3b2d4e7a414159b68 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 9 Oct 2025 19:55:47 +0500 Subject: [PATCH 13/97] [Fix]: #1290 add zindex property for stacking issue --- .../packages/lowcoder/src/comps/hooks/drawerComp.tsx | 8 ++++++-- client/packages/lowcoder/src/comps/hooks/modalComp.tsx | 10 +++++++--- client/packages/lowcoder/src/i18n/locales/en.ts | 3 ++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx index 3b33c3fb9..42a7d392f 100644 --- a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx @@ -4,7 +4,7 @@ import { ContainerCompBuilder } from "comps/comps/containerBase/containerCompBui import { gridItemCompToGridItems, InnerGrid } from "comps/comps/containerComp/containerView"; import { AutoHeightControl } from "comps/controls/autoHeightControl"; import { BoolControl } from "comps/controls/boolControl"; -import { StringControl } from "comps/controls/codeControl"; +import { StringControl, NumberControl } from "comps/controls/codeControl"; import { booleanExposingStateControl } from "comps/controls/codeStateControl"; import { PositionControl, LeftRightControl, HorizontalAlignmentControl } from "comps/controls/dropdownControl"; import { eventHandlerControl } from "comps/controls/eventHandlerControl"; @@ -122,6 +122,7 @@ const childrenMap = { showMask: withDefault(BoolControl, true), toggleClose:withDefault(BoolControl,true), escapeClosable: withDefault(BoolControl, true), + zIndex: withDefault(NumberControl, Layers.drawer), }; type ChildrenType = NewChildren> & { @@ -168,6 +169,9 @@ const DrawerPropertyView = React.memo((props: { {props.children.escapeClosable.propertyView({ label: trans("prop.escapeClose"), })} + {props.children.zIndex.propertyView({ + label: trans("prop.zIndex"), + })}
{props.children.onEvent.getPropertyView()}
{props.children.style.getPropertyView()}
@@ -251,7 +255,7 @@ const DrawerView = React.memo(( height={!props.autoHeight ? transToPxSize(props.height || DEFAULT_SIZE) : ""} onClose={onClose} afterOpenChange={afterOpenChange} - zIndex={Layers.drawer} + zIndex={props.zIndex} maskClosable={props.maskClosable} mask={true} className={clsx(`app-${appID}`, props.className)} diff --git a/client/packages/lowcoder/src/comps/hooks/modalComp.tsx b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx index 933495b5f..cf898ff1a 100644 --- a/client/packages/lowcoder/src/comps/hooks/modalComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx @@ -1,7 +1,7 @@ import { ContainerCompBuilder } from "comps/comps/containerBase/containerCompBuilder"; import { gridItemCompToGridItems, InnerGrid } from "comps/comps/containerComp/containerView"; import { AutoHeightControl } from "comps/controls/autoHeightControl"; -import { StringControl } from "comps/controls/codeControl"; +import { StringControl, NumberControl } from "comps/controls/codeControl"; import { booleanExposingStateControl } from "comps/controls/codeStateControl"; import { eventHandlerControl } from "comps/controls/eventHandlerControl"; import { styleControl } from "comps/controls/styleControl"; @@ -117,7 +117,8 @@ const childrenMap = { style: styleControl(ModalStyle), maskClosable: withDefault(BoolControl, true), showMask: withDefault(BoolControl, true), - toggleClose:withDefault(BoolControl,true) + toggleClose:withDefault(BoolControl,true), + zIndex: withDefault(NumberControl, Layers.modal) }; const ModalPropertyView = React.memo((props: { @@ -156,6 +157,9 @@ const ModalPropertyView = React.memo((props: { {props.children.toggleClose.propertyView({ label: trans("prop.toggleClose"), })} + {props.children.zIndex.propertyView({ + label: trans("prop.zIndex"), + })}
{props.children.onEvent.getPropertyView()}
{props.children.style.getPropertyView()}
@@ -278,7 +282,7 @@ const ModalView = React.memo(( onCancel={handleCancel} afterClose={handleAfterClose} afterOpenChange={handleAfterOpenChange} - zIndex={Layers.modal} + zIndex={props.zIndex} modalRender={modalRender} mask={props.showMask} className={clsx(`app-${appID}`, props.className)} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 60095c146..18e71b23c 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -237,7 +237,8 @@ export const en = { "timeZone": "TimeZone", "pickerMode": "Picker Mode", "customTags": "Allow Custom Tags", - "customTagsTooltip": "Allow users to enter custom tags that are not in the options list." + "customTagsTooltip": "Allow users to enter custom tags that are not in the options list.", + "zIndex": "z-Index" }, "autoHeightProp": { "auto": "Auto", From e74e078ebba4b3d8ac8c84d406549a0bff1ea050 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 10 Oct 2025 20:33:30 +0500 Subject: [PATCH 14/97] [Feat]: #1866 add class/id identifies for Table/TableLite --- .../comps/comps/tableComp/ResizeableTable.tsx | 4 +++ .../tableComp/column/tableColumnComp.tsx | 28 ++++++++++++------- .../comps/comps/tableComp/tableCompView.tsx | 4 +++ .../comps/tableComp/tablePropertyView.tsx | 5 ++++ .../src/comps/comps/tableComp/tableStyles.ts | 8 +++++- .../src/comps/comps/tableComp/tableUtils.tsx | 4 +++ .../tableLiteComp/column/tableColumnComp.tsx | 28 ++++++++++++------- .../comps/tableLiteComp/parts/BaseTable.tsx | 4 +++ .../tableLiteComp/parts/ResizeableTable.tsx | 4 +++ .../tableLiteComp/parts/TableContainer.tsx | 16 +++++++++-- .../comps/tableLiteComp/tableCompView.tsx | 6 ++++ .../comps/tableLiteComp/tablePropertyView.tsx | 5 ++++ .../comps/comps/tableLiteComp/tableUtils.tsx | 4 +++ 13 files changed, 97 insertions(+), 23 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx index 55bf8d694..3cdf9119a 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx @@ -165,6 +165,8 @@ function ResizeableTableComp(props: CustomTableProps< onClick: () => onCellClick(col.titleText, String(col.dataIndex)), loading: customLoading, customAlign: col.align, + className: col.columnClassName, + 'data-testid': col.columnDataTestId, }); }, [rowColorFn, rowHeightFn, columnsStyle, size, rowAutoHeight, onCellClick, customLoading]); @@ -182,6 +184,8 @@ function ResizeableTableComp(props: CustomTableProps< onResizeStop: (e: React.SyntheticEvent, { size }: { size: { width: number } }) => { handleResizeStop(size.width, index, col.onWidthResize); }, + className: col.columnClassName, + 'data-testid': col.columnDataTestId, }); }, [viewModeResizable, handleResize, handleResizeStop]); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx index 938983ac9..f94b17816 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx @@ -134,6 +134,9 @@ export const columnChildrenMap = { align: HorizontalAlignmentControl, tempHide: stateComp(false), fixed: dropdownControl(columnFixOptions, "close"), + // identifiers + className: StringControl, + dataTestId: StringControl, editable: BoolControl, background: withDefault(ColorControl, ""), margin: withDefault(RadiusControl, ""), @@ -162,6 +165,11 @@ const StyledFontFamilyIcon = styled(FontFamilyIcon)` width: 24px; margin: 0 8px const StyledTextWeightIcon = styled(TextWeightIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`; const StyledBackgroundImageIcon = styled(ImageCompIcon)` width: 24px; margin: 0 0px 0 -12px;`; +const SectionHeading = styled.div` + font-weight: bold; + margin-bottom: 8px; +`; + /** * export for test. * Put it here temporarily to avoid circular dependencies @@ -283,11 +291,7 @@ const ColumnPropertyView = React.memo(({ {(columnType === 'link' || columnType === 'links') && ( <> - {controlItem({}, ( -
- {"Link Style"} -
- ))} + Link Style {comp.children.linkColor.propertyView({ label: trans('text') // trans('style.background'), })} @@ -300,11 +304,7 @@ const ColumnPropertyView = React.memo(({ )} - {controlItem({}, ( -
- {"Column Style"} -
- ))} + Column Style {comp.children.background.propertyView({ label: trans('style.background'), })} @@ -346,6 +346,14 @@ const ColumnPropertyView = React.memo(({ })} {comp.children.textOverflow.getPropertyView()} {comp.children.cellColor.getPropertyView()} + + Identifiers + {comp.children.className.propertyView({ + label: trans("prop.className"), + })} + {comp.children.dataTestId.propertyView({ + label: trans("prop.dataTestId"), + })} )} diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index 53d423129..74d431f7a 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -100,6 +100,8 @@ export const TableCompView = React.memo((props: { const onEvent = useMemo(() => compChildren.onEvent.getView(), [compChildren.onEvent]); const currentExpandedRows = useMemo(() => compChildren.currentExpandedRows.getView(), [compChildren.currentExpandedRows]); const dynamicColumn = compChildren.dynamicColumn.getView(); + const className = compChildren.className.getView(); + const dataTestId = compChildren.dataTestId.getView(); const dynamicColumnConfig = useMemo( () => compChildren.dynamicColumnConfig.getView(), @@ -360,6 +362,8 @@ export const TableCompView = React.memo((props: { suffixNode={toolbar.position === "below" && !toolbar.fixedToolbar && !(tableMode.isAutoMode && showHorizontalScrollbar) && toolbarView} > tooltip: trans("table.dynamicColumnConfigDesc"), })} + +
+ {comp.children.className.propertyView({ label: trans("prop.className") })} + {comp.children.dataTestId.propertyView({ label: trans("prop.dataTestId") })} +
)} diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts index 5e3cc6dcd..54c856c14 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts @@ -122,7 +122,13 @@ export const BackgroundWrapper = styled.div<{ `; // TODO: find a way to limit the calc function for max-height only to first Margin value -export const TableWrapper = styled.div<{ +export const TableWrapper = styled.div.attrs<{ + className?: string; + "data-testid"?: string; +}>((props) => ({ + className: props.className, + "data-testid": props["data-testid"], +}))<{ $style: TableStyleType; $headerStyle: TableHeaderStyleType; $toolbarStyle: TableToolbarStyleType; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index d71ae5a8d..fdc5c775d 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx @@ -333,6 +333,8 @@ export type CustomColumnType = ColumnType & { style: TableColumnStyleType; linkStyle: TableColumnLinkStyleType; cellColorFn: CellColorViewType; + columnClassName?: string; + columnDataTestId?: string; }; /** @@ -400,6 +402,8 @@ export function columnsToAntdFormat( align: column.align, width: column.autoWidth === "auto" ? 0 : column.width, fixed: column.fixed === "close" ? false : column.fixed, + columnClassName: column.className, + columnDataTestId: column.dataTestId, style: { background: column.background, margin: column.margin, diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/tableColumnComp.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/tableColumnComp.tsx index 4951dea4e..fcfab56af 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/tableColumnComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/tableColumnComp.tsx @@ -136,6 +136,9 @@ export const columnChildrenMap = { align: HorizontalAlignmentControl, tempHide: stateComp(false), fixed: dropdownControl(columnFixOptions, "close"), + // identifiers + className: StringControl, + dataTestId: StringControl, background: withDefault(ColorControl, ""), margin: withDefault(RadiusControl, ""), text: withDefault(ColorControl, ""), @@ -163,6 +166,11 @@ const StyledFontFamilyIcon = styled(FontFamilyIcon)` width: 24px; margin: 0 8px const StyledTextWeightIcon = styled(TextWeightIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`; const StyledBackgroundImageIcon = styled(ImageCompIcon)` width: 24px; margin: 0 0px 0 -12px;`; +const SectionHeading = styled.div` + font-weight: bold; + margin-bottom: 8px; +`; + /** * export for test. * Put it here temporarily to avoid circular dependencies @@ -285,11 +293,7 @@ const ColumnPropertyView = React.memo(({ {(columnType === 'link' || columnType === 'links') && ( <> - {controlItem({}, ( -
- {"Link Style"} -
- ))} + Link Style {comp.children.linkColor.propertyView({ label: trans('text') // trans('style.background'), })} @@ -302,11 +306,7 @@ const ColumnPropertyView = React.memo(({ )} - {controlItem({}, ( -
- {"Column Style"} -
- ))} + Column Style {comp.children.background.propertyView({ label: trans('style.background'), })} @@ -348,6 +348,14 @@ const ColumnPropertyView = React.memo(({ })} {comp.children.textOverflow.getPropertyView()} {comp.children.cellColor.getPropertyView()} + + Identifiers + {comp.children.className.propertyView({ + label: trans("prop.className"), + })} + {comp.children.dataTestId.propertyView({ + label: trans("prop.dataTestId"), + })} )} diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/BaseTable.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/BaseTable.tsx index 31aa30f33..fffde7f21 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/BaseTable.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/BaseTable.tsx @@ -106,6 +106,8 @@ import React, { onClick: () => onCellClick(col.titleText, String(col.dataIndex)), loading: customLoading, customAlign: col.align, + className: col.columnClassName, + 'data-testid': col.columnDataTestId, }); }, [ @@ -135,6 +137,8 @@ import React, { ) => { handleResizeStop(size.width, index, col.onWidthResize); }, + className: col.columnClassName, + 'data-testid': col.columnDataTestId, }); }, [viewModeResizable, handleResize, handleResizeStop] diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/ResizeableTable.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/ResizeableTable.tsx index c7a5d39c2..16ca3a7d6 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/ResizeableTable.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/ResizeableTable.tsx @@ -108,6 +108,8 @@ function ResizeableTableComp( onClick: () => onCellClick(col.titleText, String(col.dataIndex)), loading: customLoading, customAlign: col.align, + className: col.columnClassName, + 'data-testid': col.columnDataTestId, }); }, [ @@ -138,6 +140,8 @@ function ResizeableTableComp( ) => { handleResizeStop(size.width, index, col.onWidthResize); }, + className: col.columnClassName, + 'data-testid': col.columnDataTestId, }); }, [viewModeResizable, handleResize, handleResizeStop] diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/TableContainer.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/TableContainer.tsx index 1a44cfa7d..247c0e0d4 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/TableContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/parts/TableContainer.tsx @@ -4,7 +4,13 @@ import styled from 'styled-components'; import SimpleBar from 'simplebar-react'; // import 'simplebar-react/dist/simplebar.min.css'; -const MainContainer = styled.div<{ +const MainContainer = styled.div.attrs<{ + className?: string; + "data-testid"?: string; +}>((props) => ({ + className: props.className, + "data-testid": props["data-testid"], +}))<{ $mode: 'AUTO' | 'FIXED'; $showHorizontalScrollbar: boolean; $showVerticalScrollbar: boolean; @@ -114,6 +120,8 @@ interface TableContainerProps { showVerticalScrollbar: boolean; showHorizontalScrollbar: boolean; virtual: boolean; + className?: string; + dataTestId?: string; } export const TableContainer: React.FC = ({ @@ -126,7 +134,9 @@ export const TableContainer: React.FC = ({ containerRef, showVerticalScrollbar, showHorizontalScrollbar, - virtual + virtual, + className, + dataTestId }) => { return ( = ({ $showHorizontalScrollbar={showHorizontalScrollbar} $showVerticalScrollbar={showVerticalScrollbar} $virtual={virtual} + className={className} + data-testid={dataTestId} > {/* Sticky above toolbar - always visible */} {stickyToolbar && toolbarPosition === 'above' && showToolbar && ( diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableCompView.tsx index c35fcc0ba..4d792e056 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableCompView.tsx @@ -34,6 +34,8 @@ export const TableCompView = React.memo((props: { const headerStyle = compChildren.headerStyle.getView(); const toolbarStyle = compChildren.toolbarStyle.getView(); const showHRowGridBorder = compChildren.showHRowGridBorder.getView(); + const className = compChildren.className.getView(); + const dataTestId = compChildren.dataTestId.getView(); const columns = useMemo(() => compChildren.columns.getView(), [compChildren.columns]); const columnViews = useMemo(() => columns.map((c) => c.getView()), [columns]); const data = comp.filterData; @@ -185,6 +187,8 @@ export const TableCompView = React.memo((props: { showHorizontalScrollbar={compChildren.showHorizontalScrollbar.getView()} virtual={virtualization.enabled} containerRef={containerRef} + className={className} + dataTestId={dataTestId} > @@ -206,6 +210,8 @@ export const TableCompView = React.memo((props: { showVerticalScrollbar={compChildren.showVerticalScrollbar.getView()} showHorizontalScrollbar={compChildren.showHorizontalScrollbar.getView()} virtual={virtualization.enabled} + className={className} + dataTestId={dataTestId} > diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tablePropertyView.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tablePropertyView.tsx index 62f3f1ffb..dbe4edcf8 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tablePropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tablePropertyView.tsx @@ -586,6 +586,11 @@ export function compTablePropertyView tooltip: trans("table.dynamicColumnConfigDesc"), })} + +
+ {comp.children.className.propertyView({ label: trans("prop.className") })} + {comp.children.dataTestId.propertyView({ label: trans("prop.dataTestId") })} +
)} diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx index 9297eded8..69f18ae83 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx @@ -286,6 +286,8 @@ export type CustomColumnType = ColumnType & { style: TableColumnStyleType; linkStyle: TableColumnLinkStyleType; cellColorFn: CellColorViewType; + columnClassName?: string; + columnDataTestId?: string; }; /** @@ -539,6 +541,8 @@ export function columnsToAntdFormat( fixed: column.fixed === "close" ? false : column.fixed, style, linkStyle, + columnClassName: column.className, + columnDataTestId: column.dataTestId, cellColorFn: column.cellColor, onWidthResize: column.onWidthResize, render: buildRenderFn( From a9ed70490204910bcbc7720d9aec66926770619f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 13 Oct 2025 17:21:14 +0500 Subject: [PATCH 15/97] [Fix]: #1945 allow null value --- .../src/comps/comps/numberInputComp/numberInputComp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index 9573e2a3b..fad9d36d1 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx @@ -331,7 +331,7 @@ const CustomInputNumber = (props: RecordConstructorToView) = value = Number(defaultValue); } props.value.onChange(value); - }, [defaultValue]); + }, [defaultValue, props.allowNull]); const formatFn = (value: number) => format(value, props.allowNull, props.formatter, props.precision, props.thousandsSeparator); From 99ce13d1bb9b70c87fd489908598d2f561089952 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 13 Oct 2025 22:21:48 +0500 Subject: [PATCH 16/97] [Feat]: #1289 add close icon customization --- client/packages/lowcoder/src/comps/hooks/drawerComp.tsx | 8 +++++++- client/packages/lowcoder/src/i18n/locales/en.ts | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx index 3b33c3fb9..1d89aa33d 100644 --- a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx @@ -24,6 +24,8 @@ import styled from "styled-components"; import { useUserViewMode } from "util/hooks"; import { isNumeric } from "util/stringUtils"; import { NameConfig, withExposingConfigs } from "../generators/withExposing"; +import { IconControl } from "comps/controls/iconControl"; +import { hasIcon } from "comps/utils"; import { title } from "process"; import { SliderControl } from "../controls/sliderControl"; import clsx from "clsx"; @@ -122,6 +124,7 @@ const childrenMap = { showMask: withDefault(BoolControl, true), toggleClose:withDefault(BoolControl,true), escapeClosable: withDefault(BoolControl, true), + closeIcon: withDefault(IconControl, ""), }; type ChildrenType = NewChildren> & { @@ -138,6 +141,9 @@ const DrawerPropertyView = React.memo((props: { {props.children.title.getView() && props.children.titleAlign.propertyView({ label: trans("drawer.titleAlign"), radioButton: true })} {props.children.closePosition.propertyView({ label: trans("drawer.closePosition"), radioButton: true })} {props.children.placement.propertyView({ label: trans("drawer.placement"), radioButton: true })} + {props.children.toggleClose.getView() && props.children.closeIcon.propertyView({ + label: trans("drawer.closeIcon"), + })} {["top", "bottom"].includes(props.children.placement.getView()) ? props.children.autoHeight.getPropertyView() : props.children.width.propertyView({ @@ -262,7 +268,7 @@ const DrawerView = React.memo(( $closePosition={props.closePosition} onClick={onClose} > - + {hasIcon(props.closeIcon) ? props.closeIcon : } )} Date: Tue, 14 Oct 2025 16:46:46 +0500 Subject: [PATCH 17/97] [Fix]: #1837 fix table header border styles --- .../lowcoder/src/comps/comps/tableComp/tableCompView.tsx | 2 ++ .../lowcoder/src/comps/comps/tableComp/tableStyles.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index 74d431f7a..3b3b47f6e 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -81,6 +81,7 @@ export const TableCompView = React.memo((props: { const showVerticalScrollbar = compChildren.showVerticalScrollbar.getView(); const visibleResizables = compChildren.visibleResizables.getView(); const showHRowGridBorder = compChildren.showHRowGridBorder.getView(); + const showRowGridBorder = compChildren.showRowGridBorder.getView(); const columnsStyle = compChildren.columnsStyle.getView(); const summaryRowStyle = compChildren.summaryRowStyle.getView(); const changeSet = useMemo(() => compChildren.columns.getChangeSet(), [compChildren.columns]); @@ -373,6 +374,7 @@ export const TableCompView = React.memo((props: { $fixedToolbar={toolbar.fixedToolbar && toolbar.position === 'above'} $visibleResizables={visibleResizables} $showHRowGridBorder={showHRowGridBorder} + $showRowGridBorder={showRowGridBorder} $isVirtual={scrollConfig.virtual} $showHorizontalScrollbar={showHorizontalScrollbar} $showVerticalScrollbar={showVerticalScrollbar} diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts index 54c856c14..3f783c84e 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts @@ -138,6 +138,7 @@ export const TableWrapper = styled.div.attrs<{ $fixedToolbar: boolean; $visibleResizables: boolean; $showHRowGridBorder?: boolean; + $showRowGridBorder?: boolean; $isVirtual?: boolean; $showHorizontalScrollbar?: boolean; $showVerticalScrollbar?: boolean; @@ -207,7 +208,10 @@ export const TableWrapper = styled.div.attrs<{ border-color: ${(props) => props.$headerStyle.border}; border-width: ${(props) => props.$headerStyle.borderWidth}; color: ${(props) => props.$headerStyle.headerText}; - // border-inline-end: ${(props) => `${props.$headerStyle.borderWidth} solid ${props.$headerStyle.border}`} !important; + ${(props) => props.$showRowGridBorder + ? `border-inline-end: ${props.$headerStyle.borderWidth} solid ${props.$headerStyle.border} !important;` + : `border-inline-end: none !important;` + } /* Proper styling for fixed header cells */ &.ant-table-cell-fix-left, &.ant-table-cell-fix-right { From 984eaa56e84929c07e7020b4733bf3ff69fb3b5a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 14 Oct 2025 23:11:15 +0500 Subject: [PATCH 18/97] [Feat]: #1837 add pagination styles for table --- .../comps/tableComp/tableToolbarComp.tsx | 43 ++++++++++++++++--- .../src/comps/controls/colorControl.tsx | 3 +- .../src/comps/controls/styleControl.tsx | 1 + .../comps/controls/styleControlConstants.tsx | 34 ++++++++++++++- .../packages/lowcoder/src/i18n/locales/en.ts | 10 ++++- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx index d29299c1a..cf977e96e 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx @@ -61,7 +61,7 @@ const getStyle = ( // Implement horizontal scrollbar and vertical page number selection is not blocked padding: 13px 12px; position: sticky; - postion: -webkit-sticky; + position: -webkit-sticky; left: 0px !important; margin: ${style.margin} !important; z-index: 999; @@ -116,7 +116,7 @@ const getStyle = ( .ant-pagination-prev, .ant-pagination-next { path { - ${style.toolbarText !== defaultTheme.textDark ? `fill: ${style.toolbarText}` : null}; + ${style.paginationText || style.toolbarText !== defaultTheme.textDark ? `fill: ${style.paginationText || style.toolbarText}` : null}; } svg:hover { @@ -127,25 +127,53 @@ const getStyle = ( } .ant-pagination { - color: ${style.toolbarText}; + color: ${style.paginationText || style.toolbarText}; + } + + // number items + .ant-pagination-item { + background: ${style.paginationBackground || 'transparent'}; + border-color: ${style.border || 'transparent'}; + a { + color: ${style.paginationText || style.toolbarText}; + } + &:hover a { + color: ${theme?.primary}; + } } .ant-pagination-item-active { + background: ${style.paginationActiveBackground || style.paginationBackground || 'transparent'}; border-color: ${style.border || theme?.primary}; a { - color: ${theme?.textDark}; + color: ${style.paginationActiveText || theme?.textDark}; } } .ant-pagination-item:not(.ant-pagination-item-active) a { - color: ${style.toolbarText}; + color: ${style.paginationText || style.toolbarText}; &:hover { color: ${theme?.primary}; } } + // size changer select + .ant-pagination-options { + .ant-select-selector { + background: ${style.paginationBackground || 'transparent'}; + color: ${style.paginationText || style.toolbarText}; + border-color: ${style.border || theme?.primary}; + } + .ant-select-selection-item { + color: ${style.paginationText || style.toolbarText}; + } + .ant-select-arrow { + color: ${style.paginationText || style.toolbarText}; + } + } + .ant-select:not(.ant-select-disabled):hover .ant-select-selector, .ant-select-focused:not(.ant-select-disabled).ant-select:not(.ant-select-customize-input) .ant-select-selector, @@ -153,6 +181,11 @@ const getStyle = ( .ant-pagination-options-quick-jumper input:focus { border-color: ${style.border || theme?.primary}; } + + .ant-pagination-options-quick-jumper input { + background: ${style.paginationBackground || 'transparent'}; + color: ${style.paginationText || style.toolbarText}; + } `; }; diff --git a/client/packages/lowcoder/src/comps/controls/colorControl.tsx b/client/packages/lowcoder/src/comps/controls/colorControl.tsx index 6b45a982d..333622ece 100644 --- a/client/packages/lowcoder/src/comps/controls/colorControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/colorControl.tsx @@ -74,6 +74,7 @@ type PropertyViewParam = { // auto-generated message? depMsg?: string; allowGradient?: boolean; + tooltip?: React.ReactNode; }; export class ColorControl extends ColorCodeControl { @@ -134,7 +135,7 @@ function ColorItem(props: { }, [containerRef]); return ( - + ( depMsg: depMsg, allowGradient: config.name.includes('background'), + tooltip: config.tooltip || getTooltip(name), })} ); diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 569ada9c4..602a062f2 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -10,6 +10,7 @@ type CommonColorConfig = { readonly name: string; readonly label: string; readonly platform?: SupportPlatform; // support all if undefined + readonly tooltip?: string; // Tooltip text to show on hover }; export type SimpleColorConfig = CommonColorConfig & { @@ -1767,6 +1768,38 @@ export const TableToolbarStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: toSelf, }, + // Pagination specific styling + { + name: "paginationBackground", + label: trans("style.paginationBackground"), + tooltip: trans("style.paginationBackgroundTooltip"), + depName: "background", + depType: DEP_TYPE.SELF, + transformer: toSelf, + }, + { + name: "paginationText", + label: trans("style.paginationText"), + tooltip: trans("style.paginationTextTooltip"), + depName: "paginationBackground", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, + { + name: "paginationActiveBackground", + label: trans("style.paginationActiveBackground"), + tooltip: trans("style.paginationActiveBackgroundTooltip"), + depName: "paginationBackground", + transformer: contrastBackground, + }, + { + name: "paginationActiveText", + label: trans("style.paginationActiveText"), + tooltip: trans("style.paginationActiveTextTooltip"), + depName: "paginationActiveBackground", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, ] as const; export const TableHeaderStyle = [ @@ -1774,7 +1807,6 @@ export const TableHeaderStyle = [ PADDING, FONT_FAMILY, FONT_STYLE, - TEXT, // getStaticBackground(SURFACE_COLOR), // getBackground("primarySurface"), { diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 98f07f3cc..4faaa1670 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -669,7 +669,15 @@ export const en = { "headerBackgroundImageOriginTip": "Specifies the positioning area of the header's background image. Example: padding-box, border-box, content-box.", "footerBackgroundImageOriginTip": "Specifies the positioning area of the footer's background image. Example: padding-box, border-box, content-box.", "rotationTip": "Specifies the rotation angle of the element. Example: 45deg, 90deg, -180deg.", - "lineHeightTip": "Sets the height of a line of text. Example: 1.5, 2, 120%." + "lineHeightTip": "Sets the height of a line of text. Example: 1.5, 2, 120%.", + "paginationBackground": "Pagination Background", + "paginationBackgroundTooltip": "Background color for pagination controls", + "paginationText": "Pagination Text", + "paginationTextTooltip": "Text color for pagination numbers and controls", + "paginationActiveBackground": "Pagination Active Background", + "paginationActiveBackgroundTooltip": "Background color for the active/selected page number", + "paginationActiveText": "Pagination Active Text", + "paginationActiveTextTooltip": "Text color for the active/selected page number", }, "export": { "hiddenDesc": "If true, the component is hidden", From b96646417e81d799b838c990ea9baea7956d54f4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 15 Oct 2025 16:35:54 +0500 Subject: [PATCH 19/97] [Feat]: #1868 prevent cell styles on selected row --- .../src/comps/comps/tableComp/tableStyles.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts index 54c856c14..48575922c 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts @@ -36,9 +36,6 @@ export const getStyle = ( // selected row > tr:nth-of-type(2n + 1).ant-table-row-selected { background: ${selectedRowBackground || rowStyle.background} !important; - > td.ant-table-cell { - background: transparent !important; - } // > td.ant-table-cell-row-hover, &:hover { @@ -48,9 +45,6 @@ export const getStyle = ( > tr:nth-of-type(2n).ant-table-row-selected { background: ${selectedRowBackground || alternateBackground} !important; - > td.ant-table-cell { - background: transparent !important; - } // > td.ant-table-cell-row-hover, &:hover { @@ -272,15 +266,8 @@ export const TableWrapper = styled.div.attrs<{ } /* Fix for selected and hovered rows */ - tr.ant-table-row-selected td.ant-table-cell-fix-left, - tr.ant-table-row-selected td.ant-table-cell-fix-right { - background-color: ${(props) => props.$rowStyle?.selectedRowBackground || '#e6f7ff'} !important; - } - tr.ant-table-row:hover td.ant-table-cell-fix-left, - tr.ant-table-row:hover td.ant-table-cell-fix-right { - background-color: ${(props) => props.$rowStyle?.hoverRowBackground || '#f5f5f5'} !important; - } + thead > tr:first-child { th:last-child { From ac3f139b71e5b0ee4e18ccaa08e0e683000c60e9 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 16 Oct 2025 21:38:15 +0500 Subject: [PATCH 20/97] [Fix]: #1811 column alignment --- client/packages/lowcoder/src/components/table/EditableCell.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/packages/lowcoder/src/components/table/EditableCell.tsx b/client/packages/lowcoder/src/components/table/EditableCell.tsx index 5064fffa1..b88616744 100644 --- a/client/packages/lowcoder/src/components/table/EditableCell.tsx +++ b/client/packages/lowcoder/src/components/table/EditableCell.tsx @@ -225,7 +225,6 @@ function EditableCellComp(props: EditableCellProps) { key={`normal-view-${cellIndex}`} tabIndex={editable ? 0 : -1 } onFocus={enterEditFn} - style={{ width: '100%', height: '100%'}} > {normalView} From 7c17a4852577c6e6290cd0b1fb4278b8b4584ff7 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 17 Oct 2025 17:43:56 +0500 Subject: [PATCH 21/97] [Feat]: #1795 add table download --- .../src/comps/comps/tableComp/tableComp.tsx | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index f050997a0..a8d151d1e 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx @@ -99,12 +99,65 @@ export class TableImplComp extends TableInitComp implements IContainer { } downloadData(fileName: string) { - saveDataAsFile({ - data: (this as any).exposingValues["displayData"], - filename: fileName, - fileType: "csv", - delimiter: this.children.toolbar.children.columnSeparator.getView(), - }); + const allDisplayData = (this as any).exposingValues["displayData"]; + const delimiter = this.children.toolbar.children.columnSeparator.getView(); + + try { + // Build the set of visible column keys as shown to the user (title or dataIndex) + const enableColumnSetting = this.children.toolbar.children.columnSetting.getView(); + const visibleColumnKeys = new Set(); + this.children.columns.getView().forEach((col) => { + const colView = col.getView(); + const isHidden = columnHide({ + hide: colView.hide, + tempHide: colView.tempHide, + enableColumnSetting, + }); + if (!isHidden) { + const headerKey = (colView.title as any) || colView.dataIndex; + if (headerKey) { + visibleColumnKeys.add(String(headerKey)); + } + } + }); + + const pickVisible = (row: any): any => { + const result: any = {}; + // copy only allowed keys + Object.keys(row || {}).forEach((key) => { + if (key !== COLUMN_CHILDREN_KEY && visibleColumnKeys.has(key)) { + result[key] = row[key]; + } + }); + // retain children recursively if present + if (Array.isArray(row?.[COLUMN_CHILDREN_KEY])) { + const children = row[COLUMN_CHILDREN_KEY].map((r: any) => pickVisible(r)); + if (children.length) { + result[COLUMN_CHILDREN_KEY] = children; + } + } + return result; + }; + + const dataToSave = Array.isArray(allDisplayData) + ? allDisplayData.map((r: any) => pickVisible(r)) + : allDisplayData; + + saveDataAsFile({ + data: dataToSave, + filename: fileName, + fileType: "csv", + delimiter, + }); + } catch (_e) { + // Fallback to previous behavior if anything goes wrong + saveDataAsFile({ + data: allDisplayData, + filename: fileName, + fileType: "csv", + delimiter, + }); + } } refreshData(allQueryNames: Array, setLoading: (loading: boolean) => void) { From 9d5e0aae15726dc3bb9b71feb1e5e79e963216e1 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 17 Oct 2025 22:39:56 +0500 Subject: [PATCH 22/97] [Fix]: #1795 remove unnecessary code --- .../src/comps/comps/tableComp/tableComp.tsx | 65 +++---------------- .../src/comps/comps/tableComp/tableUtils.tsx | 3 +- 2 files changed, 10 insertions(+), 58 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index a8d151d1e..52b1acf4a 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx @@ -99,65 +99,16 @@ export class TableImplComp extends TableInitComp implements IContainer { } downloadData(fileName: string) { - const allDisplayData = (this as any).exposingValues["displayData"]; + // displayData already contains only visible columns (filtered in transformDispalyData) + const displayData = (this as any).exposingValues["displayData"]; const delimiter = this.children.toolbar.children.columnSeparator.getView(); - try { - // Build the set of visible column keys as shown to the user (title or dataIndex) - const enableColumnSetting = this.children.toolbar.children.columnSetting.getView(); - const visibleColumnKeys = new Set(); - this.children.columns.getView().forEach((col) => { - const colView = col.getView(); - const isHidden = columnHide({ - hide: colView.hide, - tempHide: colView.tempHide, - enableColumnSetting, - }); - if (!isHidden) { - const headerKey = (colView.title as any) || colView.dataIndex; - if (headerKey) { - visibleColumnKeys.add(String(headerKey)); - } - } - }); - - const pickVisible = (row: any): any => { - const result: any = {}; - // copy only allowed keys - Object.keys(row || {}).forEach((key) => { - if (key !== COLUMN_CHILDREN_KEY && visibleColumnKeys.has(key)) { - result[key] = row[key]; - } - }); - // retain children recursively if present - if (Array.isArray(row?.[COLUMN_CHILDREN_KEY])) { - const children = row[COLUMN_CHILDREN_KEY].map((r: any) => pickVisible(r)); - if (children.length) { - result[COLUMN_CHILDREN_KEY] = children; - } - } - return result; - }; - - const dataToSave = Array.isArray(allDisplayData) - ? allDisplayData.map((r: any) => pickVisible(r)) - : allDisplayData; - - saveDataAsFile({ - data: dataToSave, - filename: fileName, - fileType: "csv", - delimiter, - }); - } catch (_e) { - // Fallback to previous behavior if anything goes wrong - saveDataAsFile({ - data: allDisplayData, - filename: fileName, - fileType: "csv", - delimiter, - }); - } + saveDataAsFile({ + data: displayData, + filename: fileName, + fileType: "csv", + delimiter, + }); } refreshData(allQueryNames: Array, setLoading: (loading: boolean) => void) { diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index fdc5c775d..edb26ca61 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx @@ -209,7 +209,8 @@ export function transformDispalyData( return oriDisplayData.map((row) => { const transData = _(row) .omit(OB_ROW_ORI_INDEX) - .mapKeys((value, key) => dataIndexTitleDict[key] || key) + .pickBy((value, key) => key in dataIndexTitleDict) // Only include columns in the dictionary + .mapKeys((value, key) => dataIndexTitleDict[key]) .value(); if (Array.isArray(row[COLUMN_CHILDREN_KEY])) { return { From 86674b68c5bb33f0652594f810ed3b4d298117f2 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Sat, 18 Oct 2025 20:00:23 +0200 Subject: [PATCH 23/97] fix: #1732 - fixed typos in secrets.yaml files --- deploy/helm/templates/api-service/secrets.yaml | 4 ++-- deploy/helm/templates/node-service/secrets.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/helm/templates/api-service/secrets.yaml b/deploy/helm/templates/api-service/secrets.yaml index c1e45ced8..c670b36c2 100644 --- a/deploy/helm/templates/api-service/secrets.yaml +++ b/deploy/helm/templates/api-service/secrets.yaml @@ -31,6 +31,6 @@ stringData: LOWCODER_API_KEY_SECRET: "{{ .Values.global.config.apiKeySecret }}" LOWCODER_SUPERUSER_USERNAME: {{ .Values.global.config.superuser.username | default "admin@localhost" | quote }} LOWCODER_SUPERUSER_PASSWORD: {{ .Values.global.config.superuser.password | default "" | quote }} - LOWCODER_NODE_SERVICE_SECRET: {{ .values.global.config.nodeServiceSecret | default "62e348319ab9f5c43c3b5a380b4d82525cdb68740f21140e767989b509ab0aa2" | quote }} - LOWCODER_NODE_SERVICE_SECRET_SALT: {{ .values.global.config.nodeServiceSalt | default "lowcoder.org" | quote }} + LOWCODER_NODE_SERVICE_SECRET: {{ .Values.global.config.nodeServiceSecret | default "62e348319ab9f5c43c3b5a380b4d82525cdb68740f21140e767989b509ab0aa2" | quote }} + LOWCODER_NODE_SERVICE_SECRET_SALT: {{ .Values.global.config.nodeServiceSalt | default "lowcoder.org" | quote }} diff --git a/deploy/helm/templates/node-service/secrets.yaml b/deploy/helm/templates/node-service/secrets.yaml index 2af6cfa30..88888d60f 100644 --- a/deploy/helm/templates/node-service/secrets.yaml +++ b/deploy/helm/templates/node-service/secrets.yaml @@ -10,6 +10,6 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} stringData: - LOWCODER_NODE_SERVICE_SECRET: {{ .values.global.config.nodeServiceSecret | default "62e348319ab9f5c43c3b5a380b4d82525cdb68740f21140e767989b509ab0aa2" | quote }} - LOWCODER_NODE_SERVICE_SECRET_SALT: {{ .values.global.config.nodeServiceSalt | default "lowcoder.org" | quote }} + LOWCODER_NODE_SERVICE_SECRET: {{ .Values.global.config.nodeServiceSecret | default "62e348319ab9f5c43c3b5a380b4d82525cdb68740f21140e767989b509ab0aa2" | quote }} + LOWCODER_NODE_SERVICE_SECRET_SALT: {{ .Values.global.config.nodeServiceSalt | default "lowcoder.org" | quote }} From ad164cbce417e4e268317e0bd54f820224419f4f Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Sun, 19 Oct 2025 13:06:08 +0200 Subject: [PATCH 24/97] fix #1968: add null checks when retrieving marketplace info --- .../api/home/UserHomeApiServiceImpl.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java index d8520a9d5..e7e840e01 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java @@ -344,7 +344,7 @@ public Flux getAllMarketplaceApplications(@Nulla return applicationFlux - .flatMap(application -> Mono.zip(Mono.just(application), userMapMono, orgMapMono)) + .flatMap(application -> Mono.zip(Mono.justOrEmpty(application), userMapMono, orgMapMono)) .flatMap(tuple2 -> { // build view Application application = tuple2.getT1(); @@ -356,24 +356,33 @@ public Flux getAllMarketplaceApplications(@Nulla .applicationType(application.getApplicationType()) .applicationStatus(application.getApplicationStatus()) .orgId(application.getOrganizationId()) - .orgName(orgMap.get(application.getOrganizationId()).getName()) + .orgName(Optional.ofNullable(orgMap.get(application.getOrganizationId())) + .map(Organization::getName) + .orElse("")) .creatorEmail(Optional.ofNullable(userMap.get(application.getCreatedBy())) .map(User::getName) .orElse("")) - .createAt(application.getCreatedAt().toEpochMilli()) + .createAt(Optional.ofNullable(application.getCreatedAt()) + .map(Instant::toEpochMilli) + .orElse(0L)) .createBy(application.getCreatedBy()) .build(); // marketplace specific fields return application.getPublishedApplicationDSL(applicationRecordService) - .map(pubishedApplicationDSL -> - (Map) new HashMap((Map) pubishedApplicationDSL.getOrDefault("settings", new HashMap<>()))) - .switchIfEmpty(Mono.just(new HashMap<>())) + .map(dsl -> { + Object settingsObj = dsl.getOrDefault("settings", new HashMap<>()); + if (!(settingsObj instanceof Map)) { + return new HashMap(); // fallback if not a map + } + return (Map) settingsObj; + }) + .defaultIfEmpty(new HashMap<>()) .map(settings -> { - marketplaceApplicationInfoView.setTitle((String)settings.getOrDefault("title", application.getName())); - marketplaceApplicationInfoView.setCategory((String)settings.get("category")); - marketplaceApplicationInfoView.setDescription((String)settings.get("description")); - marketplaceApplicationInfoView.setImage((String)settings.get("icon")); + marketplaceApplicationInfoView.setTitle((String) settings.getOrDefault("title", application.getName())); + marketplaceApplicationInfoView.setCategory((String) settings.get("category")); + marketplaceApplicationInfoView.setDescription((String) settings.get("description")); + marketplaceApplicationInfoView.setImage((String) settings.get("icon")); return marketplaceApplicationInfoView; }); }); From 412b6488ee012a9198fa2963f8c9f21ff3079be1 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 20 Oct 2025 20:40:35 +0500 Subject: [PATCH 25/97] fix app header dropdown warning --- .../lowcoder/src/pages/common/headerStartDropdown.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx b/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx index 498a857f0..201cf9225 100644 --- a/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx @@ -167,7 +167,9 @@ export function HeaderStartDropdown(props: { setEdit: () => void, isViewMarketpl }); } }} - items={menuItems.filter(item => item.visible)} + items={menuItems + .filter((item) => item.visible) + .map(({ visible, ...rest }) => rest)} /> )} > From 9a900cc80885f48c63b46dc406ab683332761ed5 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Mon, 20 Oct 2025 19:49:57 +0200 Subject: [PATCH 26/97] fix: normalize max request size for nginx --- deploy/docker/default.env | 4 ++-- deploy/docker/frontend/01-update-nginx-conf.sh | 8 ++++++-- deploy/helm/values.yaml | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/deploy/docker/default.env b/deploy/docker/default.env index 8b4445a3d..d37f0ce81 100644 --- a/deploy/docker/default.env +++ b/deploy/docker/default.env @@ -116,8 +116,8 @@ LOWCODER_NODE_SERVICE_SECRET_SALT="lowcoder.org" ## ## Frontend parameters ## -# Lowcoder max request size -LOWCODER_MAX_REQUEST_SIZE=20m +# Lowcoder max request size in kb/mb/gb +LOWCODER_MAX_REQUEST_SIZE=20mb # Lowcoder max query timeout (in seconds) LOWCODER_MAX_QUERY_TIMEOUT=120 # Default lowcoder query timeout diff --git a/deploy/docker/frontend/01-update-nginx-conf.sh b/deploy/docker/frontend/01-update-nginx-conf.sh index 3499286b2..88e8928e9 100644 --- a/deploy/docker/frontend/01-update-nginx-conf.sh +++ b/deploy/docker/frontend/01-update-nginx-conf.sh @@ -18,12 +18,16 @@ else ln -s /etc/nginx/nginx-http.conf /etc/nginx/nginx.conf fi; -sed -i "s@__LOWCODER_MAX_REQUEST_SIZE__@${LOWCODER_MAX_REQUEST_SIZE:=20m}@" /etc/nginx/nginx.conf +# Normalize max. request size for usage with nginx +MAX_REQUEST_SIZE=$(echo "${LOWCODER_MAX_REQUEST_SIZE:=20m}" | perl -pe 's/^([ \t]*)(?\d+(\.\d+)?)([ \t]*)(?[kKmMgGtT]{1})?([bB \t]*)$/"$+{number}" . lc($+{unit})/e') + + +sed -i "s@__LOWCODER_MAX_REQUEST_SIZE__@${MAX_REQUEST_SIZE}@" /etc/nginx/nginx.conf sed -i "s@__LOWCODER_MAX_QUERY_TIMEOUT__@${LOWCODER_MAX_QUERY_TIMEOUT:=120}@" /etc/nginx/server.conf sed -i "s@__LOWCODER_API_SERVICE_URL__@${LOWCODER_API_SERVICE_URL:=http://localhost:8080}@" /etc/nginx/server.conf sed -i "s@__LOWCODER_NODE_SERVICE_URL__@${LOWCODER_NODE_SERVICE_URL:=http://localhost:6060}@" /etc/nginx/server.conf echo "nginx config updated with:" -echo " Lowcoder max upload size: ${LOWCODER_MAX_REQUEST_SIZE:=20m}" +echo " Lowcoder max upload size: ${MAX_REQUEST_SIZE:=20m}" echo " Lowcoder api service URL: ${LOWCODER_API_SERVICE_URL:=http://localhost:8080}" echo " Lowcoder node service URL: ${LOWCODER_NODE_SERVICE_URL:=http://localhost:6060}" diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 3723fec4b..ddfe76491 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -34,7 +34,7 @@ global: nodeServiceSecret: "62e348319ab9f5c43c3b5a380b4d82525cdb68740f21140e767989b509ab0aa2" nodeServiceSalt: "lowcoder.org" maxQueryTimeout: 120 - maxRequestSize: "20m" + maxRequestSize: "20mb" snapshotRetentionTime: 30 marketplacePrivateMode: true cookie: From 2acc744a3a2a16d722dfac37fc66b005ea839694 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 22 Oct 2025 22:38:48 +0500 Subject: [PATCH 27/97] fix piechart default x-axis --- .../lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx b/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx index aaa5f0198..607a9a368 100644 --- a/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx @@ -302,7 +302,7 @@ let PieChartComp = withExposingConfigs(PieChartTmpComp, [ export const PieChartCompWithDefault = withDefault(PieChartComp, { - xAxisKey: "date", + xAxisKey: "name", series: [ { dataIndex: genRandomKey(), From 02f61ebbc705fffd95db95e624309fd1649d34de Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 23 Oct 2025 22:38:44 +0500 Subject: [PATCH 28/97] fix chart theme --- .../lowcoder-comps/src/comps/barChartComp/barChartComp.tsx | 3 ++- .../lowcoder-comps/src/comps/lineChartComp/lineChartComp.tsx | 1 + .../lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx | 1 + .../src/comps/scatterChartComp/scatterChartComp.tsx | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx index 7b64f0f6c..0998492ae 100644 --- a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx @@ -75,6 +75,7 @@ BarChartTmpComp = withViewFn(BarChartTmpComp, (comp) => { log.error('theme chart error: ', error); } + // Detect race mode changes and force chart recreation const currentRaceMode = comp.children.chartConfig?.children?.comp?.children?.race?.getView(); useEffect(() => { @@ -172,7 +173,6 @@ BarChartTmpComp = withViewFn(BarChartTmpComp, (comp) => { useResizeDetector({ targetRef: containerRef, onResize: ({width, height}) => { - console.log('barChart - resize'); if (width && height) { setChartSize({ w: width, h: height }); } @@ -194,6 +194,7 @@ BarChartTmpComp = withViewFn(BarChartTmpComp, (comp) => { notMerge={!currentRaceMode} lazyUpdate={!currentRaceMode} opts={{ locale: getEchartsLocale() }} + theme={themeConfig} option={option} mode={mode} /> diff --git a/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartComp.tsx b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartComp.tsx index 032607625..ed2f1654e 100644 --- a/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartComp.tsx @@ -174,6 +174,7 @@ LineChartTmpComp = withViewFn(LineChartTmpComp, (comp) => { notMerge lazyUpdate opts={{ locale: getEchartsLocale() }} + theme={themeConfig} option={option} mode={mode} /> diff --git a/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx b/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx index aaa5f0198..3b1dc0f34 100644 --- a/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartComp.tsx @@ -194,6 +194,7 @@ PieChartTmpComp = withViewFn(PieChartTmpComp, (comp) => { notMerge lazyUpdate opts={{ locale: getEchartsLocale() }} + theme={themeConfig} option={option} mode={mode} /> diff --git a/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartComp.tsx b/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartComp.tsx index c7fd7da9c..527a4ca2d 100644 --- a/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartComp.tsx @@ -175,6 +175,7 @@ ScatterChartTmpComp = withViewFn(ScatterChartTmpComp, (comp) => { notMerge lazyUpdate opts={{ locale: getEchartsLocale() }} + theme={themeConfig} option={option} mode={mode} /> From e0ddfb8a01e3723c0c806bafdc3eaf4d54a0f1a2 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 24 Oct 2025 21:37:57 +0500 Subject: [PATCH 29/97] [Feat]: add click/double click events for lotte --- .../src/comps/comps/jsonComp/jsonLottieComp.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index 466c37e9f..92f355936 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx @@ -10,7 +10,7 @@ import { styleControl } from "comps/controls/styleControl"; import { AnimationStyle, LottieStyle } from "comps/controls/styleControlConstants"; import { trans } from "i18n"; import { Section, sectionNames } from "lowcoder-design"; -import { useContext, lazy, useEffect, useState } from "react"; +import { useContext, lazy, useEffect, useState, useCallback } from "react"; import { stateComp, UICompBuilder, withDefault } from "../../generators"; import { NameConfig, @@ -23,9 +23,10 @@ import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconsco import { DotLottie } from "@lottiefiles/dotlottie-react"; import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl"; import { useResizeDetector } from "react-resize-detector"; -import { eventHandlerControl } from "@lowcoder-ee/comps/controls/eventHandlerControl"; +import { eventHandlerControl, clickEvent, doubleClickEvent } from "@lowcoder-ee/comps/controls/eventHandlerControl"; import { withMethodExposing } from "@lowcoder-ee/comps/generators/withMethodExposing"; import { changeChildAction } from "lowcoder-core"; +import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler"; // const Player = lazy( // () => import('@lottiefiles/react-lottie-player') @@ -128,6 +129,8 @@ const ModeOptions = [ ] as const; const EventOptions = [ + clickEvent, + doubleClickEvent, { label: trans("jsonLottie.load"), value: "load", description: trans("jsonLottie.load") }, { label: trans("jsonLottie.play"), value: "play", description: trans("jsonLottie.play") }, { label: trans("jsonLottie.pause"), value: "pause", description: trans("jsonLottie.pause") }, @@ -160,6 +163,10 @@ let JsonLottieTmpComp = (function () { }; return new UICompBuilder(childrenMap, (props, dispatch) => { const [dotLottie, setDotLottie] = useState(null); + const handleClickEvent = useCompClickEventHandler({ onEvent: props.onEvent }); + const handleClick = useCallback(() => { + handleClickEvent(); + }, [handleClickEvent]); const setLayoutAndResize = () => { const align = props.align.split(','); @@ -244,6 +251,7 @@ let JsonLottieTmpComp = (function () { padding: `${props.container.padding}`, rotate: props.container.rotation, }} + onClick={handleClick} > Date: Tue, 28 Oct 2025 23:46:01 +0500 Subject: [PATCH 30/97] [Fix]: table header on different table sizes --- .../comps/comps/tableComp/tableCompView.tsx | 1 + .../src/comps/comps/tableComp/tableStyles.ts | 19 +++++++++++++++++++ .../comps/controls/styleControlConstants.tsx | 1 - 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index 74d431f7a..9b98bcc35 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -376,6 +376,7 @@ export const TableCompView = React.memo((props: { $isVirtual={scrollConfig.virtual} $showHorizontalScrollbar={showHorizontalScrollbar} $showVerticalScrollbar={showVerticalScrollbar} + $tableSize={size as 'small' | 'middle' | 'large'} > expandable={{ diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts index 54c856c14..d706acb16 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts @@ -141,6 +141,7 @@ export const TableWrapper = styled.div.attrs<{ $isVirtual?: boolean; $showHorizontalScrollbar?: boolean; $showVerticalScrollbar?: boolean; + $tableSize?: "small" | "middle" | "large"; }>` .ant-table-wrapper { border-top: unset; @@ -207,6 +208,7 @@ export const TableWrapper = styled.div.attrs<{ border-color: ${(props) => props.$headerStyle.border}; border-width: ${(props) => props.$headerStyle.borderWidth}; color: ${(props) => props.$headerStyle.headerText}; + padding: 0 !important; /* Override Ant Design's default padding */ // border-inline-end: ${(props) => `${props.$headerStyle.borderWidth} solid ${props.$headerStyle.border}`} !important; /* Proper styling for fixed header cells */ @@ -223,6 +225,9 @@ export const TableWrapper = styled.div.attrs<{ > div { margin: ${(props) => props.$headerStyle.margin}; + /* Default padding for middle size (Ant Design default) */ + padding: 8px 8px; + min-height: 24px; &, .ant-table-column-title > div { font-size: ${(props) => props.$headerStyle.textSize}; @@ -231,6 +236,20 @@ export const TableWrapper = styled.div.attrs<{ font-style: ${(props) => props.$headerStyle.fontStyle}; color:${(props) => props.$headerStyle.headerText} } + + /* Adjust header size based on table size */ + ${(props) => props.$tableSize === 'small' && ` + padding: 1px 8px; + min-height: 14px; + `} + ${(props) => props.$tableSize === 'middle' && ` + padding: 8px 8px; + min-height: 24px; + `} + ${(props) => props.$tableSize === 'large' && ` + padding: 16px 16px; + min-height: 48px; + `} } &:last-child { diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 569ada9c4..2fcbb4f90 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -1771,7 +1771,6 @@ export const TableToolbarStyle = [ export const TableHeaderStyle = [ MARGIN, - PADDING, FONT_FAMILY, FONT_STYLE, TEXT, From 013cd908fecaf39e8ad00b7699fe0f1a6909b3e0 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Wed, 29 Oct 2025 17:51:20 +0100 Subject: [PATCH 31/97] fix #2070: error while listing mobile nav apps --- .../java/org/lowcoder/domain/application/model/Application.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java index 3e2a7c2ae..da9b3c083 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java @@ -242,7 +242,7 @@ public Mono getLiveContainerSize(ApplicationRecordService applicationRec if (ApplicationType.APPLICATION.getValue() == getApplicationType()) { return Mono.empty(); } - return Mono.just(getContainerSizeFromDSL(dsl)); + return Mono.justOrEmpty(getContainerSizeFromDSL(dsl)); }); } From a1828453d09a57465e4b2e9c0653680afde9e75e Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 30 Oct 2025 22:29:20 +0500 Subject: [PATCH 32/97] [Fix]: #1840 table theme sort color column --- .../lowcoder/src/comps/comps/tableComp/tableStyles.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts index 54c856c14..b89ca0b4a 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts @@ -269,6 +269,15 @@ export const TableWrapper = styled.div.attrs<{ transition: background-color 0.3s; } + /* Ensure sorted column cells respect theme/row background instead of AntD default */ + &.ant-table-column-sort { + background: transparent; + } + &.ant-table-cell-fix-left.ant-table-column-sort, + &.ant-table-cell-fix-right.ant-table-column-sort { + background: transparent; + } + } /* Fix for selected and hovered rows */ From 507efa65ebbb72abb5e57ab39bf7d50020dc3589 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 31 Oct 2025 22:30:39 +0500 Subject: [PATCH 33/97] [Feat]: #1827 #1937 markdown + tooltip on events --- .../src/comps/controls/eventHandlerControl.tsx | 7 ++++++- .../queries/queryComp/queryConfirmationModal.tsx | 14 +++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx b/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx index dac3dd023..631c322c8 100644 --- a/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx @@ -14,6 +14,7 @@ import { EventContent, EventDiv, EventTitle, + Tooltip, InlineEventFormWrapper, LinkButton, OptionType, @@ -123,7 +124,11 @@ class SingleEventHandlerControl< defaultVisible={props.popup} > - {!_.isEmpty(eventName) && {eventName}} + {!_.isEmpty(eventName) && ( + + {eventName} + + )} {eventAction} diff --git a/client/packages/lowcoder/src/comps/queries/queryComp/queryConfirmationModal.tsx b/client/packages/lowcoder/src/comps/queries/queryComp/queryConfirmationModal.tsx index 66f5de712..0ad6c73bc 100644 --- a/client/packages/lowcoder/src/comps/queries/queryComp/queryConfirmationModal.tsx +++ b/client/packages/lowcoder/src/comps/queries/queryComp/queryConfirmationModal.tsx @@ -1,7 +1,7 @@ import { MultiCompBuilder } from "../../generators"; import { BoolPureControl } from "../../controls/boolControl"; import { StringControl } from "../../controls/codeControl"; -import { CustomModal } from "lowcoder-design"; +import { CustomModal, TacoMarkDown } from "lowcoder-design"; import { isEmpty } from "lodash"; import { QueryResult } from "../queryComp"; import { trans } from "i18n"; @@ -16,15 +16,19 @@ export const QueryConfirmationModal = new MultiCompBuilder( new Promise((resolve) => { props.showConfirmationModal && isManual ? CustomModal.confirm({ - content: isEmpty(props.confirmationMessage) - ? trans("query.confirmationMessage") - : props.confirmationMessage, + content: ( + + {isEmpty(props.confirmationMessage) + ? trans("query.confirmationMessage") + : props.confirmationMessage} + + ), onConfirm: () => { resolve(onConfirm()); }, confirmBtnType: "primary", style: { top: "-100px" }, - bodyStyle: { marginTop: 0, height: "42px" }, + bodyStyle: { marginTop: 0 }, }) : resolve(onConfirm()); }) From f4109c652bd621d4c5e63e588109972212aa1d2e Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 4 Nov 2025 19:36:12 +0500 Subject: [PATCH 34/97] [Fix]: #2076 mobile nav app not passing params --- .../comps/comps/layout/mobileTabLayout.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index c1a04c14e..d5c052269 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -34,6 +34,8 @@ import { LayoutActionComp } from "./layoutActionComp"; import { defaultTheme } from "@lowcoder-ee/constants/themeConstants"; import { clickEvent, eventHandlerControl } from "@lowcoder-ee/comps/controls/eventHandlerControl"; import { childrenToProps } from "@lowcoder-ee/comps/generators/multi"; +import { useAppPathParam } from "util/hooks"; +import { ALL_APPLICATIONS_URL } from "constants/routesURL"; const TabBar = React.lazy(() => import("antd-mobile/es/components/tab-bar")); const TabBarItem = React.lazy(() => @@ -389,6 +391,7 @@ let MobileTabLayoutTmp = (function () { MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const [tabIndex, setTabIndex] = useState(0); const { readOnly } = useContext(ExternalEditorContext); + const pathParam = useAppPathParam(); const navStyle = comp.children.navStyle.getView(); const navItemStyle = comp.children.navItemStyle.getView(); const navItemHoverStyle = comp.children.navItemHoverStyle.getView(); @@ -466,7 +469,23 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { : undefined, }))} selectedKey={tabIndex + ""} - onChange={(key) => setTabIndex(Number(key))} + onChange={(key) => { + const nextIndex = Number(key); + setTabIndex(nextIndex); + // push URL with query/hash params like desktop nav + if (dataOptionType === DataOption.Manual) { + const selectedTab = tabViews[nextIndex]; + if (selectedTab) { + const url = [ + ALL_APPLICATIONS_URL, + pathParam.applicationId, + pathParam.viewMode, + nextIndex, + ].join("/"); + selectedTab.children.action.act(url); + } + } + }} readOnly={!!readOnly} canvasBg={bgColor} tabStyle={{ From 16e9d0a3881986646be2958b64681668bf917cc0 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 4 Nov 2025 22:30:04 +0500 Subject: [PATCH 35/97] [Feat]: Add hamburger menu mode for Mobile Nav App + refactor --- .../comps/comps/layout/mobileTabLayout.tsx | 305 +++++++++++++++--- .../comps/comps/layout/navLayoutConstants.ts | 39 +++ 2 files changed, 298 insertions(+), 46 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index d5c052269..a283c5f39 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -18,7 +18,7 @@ import { ExternalEditorContext } from "util/context/ExternalEditorContext"; import { default as Skeleton } from "antd/es/skeleton"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl"; -import { DataOption, DataOptionType, ModeOptions, menuItemStyleOptions, mobileNavJsonMenuItems } from "./navLayoutConstants"; +import { DataOption, DataOptionType, menuItemStyleOptions, mobileNavJsonMenuItems, MobileModeOptions, MobileMode, HamburgerPositionOptions, DrawerPlacementOptions } from "./navLayoutConstants"; import { styleControl } from "@lowcoder-ee/comps/controls/styleControl"; import { NavLayoutItemActiveStyle, NavLayoutItemActiveStyleType, NavLayoutItemHoverStyle, NavLayoutItemHoverStyleType, NavLayoutItemStyle, NavLayoutItemStyleType, NavLayoutStyle, NavLayoutStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants"; import Segmented from "antd/es/segmented"; @@ -43,6 +43,7 @@ const TabBarItem = React.lazy(() => default: module.TabBarItem, })) ); +const Popup = React.lazy(() => import("antd-mobile/es/components/popup")); const EventOptions = [clickEvent] as const; const AppViewContainer = styled.div` @@ -67,6 +68,92 @@ const TabLayoutViewContainer = styled.div<{ flex-direction: column; `; +const HamburgerButton = styled.button<{ + $size: string; + $position: string; // bottom-right | bottom-left | top-right | top-left + $zIndex: number; +}>` + position: fixed; + ${(props) => (props.$position.includes('bottom') ? 'bottom: 16px;' : 'top: 16px;')} + ${(props) => (props.$position.includes('right') ? 'right: 16px;' : 'left: 16px;')} + width: ${(props) => props.$size}; + height: ${(props) => props.$size}; + border-radius: 50%; + border: 1px solid rgba(0,0,0,0.1); + background: white; + display: flex; + align-items: center; + justify-content: center; + z-index: ${(props) => props.$zIndex}; + cursor: pointer; + box-shadow: 0 6px 16px rgba(0,0,0,0.15); +`; + +const BurgerIcon = styled.div<{ + $lineColor?: string; +}>` + width: 60%; + height: 2px; + background: ${(p) => p.$lineColor || '#333'}; + position: relative; + &::before, &::after { + content: ''; + position: absolute; + left: 0; + width: 100%; + height: 2px; + background: inherit; + } + &::before { top: -6px; } + &::after { top: 6px; } +`; + +const DrawerContent = styled.div<{ + $background: string; +}>` + background: ${(p) => p.$background}; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + padding: 12px; + box-sizing: border-box; +`; + +const DrawerList = styled.div<{ + $itemStyle: NavLayoutItemStyleType; + $hoverStyle: NavLayoutItemHoverStyleType; + $activeStyle: NavLayoutItemActiveStyleType; +}>` + display: flex; + flex-direction: column; + gap: 8px; + + .drawer-item { + display: flex; + align-items: center; + gap: 8px; + background-color: ${(p) => p.$itemStyle.background}; + color: ${(p) => p.$itemStyle.text}; + border-radius: ${(p) => p.$itemStyle.radius}; + border: 1px solid ${(p) => p.$itemStyle.border}; + margin: ${(p) => p.$itemStyle.margin}; + padding: ${(p) => p.$itemStyle.padding}; + cursor: pointer; + user-select: none; + } + .drawer-item:hover { + background-color: ${(p) => p.$hoverStyle.background}; + color: ${(p) => p.$hoverStyle.text}; + border: 1px solid ${(p) => p.$hoverStyle.border}; + } + .drawer-item.active { + background-color: ${(p) => p.$activeStyle.background}; + color: ${(p) => p.$activeStyle.text}; + border: 1px solid ${(p) => p.$activeStyle.border}; + } +`; + const TabBarWrapper = styled.div<{ $readOnly: boolean, $canvasBg: string, @@ -118,7 +205,7 @@ const StyledTabBar = styled(TabBar)<{ .adm-tab-bar-item-icon, .adm-tab-bar-item-title { color: ${(props) => props.$tabStyle.text}; } - .adm-tab-bar-item-icon, { + .adm-tab-bar-item-icon { font-size: ${(props) => props.$navIconSize}; } @@ -289,6 +376,69 @@ const TabOptionComp = (function () { .build(); })(); +function renderDataSection(children: any): any { + return ( +
+ {children.dataOptionType.propertyView({ + radioButton: true, + type: "oneline", + })} + {children.dataOptionType.getView() === DataOption.Manual + ? children.tabs.propertyView({}) + : children.jsonItems.propertyView({ + label: "Json Data", + })} +
+ ); +} + +function renderEventHandlersSection(children: any): any { + return ( +
+ {children.onEvent.getPropertyView()} +
+ ); +} + +function renderHamburgerLayoutSection(children: any): any { + const drawerPlacement = children.drawerPlacement.getView(); + return ( + <> + {children.hamburgerPosition.propertyView({ label: "Hamburger Position" })} + {children.hamburgerSize.propertyView({ label: "Hamburger Size" })} + {children.drawerPlacement.propertyView({ label: "Drawer Placement" })} + {(drawerPlacement === 'top' || drawerPlacement === 'bottom') && + children.drawerHeight.propertyView({ label: "Drawer Height" })} + {(drawerPlacement === 'left' || drawerPlacement === 'right') && + children.drawerWidth.propertyView({ label: "Drawer Width" })} + {children.shadowOverlay.propertyView({ label: "Shadow Overlay" })} + {children.backgroundImage.propertyView({ + label: `Background Image`, + placeholder: 'https://temp.im/350x400', + })} + + ); +} + +function renderVerticalLayoutSection(children: any): any { + return ( + <> + {children.backgroundImage.propertyView({ + label: `Background Image`, + placeholder: 'https://temp.im/350x400', + })} + {children.showSeparator.propertyView({label: trans("navLayout.mobileNavVerticalShowSeparator")})} + {children.tabBarHeight.propertyView({label: trans("navLayout.mobileNavBarHeight")})} + {children.navIconSize.propertyView({label: trans("navLayout.mobileNavIconSize")})} + {children.maxWidth.propertyView({label: trans("navLayout.mobileNavVerticalMaxWidth")})} + {children.verticalAlignment.propertyView({ + label: trans("navLayout.mobileNavVerticalOrientation"), + radioButton: true + })} + + ); +} + let MobileTabLayoutTmp = (function () { const childrenMap = { onEvent: eventHandlerControl(EventOptions), @@ -313,6 +463,14 @@ let MobileTabLayoutTmp = (function () { jsonTabs: manualOptionsControl(TabOptionComp, { initOptions: [], }), + // Mode & hamburger/drawer config + menuMode: dropdownControl(MobileModeOptions, MobileMode.Vertical), + hamburgerPosition: dropdownControl(HamburgerPositionOptions, "bottom-right"), + hamburgerSize: withDefault(StringControl, "56px"), + drawerPlacement: dropdownControl(DrawerPlacementOptions, "bottom"), + drawerHeight: withDefault(StringControl, "60%"), + drawerWidth: withDefault(StringControl, "250px"), + shadowOverlay: withDefault(BoolCodeControl, true), backgroundImage: withDefault(StringControl, ""), tabBarHeight: withDefault(StringControl, "56px"), navIconSize: withDefault(StringControl, "32px"), @@ -328,40 +486,21 @@ let MobileTabLayoutTmp = (function () { return null; }) .setPropertyViewFn((children) => { - const [styleSegment, setStyleSegment] = useState('normal') + const [styleSegment, setStyleSegment] = useState('normal'); + const isHamburgerMode = children.menuMode.getView() === MobileMode.Hamburger; + return ( -
-
- {children.dataOptionType.propertyView({ - radioButton: true, - type: "oneline", - })} - { - children.dataOptionType.getView() === DataOption.Manual - ? children.tabs.propertyView({}) - : children.jsonItems.propertyView({ - label: "Json Data", - }) - } -
-
- { children.onEvent.getPropertyView() } -
+ <> + {renderDataSection(children)} + {renderEventHandlersSection(children)}
- {children.backgroundImage.propertyView({ - label: `Background Image`, - placeholder: 'https://temp.im/350x400', - })} - { children.showSeparator.propertyView({label: trans("navLayout.mobileNavVerticalShowSeparator")})} - {children.tabBarHeight.propertyView({label: trans("navLayout.mobileNavBarHeight")})} - {children.navIconSize.propertyView({label: trans("navLayout.mobileNavIconSize")})} - {children.maxWidth.propertyView({label: trans("navLayout.mobileNavVerticalMaxWidth")})} - {children.verticalAlignment.propertyView( - { label: trans("navLayout.mobileNavVerticalOrientation"),radioButton: true } - )} + {children.menuMode.propertyView({ label: "Mode", radioButton: true })} + {isHamburgerMode + ? renderHamburgerLayoutSection(children) + : renderVerticalLayoutSection(children)}
- { children.navStyle.getPropertyView() } + {children.navStyle.getPropertyView()}
{controlItem({}, ( @@ -372,17 +511,11 @@ let MobileTabLayoutTmp = (function () { onChange={(k) => setStyleSegment(k as MenuItemStyleOptionValue)} /> ))} - {styleSegment === 'normal' && ( - children.navItemStyle.getPropertyView() - )} - {styleSegment === 'hover' && ( - children.navItemHoverStyle.getPropertyView() - )} - {styleSegment === 'active' && ( - children.navItemActiveStyle.getPropertyView() - )} + {styleSegment === 'normal' && children.navItemStyle.getPropertyView()} + {styleSegment === 'hover' && children.navItemHoverStyle.getPropertyView()} + {styleSegment === 'active' && children.navItemActiveStyle.getPropertyView()}
-
+ ); }) .build(); @@ -390,6 +523,7 @@ let MobileTabLayoutTmp = (function () { MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const [tabIndex, setTabIndex] = useState(0); + const [drawerVisible, setDrawerVisible] = useState(false); const { readOnly } = useContext(ExternalEditorContext); const pathParam = useAppPathParam(); const navStyle = comp.children.navStyle.getView(); @@ -399,6 +533,13 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const backgroundImage = comp.children.backgroundImage.getView(); const jsonItems = comp.children.jsonItems.getView(); const dataOptionType = comp.children.dataOptionType.getView(); + const menuMode = comp.children.menuMode.getView(); + const hamburgerPosition = comp.children.hamburgerPosition.getView(); + const hamburgerSize = comp.children.hamburgerSize.getView(); + const drawerPlacement = comp.children.drawerPlacement.getView(); + const drawerHeight = comp.children.drawerHeight.getView(); + const drawerWidth = comp.children.drawerWidth.getView(); + const shadowOverlay = comp.children.shadowOverlay.getView(); const tabBarHeight = comp.children.tabBarHeight.getView(); const navIconSize = comp.children.navIconSize.getView(); const maxWidth = comp.children.maxWidth.getView(); @@ -472,7 +613,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { onChange={(key) => { const nextIndex = Number(key); setTabIndex(nextIndex); - // push URL with query/hash params like desktop nav + // push URL with query/hash params if (dataOptionType === DataOption.Manual) { const selectedTab = tabViews[nextIndex]; if (selectedTab) { @@ -507,11 +648,76 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { /> ); + const containerTabBarHeight = menuMode === MobileMode.Hamburger ? '0px' : tabBarHeight; + + const hamburgerButton = ( + setDrawerVisible(true)} + > + + + ); + + const drawerBodyStyle = useMemo(() => { + if (drawerPlacement === 'left' || drawerPlacement === 'right') { + return { width: drawerWidth } as React.CSSProperties; + } + return { height: drawerHeight } as React.CSSProperties; + }, [drawerPlacement, drawerHeight, drawerWidth]); + + const drawerView = ( + }> + setDrawerVisible(false)} + onClose={() => setDrawerVisible(false)} + position={drawerPlacement as any} + mask={shadowOverlay} + bodyStyle={drawerBodyStyle} + > + + + {tabViews.map((tab, index) => ( +
{ + setTabIndex(index); + setDrawerVisible(false); + onEvent('click'); + }} + > + {tab.children.icon.toJsonValue() ? ( + {tab.children.icon.getView()} + ) : null} + {tab.children.label.getView()} +
+ ))} +
+
+
+
+ ); + if (readOnly) { return ( - + {appView} - {tabBarView} + {menuMode === MobileMode.Hamburger ? ( + <> + {hamburgerButton} + {drawerView} + + ) : ( + tabBarView + )} ); } @@ -519,7 +725,14 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { return ( {appView} - {tabBarView} + {menuMode === MobileMode.Hamburger ? ( + <> + {hamburgerButton} + {drawerView} + + ) : ( + tabBarView + )} ); }); diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts index 66043303a..aa33423d0 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts @@ -6,6 +6,45 @@ export const ModeOptions = [ { label: trans("navLayout.modeHorizontal"), value: "horizontal" }, ] as const; +// Mobile navigation specific modes and options +export const MobileMode = { + Vertical: "vertical", + Hamburger: "hamburger", +} as const; + +export const MobileModeOptions = [ + { label: "Normal", value: MobileMode.Vertical }, + { label: "Hamburger", value: MobileMode.Hamburger }, +]; + +export const HamburgerPosition = { + BottomRight: "bottom-right", + BottomLeft: "bottom-left", + TopRight: "top-right", + TopLeft: "top-left", +} as const; + +export const HamburgerPositionOptions = [ + { label: "Bottom Right", value: HamburgerPosition.BottomRight }, + { label: "Bottom Left", value: HamburgerPosition.BottomLeft }, + { label: "Top Right", value: HamburgerPosition.TopRight }, + { label: "Top Left", value: HamburgerPosition.TopLeft }, +] as const; + +export const DrawerPlacement = { + Bottom: "bottom", + Top: "top", + Left: "left", + Right: "right", +} as const; + +export const DrawerPlacementOptions = [ + { label: "Bottom", value: DrawerPlacement.Bottom }, + { label: "Top", value: DrawerPlacement.Top }, + { label: "Left", value: DrawerPlacement.Left }, + { label: "Right", value: DrawerPlacement.Right }, +]; + export const DataOption = { Manual: 'manual', Json: 'json', From 9408350f87339f2a67900096ce50f421f4f9d744 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 4 Nov 2025 22:35:23 +0500 Subject: [PATCH 36/97] add customizeable icon burger icon --- .../lowcoder/src/comps/comps/layout/mobileTabLayout.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index a283c5f39..a019c97ef 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -404,6 +404,7 @@ function renderHamburgerLayoutSection(children: any): any { const drawerPlacement = children.drawerPlacement.getView(); return ( <> + {children.hamburgerIcon.propertyView({ label: "Icon" })} {children.hamburgerPosition.propertyView({ label: "Hamburger Position" })} {children.hamburgerSize.propertyView({ label: "Hamburger Size" })} {children.drawerPlacement.propertyView({ label: "Drawer Placement" })} @@ -465,6 +466,7 @@ let MobileTabLayoutTmp = (function () { }), // Mode & hamburger/drawer config menuMode: dropdownControl(MobileModeOptions, MobileMode.Vertical), + hamburgerIcon: IconControl, hamburgerPosition: dropdownControl(HamburgerPositionOptions, "bottom-right"), hamburgerSize: withDefault(StringControl, "56px"), drawerPlacement: dropdownControl(DrawerPlacementOptions, "bottom"), @@ -536,6 +538,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const menuMode = comp.children.menuMode.getView(); const hamburgerPosition = comp.children.hamburgerPosition.getView(); const hamburgerSize = comp.children.hamburgerSize.getView(); + const hamburgerIconComp = comp.children.hamburgerIcon; const drawerPlacement = comp.children.drawerPlacement.getView(); const drawerHeight = comp.children.drawerHeight.getView(); const drawerWidth = comp.children.drawerWidth.getView(); @@ -657,7 +660,9 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { $zIndex={Layers.tabBar + 1} onClick={() => setDrawerVisible(true)} > - + {hamburgerIconComp.toJsonValue() + ? hamburgerIconComp.getView() + : } ); From b15b10ac5fed17d489c9dc4e3beefc8d399e7d02 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 5 Nov 2025 15:03:09 +0500 Subject: [PATCH 37/97] add close icon in the drawer --- .../comps/comps/layout/mobileTabLayout.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index a019c97ef..1d498be4f 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -120,6 +120,27 @@ const DrawerContent = styled.div<{ box-sizing: border-box; `; +const DrawerHeader = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; +`; + +const DrawerCloseButton = styled.button<{ + $color: string; +}>` + background: transparent; + border: none; + cursor: pointer; + color: ${(p) => p.$color}; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 16px; +`; + const DrawerList = styled.div<{ $itemStyle: NavLayoutItemStyleType; $hoverStyle: NavLayoutItemHoverStyleType; @@ -404,7 +425,8 @@ function renderHamburgerLayoutSection(children: any): any { const drawerPlacement = children.drawerPlacement.getView(); return ( <> - {children.hamburgerIcon.propertyView({ label: "Icon" })} + {children.hamburgerIcon.propertyView({ label: "MenuIcon" })} + {children.drawerCloseIcon.propertyView({ label: "Close Icon" })} {children.hamburgerPosition.propertyView({ label: "Hamburger Position" })} {children.hamburgerSize.propertyView({ label: "Hamburger Size" })} {children.drawerPlacement.propertyView({ label: "Drawer Placement" })} @@ -467,6 +489,7 @@ let MobileTabLayoutTmp = (function () { // Mode & hamburger/drawer config menuMode: dropdownControl(MobileModeOptions, MobileMode.Vertical), hamburgerIcon: IconControl, + drawerCloseIcon: IconControl, hamburgerPosition: dropdownControl(HamburgerPositionOptions, "bottom-right"), hamburgerSize: withDefault(StringControl, "56px"), drawerPlacement: dropdownControl(DrawerPlacementOptions, "bottom"), @@ -539,6 +562,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const hamburgerPosition = comp.children.hamburgerPosition.getView(); const hamburgerSize = comp.children.hamburgerSize.getView(); const hamburgerIconComp = comp.children.hamburgerIcon; + const drawerCloseIconComp = comp.children.drawerCloseIcon; const drawerPlacement = comp.children.drawerPlacement.getView(); const drawerHeight = comp.children.drawerHeight.getView(); const drawerWidth = comp.children.drawerWidth.getView(); @@ -684,6 +708,17 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { bodyStyle={drawerBodyStyle} > + + setDrawerVisible(false)} + > + {drawerCloseIconComp.toJsonValue() + ? drawerCloseIconComp.getView() + : ×} + + Date: Wed, 5 Nov 2025 19:37:27 +0500 Subject: [PATCH 38/97] add style customization for hamburger menu mode --- .../comps/comps/layout/mobileTabLayout.tsx | 120 ++++++++++++++---- .../comps/controls/styleControlConstants.tsx | 24 ++++ 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index 1d498be4f..5073dddc9 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -20,7 +20,7 @@ import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl"; import { DataOption, DataOptionType, menuItemStyleOptions, mobileNavJsonMenuItems, MobileModeOptions, MobileMode, HamburgerPositionOptions, DrawerPlacementOptions } from "./navLayoutConstants"; import { styleControl } from "@lowcoder-ee/comps/controls/styleControl"; -import { NavLayoutItemActiveStyle, NavLayoutItemActiveStyleType, NavLayoutItemHoverStyle, NavLayoutItemHoverStyleType, NavLayoutItemStyle, NavLayoutItemStyleType, NavLayoutStyle, NavLayoutStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants"; +import { HamburgerButtonStyle, DrawerContainerStyle, NavLayoutItemActiveStyle, NavLayoutItemActiveStyleType, NavLayoutItemHoverStyle, NavLayoutItemHoverStyleType, NavLayoutItemStyle, NavLayoutItemStyleType, NavLayoutStyle, NavLayoutStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants"; import Segmented from "antd/es/segmented"; import { controlItem } from "components/control"; import { check } from "@lowcoder-ee/util/convertUtils"; @@ -72,15 +72,23 @@ const HamburgerButton = styled.button<{ $size: string; $position: string; // bottom-right | bottom-left | top-right | top-left $zIndex: number; + $background?: string; + $borderColor?: string; + $radius?: string; + $margin?: string; + $padding?: string; + $borderWidth?: string; }>` position: fixed; ${(props) => (props.$position.includes('bottom') ? 'bottom: 16px;' : 'top: 16px;')} ${(props) => (props.$position.includes('right') ? 'right: 16px;' : 'left: 16px;')} width: ${(props) => props.$size}; height: ${(props) => props.$size}; - border-radius: 50%; - border: 1px solid rgba(0,0,0,0.1); - background: white; + border-radius: ${(props) => props.$radius || '50%'}; + border: ${(props) => props.$borderWidth || '1px'} solid ${(props) => props.$borderColor || 'rgba(0,0,0,0.1)'}; + background: ${(props) => props.$background || 'white'}; + margin: ${(props) => props.$margin || '0px'}; + padding: ${(props) => props.$padding || '0px'}; display: flex; align-items: center; justify-content: center; @@ -108,16 +116,34 @@ const BurgerIcon = styled.div<{ &::after { top: 6px; } `; +const IconWrapper = styled.div<{ + $iconColor?: string; +}>` + display: inline-flex; + align-items: center; + justify-content: center; + svg { + color: ${(p) => p.$iconColor || 'inherit'}; + fill: ${(p) => p.$iconColor || 'currentColor'}; + } +`; + const DrawerContent = styled.div<{ $background: string; + $padding?: string; + $borderColor?: string; + $borderWidth?: string; + $margin?: string; }>` background: ${(p) => p.$background}; width: 100%; height: 100%; display: flex; flex-direction: column; - padding: 12px; + padding: ${(p) => p.$padding || '12px'}; + margin: ${(p) => p.$margin || '0px'}; box-sizing: border-box; + border: ${(p) => p.$borderWidth || '1px'} solid ${(p) => p.$borderColor || 'transparent'}; `; const DrawerHeader = styled.div` @@ -425,7 +451,7 @@ function renderHamburgerLayoutSection(children: any): any { const drawerPlacement = children.drawerPlacement.getView(); return ( <> - {children.hamburgerIcon.propertyView({ label: "MenuIcon" })} + {children.hamburgerIcon.propertyView({ label: "Menu Icon" })} {children.drawerCloseIcon.propertyView({ label: "Close Icon" })} {children.hamburgerPosition.propertyView({ label: "Hamburger Position" })} {children.hamburgerSize.propertyView({ label: "Hamburger Size" })} @@ -462,6 +488,8 @@ function renderVerticalLayoutSection(children: any): any { ); } + + let MobileTabLayoutTmp = (function () { const childrenMap = { onEvent: eventHandlerControl(EventOptions), @@ -492,7 +520,7 @@ let MobileTabLayoutTmp = (function () { drawerCloseIcon: IconControl, hamburgerPosition: dropdownControl(HamburgerPositionOptions, "bottom-right"), hamburgerSize: withDefault(StringControl, "56px"), - drawerPlacement: dropdownControl(DrawerPlacementOptions, "bottom"), + drawerPlacement: dropdownControl(DrawerPlacementOptions, "right"), drawerHeight: withDefault(StringControl, "60%"), drawerWidth: withDefault(StringControl, "250px"), shadowOverlay: withDefault(BoolCodeControl, true), @@ -506,6 +534,8 @@ let MobileTabLayoutTmp = (function () { navItemStyle: styleControl(NavLayoutItemStyle, 'navItemStyle'), navItemHoverStyle: styleControl(NavLayoutItemHoverStyle, 'navItemHoverStyle'), navItemActiveStyle: styleControl(NavLayoutItemActiveStyle, 'navItemActiveStyle'), + hamburgerButtonStyle: styleControl(HamburgerButtonStyle, 'hamburgerButtonStyle'), + drawerContainerStyle: styleControl(DrawerContainerStyle, 'drawerContainerStyle'), }; return new MultiCompBuilder(childrenMap, (props, dispatch) => { return null; @@ -524,10 +554,18 @@ let MobileTabLayoutTmp = (function () { ? renderHamburgerLayoutSection(children) : renderVerticalLayoutSection(children)} -
- {children.navStyle.getPropertyView()} -
-
+ {!isHamburgerMode && ( +
+ {children.navStyle.getPropertyView()} +
+ )} + + {isHamburgerMode && ( +
+ {children.hamburgerButtonStyle.getPropertyView()} +
+ )} +
{controlItem({}, ( + {isHamburgerMode && ( +
+ {children.drawerContainerStyle.getPropertyView()} +
+ )} ); }) @@ -563,6 +606,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const hamburgerSize = comp.children.hamburgerSize.getView(); const hamburgerIconComp = comp.children.hamburgerIcon; const drawerCloseIconComp = comp.children.drawerCloseIcon; + const hamburgerButtonStyle = comp.children.hamburgerButtonStyle.getView(); const drawerPlacement = comp.children.drawerPlacement.getView(); const drawerHeight = comp.children.drawerHeight.getView(); const drawerWidth = comp.children.drawerWidth.getView(); @@ -572,6 +616,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const maxWidth = comp.children.maxWidth.getView(); const verticalAlignment = comp.children.verticalAlignment.getView(); const showSeparator = comp.children.showSeparator.getView(); + const drawerContainerStyle = comp.children.drawerContainerStyle.getView(); const bgColor = (useContext(ThemeContext)?.theme || defaultTheme).canvas; const onEvent = comp.children.onEvent.getView(); @@ -626,6 +671,21 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { backgroundStyle = `center / cover url('${backgroundImage}') no-repeat, ${backgroundStyle}`; } + const navigateToApp = (nextIndex: number) => { + if (dataOptionType === DataOption.Manual) { + const selectedTab = tabViews[nextIndex]; + if (selectedTab) { + const url = [ + ALL_APPLICATIONS_URL, + pathParam.applicationId, + pathParam.viewMode, + nextIndex, + ].join("/"); + selectedTab.children.action.act(url); + } + } + }; + const tabBarView = ( { const nextIndex = Number(key); setTabIndex(nextIndex); // push URL with query/hash params - if (dataOptionType === DataOption.Manual) { - const selectedTab = tabViews[nextIndex]; - if (selectedTab) { - const url = [ - ALL_APPLICATIONS_URL, - pathParam.applicationId, - pathParam.viewMode, - nextIndex, - ].join("/"); - selectedTab.children.action.act(url); - } - } + navigateToApp(nextIndex); }} readOnly={!!readOnly} canvasBg={bgColor} @@ -682,11 +731,21 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { $size={hamburgerSize} $position={hamburgerPosition} $zIndex={Layers.tabBar + 1} + $background={hamburgerButtonStyle?.background} + $borderColor={hamburgerButtonStyle?.border} + $radius={hamburgerButtonStyle?.radius} + $margin={hamburgerButtonStyle?.margin} + $padding={hamburgerButtonStyle?.padding} + $borderWidth={hamburgerButtonStyle?.borderWidth} onClick={() => setDrawerVisible(true)} > - {hamburgerIconComp.toJsonValue() - ? hamburgerIconComp.getView() - : } + {hamburgerIconComp.toJsonValue() ? ( + + {hamburgerIconComp.getView()} + + ) : ( + + )} ); @@ -707,7 +766,13 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { mask={shadowOverlay} bodyStyle={drawerBodyStyle} > - + { setTabIndex(index); setDrawerVisible(false); onEvent('click'); + navigateToApp(index); }} > {tab.children.icon.toJsonValue() ? ( diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 569ada9c4..175448bf3 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -1382,6 +1382,30 @@ export const FloatButtonStyle = [ BORDER_WIDTH, ] as const; +export const HamburgerButtonStyle = [ + getBackground(), + { + name: "iconFill", + label: trans("style.fill"), + depTheme: "primary", + depType: DEP_TYPE.SELF, + transformer: toSelf, + }, + MARGIN, + PADDING, + BORDER, + RADIUS, + BORDER_WIDTH, +] as const; + +export const DrawerContainerStyle = [ + getBackground(), + MARGIN, + PADDING, + BORDER, + BORDER_WIDTH, +] as const; + export const TransferStyle = [ getStaticBackground(SURFACE_COLOR), ...STYLING_FIELDS_CONTAINER_SEQUENCE.filter(style=>style.name!=='rotation'), From b2254a0656ce7808962820295fa56ee9ddfbe63c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 5 Nov 2025 22:40:14 +0500 Subject: [PATCH 39/97] fix scroll issue for the nav apps propertyview --- .../packages/lowcoder/src/pages/editor/right/PropertyView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx b/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx index f5e90bd7b..f051a2898 100644 --- a/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx @@ -26,7 +26,7 @@ export default function PropertyView(props: PropertyViewProps) { let propertyView; if (selectedComp) { - return <>{selectedComp.getPropertyView()}; + propertyView = selectedComp.getPropertyView(); } else if (selectedCompNames.size > 1) { propertyView = ( Date: Thu, 6 Nov 2025 18:31:10 +0500 Subject: [PATCH 40/97] [Fix]: navigation apps render inside the preview --- client/packages/lowcoder/src/pages/editor/editorView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index c722f907f..a6ae58db3 100644 --- a/client/packages/lowcoder/src/pages/editor/editorView.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx @@ -270,6 +270,7 @@ const DeviceWrapperInner = styled(Flex)` > div:first-child { > div:first-child { > div:nth-child(2) { + contain: paint; display: block !important; overflow: hidden auto !important; } From 2a8295566b56ddb1284ecc855209773893317d78 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 6 Nov 2025 21:05:18 +0500 Subject: [PATCH 41/97] [Fix]: render the drawer inside the preivew/app canvas --- .../comps/comps/layout/mobileTabLayout.tsx | 43 ++++++++++++------- .../lowcoder/src/constants/domLocators.ts | 1 + .../lowcoder/src/pages/editor/editorView.tsx | 7 ++- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index 5073dddc9..dfe9539af 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -5,13 +5,14 @@ import { manualOptionsControl } from "comps/controls/optionsControl"; import { BoolCodeControl, StringControl, jsonControl, NumberControl } from "comps/controls/codeControl"; import { IconControl } from "comps/controls/iconControl"; import styled from "styled-components"; -import React, { Suspense, useContext, useEffect, useMemo, useState } from "react"; +import React, { Suspense, useContext, useEffect, useMemo, useState, useCallback } from "react"; import { registerLayoutMap } from "comps/comps/uiComp"; import { AppSelectComp } from "comps/comps/layout/appSelectComp"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; import { ConstructorToComp, ConstructorToDataType } from "lowcoder-core"; import { CanvasContainer } from "comps/comps/gridLayoutComp/canvasView"; import { CanvasContainerID } from "constants/domLocators"; +import { PreviewContainerID } from "constants/domLocators"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { Layers } from "constants/Layers"; import { ExternalEditorContext } from "util/context/ExternalEditorContext"; @@ -30,6 +31,7 @@ import { ThemeContext } from "@lowcoder-ee/comps/utils/themeContext"; import { AlignCenter } from "lowcoder-design"; import { AlignLeft } from "lowcoder-design"; import { AlignRight } from "lowcoder-design"; +import { Drawer } from "lowcoder-design"; import { LayoutActionComp } from "./layoutActionComp"; import { defaultTheme } from "@lowcoder-ee/constants/themeConstants"; import { clickEvent, eventHandlerControl } from "@lowcoder-ee/comps/controls/eventHandlerControl"; @@ -43,7 +45,6 @@ const TabBarItem = React.lazy(() => default: module.TabBarItem, })) ); -const Popup = React.lazy(() => import("antd-mobile/es/components/popup")); const EventOptions = [clickEvent] as const; const AppViewContainer = styled.div` @@ -620,6 +621,13 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const bgColor = (useContext(ThemeContext)?.theme || defaultTheme).canvas; const onEvent = comp.children.onEvent.getView(); + const getContainer = useCallback(() => + document.querySelector(`#${PreviewContainerID}`) || + document.querySelector(`#${CanvasContainerID}`) || + document.body, + [] + ); + useEffect(() => { comp.children.jsonTabs.dispatchChangeValueAction({ manual: jsonItems as unknown as Array> @@ -749,22 +757,27 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { ); - const drawerBodyStyle = useMemo(() => { - if (drawerPlacement === 'left' || drawerPlacement === 'right') { - return { width: drawerWidth } as React.CSSProperties; - } - return { height: drawerHeight } as React.CSSProperties; - }, [drawerPlacement, drawerHeight, drawerWidth]); - const drawerView = ( }> - setDrawerVisible(false)} + setDrawerVisible(false)} - position={drawerPlacement as any} + placement={drawerPlacement as any} mask={shadowOverlay} - bodyStyle={drawerBodyStyle} + maskClosable={true} + closable={false} + styles={{ body: { padding: 0 } } as any} + getContainer={getContainer} + width={ + (drawerPlacement === 'left' || drawerPlacement === 'right') + ? (drawerWidth as any) + : undefined + } + height={ + (drawerPlacement === 'top' || drawerPlacement === 'bottom') + ? (drawerHeight as any) + : undefined + } > { ))} - + ); diff --git a/client/packages/lowcoder/src/constants/domLocators.ts b/client/packages/lowcoder/src/constants/domLocators.ts index b3d1709a5..2fefcb5f1 100644 --- a/client/packages/lowcoder/src/constants/domLocators.ts +++ b/client/packages/lowcoder/src/constants/domLocators.ts @@ -1,2 +1,3 @@ export const CanvasContainerID = "__canvas_container__"; export const CodeEditorTooltipContainerID = "__code_editor_tooltip__"; +export const PreviewContainerID = "__preview_container__"; diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index a6ae58db3..a60f9f0ef 100644 --- a/client/packages/lowcoder/src/pages/editor/editorView.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx @@ -64,6 +64,7 @@ import { isEqual, noop } from "lodash"; import { AppSettingContext, AppSettingType } from "@lowcoder-ee/comps/utils/appSettingContext"; import { getBrandingSetting } from "@lowcoder-ee/redux/selectors/enterpriseSelectors"; import Flex from "antd/es/flex"; +import { PreviewContainerID } from "constants/domLocators"; // import { BottomSkeleton } from "./bottom/BottomContent"; const Header = lazy( @@ -534,10 +535,12 @@ function EditorView(props: EditorViewProps) { deviceType={editorState.deviceType} deviceOrientation={editorState.deviceOrientation} > - {uiComp.getView()} +
+ {uiComp.getView()} +
) : ( -
+
{uiComp.getView()}
) From 1ceefdb0ed02713bb54ce83fd8ee09fb1a74f582 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 7 Nov 2025 19:45:34 +0500 Subject: [PATCH 42/97] add navigation component burger mode --- .../src/comps/comps/navComp/navComp.tsx | 199 ++++++++++++++++-- 1 file changed, 176 insertions(+), 23 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 670f4bba9..27ea11677 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -5,26 +5,34 @@ import { Section, sectionNames } from "lowcoder-design"; import styled from "styled-components"; import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; +import { dropdownControl } from "comps/controls/dropdownControl"; import { alignWithJustifyControl } from "comps/controls/alignControl"; import { navListComp } from "./navItemComp"; import { menuPropertyView } from "./components/MenuItemList"; import { default as DownOutlined } from "@ant-design/icons/DownOutlined"; +import { default as MenuOutlined } from "@ant-design/icons/MenuOutlined"; import { default as Dropdown } from "antd/es/dropdown"; import { default as Menu, MenuProps } from "antd/es/menu"; +import { default as Drawer } from "antd/es/drawer"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { styleControl } from "comps/controls/styleControl"; import { AnimationStyle, AnimationStyleType, NavigationStyle, + HamburgerButtonStyle, + DrawerContainerStyle, + NavLayoutItemStyle, + NavLayoutItemHoverStyle, + NavLayoutItemActiveStyle, } from "comps/controls/styleControlConstants"; import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; -import { useContext } from "react"; +import { useContext, useState } from "react"; import { EditorContext } from "comps/editorState"; -import { controlItem } from "lowcoder-design"; import { createNavItemsControl } from "./components/NavItemsControl"; +import { Layers } from "constants/Layers"; type IProps = { $justify: boolean; @@ -34,6 +42,7 @@ type IProps = { $borderRadius: string; $borderStyle: string; $animationStyle: AnimationStyleType; + $orientation: "horizontal" | "vertical"; }; const Wrapper = styled("div")< @@ -45,18 +54,21 @@ ${props=>props.$animationStyle} box-sizing: border-box; border: ${(props) => props.$borderWidth ? `${props.$borderWidth}` : '1px'} ${props=>props.$borderStyle} ${(props) => props.$borderColor}; background: ${(props) => props.$bgColor}; + position: relative; `; -const NavInner = styled("div") >` +const NavInner = styled("div") >` // margin: 0 -16px; height: 100%; display: flex; - justify-content: ${(props) => (props.$justify ? "space-between" : "left")}; + flex-direction: ${(props) => (props.$orientation === "vertical" ? "column" : "row")}; + justify-content: ${(props) => (props.$orientation === "vertical" ? "flex-start" : (props.$justify ? "space-between" : "left"))}; `; const Item = styled.div<{ $active: boolean; $activeColor: string; + $hoverColor: string; $color: string; $fontFamily: string; $fontStyle: string; @@ -66,12 +78,22 @@ const Item = styled.div<{ $padding: string; $textTransform:string; $textDecoration:string; + $bg?: string; + $hoverBg?: string; + $activeBg?: string; + $border?: string; + $hoverBorder?: string; + $activeBorder?: string; + $radius?: string; $disabled?: boolean; }>` height: 30px; line-height: 30px; padding: ${(props) => props.$padding ? props.$padding : '0 16px'}; color: ${(props) => props.$disabled ? `${props.$color}80` : (props.$active ? props.$activeColor : props.$color)}; + background-color: ${(props) => (props.$active ? (props.$activeBg || 'transparent') : (props.$bg || 'transparent'))}; + border: ${(props) => props.$border ? `1px solid ${props.$border}` : '1px solid transparent'}; + border-radius: ${(props) => props.$radius ? props.$radius : '0px'}; font-weight: ${(props) => (props.$textWeight ? props.$textWeight : 500)}; font-family:${(props) => (props.$fontFamily ? props.$fontFamily : 'sans-serif')}; font-style:${(props) => (props.$fontStyle ? props.$fontStyle : 'normal')}; @@ -81,7 +103,9 @@ const Item = styled.div<{ margin:${(props) => props.$margin ? props.$margin : '0px'}; &:hover { - color: ${(props) => props.$disabled ? (props.$active ? props.$activeColor : props.$color) : props.$activeColor}; + color: ${(props) => props.$disabled ? (props.$active ? props.$activeColor : props.$color) : (props.$hoverColor || props.$activeColor)}; + background-color: ${(props) => props.$disabled ? (props.$active ? (props.$activeBg || 'transparent') : (props.$bg || 'transparent')) : (props.$hoverBg || props.$activeBg || props.$bg || 'transparent')}; + border: ${(props) => props.$hoverBorder ? `1px solid ${props.$hoverBorder}` : (props.$activeBorder ? `1px solid ${props.$activeBorder}` : (props.$border ? `1px solid ${props.$border}` : '1px solid transparent'))}; cursor: ${(props) => props.$disabled ? 'not-allowed' : 'pointer'}; } @@ -101,10 +125,10 @@ const LogoWrapper = styled.div` } `; -const ItemList = styled.div<{ $align: string }>` +const ItemList = styled.div<{ $align: string, $orientation?: string }>` flex: 1; display: flex; - flex-direction: row; + flex-direction: ${(props) => (props.$orientation === "vertical" ? "column" : "row")}; justify-content: ${(props) => props.$align}; `; @@ -114,6 +138,37 @@ const StyledMenu = styled(Menu) ` } `; +const FloatingHamburgerButton = styled.button<{ + $size: string; + $position: string; // top-right | top-left | bottom-right | bottom-left + $zIndex: number; + $background?: string; + $borderColor?: string; + $radius?: string; + $margin?: string; + $padding?: string; + $borderWidth?: string; + $iconColor?: string; +}>` + position: fixed; + ${(props) => (props.$position.includes('bottom') ? 'bottom: 16px;' : 'top: 16px;')} + ${(props) => (props.$position.includes('right') ? 'right: 16px;' : 'left: 16px;')} + width: ${(props) => props.$size}; + height: ${(props) => props.$size}; + border-radius: ${(props) => props.$radius || '50%'}; + border: ${(props) => props.$borderWidth || '1px'} solid ${(props) => props.$borderColor || 'rgba(0,0,0,0.1)'}; + background: ${(props) => props.$background || 'white'}; + margin: ${(props) => props.$margin || '0px'}; + padding: ${(props) => props.$padding || '0px'}; + display: flex; + align-items: center; + justify-content: center; + z-index: ${(props) => props.$zIndex}; + cursor: pointer; + box-shadow: 0 6px 16px rgba(0,0,0,0.15); + color: ${(props) => props.$iconColor || 'inherit'}; +`; + const logoEventHandlers = [clickEvent]; // Compatible with historical style data 2022-8-26 @@ -154,8 +209,33 @@ function fixOldItemsData(oldData: any) { const childrenMap = { logoUrl: StringControl, logoEvent: withDefault(eventHandlerControl(logoEventHandlers), [{ name: "click" }]), + orientation: dropdownControl([ + { label: "Horizontal", value: "horizontal" }, + { label: "Vertical", value: "vertical" }, + ], "horizontal"), + displayMode: dropdownControl([ + { label: "Bar", value: "bar" }, + { label: "Hamburger", value: "hamburger" }, + ], "bar"), + hamburgerPosition: dropdownControl([ + { label: "Top Right", value: "top-right" }, + { label: "Top Left", value: "top-left" }, + { label: "Bottom Right", value: "bottom-right" }, + { label: "Bottom Left", value: "bottom-left" }, + ], "top-right"), + hamburgerSize: withDefault(StringControl, "56px"), + drawerPlacement: dropdownControl([ + { label: "Left", value: "left" }, + { label: "Right", value: "right" }, + ], "right"), + shadowOverlay: withDefault(BoolCodeControl, true), horizontalAlignment: alignWithJustifyControl(), style: migrateOldData(styleControl(NavigationStyle, 'style'), fixOldStyleData), + navItemStyle: styleControl(NavLayoutItemStyle, 'navItemStyle'), + navItemHoverStyle: styleControl(NavLayoutItemHoverStyle, 'navItemHoverStyle'), + navItemActiveStyle: styleControl(NavLayoutItemActiveStyle, 'navItemActiveStyle'), + hamburgerButtonStyle: styleControl(HamburgerButtonStyle, 'hamburgerButtonStyle'), + drawerContainerStyle: styleControl(DrawerContainerStyle, 'drawerContainerStyle'), animationStyle: styleControl(AnimationStyle, 'animationStyle'), items: withDefault(migrateOldData(createNavItemsControl(), fixOldItemsData), { optionType: "manual", @@ -168,6 +248,7 @@ const childrenMap = { }; const NavCompBase = new UICompBuilder(childrenMap, (props) => { + const [drawerVisible, setDrawerVisible] = useState(false); const data = props.items; const items = ( <> @@ -207,16 +288,24 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { 0} - $color={props.style.text} - $activeColor={props.style.accent} + $color={(props.navItemStyle && props.navItemStyle.text) || props.style.text} + $hoverColor={(props.navItemHoverStyle && props.navItemHoverStyle.text) || props.style.accent} + $activeColor={(props.navItemActiveStyle && props.navItemActiveStyle.text) || props.style.accent} $fontFamily={props.style.fontFamily} $fontStyle={props.style.fontStyle} $textWeight={props.style.textWeight} $textSize={props.style.textSize} - $padding={props.style.padding} + $padding={(props.navItemStyle && props.navItemStyle.padding) || props.style.padding} $textTransform={props.style.textTransform} $textDecoration={props.style.textDecoration} - $margin={props.style.margin} + $margin={(props.navItemStyle && props.navItemStyle.margin) || props.style.margin} + $bg={(props.navItemStyle && props.navItemStyle.background) || undefined} + $hoverBg={(props.navItemHoverStyle && props.navItemHoverStyle.background) || undefined} + $activeBg={(props.navItemActiveStyle && props.navItemActiveStyle.background) || undefined} + $border={(props.navItemStyle && props.navItemStyle.border) || undefined} + $hoverBorder={(props.navItemHoverStyle && props.navItemHoverStyle.border) || undefined} + $activeBorder={(props.navItemActiveStyle && props.navItemActiveStyle.border) || undefined} + $radius={(props.navItemStyle && props.navItemStyle.radius) || undefined} $disabled={disabled} onClick={() => { if (!disabled && onEvent) onEvent("click"); }} > @@ -255,6 +344,8 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { ); const justify = props.horizontalAlignment === "justify"; + const isVertical = props.orientation === "vertical"; + const isHamburger = props.displayMode === "hamburger"; return ( { $borderWidth={props.style.borderWidth} $borderRadius={props.style.radius} > - - {props.logoUrl && ( - props.logoEvent("click")}> - LOGO - - )} - {!justify ? {items} : items} - + {!isHamburger && ( + + {props.logoUrl && ( + props.logoEvent("click")}> + LOGO + + )} + {!justify ? {items} : items} + + )} + {isHamburger && ( + <> + setDrawerVisible(true)} + > + + + setDrawerVisible(false)} + open={drawerVisible} + mask={props.shadowOverlay} + styles={{ body: { padding: "8px", background: props.drawerContainerStyle?.background } }} + destroyOnClose + > + {items} + + + )} ); }) @@ -292,10 +415,21 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && (
- {children.horizontalAlignment.propertyView({ - label: trans("navigation.horizontalAlignment"), - radioButton: true, - })} + {children.orientation.propertyView({ label: "Orientation", radioButton: true })} + {children.displayMode.propertyView({ label: "Display Mode", radioButton: true })} + {children.displayMode.getView() === 'hamburger' ? ( + [ + children.hamburgerPosition.propertyView({ label: "Hamburger Position" }), + children.hamburgerSize.propertyView({ label: "Hamburger Size" }), + children.drawerPlacement.propertyView({ label: "Drawer Placement", radioButton: true }), + children.shadowOverlay.propertyView({ label: "Shadow Overlay" }), + ] + ) : ( + children.horizontalAlignment.propertyView({ + label: trans("navigation.horizontalAlignment"), + radioButton: true, + }) + )} {hiddenPropertyView(children)}
)} @@ -313,6 +447,25 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
{children.style.getPropertyView()}
+
+ {children.navItemStyle.getPropertyView()} +
+
+ {children.navItemHoverStyle.getPropertyView()} +
+
+ {children.navItemActiveStyle.getPropertyView()} +
+ {children.displayMode.getView() === 'hamburger' && ( + <> +
+ {children.hamburgerButtonStyle.getPropertyView()} +
+
+ {children.drawerContainerStyle.getPropertyView()} +
+ + )}
{children.animationStyle.getPropertyView()}
From 795c181f80ce2b6bf3e08ea65492801cda930ba2 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 10 Nov 2025 17:25:28 +0500 Subject: [PATCH 43/97] refactor navigation component --- .../src/comps/comps/navComp/navComp.tsx | 165 ++++++++++-------- 1 file changed, 96 insertions(+), 69 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 27ea11677..6d4b87288 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -206,6 +206,93 @@ function fixOldItemsData(oldData: any) { return oldData; } +// Property View Helpers +function renderBasicSection(children: any) { + return ( +
+ {children.items.propertyView()} +
+ ); +} + +function renderInteractionSection(children: any) { + return ( +
+ {hiddenPropertyView(children)} + {showDataLoadingIndicatorsPropertyView(children)} +
+ ); +} + +function renderLayoutSection(children: any) { + const isHamburger = children.displayMode.getView() === 'hamburger'; + const common = [ + children.orientation.propertyView({ label: "Orientation", radioButton: true }), + children.displayMode.propertyView({ label: "Display Mode", radioButton: true }), + ]; + const hamburger = [ + ...common, + children.hamburgerPosition.propertyView({ label: "Hamburger Position" }), + children.hamburgerSize.propertyView({ label: "Hamburger Size" }), + children.drawerPlacement.propertyView({ label: "Drawer Placement", radioButton: true }), + children.shadowOverlay.propertyView({ label: "Shadow Overlay" }), + ]; + const bar = [ + ...common, + children.horizontalAlignment.propertyView({ + label: trans("navigation.horizontalAlignment"), + radioButton: true, + }), + ]; + + return ( +
+ {isHamburger ? hamburger : bar} +
+ ); +} + +function renderAdvancedSection(children: any) { + return ( +
+ {children.logoUrl.propertyView({ label: trans("navigation.logoURL"), tooltip: trans("navigation.logoURLDesc") })} + {children.logoUrl.getView() && children.logoEvent.propertyView({ inline: true })} +
+ ); +} + +function renderStyleSections(children: any) { + return ( + <> +
+ {children.style.getPropertyView()} +
+
+ {children.navItemStyle.getPropertyView()} +
+
+ {children.navItemHoverStyle.getPropertyView()} +
+
+ {children.navItemActiveStyle.getPropertyView()} +
+ {children.displayMode.getView() === 'hamburger' && ( + <> +
+ {children.hamburgerButtonStyle.getPropertyView()} +
+
+ {children.drawerContainerStyle.getPropertyView()} +
+ + )} +
+ {children.animationStyle.getPropertyView()} +
+ + ); +} + const childrenMap = { logoUrl: StringControl, logoEvent: withDefault(eventHandlerControl(logoEventHandlers), [{ name: "click" }]), @@ -400,77 +487,17 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { ); }) .setPropertyViewFn((children) => { + const mode = useContext(EditorContext).editorModeStatus; + const showLogic = mode === "logic" || mode === "both"; + const showLayout = mode === "layout" || mode === "both"; + return ( <> -
- {children.items.propertyView()} -
- - {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {hiddenPropertyView(children)} - {showDataLoadingIndicatorsPropertyView(children)} -
- )} - - {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {children.orientation.propertyView({ label: "Orientation", radioButton: true })} - {children.displayMode.propertyView({ label: "Display Mode", radioButton: true })} - {children.displayMode.getView() === 'hamburger' ? ( - [ - children.hamburgerPosition.propertyView({ label: "Hamburger Position" }), - children.hamburgerSize.propertyView({ label: "Hamburger Size" }), - children.drawerPlacement.propertyView({ label: "Drawer Placement", radioButton: true }), - children.shadowOverlay.propertyView({ label: "Shadow Overlay" }), - ] - ) : ( - children.horizontalAlignment.propertyView({ - label: trans("navigation.horizontalAlignment"), - radioButton: true, - }) - )} - {hiddenPropertyView(children)} -
- )} - - {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {children.logoUrl.propertyView({ label: trans("navigation.logoURL"), tooltip: trans("navigation.logoURLDesc") })} - {children.logoUrl.getView() && children.logoEvent.propertyView({ inline: true })} -
- )} - - {(useContext(EditorContext).editorModeStatus === "layout" || - useContext(EditorContext).editorModeStatus === "both") && ( - <> -
- {children.style.getPropertyView()} -
-
- {children.navItemStyle.getPropertyView()} -
-
- {children.navItemHoverStyle.getPropertyView()} -
-
- {children.navItemActiveStyle.getPropertyView()} -
- {children.displayMode.getView() === 'hamburger' && ( - <> -
- {children.hamburgerButtonStyle.getPropertyView()} -
-
- {children.drawerContainerStyle.getPropertyView()} -
- - )} -
- {children.animationStyle.getPropertyView()} -
- - )} + {renderBasicSection(children)} + {showLogic && renderInteractionSection(children)} + {showLayout && renderLayoutSection(children)} + {showLogic && renderAdvancedSection(children)} + {showLayout && renderStyleSections(children)} ); }) From 0d2d8e40783762d0f55619401ab161500fec8821 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 10 Nov 2025 18:25:16 +0500 Subject: [PATCH 44/97] make drawer placement control consistent --- .../src/comps/comps/navComp/navComp.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 6d4b87288..d7123c322 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -5,7 +5,7 @@ import { Section, sectionNames } from "lowcoder-design"; import styled from "styled-components"; import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; -import { dropdownControl } from "comps/controls/dropdownControl"; +import { dropdownControl, PositionControl } from "comps/controls/dropdownControl"; import { alignWithJustifyControl } from "comps/controls/alignControl"; import { navListComp } from "./navItemComp"; import { menuPropertyView } from "./components/MenuItemList"; @@ -29,10 +29,11 @@ import { import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; -import { useContext, useState } from "react"; +import { useContext, useState, useCallback } from "react"; import { EditorContext } from "comps/editorState"; import { createNavItemsControl } from "./components/NavItemsControl"; import { Layers } from "constants/Layers"; +import { CanvasContainerID } from "constants/domLocators"; type IProps = { $justify: boolean; @@ -234,7 +235,7 @@ function renderLayoutSection(children: any) { ...common, children.hamburgerPosition.propertyView({ label: "Hamburger Position" }), children.hamburgerSize.propertyView({ label: "Hamburger Size" }), - children.drawerPlacement.propertyView({ label: "Drawer Placement", radioButton: true }), + children.placement.propertyView({ label: trans("drawer.placement"), radioButton: true }), children.shadowOverlay.propertyView({ label: "Shadow Overlay" }), ]; const bar = [ @@ -311,10 +312,7 @@ const childrenMap = { { label: "Bottom Left", value: "bottom-left" }, ], "top-right"), hamburgerSize: withDefault(StringControl, "56px"), - drawerPlacement: dropdownControl([ - { label: "Left", value: "left" }, - { label: "Right", value: "right" }, - ], "right"), + placement: PositionControl, shadowOverlay: withDefault(BoolCodeControl, true), horizontalAlignment: alignWithJustifyControl(), style: migrateOldData(styleControl(NavigationStyle, 'style'), fixOldStyleData), @@ -336,6 +334,10 @@ const childrenMap = { const NavCompBase = new UICompBuilder(childrenMap, (props) => { const [drawerVisible, setDrawerVisible] = useState(false); + const getContainer = useCallback(() => + document.querySelector(`#${CanvasContainerID}`) || document.body, + [] + ); const data = props.items; const items = ( <> @@ -471,11 +473,12 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { setDrawerVisible(false)} open={drawerVisible} mask={props.shadowOverlay} + getContainer={getContainer} styles={{ body: { padding: "8px", background: props.drawerContainerStyle?.background } }} destroyOnClose > From 5addc3273c20df542a03f6e6a8856b3bd5460bc7 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 10 Nov 2025 18:26:10 +0500 Subject: [PATCH 45/97] fix hamburger menu options --- .../src/comps/comps/navComp/navComp.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index d7123c322..5234096a8 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -141,7 +141,7 @@ const StyledMenu = styled(Menu) ` const FloatingHamburgerButton = styled.button<{ $size: string; - $position: string; // top-right | top-left | bottom-right | bottom-left + $position: string; // left | right $zIndex: number; $background?: string; $borderColor?: string; @@ -152,8 +152,8 @@ const FloatingHamburgerButton = styled.button<{ $iconColor?: string; }>` position: fixed; - ${(props) => (props.$position.includes('bottom') ? 'bottom: 16px;' : 'top: 16px;')} - ${(props) => (props.$position.includes('right') ? 'right: 16px;' : 'left: 16px;')} + top: 16px; + ${(props) => (props.$position === 'right' ? 'right: 16px;' : 'left: 16px;')} width: ${(props) => props.$size}; height: ${(props) => props.$size}; border-radius: ${(props) => props.$radius || '50%'}; @@ -233,7 +233,7 @@ function renderLayoutSection(children: any) { ]; const hamburger = [ ...common, - children.hamburgerPosition.propertyView({ label: "Hamburger Position" }), + children.hamburgerPosition.propertyView({ label: "Hamburger Position", radioButton: true }), children.hamburgerSize.propertyView({ label: "Hamburger Size" }), children.placement.propertyView({ label: trans("drawer.placement"), radioButton: true }), children.shadowOverlay.propertyView({ label: "Shadow Overlay" }), @@ -306,11 +306,9 @@ const childrenMap = { { label: "Hamburger", value: "hamburger" }, ], "bar"), hamburgerPosition: dropdownControl([ - { label: "Top Right", value: "top-right" }, - { label: "Top Left", value: "top-left" }, - { label: "Bottom Right", value: "bottom-right" }, - { label: "Bottom Left", value: "bottom-left" }, - ], "top-right"), + { label: "Left", value: "left" }, + { label: "Right", value: "right" }, + ], "right"), hamburgerSize: withDefault(StringControl, "56px"), placement: PositionControl, shadowOverlay: withDefault(BoolCodeControl, true), @@ -459,7 +457,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { <> Date: Mon, 10 Nov 2025 20:11:46 +0500 Subject: [PATCH 46/97] add conditional UI on basis of mode --- .../src/comps/comps/navComp/navComp.tsx | 65 ++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 5234096a8..cc56e3a86 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -16,6 +16,7 @@ import { default as Menu, MenuProps } from "antd/es/menu"; import { default as Drawer } from "antd/es/drawer"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { styleControl } from "comps/controls/styleControl"; +import { IconControl } from "comps/controls/iconControl"; import { AnimationStyle, AnimationStyleType, @@ -34,6 +35,8 @@ import { EditorContext } from "comps/editorState"; import { createNavItemsControl } from "./components/NavItemsControl"; import { Layers } from "constants/Layers"; import { CanvasContainerID } from "constants/domLocators"; +import { isNumeric } from "util/stringUtils"; +import { hasIcon } from "comps/utils"; type IProps = { $justify: boolean; @@ -58,6 +61,13 @@ ${props=>props.$animationStyle} position: relative; `; +const DEFAULT_SIZE = 378; + +// If it is a number, use the px unit by default +function transToPxSize(size: string | number) { + return isNumeric(size) ? size + "px" : (size as string); +} + const NavInner = styled("div") >` // margin: 0 -16px; height: 100%; @@ -228,7 +238,6 @@ function renderInteractionSection(children: any) { function renderLayoutSection(children: any) { const isHamburger = children.displayMode.getView() === 'hamburger'; const common = [ - children.orientation.propertyView({ label: "Orientation", radioButton: true }), children.displayMode.propertyView({ label: "Display Mode", radioButton: true }), ]; const hamburger = [ @@ -236,10 +245,24 @@ function renderLayoutSection(children: any) { children.hamburgerPosition.propertyView({ label: "Hamburger Position", radioButton: true }), children.hamburgerSize.propertyView({ label: "Hamburger Size" }), children.placement.propertyView({ label: trans("drawer.placement"), radioButton: true }), + ...(["top", "bottom"].includes(children.placement.getView()) + ? [children.drawerHeight.propertyView({ + label: trans("drawer.height"), + tooltip: trans("drawer.heightTooltip"), + placeholder: DEFAULT_SIZE + "", + })] + : [children.drawerWidth.propertyView({ + label: trans("drawer.width"), + tooltip: trans("drawer.widthTooltip"), + placeholder: DEFAULT_SIZE + "", + })]), + children.hamburgerIcon.propertyView({ label: "Menu Icon" }), + children.drawerCloseIcon.propertyView({ label: "Close Icon" }), children.shadowOverlay.propertyView({ label: "Shadow Overlay" }), ]; const bar = [ ...common, + children.orientation.propertyView({ label: "Orientation", radioButton: true }), children.horizontalAlignment.propertyView({ label: trans("navigation.horizontalAlignment"), radioButton: true, @@ -263,21 +286,26 @@ function renderAdvancedSection(children: any) { } function renderStyleSections(children: any) { + const isHamburger = children.displayMode.getView() === 'hamburger'; return ( <> -
- {children.style.getPropertyView()} -
-
- {children.navItemStyle.getPropertyView()} -
-
- {children.navItemHoverStyle.getPropertyView()} -
-
- {children.navItemActiveStyle.getPropertyView()} -
- {children.displayMode.getView() === 'hamburger' && ( + {!isHamburger && ( + <> +
+ {children.style.getPropertyView()} +
+
+ {children.navItemStyle.getPropertyView()} +
+
+ {children.navItemHoverStyle.getPropertyView()} +
+
+ {children.navItemActiveStyle.getPropertyView()} +
+ + )} + {isHamburger && ( <>
{children.hamburgerButtonStyle.getPropertyView()} @@ -311,6 +339,10 @@ const childrenMap = { ], "right"), hamburgerSize: withDefault(StringControl, "56px"), placement: PositionControl, + drawerWidth: StringControl, + drawerHeight: StringControl, + hamburgerIcon: withDefault(IconControl, ""), + drawerCloseIcon: withDefault(IconControl, ""), shadowOverlay: withDefault(BoolCodeControl, true), horizontalAlignment: alignWithJustifyControl(), style: migrateOldData(styleControl(NavigationStyle, 'style'), fixOldStyleData), @@ -468,15 +500,18 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { $iconColor={props.hamburgerButtonStyle?.iconFill} onClick={() => setDrawerVisible(true)} > - + {hasIcon(props.hamburgerIcon) ? props.hamburgerIcon : } setDrawerVisible(false)} open={drawerVisible} mask={props.shadowOverlay} getContainer={getContainer} + width={["left", "right"].includes(props.placement as any) ? transToPxSize(props.drawerWidth || DEFAULT_SIZE) : undefined as any} + height={["top", "bottom"].includes(props.placement as any) ? transToPxSize(props.drawerHeight || DEFAULT_SIZE) : undefined as any} styles={{ body: { padding: "8px", background: props.drawerContainerStyle?.background } }} destroyOnClose > From 5c5d19ba27beb5ffab5376fdff301d51477cf87f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 10 Nov 2025 22:31:20 +0500 Subject: [PATCH 47/97] add segmented control for both modes in the navComp --- .../src/comps/comps/navComp/navComp.tsx | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index cc56e3a86..5608d7884 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -13,10 +13,12 @@ import { default as DownOutlined } from "@ant-design/icons/DownOutlined"; import { default as MenuOutlined } from "@ant-design/icons/MenuOutlined"; import { default as Dropdown } from "antd/es/dropdown"; import { default as Menu, MenuProps } from "antd/es/menu"; +import Segmented from "antd/es/segmented"; import { default as Drawer } from "antd/es/drawer"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { styleControl } from "comps/controls/styleControl"; import { IconControl } from "comps/controls/iconControl"; +import { controlItem } from "components/control"; import { AnimationStyle, AnimationStyleType, @@ -68,6 +70,13 @@ function transToPxSize(size: string | number) { return isNumeric(size) ? size + "px" : (size as string); } +type MenuItemStyleOptionValue = "normal" | "hover" | "active"; +const menuItemStyleOptions = [ + { label: "Normal", value: "normal" }, + { label: "Hover", value: "hover" }, + { label: "Active", value: "active" }, +] as const; + const NavInner = styled("div") >` // margin: 0 -16px; height: 100%; @@ -285,26 +294,28 @@ function renderAdvancedSection(children: any) { ); } -function renderStyleSections(children: any) { +function renderStyleSections(children: any, styleSegment: MenuItemStyleOptionValue, setStyleSegment: (k: MenuItemStyleOptionValue) => void) { const isHamburger = children.displayMode.getView() === 'hamburger'; return ( <> {!isHamburger && ( - <> -
- {children.style.getPropertyView()} -
-
- {children.navItemStyle.getPropertyView()} -
-
- {children.navItemHoverStyle.getPropertyView()} -
-
- {children.navItemActiveStyle.getPropertyView()} -
- +
+ {children.style.getPropertyView()} +
)} +
+ {controlItem({}, ( + setStyleSegment(k as MenuItemStyleOptionValue)} + /> + ))} + {styleSegment === "normal" && children.navItemStyle.getPropertyView()} + {styleSegment === "hover" && children.navItemHoverStyle.getPropertyView()} + {styleSegment === "active" && children.navItemActiveStyle.getPropertyView()} +
{isHamburger && ( <>
@@ -526,6 +537,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { const mode = useContext(EditorContext).editorModeStatus; const showLogic = mode === "logic" || mode === "both"; const showLayout = mode === "layout" || mode === "both"; + const [styleSegment, setStyleSegment] = useState("normal"); return ( <> @@ -533,7 +545,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { {showLogic && renderInteractionSection(children)} {showLayout && renderLayoutSection(children)} {showLogic && renderAdvancedSection(children)} - {showLayout && renderStyleSections(children)} + {showLayout && renderStyleSections(children, styleSegment, setStyleSegment)} ); }) From 2eb467519904cb12f31029eff1389901a61009a7 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 11 Nov 2025 16:28:07 +0500 Subject: [PATCH 48/97] add ability to add icon on nav items / sub menu --- .../comps/navComp/components/NavItemsControl.tsx | 3 +++ .../lowcoder/src/comps/comps/navComp/navComp.tsx | 14 ++++++++++---- .../src/comps/comps/navComp/navItemComp.tsx | 6 ++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx index ee0817b49..752685f78 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx @@ -5,6 +5,7 @@ import { dropdownControl } from "comps/controls/dropdownControl"; import { mapOptionsControl } from "comps/controls/optionsControl"; import { trans } from "i18n"; import { navListComp } from "../navItemComp"; +import { IconControl } from "comps/controls/iconControl"; import { controlItem } from "lowcoder-design"; import { menuPropertyView } from "./MenuItemList"; @@ -17,6 +18,7 @@ export function createNavItemsControl() { const NavMapOption = new MultiCompBuilder( { label: StringControl, + icon: IconControl, hidden: BoolCodeControl, disabled: BoolCodeControl, active: BoolCodeControl, @@ -27,6 +29,7 @@ export function createNavItemsControl() { .setPropertyViewFn((children) => ( <> {children.label.propertyView({ label: trans("label"), placeholder: "{{item}}" })} + {children.icon.propertyView({ label: trans("icon") })} {children.active.propertyView({ label: trans("navItemComp.active") })} {children.hidden.propertyView({ label: trans("hidden") })} {children.disabled.propertyView({ label: trans("disabled") })} diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 5608d7884..8a42aaa52 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -107,7 +107,6 @@ const Item = styled.div<{ $radius?: string; $disabled?: boolean; }>` - height: 30px; line-height: 30px; padding: ${(props) => props.$padding ? props.$padding : '0 16px'}; color: ${(props) => props.$disabled ? `${props.$color}80` : (props.$active ? props.$activeColor : props.$color)}; @@ -303,7 +302,7 @@ function renderStyleSections(children: any, styleSegment: MenuItemStyleOptionVal {children.style.getPropertyView()}
)} -
+
{controlItem({}, ( { } const label = view?.label; + const icon = hasIcon(view?.icon) ? view.icon : undefined; const active = !!view?.active; const onEvent = view?.onEvent; const disabled = !!view?.disabled; const subItems = isCompItem ? view?.items : []; - const subMenuItems: Array<{ key: string; label: any; disabled?: boolean }> = []; + const subMenuItems: Array<{ key: string; label: any; icon?: any; disabled?: boolean }> = []; const subMenuSelectedKeys: Array = []; if (Array.isArray(subItems)) { @@ -406,9 +406,11 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { } const key = originalIndex + ""; subItem.children.active.getView() && subMenuSelectedKeys.push(key); + const subIcon = hasIcon(subItem.children.icon?.getView?.()) ? subItem.children.icon.getView() : undefined; subMenuItems.push({ key: key, label: subItem.children.label.getView(), + icon: subIcon, disabled: !!subItem.children.disabled.getView(), }); }); @@ -439,6 +441,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { $disabled={disabled} onClick={() => { if (!disabled && onEvent) onEvent("click"); }} > + {icon && {icon}} {label} {Array.isArray(subItems) && subItems.length > 0 && } @@ -455,7 +458,10 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { onSubEvent && onSubEvent("click"); }} selectedKeys={subMenuSelectedKeys} - items={subMenuItems} + items={subMenuItems.map(item => ({ + ...item, + icon: item.icon || undefined, + }))} /> ); return ( diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx index e8ce0f011..6b6458094 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx @@ -8,11 +8,13 @@ import { trans } from "i18n"; import _ from "lodash"; import { fromRecord, MultiBaseComp, Node, RecordNode, RecordNodeToValue } from "lowcoder-core"; import { ReactNode } from "react"; +import { IconControl } from "comps/controls/iconControl"; const events = [clickEvent]; const childrenMap = { label: StringControl, + icon: IconControl, hidden: BoolCodeControl, disabled: BoolCodeControl, active: BoolCodeControl, @@ -29,6 +31,7 @@ const childrenMap = { type ChildrenType = { label: InstanceType; + icon: InstanceType; hidden: InstanceType; disabled: InstanceType; active: InstanceType; @@ -45,6 +48,7 @@ export class NavItemComp extends MultiBaseComp { return ( <> {this.children.label.propertyView({ label: trans("label") })} + {this.children.icon.propertyView({ label: trans("icon") })} {hiddenPropertyView(this.children)} {this.children.active.propertyView({ label: trans("navItemComp.active") })} {disabledPropertyView(this.children)} @@ -71,6 +75,7 @@ export class NavItemComp extends MultiBaseComp { exposingNode(): RecordNode { return fromRecord({ label: this.children.label.exposingNode(), + icon: this.children.icon.exposingNode(), hidden: this.children.hidden.exposingNode(), disabled: this.children.disabled.exposingNode(), active: this.children.active.exposingNode(), @@ -81,6 +86,7 @@ export class NavItemComp extends MultiBaseComp { type NavItemExposing = { label: Node; + icon: Node; hidden: Node; disabled: Node; active: Node; From 7c1837a0b12e7b72732b5a1156c8792d2cf815c5 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 11 Nov 2025 17:12:23 +0500 Subject: [PATCH 49/97] fix drawer styles customization --- .../src/comps/comps/navComp/navComp.tsx | 71 +++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 8a42aaa52..4d7fd3450 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -14,11 +14,12 @@ import { default as MenuOutlined } from "@ant-design/icons/MenuOutlined"; import { default as Dropdown } from "antd/es/dropdown"; import { default as Menu, MenuProps } from "antd/es/menu"; import Segmented from "antd/es/segmented"; -import { default as Drawer } from "antd/es/drawer"; +import { Drawer } from "lowcoder-design"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { styleControl } from "comps/controls/styleControl"; import { IconControl } from "comps/controls/iconControl"; import { controlItem } from "components/control"; +import { PreviewContainerID } from "constants/domLocators"; import { AnimationStyle, AnimationStyleType, @@ -188,6 +189,45 @@ const FloatingHamburgerButton = styled.button<{ color: ${(props) => props.$iconColor || 'inherit'}; `; +const DrawerContent = styled.div<{ + $background: string; + $padding?: string; + $borderColor?: string; + $borderWidth?: string; + $margin?: string; +}>` + background: ${(p) => p.$background}; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + padding: ${(p) => p.$padding || '12px'}; + margin: ${(p) => p.$margin || '0px'}; + box-sizing: border-box; + border: ${(p) => p.$borderWidth || '1px'} solid ${(p) => p.$borderColor || 'transparent'}; +`; + +const DrawerHeader = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; +`; + +const DrawerCloseButton = styled.button<{ + $color: string; +}>` + background: transparent; + border: none; + cursor: pointer; + color: ${(p) => p.$color}; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 16px; +`; + const logoEventHandlers = [clickEvent]; // Compatible with historical style data 2022-8-26 @@ -375,7 +415,7 @@ const childrenMap = { const NavCompBase = new UICompBuilder(childrenMap, (props) => { const [drawerVisible, setDrawerVisible] = useState(false); const getContainer = useCallback(() => - document.querySelector(`#${CanvasContainerID}`) || document.body, + document.querySelector(`#${CanvasContainerID}`) || document.querySelector(`#${PreviewContainerID}`) || document.body, [] ); const data = props.items; @@ -521,18 +561,37 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { setDrawerVisible(false)} open={drawerVisible} mask={props.shadowOverlay} + maskClosable={true} + closable={false} getContainer={getContainer} width={["left", "right"].includes(props.placement as any) ? transToPxSize(props.drawerWidth || DEFAULT_SIZE) : undefined as any} height={["top", "bottom"].includes(props.placement as any) ? transToPxSize(props.drawerHeight || DEFAULT_SIZE) : undefined as any} - styles={{ body: { padding: "8px", background: props.drawerContainerStyle?.background } }} + styles={{ body: { padding: 0 } }} destroyOnClose > - {items} + + + setDrawerVisible(false)} + > + {hasIcon(props.drawerCloseIcon) + ? props.drawerCloseIcon + : ×} + + + {items} + )} From 978c641898965659437c0044b654e47d40ddd82a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 11 Nov 2025 20:44:56 +0500 Subject: [PATCH 50/97] add styles for the submenu --- .../src/components/Dropdown.tsx | 10 -- .../src/comps/comps/navComp/navComp.tsx | 107 +++++++++++++++++- .../comps/controls/styleControlConstants.tsx | 43 +++++++ 3 files changed, 145 insertions(+), 15 deletions(-) diff --git a/client/packages/lowcoder-design/src/components/Dropdown.tsx b/client/packages/lowcoder-design/src/components/Dropdown.tsx index b2a9d2766..55bd8b830 100644 --- a/client/packages/lowcoder-design/src/components/Dropdown.tsx +++ b/client/packages/lowcoder-design/src/components/Dropdown.tsx @@ -159,16 +159,6 @@ export function Dropdown(props: DropdownProps) { const { placement = "right" } = props; const valueInfoMap = _.fromPairs(props.options.map((option) => [option.value, option])); - useEffect(() => { - const dropdownElems = document.querySelectorAll("div.ant-dropdown ul.ant-dropdown-menu"); - for (let index = 0; index < dropdownElems.length; index++) { - const element = dropdownElems[index]; - element.style.maxHeight = "300px"; - element.style.overflowY = "scroll"; - element.style.minWidth = "150px"; - element.style.paddingRight = "10px"; - } - }, []); return ( diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 4d7fd3450..e4ae7bba2 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -29,6 +29,9 @@ import { NavLayoutItemStyle, NavLayoutItemHoverStyle, NavLayoutItemActiveStyle, + NavSubMenuItemStyle, + NavSubMenuItemHoverStyle, + NavSubMenuItemActiveStyle, } from "comps/controls/styleControlConstants"; import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; @@ -152,9 +155,61 @@ const ItemList = styled.div<{ $align: string, $orientation?: string }>` justify-content: ${(props) => props.$align}; `; -const StyledMenu = styled(Menu) ` - &.ant-dropdown-menu { - min-width: 160px; +const StyledMenu = styled(Menu) < + MenuProps & { + $color: string; + $hoverColor: string; + $activeColor: string; + $bg?: string; + $hoverBg?: string; + $activeBg?: string; + $border?: string; + $hoverBorder?: string; + $activeBorder?: string; + $radius?: string; + $fontFamily?: string; + $fontStyle?: string; + $textWeight?: string; + $textSize?: string; + $padding?: string; + $margin?: string; + $textTransform?: string; + $textDecoration?: string; + } +>` + /* Base submenu item styles */ + .ant-dropdown-menu-item{ + color: ${(p) => p.$color}; + background-color: ${(p) => p.$bg || "transparent"}; + border-radius: ${(p) => p.$radius || "0px"}; + font-weight: ${(p) => p.$textWeight || 500}; + font-family: ${(p) => p.$fontFamily || "sans-serif"}; + font-style: ${(p) => p.$fontStyle || "normal"}; + font-size: ${(p) => p.$textSize || "14px"}; + text-transform: ${(p) => p.$textTransform || "none"}; + text-decoration: ${(p) => p.$textDecoration || "none"}; + padding: ${(p) => p.$padding || "0 16px"}; + margin: ${(p) => p.$margin || "0px"}; + line-height: 30px; + } + /* Hover state */ + .ant-dropdown-menu-item:hover{ + color: ${(p) => p.$hoverColor || p.$activeColor}; + background-color: ${(p) => p.$hoverBg || "transparent"}; + cursor: pointer; + } + /* Selected/active state */ + .ant-dropdown-menu-item-selected, + .ant-menu-item-selected { + color: ${(p) => p.$activeColor}; + background-color: ${(p) => p.$activeBg || p.$bg || "transparent"}; + border: ${(p) => (p.$activeBorder ? `1px solid ${p.$activeBorder}` : "1px solid transparent")}; + } + /* Disabled state */ + .ant-dropdown-menu-item-disabled, + .ant-menu-item-disabled { + opacity: 0.5; + cursor: not-allowed; } `; @@ -333,7 +388,13 @@ function renderAdvancedSection(children: any) { ); } -function renderStyleSections(children: any, styleSegment: MenuItemStyleOptionValue, setStyleSegment: (k: MenuItemStyleOptionValue) => void) { +function renderStyleSections( + children: any, + styleSegment: MenuItemStyleOptionValue, + setStyleSegment: (k: MenuItemStyleOptionValue) => void, + subStyleSegment: MenuItemStyleOptionValue, + setSubStyleSegment: (k: MenuItemStyleOptionValue) => void +) { const isHamburger = children.displayMode.getView() === 'hamburger'; return ( <> @@ -355,6 +416,19 @@ function renderStyleSections(children: any, styleSegment: MenuItemStyleOptionVal {styleSegment === "hover" && children.navItemHoverStyle.getPropertyView()} {styleSegment === "active" && children.navItemActiveStyle.getPropertyView()}
+
+ {controlItem({}, ( + setSubStyleSegment(k as MenuItemStyleOptionValue)} + /> + ))} + {subStyleSegment === "normal" && children.subNavItemStyle.getPropertyView()} + {subStyleSegment === "hover" && children.subNavItemHoverStyle.getPropertyView()} + {subStyleSegment === "active" && children.subNavItemActiveStyle.getPropertyView()} +
{isHamburger && ( <>
@@ -402,6 +476,9 @@ const childrenMap = { hamburgerButtonStyle: styleControl(HamburgerButtonStyle, 'hamburgerButtonStyle'), drawerContainerStyle: styleControl(DrawerContainerStyle, 'drawerContainerStyle'), animationStyle: styleControl(AnimationStyle, 'animationStyle'), + subNavItemStyle: styleControl(NavSubMenuItemStyle, 'subNavItemStyle'), + subNavItemHoverStyle: styleControl(NavSubMenuItemHoverStyle, 'subNavItemHoverStyle'), + subNavItemActiveStyle: styleControl(NavSubMenuItemActiveStyle, 'subNavItemActiveStyle'), items: withDefault(migrateOldData(createNavItemsControl(), fixOldItemsData), { optionType: "manual", manual: [ @@ -502,6 +579,24 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { ...item, icon: item.icon || undefined, }))} + $color={(props.subNavItemStyle && props.subNavItemStyle.text) || props.style.text} + $hoverColor={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.text) || props.style.accent} + $activeColor={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.text) || props.style.accent} + $bg={(props.subNavItemStyle && props.subNavItemStyle.background) || undefined} + $hoverBg={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.background) || undefined} + $activeBg={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.background) || undefined} + $border={(props.subNavItemStyle && props.subNavItemStyle.border) || undefined} + $hoverBorder={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.border) || undefined} + $activeBorder={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.border) || undefined} + $radius={(props.subNavItemStyle && props.subNavItemStyle.radius) || undefined} + $fontFamily={props.style.fontFamily} + $fontStyle={props.style.fontStyle} + $textWeight={props.style.textWeight} + $textSize={props.style.textSize} + $padding={(props.subNavItemStyle && props.subNavItemStyle.padding) || props.style.padding} + $margin={(props.subNavItemStyle && props.subNavItemStyle.margin) || props.style.margin} + $textTransform={props.style.textTransform} + $textDecoration={props.style.textDecoration} /> ); return ( @@ -509,6 +604,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { key={idx} popupRender={() => subMenu} disabled={disabled} + trigger={["click"]} > {item} @@ -603,6 +699,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { const showLogic = mode === "logic" || mode === "both"; const showLayout = mode === "layout" || mode === "both"; const [styleSegment, setStyleSegment] = useState("normal"); + const [subStyleSegment, setSubStyleSegment] = useState("normal"); return ( <> @@ -610,7 +707,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { {showLogic && renderInteractionSection(children)} {showLayout && renderLayoutSection(children)} {showLogic && renderAdvancedSection(children)} - {showLayout && renderStyleSections(children, styleSegment, setStyleSegment)} + {showLayout && renderStyleSections(children, styleSegment, setStyleSegment, subStyleSegment, setSubStyleSegment)} ); }) diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 175448bf3..dc187461b 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -2386,6 +2386,46 @@ export const NavLayoutItemActiveStyle = [ }, ] as const; +// Submenu item styles (normal/hover/active), similar to top-level menu items +export const NavSubMenuItemStyle = [ + getBackground("primarySurface"), + getStaticBorder("transparent"), + RADIUS, + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, + MARGIN, + PADDING, +] as const; + +export const NavSubMenuItemHoverStyle = [ + getBackground("canvas"), + getStaticBorder("transparent"), + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, +] as const; + +export const NavSubMenuItemActiveStyle = [ + getBackground("primary"), + getStaticBorder("transparent"), + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, +] as const; + export const CarouselStyle = [getBackground("canvas")] as const; export const RichTextEditorStyle = [ @@ -2525,6 +2565,9 @@ export type NavLayoutItemHoverStyleType = StyleConfigType< export type NavLayoutItemActiveStyleType = StyleConfigType< typeof NavLayoutItemActiveStyle >; +export type NavSubMenuItemStyleType = StyleConfigType; +export type NavSubMenuItemHoverStyleType = StyleConfigType; +export type NavSubMenuItemActiveStyleType = StyleConfigType; export function widthCalculator(margin: string) { const marginArr = margin?.trim().replace(/\s+/g, " ").split(" ") || ""; From 228bd0c389fa31bc3140529c1ab0adf7ff42d03a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 12 Nov 2025 23:34:42 +0500 Subject: [PATCH 51/97] fix submenu style issues --- .../src/comps/comps/navComp/navComp.tsx | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index e4ae7bba2..d4ecd3f19 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -14,7 +14,7 @@ import { default as MenuOutlined } from "@ant-design/icons/MenuOutlined"; import { default as Dropdown } from "antd/es/dropdown"; import { default as Menu, MenuProps } from "antd/es/menu"; import Segmented from "antd/es/segmented"; -import { Drawer } from "lowcoder-design"; +import { Drawer, ScrollBar } from "lowcoder-design"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { styleControl } from "comps/controls/styleControl"; import { IconControl } from "comps/controls/iconControl"; @@ -195,7 +195,7 @@ const StyledMenu = styled(Menu) < /* Hover state */ .ant-dropdown-menu-item:hover{ color: ${(p) => p.$hoverColor || p.$activeColor}; - background-color: ${(p) => p.$hoverBg || "transparent"}; + background-color: ${(p) => p.$hoverBg || "transparent"} !important; cursor: pointer; } /* Selected/active state */ @@ -565,39 +565,41 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { ); if (subMenuItems.length > 0) { const subMenu = ( - { - if (disabled) return; - const subItem = subItems[Number(e.key)]; - const isSubDisabled = !!subItem?.children?.disabled?.getView?.(); - if (isSubDisabled) return; - const onSubEvent = subItem?.getView()?.onEvent; - onSubEvent && onSubEvent("click"); - }} - selectedKeys={subMenuSelectedKeys} - items={subMenuItems.map(item => ({ - ...item, - icon: item.icon || undefined, - }))} - $color={(props.subNavItemStyle && props.subNavItemStyle.text) || props.style.text} - $hoverColor={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.text) || props.style.accent} - $activeColor={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.text) || props.style.accent} - $bg={(props.subNavItemStyle && props.subNavItemStyle.background) || undefined} - $hoverBg={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.background) || undefined} - $activeBg={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.background) || undefined} - $border={(props.subNavItemStyle && props.subNavItemStyle.border) || undefined} - $hoverBorder={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.border) || undefined} - $activeBorder={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.border) || undefined} - $radius={(props.subNavItemStyle && props.subNavItemStyle.radius) || undefined} - $fontFamily={props.style.fontFamily} - $fontStyle={props.style.fontStyle} - $textWeight={props.style.textWeight} - $textSize={props.style.textSize} - $padding={(props.subNavItemStyle && props.subNavItemStyle.padding) || props.style.padding} - $margin={(props.subNavItemStyle && props.subNavItemStyle.margin) || props.style.margin} - $textTransform={props.style.textTransform} - $textDecoration={props.style.textDecoration} - /> + + { + if (disabled) return; + const subItem = subItems[Number(e.key)]; + const isSubDisabled = !!subItem?.children?.disabled?.getView?.(); + if (isSubDisabled) return; + const onSubEvent = subItem?.getView()?.onEvent; + onSubEvent && onSubEvent("click"); + }} + selectedKeys={subMenuSelectedKeys} + items={subMenuItems.map(item => ({ + ...item, + icon: item.icon || undefined, + }))} + $color={(props.subNavItemStyle && props.subNavItemStyle.text) || props.style.text} + $hoverColor={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.text) || props.style.accent} + $activeColor={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.text) || props.style.accent} + $bg={(props.subNavItemStyle && props.subNavItemStyle.background) || undefined} + $hoverBg={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.background) || undefined} + $activeBg={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.background) || undefined} + $border={(props.subNavItemStyle && props.subNavItemStyle.border) || undefined} + $hoverBorder={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.border) || undefined} + $activeBorder={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.border) || undefined} + $radius={(props.subNavItemStyle && props.subNavItemStyle.radius) || undefined} + $fontFamily={props.style.fontFamily} + $fontStyle={props.style.fontStyle} + $textWeight={props.style.textWeight} + $textSize={props.style.textSize} + $padding={(props.subNavItemStyle && props.subNavItemStyle.padding) || props.style.padding} + $margin={(props.subNavItemStyle && props.subNavItemStyle.margin) || props.style.margin} + $textTransform={props.style.textTransform} + $textDecoration={props.style.textDecoration} + /> + ); return ( Date: Wed, 12 Nov 2025 23:37:55 +0500 Subject: [PATCH 52/97] remove trigger --- client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index d4ecd3f19..940e0110d 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -565,7 +565,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { ); if (subMenuItems.length > 0) { const subMenu = ( - + { if (disabled) return; @@ -606,7 +606,6 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { key={idx} popupRender={() => subMenu} disabled={disabled} - trigger={["click"]} > {item} From 571122d4917de6f549009c1fadae154c93143685 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 13 Nov 2025 19:46:42 +0500 Subject: [PATCH 53/97] add desktop nav positions --- .../src/comps/comps/layout/navLayout.tsx | 35 +++++++++++++------ .../comps/comps/layout/navLayoutConstants.ts | 15 ++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 2f07839ca..ad13d415f 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -36,14 +36,14 @@ import history from "util/history"; import { DataOption, DataOptionType, - ModeOptions, jsonMenuItems, menuItemStyleOptions } from "./navLayoutConstants"; import { clickEvent, eventHandlerControl } from "@lowcoder-ee/comps/controls/eventHandlerControl"; import { childrenToProps } from "@lowcoder-ee/comps/generators/multi"; +import { NavPosition, NavPositionOptions } from "./navLayoutConstants"; -const { Header } = Layout; +const { Header, Footer } = Layout; const DEFAULT_WIDTH = 240; type MenuItemStyleOptionValue = "normal" | "hover" | "active"; @@ -197,7 +197,7 @@ let NavTmpLayout = (function () { jsonItems: jsonControl(convertTreeData, jsonMenuItems), width: withDefault(StringControl, DEFAULT_WIDTH), backgroundImage: withDefault(StringControl, ""), - mode: dropdownControl(ModeOptions, "inline"), + position: dropdownControl(NavPositionOptions, NavPosition.Left), collapse: BoolCodeControl, navStyle: styleControl(NavLayoutStyle, 'navStyle'), navItemStyle: styleControl(NavLayoutItemStyle, 'navItemStyle'), @@ -234,7 +234,7 @@ let NavTmpLayout = (function () { tooltip: trans("navLayout.widthTooltip"), placeholder: DEFAULT_WIDTH + "", })} - { children.mode.propertyView({ + { children.position.propertyView({ label: trans("labelProp.position"), radioButton: true })} @@ -280,7 +280,7 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const [selectedKey, setSelectedKey] = useState(""); const items = comp.children.items.getView(); const navWidth = comp.children.width.getView(); - const navMode = comp.children.mode.getView(); + const navPosition = comp.children.position.getView(); const navCollapse = comp.children.collapse.getView(); const navStyle = comp.children.navStyle.getView(); const navItemStyle = comp.children.navItemStyle.getView(); @@ -568,12 +568,14 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { let navMenu = ( { defaultOpenKeys={defaultOpenKeys} selectedKeys={[selectedKey]} $navItemStyle={{ - width: navMode === 'horizontal' ? 'auto' : `calc(100% - ${getHorizontalMargin(navItemStyle.margin.split(' '))})`, + width: (navPosition === 'top' || navPosition === 'bottom') ? 'auto' : `calc(100% - ${getHorizontalMargin(navItemStyle.margin.split(' '))})`, ...navItemStyle, }} $navItemHoverStyle={navItemHoverStyle} @@ -595,16 +597,27 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { let content = ( - {navMode === 'horizontal' ? ( + {(navPosition === 'top') && (
{ navMenu }
- ) : ( + )} + {(navPosition === 'left') && ( {navMenu} )} {pageView} + {(navPosition === 'bottom') && ( +
+ { navMenu } +
+ )} + {(navPosition === 'right') && ( + + {navMenu} + + )}
); return isViewMode ? ( diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts index aa33423d0..f9a2dc456 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts @@ -6,6 +6,21 @@ export const ModeOptions = [ { label: trans("navLayout.modeHorizontal"), value: "horizontal" }, ] as const; +// Desktop navigation position +export const NavPosition = { + Top: "top", + Left: "left", + Bottom: "bottom", + Right: "right", +} as const; + +export const NavPositionOptions = [ + { label: "Top", value: NavPosition.Top }, + { label: "Left", value: NavPosition.Left }, + { label: "Bottom", value: NavPosition.Bottom }, + { label: "Right", value: NavPosition.Right }, +] as const; + // Mobile navigation specific modes and options export const MobileMode = { Vertical: "vertical", From 288fe92a92e51c2fc9ae07024bdcd6611727f18b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 14 Nov 2025 18:19:15 +0500 Subject: [PATCH 54/97] refactor navlayout --- .../src/comps/comps/layout/navLayout.tsx | 152 ++++++++++-------- 1 file changed, 87 insertions(+), 65 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index ad13d415f..e9ad7207a 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -148,12 +148,6 @@ const StyledImage = styled.img` color: currentColor; `; -const defaultStyle = { - radius: '0px', - margin: '0px', - padding: '0px', -} - type UrlActionType = { url?: string; newTab?: boolean; @@ -163,7 +157,7 @@ export type MenuItemNode = { label: string; key: string; hidden?: boolean; - icon?: any; + icon?: string; action?: UrlActionType, children?: MenuItemNode[]; } @@ -208,66 +202,94 @@ let NavTmpLayout = (function () { return null; }) .setPropertyViewFn((children) => { - const [styleSegment, setStyleSegment] = useState('normal') + const [styleSegment, setStyleSegment] = useState("normal"); + + const { + dataOptionType, + items, + jsonItems, + onEvent, + width, + position, + collapse, + backgroundImage, + navStyle, + navItemStyle, + navItemHoverStyle, + navItemActiveStyle, + } = children; + + const renderMenuSection = () => ( +
+ {dataOptionType.propertyView({ + radioButton: true, + type: "oneline", + })} + {dataOptionType.getView() === DataOption.Manual + ? menuPropertyView(items) + : jsonItems.propertyView({ + label: "Json Data", + })} +
+ ); + + const renderEventHandlerSection = () => ( +
+ {onEvent.getPropertyView()} +
+ ); + + const renderLayoutSection = () => ( +
+ {width.propertyView({ + label: trans("navLayout.width"), + tooltip: trans("navLayout.widthTooltip"), + placeholder: `${DEFAULT_WIDTH}`, + })} + {position.propertyView({ + label: trans("labelProp.position"), + radioButton: true, + })} + {collapse.propertyView({ + label: trans("labelProp.collapse"), + })} + {backgroundImage.propertyView({ + label: "Background Image", + placeholder: "https://temp.im/350x400", + })} +
+ ); + + const renderNavStyleSection = () => ( +
+ {navStyle.getPropertyView()} +
+ ); + + const renderNavItemStyleSection = () => ( +
+ {controlItem( + {}, + setStyleSegment(k as MenuItemStyleOptionValue)} + /> + )} + {styleSegment === "normal" && navItemStyle.getPropertyView()} + {styleSegment === "hover" && navItemHoverStyle.getPropertyView()} + {styleSegment === "active" && navItemActiveStyle.getPropertyView()} +
+ ); return ( -
-
- {children.dataOptionType.propertyView({ - radioButton: true, - type: "oneline", - })} - { - children.dataOptionType.getView() === DataOption.Manual - ? menuPropertyView(children.items) - : children.jsonItems.propertyView({ - label: "Json Data", - }) - } -
-
- { children.onEvent.getPropertyView() } -
-
- { children.width.propertyView({ - label: trans("navLayout.width"), - tooltip: trans("navLayout.widthTooltip"), - placeholder: DEFAULT_WIDTH + "", - })} - { children.position.propertyView({ - label: trans("labelProp.position"), - radioButton: true - })} - { children.collapse.propertyView({ - label: trans("labelProp.collapse"), - })} - {children.backgroundImage.propertyView({ - label: `Background Image`, - placeholder: 'https://temp.im/350x400', - })} -
-
- { children.navStyle.getPropertyView() } -
-
- {controlItem({}, ( - setStyleSegment(k as MenuItemStyleOptionValue)} - /> - ))} - {styleSegment === 'normal' && ( - children.navItemStyle.getPropertyView() - )} - {styleSegment === 'hover' && ( - children.navItemHoverStyle.getPropertyView() - )} - {styleSegment === 'active' && ( - children.navItemActiveStyle.getPropertyView() - )} -
+
+ {renderMenuSection()} + {renderEventHandlerSection()} + {renderLayoutSection()} + {renderNavStyleSection()} + {renderNavItemStyleSection()}
); }) From 0c1c6904ce9bf5e8eb6dd09fe5bfb58a5ab95583 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 14 Nov 2025 22:18:48 +0500 Subject: [PATCH 55/97] fix center icons on collapse + mount issue --- .../src/comps/comps/layout/navLayout.tsx | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index e9ad7207a..b94fe8965 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -17,7 +17,8 @@ import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { useCallback, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { isUserViewMode, useAppPathParam } from "util/hooks"; -import { BoolCodeControl, StringControl, jsonControl } from "comps/controls/codeControl"; +import { StringControl, jsonControl } from "comps/controls/codeControl"; +import { BoolControl } from "comps/controls/boolControl"; import { styleControl } from "comps/controls/styleControl"; import { NavLayoutStyle, @@ -40,7 +41,6 @@ import { menuItemStyleOptions } from "./navLayoutConstants"; import { clickEvent, eventHandlerControl } from "@lowcoder-ee/comps/controls/eventHandlerControl"; -import { childrenToProps } from "@lowcoder-ee/comps/generators/multi"; import { NavPosition, NavPositionOptions } from "./navLayoutConstants"; const { Header, Footer } = Layout; @@ -141,6 +141,24 @@ const StyledMenu = styled(AntdMenu)<{ } } + /* Collapse mode: hide label text and center icons */ + &.ant-menu-inline-collapsed { + .ant-menu-title-content { + display: none !important; + } + + > .ant-menu-item, + > .ant-menu-submenu > .ant-menu-submenu-title { + display: flex; + justify-content: center; + align-items: center; + } + + .anticon { + line-height: 1 !important; + } + } + `; const StyledImage = styled.img` @@ -192,7 +210,7 @@ let NavTmpLayout = (function () { width: withDefault(StringControl, DEFAULT_WIDTH), backgroundImage: withDefault(StringControl, ""), position: dropdownControl(NavPositionOptions, NavPosition.Left), - collapse: BoolCodeControl, + collapse: BoolControl, navStyle: styleControl(NavLayoutStyle, 'navStyle'), navItemStyle: styleControl(NavLayoutItemStyle, 'navItemStyle'), navItemHoverStyle: styleControl(NavLayoutItemHoverStyle, 'navItemHoverStyle'), @@ -672,7 +690,7 @@ NavTmpLayout = withDispatchHook(NavTmpLayout, (dispatch) => (action) => { }); }); -export const NavLayout = class extends NavTmpLayout { +export class NavLayout extends NavTmpLayout { getAllCompItems() { return {}; } @@ -680,5 +698,5 @@ export const NavLayout = class extends NavTmpLayout { nameAndExposingInfo(): NameAndExposingInfo { return {}; } -}; +} registerLayoutMap({ compType: navLayoutCompType, comp: NavLayout }); From a4541f3e478416b5bb2e0183d16fd134dc631f21 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 17 Nov 2025 22:37:24 +0500 Subject: [PATCH 56/97] [Fix]: input blur event --- .../src/comps/comps/textInputComp/textInputConstants.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 0b6ca8f2e..3b03a2ed4 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -234,7 +234,9 @@ export const useTextInputProps = (props: RecordConstructorToView { + debouncedOnChangeRef.current.flush?.(); touchRef.current = false; + propsRef.current.onEvent("blur"); }; // Cleanup refs on unmount From 8086c26bd43f7adc8cb92fd946cc006624a43767 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 27 Nov 2025 23:27:46 +0500 Subject: [PATCH 57/97] [Feat]: extend nav item styles --- .../comps/comps/layout/mobileTabLayout.tsx | 43 ++++ .../src/comps/comps/layout/navLayout.tsx | 40 +++- .../src/comps/comps/navComp/navComp.tsx | 205 +++++++++++------- .../comps/controls/styleControlConstants.tsx | 66 +++--- 4 files changed, 243 insertions(+), 111 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index dfe9539af..8ae653ffa 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -183,6 +183,11 @@ const DrawerList = styled.div<{ gap: 8px; background-color: ${(p) => p.$itemStyle.background}; color: ${(p) => p.$itemStyle.text}; + font-size: ${(p) => p.$itemStyle.textSize}; + font-family: ${(p) => p.$itemStyle.fontFamily}; + font-style: ${(p) => p.$itemStyle.fontStyle}; + font-weight: ${(p) => p.$itemStyle.textWeight}; + text-decoration: ${(p) => p.$itemStyle.textDecoration}; border-radius: ${(p) => p.$itemStyle.radius}; border: 1px solid ${(p) => p.$itemStyle.border}; margin: ${(p) => p.$itemStyle.margin}; @@ -194,11 +199,21 @@ const DrawerList = styled.div<{ background-color: ${(p) => p.$hoverStyle.background}; color: ${(p) => p.$hoverStyle.text}; border: 1px solid ${(p) => p.$hoverStyle.border}; + font-size: ${(p) => p.$hoverStyle.textSize || p.$itemStyle.textSize}; + font-family: ${(p) => p.$hoverStyle.fontFamily || p.$itemStyle.fontFamily}; + font-style: ${(p) => p.$hoverStyle.fontStyle || p.$itemStyle.fontStyle}; + font-weight: ${(p) => p.$hoverStyle.textWeight || p.$itemStyle.textWeight}; + text-decoration: ${(p) => p.$hoverStyle.textDecoration || p.$itemStyle.textDecoration}; } .drawer-item.active { background-color: ${(p) => p.$activeStyle.background}; color: ${(p) => p.$activeStyle.text}; border: 1px solid ${(p) => p.$activeStyle.border}; + font-size: ${(p) => p.$activeStyle.textSize || p.$itemStyle.textSize}; + font-family: ${(p) => p.$activeStyle.fontFamily || p.$itemStyle.fontFamily}; + font-style: ${(p) => p.$activeStyle.fontStyle || p.$itemStyle.fontStyle}; + font-weight: ${(p) => p.$activeStyle.textWeight || p.$itemStyle.textWeight}; + text-decoration: ${(p) => p.$activeStyle.textDecoration || p.$itemStyle.textDecoration}; } `; @@ -260,16 +275,37 @@ const StyledTabBar = styled(TabBar)<{ .adm-tab-bar-item { background-color: ${(props) => props.$tabItemStyle?.background}; color: ${(props) => props.$tabItemStyle?.text}; + font-size: ${(props) => props.$tabItemStyle?.textSize}; + font-family: ${(props) => props.$tabItemStyle?.fontFamily}; + font-style: ${(props) => props.$tabItemStyle?.fontStyle}; + font-weight: ${(props) => props.$tabItemStyle?.textWeight}; + text-decoration: ${(props) => props.$tabItemStyle?.textDecoration}; border-radius: ${(props) => props.$tabItemStyle?.radius} !important; border: ${(props) => `1px solid ${props.$tabItemStyle?.border}`}; margin: ${(props) => props.$tabItemStyle?.margin}; padding: ${(props) => props.$tabItemStyle?.padding}; + + .adm-tab-bar-item-title { + font-size: ${(props) => props.$tabItemStyle?.textSize}; + font-family: ${(props) => props.$tabItemStyle?.fontFamily}; + font-style: ${(props) => props.$tabItemStyle?.fontStyle}; + font-weight: ${(props) => props.$tabItemStyle?.textWeight}; + text-decoration: ${(props) => props.$tabItemStyle?.textDecoration}; + } } .adm-tab-bar-item:hover { background-color: ${(props) => props.$tabItemHoverStyle?.background} !important; color: ${(props) => props.$tabItemHoverStyle?.text} !important; border: ${(props) => `1px solid ${props.$tabItemHoverStyle?.border}`}; + + .adm-tab-bar-item-title { + font-size: ${(props) => props.$tabItemHoverStyle?.textSize || props.$tabItemStyle?.textSize}; + font-family: ${(props) => props.$tabItemHoverStyle?.fontFamily || props.$tabItemStyle?.fontFamily}; + font-style: ${(props) => props.$tabItemHoverStyle?.fontStyle || props.$tabItemStyle?.fontStyle}; + font-weight: ${(props) => props.$tabItemHoverStyle?.textWeight || props.$tabItemStyle?.textWeight}; + text-decoration: ${(props) => props.$tabItemHoverStyle?.textDecoration || props.$tabItemStyle?.textDecoration}; + } } .adm-tab-bar-item.adm-tab-bar-item-active { @@ -278,6 +314,13 @@ const StyledTabBar = styled(TabBar)<{ .adm-tab-bar-item-icon, .adm-tab-bar-item-title { color: ${(props) => props.$tabItemActiveStyle.text}; } + .adm-tab-bar-item-title { + font-size: ${(props) => props.$tabItemActiveStyle?.textSize || props.$tabItemStyle?.textSize}; + font-family: ${(props) => props.$tabItemActiveStyle?.fontFamily || props.$tabItemStyle?.fontFamily}; + font-style: ${(props) => props.$tabItemActiveStyle?.fontStyle || props.$tabItemStyle?.fontStyle}; + font-weight: ${(props) => props.$tabItemActiveStyle?.textWeight || props.$tabItemStyle?.textWeight}; + text-decoration: ${(props) => props.$tabItemActiveStyle?.textDecoration || props.$tabItemStyle?.textDecoration}; + } } `; diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index b94fe8965..3540e1120 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -87,18 +87,42 @@ const StyledMenu = styled(AntdMenu)<{ border: ${(props) => `1px solid ${props.$navItemStyle?.border}`}; margin: ${(props) => props.$navItemStyle?.margin}; padding: ${(props) => props.$navItemStyle?.padding}; + } + .ant-menu-title-content { + font-size: ${(props) => props.$navItemStyle?.textSize}; + font-family: ${(props) => props.$navItemStyle?.fontFamily}; + font-style: ${(props) => props.$navItemStyle?.fontStyle}; + font-weight: ${(props) => props.$navItemStyle?.textWeight}; + text-decoration: ${(props) => props.$navItemStyle?.textDecoration}; } + .ant-menu-item-active { background-color: ${(props) => props.$navItemHoverStyle?.background} !important; color: ${(props) => props.$navItemHoverStyle?.text} !important; border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`}; + + .ant-menu-title-content { + font-size: ${(props) => props.$navItemHoverStyle?.textSize || props.$navItemStyle?.textSize}; + font-family: ${(props) => props.$navItemHoverStyle?.fontFamily || props.$navItemStyle?.fontFamily}; + font-style: ${(props) => props.$navItemHoverStyle?.fontStyle || props.$navItemStyle?.fontStyle}; + font-weight: ${(props) => props.$navItemHoverStyle?.textWeight || props.$navItemStyle?.textWeight}; + text-decoration: ${(props) => props.$navItemHoverStyle?.textDecoration || props.$navItemStyle?.textDecoration}; + } } .ant-menu-item-selected { background-color: ${(props) => props.$navItemActiveStyle?.background} !important; color: ${(props) => props.$navItemActiveStyle?.text} !important; border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`}; + + .ant-menu-title-content { + font-size: ${(props) => props.$navItemActiveStyle?.textSize || props.$navItemStyle?.textSize}; + font-family: ${(props) => props.$navItemActiveStyle?.fontFamily || props.$navItemStyle?.fontFamily}; + font-style: ${(props) => props.$navItemActiveStyle?.fontStyle || props.$navItemStyle?.fontStyle}; + font-weight: ${(props) => props.$navItemActiveStyle?.textWeight || props.$navItemStyle?.textWeight}; + text-decoration: ${(props) => props.$navItemActiveStyle?.textDecoration || props.$navItemStyle?.textDecoration}; + } } .ant-menu-submenu { @@ -112,11 +136,15 @@ const StyledMenu = styled(AntdMenu)<{ max-height: 100%; background-color: ${(props) => props.$navItemStyle?.background}; color: ${(props) => props.$navItemStyle?.text}; + font-size: ${(props) => props.$navItemStyle?.textSize}; + font-family: ${(props) => props.$navItemStyle?.fontFamily}; + font-style: ${(props) => props.$navItemStyle?.fontStyle}; + font-weight: ${(props) => props.$navItemStyle?.textWeight}; + text-decoration: ${(props) => props.$navItemStyle?.textDecoration}; border-radius: ${(props) => props.$navItemStyle?.radius} !important; border: ${(props) => `1px solid ${props.$navItemStyle?.border}`}; margin: 0; padding: ${(props) => props.$navItemStyle?.padding}; - } .ant-menu-item { @@ -129,6 +157,11 @@ const StyledMenu = styled(AntdMenu)<{ background-color: ${(props) => props.$navItemHoverStyle?.background} !important; color: ${(props) => props.$navItemHoverStyle?.text} !important; border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`}; + font-size: ${(props) => props.$navItemHoverStyle?.textSize || props.$navItemStyle?.textSize}; + font-family: ${(props) => props.$navItemHoverStyle?.fontFamily || props.$navItemStyle?.fontFamily}; + font-style: ${(props) => props.$navItemHoverStyle?.fontStyle || props.$navItemStyle?.fontStyle}; + font-weight: ${(props) => props.$navItemHoverStyle?.textWeight || props.$navItemStyle?.textWeight}; + text-decoration: ${(props) => props.$navItemHoverStyle?.textDecoration || props.$navItemStyle?.textDecoration}; } } &.ant-menu-submenu-selected { @@ -137,6 +170,11 @@ const StyledMenu = styled(AntdMenu)<{ background-color: ${(props) => props.$navItemActiveStyle?.background} !important; color: ${(props) => props.$navItemActiveStyle?.text} !important; border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`}; + font-size: ${(props) => props.$navItemActiveStyle?.textSize || props.$navItemStyle?.textSize}; + font-family: ${(props) => props.$navItemActiveStyle?.fontFamily || props.$navItemStyle?.fontFamily}; + font-style: ${(props) => props.$navItemActiveStyle?.fontStyle || props.$navItemStyle?.fontStyle}; + font-weight: ${(props) => props.$navItemActiveStyle?.textWeight || props.$navItemStyle?.textWeight}; + text-decoration: ${(props) => props.$navItemActiveStyle?.textDecoration || props.$navItemStyle?.textDecoration}; } } } diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 940e0110d..f852fe694 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -29,9 +29,6 @@ import { NavLayoutItemStyle, NavLayoutItemHoverStyle, NavLayoutItemActiveStyle, - NavSubMenuItemStyle, - NavSubMenuItemHoverStyle, - NavSubMenuItemActiveStyle, } from "comps/controls/styleControlConstants"; import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; @@ -98,10 +95,19 @@ const Item = styled.div<{ $fontStyle: string; $textWeight: string; $textSize: string; + $textDecoration: string; + $hoverFontFamily?: string; + $hoverFontStyle?: string; + $hoverTextWeight?: string; + $hoverTextSize?: string; + $hoverTextDecoration?: string; + $activeFontFamily?: string; + $activeFontStyle?: string; + $activeTextWeight?: string; + $activeTextSize?: string; + $activeTextDecoration?: string; $margin: string; $padding: string; - $textTransform:string; - $textDecoration:string; $bg?: string; $hoverBg?: string; $activeBg?: string; @@ -112,24 +118,40 @@ const Item = styled.div<{ $disabled?: boolean; }>` line-height: 30px; - padding: ${(props) => props.$padding ? props.$padding : '0 16px'}; + padding: ${(props) => props.$padding || '0 16px'}; color: ${(props) => props.$disabled ? `${props.$color}80` : (props.$active ? props.$activeColor : props.$color)}; background-color: ${(props) => (props.$active ? (props.$activeBg || 'transparent') : (props.$bg || 'transparent'))}; - border: ${(props) => props.$border ? `1px solid ${props.$border}` : '1px solid transparent'}; - border-radius: ${(props) => props.$radius ? props.$radius : '0px'}; - font-weight: ${(props) => (props.$textWeight ? props.$textWeight : 500)}; - font-family:${(props) => (props.$fontFamily ? props.$fontFamily : 'sans-serif')}; - font-style:${(props) => (props.$fontStyle ? props.$fontStyle : 'normal')}; - font-size:${(props) => (props.$textSize ? props.$textSize : '14px')}; - text-transform:${(props) => (props.$textTransform ? props.$textTransform : '')}; - text-decoration:${(props) => (props.$textDecoration ? props.$textDecoration : '')}; - margin:${(props) => props.$margin ? props.$margin : '0px'}; + border: ${(props) => props.$active + ? (props.$activeBorder ? `1px solid ${props.$activeBorder}` : (props.$border ? `1px solid ${props.$border}` : '1px solid transparent')) + : (props.$border ? `1px solid ${props.$border}` : '1px solid transparent')}; + border-radius: ${(props) => props.$radius || '0px'}; + font-weight: ${(props) => props.$active + ? (props.$activeTextWeight || props.$textWeight || 500) + : (props.$textWeight || 500)}; + font-family: ${(props) => props.$active + ? (props.$activeFontFamily || props.$fontFamily || 'sans-serif') + : (props.$fontFamily || 'sans-serif')}; + font-style: ${(props) => props.$active + ? (props.$activeFontStyle || props.$fontStyle || 'normal') + : (props.$fontStyle || 'normal')}; + font-size: ${(props) => props.$active + ? (props.$activeTextSize || props.$textSize || '14px') + : (props.$textSize || '14px')}; + text-decoration: ${(props) => props.$active + ? (props.$activeTextDecoration || props.$textDecoration || 'none') + : (props.$textDecoration || 'none')}; + margin: ${(props) => props.$margin || '0px'}; &:hover { color: ${(props) => props.$disabled ? (props.$active ? props.$activeColor : props.$color) : (props.$hoverColor || props.$activeColor)}; background-color: ${(props) => props.$disabled ? (props.$active ? (props.$activeBg || 'transparent') : (props.$bg || 'transparent')) : (props.$hoverBg || props.$activeBg || props.$bg || 'transparent')}; border: ${(props) => props.$hoverBorder ? `1px solid ${props.$hoverBorder}` : (props.$activeBorder ? `1px solid ${props.$activeBorder}` : (props.$border ? `1px solid ${props.$border}` : '1px solid transparent'))}; cursor: ${(props) => props.$disabled ? 'not-allowed' : 'pointer'}; + font-weight: ${(props) => props.$disabled ? undefined : (props.$hoverTextWeight || props.$textWeight || 500)}; + font-family: ${(props) => props.$disabled ? undefined : (props.$hoverFontFamily || props.$fontFamily || 'sans-serif')}; + font-style: ${(props) => props.$disabled ? undefined : (props.$hoverFontStyle || props.$fontStyle || 'normal')}; + font-size: ${(props) => props.$disabled ? undefined : (props.$hoverTextSize || props.$textSize || '14px')}; + text-decoration: ${(props) => props.$disabled ? undefined : (props.$hoverTextDecoration || props.$textDecoration || 'none')}; } .anticon { @@ -171,31 +193,46 @@ const StyledMenu = styled(Menu) < $fontStyle?: string; $textWeight?: string; $textSize?: string; + $textDecoration?: string; + $hoverFontFamily?: string; + $hoverFontStyle?: string; + $hoverTextWeight?: string; + $hoverTextSize?: string; + $hoverTextDecoration?: string; + $activeFontFamily?: string; + $activeFontStyle?: string; + $activeTextWeight?: string; + $activeTextSize?: string; + $activeTextDecoration?: string; $padding?: string; $margin?: string; - $textTransform?: string; - $textDecoration?: string; } >` /* Base submenu item styles */ - .ant-dropdown-menu-item{ + .ant-dropdown-menu-item { color: ${(p) => p.$color}; background-color: ${(p) => p.$bg || "transparent"}; border-radius: ${(p) => p.$radius || "0px"}; + border: ${(p) => p.$border ? `1px solid ${p.$border}` : "1px solid transparent"}; font-weight: ${(p) => p.$textWeight || 500}; font-family: ${(p) => p.$fontFamily || "sans-serif"}; font-style: ${(p) => p.$fontStyle || "normal"}; font-size: ${(p) => p.$textSize || "14px"}; - text-transform: ${(p) => p.$textTransform || "none"}; text-decoration: ${(p) => p.$textDecoration || "none"}; padding: ${(p) => p.$padding || "0 16px"}; margin: ${(p) => p.$margin || "0px"}; line-height: 30px; } /* Hover state */ - .ant-dropdown-menu-item:hover{ - color: ${(p) => p.$hoverColor || p.$activeColor}; - background-color: ${(p) => p.$hoverBg || "transparent"} !important; + .ant-dropdown-menu-item:hover { + color: ${(p) => p.$hoverColor || p.$color}; + background-color: ${(p) => p.$hoverBg || p.$bg || "transparent"} !important; + border: ${(p) => p.$hoverBorder ? `1px solid ${p.$hoverBorder}` : (p.$border ? `1px solid ${p.$border}` : "1px solid transparent")}; + font-weight: ${(p) => p.$hoverTextWeight || p.$textWeight || 500}; + font-family: ${(p) => p.$hoverFontFamily || p.$fontFamily || "sans-serif"}; + font-style: ${(p) => p.$hoverFontStyle || p.$fontStyle || "normal"}; + font-size: ${(p) => p.$hoverTextSize || p.$textSize || "14px"}; + text-decoration: ${(p) => p.$hoverTextDecoration || p.$textDecoration || "none"}; cursor: pointer; } /* Selected/active state */ @@ -203,7 +240,12 @@ const StyledMenu = styled(Menu) < .ant-menu-item-selected { color: ${(p) => p.$activeColor}; background-color: ${(p) => p.$activeBg || p.$bg || "transparent"}; - border: ${(p) => (p.$activeBorder ? `1px solid ${p.$activeBorder}` : "1px solid transparent")}; + border: ${(p) => p.$activeBorder ? `1px solid ${p.$activeBorder}` : (p.$border ? `1px solid ${p.$border}` : "1px solid transparent")}; + font-weight: ${(p) => p.$activeTextWeight || p.$textWeight || 500}; + font-family: ${(p) => p.$activeFontFamily || p.$fontFamily || "sans-serif"}; + font-style: ${(p) => p.$activeFontStyle || p.$fontStyle || "normal"}; + font-size: ${(p) => p.$activeTextSize || p.$textSize || "14px"}; + text-decoration: ${(p) => p.$activeTextDecoration || p.$textDecoration || "none"}; } /* Disabled state */ .ant-dropdown-menu-item-disabled, @@ -391,9 +433,7 @@ function renderAdvancedSection(children: any) { function renderStyleSections( children: any, styleSegment: MenuItemStyleOptionValue, - setStyleSegment: (k: MenuItemStyleOptionValue) => void, - subStyleSegment: MenuItemStyleOptionValue, - setSubStyleSegment: (k: MenuItemStyleOptionValue) => void + setStyleSegment: (k: MenuItemStyleOptionValue) => void ) { const isHamburger = children.displayMode.getView() === 'hamburger'; return ( @@ -416,19 +456,6 @@ function renderStyleSections( {styleSegment === "hover" && children.navItemHoverStyle.getPropertyView()} {styleSegment === "active" && children.navItemActiveStyle.getPropertyView()}
-
- {controlItem({}, ( - setSubStyleSegment(k as MenuItemStyleOptionValue)} - /> - ))} - {subStyleSegment === "normal" && children.subNavItemStyle.getPropertyView()} - {subStyleSegment === "hover" && children.subNavItemHoverStyle.getPropertyView()} - {subStyleSegment === "active" && children.subNavItemActiveStyle.getPropertyView()} -
{isHamburger && ( <>
@@ -476,9 +503,6 @@ const childrenMap = { hamburgerButtonStyle: styleControl(HamburgerButtonStyle, 'hamburgerButtonStyle'), drawerContainerStyle: styleControl(DrawerContainerStyle, 'drawerContainerStyle'), animationStyle: styleControl(AnimationStyle, 'animationStyle'), - subNavItemStyle: styleControl(NavSubMenuItemStyle, 'subNavItemStyle'), - subNavItemHoverStyle: styleControl(NavSubMenuItemHoverStyle, 'subNavItemHoverStyle'), - subNavItemActiveStyle: styleControl(NavSubMenuItemActiveStyle, 'subNavItemActiveStyle'), items: withDefault(migrateOldData(createNavItemsControl(), fixOldItemsData), { optionType: "manual", manual: [ @@ -537,24 +561,33 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { 0} - $color={(props.navItemStyle && props.navItemStyle.text) || props.style.text} - $hoverColor={(props.navItemHoverStyle && props.navItemHoverStyle.text) || props.style.accent} - $activeColor={(props.navItemActiveStyle && props.navItemActiveStyle.text) || props.style.accent} - $fontFamily={props.style.fontFamily} - $fontStyle={props.style.fontStyle} - $textWeight={props.style.textWeight} - $textSize={props.style.textSize} - $padding={(props.navItemStyle && props.navItemStyle.padding) || props.style.padding} - $textTransform={props.style.textTransform} - $textDecoration={props.style.textDecoration} - $margin={(props.navItemStyle && props.navItemStyle.margin) || props.style.margin} - $bg={(props.navItemStyle && props.navItemStyle.background) || undefined} - $hoverBg={(props.navItemHoverStyle && props.navItemHoverStyle.background) || undefined} - $activeBg={(props.navItemActiveStyle && props.navItemActiveStyle.background) || undefined} - $border={(props.navItemStyle && props.navItemStyle.border) || undefined} - $hoverBorder={(props.navItemHoverStyle && props.navItemHoverStyle.border) || undefined} - $activeBorder={(props.navItemActiveStyle && props.navItemActiveStyle.border) || undefined} - $radius={(props.navItemStyle && props.navItemStyle.radius) || undefined} + $color={props.navItemStyle?.text || props.style.accent} + $hoverColor={props.navItemHoverStyle?.text || props.navItemStyle?.text || props.style.accent} + $activeColor={props.navItemActiveStyle?.text || props.navItemStyle?.text || props.style.accent} + $fontFamily={props.navItemStyle?.fontFamily || 'sans-serif'} + $fontStyle={props.navItemStyle?.fontStyle || 'normal'} + $textWeight={props.navItemStyle?.textWeight || '500'} + $textSize={props.navItemStyle?.textSize || '14px'} + $textDecoration={props.navItemStyle?.textDecoration || 'none'} + $hoverFontFamily={props.navItemHoverStyle?.fontFamily} + $hoverFontStyle={props.navItemHoverStyle?.fontStyle} + $hoverTextWeight={props.navItemHoverStyle?.textWeight} + $hoverTextSize={props.navItemHoverStyle?.textSize} + $hoverTextDecoration={props.navItemHoverStyle?.textDecoration} + $activeFontFamily={props.navItemActiveStyle?.fontFamily} + $activeFontStyle={props.navItemActiveStyle?.fontStyle} + $activeTextWeight={props.navItemActiveStyle?.textWeight} + $activeTextSize={props.navItemActiveStyle?.textSize} + $activeTextDecoration={props.navItemActiveStyle?.textDecoration} + $padding={props.navItemStyle?.padding || '0 16px'} + $margin={props.navItemStyle?.margin || '0px'} + $bg={props.navItemStyle?.background} + $hoverBg={props.navItemHoverStyle?.background} + $activeBg={props.navItemActiveStyle?.background} + $border={props.navItemStyle?.border} + $hoverBorder={props.navItemHoverStyle?.border} + $activeBorder={props.navItemActiveStyle?.border} + $radius={props.navItemStyle?.radius} $disabled={disabled} onClick={() => { if (!disabled && onEvent) onEvent("click"); }} > @@ -565,7 +598,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { ); if (subMenuItems.length > 0) { const subMenu = ( - + { if (disabled) return; @@ -580,24 +613,33 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { ...item, icon: item.icon || undefined, }))} - $color={(props.subNavItemStyle && props.subNavItemStyle.text) || props.style.text} - $hoverColor={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.text) || props.style.accent} - $activeColor={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.text) || props.style.accent} - $bg={(props.subNavItemStyle && props.subNavItemStyle.background) || undefined} - $hoverBg={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.background) || undefined} - $activeBg={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.background) || undefined} - $border={(props.subNavItemStyle && props.subNavItemStyle.border) || undefined} - $hoverBorder={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.border) || undefined} - $activeBorder={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.border) || undefined} - $radius={(props.subNavItemStyle && props.subNavItemStyle.radius) || undefined} - $fontFamily={props.style.fontFamily} - $fontStyle={props.style.fontStyle} - $textWeight={props.style.textWeight} - $textSize={props.style.textSize} - $padding={(props.subNavItemStyle && props.subNavItemStyle.padding) || props.style.padding} - $margin={(props.subNavItemStyle && props.subNavItemStyle.margin) || props.style.margin} - $textTransform={props.style.textTransform} - $textDecoration={props.style.textDecoration} + $color={props.navItemStyle?.text || props.style.accent} + $hoverColor={props.navItemHoverStyle?.text || props.navItemStyle?.text || props.style.accent} + $activeColor={props.navItemActiveStyle?.text || props.navItemStyle?.text || props.style.accent} + $bg={props.navItemStyle?.background} + $hoverBg={props.navItemHoverStyle?.background} + $activeBg={props.navItemActiveStyle?.background} + $border={props.navItemStyle?.border} + $hoverBorder={props.navItemHoverStyle?.border} + $activeBorder={props.navItemActiveStyle?.border} + $radius={props.navItemStyle?.radius} + $fontFamily={props.navItemStyle?.fontFamily || 'sans-serif'} + $fontStyle={props.navItemStyle?.fontStyle || 'normal'} + $textWeight={props.navItemStyle?.textWeight || '500'} + $textSize={props.navItemStyle?.textSize || '14px'} + $textDecoration={props.navItemStyle?.textDecoration || 'none'} + $hoverFontFamily={props.navItemHoverStyle?.fontFamily} + $hoverFontStyle={props.navItemHoverStyle?.fontStyle} + $hoverTextWeight={props.navItemHoverStyle?.textWeight} + $hoverTextSize={props.navItemHoverStyle?.textSize} + $hoverTextDecoration={props.navItemHoverStyle?.textDecoration} + $activeFontFamily={props.navItemActiveStyle?.fontFamily} + $activeFontStyle={props.navItemActiveStyle?.fontStyle} + $activeTextWeight={props.navItemActiveStyle?.textWeight} + $activeTextSize={props.navItemActiveStyle?.textSize} + $activeTextDecoration={props.navItemActiveStyle?.textDecoration} + $padding={props.navItemStyle?.padding || '0 16px'} + $margin={props.navItemStyle?.margin || '0px'} /> ); @@ -679,7 +721,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { setDrawerVisible(false)} > {hasIcon(props.drawerCloseIcon) @@ -700,7 +742,6 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { const showLogic = mode === "logic" || mode === "both"; const showLayout = mode === "layout" || mode === "both"; const [styleSegment, setStyleSegment] = useState("normal"); - const [subStyleSegment, setSubStyleSegment] = useState("normal"); return ( <> @@ -708,7 +749,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { {showLogic && renderInteractionSection(children)} {showLayout && renderLayoutSection(children)} {showLogic && renderAdvancedSection(children)} - {showLayout && renderStyleSections(children, styleSegment, setStyleSegment, subStyleSegment, setSubStyleSegment)} + {showLayout && renderStyleSections(children, styleSegment, setStyleSegment)} ); }) diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index f2516f881..9a117620d 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -2017,34 +2017,14 @@ export const CircleProgressStyle = [ ]; export const NavigationStyle = [ - ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE.filter(style=>style.name!=='rotation'), "text", [ - { - name: "text", - label: trans("text"), - depName: "background", - depType: DEP_TYPE.CONTRAST_TEXT, - transformer: contrastText, - }, - ACCENT, - getStaticBackground("#FFFFFF00"), - ]), - // { - // name: "text", - // label: trans("text"), - // depName: "background", - // depType: DEP_TYPE.CONTRAST_TEXT, - // transformer: contrastText, - // }, - // ACCENT, - // getStaticBackground("#FFFFFF00"), - // getStaticBorder("#FFFFFF00"), - // MARGIN, - // PADDING, - // FONT_FAMILY, - // FONT_STYLE, - // TEXT_WEIGHT, - // TEXT_SIZE, - // BORDER_WIDTH + getStaticBackground("#FFFFFF00"), + getStaticBorder("#FFFFFF00"), + BORDER_STYLE, + BORDER_WIDTH, + RADIUS, + MARGIN, + PADDING, + ACCENT, ] as const; export const ImageStyle = [ @@ -2390,6 +2370,11 @@ export const NavLayoutItemStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, + TEXT_SIZE, + TEXT_WEIGHT, + FONT_FAMILY, + FONT_STYLE, + TEXT_DECORATION, MARGIN, PADDING, ] as const; @@ -2404,6 +2389,11 @@ export const NavLayoutItemHoverStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, + TEXT_SIZE, + TEXT_WEIGHT, + FONT_FAMILY, + FONT_STYLE, + TEXT_DECORATION, ] as const; export const NavLayoutItemActiveStyle = [ @@ -2416,6 +2406,11 @@ export const NavLayoutItemActiveStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, + TEXT_SIZE, + TEXT_WEIGHT, + FONT_FAMILY, + FONT_STYLE, + TEXT_DECORATION, ] as const; // Submenu item styles (normal/hover/active), similar to top-level menu items @@ -2430,6 +2425,11 @@ export const NavSubMenuItemStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, + TEXT_SIZE, + TEXT_WEIGHT, + FONT_FAMILY, + FONT_STYLE, + TEXT_DECORATION, MARGIN, PADDING, ] as const; @@ -2444,6 +2444,11 @@ export const NavSubMenuItemHoverStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, + TEXT_SIZE, + TEXT_WEIGHT, + FONT_FAMILY, + FONT_STYLE, + TEXT_DECORATION, ] as const; export const NavSubMenuItemActiveStyle = [ @@ -2456,6 +2461,11 @@ export const NavSubMenuItemActiveStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, + TEXT_SIZE, + TEXT_WEIGHT, + FONT_FAMILY, + FONT_STYLE, + TEXT_DECORATION, ] as const; export const CarouselStyle = [getBackground("canvas")] as const; From c3b53291a52f4e973fcf0589deb96e47877f107c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 1 Dec 2025 21:59:19 +0500 Subject: [PATCH 58/97] [Fix]: #2086 nav desktop app layout in publish mode --- .../src/comps/comps/layout/navLayout.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 3540e1120..4f0879879 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -49,8 +49,8 @@ const DEFAULT_WIDTH = 240; type MenuItemStyleOptionValue = "normal" | "hover" | "active"; const EventOptions = [clickEvent] as const; -const StyledSide = styled(LayoutSider)` - max-height: calc(100vh - ${TopHeaderHeight}); +const StyledSide = styled(LayoutSider)<{ $isPreview: boolean }>` + max-height: ${(props) => (props.$isPreview ? `calc(100vh - ${TopHeaderHeight})` : "100vh")}; overflow: auto; .ant-menu-item:first-child { @@ -199,6 +199,11 @@ const StyledMenu = styled(AntdMenu)<{ `; + +const ViewerMainContent = styled(MainContent)<{ $isPreview: boolean }>` + height: ${(props) => (props.$isPreview ? `calc(100vh - ${TopHeaderHeight})` : "100vh")}; +`; + const StyledImage = styled.img` height: 1em; color: currentColor; @@ -355,6 +360,7 @@ let NavTmpLayout = (function () { NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const pathParam = useAppPathParam(); const isViewMode = isUserViewMode(pathParam); + const isPreview = pathParam.viewMode === "preview"; const [selectedKey, setSelectedKey] = useState(""); const items = comp.children.items.getView(); const navWidth = comp.children.width.getView(); @@ -674,25 +680,25 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { ); let content = ( - + {(navPosition === 'top') && (
{ navMenu }
)} {(navPosition === 'left') && ( - + {navMenu} )} - {pageView} + {pageView} {(navPosition === 'bottom') && (
{ navMenu }
)} {(navPosition === 'right') && ( - + {navMenu} )} From e18fac718c76d78f44a6828bf6c2a6bcb4a2ed16 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 5 Dec 2025 21:15:41 +0500 Subject: [PATCH 59/97] [feat]: add params supports in the runQuery custom comp --- .../lowcoder/src/comps/comps/customComp/customComp.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/customComp/customComp.tsx b/client/packages/lowcoder/src/comps/comps/customComp/customComp.tsx index bd58829cb..dd9d6489f 100644 --- a/client/packages/lowcoder/src/comps/comps/customComp/customComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/customComp/customComp.tsx @@ -45,7 +45,7 @@ const defaultCode = ` @@ -104,10 +104,11 @@ function InnerCustomComponent(props: IProps) { const methodsRef = useRef({ runQuery: async (data: any) => { - const { queryName } = data; + + const { queryName, params } = data.queryName; return getPromiseAfterDispatch( dispatch, - routeByNameAction(queryName, executeQueryAction({})) + routeByNameAction(queryName, executeQueryAction({ args: params })) ).catch((error) => Promise.resolve({})); }, From 24440dcc3df07c3c66fbd52903bc701b98172ca7 Mon Sep 17 00:00:00 2001 From: Falk Wolsky Date: Wed, 2 Jul 2025 11:02:54 +0200 Subject: [PATCH 60/97] Update docker-images.yml Adding Enterprise Edition Docker Image --- .github/workflows/docker-images.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index d075f1fdc..97b1c85aa 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -146,6 +146,24 @@ jobs: push: true tags: ${{ env.FRONTEND_IMAGE_NAMES }} + - name: Build and push the enterprise edition frontend image + if: ${{ env.BUILD_FRONTEND == 'true' }} + uses: docker/build-push-action@v6 + env: + NODE_ENV: production + with: + file: ./deploy/docker/Dockerfile + target: lowcoder-ce-frontend + build-args: | + REACT_APP_ENV=production + REACT_APP_EDITION=enterprise + REACT_APP_COMMIT_ID="dev #${{ env.SHORT_SHA }}" + platforms: | + linux/amd64 + linux/arm64 + push: true + tags: ${{ env.FRONTEND_IMAGE_NAMES }} + - name: Build and push the node service image if: ${{ env.BUILD_NODESERVICE == 'true' }} uses: docker/build-push-action@v6 From 1de676884d046a4525daaeb60429d62fe57d5986 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 2 Jul 2025 15:14:40 +0500 Subject: [PATCH 61/97] added ee build command --- client/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/package.json b/client/package.json index 37849e6fa..4b58716c6 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "start:ee": "REACT_APP_EDITION=enterprise yarn workspace lowcoder start", "translate": "node --loader ts-node/esm ./scripts/translate.js", "build": "yarn node ./scripts/build.js", + "build:ee": "REACT_APP_EDITION=enterprise yarn node ./scripts/build.js", "test": "jest && yarn workspace lowcoder-comps test", "prepare": "yarn workspace lowcoder prepare", "build:core": "yarn workspace lowcoder-core build", From eaab81a7e1882f6c6bdef0cdfb68feeb939c3161 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 2 Jul 2025 15:27:52 +0500 Subject: [PATCH 62/97] updated Dockerfile to add separate lowcoder-ee-frontend image --- deploy/docker/Dockerfile | 78 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index ba589dffa..9d386a682 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -185,6 +185,84 @@ EXPOSE 3443 ############################################################################# +## +## Build lowcoder client (Enterprise) application +## +FROM node:20.2-slim AS build-client-ee + +# curl is required for yarn build to succeed, because it calls it while building client +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates + +# Build client +COPY ./client /lowcoder-client-ee +WORKDIR /lowcoder-client-ee +RUN yarn --immutable + +ARG REACT_APP_COMMIT_ID=test +ARG REACT_APP_ENV=production +ARG REACT_APP_EDITION=community +ARG REACT_APP_DISABLE_JS_SANDBOX=true +RUN yarn build:ee + +# Build lowcoder-comps +WORKDIR /lowcoder-client-ee/packages/lowcoder-comps +RUN yarn install +RUN yarn build +RUN tar -zxf lowcoder-comps-*.tgz && mv package lowcoder-comps + +# Build lowcoder-sdk +WORKDIR /lowcoder-client-ee/packages/lowcoder-sdk +RUN yarn install +RUN yarn build + +WORKDIR /lowcoder-client-ee/packages/lowcoder-sdk-webpack-bundle +RUN yarn install +RUN yarn build + +## +## Intermediary Lowcoder client (Enterprise) image +## +## To create a separate image out of it, build it with: +## DOCKER_BUILDKIT=1 docker build -f deploy/docker/Dockerfile -t lowcoderorg/lowcoder-ee-frontend --target lowcoder-ee-frontend . +## +FROM nginx:1.27.1 AS lowcoder-ee-frontend +LABEL maintainer="lowcoder" + +# Change default nginx user into lowcoder user and remove default nginx config +RUN usermod --login lowcoder --uid 9001 nginx \ + && groupmod --new-name lowcoder --gid 9001 nginx \ + && rm -f /etc/nginx/nginx.conf \ + && mkdir -p /lowcoder/assets + +# Copy lowcoder client +COPY --chown=lowcoder:lowcoder --from=build-client-ee /lowcoder-client-ee/packages/lowcoder/build/ /lowcoder/client +# Copy lowcoder components +COPY --chown=lowcoder:lowcoder --from=build-client-ee /lowcoder-client-ee/packages/lowcoder-comps/lowcoder-comps /lowcoder/client-comps +# Copy lowcoder SDK +COPY --chown=lowcoder:lowcoder --from=build-client-ee /lowcoder-client-ee/packages/lowcoder-sdk /lowcoder/client-sdk +# Copy lowcoder SDK webpack bundle +COPY --chown=lowcoder:lowcoder --from=build-client-ee /lowcoder-client-ee/packages/lowcoder-sdk-webpack-bundle/dist /lowcoder/client-embed + + +# Copy additional nginx init scripts +COPY deploy/docker/frontend/00-change-nginx-user.sh /docker-entrypoint.d/00-change-nginx-user.sh +COPY deploy/docker/frontend/01-update-nginx-conf.sh /docker-entrypoint.d/01-update-nginx-conf.sh + +RUN chmod +x /docker-entrypoint.d/00-change-nginx-user.sh && \ + chmod +x /docker-entrypoint.d/01-update-nginx-conf.sh + +COPY deploy/docker/frontend/server.conf /etc/nginx/server.conf +COPY deploy/docker/frontend/nginx-http.conf /etc/nginx/nginx-http.conf +COPY deploy/docker/frontend/nginx-https.conf /etc/nginx/nginx-https.conf +COPY deploy/docker/frontend/ssl-certificate.conf /etc/nginx/ssl-certificate.conf +COPY deploy/docker/frontend/ssl-params.conf /etc/nginx/ssl-params.conf + + +EXPOSE 3000 +EXPOSE 3444 + +############################################################################# + ## ## Build Lowcoder all-in-one image ## From 4c5e427488847164651705b1317fa43094755f8c Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 2 Jul 2025 15:35:34 +0500 Subject: [PATCH 63/97] updated github workflow ee image --- .github/workflows/docker-images.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index 97b1c85aa..e26af7636 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -80,18 +80,21 @@ jobs: # Image names ALLINONE_IMAGE_NAMES=lowcoderorg/lowcoder-ce:${IMAGE_TAG} FRONTEND_IMAGE_NAMES=lowcoderorg/lowcoder-ce-frontend:${IMAGE_TAG} + FRONTEND_EE_IMAGE_NAMES=lowcoderorg/lowcoder-ee-frontend:${IMAGE_TAG} APISERVICE_IMAGE_NAMES=lowcoderorg/lowcoder-ce-api-service:${IMAGE_TAG} NODESERVICE_IMAGE_NAMES=lowcoderorg/lowcoder-ce-node-service:${IMAGE_TAG} if [[ "${IS_LATEST}" == "true" ]]; then ALLINONE_IMAGE_NAMES="lowcoderorg/lowcoder-ce:latest,${ALLINONE_IMAGE_NAMES}" FRONTEND_IMAGE_NAMES="lowcoderorg/lowcoder-ce-frontend:latest,${FRONTEND_IMAGE_NAMES}" + FRONTEND_EE_IMAGE_NAMES="lowcoderorg/lowcoder-ee-frontend:latest,${FRONTEND_EE_IMAGE_NAMES}" APISERVICE_IMAGE_NAMES="lowcoderorg/lowcoder-ce-api-service:latest,${APISERVICE_IMAGE_NAMES}" NODESERVICE_IMAGE_NAMES="lowcoderorg/lowcoder-ce-node-service:latest,${NODESERVICE_IMAGE_NAMES}" fi; echo "ALLINONE_IMAGE_NAMES=${ALLINONE_IMAGE_NAMES}" >> "${GITHUB_ENV}" echo "FRONTEND_IMAGE_NAMES=${FRONTEND_IMAGE_NAMES}" >> "${GITHUB_ENV}" + echo "FRONTEND_EE_IMAGE_NAMES=${FRONTEND_EE_IMAGE_NAMES}" >> "${GITHUB_ENV}" echo "APISERVICE_IMAGE_NAMES=${APISERVICE_IMAGE_NAMES}" >> "${GITHUB_ENV}" echo "NODESERVICE_IMAGE_NAMES=${NODESERVICE_IMAGE_NAMES}" >> "${GITHUB_ENV}" @@ -153,7 +156,7 @@ jobs: NODE_ENV: production with: file: ./deploy/docker/Dockerfile - target: lowcoder-ce-frontend + target: lowcoder-ee-frontend build-args: | REACT_APP_ENV=production REACT_APP_EDITION=enterprise @@ -162,7 +165,7 @@ jobs: linux/amd64 linux/arm64 push: true - tags: ${{ env.FRONTEND_IMAGE_NAMES }} + tags: ${{ env.FRONTEND_EE_IMAGE_NAMES }} - name: Build and push the node service image if: ${{ env.BUILD_NODESERVICE == 'true' }} From 6e329d69e3a0fbb1d5f801ded91d88508a327396 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 2 Jul 2025 17:34:21 +0500 Subject: [PATCH 64/97] update env variable --- deploy/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 9d386a682..5287942de 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -200,7 +200,7 @@ RUN yarn --immutable ARG REACT_APP_COMMIT_ID=test ARG REACT_APP_ENV=production -ARG REACT_APP_EDITION=community +ARG REACT_APP_EDITION=enterprise ARG REACT_APP_DISABLE_JS_SANDBOX=true RUN yarn build:ee From 1aa5c589465fc919dd9a6525c2006ebf8e41ccf3 Mon Sep 17 00:00:00 2001 From: Falk Wolsky Date: Wed, 2 Jul 2025 14:44:57 +0200 Subject: [PATCH 65/97] Update docker-images.yml --- .github/workflows/docker-images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index e26af7636..9132a02c5 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -156,7 +156,7 @@ jobs: NODE_ENV: production with: file: ./deploy/docker/Dockerfile - target: lowcoder-ee-frontend + target: lowcoder-enterprise-frontend build-args: | REACT_APP_ENV=production REACT_APP_EDITION=enterprise From 390fb74399de98bf0d14c6bf28d459d706aba409 Mon Sep 17 00:00:00 2001 From: Falk Wolsky Date: Wed, 2 Jul 2025 15:15:21 +0200 Subject: [PATCH 66/97] Update docker-images.yml --- .github/workflows/docker-images.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index 9132a02c5..439280b43 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -72,22 +72,22 @@ jobs: fi; # Control which images to build - echo "BUILD_ALLINONE=${{ inputs.build_allinone || true }}" >> "${GITHUB_ENV}" - echo "BUILD_FRONTEND=${{ inputs.build_frontend || true }}" >> "${GITHUB_ENV}" - echo "BUILD_NODESERVICE=${{ inputs.build_nodeservice || true }}" >> "${GITHUB_ENV}" - echo "BUILD_APISERVICE=${{ inputs.build_apiservice || true }}" >> "${GITHUB_ENV}" + echo "BUILD_ALLINONE=${{ inputs.build_allinone || false }}" >> "${GITHUB_ENV}" + echo "BUILD_FRONTEND=${{ inputs.build_frontend || false }}" >> "${GITHUB_ENV}" + echo "BUILD_NODESERVICE=${{ inputs.build_nodeservice || false }}" >> "${GITHUB_ENV}" + echo "BUILD_APISERVICE=${{ inputs.build_apiservice || false }}" >> "${GITHUB_ENV}" # Image names ALLINONE_IMAGE_NAMES=lowcoderorg/lowcoder-ce:${IMAGE_TAG} FRONTEND_IMAGE_NAMES=lowcoderorg/lowcoder-ce-frontend:${IMAGE_TAG} - FRONTEND_EE_IMAGE_NAMES=lowcoderorg/lowcoder-ee-frontend:${IMAGE_TAG} + FRONTEND_EE_IMAGE_NAMES=lowcoderorg/lowcoder-enterprise-frontend:${IMAGE_TAG} APISERVICE_IMAGE_NAMES=lowcoderorg/lowcoder-ce-api-service:${IMAGE_TAG} NODESERVICE_IMAGE_NAMES=lowcoderorg/lowcoder-ce-node-service:${IMAGE_TAG} if [[ "${IS_LATEST}" == "true" ]]; then ALLINONE_IMAGE_NAMES="lowcoderorg/lowcoder-ce:latest,${ALLINONE_IMAGE_NAMES}" FRONTEND_IMAGE_NAMES="lowcoderorg/lowcoder-ce-frontend:latest,${FRONTEND_IMAGE_NAMES}" - FRONTEND_EE_IMAGE_NAMES="lowcoderorg/lowcoder-ee-frontend:latest,${FRONTEND_EE_IMAGE_NAMES}" + FRONTEND_EE_IMAGE_NAMES="lowcoderorg/lowcoder-enterprise-frontend:latest,${FRONTEND_EE_IMAGE_NAMES}" APISERVICE_IMAGE_NAMES="lowcoderorg/lowcoder-ce-api-service:latest,${APISERVICE_IMAGE_NAMES}" NODESERVICE_IMAGE_NAMES="lowcoderorg/lowcoder-ce-node-service:latest,${NODESERVICE_IMAGE_NAMES}" fi; From d82ad75be0c349d0267e6cf5a1c7a8865a13582b Mon Sep 17 00:00:00 2001 From: Falk Wolsky Date: Wed, 2 Jul 2025 15:40:53 +0200 Subject: [PATCH 67/97] Update Dockerfile --- deploy/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 5287942de..7b307946f 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -225,7 +225,7 @@ RUN yarn build ## To create a separate image out of it, build it with: ## DOCKER_BUILDKIT=1 docker build -f deploy/docker/Dockerfile -t lowcoderorg/lowcoder-ee-frontend --target lowcoder-ee-frontend . ## -FROM nginx:1.27.1 AS lowcoder-ee-frontend +FROM nginx:1.27.1 AS lowcoder-enterprise-frontend LABEL maintainer="lowcoder" # Change default nginx user into lowcoder user and remove default nginx config From 5520a3f9839bf87296fa7548df85fa841eb51058 Mon Sep 17 00:00:00 2001 From: Falk Wolsky Date: Thu, 24 Jul 2025 16:40:24 +0200 Subject: [PATCH 68/97] Update docker-images.yml --- .github/workflows/docker-images.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index 439280b43..be06cf1a4 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -72,10 +72,10 @@ jobs: fi; # Control which images to build - echo "BUILD_ALLINONE=${{ inputs.build_allinone || false }}" >> "${GITHUB_ENV}" - echo "BUILD_FRONTEND=${{ inputs.build_frontend || false }}" >> "${GITHUB_ENV}" - echo "BUILD_NODESERVICE=${{ inputs.build_nodeservice || false }}" >> "${GITHUB_ENV}" - echo "BUILD_APISERVICE=${{ inputs.build_apiservice || false }}" >> "${GITHUB_ENV}" + echo "BUILD_ALLINONE=${{ inputs.build_allinone || true }}" >> "${GITHUB_ENV}" + echo "BUILD_FRONTEND=${{ inputs.build_frontend || true }}" >> "${GITHUB_ENV}" + echo "BUILD_NODESERVICE=${{ inputs.build_nodeservice || true }}" >> "${GITHUB_ENV}" + echo "BUILD_APISERVICE=${{ inputs.build_apiservice || true }}" >> "${GITHUB_ENV}" # Image names ALLINONE_IMAGE_NAMES=lowcoderorg/lowcoder-ce:${IMAGE_TAG} From bfe0e5f5ead463912e8327e18ad78e3ffaaa4df9 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 28 Jul 2025 20:35:57 +0500 Subject: [PATCH 69/97] [Feat]: #1820 add search filter/sorting for columns gui query --- .../queries/sqlQuery/columnNameDropdown.tsx | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/client/packages/lowcoder/src/comps/queries/sqlQuery/columnNameDropdown.tsx b/client/packages/lowcoder/src/comps/queries/sqlQuery/columnNameDropdown.tsx index e33be20bd..d79c00a78 100644 --- a/client/packages/lowcoder/src/comps/queries/sqlQuery/columnNameDropdown.tsx +++ b/client/packages/lowcoder/src/comps/queries/sqlQuery/columnNameDropdown.tsx @@ -1,6 +1,6 @@ import { DispatchType } from "lowcoder-core"; import { ControlPlacement } from "../../controls/controlParams"; -import React, { useContext } from "react"; +import React, { useContext, useState, useEffect } from "react"; import { Dropdown, OptionsType } from "lowcoder-design"; import { isEmpty, values } from "lodash"; import { useSelector } from "react-redux"; @@ -8,6 +8,8 @@ import { getDataSourceStructures } from "../../../redux/selectors/datasourceSele import { changeValueAction } from "lowcoder-core"; import { QueryContext } from "../../../util/context/QueryContext"; +const COLUMN_SORT_KEY = "lowcoder_column_sort"; + export const ColumnNameDropdown = (props: { table: string; value: string; @@ -18,13 +20,27 @@ export const ColumnNameDropdown = (props: { }) => { const context = useContext(QueryContext); const datasourceId = context?.datasourceId ?? ""; - const columns: OptionsType = - values(useSelector(getDataSourceStructures)[datasourceId]) - ?.find((t) => t.name === props.table) - ?.columns.map((column) => ({ - label: column.name, - value: column.name, - })) ?? []; + + // Simple sort preference from localStorage + const [sortColumns, setSortColumns] = useState(() => { + return localStorage.getItem(COLUMN_SORT_KEY) === 'true'; + }); + + useEffect(() => { + localStorage.setItem(COLUMN_SORT_KEY, sortColumns.toString()); + }, [sortColumns]); + + const rawColumns = values(useSelector(getDataSourceStructures)[datasourceId]) + ?.find((t) => t.name === props.table) + ?.columns.map((column) => ({ + label: column.name, + value: column.name, + })) ?? []; + + const columns: OptionsType = sortColumns + ? [...rawColumns].sort((a, b) => a.label.localeCompare(b.label)) + : rawColumns; + return ( ( +
+ +
+ )} /> ); }; From 3eca6dcb9bc4c2bc02137f97494f2b001ebe9edb Mon Sep 17 00:00:00 2001 From: Sanjai Kumar Date: Tue, 18 Nov 2025 11:43:09 +0530 Subject: [PATCH 70/97] feat: Add SCIM + SSO JIT provisioning support for Entra ID feat: Add SCIM + SSO JIT provisioning support for Entra ID feat: Add SCIM + SSO JIT provisioning support for Entra ID - Fix NullPointerException in createSCIMUserAndAddToOrg endpoint - Support placeholder user creation via SCIM for compliance - Enable JIT provisioning on first SSO login with correct auth source - Add comprehensive documentation for Entra ID integration - Prevent auth source mismatch between SCIM and SSO authentication This allows organizations to use SCIM for user pre-provisioning while maintaining proper SSO authentication with Entra ID (Azure AD). --- .../JIT_PROVISIONING_WITH_ENTRA_ID.md | 266 ++++++++++++++++++ .../self-hosting/JIT_QUICK_START.md | 80 ++++++ .../api/usermanagement/UserController.java | 38 +++ .../api/usermanagement/UserEndpoints.java | 9 + 4 files changed, 393 insertions(+) create mode 100644 docs/setup-and-run/self-hosting/JIT_PROVISIONING_WITH_ENTRA_ID.md create mode 100644 docs/setup-and-run/self-hosting/JIT_QUICK_START.md diff --git a/docs/setup-and-run/self-hosting/JIT_PROVISIONING_WITH_ENTRA_ID.md b/docs/setup-and-run/self-hosting/JIT_PROVISIONING_WITH_ENTRA_ID.md new file mode 100644 index 000000000..355547bc8 --- /dev/null +++ b/docs/setup-and-run/self-hosting/JIT_PROVISIONING_WITH_ENTRA_ID.md @@ -0,0 +1,266 @@ +# JIT (Just-In-Time) Provisioning with Entra ID (Azure AD) + +This guide explains how to set up JIT user provisioning with Microsoft Entra ID (formerly Azure AD) for Lowcoder. + +## Overview + +**JIT Provisioning** automatically creates user accounts in Lowcoder when users authenticate via SSO for the first time. This eliminates the need for manual user creation or SCIM provisioning for basic SSO scenarios. + +## How It Works + +1. User clicks **"Sign in with Entra ID"** on Lowcoder login page +2. User authenticates with Microsoft Entra ID +3. Entra ID redirects back to Lowcoder with authentication details +4. Lowcoder automatically: + - Creates a new user account (if doesn't exist) + - Sets up the correct auth connection (Entra ID, not EMAIL) + - Adds user to the organization + - Logs user in + +## Prerequisites + +- Lowcoder instance running (self-hosted or cloud) +- Admin access to Lowcoder +- Microsoft Entra ID (Azure AD) tenant +- Admin access to Azure Portal + +## Step 1: Configure Application in Azure Portal + +### 1.1 Register a New Application + +1. Go to [Azure Portal](https://portal.azure.com) +2. Navigate to **Azure Active Directory** → **App registrations** +3. Click **New registration** +4. Configure: + - **Name**: `Lowcoder SSO` + - **Supported account types**: Choose based on your needs + - Single tenant (most common for enterprise) + - Multi-tenant (if needed) + - **Redirect URI**: + - Platform: `Web` + - URI: `https://your-lowcoder-domain.com/api/auth/oauth2/callback` +5. Click **Register** + +### 1.2 Configure API Permissions + +1. In your app registration, go to **API permissions** +2. Click **Add a permission** +3. Select **Microsoft Graph** +4. Choose **Delegated permissions** +5. Add these permissions: + - `openid` + - `profile` + - `email` + - `User.Read` +6. Click **Add permissions** +7. Click **Grant admin consent** (if you have admin rights) + +### 1.3 Create Client Secret + +1. Go to **Certificates & secrets** +2. Click **New client secret** +3. Description: `Lowcoder SSO Secret` +4. Expires: Choose appropriate duration (12-24 months recommended) +5. Click **Add** +6. **IMPORTANT**: Copy the secret **Value** immediately (you won't see it again!) + +### 1.4 Note Your Configuration Details + +From **Overview** page, note: + +- **Application (client) ID**: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` +- **Directory (tenant) ID**: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` + +## Step 2: Configure Entra ID Provider in Lowcoder + +### 2.1 Access Admin Settings + +1. Log in to Lowcoder as **admin** +2. Go to **Settings** → **Authentication** (or Admin panel) +3. Click **Add Authentication Provider** + +### 2.2 Configure Generic OAuth Provider + +Fill in the following details: + +| Field | Value | +| -------------------------- | --------------------------------------------------------------------- | +| **Provider Name** | `Entra ID` or `Azure AD` | +| **Auth Type** | `GENERIC` (Generic OAuth2) | +| **Client ID** | `` | +| **Client Secret** | `` | +| **Authorization Endpoint** | `https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize` | +| **Token Endpoint** | `https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token` | +| **User Info Endpoint** | `https://graph.microsoft.com/v1.0/me` | +| **Scope** | `openid profile email User.Read` | +| **Enable Registration** | ✅ **TRUE** (This enables JIT provisioning!) | + +Replace `{tenant-id}` with your actual tenant ID. + +### 2.3 Configure User Attribute Mappings + +Set up how Entra ID user attributes map to Lowcoder user fields: + +```json +{ + "uid": "id", + "username": "userPrincipalName", + "email": "mail", + "avatar": "photo" +} +``` + +Or if email field is sometimes null: + +```json +{ + "uid": "id", + "username": "userPrincipalName", + "email": "userPrincipalName", + "avatar": "photo" +} +``` + +### 2.4 Save Configuration + +Click **Save** or **Enable** to activate the provider. + +## Step 3: Test JIT Provisioning + +### 3.1 Test User Login + +1. **Log out** from Lowcoder (if logged in) +2. Go to Lowcoder login page +3. You should see **"Sign in with Entra ID"** button +4. Click the button +5. You'll be redirected to Microsoft login +6. Enter credentials and authenticate +7. You'll be redirected back to Lowcoder +8. **User account is automatically created** ✅ + +### 3.2 Verify User Creation + +As admin: + +1. Go to **Settings** → **Members** or **Users** +2. You should see the newly created user +3. Check user details: + - Auth Source should be **"Entra ID"** (not EMAIL) + - Email should match the Entra ID account + - User should be active + +## SCIM + JIT Combined Approach (Optional) + +If you need both SCIM provisioning AND SSO authentication: + +### Use Case + +- SCIM pre-provisions users (for compliance/audit) +- Users still authenticate via SSO +- JIT adds auth connection on first login + +### How It Works with Updated Code + +The updated `createSCIMUserAndAddToOrg` endpoint now: + +1. **Creates a placeholder user** via SCIM (no auth connection) +2. **On first SSO login**, JIT provisioning adds the Entra ID auth connection +3. **Subsequent logins** work normally via SSO + +### Configure Entra ID SCIM Provisioning + +1. In Azure Portal, go to your **Enterprise Application** +2. Navigate to **Provisioning** +3. Set **Provisioning Mode**: `Automatic` +4. Configure: + - **Tenant URL**: `https://your-lowcoder-domain.com/api/v1/organizations/{orgId}/scim/users` + - **Secret Token**: Your API token +5. **Mappings**: Map Azure AD attributes to SCIM attributes +6. **Save** and **Start Provisioning** + +## Troubleshooting + +### Issue: "Sign in with Entra ID" button doesn't appear + +**Solution**: + +- Verify the auth provider is **enabled** in settings +- Check that `enableRegister` is set to `true` +- Clear browser cache and reload + +### Issue: User login fails with "LOG_IN_SOURCE_NOT_SUPPORTED" + +**Solution**: + +- Verify Authorization, Token, and User Info endpoints are correct +- Check that tenant ID is properly replaced in URLs +- Verify client ID and secret are correct + +### Issue: User created but with wrong auth source (EMAIL instead of Entra ID) + +**Solution**: + +- This means SCIM endpoint was used with old code +- With updated code, SCIM creates placeholder users +- JIT adds correct auth connection on first SSO login +- Rebuild Docker image with the fix + +### Issue: "EMAIL_PROVIDER_DISABLED" error + +**Solution**: + +- This error occurs if trying to create EMAIL-based users when EMAIL auth is disabled +- Use SSO with JIT provisioning instead +- Don't use the old SCIM implementation that creates EMAIL users + +## Security Considerations + +1. **Client Secret**: Store securely, rotate regularly +2. **Redirect URI**: Must exactly match what's configured in Azure +3. **Scope**: Request minimum permissions needed +4. **Enable Registration**: Only enable if you want JIT provisioning +5. **Organization**: Configure which organization new users join + +## Advanced Configuration + +### Multi-Organization Support + +If you have multiple organizations: + +- Configure separate OAuth providers per organization +- Or use organization domains to auto-assign users + +### Custom User Roles + +To assign custom roles on JIT provisioning: + +- Modify `AuthenticationApiServiceImpl.onUserLogin()` +- Add logic to check Entra ID groups/roles +- Map to Lowcoder roles accordingly + +### Email Verification + +JIT-provisioned users are automatically verified since they authenticated via SSO. + +## Comparison: SCIM vs JIT + +| Feature | SCIM Provisioning | JIT Provisioning | +| -------------------- | ---------------------------- | ---------------------- | +| **User Creation** | Pre-provisioned via SCIM | Created on first login | +| **Setup Complexity** | High | Low | +| **Auth Source** | EMAIL (with old code) | Correct SSO source | +| **Works with SSO** | Requires fix | ✅ Native support | +| **Audit Trail** | Full provisioning audit | Login-based audit | +| **Use Case** | Compliance, pre-provisioning | Simple SSO | + +## Recommendation + +For most use cases: **Use JIT Provisioning** (simpler, works correctly with SSO) + +For compliance requirements: **Use SCIM + JIT Combined** (with updated code) + +## References + +- [Microsoft Entra ID OAuth2 Documentation](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) +- [Lowcoder Authentication Documentation](../../../workspaces-and-teamwork/oauth/) +- [Generic OAuth Provider Setup](../../../workspaces-and-teamwork/oauth/generic-oauth-provider.md) diff --git a/docs/setup-and-run/self-hosting/JIT_QUICK_START.md b/docs/setup-and-run/self-hosting/JIT_QUICK_START.md new file mode 100644 index 000000000..ab4a4a1f0 --- /dev/null +++ b/docs/setup-and-run/self-hosting/JIT_QUICK_START.md @@ -0,0 +1,80 @@ +# Quick Start: Enable JIT Provisioning with Entra ID + +## What is JIT Provisioning? + +**Just-In-Time (JIT) Provisioning** automatically creates user accounts when they first login via SSO. No manual user creation or SCIM needed! + +## Why Use JIT? + +✅ **Simple** - Minimal configuration +✅ **Secure** - Users authenticated via Entra ID +✅ **Correct** - Proper auth source (not EMAIL) +✅ **Fast** - Users created on first login + +## 5-Minute Setup + +### 1. Azure Portal Setup (3 minutes) + +1. Go to [Azure Portal](https://portal.azure.com) → **App registrations** → **New registration** +2. Name: `Lowcoder SSO`, Redirect URI: `https://your-domain.com/api/auth/oauth2/callback` +3. Create a **Client Secret** (save the value!) +4. Add permissions: **API permissions** → **Microsoft Graph** → `User.Read`, `email`, `profile`, `openid` +5. Note your **Client ID** and **Tenant ID** + +### 2. Lowcoder Configuration (2 minutes) + +Login as admin → **Settings** → **Authentication** → **Add Provider**: + +```yaml +Provider Name: Entra ID +Auth Type: GENERIC +Client ID: +Client Secret: +Authorization Endpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize +Token Endpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token +User Info Endpoint: https://graph.microsoft.com/v1.0/me +Scope: openid profile email User.Read +Enable Registration: ✅ TRUE # ← This enables JIT! +``` + +**User Mappings**: + +```json +{ + "uid": "id", + "username": "userPrincipalName", + "email": "mail" +} +``` + +### 3. Test (30 seconds) + +1. Logout from Lowcoder +2. Click **"Sign in with Entra ID"** +3. Authenticate with Microsoft +4. ✅ User automatically created and logged in! + +## That's It! + +No SCIM needed. No manual user creation. Users are provisioned automatically on first login with the correct authentication source. + +## Need SCIM? + +If you need SCIM for compliance, see the full guide: [JIT_PROVISIONING_WITH_ENTRA_ID.md](./JIT_PROVISIONING_WITH_ENTRA_ID.md) + +## Troubleshooting + +**Can't see "Sign in with Entra ID" button?** + +- Check `Enable Registration` is ✅ TRUE +- Verify provider is enabled in settings + +**Login fails?** + +- Verify redirect URI matches exactly +- Check client ID and secret are correct +- Verify tenant ID in endpoints + +## Support + +For detailed documentation, see: [JIT_PROVISIONING_WITH_ENTRA_ID.md](./JIT_PROVISIONING_WITH_ENTRA_ID.md) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index 0cc4d89df..f6bb280a0 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -18,6 +18,7 @@ import org.lowcoder.domain.organization.service.OrgMemberService; import org.lowcoder.domain.organization.service.OrganizationService; import org.lowcoder.domain.user.constant.UserStatusType; +import org.lowcoder.domain.user.model.AuthUser; import org.lowcoder.domain.user.model.User; import org.lowcoder.domain.user.model.UserDetail; import org.lowcoder.domain.user.service.UserService; @@ -25,6 +26,7 @@ import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.constants.AuthSourceConstants; import org.lowcoder.sdk.exception.BizError; +import org.lowcoder.sdk.exception.BizException; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.codec.multipart.Part; @@ -39,6 +41,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.HashSet; import java.util.stream.Collectors; import static org.lowcoder.sdk.exception.BizError.INVALID_USER_STATUS; @@ -70,6 +73,41 @@ public Mono> createUserAndAddToOrg(@PathVariable String orgId, C .map(ResponseView::success); } + @Override + public Mono> createSCIMUserAndAddToOrg(@PathVariable String orgId, CreateUserRequest request) { + return orgApiService.checkVisitorAdminRole(orgId) + .flatMap(__ -> { + // For SCIM provisioning: Create a minimal user without auth connection + // The auth connection will be added on first SSO login via JIT provisioning + // This allows SCIM to pre-provision users while SSO handles authentication + + // Check if user already exists by email + return userService.findByEmailDeep(request.email()) + .flatMap(existingUser -> { + // User already exists, just add to org if not already a member + return orgMemberService.tryAddOrgMember(orgId, existingUser.getId(), MemberRole.MEMBER) + .thenReturn(existingUser); + }) + .switchIfEmpty( + // Create new user without auth connection (placeholder for SSO) + Mono.defer(() -> { + User newUser = User.builder() + .name(request.email()) + .email(request.email()) + .isEnabled(true) + .build(); + newUser.setConnections(new HashSet<>()); + newUser.setIsNewUser(true); + + return userService.saveUser(newUser) + .delayUntil(user -> orgMemberService.tryAddOrgMember(orgId, user.getId(), MemberRole.MEMBER)); + }) + ); + }) + .delayUntil(user -> orgApiService.switchCurrentOrganizationTo(user.getId(), orgId)) + .map(ResponseView::success); + } + @Override public Mono> getUserProfile(ServerWebExchange exchange) { return sessionUserService.getVisitor() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java index 955bb70bf..9ba473780 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java @@ -39,6 +39,15 @@ public interface UserEndpoints @PostMapping("/new/{orgId}") public Mono> createUserAndAddToOrg(@PathVariable String orgId, @RequestBody CreateUserRequest request); + +@Operation( + tags = TAG_USER_MANAGEMENT, + operationId = "createSCIMUserAndAddToOrg", + summary = "Create SCIM user and add to the org", + description = "Create a new user from Entra/Auth0 through SCIM add to the Organization." + ) + @PostMapping("/newscim/{orgId}") + public Mono> createSCIMUserAndAddToOrg(@PathVariable String orgId, @RequestBody CreateUserRequest request); @Operation( tags = TAG_USER_MANAGEMENT, operationId = "getUserProfile", From 5be03bea18549abc846377f0acf478c11bce0eca Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 10 Dec 2025 02:25:27 +0500 Subject: [PATCH 71/97] [Fix]: column width adjusted by user --- .../lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx index 3cdf9119a..b85c16afb 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx @@ -65,7 +65,7 @@ const ResizeableTitle = React.forwardRef((props React.useImperativeHandle(ref, () => resizeRef.current!, []); const isNotDataColumn = _.isNil(restProps.title); - if ((isUserViewMode && !restProps.viewModeResizable) || isNotDataColumn) { + if ((isUserViewMode && !viewModeResizable) || isNotDataColumn) { return ; } From e12d82a7c95b0f9c80c8c8e7fddf30831ff86b25 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 11 Dec 2025 21:55:10 +0500 Subject: [PATCH 72/97] [Fix]: tutorial modal reappear --- client/packages/lowcoder-design/src/components/toolTip.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder-design/src/components/toolTip.tsx b/client/packages/lowcoder-design/src/components/toolTip.tsx index 3a6b53843..e12b47821 100644 --- a/client/packages/lowcoder-design/src/components/toolTip.tsx +++ b/client/packages/lowcoder-design/src/components/toolTip.tsx @@ -210,6 +210,7 @@ export const TutorialsTooltip = ({ step, backProps, skipProps, + closeProps, primaryProps, tooltipProps, isLastStep, @@ -219,7 +220,7 @@ export const TutorialsTooltip = ({ {step.title && {step.title}} - + From 2addb44f5684d74bb5f0b585a760e33148c1c06d Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Sat, 13 Dec 2025 00:31:38 +0500 Subject: [PATCH 73/97] [Feat]: #2089 add password column type --- .../comps/tableComp/column/columnTypeComp.tsx | 6 + .../columnTypeComps/columnPasswordComp.tsx | 160 ++++++++++++++++++ .../src/comps/comps/tableComp/tableUtils.tsx | 2 +- .../tableLiteComp/column/columnTypeComp.tsx | 6 + .../columnTypeComps/columnPasswordComp.tsx | 118 +++++++++++++ .../comps/comps/tableLiteComp/tableUtils.tsx | 2 +- 6 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnPasswordComp.tsx create mode 100644 client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComps/columnPasswordComp.tsx diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx index ff3df44c4..e2e82f58d 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx @@ -22,6 +22,7 @@ import { ColumnNumberComp } from "./columnTypeComps/ColumnNumberComp"; import { ColumnAvatarsComp } from "./columnTypeComps/columnAvatarsComp"; import { ColumnDropdownComp } from "./columnTypeComps/columnDropdownComp"; +import { ColumnPasswordComp } from "./columnTypeComps/columnPasswordComp"; const actionOptions = [ { @@ -101,6 +102,10 @@ const actionOptions = [ label: trans("table.progress"), value: "progress", }, + { + label: "Password", + value: "password", + }, ] as const; export const ColumnTypeCompMap = { @@ -123,6 +128,7 @@ export const ColumnTypeCompMap = { progress: ProgressComp, date: DateComp, time: TimeComp, + password: ColumnPasswordComp, }; type ColumnTypeMapType = typeof ColumnTypeCompMap; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnPasswordComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnPasswordComp.tsx new file mode 100644 index 000000000..5c0d62ff4 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnPasswordComp.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useMemo, useState } from "react"; +import styled from "styled-components"; +import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons"; +import { default as Input } from "antd/es/input"; +import { StringOrNumberControl } from "comps/controls/codeControl"; +import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder"; +import { ColumnValueTooltip } from "../simpleColumnTypeComps"; + +const Wrapper = styled.div` + display: inline-flex; + align-items: center; + min-width: 0; + gap: 6px; +`; + +const ValueText = styled.span` + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-variant-ligatures: none; +`; + +const ToggleButton = styled.button` + all: unset; + display: inline-flex; + align-items: center; + cursor: pointer; + opacity: 0.6; + + &:hover { + opacity: 1; + } + + svg { + font-size: 14px; + } +`; + +const childrenMap = { + text: StringOrNumberControl, +}; + +function normalizeToString(value: unknown) { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function maskPassword(raw: string, maskChar = "•", maxMaskLen = 12) { + if (!raw) return ""; + const len = raw.length; + const maskLen = Math.min(len, maxMaskLen); + const masked = maskChar.repeat(maskLen); + return len > maxMaskLen ? `${masked}…` : masked; +} + +const getBaseValue: ColumnTypeViewFn = (props) => + normalizeToString(props.text); + +const PasswordCell = React.memo( + ({ + value, + cellIndex, + }: { + value: string; + cellIndex?: string; + }) => { + const [visible, setVisible] = useState(false); + + const masked = useMemo(() => maskPassword(value), [value]); + + const onToggle = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setVisible((v) => !v); + }, []); + + React.useEffect(() => { + setVisible(false); + }, [cellIndex, value]); + + if (!value) { + return ; + } + + return ( + + {visible ? value : masked} + + {visible ? : } + + + ); + } +); + +PasswordCell.displayName = "PasswordCell"; + +const PasswordEditView = React.memo( + ({ + value, + onChange, + onChangeEnd, + }: { + value: string; + onChange: (value: string) => void; + onChangeEnd: () => void; + }) => { + const handleChange = useCallback( + (e: React.ChangeEvent) => { + onChange(e.target.value); + }, + [onChange] + ); + + return ( + + ); + } +); + +PasswordEditView.displayName = "PasswordEditView"; + +export const ColumnPasswordComp = new ColumnTypeCompBuilder( + childrenMap, + (props, dispatch) => { + const value = props.changeValue ?? getBaseValue(props, dispatch); + return ; + }, + (nodeValue) => maskPassword(normalizeToString(nodeValue.text.value)), + getBaseValue +) + .setEditViewFn((props) => { + return ( + props.onChange(v)} + onChangeEnd={props.onChangeEnd} + /> + ); + }) + .setPropertyViewFn((children) => ( + <> + {children.text.propertyView({ + label: "Value", + tooltip: ColumnValueTooltip, + })} + + )) + .build(); + + diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index edb26ca61..69948171f 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx @@ -451,7 +451,7 @@ export function columnsToAntdFormat( }), editMode, onTableEvent, - cellIndex: `${column.dataIndex}-${index}`, + cellIndex: `${column.dataIndex}-${record?.[OB_ROW_ORI_INDEX] ?? index}`, }); }, ...(column.sortable diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx index 842057fbb..5f585df6e 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx @@ -21,6 +21,7 @@ import { ColumnNumberComp } from "./columnTypeComps/ColumnNumberComp"; import { ColumnAvatarsComp } from "./columnTypeComps/columnAvatarsComp"; import { ColumnDropdownComp } from "./columnTypeComps/columnDropdownComp"; +import { ColumnPasswordComp } from "./columnTypeComps/columnPasswordComp"; export type CellProps = { tableSize?: string; @@ -110,6 +111,10 @@ const actionOptions = [ label: trans("table.progress"), value: "progress", }, + { + label: "Password", + value: "password", + }, ] as const; export const ColumnTypeCompMap = { @@ -132,6 +137,7 @@ export const ColumnTypeCompMap = { progress: ProgressComp, date: DateComp, time: TimeComp, + password: ColumnPasswordComp, }; type ColumnTypeMapType = typeof ColumnTypeCompMap; diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComps/columnPasswordComp.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComps/columnPasswordComp.tsx new file mode 100644 index 000000000..41739d0f5 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComps/columnPasswordComp.tsx @@ -0,0 +1,118 @@ +import React, { useCallback, useMemo, useState } from "react"; +import styled from "styled-components"; +import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons"; +import { StringOrNumberControl } from "comps/controls/codeControl"; +import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder"; +import { ColumnValueTooltip } from "../simpleColumnTypeComps"; + +const Wrapper = styled.div` + display: inline-flex; + align-items: center; + min-width: 0; + gap: 6px; +`; + +const ValueText = styled.span` + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-variant-ligatures: none; +`; + +const ToggleButton = styled.button` + all: unset; + display: inline-flex; + align-items: center; + cursor: pointer; + opacity: 0.6; + + &:hover { + opacity: 1; + } + + svg { + font-size: 14px; + } +`; + +const childrenMap = { + text: StringOrNumberControl, +}; + +function normalizeToString(value: unknown) { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function maskPassword(raw: string, maskChar = "•", maxMaskLen = 12) { + if (!raw) return ""; + const len = raw.length; + const maskLen = Math.min(len, maxMaskLen); + const masked = maskChar.repeat(maskLen); + return len > maxMaskLen ? `${masked}…` : masked; +} + +const getBaseValue: ColumnTypeViewFn = (props) => + normalizeToString(props.text); + +const PasswordCell = React.memo( + ({ + value, + cellIndex, + }: { + value: string; + cellIndex?: string; + }) => { + const [visible, setVisible] = useState(false); + + const masked = useMemo(() => maskPassword(value), [value]); + + const onToggle = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setVisible((v) => !v); + }, []); + + + React.useEffect(() => { + setVisible(false); + }, [cellIndex, value]); + + if (!value) { + return ; + } + + return ( + + {visible ? value : masked} + + {visible ? : } + + + ); + } +); + +PasswordCell.displayName = "PasswordCell"; + +export const ColumnPasswordComp = new ColumnTypeCompBuilder( + childrenMap, + (props, dispatch) => { + const value = getBaseValue(props, dispatch); + return ; + }, + (nodeValue) => maskPassword(normalizeToString(nodeValue.text.value)), + getBaseValue +) + .setPropertyViewFn((children) => ( + <> + {children.text.propertyView({ + label: "Value", + tooltip: ColumnValueTooltip, + })} + + )) + .build(); + + diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx index 69f18ae83..7e24bd33b 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx @@ -490,7 +490,7 @@ function buildRenderFn( currentIndex: index, }), onTableEvent, - cellIndex: `${column.dataIndex}-${index}`, + cellIndex: `${column.dataIndex}-${record?.[OB_ROW_ORI_INDEX] ?? index}`, }); }; } From 46a43f9a8bd4937663b57506f43ff9d78b88727e Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 22 Dec 2025 21:27:27 +0500 Subject: [PATCH 74/97] [Feat]: #2095 add double click event on the Tree Component --- .../lowcoder/src/comps/comps/treeComp/treeComp.tsx | 6 +++--- .../lowcoder/src/comps/controls/eventHandlerControl.tsx | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx index cc7489237..a2747e009 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx @@ -24,7 +24,7 @@ import { SelectInputValidationSection, } from "../selectInputComp/selectInputConstants"; import { selectInputValidate } from "../selectInputComp/selectInputConstants"; -import { SelectEventHandlerControl } from "comps/controls/eventHandlerControl"; +import { TreeEventHandlerControl } from "comps/controls/eventHandlerControl"; import { trans } from "i18n"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; @@ -63,8 +63,7 @@ const childrenMap = { label: withDefault(LabelControl, { position: "column" }), autoHeight: AutoHeightControl, verticalScrollbar: withDefault(BoolControl, false), - // TODO: more event - onEvent: SelectEventHandlerControl, + onEvent: TreeEventHandlerControl, style: styleControl(InputFieldStyle , 'style'), labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false), 'labelStyle'), inputFieldStyle:styleControl(TreeStyle, 'inputFieldStyle') @@ -127,6 +126,7 @@ const TreeCompView = (props: RecordConstructorToView) => { }} onFocus={() => props.onEvent("focus")} onBlur={() => props.onEvent("blur")} + onDoubleClick={() => props.onEvent("doubleClick")} />
diff --git a/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx b/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx index 631c322c8..967e29210 100644 --- a/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx @@ -778,6 +778,13 @@ export const SelectEventHandlerControl = eventHandlerControl([ blurEvent, ] as const); +export const TreeEventHandlerControl = eventHandlerControl([ + changeEvent, + focusEvent, + blurEvent, + doubleClickEvent, +] as const); + export const ScannerEventHandlerControl = eventHandlerControl([ clickEvent, // scannerSuccessEvent, From 121de6cdf196cb9b1f3983be4ad5fa73339af98d Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 24 Dec 2025 17:42:08 +0500 Subject: [PATCH 75/97] [Feat]: merge app version --- .../lowcoder-design/src/components/Switch.tsx | 32 +- .../lowcoder/src/api/applicationApi.ts | 14 +- .../PermissionDialog/AppPermissionDialog.tsx | 366 +++++++++++++----- .../PermissionDialog/PermissionDialog.tsx | 79 +--- .../PermissionDialog/PermissionList.tsx | 2 +- .../lowcoder/src/components/StepModal.tsx | 2 +- .../src/constants/applicationConstants.ts | 2 + .../packages/lowcoder/src/i18n/locales/en.ts | 5 + .../lowcoder/src/pages/common/header.tsx | 41 +- .../src/pages/common/versionDataForm.tsx | 35 ++ .../pages/queryLibrary/QueryLibraryEditor.tsx | 40 +- .../redux/reduxActions/applicationActions.ts | 2 + .../src/redux/sagas/applicationSagas.ts | 16 +- .../lowcoder/src/util/versionOptions.ts | 25 ++ 14 files changed, 429 insertions(+), 232 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/common/versionDataForm.tsx create mode 100644 client/packages/lowcoder/src/util/versionOptions.ts diff --git a/client/packages/lowcoder-design/src/components/Switch.tsx b/client/packages/lowcoder-design/src/components/Switch.tsx index 576d30411..83dbfba5b 100644 --- a/client/packages/lowcoder-design/src/components/Switch.tsx +++ b/client/packages/lowcoder-design/src/components/Switch.tsx @@ -52,6 +52,24 @@ const SwitchStyle: any = styled.input` border-radius: 20px; background-color: #ffffff; } + + &:disabled { + background-color: #e0e0e0; + opacity: 0.6; + cursor: not-allowed; + } + + &:disabled::before { + background-color: #cccccc; + } + + &:disabled:checked { + background-color: #a0a0a0; + } + + &:disabled:hover { + cursor: not-allowed; + } `; const SwitchDiv = styled.div<{ @@ -104,16 +122,18 @@ const JsIconGray = styled(jsIconGray)` interface SwitchProps extends Omit, "value" | "onChange"> { value: boolean; onChange: (value: boolean) => void; + disabled?: boolean; } export const Switch = (props: SwitchProps) => { - const { value, onChange, ...inputChanges } = props; + const { value, onChange, disabled, ...inputChanges } = props; return ( props.onChange(!props.value)} + checked={value} + onClick={() => onChange(!value)} onChange={() => {}} + disabled={disabled} {...inputChanges} /> ); @@ -154,15 +174,17 @@ export const SwitchWrapper = (props: { export function TacoSwitch(props: { label: string; checked: boolean; - onChange: (checked: boolean) => void; + disabled?: boolean; + onChange?: (checked: boolean) => void; }) { return ( { - props.onChange(value); + props.onChange ? props.onChange(value) : null; }} value={props.checked} + disabled={props.disabled} /> ); diff --git a/client/packages/lowcoder/src/api/applicationApi.ts b/client/packages/lowcoder/src/api/applicationApi.ts index 8ed818b37..9e5234a69 100644 --- a/client/packages/lowcoder/src/api/applicationApi.ts +++ b/client/packages/lowcoder/src/api/applicationApi.ts @@ -70,6 +70,11 @@ export interface ApplicationResp extends ApiResponse { data: ApplicationDetail; } +export interface ApplicationPublishRequest { + commitMessage?: string; + tag: string; +} + interface GrantAppPermissionReq { applicationId: string; role: ApplicationRoleType; @@ -171,8 +176,13 @@ class ApplicationApi extends Api { return Api.put(ApplicationApi.updateApplicationURL(applicationId), rest); } - static publishApplication(request: PublishApplicationPayload): AxiosPromise { - return Api.post(ApplicationApi.publishApplicationURL(request.applicationId)); + static publishApplication( + request: PublishApplicationPayload + ): AxiosPromise { + return Api.post( + ApplicationApi.publishApplicationURL(request.applicationId), + request?.request + ); } static getApplicationDetail(request: FetchAppInfoPayload): AxiosPromise { diff --git a/client/packages/lowcoder/src/components/PermissionDialog/AppPermissionDialog.tsx b/client/packages/lowcoder/src/components/PermissionDialog/AppPermissionDialog.tsx index 69a9afe88..be9abcc46 100644 --- a/client/packages/lowcoder/src/components/PermissionDialog/AppPermissionDialog.tsx +++ b/client/packages/lowcoder/src/components/PermissionDialog/AppPermissionDialog.tsx @@ -12,6 +12,7 @@ import { fetchApplicationPermissions, updateAppPermission, updateAppPermissionInfo, + publishApplication, } from "../../redux/reduxActions/applicationActions"; import { PermissionItemsType } from "./PermissionList"; import { trans } from "../../i18n"; @@ -29,19 +30,62 @@ import { StyledLoading } from "./commonComponents"; import { PermissionRole } from "./Permission"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { default as Divider } from "antd/es/divider"; -import { SocialShareButtons } from "components/SocialShareButtons"; +import { default as Form } from "antd/es/form"; +import { Typography } from "antd"; +import StepModal from "../StepModal"; +import { AddIcon } from "icons"; +import { GreyTextColor } from "constants/style"; +import { VersionDataForm } from "@lowcoder-ee/pages/common/versionDataForm"; -export const AppPermissionDialog = React.memo((props: { - applicationId: string; - visible: boolean; - onVisibleChange: (visible: boolean) => void; -}) => { - const { applicationId } = props; - const dispatch = useDispatch(); - const appPermissionInfo = useSelector(getAppPermissionInfo); +const BottomWrapper = styled.div` + margin: 12px 16px 0 16px; + display: flex; + justify-content: space-between; +`; + +const AddPermissionButton = styled(TacoButton)` + &, + &:hover, + &:focus { + border: none; + box-shadow: none; + padding: 0; + display: flex; + align-items: center; + font-size: 14px; + line-height: 14px; + background: #ffffff; + transition: unset; + } + + svg { + margin-right: 4px; + } - const { appType } = useContext(ExternalEditorContext); - const isModule = appType === AppTypeEnum.Module; + &:hover { + color: #315efb; + + svg g path { + fill: #315efb; + } + } +`; + +export const AppPermissionDialog = React.memo( + (props: { + applicationId: string; + visible: boolean; + onVisibleChange: (visible: boolean) => void; + publishedVersion?: string | undefined; + }) => { + const [form] = Form.useForm(); + const { appType } = useContext(ExternalEditorContext); + const isModule = appType === AppTypeEnum.Module; + const { applicationId, publishedVersion } = props; + + const dispatch = useDispatch(); + const appPermissionInfo = useSelector(getAppPermissionInfo); + const [activeStepKey, setActiveStepKey] = useState("permission"); useEffect(() => { dispatch(fetchApplicationPermissions({ applicationId: applicationId })); @@ -80,76 +124,169 @@ export const AppPermissionDialog = React.memo((props: { } } - return ( - { - if (!appPermissionInfo) { - return ; - } - return ( - <> - - - {list} - - ); - }} - supportRoles={[ - { label: trans("share.viewer"), value: PermissionRole.Viewer }, - { - label: trans("share.editor"), - value: PermissionRole.Editor, - }, - { - label: trans("share.owner"), - value: PermissionRole.Owner, - }, - ]} - permissionItems={permissions} - addPermission={(userIds, groupIds, role, onSuccess) => - ApplicationApi.grantAppPermission({ - applicationId: applicationId, - userIds: userIds, - groupIds: groupIds, - role: role as any, - }) - .then((resp) => { - if (validateResponse(resp)) { - dispatch(fetchApplicationPermissions({ applicationId: applicationId })); - onSuccess(); - } - }) - .catch((e) => { - messageInstance.error(trans("home.addPermissionErrorMessage", { message: e.message })); - }) - } - updatePermission={(permissionId, role) => - dispatch( - updateAppPermission({ - applicationId: applicationId, - role: role as ApplicationRoleType, - permissionId: permissionId, - }) - ) - } - deletePermission={(permissionId) => - dispatch( - deleteAppPermission({ - applicationId: applicationId, - permissionId: permissionId, - }) - ) - } - /> - ); -}); + return ( + { + setActiveStepKey("permission"); + props.onVisibleChange(false); + }} + showOkButton={true} + showBackLink={true} + showCancelButton={true} + width="440px" + onStepChange={setActiveStepKey} + activeStepKey={activeStepKey} + steps={[ + { + key: "permission", + titleRender: () => null, + bodyRender: (modalProps) => ( + { + if (!appPermissionInfo) { + return ; + } + return <>{list}; + }} + supportRoles={[ + { + label: trans("share.viewer"), + value: PermissionRole.Viewer, + }, + { + label: trans("share.editor"), + value: PermissionRole.Editor, + }, + { + label: trans("share.owner"), + value: PermissionRole.Owner, + }, + ]} + permissionItems={permissions} + addPermission={(userIds, groupIds, role, onSuccess) => + ApplicationApi.grantAppPermission({ + applicationId: applicationId, + userIds: userIds, + groupIds: groupIds, + role: role as any, + }) + .then((resp) => { + if (validateResponse(resp)) { + dispatch( + fetchApplicationPermissions({ + applicationId: applicationId, + }) + ); + onSuccess(); + } + }) + .catch((e) => { + messageInstance.error( + trans("home.addPermissionErrorMessage", { + message: e.message, + }) + ); + }) + } + updatePermission={(permissionId, role) => + dispatch( + updateAppPermission({ + applicationId: applicationId, + role: role as ApplicationRoleType, + permissionId: permissionId, + }) + ) + } + deletePermission={(permissionId) => + dispatch( + deleteAppPermission({ + applicationId: applicationId, + permissionId: permissionId, + }) + ) + } + viewFooterRender={(primaryModelProps, props) => ( + + } + onClick={() => { + props.next(); + }} + > + {trans("home.addMember")} + + + { + primaryModelProps.next(); + }} + > + {trans("event.next") + " "} + + + )} + primaryModelProps={modalProps} + /> + ), + footerRender: () => null, + }, + { + key: "versions", + titleRender: () => trans("home.versions"), + bodyRender: () => ( + + ), + footerRender: (modalProps) => ( + + { + modalProps.back(); + }} + > + {trans("back")} + + { + form.validateFields().then(() => { + dispatch( + publishApplication({ + applicationId: applicationId, + request: form.getFieldsValue(), + }) + ); + modalProps.back(); + props.onVisibleChange(false); + }); + }} + > + {trans("queryLibrary.publish")} + + + ), + }, + ]} + /> + ); + } +); const InviteInputBtn = styled.div` display: flex; @@ -196,8 +333,16 @@ function AppShareView(props: { applicationId: string; permissionInfo: AppPermissionInfo; isModule: boolean; + form: any; + publishedVersion?: string; }) { - const { applicationId, permissionInfo, isModule } = props; + const { + applicationId, + permissionInfo, + isModule, + form, + publishedVersion, + } = props; const [isPublic, setPublic] = useState(permissionInfo.publicToAll); const [isPublicToMarketplace, setPublicToMarketplace] = useState(permissionInfo.publicToMarketplace); const dispatch = useDispatch(); @@ -207,11 +352,20 @@ function AppShareView(props: { useEffect(() => { setPublicToMarketplace(permissionInfo.publicToMarketplace); }, [permissionInfo.publicToMarketplace]); - const inviteLink = window.location.origin + APPLICATION_VIEW_URL(props.applicationId, "view"); return (
- + + + - {isPublic && + {isPublic && ( { messageInstance.error(e.message); }); - } } - label={isModule ? trans("home.moduleMarketplaceMessage") : trans("home.appMarketplaceMessage")} /> - } - { isPublicToMarketplace && <>
- {trans("home.marketplaceGoodPublishing")} -
} + }} + label={ + isModule + ? trans("home.moduleMarketplaceMessage") + : trans("home.appMarketplaceMessage") + } + /> + + )} + {isPublicToMarketplace && isPublic && ( +
+ + {trans("home.marketplaceGoodPublishing")} + + +
+ )} - {isPublic && } + {isPublic && } + - {isPublic && - <> - - - - } + - +
+ + {trans("home.publishVersionDescription")} + +
); } diff --git a/client/packages/lowcoder/src/components/PermissionDialog/PermissionDialog.tsx b/client/packages/lowcoder/src/components/PermissionDialog/PermissionDialog.tsx index 42fb2a070..d43ba4e31 100644 --- a/client/packages/lowcoder/src/components/PermissionDialog/PermissionDialog.tsx +++ b/client/packages/lowcoder/src/components/PermissionDialog/PermissionDialog.tsx @@ -2,44 +2,7 @@ import React, { ReactNode, useState } from "react"; import { PermissionItemsType, PermissionList } from "./PermissionList"; import StepModal from "../StepModal"; import { trans } from "../../i18n"; -import { TacoButton } from "components/button"; -import { AddIcon } from "icons"; -import { GreyTextColor } from "constants/style"; import { Permission, PermissionRole } from "./Permission"; -import styled from "styled-components"; - -const BottomWrapper = styled.div` - margin: 12px 16px 0 16px; - display: flex; -`; - -const AddPermissionButton = styled(TacoButton)` - &, - &:hover, - &:focus { - border: none; - box-shadow: none; - padding: 0; - display: flex; - align-items: center; - font-size: 14px; - line-height: 14px; - background: #ffffff; - transition: unset; - } - - svg { - margin-right: 4px; - } - - &:hover { - color: #315efb; - - svg g path { - fill: #315efb; - } - } -`; export const PermissionDialog = (props: { title: string; @@ -47,6 +10,7 @@ export const PermissionDialog = (props: { visible: boolean; onVisibleChange: (visible: boolean) => void; viewBodyRender?: (list: ReactNode) => ReactNode; + viewFooterRender?: (primaryModelProps: any, props: any) => ReactNode; permissionItems: PermissionItemsType; supportRoles: { label: string; value: PermissionRole }[]; addPermission: ( @@ -57,11 +21,22 @@ export const PermissionDialog = (props: { ) => void; updatePermission: (permissionId: string, role: string) => void; deletePermission: (permissionId: string) => void; + primaryModelProps?: {}; contextType?: "application" | "organization"; organizationId?: string; }) => { - const { supportRoles, permissionItems, visible, onVisibleChange, addPermission, viewBodyRender, contextType, organizationId } = - props; + const { + supportRoles, + permissionItems, + visible, + onVisibleChange, + addPermission, + viewBodyRender, + viewFooterRender, + primaryModelProps, + contextType, + organizationId, + } = props; const [activeStepKey, setActiveStepKey] = useState("view"); return ( @@ -87,26 +62,10 @@ export const PermissionDialog = (props: { ) : ( ), - footerRender: (props) => ( - - } - onClick={() => { - props.next(); - }} - > - {trans("home.addMember")} - - onVisibleChange(false)} - style={{ marginLeft: "auto", width: "76px", height: "28px" }} - > - {trans("finish") + " "} - - - ), + footerRender: (props) => + viewFooterRender + ? viewFooterRender(primaryModelProps, props) + : null, }, { key: "add", @@ -123,7 +82,7 @@ export const PermissionDialog = (props: { organizationId={organizationId} /> ), - footerRender: (props) => null, + footerRender: () => null, }, ]} /> diff --git a/client/packages/lowcoder/src/components/PermissionDialog/PermissionList.tsx b/client/packages/lowcoder/src/components/PermissionDialog/PermissionList.tsx index f29ef424e..dd44f247c 100644 --- a/client/packages/lowcoder/src/components/PermissionDialog/PermissionList.tsx +++ b/client/packages/lowcoder/src/components/PermissionDialog/PermissionList.tsx @@ -165,7 +165,7 @@ export const PermissionList = (props: { }) => ( <> - {trans("home.memberPermissionList")} + {`${trans("memberSettings.title")}:`} {props.permissionItems.map((item, index) => ( diff --git a/client/packages/lowcoder/src/components/StepModal.tsx b/client/packages/lowcoder/src/components/StepModal.tsx index 13d08319b..5636b2c24 100644 --- a/client/packages/lowcoder/src/components/StepModal.tsx +++ b/client/packages/lowcoder/src/components/StepModal.tsx @@ -25,7 +25,7 @@ export interface StepModalProps extends CustomModalProps { export default function StepModal(props: StepModalProps) { const { steps, activeStepKey, onStepChange, ...modalProps } = props; const [current, setCurrent] = useState(steps[0]?.key); - const currentStepIndex = steps.findIndex((i) => i.key === activeStepKey ?? current); + const currentStepIndex = steps.findIndex((i) => i.key === (activeStepKey ?? current)); const currentStep = currentStepIndex >= 0 ? steps[currentStepIndex] : null; const handleChangeStep = (key: string) => { diff --git a/client/packages/lowcoder/src/constants/applicationConstants.ts b/client/packages/lowcoder/src/constants/applicationConstants.ts index f685eeb8e..136b3be9d 100644 --- a/client/packages/lowcoder/src/constants/applicationConstants.ts +++ b/client/packages/lowcoder/src/constants/applicationConstants.ts @@ -107,6 +107,8 @@ export interface ApplicationMeta { applicationStatus: "NORMAL" | "RECYCLED" | "DELETED"; editingUserId: string | null; lastEditedAt: number; + publishedVersion?: string; + lastPublishedTime?: number; } export interface FolderMeta { diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 47e93620f..a9a7686b8 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -4048,6 +4048,7 @@ export const en = { "fileUploadError": "File upload error", "fileFormatError": "File format error", "groupWithSquareBrackets": "[Group] ", + "managePermissions": "Manage permissions", "allPermissions": "Owner", "appSharingDialogueTitle" : "App Sharing and Permissions", "appSocialSharing" : "Share Your App and Experience on:", @@ -4058,6 +4059,8 @@ export const en = { "appPublicMessage": "Make the app public. Anyone can view.", "modulePublicMessage": "Make the module public. Anyone can view.", "marketplaceURL": "https://api-service.lowcoder.cloud", + "appMemberMessage": "All shared members can view this app.", + "moduleMemberMessage": "All shared members can view this module.", "appMarketplaceMessage": "Publish your App on the Public Marketplace. Anyone can view and copy it from there.", "moduleMarketplaceMessage": "Publish your Module on the Public Marketplace. Anyone can view and copy it from there.", "marketplaceGoodPublishing": "Please make sure your app is well-named and easy to use. Remove any sensitive information before publishing. Also, remove local datasources and replace by static built-in temporary data.", @@ -4080,6 +4083,8 @@ export const en = { "createNavigation": "Create Navigation", "howToUseAPI": "How to use the Open Rest API", "support": "Support", + "versions": "Versions", + "publishVersionDescription": "By publishing, your users will see the current state of your app. Further editing will not be visible until you publish again", }, "support" : { diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index dc17ccb37..8dd758386 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -60,7 +60,6 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" import { EditorContext } from "../../comps/editorState"; import Tooltip from "antd/es/tooltip"; import { LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; -import Avatar from 'antd/es/avatar'; import UserApi from "@lowcoder-ee/api/userApi"; import { validateResponse } from "@lowcoder-ee/api/apiUtils"; import ProfileImage from "./profileImage"; @@ -612,18 +611,22 @@ export default function Header(props: HeaderProps) { onVisibleChange={(visible) => !visible && setPermissionDialogVisible(false) } + publishedVersion={application?.publishedVersion} /> )} {canManageApp(user, application) && ( - setPermissionDialogVisible(true)} disabled={blockEditing}> - {SHARE_TITLE} + setPermissionDialogVisible(true)} + disabled={blockEditing} + > + {trans("header.deploy")} )} - + preview(applicationId)}> {trans("header.preview")} - + { if (blockEditing) return; // Prevent clicks if the app is being edited by someone else - if (e.key === "deploy") { - dispatch(publishApplication({ applicationId })); - } else if (e.key === "snapshot") { + if (e.key === "snapshot") { dispatch(setShowAppSnapshot(true)); } }} items={[ - { - key: "deploy", - label: ( -
- {blockEditing && } - - {trans("header.deploy")} - -
- ), - disabled: blockEditing, - }, { key: "snapshot", label: ( -
- {blockEditing && } - +
+ {blockEditing && ( + + )} + {trans("header.snapshot")}
@@ -672,7 +665,7 @@ export default function Header(props: HeaderProps) { - + ); diff --git a/client/packages/lowcoder/src/pages/common/versionDataForm.tsx b/client/packages/lowcoder/src/pages/common/versionDataForm.tsx new file mode 100644 index 000000000..c513ae8bd --- /dev/null +++ b/client/packages/lowcoder/src/pages/common/versionDataForm.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { + DatasourceForm, + FormInputItem, + FormRadioItem, + FormSection, +} from "lowcoder-design"; +import { getVersionOptions } from "@lowcoder-ee/util/versionOptions"; +import { trans } from "../../i18n"; + +export const VersionDataForm = (props: { form: any; preserve: boolean, latestVersion?: string }) => { + const { form, preserve, latestVersion } = props; + const versionOptions = getVersionOptions(latestVersion); + + return ( + + + + + + + ); +}; diff --git a/client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx b/client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx index 706c544b4..8f494f21d 100644 --- a/client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx +++ b/client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx @@ -48,6 +48,8 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" import { Helmet } from "react-helmet"; import {fetchQLPaginationByOrg} from "@lowcoder-ee/util/pagination/axios"; import { isEmpty } from "lodash"; +import { getVersionOptions } from "@lowcoder-ee/util/versionOptions"; +import { VersionDataForm } from "../common/versionDataForm"; import { processCurlData } from "../../util/curlUtils"; const Wrapper = styled.div` @@ -69,7 +71,7 @@ interface ElementsState { function transformData(input: LibraryQuery[]) { const output: any = {}; - input.forEach(item => { + input.forEach((item) => { output[item.id] = item; }); return output; @@ -369,45 +371,11 @@ const PublishModal = (props: {
} > - - - - - - + ); }; -function getVersionOptions(version?: string): Array { - if (!version) { - return [ - { label: "v1.0.0", value: "v1.0.0" }, - { label: "v0.1.0", value: "v0.1.0" }, - ]; - } - const [major, minor, patch] = version.slice(1).split("."); - return [ - { - label: ["v" + (Number(major) + 1), 0, 0].join("."), - value: ["v" + (Number(major) + 1), 0, 0].join("."), - }, - { - label: ["v" + major, Number(minor) + 1, 0].join("."), - value: ["v" + major, Number(minor) + 1, 0].join("."), - }, - { - label: ["v" + major, minor, Number(patch) + 1].join("."), - value: ["v" + major, minor, Number(patch) + 1].join("."), - }, - ]; -} - function useSaveQueryLibrary( query: LibraryQuery, instance: InstanceType | null diff --git a/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts b/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts index 83be6cdbb..c07ce8c7b 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts @@ -8,6 +8,7 @@ import { } from "constants/applicationConstants"; import { JSONValue } from "util/jsonTypes"; import { CommonSettingResponseData } from "api/commonSettingApi"; +import { ApplicationPublishRequest } from "@lowcoder-ee/api/applicationApi"; export interface HomeDataPayload { applicationType?: AppTypeEnum; @@ -114,6 +115,7 @@ export const updateAppMetaAction = (payload: UpdateAppMetaPayload) => ({ export type PublishApplicationPayload = { applicationId: string; + request: ApplicationPublishRequest; }; export const publishApplication = (payload: PublishApplicationPayload) => ({ type: ReduxActionTypes.PUBLISH_APPLICATION, diff --git a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts index 062f38145..ef5e22e85 100644 --- a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts @@ -179,7 +179,7 @@ export function* updateApplicationMetaSaga(action: ReduxAction) { try { - const response: AxiosResponse = yield call( + const response: AxiosResponse = yield call( ApplicationApi.publishApplication, action.payload ); @@ -189,6 +189,20 @@ export function* publishApplicationSaga(action: ReduxAction { + if (!version) { + return [ + { label: "v1.0.0", value: "v1.0.0" }, + { label: "v0.1.0", value: "v0.1.0" }, + ]; + } + const [major, minor, patch] = version.slice(1).split("."); + return [ + { + label: ["v" + (Number(major) + 1), 0, 0].join("."), + value: ["v" + (Number(major) + 1), 0, 0].join("."), + }, + { + label: ["v" + major, Number(minor) + 1, 0].join("."), + value: ["v" + major, Number(minor) + 1, 0].join("."), + }, + { + label: ["v" + major, minor, Number(patch) + 1].join("."), + value: ["v" + major, minor, Number(patch) + 1].join("."), + }, + ]; +} From 59784202fad995747e0dbe7dd48260595adb0af8 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 24 Dec 2025 22:22:50 +0500 Subject: [PATCH 76/97] fix UI of select component inside permission dialog --- .../src/components/PermissionDialog/commonComponents.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/components/PermissionDialog/commonComponents.tsx b/client/packages/lowcoder/src/components/PermissionDialog/commonComponents.tsx index f5a9f884a..8a06b39ca 100644 --- a/client/packages/lowcoder/src/components/PermissionDialog/commonComponents.tsx +++ b/client/packages/lowcoder/src/components/PermissionDialog/commonComponents.tsx @@ -9,8 +9,10 @@ export const StyledRoleSelect = styled(CustomSelect)` right: 0; } - .ant-select-selector { - border: none !important; + .ant-select .ant-select-selector { + margin-right: 0 !important; + padding: 0 !important; + padding: 12px !important; } .ant-select:hover { From 4cfd8d9250f30751e97ac7fe46ad7bab8cea0955 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 12:02:28 +0500 Subject: [PATCH 77/97] fix code --- client/packages/lowcoder/package.json | 7 ++- .../navComp/components/DroppableMenuItem.tsx | 2 - .../comps/navComp/components/MenuItemList.tsx | 2 - .../src/comps/comps/navComp/navComp.tsx | 1 - client/yarn.lock | 60 +++++++++++++------ 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index 418d72a39..6ed1d1b1e 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -16,10 +16,10 @@ "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-sql": "^6.5.4", "@codemirror/search": "^6.5.5", - "@dnd-kit/core": "^5.0.1", + "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^7.0.0", - "@dnd-kit/sortable": "^6.0.0", - "@dnd-kit/utilities": "^3.1.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", @@ -48,6 +48,7 @@ "copy-to-clipboard": "^3.3.3", "core-js": "^3.25.2", "dayjs": "^1.11.13", + "dnd-kit-sortable-tree": "^0.1.73", "echarts": "^5.4.3", "echarts-for-react": "^3.0.2", "echarts-wordcloud": "^2.1.0", diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx index c4f22191a..ffd56d902 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx @@ -66,8 +66,6 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) { data: dropData, }); - // TODO: Remove this later. - // Set ItemKey for previously added sub-menus useEffect(() => { if(!items.length) return; if(!(items[0] instanceof LayoutMenuItemComp)) return; diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index 4c9d0de1e..983489ebf 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -60,7 +60,6 @@ function MenuItemList(props: IMenuItemListProps) { sourcePath.length === targetPath.length && _.isEqual(sourcePath.slice(0, -1), targetPath.slice(0, -1)) ) { - // same level move const from = sourcePath[sourcePath.length - 1]; let to = targetPath[targetPath.length - 1]; if (from < to) { @@ -68,7 +67,6 @@ function MenuItemList(props: IMenuItemListProps) { } onMoveItem(targetPath, from, to); } else { - // cross level move let targetIndex = targetPath[targetPath.length - 1]; let targetListPath = targetPath; let size = 0; diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 940e0110d..bdff4240b 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -69,7 +69,6 @@ ${props=>props.$animationStyle} const DEFAULT_SIZE = 378; -// If it is a number, use the px unit by default function transToPxSize(size: string | number) { return isNumeric(size) ? size + "px" : (size as string); } diff --git a/client/yarn.lock b/client/yarn.lock index b3885ff80..c1fde16cd 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1877,7 +1877,7 @@ __metadata: languageName: node linkType: hard -"@dnd-kit/accessibility@npm:^3.0.0": +"@dnd-kit/accessibility@npm:^3.1.1": version: 3.1.1 resolution: "@dnd-kit/accessibility@npm:3.1.1" dependencies: @@ -1888,17 +1888,17 @@ __metadata: languageName: node linkType: hard -"@dnd-kit/core@npm:^5.0.1": - version: 5.0.3 - resolution: "@dnd-kit/core@npm:5.0.3" +"@dnd-kit/core@npm:^6.3.1": + version: 6.3.1 + resolution: "@dnd-kit/core@npm:6.3.1" dependencies: - "@dnd-kit/accessibility": ^3.0.0 - "@dnd-kit/utilities": ^3.1.0 + "@dnd-kit/accessibility": ^3.1.1 + "@dnd-kit/utilities": ^3.2.2 tslib: ^2.0.0 peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 4ace7c45057ed0a7257ab16b8b0ebf76b135e8d5675d6dd285138b99a17b0edf7f57e02f251b1b17efb055bad32d7c90b96616b6c77b4e775afbfbaddea401c5 + checksum: abe5ca5c63af2652b50df2636111a8eecb1560338f3b57e27af0d4eac31f89a278347049dbd59897aeec262477ef88d7a906a79254360c40480e490ee910947c languageName: node linkType: hard @@ -1915,20 +1915,20 @@ __metadata: languageName: node linkType: hard -"@dnd-kit/sortable@npm:^6.0.0": - version: 6.0.1 - resolution: "@dnd-kit/sortable@npm:6.0.1" +"@dnd-kit/sortable@npm:^10.0.0": + version: 10.0.0 + resolution: "@dnd-kit/sortable@npm:10.0.0" dependencies: - "@dnd-kit/utilities": ^3.1.0 + "@dnd-kit/utilities": ^3.2.2 tslib: ^2.0.0 peerDependencies: - "@dnd-kit/core": ^5.0.2 + "@dnd-kit/core": ^6.3.0 react: ">=16.8.0" - checksum: beb80a229a50885a654ff15ee98af3b34b02826cadd6bc2f94b79dd103a140f70c35d0a3bf422adf87327573ff15dc3e26e9e5769e0f67b68943d8eaa9560183 + checksum: c853cb65d2ffb3d58d400d9f1c993b00413932acf5cf5b780c76acf3b1057aa88e7866021c6b178c4b33fc17db7fe7640584dba4449772e02edcb72cc797eeb0 languageName: node linkType: hard -"@dnd-kit/utilities@npm:^3.1.0, @dnd-kit/utilities@npm:^3.2.2": +"@dnd-kit/utilities@npm:^3.2.2": version: 3.2.2 resolution: "@dnd-kit/utilities@npm:3.2.2" dependencies: @@ -7340,7 +7340,7 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^1.0.4, clsx@npm:^1.1.1": +"clsx@npm:^1.0.4, clsx@npm:^1.1.1, clsx@npm:^1.2.1": version: 1.2.1 resolution: "clsx@npm:1.2.1" checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 @@ -8899,6 +8899,22 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"dnd-kit-sortable-tree@npm:^0.1.73": + version: 0.1.73 + resolution: "dnd-kit-sortable-tree@npm:0.1.73" + dependencies: + clsx: ^1.2.1 + react-merge-refs: ^2.0.1 + peerDependencies: + "@dnd-kit/core": ">=6.0.5" + "@dnd-kit/sortable": ">=7.0.1" + "@dnd-kit/utilities": ">=3.2.0" + react: ">=16" + react-dom: ">=16" + checksum: 86bec921ebb4484f03848fccac21654b9a98ef590978815c45b908a297d28faf88094093545e74387315a70a8f661c497d32987f34573e6cd2bd44aed0314cad + languageName: node + linkType: hard + "dns-packet@npm:^5.2.2": version: 5.6.1 resolution: "dns-packet@npm:5.6.1" @@ -14113,10 +14129,10 @@ coolshapes-react@lowcoder-org/coolshapes-react: "@codemirror/lang-json": ^6.0.1 "@codemirror/lang-sql": ^6.5.4 "@codemirror/search": ^6.5.5 - "@dnd-kit/core": ^5.0.1 + "@dnd-kit/core": ^6.3.1 "@dnd-kit/modifiers": ^7.0.0 - "@dnd-kit/sortable": ^6.0.0 - "@dnd-kit/utilities": ^3.1.0 + "@dnd-kit/sortable": ^10.0.0 + "@dnd-kit/utilities": ^3.2.2 "@fortawesome/fontawesome-svg-core": ^6.5.1 "@fortawesome/free-brands-svg-icons": ^6.5.1 "@fortawesome/free-regular-svg-icons": ^6.5.1 @@ -14154,6 +14170,7 @@ coolshapes-react@lowcoder-org/coolshapes-react: copy-to-clipboard: ^3.3.3 core-js: ^3.25.2 dayjs: ^1.11.13 + dnd-kit-sortable-tree: ^0.1.73 dotenv: ^16.0.3 echarts: ^5.4.3 echarts-for-react: ^3.0.2 @@ -17955,6 +17972,13 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"react-merge-refs@npm:^2.0.1": + version: 2.1.1 + resolution: "react-merge-refs@npm:2.1.1" + checksum: 40564bc4c16520ef830d4fe7a2bd298c23a42d644a8fcb2353cdc6cf16aa82eac681c554df4c849397b25af9dbe728086566e1a01f65f95d2301dc8fc8f6809f + languageName: node + linkType: hard + "react-player@npm:^2.11.0": version: 2.16.0 resolution: "react-player@npm:2.16.0" From b491ce0b22ee9fa1ec2f60d5200dcd2ae9a66180 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 12:13:03 +0500 Subject: [PATCH 78/97] add sortable tree for nav --- .../navComp/components/DraggableItem.tsx | 106 -------- .../navComp/components/DroppableMenuItem.tsx | 128 ---------- .../components/DroppablePlaceHolder.tsx | 43 ---- .../comps/navComp/components/MenuItem.tsx | 85 +++---- .../comps/navComp/components/MenuItemList.tsx | 238 +++++++++++------- .../comps/comps/navComp/components/types.ts | 15 +- 6 files changed, 193 insertions(+), 422 deletions(-) delete mode 100644 client/packages/lowcoder/src/comps/comps/navComp/components/DraggableItem.tsx delete mode 100644 client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx delete mode 100644 client/packages/lowcoder/src/comps/comps/navComp/components/DroppablePlaceHolder.tsx diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/DraggableItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/DraggableItem.tsx deleted file mode 100644 index 7a4c6ba1b..000000000 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/DraggableItem.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { DragIcon } from "lowcoder-design"; -import React, { Ref } from "react"; -import { HTMLAttributes, ReactNode } from "react"; -import styled from "styled-components"; - -const Wrapper = styled.div<{ $dragging: boolean; $isOver: boolean; $dropInAsSub: boolean }>` - position: relative; - width: 100%; - height: 30px; - border: 1px solid #d7d9e0; - border-radius: 4px; - margin-bottom: 4px; - display: flex; - padding: 0 8px; - background-color: #ffffff; - align-items: center; - opacity: ${(props) => (props.$dragging ? "0.5" : 1)}; - - &::after { - content: ""; - display: ${(props) => (props.$isOver ? "block" : "none")}; - height: 4px; - border-radius: 4px; - position: absolute; - left: ${(props) => (props.$dropInAsSub ? "15px" : "-1px")}; - right: 0; - background-color: #315efb; - bottom: -5px; - } - - .draggable-handle-icon { - &:hover, - &:focus { - cursor: grab; - } - - &, - & > svg { - width: 16px; - height: 16px; - } - } - - .draggable-text { - color: #333; - font-size: 13px; - margin-left: 4px; - height: 100%; - display: flex; - align-items: center; - flex: 1; - overflow: hidden; - cursor: pointer; - - & > div { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - display: inline-block; - height: 28px; - line-height: 28px; - } - } - - .draggable-extra-icon { - cursor: pointer; - - &, - & > svg { - width: 16px; - height: 16px; - } - } -`; - -interface IProps extends HTMLAttributes { - dragContent: ReactNode; - isOver?: boolean; - extra?: ReactNode; - dragging?: boolean; - dropInAsSub?: boolean; - dragListeners?: Record; -} - -function DraggableItem(props: IProps, ref: Ref) { - const { - dragContent: text, - extra, - dragging = false, - dropInAsSub = true, - isOver = false, - dragListeners, - ...divProps - } = props; - return ( - -
- -
-
{text}
-
{extra}
-
- ); -} - -export default React.forwardRef(DraggableItem); diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx deleted file mode 100644 index ffd56d902..000000000 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useDraggable, useDroppable } from "@dnd-kit/core"; -import { trans } from "i18n"; -import { Fragment, useEffect } from "react"; -import styled from "styled-components"; -import DroppablePlaceholder from "./DroppablePlaceHolder"; -import MenuItem, { ICommonItemProps } from "./MenuItem"; -import { IDragData, IDropData } from "./types"; -import { LayoutMenuItemComp } from "comps/comps/layout/layoutMenuItemComp"; -import { genRandomKey } from "comps/utils/idGenerator"; - -const DraggableMenuItemWrapper = styled.div` - position: relative; -`; - -interface IDraggableMenuItemProps extends ICommonItemProps { - level: number; - active?: boolean; - disabled?: boolean; - disableDropIn?: boolean; - parentDragging?: boolean; -} - -export default function DraggableMenuItem(props: IDraggableMenuItemProps) { - const { - item, - path, - active, - disabled, - parentDragging, - disableDropIn, - dropInAsSub = true, - onAddSubMenu, - onDelete, - } = props; - - const id = path.join("_"); - const items = item.getView().items; - - const handleAddSubMenu = (path: number[]) => { - onAddSubMenu?.(path, { - label: trans("droppadbleMenuItem.subMenu", { number: items.length + 1 }), - }); - }; - - const dragData: IDragData = { - path, - item, - }; - const { - listeners: dragListeners, - setNodeRef: setDragNodeRef, - isDragging, - } = useDraggable({ - id, - data: dragData, - }); - - const dropData: IDropData = { - targetListSize: items.length, - targetPath: dropInAsSub ? [...path, 0] : [...path.slice(0, -1), path[path.length - 1] + 1], - dropInAsSub, - }; - const { setNodeRef: setDropNodeRef, isOver } = useDroppable({ - id, - disabled: isDragging || disabled || disableDropIn, - data: dropData, - }); - - useEffect(() => { - if(!items.length) return; - if(!(items[0] instanceof LayoutMenuItemComp)) return; - - return items.forEach(item => { - const subItem = item as LayoutMenuItemComp; - const itemKey = subItem.children.itemKey.getView(); - if(itemKey === '') { - subItem.children.itemKey.dispatchChangeValueAction(genRandomKey()) - } - }) - }, [items]) - - return ( - <> - - {active && ( - - )} - { - setDragNodeRef(node); - setDropNodeRef(node); - }} - isOver={isOver} - dropInAsSub={dropInAsSub} - dragging={isDragging || parentDragging} - dragListeners={{ ...dragListeners }} - onAddSubMenu={onAddSubMenu && handleAddSubMenu} - onDelete={onDelete} - /> - - {items.length > 0 && ( -
- {items.map((subItem, i) => ( - - - - ))} -
- )} - - ); -} diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppablePlaceHolder.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppablePlaceHolder.tsx deleted file mode 100644 index 72c15cf85..000000000 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppablePlaceHolder.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useDroppable } from "@dnd-kit/core"; -import styled from "styled-components"; -import { IDropData } from "./types"; - -interface IDroppablePlaceholderProps { - path: number[]; - disabled?: boolean; - targetListSize: number; -} - -const PlaceHolderWrapper = styled.div<{ $active: boolean }>` - position: absolute; - width: 100%; - top: -4px; - height: 25px; - z-index: 10; - /* background-color: rgba(0, 0, 0, 0.2); */ - .position-line { - height: 4px; - border-radius: 4px; - background-color: ${(props) => (props.$active ? "#315efb" : "transparent")}; - width: 100%; - } -`; - -export default function DroppablePlaceholder(props: IDroppablePlaceholderProps) { - const { path, disabled, targetListSize } = props; - const data: IDropData = { - targetPath: path, - targetListSize, - dropInAsSub: false, - }; - const { setNodeRef: setDropNodeRef, isOver } = useDroppable({ - id: `p_${path.join("_")}`, - disabled, - data, - }); - return ( - -
-
- ); -} diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItem.tsx index b328c30b0..8eb740199 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItem.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItem.tsx @@ -1,24 +1,16 @@ import { ActiveTextColor, GreyTextColor } from "constants/style"; import { EditPopover, SimplePopover } from "lowcoder-design"; import { PointIcon } from "lowcoder-design"; -import React, { HTMLAttributes, useState } from "react"; +import React, { useState } from "react"; import styled from "styled-components"; -import DraggableItem from "./DraggableItem"; import { NavCompType } from "comps/comps/navComp/components/types"; import { trans } from "i18n"; -export interface ICommonItemProps { - path: number[]; +export interface IMenuItemProps { item: NavCompType; - dropInAsSub?: boolean; - onDelete?: (path: number[]) => void; - onAddSubMenu?: (path: number[], value?: any) => void; -} - -interface IMenuItemProps extends ICommonItemProps, Omit, "id"> { - isOver?: boolean; - dragging?: boolean; - dragListeners?: Record; + onDelete?: () => void; + onAddSubMenu?: () => void; + showAddSubMenu?: boolean; } const MenuItemWrapper = styled.div` @@ -29,6 +21,13 @@ const MenuItemWrapper = styled.div` const MenuItemContent = styled.div` width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; + flex: 1; + color: #333; + font-size: 13px; `; const StyledPointIcon = styled(PointIcon)` @@ -39,61 +38,45 @@ const StyledPointIcon = styled(PointIcon)` } `; -const MenuItem = React.forwardRef((props: IMenuItemProps, ref: React.Ref) => { +const MenuItem: React.FC = (props) => { const { - path, item, - isOver, - dragging, - dragListeners, - dropInAsSub = true, onDelete, onAddSubMenu, - ...divProps + showAddSubMenu = true, } = props; const [isConfigPopShow, showConfigPop] = useState(false); const handleDel = () => { - onDelete?.(path); + onDelete?.(); }; const handleAddSubMenu = () => { - onAddSubMenu?.(path); + onAddSubMenu?.(); }; const content = {item.getPropertyView()}; return ( - - {item.children.label.getView()} - - } - extra={ - - - - } - /> + <> + + {item.children.label.getView()} + + + + + ); -}); +}; export default MenuItem; diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index 983489ebf..53c822049 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -1,14 +1,11 @@ -import { DndContext, DragEndEvent, DragOverlay, DragStartEvent } from "@dnd-kit/core"; +import { SortableTree, TreeItems, TreeItemComponentProps, SimpleTreeItemWrapper } from "dnd-kit-sortable-tree"; import LinkPlusButton from "components/LinkPlusButton"; import { BluePlusIcon, controlItem } from "lowcoder-design"; import { trans } from "i18n"; -import _ from "lodash"; -import { useState } from "react"; +import React, { useMemo, useCallback, createContext, useContext } from "react"; import styled from "styled-components"; -import DraggableMenuItem from "./DroppableMenuItem"; -import DroppablePlaceholder from "./DroppablePlaceHolder"; +import { NavCompType, NavListCompType, NavTreeItemData } from "./types"; import MenuItem from "./MenuItem"; -import { IDragData, IDropData, NavCompType, NavListCompType } from "./types"; const Wrapper = styled.div` .menu-title { @@ -16,118 +13,155 @@ const Wrapper = styled.div` flex-direction: row; justify-content: space-between; align-items: center; + margin-bottom: 8px; } .menu-list { - margin-top: 8px; position: relative; } +`; - .sub-menu-list { - padding-left: 16px; +const TreeItemContent = styled.div` + display: flex; + align-items: center; + width: 100%; + height: 30px; + background-color: #ffffff; + border: 1px solid #d7d9e0; + border-radius: 4px; + padding: 0 8px; + box-sizing: border-box; + gap: 8px; + + &:hover { + border-color: #315efb; } `; +// Context for passing handlers to tree items +interface MenuItemHandlers { + onDeleteItem: (path: number[]) => void; + onAddSubItem: (path: number[], value?: any) => void; +} + +const MenuItemHandlersContext = createContext(null); + +// Tree item component +const NavTreeItemComponent = React.forwardRef< + HTMLDivElement, + TreeItemComponentProps +>((props, ref) => { + const { item, depth, ...rest } = props; + const { comp, path } = item; + const handlers = useContext(MenuItemHandlersContext); + + const hasChildren = item.children && item.children.length > 0; + + const handleDelete = () => { + handlers?.onDeleteItem(path); + }; + + const handleAddSubMenu = () => { + handlers?.onAddSubItem(path, { + label: `Sub Menu ${(item.children?.length || 0) + 1}`, + }); + }; + + return ( + + + + + + ); +}); + +NavTreeItemComponent.displayName = "NavTreeItemComponent"; + interface IMenuItemListProps { items: NavCompType[]; onAddItem: (path: number[], value?: any) => number; onDeleteItem: (path: number[]) => void; onAddSubItem: (path: number[], value: any, unshift?: boolean) => number; onMoveItem: (path: number[], from: number, to: number) => void; + onReorderItems: (newOrder: TreeItems) => void; } const menuItemLabel = trans("navigation.itemsDesc"); +// Convert NavCompType[] to TreeItems format for dnd-kit-sortable-tree +function convertToTreeItems( + items: NavCompType[], + basePath: number[] = [] +): TreeItems { + return items.map((item, index) => { + const path = [...basePath, index]; + const subItems = item.getView().items || []; + + return { + id: path.join("_"), + comp: item, + path: path, + children: subItems.length > 0 + ? convertToTreeItems(subItems, path) + : [], + }; + }); +} + function MenuItemList(props: IMenuItemListProps) { - const { items, onAddItem, onDeleteItem, onMoveItem, onAddSubItem } = props; + const { items, onAddItem, onDeleteItem, onAddSubItem, onReorderItems } = props; - const [active, setActive] = useState(null); - const isDraggingWithSub = active && active.item.children.items.getView().length > 0; + // Convert items to tree format + const treeItems = useMemo(() => convertToTreeItems(items), [items]); - function handleDragStart(event: DragStartEvent) { - setActive(event.active.data.current as IDragData); - } - - function handleDragEnd(e: DragEndEvent) { - const activeData = e.active.data.current as IDragData; - const overData = e.over?.data.current as IDropData; - - if (overData) { - const sourcePath = activeData.path; - const targetPath = overData.targetPath; - - if ( - sourcePath.length === targetPath.length && - _.isEqual(sourcePath.slice(0, -1), targetPath.slice(0, -1)) - ) { - const from = sourcePath[sourcePath.length - 1]; - let to = targetPath[targetPath.length - 1]; - if (from < to) { - to -= 1; - } - onMoveItem(targetPath, from, to); - } else { - let targetIndex = targetPath[targetPath.length - 1]; - let targetListPath = targetPath; - let size = 0; - - onDeleteItem(sourcePath); - - if (overData.dropInAsSub) { - targetListPath = targetListPath.slice(0, -1); - size = onAddSubItem(targetListPath, activeData.item.toJsonValue()); - } else { - size = onAddItem(targetListPath, activeData.item.toJsonValue()); - } - - if (overData.targetListSize !== -1) { - onMoveItem(targetListPath, size, targetIndex); - } - } - } + // Handle tree changes from drag and drop + const handleItemsChanged = useCallback( + (newItems: TreeItems) => { + onReorderItems(newItems); + }, + [onReorderItems] + ); - setActive(null); - } + // Handlers context value + const handlers = useMemo( + () => ({ + onDeleteItem, + onAddSubItem, + }), + [onDeleteItem, onAddSubItem] + ); return ( - -
-
{menuItemLabel}
- onAddItem([0])} icon={}> - {trans("newItem")} - -
-
- {items.map((i, idx) => { - return ( - - ); - })} -
- {active && } -
-
- - {active && } - -
+
+
{menuItemLabel}
+ onAddItem([0])} icon={}> + {trans("newItem")} + +
+
+ + + +
); } export function menuPropertyView(itemsComp: NavListCompType) { const items = itemsComp.getView(); + const getItemByPath = (path: number[], scope?: NavCompType[]): NavCompType => { if (!scope) { scope = items; @@ -148,6 +182,37 @@ export function menuPropertyView(itemsComp: NavListCompType) { return getItemListByPath(path.slice(1), root.getView()[path[0]].children.items); }; + // Convert flat tree structure back to nested comp structure + const handleReorderItems = (newItems: TreeItems) => { + // Build the new order from tree items + const buildJsonFromTree = (treeItems: TreeItems): any[] => { + return treeItems.map((item) => { + const jsonValue = item.comp.toJsonValue() as Record; + return { + ...jsonValue, + items: item.children && item.children.length > 0 + ? buildJsonFromTree(item.children) + : [], + }; + }); + }; + + const newJson = buildJsonFromTree(newItems); + + // Clear all existing items and re-add in new order + const currentLength = itemsComp.getView().length; + + // Delete all items from end to start + for (let i = currentLength - 1; i >= 0; i--) { + itemsComp.deleteItem(i); + } + + // Add items back in new order + newJson.forEach((itemJson) => { + itemsComp.addItem(itemJson); + }); + }; + return controlItem( { filterText: menuItemLabel }, { getItemListByPath(path).moveItem(from, to); }} + onReorderItems={handleReorderItems} /> ); } diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/types.ts b/client/packages/lowcoder/src/comps/comps/navComp/components/types.ts index 09640aac3..ede5270cc 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/types.ts +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/types.ts @@ -1,5 +1,6 @@ import { NavItemComp, navListComp } from "../navItemComp"; import { LayoutMenuItemComp, LayoutMenuItemListComp } from "comps/comps/layout/layoutMenuItemComp"; +import { TreeItem } from "dnd-kit-sortable-tree"; export type NavCompType = NavItemComp | LayoutMenuItemComp; @@ -15,13 +16,11 @@ export interface NavCompItemType { onEvent: (name: string) => void; } -export interface IDropData { - targetListSize: number; - targetPath: number[]; - dropInAsSub: boolean; -} - -export interface IDragData { - item: NavCompType; +// Tree item data for dnd-kit-sortable-tree +export interface NavTreeItemData { + comp: NavCompType; path: number[]; } + +// Full tree item type for the sortable tree +export type NavTreeItem = TreeItem; From b21552a72a50625ab80cde7ac7097e2c5278eee9 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 13:50:38 +0500 Subject: [PATCH 79/97] add collapsible --- .../comps/navComp/components/MenuItemList.tsx | 57 +++++++++++++++++-- .../comps/comps/navComp/components/types.ts | 1 + 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index 53c822049..6bc10de97 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -2,7 +2,7 @@ import { SortableTree, TreeItems, TreeItemComponentProps, SimpleTreeItemWrapper import LinkPlusButton from "components/LinkPlusButton"; import { BluePlusIcon, controlItem } from "lowcoder-design"; import { trans } from "i18n"; -import React, { useMemo, useCallback, createContext, useContext } from "react"; +import React, { useMemo, useCallback, createContext, useContext, useState } from "react"; import styled from "styled-components"; import { NavCompType, NavListCompType, NavTreeItemData } from "./types"; import MenuItem from "./MenuItem"; @@ -42,6 +42,8 @@ const TreeItemContent = styled.div` interface MenuItemHandlers { onDeleteItem: (path: number[]) => void; onAddSubItem: (path: number[], value?: any) => void; + collapsedItems: Set; + onToggleCollapse: (id: string) => void; } const MenuItemHandlersContext = createContext(null); @@ -51,6 +53,7 @@ const NavTreeItemComponent = React.forwardRef< HTMLDivElement, TreeItemComponentProps >((props, ref) => { + console.log("NavTreeItemComponent", props); const { item, depth, ...rest } = props; const { comp, path } = item; const handlers = useContext(MenuItemHandlersContext); @@ -97,18 +100,21 @@ const menuItemLabel = trans("navigation.itemsDesc"); // Convert NavCompType[] to TreeItems format for dnd-kit-sortable-tree function convertToTreeItems( items: NavCompType[], - basePath: number[] = [] + basePath: number[] = [], + collapsedItems: Set = new Set() ): TreeItems { return items.map((item, index) => { const path = [...basePath, index]; + const id = path.join("_"); const subItems = item.getView().items || []; return { - id: path.join("_"), + id, comp: item, path: path, + collapsed: collapsedItems.has(id), children: subItems.length > 0 - ? convertToTreeItems(subItems, path) + ? convertToTreeItems(subItems, path, collapsedItems) : [], }; }); @@ -117,12 +123,48 @@ function convertToTreeItems( function MenuItemList(props: IMenuItemListProps) { const { items, onAddItem, onDeleteItem, onAddSubItem, onReorderItems } = props; + // State for tracking collapsed items + const [collapsedItems, setCollapsedItems] = useState>(new Set()); + + // Toggle collapse state for an item + const handleToggleCollapse = useCallback((id: string) => { + setCollapsedItems(prev => { + const newSet = new Set(prev); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + return newSet; + }); + }, []); + // Convert items to tree format - const treeItems = useMemo(() => convertToTreeItems(items), [items]); + const treeItems = useMemo(() => convertToTreeItems(items, [], collapsedItems), [items, collapsedItems]); // Handle tree changes from drag and drop const handleItemsChanged = useCallback( (newItems: TreeItems) => { + // Update collapsed state from the new items + const updateCollapsedState = (treeItems: TreeItems) => { + treeItems.forEach(item => { + if (item.collapsed !== undefined) { + setCollapsedItems(prev => { + const newSet = new Set(prev); + if (item.collapsed) { + newSet.add(item.id as string); + } else { + newSet.delete(item.id as string); + } + return newSet; + }); + } + if (item.children && item.children.length > 0) { + updateCollapsedState(item.children); + } + }); + }; + updateCollapsedState(newItems); onReorderItems(newItems); }, [onReorderItems] @@ -133,8 +175,10 @@ function MenuItemList(props: IMenuItemListProps) { () => ({ onDeleteItem, onAddSubItem, + collapsedItems, + onToggleCollapse: handleToggleCollapse, }), - [onDeleteItem, onAddSubItem] + [onDeleteItem, onAddSubItem, collapsedItems, handleToggleCollapse] ); return ( @@ -152,6 +196,7 @@ function MenuItemList(props: IMenuItemListProps) { onItemsChanged={handleItemsChanged} TreeItemComponent={NavTreeItemComponent} indentationWidth={20} + />
diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/types.ts b/client/packages/lowcoder/src/comps/comps/navComp/components/types.ts index ede5270cc..86e45194c 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/types.ts +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/types.ts @@ -20,6 +20,7 @@ export interface NavCompItemType { export interface NavTreeItemData { comp: NavCompType; path: number[]; + collapsed?: boolean; } // Full tree item type for the sortable tree From 9ee94782307275b3f4066223e87598952764ce7a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 14:03:37 +0500 Subject: [PATCH 80/97] fix linter errors --- client/packages/lowcoder-design/src/components/option.tsx | 8 ++++---- .../lowcoder/src/comps/comps/formComp/createForm.tsx | 4 ++-- .../lowcoder/src/comps/comps/listViewComp/listView.tsx | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/packages/lowcoder-design/src/components/option.tsx b/client/packages/lowcoder-design/src/components/option.tsx index 4e62301f8..d35ee0be1 100644 --- a/client/packages/lowcoder-design/src/components/option.tsx +++ b/client/packages/lowcoder-design/src/components/option.tsx @@ -9,7 +9,7 @@ import { CSS } from "@dnd-kit/utilities"; import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { ConstructorToComp, MultiCompConstructor } from "lowcoder-core"; import { ReactComponent as WarnIcon } from "icons/v1/icon-warning-white.svg"; -import { DndContext } from "@dnd-kit/core"; +import { DndContext, DragEndEvent } from "@dnd-kit/core"; import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; import { ActiveTextColor, GreyTextColor } from "constants/style"; import { trans } from "i18n/design"; @@ -225,12 +225,12 @@ function Option>(props: { } return -1; }; - const handleDragEnd = (e: { active: { id: string }; over: { id: string } | null }) => { + const handleDragEnd = (e: DragEndEvent) => { if (!e.over) { return; } - const fromIndex = findIndex(e.active.id); - const toIndex = findIndex(e.over.id); + const fromIndex = findIndex(String(e.active.id)); + const toIndex = findIndex(String(e.over.id)); if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) { return; } diff --git a/client/packages/lowcoder/src/comps/comps/formComp/createForm.tsx b/client/packages/lowcoder/src/comps/comps/formComp/createForm.tsx index 6d4f2fc9a..46ca12d17 100644 --- a/client/packages/lowcoder/src/comps/comps/formComp/createForm.tsx +++ b/client/packages/lowcoder/src/comps/comps/formComp/createForm.tsx @@ -28,7 +28,7 @@ import log from "loglevel"; import { Datasource } from "@lowcoder-ee/constants/datasourceConstants"; import DataSourceIcon from "components/DataSourceIcon"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; -import { DndContext } from "@dnd-kit/core"; +import { DndContext, DragEndEvent } from "@dnd-kit/core"; import { SortableContext, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; @@ -599,7 +599,7 @@ const CreateFormBody = (props: { onCreate: CreateHandler }) => { setItems(initItems); }, [dataSourceTypeConfig, tableStructure, form]); - const handleDragEnd = useCallback((e: { active: { id: string }; over: { id: string } | null }) => { + const handleDragEnd = useCallback((e: DragEndEvent) => { if (!e.over) { return; } diff --git a/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx b/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx index 44ff6753d..849bb4322 100644 --- a/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx +++ b/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx @@ -22,7 +22,7 @@ import { useMergeCompStyles } from "@lowcoder-ee/util/hooks"; import { childrenToProps } from "@lowcoder-ee/comps/generators/multi"; import { AnimationStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants"; import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; -import { DndContext } from "@dnd-kit/core"; +import { DndContext, DragEndEvent } from "@dnd-kit/core"; import { SortableContext, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { JSONObject } from "@lowcoder-ee/util/jsonTypes"; @@ -354,7 +354,7 @@ export function ListView(props: Props) { useMergeCompStyles(childrenProps, comp.dispatch); - const handleDragEnd = (e: { active: { id: string }; over: { id: string } | null }) => { + const handleDragEnd = (e: DragEndEvent) => { if (!e.over) { return; } From c85ecb55d5948f5ef32a1842720244f65e4093c3 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 14:15:47 +0500 Subject: [PATCH 81/97] add max depth --- .../src/comps/comps/navComp/components/MenuItemList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index 6bc10de97..eb583edd7 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -6,7 +6,7 @@ import React, { useMemo, useCallback, createContext, useContext, useState } from import styled from "styled-components"; import { NavCompType, NavListCompType, NavTreeItemData } from "./types"; import MenuItem from "./MenuItem"; - +const MAX_DEPTH = 3; const Wrapper = styled.div` .menu-title { display: flex; @@ -59,6 +59,8 @@ const NavTreeItemComponent = React.forwardRef< const handlers = useContext(MenuItemHandlersContext); const hasChildren = item.children && item.children.length > 0; + // allow adding sub-menu only if we are above the max depth (depth is 0-indexed) + const canAddSubMenu = depth < MAX_DEPTH - 1; const handleDelete = () => { handlers?.onDeleteItem(path); @@ -77,7 +79,7 @@ const NavTreeItemComponent = React.forwardRef< item={comp} onDelete={handleDelete} onAddSubMenu={handleAddSubMenu} - showAddSubMenu={!hasChildren || depth === 0} + showAddSubMenu={(!hasChildren || depth === 0) && canAddSubMenu} /> From e9f6152fee495005c73b0f172838c2623e926fdf Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 16:06:20 +0500 Subject: [PATCH 82/97] add submenu --- .../src/comps/comps/navComp/navComp.tsx | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index bdff4240b..60722e814 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -512,25 +512,27 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { const disabled = !!view?.disabled; const subItems = isCompItem ? view?.items : []; - const subMenuItems: Array<{ key: string; label: any; icon?: any; disabled?: boolean }> = []; const subMenuSelectedKeys: Array = []; - - if (Array.isArray(subItems)) { - subItems.forEach((subItem: any, originalIndex: number) => { - if (subItem.children.hidden.getView()) { - return; - } - const key = originalIndex + ""; - subItem.children.active.getView() && subMenuSelectedKeys.push(key); - const subIcon = hasIcon(subItem.children.icon?.getView?.()) ? subItem.children.icon.getView() : undefined; - subMenuItems.push({ - key: key, - label: subItem.children.label.getView(), - icon: subIcon, - disabled: !!subItem.children.disabled.getView(), - }); - }); - } + const buildSubMenuItems = (list: any[], prefix = ""): Array => { + if (!Array.isArray(list)) return []; + return list + .map((subItem: any, originalIndex: number) => { + if (subItem.children.hidden.getView()) return null; + const key = prefix ? `${prefix}-${originalIndex}` : `${originalIndex}`; + subItem.children.active.getView() && subMenuSelectedKeys.push(key); + const subIcon = hasIcon(subItem.children.icon?.getView?.()) ? subItem.children.icon.getView() : undefined; + const children = buildSubMenuItems(subItem.getView()?.items, key); + return { + key, + label: subItem.children.label.getView(), + icon: subIcon, + disabled: !!subItem.children.disabled.getView(), + ...(children.length > 0 ? { children } : {}), + }; + }) + .filter(Boolean); + }; + const subMenuItems: Array = buildSubMenuItems(subItems); const item = ( { { if (disabled) return; - const subItem = subItems[Number(e.key)]; - const isSubDisabled = !!subItem?.children?.disabled?.getView?.(); + const parts = String(e.key).split("-").filter(Boolean); + let currentList: any[] = subItems; + let current: any = null; + for (const part of parts) { + current = currentList?.[Number(part)]; + if (!current) return; + currentList = current.getView()?.items || []; + } + const isSubDisabled = !!current?.children?.disabled?.getView?.(); if (isSubDisabled) return; - const onSubEvent = subItem?.getView()?.onEvent; + const onSubEvent = current?.getView?.()?.onEvent; onSubEvent && onSubEvent("click"); }} selectedKeys={subMenuSelectedKeys} From a81d69e329cf4aec4409814249562dec219a8fa8 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 16:42:40 +0500 Subject: [PATCH 83/97] add collapsible --- .../comps/navComp/components/MenuItemList.tsx | 92 +++++++++---------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index eb583edd7..8dad5efdd 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -42,8 +42,6 @@ const TreeItemContent = styled.div` interface MenuItemHandlers { onDeleteItem: (path: number[]) => void; onAddSubItem: (path: number[], value?: any) => void; - collapsedItems: Set; - onToggleCollapse: (id: string) => void; } const MenuItemHandlersContext = createContext(null); @@ -53,9 +51,10 @@ const NavTreeItemComponent = React.forwardRef< HTMLDivElement, TreeItemComponentProps >((props, ref) => { - console.log("NavTreeItemComponent", props); - const { item, depth, ...rest } = props; + const { item, depth, collapsed, ...rest } = props; const { comp, path } = item; + + console.log("NavTreeItemComponent", "collapsed", collapsed); const handlers = useContext(MenuItemHandlersContext); const hasChildren = item.children && item.children.length > 0; @@ -73,7 +72,13 @@ const NavTreeItemComponent = React.forwardRef< }; return ( - + = new Set() + collapsedIds: Set = new Set() ): TreeItems { return items.map((item, index) => { const path = [...basePath, index]; @@ -112,62 +119,51 @@ function convertToTreeItems( return { id, + collapsed: collapsedIds.has(id), comp: item, path: path, - collapsed: collapsedItems.has(id), children: subItems.length > 0 - ? convertToTreeItems(subItems, path, collapsedItems) + ? convertToTreeItems(subItems, path, collapsedIds) : [], }; }); } +function extractCollapsedIds(treeItems: TreeItems): Set { + const ids = new Set(); + const walk = (items: TreeItems) => { + items.forEach((item) => { + if (item.collapsed) { + ids.add(String(item.id)); + } + if (item.children?.length) { + walk(item.children); + } + }); + }; + walk(treeItems); + return ids; +} + function MenuItemList(props: IMenuItemListProps) { const { items, onAddItem, onDeleteItem, onAddSubItem, onReorderItems } = props; - // State for tracking collapsed items - const [collapsedItems, setCollapsedItems] = useState>(new Set()); - - // Toggle collapse state for an item - const handleToggleCollapse = useCallback((id: string) => { - setCollapsedItems(prev => { - const newSet = new Set(prev); - if (newSet.has(id)) { - newSet.delete(id); - } else { - newSet.add(id); - } - return newSet; - }); - }, []); + const [collapsedIds, setCollapsedIds] = useState>(() => new Set()); // Convert items to tree format - const treeItems = useMemo(() => convertToTreeItems(items, [], collapsedItems), [items, collapsedItems]); + const treeItems = useMemo(() => convertToTreeItems(items, [], collapsedIds), [items, collapsedIds]); // Handle tree changes from drag and drop const handleItemsChanged = useCallback( - (newItems: TreeItems) => { - // Update collapsed state from the new items - const updateCollapsedState = (treeItems: TreeItems) => { - treeItems.forEach(item => { - if (item.collapsed !== undefined) { - setCollapsedItems(prev => { - const newSet = new Set(prev); - if (item.collapsed) { - newSet.add(item.id as string); - } else { - newSet.delete(item.id as string); - } - return newSet; - }); - } - if (item.children && item.children.length > 0) { - updateCollapsedState(item.children); - } - }); - }; - updateCollapsedState(newItems); - onReorderItems(newItems); + (newItems: TreeItems, reason: TreeChangeReason) => { + // Persist collapsed/expanded state locally (SortableTree is controlled by `items` prop) + setCollapsedIds(extractCollapsedIds(newItems)); + + // Only rewrite the underlying nav structure when the tree structure actually changed. + // (If we rebuild on collapsed/expanded, it immediately resets the UI and looks like "toggle does nothing".) + if (reason.type === "dropped" || reason.type === "removed") { + onReorderItems(newItems); + } }, [onReorderItems] ); @@ -177,10 +173,8 @@ function MenuItemList(props: IMenuItemListProps) { () => ({ onDeleteItem, onAddSubItem, - collapsedItems, - onToggleCollapse: handleToggleCollapse, }), - [onDeleteItem, onAddSubItem, collapsedItems, handleToggleCollapse] + [onDeleteItem, onAddSubItem] ); return ( From ff87f39b6b65e4a651230e1e39354fe1aa7279f1 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 17:07:35 +0500 Subject: [PATCH 84/97] add keys --- .../comps/navComp/components/MenuItemList.tsx | 5 ++- .../src/comps/comps/navComp/navItemComp.tsx | 43 ++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index 8dad5efdd..a0bba8c9b 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -54,7 +54,6 @@ const NavTreeItemComponent = React.forwardRef< const { item, depth, collapsed, ...rest } = props; const { comp, path } = item; - console.log("NavTreeItemComponent", "collapsed", collapsed); const handlers = useContext(MenuItemHandlersContext); const hasChildren = item.children && item.children.length > 0; @@ -114,7 +113,9 @@ function convertToTreeItems( ): TreeItems { return items.map((item, index) => { const path = [...basePath, index]; - const id = path.join("_"); + // Use stable itemKey if available, fallback to path-based ID for backwards compatibility + const itemKey = item.getItemKey?.() || ""; + const id = itemKey || path.join("_"); const subItems = item.getView().items || []; return { diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx index 6b6458094..599a8ebe7 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx @@ -1,9 +1,11 @@ import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; +import { valueComp } from "comps/generators"; import { list } from "comps/generators/list"; import { parseChildrenFromValueAndChildrenMap, ToViewReturn } from "comps/generators/multi"; -import { withDefault } from "comps/generators/simpleGenerators"; +import { migrateOldData, withDefault } from "comps/generators/simpleGenerators"; import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils"; +import { genRandomKey } from "comps/utils/idGenerator"; import { trans } from "i18n"; import _ from "lodash"; import { fromRecord, MultiBaseComp, Node, RecordNode, RecordNodeToValue } from "lowcoder-core"; @@ -18,6 +20,7 @@ const childrenMap = { hidden: BoolCodeControl, disabled: BoolCodeControl, active: BoolCodeControl, + itemKey: valueComp(""), onEvent: withDefault(eventHandlerControl(events), [ { // name: "click", @@ -35,6 +38,7 @@ type ChildrenType = { hidden: InstanceType; disabled: InstanceType; active: InstanceType; + itemKey: InstanceType>>; onEvent: InstanceType>; items: InstanceType>; }; @@ -72,6 +76,10 @@ export class NavItemComp extends MultiBaseComp { this.children.items.addItem(value); } + getItemKey(): string { + return this.children.itemKey.getView(); + } + exposingNode(): RecordNode { return fromRecord({ label: this.children.label.exposingNode(), @@ -93,17 +101,42 @@ type NavItemExposing = { items: Node[]>; }; +// Migrate old nav items to ensure they have a stable itemKey +function migrateNavItemData(oldData: any): any { + if (!oldData) return oldData; + + const migrated = { + ...oldData, + itemKey: oldData.itemKey || genRandomKey(), + }; + + // Also migrate nested items recursively + if (Array.isArray(oldData.items)) { + migrated.items = oldData.items.map((item: any) => migrateNavItemData(item)); + } + + return migrated; +} + +const NavItemCompMigrate = migrateOldData(NavItemComp, migrateNavItemData); + export function navListComp() { - const NavItemListCompBase = list(NavItemComp); + const NavItemListCompBase = list(NavItemCompMigrate); return class NavItemListComp extends NavItemListCompBase { addItem(value?: any) { const data = this.getView(); this.dispatch( this.pushAction( - value || { - label: trans("menuItem") + " " + (data.length + 1), - } + value + ? { + ...value, + itemKey: value.itemKey || genRandomKey(), + } + : { + label: trans("menuItem") + " " + (data.length + 1), + itemKey: genRandomKey(), + } ) ); } From 0caae9a1caa1e9eb0f06c71bb540e58beed89c17 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 17:37:41 +0500 Subject: [PATCH 85/97] fix propagation --- .../lowcoder/src/comps/comps/navComp/components/MenuItem.tsx | 4 +++- .../src/comps/comps/navComp/components/MenuItemList.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItem.tsx index 8eb740199..4e687fc42 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItem.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItem.tsx @@ -66,7 +66,9 @@ const MenuItem: React.FC = (props) => { visible={isConfigPopShow} setVisible={showConfigPop} > - {item.children.label.getView()} + e.stopPropagation()}> + {item.children.label.getView()} + - + e.stopPropagation()}> Date: Thu, 1 Jan 2026 18:00:22 +0500 Subject: [PATCH 86/97] add scrollbar wrapper --- .../comps/navComp/components/MenuItemList.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index 0ee447293..4d98678b2 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -1,6 +1,6 @@ import { SortableTree, TreeItems, TreeItemComponentProps, SimpleTreeItemWrapper } from "dnd-kit-sortable-tree"; import LinkPlusButton from "components/LinkPlusButton"; -import { BluePlusIcon, controlItem } from "lowcoder-design"; +import { BluePlusIcon, controlItem, ScrollBar } from "lowcoder-design"; import { trans } from "i18n"; import React, { useMemo, useCallback, createContext, useContext, useState } from "react"; import styled from "styled-components"; @@ -187,15 +187,16 @@ function MenuItemList(props: IMenuItemListProps) {
- - - + + + + +
); From 284007417eef19f8d12237a18b4a51dcc088c848 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 18:34:50 +0500 Subject: [PATCH 87/97] style wrapper --- .../comps/navComp/components/MenuItemList.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index 4d98678b2..fb64b9a88 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -21,21 +21,21 @@ const Wrapper = styled.div` } `; +const StyledTreeItem = styled.div` + .dnd-sortable-tree_simple_tree-item { + padding: 5px; + border-radius: 4px; + &:hover { + background-color: #f5f5f6; + } + } +`; + const TreeItemContent = styled.div` display: flex; align-items: center; width: 100%; - height: 30px; - background-color: #ffffff; - border: 1px solid #d7d9e0; - border-radius: 4px; - padding: 0 8px; box-sizing: border-box; - gap: 8px; - - &:hover { - border-color: #315efb; - } `; // Context for passing handlers to tree items @@ -71,6 +71,7 @@ const NavTreeItemComponent = React.forwardRef< }; return ( +
+ ); }); @@ -187,7 +189,7 @@ function MenuItemList(props: IMenuItemListProps) {
- + Date: Thu, 1 Jan 2026 19:59:03 +0500 Subject: [PATCH 88/97] adjust scrollbar height --- .../src/comps/comps/navComp/components/MenuItemList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index fb64b9a88..41a9ada83 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -189,7 +189,7 @@ function MenuItemList(props: IMenuItemListProps) {
- + Date: Thu, 1 Jan 2026 20:08:55 +0500 Subject: [PATCH 89/97] add dispatch --- .../comps/navComp/components/MenuItemList.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index 41a9ada83..a3b3e2562 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -244,18 +244,9 @@ export function menuPropertyView(itemsComp: NavListCompType) { const newJson = buildJsonFromTree(newItems); - // Clear all existing items and re-add in new order - const currentLength = itemsComp.getView().length; - - // Delete all items from end to start - for (let i = currentLength - 1; i >= 0; i--) { - itemsComp.deleteItem(i); - } - - // Add items back in new order - newJson.forEach((itemJson) => { - itemsComp.addItem(itemJson); - }); + // Use setChildrensAction for atomic update instead of delete-all/add-all + // This is more efficient and prevents UI glitches from multiple re-renders + itemsComp.dispatch(itemsComp.setChildrensAction(newJson)); }; return controlItem( From 63dc2438050a1b1757d425f0bb426dae691b99bb Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 1 Jan 2026 22:54:35 +0500 Subject: [PATCH 90/97] add collapsed property --- .../comps/comps/layout/layoutMenuItemComp.tsx | 16 +++++- .../comps/navComp/components/MenuItemList.tsx | 53 +++++-------------- .../src/comps/comps/navComp/navItemComp.tsx | 16 +++++- 3 files changed, 42 insertions(+), 43 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx index 0999a4012..dc8bee225 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx @@ -1,6 +1,7 @@ import { MultiBaseComp } from "lowcoder-core"; import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; -import { valueComp } from "comps/generators"; +import { BoolControl } from "comps/controls/boolControl"; +import { valueComp, withPropertyViewFn } from "comps/generators"; import { list } from "comps/generators/list"; import { parseChildrenFromValueAndChildrenMap, @@ -16,16 +17,21 @@ import { genRandomKey } from "comps/utils/idGenerator"; import { LayoutActionComp } from "comps/comps/layout/layoutActionComp"; import { migrateOldData } from "comps/generators/simpleGenerators"; +// BoolControl without property view (internal state only) +const CollapsedControl = withPropertyViewFn(BoolControl, () => null); + const childrenMap = { label: StringControl, hidden: BoolCodeControl, action: LayoutActionComp, + collapsed: CollapsedControl, // tree editor collapsed state itemKey: valueComp(""), icon: IconControl, }; type ChildrenType = ToInstanceType & { items: InstanceType; + collapsed: InstanceType; }; /** @@ -73,6 +79,14 @@ export class LayoutMenuItemComp extends MultiBaseComp { getItemKey() { return this.children.itemKey.getView(); } + + getCollapsed(): boolean { + return this.children.collapsed.getView(); + } + + setCollapsed(collapsed: boolean) { + this.children.collapsed.dispatchChangeValueAction(collapsed); + } } const LayoutMenuItemCompMigrate = migrateOldData(LayoutMenuItemComp, (oldData: any) => { diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx index a3b3e2562..8915b6c4e 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx @@ -2,7 +2,7 @@ import { SortableTree, TreeItems, TreeItemComponentProps, SimpleTreeItemWrapper import LinkPlusButton from "components/LinkPlusButton"; import { BluePlusIcon, controlItem, ScrollBar } from "lowcoder-design"; import { trans } from "i18n"; -import React, { useMemo, useCallback, createContext, useContext, useState } from "react"; +import React, { useMemo, useCallback, createContext, useContext } from "react"; import styled from "styled-components"; import { NavCompType, NavListCompType, NavTreeItemData } from "./types"; import MenuItem from "./MenuItem"; @@ -105,13 +105,10 @@ interface IMenuItemListProps { const menuItemLabel = trans("navigation.itemsDesc"); -type TreeChangeReason = { type: string }; - // Convert NavCompType[] to TreeItems format for dnd-kit-sortable-tree function convertToTreeItems( items: NavCompType[], - basePath: number[] = [], - collapsedIds: Set = new Set() + basePath: number[] = [] ): TreeItems { return items.map((item, index) => { const path = [...basePath, index]; @@ -119,54 +116,31 @@ function convertToTreeItems( const itemKey = item.getItemKey?.() || ""; const id = itemKey || path.join("_"); const subItems = item.getView().items || []; + // Read collapsed state from the item itself + const collapsed = item.getCollapsed?.() ?? false; return { id, - collapsed: collapsedIds.has(id), + collapsed, comp: item, path: path, children: subItems.length > 0 - ? convertToTreeItems(subItems, path, collapsedIds) + ? convertToTreeItems(subItems, path) : [], }; }); } -function extractCollapsedIds(treeItems: TreeItems): Set { - const ids = new Set(); - const walk = (items: TreeItems) => { - items.forEach((item) => { - if (item.collapsed) { - ids.add(String(item.id)); - } - if (item.children?.length) { - walk(item.children); - } - }); - }; - walk(treeItems); - return ids; -} - function MenuItemList(props: IMenuItemListProps) { const { items, onAddItem, onDeleteItem, onAddSubItem, onReorderItems } = props; - const [collapsedIds, setCollapsedIds] = useState>(() => new Set()); - // Convert items to tree format - const treeItems = useMemo(() => convertToTreeItems(items, [], collapsedIds), [items, collapsedIds]); + const treeItems = useMemo(() => convertToTreeItems(items), [items]); - // Handle tree changes from drag and drop + // Handle all tree changes (drag/drop, collapse/expand) const handleItemsChanged = useCallback( - (newItems: TreeItems, reason: TreeChangeReason) => { - // Persist collapsed/expanded state locally (SortableTree is controlled by `items` prop) - setCollapsedIds(extractCollapsedIds(newItems)); - - // Only rewrite the underlying nav structure when the tree structure actually changed. - // (If we rebuild on collapsed/expanded, it immediately resets the UI and looks like "toggle does nothing".) - if (reason.type === "dropped" || reason.type === "removed") { - onReorderItems(newItems); - } + (newItems: TreeItems) => { + onReorderItems(newItems); }, [onReorderItems] ); @@ -227,14 +201,14 @@ export function menuPropertyView(itemsComp: NavListCompType) { return getItemListByPath(path.slice(1), root.getView()[path[0]].children.items); }; - // Convert flat tree structure back to nested comp structure + // Convert tree structure back to nested comp structure const handleReorderItems = (newItems: TreeItems) => { - // Build the new order from tree items const buildJsonFromTree = (treeItems: TreeItems): any[] => { return treeItems.map((item) => { const jsonValue = item.comp.toJsonValue() as Record; return { ...jsonValue, + collapsed: item.collapsed ?? false, // sync collapsed from tree item items: item.children && item.children.length > 0 ? buildJsonFromTree(item.children) : [], @@ -243,9 +217,6 @@ export function menuPropertyView(itemsComp: NavListCompType) { }; const newJson = buildJsonFromTree(newItems); - - // Use setChildrensAction for atomic update instead of delete-all/add-all - // This is more efficient and prevents UI glitches from multiple re-renders itemsComp.dispatch(itemsComp.setChildrensAction(newJson)); }; diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx index 599a8ebe7..344b398d0 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx @@ -1,6 +1,7 @@ import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; +import { BoolControl } from "comps/controls/boolControl"; import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; -import { valueComp } from "comps/generators"; +import { valueComp, withPropertyViewFn } from "comps/generators"; import { list } from "comps/generators/list"; import { parseChildrenFromValueAndChildrenMap, ToViewReturn } from "comps/generators/multi"; import { migrateOldData, withDefault } from "comps/generators/simpleGenerators"; @@ -14,12 +15,16 @@ import { IconControl } from "comps/controls/iconControl"; const events = [clickEvent]; +// BoolControl without property view (internal state only) +const CollapsedControl = withPropertyViewFn(BoolControl, () => null); + const childrenMap = { label: StringControl, icon: IconControl, hidden: BoolCodeControl, disabled: BoolCodeControl, active: BoolCodeControl, + collapsed: CollapsedControl, // tree editor collapsed state itemKey: valueComp(""), onEvent: withDefault(eventHandlerControl(events), [ { @@ -38,6 +43,7 @@ type ChildrenType = { hidden: InstanceType; disabled: InstanceType; active: InstanceType; + collapsed: InstanceType; itemKey: InstanceType>>; onEvent: InstanceType>; items: InstanceType>; @@ -80,6 +86,14 @@ export class NavItemComp extends MultiBaseComp { return this.children.itemKey.getView(); } + getCollapsed(): boolean { + return this.children.collapsed.getView(); + } + + setCollapsed(collapsed: boolean) { + this.children.collapsed.dispatchChangeValueAction(collapsed); + } + exposingNode(): RecordNode { return fromRecord({ label: this.children.label.exposingNode(), From d415868651508b069b3bc9f80b6b3a2550b57e35 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Thu, 1 Jan 2026 22:34:34 +0100 Subject: [PATCH 91/97] Prepare Release 2.7.6 --- client/package.json | 1 + .../src/pages/userAuth/authComponents.tsx | 275 ++++++++++--- client/yarn.lock | 20 + server/node-service/yarn.lock | 370 ++++++------------ 4 files changed, 370 insertions(+), 296 deletions(-) diff --git a/client/package.json b/client/package.json index 4b58716c6..051f6fe00 100644 --- a/client/package.json +++ b/client/package.json @@ -81,6 +81,7 @@ "@types/styled-components": "^5.1.34", "antd-mobile": "^5.34.0", "chalk": "4", + "dompurify": "^3.3.1", "flag-icons": "^7.2.1", "number-precision": "^1.6.0", "react-countup": "^6.5.3", diff --git a/client/packages/lowcoder/src/pages/userAuth/authComponents.tsx b/client/packages/lowcoder/src/pages/userAuth/authComponents.tsx index c468bbab4..27d827e28 100644 --- a/client/packages/lowcoder/src/pages/userAuth/authComponents.tsx +++ b/client/packages/lowcoder/src/pages/userAuth/authComponents.tsx @@ -10,12 +10,182 @@ import { favicon } from "assets/images"; import { Col, Row, Typography } from "antd"; import { getBrandingSetting } from "@lowcoder-ee/redux/selectors/enterpriseSelectors"; import { useSelector } from "react-redux"; -import { buildMaterialPreviewURL } from "@lowcoder-ee/util/materialUtils"; import { isEmpty } from "lodash"; -const StyledBrandingColumn = styled(Col)<{$bgImage?: string | null}>` +// Safe rendering dependencies +import DOMPurify from "dompurify"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +type BrandingFormat = "text" | "markdown" | "html"; + +/** + * Prefer an explicit config field (recommended): + * brandingSettings?.config_set?.signUpPageTextFormat in {"text","markdown","html"} + * + * If absent, we fall back to a heuristic. + */ +function detectFormat(value: string): BrandingFormat { + const v = (value ?? "").trim(); + if (!v) return "text"; + + // Heuristic: if it looks like HTML, treat as HTML + if (v.startsWith("<") && v.includes(">")) return "html"; + + // Heuristic: if it looks like markdown, treat as markdown + // (Keep this light; explicit format is better.) + const looksLikeMarkdown = + /(^|\n)\s{0,3}#{1,6}\s+/.test(v) || // headings + /(\*{1,2}.+\*{1,2})/.test(v) || // emphasis + /(\[.+\]\(.+\))/.test(v) || // links + /(^|\n)\s{0,3}[-*+]\s+/.test(v) || // lists + /(^|\n)\s{0,3}\d+\.\s+/.test(v) || // ordered lists + /`{1,3}[^`]+`{1,3}/.test(v); // code + + if (looksLikeMarkdown) return "markdown"; + return "text"; +} + +/** + * HTML sanitizer: + * - Forbids scripts, iframes, embeds, and