fix: procomponent & nav edit panel
This commit is contained in:
parent
c5ad2c7fc8
commit
8541a90a38
|
|
@ -4,6 +4,5 @@
|
|||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "always"
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const getAssetUrl = (src = "") => {
|
||||
if (src === "/oracle.svg") {
|
||||
if (src.toLowerCase() === "/oracle.svg") {
|
||||
return "/logo/Oracle.svg";
|
||||
}
|
||||
return src;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable prettier/prettier */
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import type {
|
||||
ApplicationPayload,
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ function GeneratePageSubmitBtn({
|
|||
isLoading={isLoading}
|
||||
onClick={() => !disabled && onSubmit()}
|
||||
size={Size.large}
|
||||
text="Generate Page"
|
||||
text="生成新页面"
|
||||
type="button"
|
||||
/>
|
||||
) : null;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"baseUrl": "./src",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"importsNotUsedAsValues": "remove",
|
||||
"declaration": false,
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
Loading…
Reference in New Issue