From 8541a90a38dca798b9ba1dfe7f9c877a0fa20927 Mon Sep 17 00:00:00 2001 From: dengll Date: Fri, 28 Jul 2023 10:31:15 +0800 Subject: [PATCH] fix: procomponent & nav edit panel --- app/client/.prettierrc | 3 +- app/client/src/ce/constants/messages.ts | 4 +- app/client/src/ce/utils/airgapHelpers.tsx | 2 +- .../editorComponents/ApiResponseView.tsx | 61 +- app/client/src/constants/Colors.tsx | 4 +- app/client/src/constants/DefaultTheme.tsx | 4 +- .../src/pages/AppViewer/AppViewerLayout.tsx | 120 ++-- app/client/src/pages/AppViewer/PageMenu.tsx | 1 - app/client/src/pages/AppViewer/index.tsx | 17 +- .../GeneratePageForm/GeneratePageForm.tsx | 2 +- .../IntegrationEditor/DatasourceHome.tsx | 2 +- .../pages/Editor/ViewerLayoutEditor/index.tsx | 549 ++++++++++-------- .../Editor/ViewerLayoutEditor/index_C.tsx | 506 ++++++++++++++++ .../pages/Editor/ViewerLayoutEditor/mock.ts | 97 ++++ .../sagas/ActionExecution/PluginActionSaga.ts | 235 +++++++- app/client/src/utils/WidgetFactoryHelpers.ts | 4 +- app/client/src/utils/treeUtils.ts | 38 ++ app/client/tsconfig.json | 1 + app/server/.env | 4 +- 19 files changed, 1258 insertions(+), 396 deletions(-) create mode 100644 app/client/src/pages/Editor/ViewerLayoutEditor/index_C.tsx create mode 100644 app/client/src/pages/Editor/ViewerLayoutEditor/mock.ts diff --git a/app/client/.prettierrc b/app/client/.prettierrc index b1f3b1c6ce..83f602a56d 100644 --- a/app/client/.prettierrc +++ b/app/client/.prettierrc @@ -4,6 +4,5 @@ "useTabs": false, "semi": true, "singleQuote": false, - "trailingComma": "all", - "arrowParens": "always" + "trailingComma": "all" } diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index dea9c47374..e25cd56a3c 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1411,9 +1411,9 @@ export const IN_APP_EMBED_SETTING = { upgradeHeadingForInviteModal: () => "使用嵌入功能需要先在设置中公开您的应用", upgradeContent: () => "想将嵌入企业内的系统", appsmithBusinessEdition: () => "升级至企业版使用", - secondaryHeadingForAppSettings: () => "Make your app public to embed", + secondaryHeadingForAppSettings: () => "公开应用嵌入", secondaryHeading: () => - "Please contact your workspace admin to make the app public before embedding", + "请联系工作区管理员,使用嵌入功能需要先在设置中公开您的应用", }; export const APP_NAVIGATION_SETTING = { diff --git a/app/client/src/ce/utils/airgapHelpers.tsx b/app/client/src/ce/utils/airgapHelpers.tsx index 1ae6dd5d06..72d50df6d7 100644 --- a/app/client/src/ce/utils/airgapHelpers.tsx +++ b/app/client/src/ce/utils/airgapHelpers.tsx @@ -1,5 +1,5 @@ export const getAssetUrl = (src = "") => { - if (src === "/oracle.svg") { + if (src.toLowerCase() === "/oracle.svg") { return "/logo/Oracle.svg"; } return src; diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 30f4ff0d69..c61f36d5d1 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -484,7 +484,7 @@ function ApiResponseView(props: Props) { const tabs = [ { key: "response", - title: "返回结果", + title: "Response", panelComponent: ( {Array.isArray(messages) && messages.length > 0 && ( @@ -572,53 +572,12 @@ function ApiResponseView(props: Props) { )} )} - - {isEmpty(response.statusCode) ? ( - - - - {EMPTY_RESPONSE_FIRST_HALF()} - - {EMPTY_RESPONSE_LAST_HALF()} - - - ) : ( - - {isString(response?.body) && isHtml(response?.body) ? ( - - ) : responseTabs && - responseTabs.length > 0 && - selectedTabIndex !== -1 ? ( - - ) : null} - - )} - ), }, { key: "headers", - title: "请求头", + title: "Headers", panelComponent: ( {hasFailed && !isRunning && ( @@ -647,7 +606,7 @@ function ApiResponseView(props: Props) { onClick={onRunClick} size={Size.medium} tag="button" - text="运行" + text="Run" type="button" /> {EMPTY_RESPONSE_LAST_HALF()} @@ -718,7 +677,7 @@ function ApiResponseView(props: Props) { }} size={Size.medium} tag="button" - text="取消请求" + text="Cancel Request" type="button" /> @@ -730,7 +689,7 @@ function ApiResponseView(props: Props) { {response.statusCode && ( - 状态码: + Status: {response.duration && ( - 耗时: + Time: {response.duration} ms )} {response.size && ( - 响应大小: + Size: {formatBytes(parseInt(response.size))} @@ -757,9 +716,11 @@ function ApiResponseView(props: Props) { )} {!isEmpty(response?.body) && Array.isArray(response?.body) && ( - 返回结果: + Result: - {`${response.body.length} 条记录`} + {`${response?.body.length} Record${ + response?.body.length > 1 ? "s" : "" + }`} )} diff --git a/app/client/src/constants/Colors.tsx b/app/client/src/constants/Colors.tsx index 7bec558e9c..d6322d0dbf 100644 --- a/app/client/src/constants/Colors.tsx +++ b/app/client/src/constants/Colors.tsx @@ -51,8 +51,8 @@ export const Colors = { ALTO: "#DFDFDF", PRIMARY: primaryColor, - PRIMARY_DARK: darkenColor(primaryColor, 50), - PRIMARY_LIGHT: lightenColor(primaryColor, "0.93"), + PRIMARY_DARK: "#3ababc", + PRIMARY_LIGHT: "#CAECDC", GREEN: primaryColor, FOAM: "#D9FDED", LIGHT_GREEN_CYAN: "#e5f6ec", diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index d56fe99f1f..e5fa4d9a19 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -2085,8 +2085,8 @@ export const dark: ColorType = { multiDropdownBoxHoverBg: darkShades[0], iconColor: darkShades[5], ctaTextColor: "#202223", - ctaBackgroundColor: "rgb(248, 106, 43, 0.1)", - ctaLearnMoreTextColor: "#f86a2b", + ctaBackgroundColor: "rgb(39, 183, 183, 0.1)", + ctaLearnMoreTextColor: "rgb(39 ,183 ,183 ,1)", connections: { error: "#f22b2b", connectionsCount: darkShades[11], diff --git a/app/client/src/pages/AppViewer/AppViewerLayout.tsx b/app/client/src/pages/AppViewer/AppViewerLayout.tsx index 6d62d2f7a5..3bfbec6b40 100644 --- a/app/client/src/pages/AppViewer/AppViewerLayout.tsx +++ b/app/client/src/pages/AppViewer/AppViewerLayout.tsx @@ -34,40 +34,43 @@ const ColorfulLayout = styled.div<{ const getIconType = (icon: any) => (icon ? `icon-${icon}` : undefined); -const makeRouteNode = (pagesMap: any, newTree: any[]) => (node: any) => { - let item: any; - const icon = getIconType(node.icon); - if (node.isPage) { - if (pagesMap[node.title]) { +const makeRouteNode = + (pagesMap: any, newTree: any[], hideRow: any[]) => (node: any) => { + let item: any; + const icon = getIconType(node.icon); + if (node.isPage) { + if (pagesMap[node.title]) { + item = { + name: node.title, + icon, + path: viewerURL({ + pageId: pagesMap[node.title].pageId, + }), + }; + pagesMap[node.title].visited = true; + } + } else if (node.children) { + const routes: any = []; + node.children.forEach(makeRouteNode(pagesMap, routes, hideRow)); item = { name: node.title, icon, - path: viewerURL({ - pageId: pagesMap[node.title].pageId, - }), + path: "/", + routes, + }; + } else { + item = { + name: node.title, + icon, + path: "/", }; - pagesMap[node.title].visited = true; } - } else if (node.children) { - const routes: any = []; - node.children.forEach(makeRouteNode(pagesMap, routes)); - item = { - name: node.title, - icon, - path: "/", - routes, - }; - } else { - item = { - name: node.title, - icon, - path: "/", - }; - } - if (item) { - newTree.push(item); - } -}; + if (item) { + if (!hideRow.find((hn) => hn.pageId === node.pageId)) { + newTree.push(item); + } + } + }; const StyledIcon = styled(Icon)` text-align: center; @@ -114,8 +117,12 @@ function AppViewerLayout({ children, location }: AppViewerLayoutType) { }, {}); const newMenuTree: any = []; const newOuterTree: any = []; - current.treeData.forEach(makeRouteNode(pagesMap, newMenuTree)); - current.outsiderTree.forEach(makeRouteNode(pagesMap, newOuterTree)); + current.treeData.forEach( + makeRouteNode(pagesMap, newMenuTree, current.outsiderTree), + ); + current.outsiderTree.forEach( + makeRouteNode(pagesMap, newOuterTree, current.outsiderTree), + ); const newPages = Object.values(pagesMap) .filter((p: any) => !p.visited) .map((p: any) => ({ @@ -125,7 +132,6 @@ function AppViewerLayout({ children, location }: AppViewerLayoutType) { pageId: p.pageId, }), })); - init = { logoUrl: current.logoUrl, color: current.color, @@ -141,19 +147,10 @@ function AppViewerLayout({ children, location }: AppViewerLayoutType) { if (!isInitialized) { return null; } + console.log(initState.treeData); if (isMobile || isEmbed) { - return
{children}
; - } - - function CollapseToggle(props: { isOpen: boolean }) { - const { isOpen } = props; - return ( - - ); + return
{children}
; } return ( @@ -162,32 +159,17 @@ function AppViewerLayout({ children, location }: AppViewerLayoutType) { title={appName} logo={initState.logoUrl || DEFAULT_VIEWER_LOGO} layout="mix" - // collapsedButtonRender={false} - // collapsed={collapsed} - // onCollapse={setCollapsed} - // postMenuData={(menuData) => { - // return [ - // { - // icon: collapsed ? ( - // - // ) : ( - // - // ), - // name: collapsed ? "" : "收缩", - // onTitleClick: () => setCollapsed(!collapsed), - // }, - // ...(menuData || []), - // ]; - // }} - menuItemRender={(item: any, dom: any) => ( - { - history.push(item.path); - }} - > - {dom} - - )} + menuItemRender={(item: any, dom: any) => { + return ( + { + history.push(item.path); + }} + > + {dom} + + ); + }} route={{ routes: initState.treeData, }} diff --git a/app/client/src/pages/AppViewer/PageMenu.tsx b/app/client/src/pages/AppViewer/PageMenu.tsx index a11fed871e..3f3901ba95 100644 --- a/app/client/src/pages/AppViewer/PageMenu.tsx +++ b/app/client/src/pages/AppViewer/PageMenu.tsx @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ import React, { useState, useEffect, useRef, useMemo } from "react"; import type { ApplicationPayload, diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index 12a68e2721..d33ebb60c8 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -207,7 +207,7 @@ function AppViewer(props: Props) { document.body.style.fontFamily = "inherit"; }; }, [selectedTheme.properties.fontFamily.appFont]); - + console.log("appviewer", isMobile, "isMobile"); return ( @@ -219,10 +219,15 @@ function AppViewer(props: Props) { description={pageDescription} name={currentApplicationDetails?.name} /> - - - - + + + eeeeeeeee + {/* {isInitialized && registered && } - + */} diff --git a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx index 23034784e9..953970aa4c 100644 --- a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx +++ b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx @@ -170,7 +170,7 @@ function GeneratePageSubmitBtn({ isLoading={isLoading} onClick={() => !disabled && onSubmit()} size={Size.large} - text="Generate Page" + text="生成新页面" type="button" /> ) : null; diff --git a/app/client/src/pages/Editor/IntegrationEditor/DatasourceHome.tsx b/app/client/src/pages/Editor/IntegrationEditor/DatasourceHome.tsx index f41298817f..9fb4d7c944 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/DatasourceHome.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/DatasourceHome.tsx @@ -192,7 +192,7 @@ class DatasourceHomeScreen extends React.Component { const { currentApplication, pluginImages, plugins } = this.props; // console.log(pluginImages); const _pluginImages = mapValues(pluginImages, (o: any) => { - if (o.includes("oracle")) { + if (o.toLowerCase() === "oracle") { return "/logo/Oracle.svg"; } return o; diff --git a/app/client/src/pages/Editor/ViewerLayoutEditor/index.tsx b/app/client/src/pages/Editor/ViewerLayoutEditor/index.tsx index 749003ef04..e2e3edeaf2 100644 --- a/app/client/src/pages/Editor/ViewerLayoutEditor/index.tsx +++ b/app/client/src/pages/Editor/ViewerLayoutEditor/index.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import styled, { useTheme } from "styled-components"; import { useHistory } from "react-router"; import { useDispatch, useSelector } from "react-redux"; -import { get } from "lodash"; +import { get, size, map, flatMapDeep, cloneDeep } from "lodash"; import { ControlIcons } from "icons/ControlIcons"; import { getCurrentApplication } from "selectors/applicationSelectors"; import { @@ -12,22 +12,37 @@ import { } from "selectors/editorSelectors"; import { getSelectedAppThemeProperties } from "selectors/appThemingSelectors"; import { builderURL } from "RouteBuilder"; -import { - SortableTreeWithoutDndContext as SortableTree, - addNodeUnderParent, - removeNodeAtPath, - changeNodeAtPath, - getNodeAtPath, - walk, -} from "react-sortable-tree-patch-react-17/dist/index.cjs.js"; -import FileExplorerTheme from "react-sortable-tree-theme-full-node-drag"; -import IconSelect from "./IconSelect"; -import { Button, Input, Form, message } from "antd"; +// import { +// SortableTreeWithoutDndContext as SortableTree, +// addNodeUnderParent, +// removeNodeAtPath, +// changeNodeAtPath, +// getNodeAtPath, +// walk, +// } from "react-sortable-tree-patch-react-17/dist/index.cjs.js"; +// import FileExplorerTheme from "react-sortable-tree-theme-full-node-drag"; +// import IconSelect from "./IconSelect"; +import { Button, Input, Form, message, Tree, Divider, Typography } from "antd"; import ColorPickerComponent from "components/propertyControls/ColorPickerComponentV2"; import { updateApplication } from "actions/applicationActions"; import { Colors } from "constants/Colors"; +import { Icon } from "@blueprintjs/core"; import { DEFAULT_VIEWER_LOGO } from "constants/AppConstants"; -// } from "@nosferatu500/react-sortable-tree"; +import type { DataNode, TreeProps } from "antd/es/tree"; +// import { menutree, navdata, mockpages } from "./mock"; +import { + processTreeData, + generateUuid, + mapTree, + // traverseTree, + removeNodeByKey, +} from "utils/treeUtils"; +const { Paragraph } = Typography; + +const x = 3; +const y = 2; +const z = 1; +const defaultData: DataNode[] = []; const Wrapper = styled.div` padding: 20px; @@ -52,11 +67,8 @@ const Header = styled.div` } `; const MenuContainer = styled.div` - display: flex; - & > div { - flex: 1; - border: 1px solid ${(props) => props.theme.colors.primary}; + // border: 1px solid ${(props) => props.theme.colors.primary}; margin: 10px; border-radius: 4px; @@ -71,14 +83,17 @@ const MenuContainer = styled.div` } `; const TreeContainer = styled.div` - height: 400px; -`; - -const NameInput = styled.input` - border: none; - background: ${Colors.MINT_GREEN_LIGHT}; - border-radius: 4px; - padding: 4px 6px; + min-height: 200px; + width: 65%; + padding: 4rem 4rem; + && .ant-tree .ant-tree-treenode { + align-items: center; + } + && .ant-tree .ant-tree-switcher { + // align-self: center; + } + // transform: scale(1.2); + // transform-origin: top left; `; const ConfigContainer = styled.div` @@ -224,7 +239,9 @@ function PagesEditor() { const [color, setColor] = useState(initState.color); const [treeData, setTreeData] = useState(initState.treeData); const [outsiderTree, setOutsiderTree] = useState(initState.outsiderTree); - const [form] = Form.useForm(); + const [gData, setGData] = useState(defaultData); + const [hideNodes, setHideNodes] = useState([]); + const [, setSymbol] = useState(); useEffect(() => { const pagesMap = pages.reduce((a: any, c: any) => { @@ -243,147 +260,160 @@ function PagesEditor() { pageId: p.pageId, isPage: true, })); - setTreeData(newMenuTree.concat(newPages)); - // update outsider pages + const _tree = newMenuTree.concat(newPages); + setTreeData(_tree); setOutsiderTree(newOuterTree); + setHideNodes(newOuterTree.map((o: any) => o.pageId)); + initNewTree(_tree); }, [pages]); + const initNewTree = (tree: any) => { + const _formatTree = processTreeData(tree); + setGData(_formatTree); + }; + + const onDragEnter: TreeProps["onDragEnter"] = (info) => { + // expandedKeys, set it when controlled is needed + // setExpandedKeys(info.expandedKeys) + }; + + const onDrop: TreeProps["onDrop"] = (info) => { + const dropKey = info.node.key; + const dragKey = info.dragNode.key; + const dropPos = info.node.pos.split("-"); + const dropPosition = + info.dropPosition - Number(dropPos[dropPos.length - 1]); + + const loop = ( + data: DataNode[], + key: React.Key, + callback: (node: DataNode, i: number, data: DataNode[]) => void, + ) => { + for (let i = 0; i < data.length; i++) { + if (data[i].key === key) { + return callback(data[i], i, data); + } + if (data[i].children) { + loop(data[i].children!, key, callback); + } + } + }; + const data = [...gData]; + + // Find dragObject + let dragObj: DataNode; + loop(data, dragKey, (item, index, arr) => { + arr.splice(index, 1); + dragObj = item; + }); + + if (!info.dropToGap) { + // Drop on the content + loop(data, dropKey, (item) => { + item.children = item.children || []; + // where to insert. New item was inserted to the start of the array in this example, but can be anywhere + item.children.unshift(dragObj); + }); + } else if ( + ((info.node as any).props.children || []).length > 0 && // Has children + (info.node as any).props.expanded && // Is expanded + dropPosition === 1 // On the bottom gap + ) { + loop(data, dropKey, (item) => { + item.children = item.children || []; + // where to insert. New item was inserted to the start of the array in this example, but can be anywhere + item.children.unshift(dragObj); + // in previous version, we use item.children.push(dragObj) to insert the + // item to the tail of the children + }); + } else { + let ar: DataNode[] = []; + let i: number; + loop(data, dropKey, (_item, index, arr) => { + ar = arr; + i = index; + }); + if (dropPosition === -1) { + ar.splice(i!, 0, dragObj!); + } else { + ar.splice(i! + 1, 0, dragObj!); + } + } + setGData(data); + }; + const onClose = useCallback(() => { history.push(builderURL({ pageId })); }, [pageId]); - const getNodeKey = ({ treeIndex }: any) => treeIndex; - - const removeNode = (path: any) => () => { - moveToOutsider(path); - setTreeData( - removeNodeAtPath({ - treeData, - path, - getNodeKey, - }), - ); - }; - - const moveToOutsider = (path: any) => { - const targetNode = getNodeAtPath({ - treeData, - path, - getNodeKey, - ignoreCollapsed: false, - }); - const removedPages: any[] = []; - walk({ - treeData: [targetNode.node], - getNodeKey, - ignoreCollapsed: false, - callback: (nodeInfo: any) => { - if (nodeInfo.node.isPage) { - removedPages.push(nodeInfo); - } - }, - }); - const outer = removedPages.map((p) => p.node).concat(outsiderTree); - setOutsiderTree(outer); - }; - - const onOutsiderTreeChanged = (tree: any[]) => { - const menus: any[] = []; - const pages: any[] = []; - tree.forEach((t: any) => { - if (t.isPage) { - pages.push(t); - } else { - menus.push(t); - } - }); - if (menus.length) { - menus.forEach((menu: any) => getPagesInTree(pages)(menu)); - } - setOutsiderTree(pages); - }; - - const addNodeAt = (node: any, path: any) => () => { - setTreeData( - addNodeUnderParent({ - treeData, - parentKey: path[path.length - 1], - expandParent: true, - getNodeKey, - newNode: { - title: "二级菜单", - }, - }).treeData, - ); - }; - - const addRootNode = () => { - setTreeData( - treeData.concat({ - title: "一级菜单", - }), - ); - }; - - const editNodeTitle = (node: any, path: any) => (event: any) => { - const title = event.target.value; - setTreeData( - changeNodeAtPath({ - treeData, - path, - getNodeKey, - newNode: { ...node, title }, - }), - ); - }; - - const onIconSelected = (node: any, path: any) => (icon?: string) => { - setTreeData( - changeNodeAtPath({ - treeData, - path, - getNodeKey, - newNode: { ...node, icon }, - }), - ); - }; - const saveConfig = async () => { + const _outsiderTree: any = []; + gData.forEach((gItem: any) => { + mapTree(gItem, (tn: any) => { + if (hideNodes.includes(tn.key)) { + _outsiderTree.push({ + title: tn.title, + pageId: tn.pageId, + isPage: true, + }); + } + }); + }); + setOutsiderTree(_outsiderTree); const data = { name: name, viewerLayout: JSON.stringify({ color, logoUrl, name, - treeData, - outsiderTree, + treeData: gData, + outsiderTree: _outsiderTree, }), }; dispatch(updateApplication(applicationId, data)); message.success("保存成功"); }; - // console.log(treeData, "treeData"); + const addRootNode = () => { + setGData( + gData.concat({ + title: "一级目录", + key: generateUuid(), + }), + ); + }; - const renderTitle = (node: any, path: any) => { - const iconContent = - path.length === 1 ? ( - - ) : null; - const titleContent = node.isPage ? ( - node.title - ) : ( - - ); - return ( - <> - {iconContent} - {titleContent} - - ); + // deleteMenu + const onDeleteMenu = (node: any) => { + if (size(node.children) > 0) { + message.warning("子级页面或目录移除后再删除目录"); + } else { + const _gdata = cloneDeep(gData); + removeNodeByKey(_gdata, node.key); + setGData(_gdata); + setSymbol(Symbol("deletemenu")); + message.info("删除目录"); + } + }; + + const toggleHidePage = (node: any) => { + if (hideNodes.includes(node.key)) { + // 打开 + setHideNodes(hideNodes.filter((p: any) => p !== node.key)); + } else { + setHideNodes([...hideNodes, node.key]); // 隐藏 + } + }; + + const nodeNameChange = (name: string, node: any) => { + gData.map((gnode: any) => { + return mapTree(gnode, (gn: any) => { + if (gn.key === node.key) { + gn.title = name; + } + }); + }); + setGData(gData); }; return ( @@ -399,106 +429,139 @@ function PagesEditor() {

应用菜单编辑

+
+
+
顶部导航
+ + +

{name}

+
+ +
+ + setName(e.target.value)} /> + + + setLogoUrl(e.target.value)} + /> + + + setColor(c)} + color={color} + showApplicationColors + showThemeColors + /> + +
+
+
+ +
+
+ 菜单导航 + +
+ + !dropNode.isPage} + blockNode + onDragEnter={onDragEnter} + onDrop={onDrop} + treeData={gData} + showLine={true} + showIcon={false} + titleRender={(node: any) => { + return ( +
+
+
+ {node.isPage ? ( + + ) : ( + + )} +
+
+ {node.isPage ? ( + node.title + ) : ( + + nodeNameChange(value, node), + }} + style={{ marginBottom: 0 }} + > + {node.title} + + )} +
+
- -
- - setName(e.target.value)} /> - - - setLogoUrl(e.target.value)} - /> - - - setColor(c)} - color={color} - showApplicationColors - showThemeColors - /> - -
- - -

{name}

-
-
- -
- -
-

- 菜单导航 - -

- - setTreeData(treeData)} - canNodeHaveChildren={(node: any) => !node.isPage} - generateNodeProps={({ node, path }: any) => ({ - title: renderTitle(node, path), - buttons: [ - node.isPage || path.length >= MAX_DEPTH - 1 ? null : ( - + {!node.isPage && !size(node.children) ? ( + onDeleteMenu(node)} + /> + ) : null} + toggleHidePage(node)} /> - ), - , - ], - listIndex: 0, - lowerSiblingCounts: [], - })} - /> - -
-
-

菜单隐藏页面

- - !node.isPage} - shouldCopyOnOutsideDrop={false} - dndType={EXTERNAL_NODE_TYPE} - theme={FileExplorerTheme} - rowHeight={64} - maxDepth={1} - /> - -
-
+
+
+ ); + }} + /> +
+
+ +
+ +
- - ); } diff --git a/app/client/src/pages/Editor/ViewerLayoutEditor/index_C.tsx b/app/client/src/pages/Editor/ViewerLayoutEditor/index_C.tsx new file mode 100644 index 0000000000..749003ef04 --- /dev/null +++ b/app/client/src/pages/Editor/ViewerLayoutEditor/index_C.tsx @@ -0,0 +1,506 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import styled, { useTheme } from "styled-components"; +import { useHistory } from "react-router"; +import { useDispatch, useSelector } from "react-redux"; +import { get } from "lodash"; +import { ControlIcons } from "icons/ControlIcons"; +import { getCurrentApplication } from "selectors/applicationSelectors"; +import { + getCurrentApplicationId, + getCurrentPageId, + getVisiblePageList, +} from "selectors/editorSelectors"; +import { getSelectedAppThemeProperties } from "selectors/appThemingSelectors"; +import { builderURL } from "RouteBuilder"; +import { + SortableTreeWithoutDndContext as SortableTree, + addNodeUnderParent, + removeNodeAtPath, + changeNodeAtPath, + getNodeAtPath, + walk, +} from "react-sortable-tree-patch-react-17/dist/index.cjs.js"; +import FileExplorerTheme from "react-sortable-tree-theme-full-node-drag"; +import IconSelect from "./IconSelect"; +import { Button, Input, Form, message } from "antd"; +import ColorPickerComponent from "components/propertyControls/ColorPickerComponentV2"; +import { updateApplication } from "actions/applicationActions"; +import { Colors } from "constants/Colors"; +import { DEFAULT_VIEWER_LOGO } from "constants/AppConstants"; +// } from "@nosferatu500/react-sortable-tree"; + +const Wrapper = styled.div` + padding: 20px; + height: 100%; + overflow: auto; +`; +const Header = styled.div` + display: flex; + padding-bottom: 20px; + button { + margin-left: auto; + } + & > div { + display: flex; + align-items: center; + h1 { + margin: 0; + font-size: 18px; + color: ${(props) => props.theme.colors.text.heading}; + margin-left: 10px; + } + } +`; +const MenuContainer = styled.div` + display: flex; + + & > div { + flex: 1; + border: 1px solid ${(props) => props.theme.colors.primary}; + margin: 10px; + border-radius: 4px; + + h2 { + font-size: 16px; + padding: 12px 20px; + + button { + float: right; + } + } + } +`; +const TreeContainer = styled.div` + height: 400px; +`; + +const NameInput = styled.input` + border: none; + background: ${Colors.MINT_GREEN_LIGHT}; + border-radius: 4px; + padding: 4px 6px; +`; + +const ConfigContainer = styled.div` + padding: 20px; + display: flex; + + & > .ant-form { + width: 600px; + } +`; +const NavPreview = styled.div<{ + color?: string; +}>` + flex: 1; + background: ${(props) => props.color || Colors.MINT_GREEN}; + height: 48px; + border-radius: 4px; + padding: 8px 16px; + + img { + display: inline-block; + width: 32px; + height: 32px; + } + + h2 { + color: #fff; + font-size: 16px; + display: inline-block; + height: 32px; + margin: 0 0 0 12px; + line-height: 32px; + vertical-align: middle; + } +`; +const ColorPicker = styled(ColorPickerComponent)` + border: 1px solid #999; +`; + +const CloseIcon = ControlIcons.CLOSE_CONTROL; +const DeleteIcon = ControlIcons.DELETE_CONTROL; +const AddIcon = ControlIcons.INCREASE_CONTROL; + +const MAX_DEPTH = 3; +const EXTERNAL_NODE_TYPE = "MENU_DATA_NODE"; + +const getPagesInTree = (all: any[]) => (node: any) => { + if (node.isPage) { + all.push(node); + return; + } + if (node.children) { + node.children.forEach(getPagesInTree(all)); + } +}; + +const updateMenuTree = (pagesMap: any, newTree: any[]) => (node: any) => { + let item: any; + if (node.isPage) { + if (pagesMap[node.pageId]) { + item = { + ...node, + title: pagesMap[node.pageId].pageName, + }; + pagesMap[node.pageId].visited = true; + } + } else if (node.children) { + const children: any = []; + node.children.forEach(updateMenuTree(pagesMap, children)); + item = { + ...node, + children, + }; + } else { + item = { ...node }; + } + if (item) { + newTree.push(item); + } +}; + +const updateJsonPageId: any = (pagesMap: any, list: any[]) => { + return list.map((item: any) => { + if (item.children) { + return { + ...item, + children: updateJsonPageId(pagesMap, item.children), + }; + } + return { + ...item, + pageId: pagesMap[item.title], + }; + }); +}; + +function PagesEditor() { + const theme = useTheme(); + const dispatch = useDispatch(); + const history = useHistory(); + const applicationId = useSelector(getCurrentApplicationId) as string; + const pageId = useSelector(getCurrentPageId); + const appName = useSelector(getCurrentApplication)?.name; + const currentLayout = useSelector(getCurrentApplication)?.viewerLayout; + const pages = useSelector(getVisiblePageList); + const appPrimaryColor = useSelector(getSelectedAppThemeProperties)?.colors + .primaryColor; + + const initState = useMemo(() => { + let init = { + logoUrl: "", + name: "", + color: appPrimaryColor, + treeData: pages.map((p) => ({ + title: p.pageName, + pageId: p.pageId, + isPage: true, + })), + outsiderTree: [], + }; + if (currentLayout) { + try { + const pagesMap = pages.reduce((a: any, p: any) => { + a[p.pageName] = p.pageId; + return a; + }, {}); + const current = JSON.parse(currentLayout); + init = { + ...current, + name: appName?.startsWith(current.name) ? appName : current.name, + treeData: updateJsonPageId(pagesMap, current.treeData), + outsiderTree: updateJsonPageId(pagesMap, current.outsiderTree), + }; + } catch (e) { + console.log(e); + } + } + return init; + }, [currentLayout]); + + const [logoUrl, setLogoUrl] = useState(initState.logoUrl); + const [name, setName] = useState(initState.name || appName); + const [color, setColor] = useState(initState.color); + const [treeData, setTreeData] = useState(initState.treeData); + const [outsiderTree, setOutsiderTree] = useState(initState.outsiderTree); + const [form] = Form.useForm(); + + useEffect(() => { + const pagesMap = pages.reduce((a: any, c: any) => { + a[c.pageId] = { ...c }; + return a; + }, {}); + // update menu tree + const newMenuTree: any = []; + const newOuterTree: any = []; + treeData.forEach(updateMenuTree(pagesMap, newMenuTree)); + outsiderTree.forEach(updateMenuTree(pagesMap, newOuterTree)); + const newPages = Object.values(pagesMap) + .filter((p: any) => !p.visited) + .map((p: any) => ({ + title: p.pageName, + pageId: p.pageId, + isPage: true, + })); + setTreeData(newMenuTree.concat(newPages)); + // update outsider pages + setOutsiderTree(newOuterTree); + }, [pages]); + + const onClose = useCallback(() => { + history.push(builderURL({ pageId })); + }, [pageId]); + + const getNodeKey = ({ treeIndex }: any) => treeIndex; + + const removeNode = (path: any) => () => { + moveToOutsider(path); + setTreeData( + removeNodeAtPath({ + treeData, + path, + getNodeKey, + }), + ); + }; + + const moveToOutsider = (path: any) => { + const targetNode = getNodeAtPath({ + treeData, + path, + getNodeKey, + ignoreCollapsed: false, + }); + const removedPages: any[] = []; + walk({ + treeData: [targetNode.node], + getNodeKey, + ignoreCollapsed: false, + callback: (nodeInfo: any) => { + if (nodeInfo.node.isPage) { + removedPages.push(nodeInfo); + } + }, + }); + const outer = removedPages.map((p) => p.node).concat(outsiderTree); + setOutsiderTree(outer); + }; + + const onOutsiderTreeChanged = (tree: any[]) => { + const menus: any[] = []; + const pages: any[] = []; + tree.forEach((t: any) => { + if (t.isPage) { + pages.push(t); + } else { + menus.push(t); + } + }); + if (menus.length) { + menus.forEach((menu: any) => getPagesInTree(pages)(menu)); + } + setOutsiderTree(pages); + }; + + const addNodeAt = (node: any, path: any) => () => { + setTreeData( + addNodeUnderParent({ + treeData, + parentKey: path[path.length - 1], + expandParent: true, + getNodeKey, + newNode: { + title: "二级菜单", + }, + }).treeData, + ); + }; + + const addRootNode = () => { + setTreeData( + treeData.concat({ + title: "一级菜单", + }), + ); + }; + + const editNodeTitle = (node: any, path: any) => (event: any) => { + const title = event.target.value; + setTreeData( + changeNodeAtPath({ + treeData, + path, + getNodeKey, + newNode: { ...node, title }, + }), + ); + }; + + const onIconSelected = (node: any, path: any) => (icon?: string) => { + setTreeData( + changeNodeAtPath({ + treeData, + path, + getNodeKey, + newNode: { ...node, icon }, + }), + ); + }; + + const saveConfig = async () => { + const data = { + name: name, + viewerLayout: JSON.stringify({ + color, + logoUrl, + name, + treeData, + outsiderTree, + }), + }; + dispatch(updateApplication(applicationId, data)); + message.success("保存成功"); + }; + + // console.log(treeData, "treeData"); + + const renderTitle = (node: any, path: any) => { + const iconContent = + path.length === 1 ? ( + + ) : null; + const titleContent = node.isPage ? ( + node.title + ) : ( + + ); + return ( + <> + {iconContent} + {titleContent} + + ); + }; + + return ( + +
+
+ +

应用菜单编辑

+
+
+ + +
+ + setName(e.target.value)} /> + + + setLogoUrl(e.target.value)} + /> + + + setColor(c)} + color={color} + showApplicationColors + showThemeColors + /> + +
+ + +

