diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartComp.tsx index 7b64f0f6c5..0998492ae3 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 032607625b..ed2f1654e2 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 aaa5f01984..b52c99846a 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} /> @@ -302,7 +303,7 @@ let PieChartComp = withExposingConfigs(PieChartTmpComp, [ export const PieChartCompWithDefault = withDefault(PieChartComp, { - xAxisKey: "date", + xAxisKey: "name", series: [ { dataIndex: genRandomKey(), diff --git a/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartComp.tsx b/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartComp.tsx index c7fd7da9cd..527a4ca2d7 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} /> diff --git a/client/packages/lowcoder-design/src/components/Dropdown.tsx b/client/packages/lowcoder-design/src/components/Dropdown.tsx index b2a9d27663..55bd8b8303 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/components/table/EditableCell.tsx b/client/packages/lowcoder/src/components/table/EditableCell.tsx index 5064fffa13..b886167448 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} diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/scannerComp.tsx index 8b061cb4c1..8900ea914c 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), @@ -150,17 +151,27 @@ 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; + 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); @@ -205,6 +216,7 @@ const ScannerTmpComp = (function () { props.onEvent("click"); setShowModal(true); continuousValue.current = []; + seenSetRef.current = new Set(); }} > {props.text} @@ -317,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, ]); diff --git a/client/packages/lowcoder/src/comps/comps/formComp/formComp.tsx b/client/packages/lowcoder/src/comps/comps/formComp/formComp.tsx index 8255813932..bece24fe23 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; diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index 466c37e9f2..92f3559368 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} > import("antd-mobile/es/components/tab-bar")); const TabBarItem = React.lazy(() => @@ -65,6 +69,139 @@ 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; + $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: ${(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); +`; + +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 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: ${(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 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, @@ -116,7 +253,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}; } @@ -287,6 +424,73 @@ 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.hamburgerIcon.propertyView({ label: "Menu Icon" })} + {children.drawerCloseIcon.propertyView({ label: "Close Icon" })} + {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), @@ -311,6 +515,16 @@ let MobileTabLayoutTmp = (function () { jsonTabs: manualOptionsControl(TabOptionComp, { initOptions: [], }), + // 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, "right"), + drawerHeight: withDefault(StringControl, "60%"), + drawerWidth: withDefault(StringControl, "250px"), + shadowOverlay: withDefault(BoolCodeControl, true), backgroundImage: withDefault(StringControl, ""), tabBarHeight: withDefault(StringControl, "56px"), navIconSize: withDefault(StringControl, "32px"), @@ -321,47 +535,38 @@ 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; }) .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() } -
-
+ {!isHamburgerMode && ( +
+ {children.navStyle.getPropertyView()} +
+ )} + + {isHamburgerMode && ( +
+ {children.hamburgerButtonStyle.getPropertyView()} +
+ )} +
{controlItem({}, ( 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()}
-
+ {isHamburgerMode && ( +
+ {children.drawerContainerStyle.getPropertyView()} +
+ )} + ); }) .build(); @@ -388,7 +592,9 @@ 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(); const navItemStyle = comp.children.navItemStyle.getView(); const navItemHoverStyle = comp.children.navItemHoverStyle.getView(); @@ -396,14 +602,32 @@ 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 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(); + const shadowOverlay = comp.children.shadowOverlay.getView(); const tabBarHeight = comp.children.tabBarHeight.getView(); const navIconSize = comp.children.navIconSize.getView(); 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(); + const getContainer = useCallback(() => + document.querySelector(`#${PreviewContainerID}`) || + document.querySelector(`#${CanvasContainerID}`) || + document.body, + [] + ); + useEffect(() => { comp.children.jsonTabs.dispatchChangeValueAction({ manual: jsonItems as unknown as Array> @@ -455,6 +679,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 = ( { : undefined, }))} selectedKey={tabIndex + ""} - onChange={(key) => setTabIndex(Number(key))} + onChange={(key) => { + const nextIndex = Number(key); + setTabIndex(nextIndex); + // push URL with query/hash params + navigateToApp(nextIndex); + }} readOnly={!!readOnly} canvasBg={bgColor} tabStyle={{ @@ -488,11 +732,111 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { /> ); + const containerTabBarHeight = menuMode === MobileMode.Hamburger ? '0px' : tabBarHeight; + + const hamburgerButton = ( + setDrawerVisible(true)} + > + {hamburgerIconComp.toJsonValue() ? ( + + {hamburgerIconComp.getView()} + + ) : ( + + )} + + ); + + const drawerView = ( + }> + setDrawerVisible(false)} + placement={drawerPlacement as any} + mask={shadowOverlay} + 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 + } + > + + + setDrawerVisible(false)} + > + {drawerCloseIconComp.toJsonValue() + ? drawerCloseIconComp.getView() + : ×} + + + + {tabViews.map((tab, index) => ( +
{ + setTabIndex(index); + setDrawerVisible(false); + onEvent('click'); + navigateToApp(index); + }} + > + {tab.children.icon.toJsonValue() ? ( + {tab.children.icon.getView()} + ) : null} + {tab.children.label.getView()} +
+ ))} +
+
+
+
+ ); + if (readOnly) { return ( - + {appView} - {tabBarView} + {menuMode === MobileMode.Hamburger ? ( + <> + {hamburgerButton} + {drawerView} + + ) : ( + tabBarView + )} ); } @@ -500,7 +844,14 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { return ( {appView} - {tabBarView} + {menuMode === MobileMode.Hamburger ? ( + <> + {hamburgerButton} + {drawerView} + + ) : ( + tabBarView + )} ); }); diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 2f07839ca2..b94fe89658 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, @@ -36,14 +37,13 @@ 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"; @@ -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` @@ -148,12 +166,6 @@ const StyledImage = styled.img` color: currentColor; `; -const defaultStyle = { - radius: '0px', - margin: '0px', - padding: '0px', -} - type UrlActionType = { url?: string; newTab?: boolean; @@ -163,7 +175,7 @@ export type MenuItemNode = { label: string; key: string; hidden?: boolean; - icon?: any; + icon?: string; action?: UrlActionType, children?: MenuItemNode[]; } @@ -197,8 +209,8 @@ let NavTmpLayout = (function () { jsonItems: jsonControl(convertTreeData, jsonMenuItems), width: withDefault(StringControl, DEFAULT_WIDTH), backgroundImage: withDefault(StringControl, ""), - mode: dropdownControl(ModeOptions, "inline"), - collapse: BoolCodeControl, + position: dropdownControl(NavPositionOptions, NavPosition.Left), + collapse: BoolControl, navStyle: styleControl(NavLayoutStyle, 'navStyle'), navItemStyle: styleControl(NavLayoutItemStyle, 'navItemStyle'), navItemHoverStyle: styleControl(NavLayoutItemHoverStyle, 'navItemHoverStyle'), @@ -208,66 +220,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.mode.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()}
); }) @@ -280,7 +320,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 +608,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 +637,27 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { let content = ( - {navMode === 'horizontal' ? ( + {(navPosition === 'top') && (
{ navMenu }
- ) : ( + )} + {(navPosition === 'left') && ( {navMenu} )} {pageView} + {(navPosition === 'bottom') && ( +
+ { navMenu } +
+ )} + {(navPosition === 'right') && ( + + {navMenu} + + )}
); return isViewMode ? ( @@ -637,7 +690,7 @@ NavTmpLayout = withDispatchHook(NavTmpLayout, (dispatch) => (action) => { }); }); -export const NavLayout = class extends NavTmpLayout { +export class NavLayout extends NavTmpLayout { getAllCompItems() { return {}; } @@ -645,5 +698,5 @@ export const NavLayout = class extends NavTmpLayout { nameAndExposingInfo(): NameAndExposingInfo { return {}; } -}; +} registerLayoutMap({ compType: navLayoutCompType, comp: NavLayout }); diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts index 66043303ac..f9a2dc456c 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts @@ -6,6 +6,60 @@ 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", + 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', 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 0000000000..752685f78c --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/NavItemsControl.tsx @@ -0,0 +1,80 @@ +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 { IconControl } from "comps/controls/iconControl"; +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, + icon: IconControl, + hidden: BoolCodeControl, + disabled: BoolCodeControl, + active: BoolCodeControl, + onEvent: eventHandlerControl([clickEvent]), + }, + (props) => props + ) + .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") })} + {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 e3727508ec..940e0110d0 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -1,27 +1,48 @@ 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 { dropdownControl, PositionControl } 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 Segmented from "antd/es/segmented"; +import { Drawer, ScrollBar } 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, NavigationStyle, + HamburgerButtonStyle, + DrawerContainerStyle, + NavLayoutItemStyle, + NavLayoutItemHoverStyle, + NavLayoutItemActiveStyle, + NavSubMenuItemStyle, + NavSubMenuItemHoverStyle, + NavSubMenuItemActiveStyle, } from "comps/controls/styleControlConstants"; import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; -import { useContext } 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"; +import { isNumeric } from "util/stringUtils"; +import { hasIcon } from "comps/utils"; type IProps = { $justify: boolean; @@ -31,6 +52,7 @@ type IProps = { $borderRadius: string; $borderStyle: string; $animationStyle: AnimationStyleType; + $orientation: "horizontal" | "vertical"; }; const Wrapper = styled("div")< @@ -42,18 +64,35 @@ ${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 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); +} + +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%; 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; @@ -63,11 +102,21 @@ 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.$active ? props.$activeColor : props.$color)}; + 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')}; @@ -77,8 +126,10 @@ 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.$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'}; } .anticon { @@ -97,19 +148,141 @@ 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}; `; -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"} !important; + 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; } `; +const FloatingHamburgerButton = styled.button<{ + $size: string; + $position: string; // left | right + $zIndex: number; + $background?: string; + $borderColor?: string; + $radius?: string; + $margin?: string; + $padding?: string; + $borderWidth?: string; + $iconColor?: string; +}>` + position: fixed; + top: 16px; + ${(props) => (props.$position === '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 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 @@ -131,76 +304,308 @@ 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; +} + +// 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.displayMode.propertyView({ label: "Display Mode", radioButton: true }), + ]; + const hamburger = [ + ...common, + 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, + }), + ]; + + 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, + styleSegment: MenuItemStyleOptionValue, + setStyleSegment: (k: MenuItemStyleOptionValue) => void, + subStyleSegment: MenuItemStyleOptionValue, + setSubStyleSegment: (k: MenuItemStyleOptionValue) => void +) { + const isHamburger = children.displayMode.getView() === 'hamburger'; + return ( + <> + {!isHamburger && ( +
+ {children.style.getPropertyView()} +
+ )} +
+ {controlItem({}, ( + setStyleSegment(k as MenuItemStyleOptionValue)} + /> + ))} + {styleSegment === "normal" && children.navItemStyle.getPropertyView()} + {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 && ( + <> +
+ {children.hamburgerButtonStyle.getPropertyView()} +
+
+ {children.drawerContainerStyle.getPropertyView()} +
+ + )} +
+ {children.animationStyle.getPropertyView()} +
+ + ); +} + 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: "Left", value: "left" }, + { label: "Right", value: "right" }, + ], "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), + 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(navListComp(), [ - { - label: trans("menuItem") + " 1", - }, - ]), + subNavItemStyle: styleControl(NavSubMenuItemStyle, 'subNavItemStyle'), + subNavItemHoverStyle: styleControl(NavSubMenuItemHoverStyle, 'subNavItemHoverStyle'), + subNavItemActiveStyle: styleControl(NavSubMenuItemActiveStyle, 'subNavItemActiveStyle'), + items: withDefault(migrateOldData(createNavItemsControl(), fixOldItemsData), { + optionType: "manual", + manual: [ + { + label: trans("menuItem") + " 1", + }, + ], + }), }; const NavCompBase = new UICompBuilder(childrenMap, (props) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const getContainer = useCallback(() => + document.querySelector(`#${CanvasContainerID}`) || document.querySelector(`#${PreviewContainerID}`) || document.body, + [] + ); 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 subMenuItems: Array<{ key: string; label: string }> = []; + + 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; icon?: any; disabled?: boolean }> = []; 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); + 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 item = ( 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} - onClick={() => onEvent("click")} + $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"); }} > + {icon && {icon}} {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"); - }} - selectedKeys={subMenuSelectedKeys} - items={subMenuItems} - /> + + { + 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 ( subMenu} + disabled={disabled} > {item} @@ -212,6 +617,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)} + > + {hasIcon(props.hamburgerIcon) ? props.hamburgerIcon : } + + 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: 0 } }} + destroyOnClose + > + + + setDrawerVisible(false)} + > + {hasIcon(props.drawerCloseIcon) + ? props.drawerCloseIcon + : ×} + + + {items} + + + + )} ); }) .setPropertyViewFn((children) => { + const mode = useContext(EditorContext).editorModeStatus; + const showLogic = mode === "logic" || mode === "both"; + const showLayout = mode === "layout" || mode === "both"; + const [styleSegment, setStyleSegment] = useState("normal"); + const [subStyleSegment, setSubStyleSegment] = useState("normal"); + return ( <> -
- {menuPropertyView(children.items)} -
- - {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {hiddenPropertyView(children)} - {showDataLoadingIndicatorsPropertyView(children)} -
- )} - - {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {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.animationStyle.getPropertyView()} -
- - )} + {renderBasicSection(children)} + {showLogic && renderInteractionSection(children)} + {showLayout && renderLayoutSection(children)} + {showLogic && renderAdvancedSection(children)} + {showLayout && renderStyleSections(children, styleSegment, setStyleSegment, subStyleSegment, setSubStyleSegment)} ); }) diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx index 565013ab4f..6b64580941 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx @@ -3,17 +3,20 @@ 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"; 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, onEvent: withDefault(eventHandlerControl(events), [ { @@ -28,7 +31,9 @@ const childrenMap = { type ChildrenType = { label: InstanceType; + icon: InstanceType; hidden: InstanceType; + disabled: InstanceType; active: InstanceType; onEvent: InstanceType>; items: InstanceType>; @@ -43,8 +48,10 @@ 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)} {this.children.onEvent.propertyView({ inline: true })} ); @@ -68,7 +75,9 @@ 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(), items: this.children.items.exposingNode(), }); @@ -77,7 +86,9 @@ export class NavItemComp extends MultiBaseComp { type NavItemExposing = { label: Node; + icon: Node; hidden: Node; + disabled: Node; active: Node; items: Node[]>; }; diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index 9573e2a3b1..fad9d36d14 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); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx index 55bf8d694b..3cdf9119a4 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/simpleColumnTypeComps.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx index f9bedc7549..eaac533278 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/comps/tableComp/column/tableColumnComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx index 938983ac9e..f94b17816d 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/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index 725543effe..52b1acf4aa 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx @@ -99,11 +99,15 @@ export class TableImplComp extends TableInitComp implements IContainer { } downloadData(fileName: string) { + // displayData already contains only visible columns (filtered in transformDispalyData) + const displayData = (this as any).exposingValues["displayData"]; + const delimiter = this.children.toolbar.children.columnSeparator.getView(); + saveDataAsFile({ - data: (this as any).exposingValues["displayData"], + data: displayData, filename: fileName, fileType: "csv", - delimiter: this.children.toolbar.children.columnSeparator.getView(), + delimiter, }); } @@ -822,6 +826,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 +1105,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) => { diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index 53d4231290..3b3b47f6e5 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]); @@ -100,6 +101,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 +363,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 5e3cc6dcd7..bc92f34a65 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 { @@ -122,7 +116,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; @@ -132,6 +132,7 @@ export const TableWrapper = styled.div<{ $fixedToolbar: boolean; $visibleResizables: boolean; $showHRowGridBorder?: boolean; + $showRowGridBorder?: boolean; $isVirtual?: boolean; $showHorizontalScrollbar?: boolean; $showVerticalScrollbar?: boolean; @@ -201,7 +202,10 @@ export const TableWrapper = styled.div<{ 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 { @@ -263,18 +267,20 @@ export const TableWrapper = styled.div<{ 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 */ - 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 { diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx index d29299c1ad..cf977e96ef 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/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index d71ae5a8d4..edb26ca61d 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 { @@ -333,6 +334,8 @@ export type CustomColumnType = ColumnType & { style: TableColumnStyleType; linkStyle: TableColumnLinkStyleType; cellColorFn: CellColorViewType; + columnClassName?: string; + columnDataTestId?: string; }; /** @@ -400,6 +403,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 4951dea4e2..fcfab56af1 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 31aa30f33e..fffde7f21f 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 c7a5d39c2b..16ca3a7d60 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 1a44cfa7d7..247c0e0d47 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 c35fcc0ba3..4d792e0565 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 62f3f1ffbc..dbe4edcf88 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 9297eded86..69f18ae83d 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( diff --git a/client/packages/lowcoder/src/comps/controls/colorControl.tsx b/client/packages/lowcoder/src/comps/controls/colorControl.tsx index 6b45a982da..333622ece0 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 ( - + - {!_.isEmpty(eventName) && {eventName}} + {!_.isEmpty(eventName) && ( + + {eventName} + + )} {eventAction} diff --git a/client/packages/lowcoder/src/comps/controls/styleControl.tsx b/client/packages/lowcoder/src/comps/controls/styleControl.tsx index 5d40a2db4d..f8f8cdecbf 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 && ( ( 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 330e43cfdd..f2516f881d 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 & { @@ -1382,6 +1383,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'), @@ -1767,6 +1792,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 +1831,6 @@ export const TableHeaderStyle = [ PADDING, FONT_FAMILY, FONT_STYLE, - TEXT, // getStaticBackground(SURFACE_COLOR), // getBackground("primarySurface"), { @@ -2362,6 +2418,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 = [ @@ -2371,6 +2467,18 @@ export const RichTextEditorStyle = [ BORDER_WIDTH, ] as const; +export const TableColumnButtonStyle = [ + getBackground('primary'), + { + name: "text", + label: trans("style.text"), + color: "#000000", + }, + PADDING, + BORDER, +] as const; + + export type QRCodeStyleType = StyleConfigType; export type TimeLineStyleType = StyleConfigType; export type AvatarStyleType = StyleConfigType; @@ -2437,6 +2545,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; @@ -2488,6 +2597,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(" ") || ""; diff --git a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx index 3b33c3fb96..7f005d1aae 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"; @@ -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,8 @@ const childrenMap = { showMask: withDefault(BoolControl, true), toggleClose:withDefault(BoolControl,true), escapeClosable: withDefault(BoolControl, true), + closeIcon: withDefault(IconControl, ""), + zIndex: withDefault(NumberControl, Layers.drawer), }; type ChildrenType = NewChildren> & { @@ -138,6 +142,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({ @@ -168,6 +175,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 +261,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)} @@ -262,7 +272,7 @@ const DrawerView = React.memo(( $closePosition={props.closePosition} onClick={onClose} > - + {hasIcon(props.closeIcon) ? props.closeIcon : } )}
{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/comps/hooks/utilsComp.ts b/client/packages/lowcoder/src/comps/hooks/utilsComp.ts index 2a8689efcb..a7190b1f37 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" }); } }, }, diff --git a/client/packages/lowcoder/src/comps/queries/queryComp/queryConfirmationModal.tsx b/client/packages/lowcoder/src/comps/queries/queryComp/queryConfirmationModal.tsx index 66f5de7125..0ad6c73bc5 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()); }) diff --git a/client/packages/lowcoder/src/constants/domLocators.ts b/client/packages/lowcoder/src/constants/domLocators.ts index b3d1709a5b..2fefcb5f1f 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/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 60095c1465..47e93620ff 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", @@ -237,7 +239,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", @@ -666,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", @@ -2363,7 +2374,9 @@ export const en = { "openDrawerDesc": "Open Drawer", "closeDrawerDesc": "Close Drawer", "width": "Drawer Width", - "height": "Drawer Height" + "height": "Drawer Height", + "closeIcon": "Close Icon", + }, "meeting": { "logLevel": "Agora SDK Log Level", @@ -3204,7 +3217,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}" diff --git a/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx b/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx index 498a857f07..201cf9225e 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)} /> )} > diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index c722f907f7..a60f9f0ef3 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( @@ -270,6 +271,7 @@ const DeviceWrapperInner = styled(Flex)` > div:first-child { > div:first-child { > div:nth-child(2) { + contain: paint; display: block !important; overflow: hidden auto !important; } @@ -533,10 +535,12 @@ function EditorView(props: EditorViewProps) { deviceType={editorState.deviceType} deviceOrientation={editorState.deviceOrientation} > - {uiComp.getView()} +
+ {uiComp.getView()} +
) : ( -
+
{uiComp.getView()}
) diff --git a/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx b/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx index f5e90bd7b2..f051a28983 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 = ( \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/templates/api-service/secrets.yaml b/deploy/helm/templates/api-service/secrets.yaml index c1e45ced8e..c670b36c2e 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 2af6cfa30b..88888d60f3 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 }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 3723fec4b4..ddfe76491a 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: 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 3e2a7c2aea..da9b3c0836 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)); }); } 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 d8520a9d54..e7e840e010 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; }); });