diff --git a/README.md b/README.md index 225c3f53d..b603176f7 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@

Lowcoder

-

The Open Source Retool, Tooljet and Appsmith Alternative

+

Lowcoder is the best Retool, Appsmith or Tooljet Alternative.

- Build internal and customer facing Apps fast, with no limitations + Create internal and external software applications for your Company and your Customers with minimal coding experience.

- + ## 📢 Use Lowcoder in 3 steps 1. Connect to any data sources or APIs. @@ -21,32 +21,42 @@ It's cumbersome to create a single app. You had to design user interfaces, write Low-code/No-code platforms are fast to get started with but quickly become unmaintainable and inflexible. This creates more problems than it solves. -Retool-like solutions are great for their simplicity and flexibility, but they can also be limited in different ways compared to frameworks like React/Vue. +NewGen Lowcode Platforms like Retool and others are great for their simplicity and flexibility - like Lowcoder too, but they can also be limited in different ways, especially when it comes to "external" applications for everyone. -Lowcoder wants to take a step forward. More specifically, Lowcoder is -- An all-in-one IDE to create internal or customer-facing apps. +Lowcoder wants to take a step forward. More specifically, Lowcoder is: +- An all-in-one IDE to create internal or customer-facing (external) apps. - A place to create, build and share building blocks of web applications. -- A domain-specific language that UI-configurable block is the first-class citizen. +- The tool and community to support your business, and lower the cost and time to develop interactive applications. +- The only platform to embed Lowcode Apps natively in Websites (no iFrame!) +- The only platform where you can build your own Meeting Tool - like Teams, Zoom or Google Meets, - just in the Lowcode way. ## 🪄 Features -- **Visual UI builder** with 50+ built-in components. +- **Visual UI builder** with 50+ built-in components. Save 90% of time to build apps. - **Modules** for reusable (!) component sets in the UI builder. -- **Embed Lowcoder Apps as native React component** instead of iFrame (!). [Demo](https://github.com/lowcoder-org/lowcoder-sdk-demo) +- **Embed Lowcoder Apps as native parts of any Website** instead of iFrame (!). [Demo](https://github.com/lowcoder-org/lowcoder-sdk-demo) +- **Video Meeting Components** to create your own individual Web-Meeting tool. - **Query Library** for reusable data queries of your data sources. - **Custom components** to develop own components and use them in the UI builder. - **Native Data connections** to PostgreSQL, MongoDB, MySQL, Redis, Elasticsearch, REST API, SMTP, etc. +- **Stream Data connections** to Websockets for realtime data updates & collaboration - **JavaScript supported everywhere** to transform data, control components, etc. - **Role-based access control (RBAC)** for granular permission management. - **Auto-saved and restorable history** for release and version control. - **App Themes and Theme Editor** to precisely align with your company's brand guidelines. -- **Self Hosting** to use Lowcoder in your internal company network. +- **Self Hosting** to use Lowcoder in your internal company network, even behind the firewall. - **Free Community Cloud** to start within a minute and build your first Apps. [Start here](https://app.lowcoder.cloud) ## 🏆 Comparisons +### Lowcoder vs Teams, Google Meets, Zoom +- build a Meeting tool with peace in mind. Blue buttons - ok. Red corners or circle Videostream - ok too. +- embedd applications in your Video-Meetings, so attendees can enjoy collaborative "anything". From shopping to working and gaming... +### Lowcoder vs Powerapps +- build a apps way faster than in Power Apps. Save up to 50& of the time at least. +- Use self-hosting to keep all apps and data under your control for example at the own baremetals. ### Lowcoder vs Retool - Lowcoder is open-source. You don't need to worry about vendor lock-in or being stuck with an outdated version of the software. -- In Lowcoder, developers can create and use their own components instead of depending on official updates. +- In Lowcoder, developers can build truly responsive apps - not as cumbersome as the "Desktop / Mobile switch" in Retool - Lowcoder is free and you can contribute! - The EE Version of Lowcoder comes with a much better pricing model, so you have no "per-user costs". ### Lowcoder vs Appsmith, Tooljet @@ -55,15 +65,17 @@ Lowcoder wants to take a step forward. More specifically, Lowcoder is - In Lowcoder, you can reuse common structures when building apps with modules and query library features. ### Lowcoder vs Mendix, Outsystems, Pega - Lowcoder is modern. The codebase is fresh and uses modern standards. -- Lowcoder Apps do not need a compile and deployment. Just publish and use. +- Lowcoder Apps do not need a compile and deployment. Just publish and use. Within seconds! - Lowcoder Apps can get embedded natively in websites and apps, even in mobile apps. ### Lowcoder vs internal Tool platforms - Lowcoder supports internal tools like admin panels perfectly, but also customer-facing apps can get developed and published. - The Lowcoder UI builder is straightforward and better to use than Bubble. - App release cycles and updates can be done nearly daily without service downtimes for customers and users. + ## 👐 Support and Community If you have any questions, please feel free to contact us or share them with our community. Our team is here ready to help. +And we mean it... Day by day! 📮 Best way is to chat with us on [Discord](https://discord.gg/qMG9uTmAx2) @@ -72,10 +84,16 @@ If you have any questions, please feel free to contact us or share them with our 🔎 Submit an issue here on [GitHub](https://github.com/lowcoder-org/lowcoder/issues) ## 💻 Deployment Options -You can access Lowcoder from [cloud-hosted version](https://www.lowcoder.cloud/) at any time, or use the following resources for deploying Lowcoder on different platforms: -- [Docker](docs/self-hosting/README.md) +You can access Lowcoder from [cloud-hosted version](https://app.lowcoder.cloud/) at any time, or use the following resources for deploying Lowcoder on different platforms: +- [Docker](https://docs.lowcoder.cloud/lowcoder-documentation/setup-and-run/self-hosting) ## 💪 Contributing - Language support: If you have experience with a language that isn't currently supported by our product, send us a pull request. - Create and share components or demos: If you've created something that might be useful to others, add the link here. - [Frontend contributing guide](https://github.com/lowcoder-org/lowcoder/tree/develop/client) + +## 🥇 Sponsors +Accelerate the growth of Lowcoder and unleash its potential with your Sponsorship – together, we're shaping the future of Lowcode for everyone! +[Be a Sponsor](https://github.com/sponsors/lowcoder-org) + +Like ... @CHSchuepfer. Thank you very much! \ No newline at end of file diff --git a/app.json b/app.json index 0889503a9..5d6a647c0 100644 --- a/app.json +++ b/app.json @@ -1,11 +1,16 @@ { "name": "lowcoder", - "description": "Lowcoder is a developer-friendly open-source low code platform to build internal apps within minutes.", + "description": "An all-in-one IDE to create internal or customer-facing apps. · Visual UI builder with 50+ built-in components", "repository": "https://github.com/lowcoder-org/lowcoder", - "logo": "https://cdn-files.openblocks.dev/logo.png", + "logo": "https://lowcoder.cloud/images/webclip.png", "keywords": [ - "low code", - "develop tool" + "LowCode", + "Low code", + "develop tool", + "Fast Application Development", + "Rapid development", + "Collaboration tool", + "Video conferencing" ], "stack": "container", "formation": { diff --git a/client/VERSION b/client/VERSION index cd57a8b95..9671f9a9b 100644 --- a/client/VERSION +++ b/client/VERSION @@ -1 +1 @@ -2.1.5 +2.1.7 \ No newline at end of file diff --git a/client/packages/lowcoder-design/src/components/Section.tsx b/client/packages/lowcoder-design/src/components/Section.tsx index a0c18134f..da58f9588 100644 --- a/client/packages/lowcoder-design/src/components/Section.tsx +++ b/client/packages/lowcoder-design/src/components/Section.tsx @@ -142,5 +142,5 @@ export const sectionNames = { validation: trans("prop.validation"), layout: trans("prop.layout"), style: trans("prop.style"), - meetings : trans("prop.meetings"), + meetings : trans("prop.meetings"), // added by Falk Wolsky }; diff --git a/client/packages/lowcoder-design/src/icons/icon-autocomplete-comp.svg b/client/packages/lowcoder-design/src/icons/icon-autocomplete-comp.svg index dd882963a..505c59adb 100644 --- a/client/packages/lowcoder-design/src/icons/icon-autocomplete-comp.svg +++ b/client/packages/lowcoder-design/src/icons/icon-autocomplete-comp.svg @@ -1 +1,20 @@ - \ No newline at end of file + + + + + + + + + diff --git a/client/packages/lowcoder-design/src/icons/icon-comment-comp.svg b/client/packages/lowcoder-design/src/icons/icon-comment-comp.svg index b6828e6a0..5a737ee86 100644 --- a/client/packages/lowcoder-design/src/icons/icon-comment-comp.svg +++ b/client/packages/lowcoder-design/src/icons/icon-comment-comp.svg @@ -1 +1,13 @@ - \ No newline at end of file + + + + + + diff --git a/client/packages/lowcoder-design/src/icons/icon-mention-comp.svg b/client/packages/lowcoder-design/src/icons/icon-mention-comp.svg index 4c04c61e2..5b311e0b4 100644 --- a/client/packages/lowcoder-design/src/icons/icon-mention-comp.svg +++ b/client/packages/lowcoder-design/src/icons/icon-mention-comp.svg @@ -1 +1,20 @@ - \ No newline at end of file + + + + + + + + diff --git a/client/packages/lowcoder-design/src/icons/icon-timeline-comp.svg b/client/packages/lowcoder-design/src/icons/icon-timeline-comp.svg index 329690d6d..e47f5fc79 100644 --- a/client/packages/lowcoder-design/src/icons/icon-timeline-comp.svg +++ b/client/packages/lowcoder-design/src/icons/icon-timeline-comp.svg @@ -1 +1,23 @@ - \ No newline at end of file + + + + + + + + + + + + diff --git a/client/packages/lowcoder-design/src/icons/icon-undo.svg b/client/packages/lowcoder-design/src/icons/icon-undo.svg index e7296453f..a1199d1bb 100644 --- a/client/packages/lowcoder-design/src/icons/icon-undo.svg +++ b/client/packages/lowcoder-design/src/icons/icon-undo.svg @@ -1,3 +1,10 @@ - - - \ No newline at end of file + + + + + + diff --git a/client/packages/lowcoder/site.webmanifest b/client/packages/lowcoder/site.webmanifest new file mode 100644 index 000000000..91352b1a9 --- /dev/null +++ b/client/packages/lowcoder/site.webmanifest @@ -0,0 +1 @@ +{"name":"Lowcoder.cloud","short_name":"Lowcoder","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} diff --git a/client/packages/lowcoder/src/assets/images/android-chrome-192x192.png b/client/packages/lowcoder/src/assets/images/android-chrome-192x192.png new file mode 100644 index 000000000..23d6e525e Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/android-chrome-192x192.png differ diff --git a/client/packages/lowcoder/src/assets/images/android-chrome-512x512.png b/client/packages/lowcoder/src/assets/images/android-chrome-512x512.png new file mode 100644 index 000000000..c690d113c Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/android-chrome-512x512.png differ diff --git a/client/packages/lowcoder/src/assets/images/apple-touch-icon.png b/client/packages/lowcoder/src/assets/images/apple-touch-icon.png new file mode 100644 index 000000000..6fff652aa Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/apple-touch-icon.png differ diff --git a/client/packages/lowcoder/src/assets/images/favicon-16x16.png b/client/packages/lowcoder/src/assets/images/favicon-16x16.png new file mode 100644 index 000000000..a87cf442d Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/favicon-16x16.png differ diff --git a/client/packages/lowcoder/src/assets/images/favicon-32x32.png b/client/packages/lowcoder/src/assets/images/favicon-32x32.png new file mode 100644 index 000000000..127dc44cb Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/favicon-32x32.png differ diff --git a/client/packages/lowcoder/src/assets/images/favicon.ico b/client/packages/lowcoder/src/assets/images/favicon.ico index 91f699e41..91f1543d0 100644 Binary files a/client/packages/lowcoder/src/assets/images/favicon.ico and b/client/packages/lowcoder/src/assets/images/favicon.ico differ diff --git a/client/packages/lowcoder/src/components/CompName.tsx b/client/packages/lowcoder/src/components/CompName.tsx index cf63dbab4..2f0b26b8b 100644 --- a/client/packages/lowcoder/src/components/CompName.tsx +++ b/client/packages/lowcoder/src/components/CompName.tsx @@ -11,6 +11,7 @@ import { GreyTextColor } from "constants/style"; import { UICompType } from "comps/uiCompRegistry"; import { trans } from "i18n"; import { getComponentDocUrl } from "comps/utils/compDocUtil"; +import { getComponentPlaygroundUrl } from "comps/utils/compDocUtil"; import { parseCompType } from "comps/utils/remote"; const CompDiv = styled.div<{ width?: number; hasSearch?: boolean; showSearch?: boolean }>` @@ -78,6 +79,7 @@ export const CompName = (props: Iprops) => { const compType = selectedComp.children.compType.getView() as UICompType; const compInfo = parseCompType(compType); const docUrl = getComponentDocUrl(compType); + const playgroundUrl = getComponentPlaygroundUrl(compType); const items: EditPopoverItemType[] = []; @@ -99,6 +101,16 @@ export const CompName = (props: Iprops) => { }); } + if (playgroundUrl) { + items.push({ + text: trans("comp.menuViewPlayground"), + onClick: () => { + window.open(playgroundUrl, "_blank"); + }, + }); + } + + if (compInfo.isRemote) { items.push({ text: trans("comp.menuUpgradeToLatest"), diff --git a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx index 62b55a7da..0999a4012 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx @@ -38,15 +38,13 @@ export class LayoutMenuItemComp extends MultiBaseComp { } override getPropertyView(): ReactNode { - const isLeaf = this.children.items.getView().length === 0; return ( <> - {isLeaf && - this.children.action.propertyView({ - onAppChange: (label) => { - label && this.children.label.dispatchChangeValueAction(label); - }, - })} + {this.children.action.propertyView({ + onAppChange: (label) => { + label && this.children.label.dispatchChangeValueAction(label); + }, + })} {this.children.label.propertyView({ label: trans("label") })} {this.children.icon.propertyView({ label: trans("icon"), @@ -98,12 +96,17 @@ const LayoutMenuItemCompMigrate = migrateOldData(LayoutMenuItemComp, (oldData: a export class LayoutMenuItemListComp extends list(LayoutMenuItemCompMigrate) { addItem(value?: any) { const data = this.getView(); + this.dispatch( this.pushAction( - value || { - label: trans("menuItem") + " " + (data.length + 1), - itemKey: genRandomKey(), - } + value + ? { + ...value, + itemKey: value.itemKey || genRandomKey(), + } : { + label: trans("menuItem") + " " + (data.length + 1), + itemKey: genRandomKey(), + } ) ); } diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 5e8d47320..368e459a9 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -1,4 +1,4 @@ -import { Layout, Menu as AntdMenu, MenuProps } from "antd"; +import { Layout, Menu as AntdMenu, MenuProps, Segmented } from "antd"; import MainContent from "components/layout/MainContent"; import { LayoutMenuItemComp, LayoutMenuItemListComp } from "comps/comps/layout/layoutMenuItemComp"; import { menuPropertyView } from "comps/comps/navComp/components/MenuItemList"; @@ -8,12 +8,38 @@ import { withDispatchHook } from "comps/generators/withDispatchHook"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; import { ALL_APPLICATIONS_URL } from "constants/routesURL"; import { TopHeaderHeight } from "constants/style"; -import { Section } from "lowcoder-design"; +import { Section, controlItem, sectionNames } from "lowcoder-design"; import { trans } from "i18n"; 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 { StringControl, jsonControl } from "comps/controls/codeControl"; +import { styleControl } from "comps/controls/styleControl"; +import { + NavLayoutStyle, + NavLayoutItemStyle, + NavLayoutItemStyleType, + NavLayoutItemHoverStyle, + NavLayoutItemHoverStyleType, + NavLayoutItemActiveStyle, + NavLayoutItemActiveStyleType, +} from "comps/controls/styleControlConstants"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import _ from "lodash"; +import { check } from "util/convertUtils"; +import { genRandomKey } from "comps/utils/idGenerator"; +import history from "util/history"; +import { + DataOption, + DataOptionType, + ModeOptions, + jsonMenuItems, + menuItemStyleOptions +} from "./navLayoutConstants"; + +const DEFAULT_WIDTH = 240; +type MenuItemStyleOptionValue = "normal" | "hover" | "active"; const StyledSide = styled(Layout.Sider)` max-height: calc(100vh - ${TopHeaderHeight}); @@ -39,22 +65,192 @@ const ContentWrapper = styled.div` } `; +const StyledMenu = styled(AntdMenu)<{ + $navItemStyle?: NavLayoutItemStyleType & { width: string}, + $navItemHoverStyle?: NavLayoutItemHoverStyleType, + $navItemActiveStyle?: NavLayoutItemActiveStyleType, +}>` + .ant-menu-item { + height: auto; + width: ${(props) => props.$navItemStyle?.width}; + background-color: ${(props) => props.$navItemStyle?.background}; + color: ${(props) => props.$navItemStyle?.text}; + border-radius: ${(props) => props.$navItemStyle?.radius} !important; + border: ${(props) => `1px solid ${props.$navItemStyle?.border}`}; + margin: ${(props) => props.$navItemStyle?.margin}; + padding: ${(props) => props.$navItemStyle?.padding}; + + } + .ant-menu-item-active { + background-color: ${(props) => props.$navItemHoverStyle?.background} !important; + color: ${(props) => props.$navItemHoverStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`}; + } + + .ant-menu-item-selected { + background-color: ${(props) => props.$navItemActiveStyle?.background} !important; + color: ${(props) => props.$navItemActiveStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`}; + } + + .ant-menu-submenu { + margin: ${(props) => props.$navItemStyle?.margin}; + width: ${(props) => props.$navItemStyle?.width}; + + .ant-menu-submenu-title { + width: 100%; + height: auto !important; + background-color: ${(props) => props.$navItemStyle?.background}; + color: ${(props) => props.$navItemStyle?.text}; + border-radius: ${(props) => props.$navItemStyle?.radius} !important; + border: ${(props) => `1px solid ${props.$navItemStyle?.border}`}; + margin: 0; + padding: ${(props) => props.$navItemStyle?.padding}; + + } + + .ant-menu-item { + width: 100%; + } + + &.ant-menu-submenu-active { + >.ant-menu-submenu-title { + width: 100%; + background-color: ${(props) => props.$navItemHoverStyle?.background} !important; + color: ${(props) => props.$navItemHoverStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`}; + } + } + &.ant-menu-submenu-selected { + >.ant-menu-submenu-title { + width: 100%; + background-color: ${(props) => props.$navItemActiveStyle?.background} !important; + color: ${(props) => props.$navItemActiveStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`}; + } + } + } + +`; + +const StyledImage = styled.img` + height: 1em; + color: currentColor; +`; + +const defaultStyle = { + radius: '0px', + margin: '0px', + padding: '0px', +} + +type UrlActionType = { + url?: string; + newTab?: boolean; +} + +export type MenuItemNode = { + label: string; + key: string; + hidden?: boolean; + icon?: any; + action?: UrlActionType, + children?: MenuItemNode[]; +} + +function checkDataNodes(value: any, key?: string): MenuItemNode[] | 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["action"], ["object", "undefined"], "action"); + checkDataNodes(node["children"], "children"); + return node; + }); +} + +function convertTreeData(data: any) { + return data === "" ? [] : checkDataNodes(data) ?? []; +} + let NavTmpLayout = (function () { const childrenMap = { + dataOptionType: dropdownControl(DataOptionType, DataOption.Manual), items: withDefault(LayoutMenuItemListComp, [ { label: trans("menuItem") + " 1", + itemKey: genRandomKey(), }, ]), + jsonItems: jsonControl(convertTreeData, jsonMenuItems), + width: withDefault(StringControl, DEFAULT_WIDTH), + 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 ( - <> -
{menuPropertyView(children.items)}
- +
+
+ {children.dataOptionType.propertyView({ + radioButton: true, + type: "oneline", + })} + { + children.dataOptionType.getView() === DataOption.Manual + ? menuPropertyView(children.items) + : children.jsonItems.propertyView({ + label: "Json Data", + }) + } +
+
+ { children.width.propertyView({ + label: trans("navLayout.width"), + tooltip: trans("navLayout.widthTooltip"), + placeholder: DEFAULT_WIDTH + "", + })} + { 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(); @@ -64,13 +260,98 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const pathParam = useAppPathParam(); const isViewMode = isUserViewMode(pathParam); const [selectedKey, setSelectedKey] = useState(""); - const items = useMemo(() => comp.children.items.getView(), [comp.children.items]); - + const items = comp.children.items.getView(); + const navWidth = comp.children.width.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(); + // filter out hidden. unauthorised items filtered by server const filterItem = useCallback((item: LayoutMenuItemComp): boolean => { return !item.children.hidden.getView(); }, []); + const generateItemKeyRecord = useCallback( + (items: LayoutMenuItemComp[] | MenuItemNode[]) => { + const result: Record = {}; + if(dataOptionType === DataOption.Manual) { + (items as LayoutMenuItemComp[])?.forEach((item) => { + const subItems = item.children.items.getView(); + if (subItems.length > 0) { + Object.assign(result, generateItemKeyRecord(subItems)) + } + result[item.getItemKey()] = item; + }); + } + if(dataOptionType === DataOption.Json) { + (items as MenuItemNode[])?.forEach((item) => { + if (item.children?.length) { + Object.assign(result, generateItemKeyRecord(item.children)) + } + result[item.key] = item; + }) + } + return result; + }, [dataOptionType] + ) + + const itemKeyRecord = useMemo(() => { + if(dataOptionType === DataOption.Json) { + return generateItemKeyRecord(jsonItems) + } + return generateItemKeyRecord(items) + }, [dataOptionType, jsonItems, items, generateItemKeyRecord]); + + const onMenuItemClick = useCallback(({key}: {key: string}) => { + const itemComp = itemKeyRecord[key] + + const url = [ + ALL_APPLICATIONS_URL, + pathParam.applicationId, + pathParam.viewMode, + key, + ].join("/"); + + // handle manual menu item action + if(dataOptionType === DataOption.Manual) { + (itemComp as LayoutMenuItemComp).children.action.act(url); + return; + } + // handle json menu item action + if((itemComp as MenuItemNode).action?.newTab) { + return window.open((itemComp as MenuItemNode).action?.url, '_blank') + } + history.push(url); + }, [pathParam.applicationId, pathParam.viewMode, dataOptionType, itemKeyRecord]) + + const getJsonMenuItem = useCallback( + (items: MenuItemNode[]): MenuProps["items"] => { + return items?.map((item: MenuItemNode) => { + const { + label, + key, + hidden, + icon, + children, + } = item; + return { + label, + key, + hidden, + icon: , + onTitleClick: onMenuItemClick, + onClick: onMenuItemClick, + ...(children?.length && { children: getJsonMenuItem(children) }), + } + }) + }, [onMenuItemClick] + ) + const getMenuItem = useCallback( (itemComps: LayoutMenuItemComp[]): MenuProps["items"] => { return itemComps.filter(filterItem).map((item) => { @@ -81,14 +362,20 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { title: label, key: item.getItemKey(), icon: {item.children.icon.getView()}, + onTitleClick: onMenuItemClick, + onClick: onMenuItemClick, ...(subItems.length > 0 && { children: getMenuItem(subItems) }), }; }); }, - [filterItem] + [onMenuItemClick, filterItem] ); - const menuItems = useMemo(() => getMenuItem(items), [items, getMenuItem]); + const menuItems = useMemo(() => { + if(dataOptionType === DataOption.Json) return getJsonMenuItem(jsonItems) + + return getMenuItem(items) + }, [dataOptionType, jsonItems, getJsonMenuItem, items, getMenuItem]); // Find by path itemKey const findItemPathByKey = useCallback( @@ -134,22 +421,60 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { [filterItem] ); - const itemKeyRecord = useMemo(() => { - const result: Record = {}; - items.forEach((item) => { - const subItems = item.children.items.getView(); - if (subItems.length > 0) { - item.children.items - .getView() - .forEach((subItem) => (result[subItem.getItemKey()] = subItem)); - } else { - result[item.getItemKey()] = item; + // Find by path itemKey + const findItemPathByKeyJson = useCallback( + (itemComps: MenuItemNode[], itemKey: string): string[] => { + for (let item of itemComps) { + const subItems = item.children; + if (subItems?.length) { + // have subMenus + const childPath = findItemPathByKeyJson(subItems, itemKey); + if (childPath.length > 0) { + return [item.key, ...childPath]; + } + } else { + if (item.key === itemKey) { + return [item.key]; + } + } + } + return []; + }, + [] + ); + + // Get the first visible menu + const findFirstItemPathJson = useCallback( + (itemComps: MenuItemNode[]): string[] => { + for (let item of itemComps) { + if (!item.hidden) { + const subItems = item.children; + if (subItems?.length) { + // have subMenus + const childPath = findFirstItemPathJson(subItems); + if (childPath.length > 0) { + return [item.key, ...childPath]; + } + } else { + return [item.key]; + } + } } - }); - return result; - }, [items]); + return []; + }, [] + ); const defaultOpenKeys = useMemo(() => { + if(dataOptionType === DataOption.Json) { + let itemPath: string[]; + if (pathParam.appPageId) { + itemPath = findItemPathByKeyJson(jsonItems, pathParam.appPageId); + } else { + itemPath = findFirstItemPathJson(jsonItems); + } + return itemPath.slice(0, itemPath.length - 1); + } + let itemPath: string[]; if (pathParam.appPageId) { itemPath = findItemPathByKey(items, pathParam.appPageId); @@ -170,34 +495,79 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { setSelectedKey(selectedKey); }, [pathParam.appPageId]); - let pageView = ; - const selectedItem = itemKeyRecord[selectedKey]; - if (selectedItem && !selectedItem.children.hidden.getView()) { - const compView = selectedItem.children.action.getView(); - if (compView) { - pageView = compView; + const pageView = useMemo(() => { + let pageView = ; + + if(dataOptionType === DataOption.Manual) { + const selectedItem = (itemKeyRecord[selectedKey] as LayoutMenuItemComp); + if (selectedItem && !selectedItem.children.hidden.getView()) { + const compView = selectedItem.children.action.getView(); + if (compView) { + pageView = compView; + } + } + } + if(dataOptionType === DataOption.Json) { + const item = (itemKeyRecord[selectedKey] as MenuItemNode) + if(item?.action?.url) { + pageView =