{name}

+
+
+ +
+ +
+

+ 菜单导航 + +

+ + setTreeData(treeData)} + canNodeHaveChildren={(node: any) => !node.isPage} + generateNodeProps={({ node, path }: any) => ({ + title: renderTitle(node, path), + buttons: [ + node.isPage || path.length >= MAX_DEPTH - 1 ? null : ( + + ), + , + ], + listIndex: 0, + lowerSiblingCounts: [], + })} + /> + +
+
+

菜单隐藏页面

+ + !node.isPage} + shouldCopyOnOutsideDrop={false} + dndType={EXTERNAL_NODE_TYPE} + theme={FileExplorerTheme} + rowHeight={64} + maxDepth={1} + /> + +
+
+
+ + +
+ ); +} + +export default PagesEditor; diff --git a/app/client/src/pages/Editor/ViewerLayoutEditor/mock.ts b/app/client/src/pages/Editor/ViewerLayoutEditor/mock.ts new file mode 100644 index 0000000000..a975a661da --- /dev/null +++ b/app/client/src/pages/Editor/ViewerLayoutEditor/mock.ts @@ -0,0 +1,97 @@ +export const menutree = [ + { + title: "文件夹9999", + expanded: true, + children: [ + { + title: "文件夹4444", + expanded: true, + children: [ + { + title: "文件夹55555", + }, + ], + }, + ], + }, + { + title: "文件夹6666", + expanded: true, + children: [ + { + title: "Page1", + pageId: "64a7bb0a6fbfd93d5cd4a44b", + isPage: true, + expanded: true, + icon: "arrow-left", + }, + { + title: "page2", + }, + ], + }, +]; + +export const mockpages = [ + { + pageId: "6478856ade5d5825c7aee5a0", + pageName: "Page1", + slug: "page1", + isDefault: true, + }, + { + id: "64c225a16fbfd93d5cd4e7ed", + name: "hhhhhhhh ", + slug: "hhhhhhhh", + isDefault: false, + isHidden: false, + }, + { + id: "64c245136fbfd93d5cd4e8c9", + name: "页面1", + slug: "1", + isDefault: false, + }, + { + id: "64c245176fbfd93d5cd4e8cc", + name: "页面2", + slug: "2", + isDefault: false, + }, +]; + +export const navdata = { + color: "var(--ads-color-brand)", + logoUrl: "", + name: "应用 3", + treeData: [ + { + title: "Page1", + pageId: "6478856ade5d5825c7aee5a0", + isPage: true, + }, + { + title: "一级菜单", + expanded: true, + children: [ + { + title: "页面1", + pageId: "64c245136fbfd93d5cd4e8c9", + isPage: true, + }, + ], + }, + ], + outsiderTree: [ + { + title: "页面2", + pageId: "64c245176fbfd93d5cd4e8cc", + isPage: true, + }, + { + title: "hhhhhhhh ", + pageId: "64c225a16fbfd93d5cd4e7ed", + isPage: true, + }, + ], +}; diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index 8fdaa6364c..20d5e0a251 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -37,7 +37,18 @@ import { getAppMode, getCurrentApplication, } from "@appsmith/selectors/applicationSelectors"; -import { get, isArray, isString, set, find, isNil, flatten } from "lodash"; +import { + get, + isArray, + isString, + set, + find, + isNil, + flatten, + isArrayBuffer, + isEmpty, + unset, +} from "lodash"; import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE, PLATFORM_ERROR } from "entities/AppsmithConsole"; import { @@ -122,11 +133,19 @@ import { setDefaultActionDisplayFormat } from "./PluginActionSagaUtils"; import { checkAndLogErrorsIfCyclicDependency } from "sagas/helper"; import type { TRunDescription } from "workers/Evaluation/fns/actionFns"; import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/helpers"; +import { FILE_SIZE_LIMIT_FOR_BLOBS } from "constants/WidgetConstants"; enum ActionResponseDataTypes { BINARY = "BINARY", } +type FilePickerInstumentationObject = { + numberOfFiles: number; + totalSize: number; + fileTypes: Array; + fileSizes: Array; +}; + export const getActionTimeout = ( state: AppState, actionId: string, @@ -188,7 +207,16 @@ function* readBlob(blobUrl: string): any { if (fileType === FileDataTypes.Base64) { reader.readAsDataURL(file); } else if (fileType === FileDataTypes.Binary) { - reader.readAsBinaryString(file); + if (file.size < FILE_SIZE_LIMIT_FOR_BLOBS) { + //check size of the file, if less than 5mb, go with binary string method + // TODO: this method is deprecated, use readAsText instead + reader.readAsBinaryString(file); + } else { + // For files greater than 5 mb, use array buffer method + // This is to remove the bloat from the file which is added + // when using read as binary string method + reader.readAsArrayBuffer(file); + } } else { reader.readAsText(file); } @@ -221,7 +249,9 @@ function* resolvingBlobUrls( //If array elements then dont push datatypes to payload. isArray ? arrDatatype?.push(dataType) - : (executeActionRequest.paramProperties[`k${index}`] = dataType); + : (executeActionRequest.paramProperties[`k${index}`] = { + datatype: dataType, + }); if (isTrueObject(value)) { const blobUrlPaths: string[] = []; @@ -235,6 +265,17 @@ function* resolvingBlobUrls( const blobUrl = value[blobUrlPath] as string; const resolvedBlobValue: unknown = yield call(readBlob, blobUrl); set(value, blobUrlPath, resolvedBlobValue); + + // We need to store the url path map to be able to update the blob data + // and send the info to server + + // Here we fetch the blobUrlPathMap from the action payload and update it + const blobUrlPathMap = get(value, "blobUrlPaths", {}) as Record< + string, + string + >; + set(blobUrlPathMap, blobUrlPath, blobUrl); + set(value, "blobUrlPaths", blobUrlPathMap); } } else if (isBlobUrl(value)) { // @ts-expect-error: Values can take many types @@ -244,6 +285,28 @@ function* resolvingBlobUrls( return value; } +// Function that updates the blob data in the action payload for large file +// uploads +function updateBlobDataFromUrls( + blobUrlPaths: Record, + newVal: any, + blobMap: string[], + blobDataMap: Record, +) { + Object.entries(blobUrlPaths as Record).forEach( + // blobUrl: string eg: blob:1234-1234-1234?type=binary + ([path, blobUrl]) => { + if (isArrayBuffer(newVal[path])) { + // remove the ?type=binary from the blob url if present + const sanitisedBlobURL = blobUrl.split("?")[0]; + blobMap.push(sanitisedBlobURL); + set(blobDataMap, sanitisedBlobURL, new Blob([newVal[path]])); + set(newVal, path, sanitisedBlobURL); + } + }, + ); +} + /** * Api1 * URL: https://example.com/{{Text1.text}} @@ -277,6 +340,7 @@ function* evaluateActionParams( bindings: string[] | undefined, formData: FormData, executeActionRequest: ExecuteActionRequest, + filePickerInstrumentation: FilePickerInstumentationObject, executionParams?: Record | string, ) { if (isNil(bindings) || bindings.length === 0) { @@ -290,17 +354,32 @@ function* evaluateActionParams( const bindingsMap: Record = {}; const bindingBlob = []; + // Maintain a blob data map to resolve blob urls of large files as array buffer + const blobDataMap: Record = {}; + + let recordFilePickerInstrumentation = false; + + // if json bindings have filepicker reference, we need to init the instrumentation object + // which we will send post execution + recordFilePickerInstrumentation = bindings.some((binding) => + binding.includes(".files"), + ); + // Add keys values to formData for the multipart submission for (let i = 0; i < bindings.length; i++) { const key = bindings[i]; let value = values[i]; + let useBlobMaps = false; + // Maintain a blob map to resolv e blob urls of large files + const blobMap: Array = []; + if (isArray(value)) { const tempArr = []; - const arrDatatype: string[] = []; + const arrDatatype: Array = []; // array of objects containing blob urls that is loops and individual object is checked for resolution of blob urls. for (const val of value) { - const newVal: unknown = yield call( + const newVal: Record = yield call( resolvingBlobUrls, val, executeActionRequest, @@ -308,30 +387,83 @@ function* evaluateActionParams( true, arrDatatype, ); + + if (newVal.hasOwnProperty("blobUrlPaths")) { + updateBlobDataFromUrls( + newVal.blobUrlPaths, + newVal, + blobMap, + blobDataMap, + ); + useBlobMaps = true; + unset(newVal, "blobUrlPaths"); + } + tempArr.push(newVal); + + if (key.includes(".files") && recordFilePickerInstrumentation) { + filePickerInstrumentation["numberOfFiles"] += 1; + const { size, type } = newVal; + filePickerInstrumentation["totalSize"] += size; + filePickerInstrumentation["fileSizes"].push(size); + filePickerInstrumentation["fileTypes"].push(type); + } } //Adding array datatype along with the datatype of first element of the array executeActionRequest.paramProperties[`k${i}`] = { - array: [arrDatatype[0]], + datatype: { array: [arrDatatype[0]] }, }; value = tempArr; } else { // @ts-expect-error: Values can take many types value = yield call(resolvingBlobUrls, value, executeActionRequest, i); + if (key.includes(".files") && recordFilePickerInstrumentation) { + filePickerInstrumentation["numberOfFiles"] += 1; + filePickerInstrumentation["totalSize"] += value.size; + filePickerInstrumentation["fileSizes"].push(value.size); + filePickerInstrumentation["fileTypes"].push(value.type); + } } if (typeof value === "object") { + // This is used in cases of large files, we store the bloburls with the path they were set in + // This helps in creating a unique map of blob urls to blob data when passing to the server + if (!!value && value.hasOwnProperty("blobUrlPaths")) { + updateBlobDataFromUrls(value.blobUrlPaths, value, blobMap, blobDataMap); + unset(value, "blobUrlPaths"); + } + value = JSON.stringify(value); } - value = new Blob([value], { type: "text/plain" }); + // If there are no blob urls in the value, we can directly add it to the formData + // If there are blob urls, we need to add them to the blobDataMap + if (!useBlobMaps) { + value = new Blob([value], { type: "text/plain" }); + } bindingsMap[key] = `k${i}`; bindingBlob.push({ name: `k${i}`, value: value }); + + // We need to add the blob map to the param properties + // This will allow the server to handle the scenaio of large files upload using blob data + const paramProperties = executeActionRequest.paramProperties[`k${i}`]; + if (!!paramProperties && typeof paramProperties === "object") { + paramProperties["blobIdentifiers"] = blobMap; + } } formData.append("executeActionDTO", JSON.stringify(executeActionRequest)); formData.append("parameterMap", JSON.stringify(bindingsMap)); bindingBlob?.forEach((item) => formData.append(item.name, item.value)); + + // Append blob data map to formData if not empty + if (!isEmpty(blobDataMap)) { + // blobDataMap is used to resolve blob urls of large files as array buffer + // we need to add each blob data to formData as a separate entry + Object.entries(blobDataMap).forEach(([path, blobData]) => + formData.append(path, blobData), + ); + } } export default function* executePluginActionTriggerSaga( @@ -706,16 +838,12 @@ function* runActionSaga( }, ]); - Toaster.show({ - text: createMessage(ERROR_ACTION_EXECUTE_FAIL, actionObject.name), - variant: Variant.danger, - }); - yield put({ type: ReduxActionErrorTypes.RUN_ACTION_ERROR, payload: { error: appsmithConsoleErrorMessageList[0].message, id: reduxAction.payload.id, + show: false, }, }); return; @@ -802,6 +930,7 @@ function* executeOnPageLoadJSAction(pageAction: PageAction) { collectionName: collection.name, action: jsAction, collectionId: collectionId, + isExecuteJSFunc: true, }; yield call(handleExecuteJSFunctionSaga, data); } @@ -1036,11 +1165,21 @@ function* executePluginActionSaga( const formData = new FormData(); + // Initialising instrumentation object, will only be populated in case + // files are being uplaoded + const filePickerInstrumentation: FilePickerInstumentationObject = { + numberOfFiles: 0, + totalSize: 0, + fileTypes: [], + fileSizes: [], + }; + yield call( evaluateActionParams, pluginAction.jsonPathKeys, formData, executeActionRequest, + filePickerInstrumentation, params, ); @@ -1075,12 +1214,35 @@ function* executePluginActionSaga( } catch (e) { log.error("plugin no found", e); } + + const isError = isErrorResponse(response); + if (filePickerInstrumentation.numberOfFiles > 0) { + triggerFileUploadInstrumentation( + filePickerInstrumentation, + isError ? "ERROR" : "SUCCESS", + response.data.statusCode, + pluginAction.name, + pluginAction.pluginType, + response.clientMeta.duration, + ); + } return { payload, - isError: isErrorResponse(response), + isError, }; } catch (e) { if ("clientDefinedError" in (e as any)) { + // Case: error from client side validation + if (filePickerInstrumentation.numberOfFiles > 0) { + triggerFileUploadInstrumentation( + filePickerInstrumentation, + "ERROR", + "400", + pluginAction.name, + pluginAction.pluginType, + "NA", + ); + } throw e; } @@ -1091,13 +1253,60 @@ function* executePluginActionSaga( }), ); if (e instanceof UserCancelledActionExecutionError) { + // Case: user cancelled the request of file upload + if (filePickerInstrumentation.numberOfFiles > 0) { + triggerFileUploadInstrumentation( + filePickerInstrumentation, + "CANCELLED", + "499", + pluginAction.name, + pluginAction.pluginType, + "NA", + ); + } throw new UserCancelledActionExecutionError(); } + // In case there is no response from server and files are being uploaded + // we report it as INVALID_RESPONSE. The server didn't send any code or the + // request was cancelled due to timeout + if (filePickerInstrumentation.numberOfFiles > 0) { + triggerFileUploadInstrumentation( + filePickerInstrumentation, + "INVALID_RESPONSE", + "444", + pluginAction.name, + pluginAction.pluginType, + "NA", + ); + } throw new PluginActionExecutionError("Response not valid", false); } } +// Function to send the file upload event to segment +function triggerFileUploadInstrumentation( + filePickerInfo: Record, + status: string, + statusCode: string, + pluginName: string, + pluginType: string, + timeTaken: string, +) { + const { fileSizes, fileTypes, numberOfFiles, totalSize } = filePickerInfo; + AnalyticsUtil.logEvent("FILE_UPLOAD_COMPLETE", { + totalSize, + fileSizes, + numberOfFiles, + fileTypes, + status, + statusCode, + pluginName, + pluginType, + timeTaken, + }); +} + //Open debugger with response tab selected. function* openDebugger() { yield put(showDebugger(true)); diff --git a/app/client/src/utils/WidgetFactoryHelpers.ts b/app/client/src/utils/WidgetFactoryHelpers.ts index 880654bcd7..977535f411 100644 --- a/app/client/src/utils/WidgetFactoryHelpers.ts +++ b/app/client/src/utils/WidgetFactoryHelpers.ts @@ -180,7 +180,9 @@ export function enhancePropertyPaneConfig( const sectionName = (config[sectionIndex] as PropertyPaneSectionConfig) ?.sectionName; if (!sectionName || sectionName !== "General") { - log.error(`Invalid section index for feature: ${registeredFeature}`); + if (registeredFeature !== "dynamicHeight") { + log.error(`Invalid section index for feature: ${registeredFeature}`); + } } if ( Array.isArray(config[sectionIndex].children) && diff --git a/app/client/src/utils/treeUtils.ts b/app/client/src/utils/treeUtils.ts index a84ccd8dc0..3f894080fd 100644 --- a/app/client/src/utils/treeUtils.ts +++ b/app/client/src/utils/treeUtils.ts @@ -33,3 +33,41 @@ export const sortObjectWithArray = (data: Record>) => { }); return data; }; + +export function generateUuid() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +export function processTreeData(treeArray: any) { + function processChildren(children: Tree) { + return children.map((child: any) => { + return { + ...child, + key: child.pageId || child.key || generateUuid(), + children: processChildren(child.children || []), + }; + }); + } + + return processChildren(treeArray); +} + +export function removeNodeByKey(data: any, key?: string) { + for (let i = 0; i < data.length; i++) { + const node = data[i]; + if (node.key === key) { + data.splice(i, 1); + return true; + } + if (node.children && node.children.length > 0) { + if (removeNodeByKey(node.children, key)) { + return true; + } + } + } + return false; +} diff --git a/app/client/tsconfig.json b/app/client/tsconfig.json index f8ccc01e0f..09cc2cc651 100644 --- a/app/client/tsconfig.json +++ b/app/client/tsconfig.json @@ -27,6 +27,7 @@ "baseUrl": "./src", "noFallthroughCasesInSwitch": true, "importsNotUsedAsValues": "remove", + "declaration": false, }, "include": [ "./src/**/*", diff --git a/app/server/.env b/app/server/.env index 8a8b5b6514..90ae7d219c 100644 --- a/app/server/.env +++ b/app/server/.env @@ -36,9 +36,9 @@ APPSMITH_CODEC_SIZE=10 #APPSMITH_RECAPTCHA_SECRET_KEY="" APPSMITH_ENVFILE_PATH=../.env -APPSMITH_BMAP_AK=nWCpSjRnXLfGuBc3iLZ9kYv8Y6wYaxf8 +APPSMITH_BMAP_AK= -PAGEPLUG_LICENSE_KEY=UwrTr/3kX6/3Zi7Z1Z37GmDKXkWa+vOCSVMobGuLy5Bf4txDk8DS7L3aidzC6PuiZuhVRBjBhUlLDq/5g1ip6GyDWRPTKf+iAUmUwrKuPy5ufAkv7idv/es7JIjFwRQOyWL0wjxG4ukrMQjARem55xkgx4Q/TAulWKot0z+5SzLGY8lq7pWGafTy3kmLjQTXwwSyIgW+/4w3bsSVwUPcwTJXTmibGNJVnr5ghGOKi35hqFN8pDuMNjE4qZJvqFoMDQK72eeMdB5AHdkgLjrcrVtUnPTbd/loiW5r3kHpVfyjipnAifagKAIjqZnTC8Z9zoC/ms7+unYZsN0nedY/eQ== +PAGEPLUG_LICENSE_KEY= #APPSMITH_OAUTH2_OIDC_CLIENT_ID= #APPSMITH_OAUTH2_OIDC_CLIENT_SECRET=