From cd736be0d3d2302fe31a3dba4a03c7d8a43c6e6e Mon Sep 17 00:00:00 2001 From: timphamcmc <108319981+timphamcmc@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:01:18 +0700 Subject: [PATCH] Feat/step draggable (#15) * Feature: Step Draggable * feat: step bar * fix: code smell --------- Co-authored-by: Tim --- package-lock.json | 45 +++ package.json | 1 + src/assets/newAPIServer/close.svg | 9 + .../NewAPIServer/__test__/components.test.tsx | 16 ++ src/pages/NewAPIServer/components/AddEnv.tsx | 18 +- .../components/SelectAPIServer.tsx | 165 +++++------ .../components/SelectDownStreamAPI.tsx | 33 +-- src/pages/NewAPIServer/components/StepBar.tsx | 265 +++++++++++++++--- .../NewAPIServer/components/index.module.scss | 60 +++- src/pages/NewAPIServer/index.module.scss | 3 + src/pages/NewAPIServer/index.tsx | 44 ++- 11 files changed, 470 insertions(+), 189 deletions(-) create mode 100644 src/assets/newAPIServer/close.svg create mode 100644 src/pages/NewAPIServer/__test__/components.test.tsx diff --git a/package-lock.json b/package-lock.json index 37673a91..9e5208e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "qs": "^6.12.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-router-dom": "^6.23.1", "rollup-plugin-node-polyfills": "^0.2.1", "stream-browserify": "^3.0.0", @@ -4823,6 +4824,14 @@ "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", "dev": true }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -5110,6 +5119,21 @@ "node": ">= 0.6.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5772,6 +5796,27 @@ "react": "^18.3.1" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/package.json b/package.json index 2ddf3b2b..c108f55e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "qs": "^6.12.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-router-dom": "^6.23.1", "rollup-plugin-node-polyfills": "^0.2.1", "stream-browserify": "^3.0.0", diff --git a/src/assets/newAPIServer/close.svg b/src/assets/newAPIServer/close.svg new file mode 100644 index 00000000..10c38319 --- /dev/null +++ b/src/assets/newAPIServer/close.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/pages/NewAPIServer/__test__/components.test.tsx b/src/pages/NewAPIServer/__test__/components.test.tsx new file mode 100644 index 00000000..c3e726de --- /dev/null +++ b/src/pages/NewAPIServer/__test__/components.test.tsx @@ -0,0 +1,16 @@ +import { render } from "@testing-library/react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { queryClient } from "@/utils/helpers/reactQuery"; +import { BrowserRouter } from "react-router-dom"; +import StepBar from "../components/StepBar"; + +test("test API step bar", () => { + const { container } = render( + + + + + + ); + expect(container).toBeInTheDocument(); +}); diff --git a/src/pages/NewAPIServer/components/AddEnv.tsx b/src/pages/NewAPIServer/components/AddEnv.tsx index f57c926a..3806a9a0 100644 --- a/src/pages/NewAPIServer/components/AddEnv.tsx +++ b/src/pages/NewAPIServer/components/AddEnv.tsx @@ -11,12 +11,18 @@ const AddEnv = ({ form, active }: Props) => { const isStage = Form.useWatch("isStage", form); const isUat = Form.useWatch("isUat", form); return ( -
+
Add information for the API server

Console connect application

-
+
@@ -35,7 +41,7 @@ const AddEnv = ({ form, active }: Props) => { className={styles.inputUrl} rules={[{ required: isSIT, message: "Please fill the url" }]} > - + @@ -50,7 +56,7 @@ const AddEnv = ({ form, active }: Props) => { className={styles.inputUrl} rules={[{ required: isProd, message: "Please fill the url" }]} > - + @@ -65,7 +71,7 @@ const AddEnv = ({ form, active }: Props) => { className={styles.inputUrl} rules={[{ required: isStage, message: "Please fill the url" }]} > - + @@ -80,7 +86,7 @@ const AddEnv = ({ form, active }: Props) => { className={styles.inputUrl} rules={[{ required: isUat, message: "Please fill the url" }]} > - + diff --git a/src/pages/NewAPIServer/components/SelectAPIServer.tsx b/src/pages/NewAPIServer/components/SelectAPIServer.tsx index 622a2d75..ecbfd2f5 100644 --- a/src/pages/NewAPIServer/components/SelectAPIServer.tsx +++ b/src/pages/NewAPIServer/components/SelectAPIServer.tsx @@ -1,6 +1,6 @@ import Text from "@/components/Text"; import styles from "./index.module.scss"; -import { Col, Form, Row, Input, Upload, Button, FormInstance } from "antd"; +import { Form, Input, Upload, Button, FormInstance } from "antd"; import { PaperClipOutlined, UploadOutlined } from "@ant-design/icons"; import Flex from "@/components/Flex"; import { get, isEmpty } from "lodash"; @@ -31,101 +31,88 @@ const SelectAPIServer = ({ form, active }: Props) => { }; return ( -
+
Add information for the API server -
- - - + + + + + + + { + if (isURL(value) || isEmpty(value)) { + return Promise.resolve(); + } + return Promise.reject(new Error("Please enter a valid URL")); + }, + }, + ]} + labelCol={{ span: 24 }} + validateTrigger="onBlur" + > + + + + {isEmpty(file?.file) ? ( + false} > - - - - - - - - - - { - if (isURL(value) || isEmpty(value)) { - return Promise.resolve(); - } - return Promise.reject( - new Error("Please enter a valid URL") - ); - }, - }, - ]} - labelCol={{ span: 24 }} - validateTrigger="onBlur" - > - - - - - - {isEmpty(file?.file) ? ( - false} - > - - - ) : ( - - )} - - - - {file ? ( - - - - {get(file, "file.name", "")} - - - ) : null} - - + + + ) : ( + + )} + + {file ? ( + + + {get(file, "file.name", "")} + + ) : null}
); diff --git a/src/pages/NewAPIServer/components/SelectDownStreamAPI.tsx b/src/pages/NewAPIServer/components/SelectDownStreamAPI.tsx index 86b49009..e4e42dbb 100644 --- a/src/pages/NewAPIServer/components/SelectDownStreamAPI.tsx +++ b/src/pages/NewAPIServer/components/SelectDownStreamAPI.tsx @@ -9,18 +9,14 @@ import { notification, } from "antd"; import { useEffect, useState } from "react"; -import { isEmpty } from "lodash"; +import { get, isEmpty } from "lodash"; import SwaggerInfo from "./SwaggerInfo"; -import BtnStep from "./BtnStep"; import yaml from "js-yaml"; import { decode } from "js-base64"; type Props = { form: FormInstance; active: boolean; - onNext: () => void; - onPrev: () => void; - currentStep: number; }; // eslint-disable-next-line react-refresh/only-export-components @@ -43,13 +39,7 @@ export const tranformSwaggerToArray = (data: any) => { return pathsArray; }; -const SelectDownStreamAPI = ({ - form, - active, - currentStep, - onPrev, - onNext, -}: Props) => { +const SelectDownStreamAPI = ({ form, active }: Props) => { const [selectedAPI, setSelectedAPI] = useState(); const [transferData, setTransferData] = useState([]); const [targetKeys, setTargetKeys] = useState([]); @@ -66,8 +56,10 @@ const SelectDownStreamAPI = ({ }); const data = yaml.load(decode(swaggerData as any)) as any; + const newTransferData = tranformSwaggerToArray(data); setSchemas(data?.components?.schemas); - setTransferData(tranformSwaggerToArray(data)); + setTransferData(newTransferData); + setSelectedAPI(get(newTransferData, "[0]")); } catch (error) { form.setFieldValue("file", undefined); notification.error({ message: "Please select a valid swagger file" }); @@ -86,7 +78,14 @@ const SelectDownStreamAPI = ({ }, [file]); return ( -
+
-
-
+
diff --git a/src/pages/NewAPIServer/components/StepBar.tsx b/src/pages/NewAPIServer/components/StepBar.tsx index faabe9fe..6eb2ae3e 100644 --- a/src/pages/NewAPIServer/components/StepBar.tsx +++ b/src/pages/NewAPIServer/components/StepBar.tsx @@ -1,61 +1,242 @@ -import styles from "./index.module.scss"; import StepIcon from "@/assets/stepstart.svg"; -import ETIcon from "@/assets/et.svg"; -import { Avatar, Button, Divider } from "antd"; -import clsx from "clsx"; -import { DoubleLeftOutlined } from "@ant-design/icons"; import Text from "@/components/Text"; +import { + CheckCircleFilled, + CloseOutlined, + DownOutlined, + UpOutlined, +} from "@ant-design/icons"; +import { Button, Collapse, CollapseProps } from "antd"; +import clsx from "clsx"; +import { CSSProperties, useRef, useState } from "react"; +import type { DraggableData, DraggableEvent } from "react-draggable"; +import Draggable from "react-draggable"; +import styles from "./index.module.scss"; type Props = { currentStep: number; }; -const Step = ({ active = false, step = "1", content = "" }) => { +interface IStepTitle { + activeKey: string | string[]; + stepKey: string; + isFinished: boolean; + content: string; + isCurrentStep?: boolean; +} + +interface IStepIndicator { + currentStep: number; +} + +const StepTitle = ({ + activeKey, + stepKey, + isFinished, + content, + isCurrentStep, +}: IStepTitle) => { return ( -
- {step} - {content} +
+ {isFinished ? ( + + ) : ( + + )} + {content}
); }; +const StepIndicator = ({ currentStep }: IStepIndicator) => { + return ( +
+
+
+
+
+ ); +}; + +const getItems: ( + panelStyle: CSSProperties, + currentStep: number, + activeKey: string | string[] +) => CollapseProps["items"] = (panelStyle, currentStep, activeKey) => [ + { + key: "0", + label: ( + 0} + /> + ), + children: ( + + Add basic information to your API server and upload API spec, with which + the system can abstract your API list. + + ), + style: panelStyle, + }, + { + key: "1", + label: ( + 1} + /> + ), + children: ( + + Select seller APIs from seller API spec list which will be used in + Sonata API mapping, and add to the right. +
+
+ You can add multiple APIs to the right side. +
+ ), + style: panelStyle, + }, + { + key: "2", + label: ( + 2} + /> + ), + children: ( + Select the environments and add URLs. + ), + style: panelStyle, + }, +]; + const StepBar = ({ currentStep = 0 }: Props) => { + const [isOpen, setIsOpen] = useState(true); + const [isStart, setIsStart] = useState(false); + const [activeKey, setActiveKey] = useState("1"); + const [bounds, setBounds] = useState({ + left: 0, + top: 0, + bottom: 0, + right: 0, + }); + const draggleRef = useRef(null); + + const panelStyle: React.CSSProperties = { + marginBottom: 24, + borderRadius: 4, + border: "1px solid #DDE1E5", + }; + + const onStart = (_event: DraggableEvent, uiData: DraggableData) => { + const { clientWidth, clientHeight } = window.document.documentElement; + const targetRect = draggleRef.current?.getBoundingClientRect(); + if (!targetRect) { + return; + } + setBounds({ + left: -targetRect.left + uiData.x, + right: clientWidth - (targetRect.right - uiData.x), + top: -targetRect.top + uiData.y, + bottom: clientHeight - (targetRect.bottom - uiData.y), + }); + }; + + const onChange = (key: string | string[]) => { + setActiveKey(key); + }; + return ( -
-
- - - Let’s get you start! - -
- - - + onStart(event, uiData)} + > +
+ {isStart && } +
setIsOpen(false)} + role="none" + > +
+ {!isStart ? ( +
+

+ Starting with seller API setup +

+ + +
+ ) : ( +
+

Seller API setup

+ + !isActive ? : + } + expandIconPosition="end" + accordion + defaultActiveKey={["0"]} + onChange={onChange} + /> +
+ )}
-
-
- - V1.0 | A product by - - -
- -
- -
-
-
+ ); }; diff --git a/src/pages/NewAPIServer/components/index.module.scss b/src/pages/NewAPIServer/components/index.module.scss index c02d4c31..e6e174eb 100644 --- a/src/pages/NewAPIServer/components/index.module.scss +++ b/src/pages/NewAPIServer/components/index.module.scss @@ -47,17 +47,57 @@ } } -.active { - span { - color: #f632fa; - } - :global { - .ant-avatar-circle { - border: 2px solid #f632fa; - } +.draggableModal { + cursor: move; + position: absolute; + z-index: 1000; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2); + border: 1px solid #f4f4f4; + padding: 32px; + background-color: #fff; + bottom: 60px; + right: 60px; + width: 350px; +} + +.hiddenModal { + display: none; +} + +.collapseTitle { + display: flex; + flex-direction: row; + align-items: center; +} + +.closeIcon { + position: absolute; + top: 24px; + right: 24px; + cursor: pointer; + + &:hover { + opacity: 0.8; } } +.stepIndicator { + height: 10px; + width: 56px; + border-radius: 5px; + background-color: #dde1e5; + margin-right: 8px; +} + +.stepIndicatorActive { + background-color: #2962FF; +} + +.active { + font-weight: 700; + color: #595959; +} + .version { display: flex; justify-content: center; @@ -80,7 +120,7 @@ .transfer { :global { .ant-transfer-list { - height: calc(100vh - 308px); + height: calc(100vh - 244px); flex: 1; } .ant-transfer-operation { @@ -145,7 +185,7 @@ background: #fff; width: 100%; max-width: calc((100vw - 280px) * 2 / 3); - height: calc(100vh - 98px); + height: calc(100vh - 134px); padding: 10px 20px; box-sizing: border-box; > div { diff --git a/src/pages/NewAPIServer/index.module.scss b/src/pages/NewAPIServer/index.module.scss index cc57740b..1b87a426 100644 --- a/src/pages/NewAPIServer/index.module.scss +++ b/src/pages/NewAPIServer/index.module.scss @@ -1,10 +1,13 @@ .root { display: flex; background: #f0f2f5; + height: calc(100vh - 58px); } .container { padding: 20px; width: 100%; min-width: 800px; + display: flex; + flex-direction: column; } diff --git a/src/pages/NewAPIServer/index.tsx b/src/pages/NewAPIServer/index.tsx index f00bc664..abe42f9f 100644 --- a/src/pages/NewAPIServer/index.tsx +++ b/src/pages/NewAPIServer/index.tsx @@ -100,32 +100,24 @@ const NewAPIServer = () => {
- - {step !== 1 && ( - - {({ getFieldValue }) => { - const disabled = - (isEmpty(getFieldValue("name")) || - isEmpty(getFieldValue("file"))) && - step === 0; - return ( - - ); - }} - - )} + + + {({ getFieldValue }) => { + const disabled = + (isEmpty(getFieldValue("name")) || + isEmpty(getFieldValue("file"))) && + step === 0; + return ( + + ); + }} +