From 5ce5d2108d2b7894b45762a9f57b82babdb750b4 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 5 Jun 2024 00:58:07 +0500 Subject: [PATCH 1/2] mobile nav layout options + styling --- .../comps/comps/layout/mobileTabLayout.tsx | 173 +++++++++++++++--- .../comps/comps/layout/navLayoutConstants.ts | 30 +++ 2 files changed, 182 insertions(+), 21 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index e08567a82..0b91d8f86 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -1,11 +1,11 @@ -import { MultiCompBuilder, withViewFn } from "comps/generators"; +import { MultiCompBuilder, withDefault, withViewFn } from "comps/generators"; import { trans } from "i18n"; -import { Section } from "components/Section"; +import { Section, sectionNames } from "components/Section"; import { manualOptionsControl } from "comps/controls/optionsControl"; -import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; +import { BoolCodeControl, StringControl, jsonControl } from "comps/controls/codeControl"; import { IconControl } from "comps/controls/iconControl"; import styled from "styled-components"; -import React, { Suspense, useContext, useState } from "react"; +import React, { Suspense, useContext, useMemo, useState } from "react"; import { registerLayoutMap } from "comps/comps/uiComp"; import { AppSelectComp } from "comps/comps/layout/appSelectComp"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; @@ -17,6 +17,18 @@ import { Layers } from "constants/Layers"; import { ExternalEditorContext } from "util/context/ExternalEditorContext"; import { default as Skeleton } from "antd/es/skeleton"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; +import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl"; +import { DataOption, DataOptionType, ModeOptions, jsonMenuItems, menuItemStyleOptions, mobileNavJsonMenuItems } from "./navLayoutConstants"; +import { styleControl } from "@lowcoder-ee/comps/controls/styleControl"; +import { NavLayoutItemActiveStyle, NavLayoutItemHoverStyle, NavLayoutItemStyle, NavLayoutStyle } from "@lowcoder-ee/comps/controls/styleControlConstants"; +import Segmented from "antd/es/segmented"; +import { controlItem } from "components/control"; +import { MenuItemNode } from "./navLayout"; +import { check } from "@lowcoder-ee/util/convertUtils"; +import { JSONObject } from "@lowcoder-ee/util/jsonTypes"; +import { getCompContainer, useCompContainer, useCompInstance } from "@lowcoder-ee/comps/utils/useCompInstance"; +import { ModuleComp } from "../moduleComp/moduleComp"; +import { evalAndReduceWithExposing } from "comps/utils"; const TabBar = React.lazy(() => import("antd-mobile/es/components/tab-bar")); const TabBarItem = React.lazy(() => @@ -61,6 +73,21 @@ const TabBarWrapper = styled.div<{ $readOnly: boolean }>` } `; +const defaultStyle = { + radius: '0px', + margin: '0px', + padding: '0px', +} + +type MenuItemStyleOptionValue = "normal" | "hover" | "active"; + +type JsonItemNode = { + label: string; + hidden?: boolean; + icon?: any; + app?: JSONObject, +} + type TabBarProps = { tabs: Array<{ title: string; @@ -72,7 +99,23 @@ type TabBarProps = { readOnly: boolean; }; +function checkDataNodes(value: any, key?: string): JsonItemNode[] | undefined { + return check(value, ["array", "undefined"], key, (node, k) => { + check(node, ["object"], k); + check(node["label"], ["string"], "label"); + check(node["hidden"], ["boolean", "undefined"], "hidden"); + check(node["icon"], ["string", "undefined"], "icon"); + check(node["app"], ["object", "undefined"], "app"); + return node; + }); +} + +function convertTreeData(data: any) { + return data === "" ? [] : checkDataNodes(data) ?? []; +} + function TabBarView(props: TabBarProps) { + console.log(props); return ( }> @@ -126,6 +169,8 @@ const TabOptionComp = (function () { let MobileTabLayoutTmp = (function () { const childrenMap = { + dataOptionType: dropdownControl(DataOptionType, DataOption.Manual), + jsonItems: jsonControl(convertTreeData, mobileNavJsonMenuItems), tabs: manualOptionsControl(TabOptionComp, { initOptions: [ { @@ -142,17 +187,67 @@ let MobileTabLayoutTmp = (function () { }, ], }), + backgroundImage: withDefault(StringControl, ""), + mode: dropdownControl(ModeOptions, "inline"), + navStyle: withDefault(styleControl(NavLayoutStyle), defaultStyle), + navItemStyle: withDefault(styleControl(NavLayoutItemStyle), defaultStyle), + navItemHoverStyle: withDefault(styleControl(NavLayoutItemHoverStyle), {}), + navItemActiveStyle: withDefault(styleControl(NavLayoutItemActiveStyle), {}), }; return new MultiCompBuilder(childrenMap, (props) => { return null; }) .setPropertyViewFn((children) => { + const [styleSegment, setStyleSegment] = useState('normal') return ( - <> +
- {children.tabs.propertyView({})} + {children.dataOptionType.propertyView({ + radioButton: true, + type: "oneline", + })} + {/* {children.tabs.propertyView({})} */} + { + children.dataOptionType.getView() === DataOption.Manual + ? children.tabs.propertyView({}) + : children.jsonItems.propertyView({ + label: "Json Data", + }) + }
- +
+ { children.mode.propertyView({ + label: trans("labelProp.position"), + radioButton: true + })} + {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() + )} +
+
); }) .build(); @@ -161,20 +256,56 @@ let MobileTabLayoutTmp = (function () { MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const [tabIndex, setTabIndex] = useState(0); const { readOnly } = useContext(ExternalEditorContext); - const tabViews = ( - comp.children.tabs.children.manual.getView() as unknown as Array< - ConstructorToComp - > - ).filter((tab) => !tab.children.hidden.getView()); - const currentTab = tabViews[tabIndex]; - const appView = (currentTab && - currentTab.children.app.getAppId() && - currentTab.children.app.getView()) || ( - - ); + const tabs = comp.children.tabs.getView(); + const navMode = comp.children.mode.getView(); + const navStyle = comp.children.navStyle.getView(); + const navItemStyle = comp.children.navItemStyle.getView(); + const navItemHoverStyle = comp.children.navItemHoverStyle.getView(); + const navItemActiveStyle = comp.children.navItemActiveStyle.getView(); + const backgroundImage = comp.children.backgroundImage.getView(); + const jsonItems = comp.children.jsonItems.getView(); + const dataOptionType = comp.children.dataOptionType.getView(); + // const tabViews = ( + // comp.children.tabs.children.manual.getView() as unknown as Array< + // ConstructorToComp + // > + // ).filter((tab) => !tab.children.hidden.getView()); + const tabViews = useMemo(() => { + if (dataOptionType === DataOption.Manual) { + return (comp.children.tabs.children.manual.getView() as unknown as Array< + ConstructorToComp + > + ).filter((tab) => !tab.children.hidden.getView()); + } + return jsonItems.filter(item => !Boolean(item.hidden)).map((item) => { + const container = getCompContainer({ + Comp: TabOptionComp, + initialValue: { + ...item, + } + }) + if (container) { + container.initialized = true; + container.comp = evalAndReduceWithExposing(container.comp); + } + return container!.comp as unknown as ConstructorToComp + }) + }, [dataOptionType]) + + console.log(tabViews); + + const appView = useMemo(() => { + const currentTab = tabViews[tabIndex]; + + return (currentTab && + currentTab.children.app.getAppId() && + currentTab.children.app.getView()) || ( + + ) + }, [tabIndex]); const tabBarView = ( Date: Wed, 5 Jun 2024 21:54:52 +0500 Subject: [PATCH 2/2] fix view loading issue in json mode + added styling --- .../comps/comps/layout/mobileTabLayout.tsx | 159 ++++++++++++------ .../comps/comps/layout/navLayoutConstants.ts | 9 +- 2 files changed, 114 insertions(+), 54 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index 0b91d8f86..02807f261 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -5,11 +5,11 @@ import { manualOptionsControl } from "comps/controls/optionsControl"; import { BoolCodeControl, StringControl, jsonControl } from "comps/controls/codeControl"; import { IconControl } from "comps/controls/iconControl"; import styled from "styled-components"; -import React, { Suspense, useContext, useMemo, useState } from "react"; +import React, { Suspense, useContext, useEffect, useMemo, useState } from "react"; import { registerLayoutMap } from "comps/comps/uiComp"; import { AppSelectComp } from "comps/comps/layout/appSelectComp"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; -import { ConstructorToComp } from "lowcoder-core"; +import { ConstructorToComp, ConstructorToDataType } from "lowcoder-core"; import { CanvasContainer } from "comps/comps/gridLayoutComp/canvasView"; import { CanvasContainerID } from "constants/domLocators"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; @@ -18,17 +18,15 @@ import { ExternalEditorContext } from "util/context/ExternalEditorContext"; import { default as Skeleton } from "antd/es/skeleton"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl"; -import { DataOption, DataOptionType, ModeOptions, jsonMenuItems, menuItemStyleOptions, mobileNavJsonMenuItems } from "./navLayoutConstants"; +import { DataOption, DataOptionType, ModeOptions, menuItemStyleOptions, mobileNavJsonMenuItems } from "./navLayoutConstants"; import { styleControl } from "@lowcoder-ee/comps/controls/styleControl"; -import { NavLayoutItemActiveStyle, NavLayoutItemHoverStyle, NavLayoutItemStyle, NavLayoutStyle } from "@lowcoder-ee/comps/controls/styleControlConstants"; +import { NavLayoutItemActiveStyle, NavLayoutItemActiveStyleType, NavLayoutItemHoverStyle, NavLayoutItemHoverStyleType, NavLayoutItemStyle, NavLayoutItemStyleType, NavLayoutStyle, NavLayoutStyleType, defaultTheme } from "@lowcoder-ee/comps/controls/styleControlConstants"; import Segmented from "antd/es/segmented"; import { controlItem } from "components/control"; -import { MenuItemNode } from "./navLayout"; import { check } from "@lowcoder-ee/util/convertUtils"; import { JSONObject } from "@lowcoder-ee/util/jsonTypes"; -import { getCompContainer, useCompContainer, useCompInstance } from "@lowcoder-ee/comps/utils/useCompInstance"; -import { ModuleComp } from "../moduleComp/moduleComp"; -import { evalAndReduceWithExposing } from "comps/utils"; +import { isEmpty } from "lodash"; +import { ThemeContext } from "@lowcoder-ee/comps/utils/themeContext"; const TabBar = React.lazy(() => import("antd-mobile/es/components/tab-bar")); const TabBarItem = React.lazy(() => @@ -55,9 +53,12 @@ const TabLayoutViewContainer = styled.div` height: calc(100% - ${TabBarHeight}px); `; -const TabBarWrapper = styled.div<{ $readOnly: boolean }>` +const TabBarWrapper = styled.div<{ + $readOnly: boolean, + $canvasBg: string, +}>` max-width: inherit; - background: white; + background: ${(props) => (props.$canvasBg)}; margin: 0 auto; position: fixed; bottom: 0; @@ -73,6 +74,50 @@ const TabBarWrapper = styled.div<{ $readOnly: boolean }>` } `; +const StyledTabBar = styled(TabBar)<{ + $tabStyle: NavLayoutStyleType, + $tabItemStyle: NavLayoutItemStyleType, + $tabItemHoverStyle: NavLayoutItemHoverStyleType, + $tabItemActiveStyle: NavLayoutItemActiveStyleType, +}>` + width: ${(props) => `calc(100% - ${props.$tabStyle.margin} - ${props.$tabStyle.margin})`}; + border: ${(props) => props.$tabStyle.border}; + background: ${(props) => props.$tabStyle.background}; + border-radius: ${(props) => props.$tabStyle.radius }; + margin: ${(props) => props.$tabStyle.margin }; + padding: ${(props) => props.$tabStyle.padding }; + + .adm-tab-bar-item:not(:last-child) { + border-right: ${(props) => props.$tabStyle.border}; + } + .adm-tab-bar-item-icon, .adm-tab-bar-item-title { + color: ${(props) => props.$tabStyle.text}; + } + + .adm-tab-bar-item { + background-color: ${(props) => props.$tabItemStyle?.background}; + color: ${(props) => props.$tabItemStyle?.text}; + border-radius: ${(props) => props.$tabItemStyle?.radius} !important; + border: ${(props) => `1px solid ${props.$tabItemStyle?.border}`}; + margin: ${(props) => props.$tabItemStyle?.margin}; + padding: ${(props) => props.$tabItemStyle?.padding}; + } + + .adm-tab-bar-item:hover { + background-color: ${(props) => props.$tabItemHoverStyle?.background} !important; + color: ${(props) => props.$tabItemHoverStyle?.text} !important; + border: ${(props) => `1px solid ${props.$tabItemHoverStyle?.border}`}; + } + + .adm-tab-bar-item.adm-tab-bar-item-active { + background-color: ${(props) => props.$tabItemActiveStyle.background}; + // border: ${(props) => `1px solid ${props.$tabItemActiveStyle.border}`}; + .adm-tab-bar-item-icon, .adm-tab-bar-item-title { + color: ${(props) => props.$tabItemActiveStyle.text}; + } + } +`; + const defaultStyle = { radius: '0px', margin: '0px', @@ -97,6 +142,11 @@ type TabBarProps = { selectedKey: string; onChange: (key: string) => void; readOnly: boolean; + canvasBg: string; + tabStyle: NavLayoutStyleType; + tabItemStyle: NavLayoutItemStyleType; + tabItemHoverStyle: NavLayoutItemHoverStyleType; + tabItemActiveStyle: NavLayoutItemActiveStyleType; }; function checkDataNodes(value: any, key?: string): JsonItemNode[] | undefined { @@ -115,25 +165,33 @@ function convertTreeData(data: any) { } function TabBarView(props: TabBarProps) { - console.log(props); + const { + canvasBg, tabStyle, tabItemStyle, tabItemHoverStyle, tabItemActiveStyle, + } = props; return ( }> - - + { if (key) { props.onChange(key); } }} - style={{ width: "100%" }} activeKey={props.selectedKey} + $tabStyle={tabStyle} + $tabItemStyle={tabItemStyle} + $tabItemHoverStyle={tabItemHoverStyle} + $tabItemActiveStyle={tabItemActiveStyle} > {props.tabs.map((tab) => { return ( ); })} - + ); @@ -187,8 +245,10 @@ let MobileTabLayoutTmp = (function () { }, ], }), + jsonTabs: manualOptionsControl(TabOptionComp, { + initOptions: [], + }), backgroundImage: withDefault(StringControl, ""), - mode: dropdownControl(ModeOptions, "inline"), navStyle: withDefault(styleControl(NavLayoutStyle), defaultStyle), navItemStyle: withDefault(styleControl(NavLayoutItemStyle), defaultStyle), navItemHoverStyle: withDefault(styleControl(NavLayoutItemHoverStyle), {}), @@ -206,7 +266,6 @@ let MobileTabLayoutTmp = (function () { radioButton: true, type: "oneline", })} - {/* {children.tabs.propertyView({})} */} { children.dataOptionType.getView() === DataOption.Manual ? children.tabs.propertyView({}) @@ -216,10 +275,6 @@ let MobileTabLayoutTmp = (function () { }
- { children.mode.propertyView({ - label: trans("labelProp.position"), - radioButton: true - })} {children.backgroundImage.propertyView({ label: `Background Image`, placeholder: 'https://temp.im/350x400', @@ -256,8 +311,6 @@ let MobileTabLayoutTmp = (function () { MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const [tabIndex, setTabIndex] = useState(0); const { readOnly } = useContext(ExternalEditorContext); - const tabs = comp.children.tabs.getView(); - const navMode = comp.children.mode.getView(); const navStyle = comp.children.navStyle.getView(); const navItemStyle = comp.children.navItemStyle.getView(); const navItemHoverStyle = comp.children.navItemHoverStyle.getView(); @@ -265,11 +318,14 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const backgroundImage = comp.children.backgroundImage.getView(); const jsonItems = comp.children.jsonItems.getView(); const dataOptionType = comp.children.dataOptionType.getView(); - // const tabViews = ( - // comp.children.tabs.children.manual.getView() as unknown as Array< - // ConstructorToComp - // > - // ).filter((tab) => !tab.children.hidden.getView()); + const bgColor = (useContext(ThemeContext)?.theme || defaultTheme).canvas; + + useEffect(() => { + comp.children.jsonTabs.dispatchChangeValueAction({ + manual: jsonItems as unknown as Array> + }); + }, [jsonItems]); + const tabViews = useMemo(() => { if (dataOptionType === DataOption.Manual) { return (comp.children.tabs.children.manual.getView() as unknown as Array< @@ -277,26 +333,18 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { > ).filter((tab) => !tab.children.hidden.getView()); } - return jsonItems.filter(item => !Boolean(item.hidden)).map((item) => { - const container = getCompContainer({ - Comp: TabOptionComp, - initialValue: { - ...item, - } - }) - if (container) { - container.initialized = true; - container.comp = evalAndReduceWithExposing(container.comp); - } - return container!.comp as unknown as ConstructorToComp - }) - }, [dataOptionType]) - - console.log(tabViews); + if (dataOptionType === DataOption.Json) { + return (comp.children.jsonTabs.children.manual.getView() as unknown as Array< + ConstructorToComp + > + ).filter((tab) => !tab.children.hidden.getView()); + } + return []; + }, [dataOptionType, jsonItems, comp.children.tabs, comp.children.jsonTabs]) const appView = useMemo(() => { const currentTab = tabViews[tabIndex]; - + return (currentTab && currentTab.children.app.getAppId() && currentTab.children.app.getView()) || ( @@ -305,7 +353,12 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { style={{ height: "100%", backgroundColor: "white" }} /> ) - }, [tabIndex]); + }, [tabIndex, tabViews, dataOptionType]); + + let backgroundStyle = navStyle.background; + if(!isEmpty(backgroundImage)) { + backgroundStyle = `center / cover url('${backgroundImage}') no-repeat, ${backgroundStyle}`; + } const tabBarView = ( { selectedKey={tabIndex + ""} onChange={(key) => setTabIndex(Number(key))} readOnly={!!readOnly} + canvasBg={bgColor} + tabStyle={{ + border: `1px solid ${navStyle.border}`, + radius: navStyle.radius, + text: navStyle.text, + margin: navStyle.margin, + padding: navStyle.padding, + background: backgroundStyle, + }} + tabItemStyle={navItemStyle} + tabItemHoverStyle={navItemHoverStyle} + tabItemActiveStyle={navItemActiveStyle} /> ); - //console.log("appView", appView); - if (readOnly) { return ( diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts index 9397bcbea..d102dfdb1 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts @@ -79,28 +79,25 @@ export const jsonMenuItems = [ export const mobileNavJsonMenuItems = [ { label: "Option 1", - // key: 'option-1', icon: "https://cdn-icons-png.flaticon.com/128/149/149338.png", app: { - appId: "" + appId: "", }, hidden: false, }, { label: "Option 2", - // key: 'option-2', icon: "https://cdn-icons-png.flaticon.com/128/149/149206.png", app: { - appId: "" + appId: "", }, hidden: false, }, { label: "Option 2", - // key: 'option-2', icon: "https://cdn-icons-png.flaticon.com/128/149/149206.png", app: { - appId: "" + appId: "", }, hidden: true, }