fix: procomponent & nav edit panel

This commit is contained in:
dengll 2023-07-28 10:31:15 +08:00
parent c5ad2c7fc8
commit 8541a90a38
19 changed files with 1258 additions and 396 deletions

View File

@ -4,6 +4,5 @@
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"arrowParens": "always"
"trailingComma": "all"
}

View File

@ -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 = {

View File

@ -1,5 +1,5 @@
export const getAssetUrl = (src = "") => {
if (src === "/oracle.svg") {
if (src.toLowerCase() === "/oracle.svg") {
return "/logo/Oracle.svg";
}
return src;

View File

@ -484,7 +484,7 @@ function ApiResponseView(props: Props) {
const tabs = [
{
key: "response",
title: "返回结果",
title: "Response",
panelComponent: (
<ResponseTabWrapper>
{Array.isArray(messages) && messages.length > 0 && (
@ -572,53 +572,12 @@ function ApiResponseView(props: Props) {
)}
</ResponseDataContainer>
)}
<ResponseDataContainer>
{isEmpty(response.statusCode) ? (
<NoResponseContainer>
<Icon name="no-response" />
<Text type={TextType.P1}>
{EMPTY_RESPONSE_FIRST_HALF()}
<InlineButton
disabled={disabled}
isLoading={isRunning}
onClick={onRunClick}
size={Size.medium}
tag="button"
text="运行"
type="button"
/>
{EMPTY_RESPONSE_LAST_HALF()}
</Text>
</NoResponseContainer>
) : (
<ResponseBodyContainer>
{isString(response?.body) && isHtml(response?.body) ? (
<ReadOnlyEditor
folding
height={"100%"}
input={{
value: response?.body,
}}
/>
) : responseTabs &&
responseTabs.length > 0 &&
selectedTabIndex !== -1 ? (
<EntityBottomTabs
onSelect={onResponseTabSelect}
responseViewer
selectedTabKey={responseDisplayFormat.value}
tabs={responseTabs}
/>
) : null}
</ResponseBodyContainer>
)}
</ResponseDataContainer>
</ResponseTabWrapper>
),
},
{
key: "headers",
title: "请求头",
title: "Headers",
panelComponent: (
<ResponseTabWrapper>
{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"
/>
</div>
@ -730,7 +689,7 @@ function ApiResponseView(props: Props) {
<ResponseMetaWrapper>
{response.statusCode && (
<Flex>
<Text type={TextType.P3}>: </Text>
<Text type={TextType.P3}>Status: </Text>
<StatusCodeText
accent="secondary"
className="t--response-status-code"
@ -743,13 +702,13 @@ function ApiResponseView(props: Props) {
<ResponseMetaInfo>
{response.duration && (
<Flex>
<Text type={TextType.P3}>: </Text>
<Text type={TextType.P3}>Time: </Text>
<Text type={TextType.H5}>{response.duration} ms</Text>
</Flex>
)}
{response.size && (
<Flex>
<Text type={TextType.P3}>: </Text>
<Text type={TextType.P3}>Size: </Text>
<Text type={TextType.H5}>
{formatBytes(parseInt(response.size))}
</Text>
@ -757,9 +716,11 @@ function ApiResponseView(props: Props) {
)}
{!isEmpty(response?.body) && Array.isArray(response?.body) && (
<Flex>
<Text type={TextType.P3}>: </Text>
<Text type={TextType.P3}>Result: </Text>
<Text type={TextType.H5}>
{`${response.body.length} 条记录`}
{`${response?.body.length} Record${
response?.body.length > 1 ? "s" : ""
}`}
</Text>
</Flex>
)}

View File

@ -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",

View File

@ -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],

View File

@ -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 <div>{children}</div>;
}
function CollapseToggle(props: { isOpen: boolean }) {
const { isOpen } = props;
return (
<StyledIcon
className={isOpen ? "open-collapse" : ""}
icon={IconNames.CHEVRON_LEFT}
/>
);
return <div className="mobile-viewLayout">{children}</div>;
}
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 ? (
// <CollapseToggle isOpen={true} />
// ) : (
// <CollapseToggle isOpen={false} />
// ),
// name: collapsed ? "" : "收缩",
// onTitleClick: () => setCollapsed(!collapsed),
// },
// ...(menuData || []),
// ];
// }}
menuItemRender={(item: any, dom: any) => (
<a
onClick={() => {
history.push(item.path);
}}
>
{dom}
</a>
)}
menuItemRender={(item: any, dom: any) => {
return (
<a
onClick={() => {
history.push(item.path);
}}
>
{dom}
</a>
);
}}
route={{
routes: initState.treeData,
}}

View File

@ -1,4 +1,3 @@
/* eslint-disable prettier/prettier */
import React, { useState, useEffect, useRef, useMemo } from "react";
import type {
ApplicationPayload,

View File

@ -207,7 +207,7 @@ function AppViewer(props: Props) {
document.body.style.fontFamily = "inherit";
};
}, [selectedTheme.properties.fontFamily.appFont]);
console.log("appviewer", isMobile, "isMobile");
return (
<ThemeProvider theme={lightTheme}>
<EditorContextProvider renderMode="PAGE">
@ -219,10 +219,15 @@ function AppViewer(props: Props) {
description={pageDescription}
name={currentApplicationDetails?.name}
/>
<AppViewerLayout>
<StableContainer>
<ContainerForBottom isMobile={isMobile}>
<AppViewerBodyContainer
<AppViewerLayout className="AppViewerLayout">
<StableContainer className="StableContainer">
<ContainerForBottom
isMobile={isMobile}
className="ContainerForBottom"
>
eeeeeeeee
{/* <AppViewerBodyContainer
className="AppViewerBodyContainer"
backgroundColor={
isMobile
? "radial-gradient(#27b7b733, #ffec8f36)"
@ -240,7 +245,7 @@ function AppViewer(props: Props) {
>
{isInitialized && registered && <AppViewerPageContainer />}
</AppViewerBody>
</AppViewerBodyContainer>
</AppViewerBodyContainer> */}
</ContainerForBottom>
<TabBar />
<PreviewQRCode />

View File

@ -170,7 +170,7 @@ function GeneratePageSubmitBtn({
isLoading={isLoading}
onClick={() => !disabled && onSubmit()}
size={Size.large}
text="Generate Page"
text="生成新页面"
type="button"
/>
) : null;

View File

@ -192,7 +192,7 @@ class DatasourceHomeScreen extends React.Component<Props> {
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;

View File

@ -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<any>(initState.treeData);
const [outsiderTree, setOutsiderTree] = useState<any>(initState.outsiderTree);
const [form] = Form.useForm();
const [gData, setGData] = useState(defaultData);
const [hideNodes, setHideNodes] = useState<any>([]);
const [, setSymbol] = useState<any>();
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 ? (
<IconSelect
iconName={node.icon || ""}
onIconSelected={onIconSelected(node, path)}
/>
) : null;
const titleContent = node.isPage ? (
node.title
) : (
<NameInput value={node.title} onChange={editNodeTitle(node, path)} />
);
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() {
<h1></h1>
</div>
</Header>
<div className="px-[10%] mt-2 ">
<div>
<div className="text-xl mb-4 bold"></div>
<NavPreview color={color}>
<img src={logoUrl.trim() || DEFAULT_VIEWER_LOGO} />
<h2>{name}</h2>
</NavPreview>
<ConfigContainer>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 12 }}>
<Form.Item label="应用名称">
<Input value={name} onChange={(e) => setName(e.target.value)} />
</Form.Item>
<Form.Item label="Logo地址">
<Input
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
/>
</Form.Item>
<Form.Item label="导航栏颜色">
<ColorPicker
changeColor={(c: string) => setColor(c)}
color={color}
showApplicationColors
showThemeColors
/>
</Form.Item>
</Form>
</ConfigContainer>
</div>
<Divider type="horizontal"></Divider>
<div data-no-touch-simulate>
<div className="text-xl mb-4 bold flex justify-between">
<Button onClick={addRootNode} type="default">
+
</Button>
</div>
<TreeContainer>
<Tree
defaultExpandAll
className="draggable-tree"
// defaultExpandedKeys={expandedKeys}
draggable={{
icon: false,
}}
allowDrop={({ dropNode }) => !dropNode.isPage}
blockNode
onDragEnter={onDragEnter}
onDrop={onDrop}
treeData={gData}
showLine={true}
showIcon={false}
titleRender={(node: any) => {
return (
<div
className={`px-4 py-2 border border-teal-500 ${
!hideNodes.includes(node.key)
? "bg-neutral-50"
: "bg-gray-200"
} rounded flex gap-2 justify-between items-center`}
>
<div className="flex">
<div className="flex items-center mr-4">
{node.isPage ? (
<Icon
className="icon"
color="#4B4848"
data-testid="pages-collapse-icon"
icon="document"
size={12}
/>
) : (
<Icon
className="icon"
color="#4B4848"
data-testid="fold-collapse-icon"
icon="folder-close"
size={12}
/>
)}
</div>
<div>
{node.isPage ? (
node.title
) : (
<Paragraph
editable={{
onChange: (value: string) =>
nodeNameChange(value, node),
}}
style={{ marginBottom: 0 }}
>
{node.title}
</Paragraph>
)}
</div>
</div>
<ConfigContainer>
<Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 12 }}>
<Form.Item label="应用名称">
<Input value={name} onChange={(e) => setName(e.target.value)} />
</Form.Item>
<Form.Item label="Logo地址">
<Input
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
/>
</Form.Item>
<Form.Item label="导航栏颜色">
<ColorPicker
changeColor={(c: string) => setColor(c)}
color={color}
showApplicationColors
showThemeColors
/>
</Form.Item>
</Form>
<NavPreview color={color}>
<img src={logoUrl.trim() || DEFAULT_VIEWER_LOGO} />
<h2>{name}</h2>
</NavPreview>
</ConfigContainer>
<div data-no-touch-simulate>
<MenuContainer className="pageplug-rst">
<div>
<h2>
<Button onClick={addRootNode} type="primary">
</Button>
</h2>
<TreeContainer>
<SortableTree
treeData={treeData}
// theme={FileExplorerTheme}
rowHeight={64}
maxDepth={MAX_DEPTH}
shouldCopyOnOutsideDrop={false}
dndType={EXTERNAL_NODE_TYPE}
onChange={(treeData: any) => setTreeData(treeData)}
canNodeHaveChildren={(node: any) => !node.isPage}
generateNodeProps={({ node, path }: any) => ({
title: renderTitle(node, path),
buttons: [
node.isPage || path.length >= MAX_DEPTH - 1 ? null : (
<AddIcon
key="add"
width={16}
height={16}
color="#999"
style={{ marginTop: 6, marginRight: 5 }}
onClick={addNodeAt(node, path)}
<div className="flex items-center gap-4">
{!node.isPage && !size(node.children) ? (
<Icon
className="icon"
color="#4B4848"
data-testid="fold-collapse-icon"
icon="trash"
size={12}
onClick={() => onDeleteMenu(node)}
/>
) : null}
<Icon
className="icon"
color={hideNodes.includes(node.key) ? "red" : "#4B4848"}
data-testid="fold-collapse-icon"
icon={
hideNodes.includes(node.key) ? "eye-off" : "eye-open"
}
size={12}
onClick={() => toggleHidePage(node)}
/>
),
<DeleteIcon
key="remove"
width={16}
height={16}
color="#999"
style={{ marginTop: 6 }}
onClick={removeNode(path)}
/>,
],
listIndex: 0,
lowerSiblingCounts: [],
})}
/>
</TreeContainer>
</div>
<div>
<h2></h2>
<TreeContainer>
<SortableTree
treeData={outsiderTree}
onChange={onOutsiderTreeChanged}
canNodeHaveChildren={(node: any) => !node.isPage}
shouldCopyOnOutsideDrop={false}
dndType={EXTERNAL_NODE_TYPE}
theme={FileExplorerTheme}
rowHeight={64}
maxDepth={1}
/>
</TreeContainer>
</div>
</MenuContainer>
</div>
</div>
);
}}
/>
</TreeContainer>
</div>
<Divider type="horizontal"></Divider>
<div className="flex flex-row-reverse p-1">
<Button type="primary" size="large" onClick={saveConfig}>
</Button>
</div>
</div>
<Button
type="primary"
size="large"
onClick={saveConfig}
style={{ margin: "20px 36px" }}
>
</Button>
</Wrapper>
);
}

View File

@ -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<any>(initState.treeData);
const [outsiderTree, setOutsiderTree] = useState<any>(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 ? (
<IconSelect
iconName={node.icon || ""}
onIconSelected={onIconSelected(node, path)}
/>
) : null;
const titleContent = node.isPage ? (
node.title
) : (
<NameInput value={node.title} onChange={editNodeTitle(node, path)} />
);
return (
<>
{iconContent}
{titleContent}
</>
);
};
return (
<Wrapper>
<Header>
<div>
<CloseIcon
color={get(theme, "colors.text.heading")}
width={20}
height={20}
onClick={onClose}
/>
<h1></h1>
</div>
</Header>
<ConfigContainer>
<Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 12 }}>
<Form.Item label="应用名称">
<Input value={name} onChange={(e) => setName(e.target.value)} />
</Form.Item>
<Form.Item label="Logo地址">
<Input
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
/>
</Form.Item>
<Form.Item label="导航栏颜色">
<ColorPicker
changeColor={(c: string) => setColor(c)}
color={color}
showApplicationColors
showThemeColors
/>
</Form.Item>
</Form>
<NavPreview color={color}>
<img src={logoUrl.trim() || DEFAULT_VIEWER_LOGO} />
<h2>{name}</h2>
</NavPreview>
</ConfigContainer>
<div data-no-touch-simulate>
<MenuContainer className="pageplug-rst">
<div>
<h2>
<Button onClick={addRootNode} type="primary">
</Button>
</h2>
<TreeContainer>
<SortableTree
treeData={treeData}
// theme={FileExplorerTheme}
rowHeight={64}
maxDepth={MAX_DEPTH}
shouldCopyOnOutsideDrop={false}
dndType={EXTERNAL_NODE_TYPE}
onChange={(treeData: any) => setTreeData(treeData)}
canNodeHaveChildren={(node: any) => !node.isPage}
generateNodeProps={({ node, path }: any) => ({
title: renderTitle(node, path),
buttons: [
node.isPage || path.length >= MAX_DEPTH - 1 ? null : (
<AddIcon
key="add"
width={16}
height={16}
color="#999"
style={{ marginTop: 6, marginRight: 5 }}
onClick={addNodeAt(node, path)}
/>
),
<DeleteIcon
key="remove"
width={16}
height={16}
color="#999"
style={{ marginTop: 6 }}
onClick={removeNode(path)}
/>,
],
listIndex: 0,
lowerSiblingCounts: [],
})}
/>
</TreeContainer>
</div>
<div>
<h2></h2>
<TreeContainer>
<SortableTree
treeData={outsiderTree}
onChange={onOutsiderTreeChanged}
canNodeHaveChildren={(node: any) => !node.isPage}
shouldCopyOnOutsideDrop={false}
dndType={EXTERNAL_NODE_TYPE}
theme={FileExplorerTheme}
rowHeight={64}
maxDepth={1}
/>
</TreeContainer>
</div>
</MenuContainer>
</div>
<Button
type="primary"
size="large"
onClick={saveConfig}
style={{ margin: "20px 36px" }}
>
</Button>
</Wrapper>
);
}
export default PagesEditor;

View File

@ -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,
},
],
};

View File

@ -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<string>;
fileSizes: Array<number>;
};
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<string, string>,
newVal: any,
blobMap: string[],
blobDataMap: Record<string, Blob>,
) {
Object.entries(blobUrlPaths as Record<string, string>).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, any> | string,
) {
if (isNil(bindings) || bindings.length === 0) {
@ -290,17 +354,32 @@ function* evaluateActionParams(
const bindingsMap: Record<string, string> = {};
const bindingBlob = [];
// Maintain a blob data map to resolve blob urls of large files as array buffer
const blobDataMap: Record<string, Blob> = {};
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<string> = [];
if (isArray(value)) {
const tempArr = [];
const arrDatatype: string[] = [];
const arrDatatype: Array<string> = [];
// 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<string, any> = 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<string, any>,
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));

View File

@ -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) &&

View File

@ -33,3 +33,41 @@ export const sortObjectWithArray = (data: Record<string, Array<string>>) => {
});
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;
}

View File

@ -27,6 +27,7 @@
"baseUrl": "./src",
"noFallthroughCasesInSwitch": true,
"importsNotUsedAsValues": "remove",
"declaration": false,
},
"include": [
"./src/**/*",

View File

@ -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=