Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 27ea453

Browse files
Merge pull request #1803 from iamfaran/feat/1799-curl-import
[Feat]: Add Import from cURL in Query Library
2 parents 4278242 + 12b4b41 commit 27ea453

File tree

7 files changed

+304
-16
lines changed

7 files changed

+304
-16
lines changed

‎client/packages/lowcoder/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"types": "src/index.sdk.ts",
88
"dependencies": {
99
"@ant-design/icons": "^5.3.0",
10+
"@bany/curl-to-json": "^1.2.8",
1011
"@codemirror/autocomplete": "^6.11.1",
1112
"@codemirror/commands": "^6.3.2",
1213
"@codemirror/lang-css": "^6.2.1",
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, { useState } from "react";
2+
import { Modal, Input, Button, message } from "antd";
3+
import { trans } from "i18n";
4+
import parseCurl from "@bany/curl-to-json";
5+
const { TextArea } = Input;
6+
interface CurlImportModalProps {
7+
open: boolean;
8+
onCancel: () => void;
9+
onSuccess: (parsedData: any) => void;
10+
}
11+
12+
export function CurlImportModal(props: CurlImportModalProps) {
13+
const { open, onCancel, onSuccess } = props;
14+
const [curlCommand, setCurlCommand] = useState("");
15+
const [loading, setLoading] = useState(false);
16+
17+
const handleImport = async () => {
18+
if (!curlCommand.trim()) {
19+
message.error("Please enter a cURL command");
20+
return;
21+
}
22+
23+
setLoading(true);
24+
try {
25+
// Parse the cURL command using the correct import
26+
const parsedData = parseCurl(curlCommand);
27+
28+
29+
30+
// Log the result for now as requested
31+
// console.log("Parsed cURL data:", parsedData);
32+
33+
// Call success callback with parsed data
34+
onSuccess(parsedData);
35+
36+
// Reset form and close modal
37+
setCurlCommand("");
38+
onCancel();
39+
40+
message.success("cURL command imported successfully!");
41+
} catch (error: any) {
42+
console.error("Error parsing cURL command:", error);
43+
message.error(`Failed to parse cURL command: ${error.message}`);
44+
} finally {
45+
setLoading(false);
46+
}
47+
};
48+
49+
const handleCancel = () => {
50+
setCurlCommand("");
51+
onCancel();
52+
};
53+
54+
return (
55+
<Modal
56+
title="Import from cURL"
57+
open={open}
58+
onCancel={handleCancel}
59+
footer={[
60+
<Button key="cancel" onClick={handleCancel}>
61+
Cancel
62+
</Button>,
63+
<Button key="import" type="primary" loading={loading} onClick={handleImport}>
64+
Import
65+
</Button>,
66+
]}
67+
width={600}
68+
>
69+
<div style={{ marginBottom: 16 }}>
70+
<div style={{ marginBottom: 8, fontWeight: 500 }}>
71+
Paste cURL Command Here
72+
</div>
73+
<div style={{ marginBottom: 12, color: "#666", fontSize: "12px" }}>
74+
<div style={{ marginBottom: 4 }}>
75+
<strong>Examples:</strong>
76+
</div>
77+
<div style={{ marginBottom: 2 }}>
78+
GET: <code>curl -X GET https://jsonplaceholder.typicode.com/posts/1</code>
79+
</div>
80+
<div style={{ marginBottom: 2 }}>
81+
POST: <code>curl -X POST https://jsonplaceholder.typicode.com/posts -H "Content-Type: application/json" -d '&#123;"title":"foo","body":"bar","userId":1&#125;'</code>
82+
</div>
83+
<div>
84+
Users: <code>curl -X GET https://jsonplaceholder.typicode.com/users</code>
85+
</div>
86+
</div>
87+
<TextArea
88+
value={curlCommand}
89+
onChange={(e) => setCurlCommand(e.target.value)}
90+
placeholder="curl -X GET https://jsonplaceholder.typicode.com/posts/1"
91+
rows={8}
92+
style={{ fontFamily: "monospace" }}
93+
/>
94+
</div>
95+
</Modal>
96+
);
97+
}

‎client/packages/lowcoder/src/components/ResCreatePanel.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { getUser } from "../redux/selectors/usersSelectors";
2626
import DataSourceIcon from "./DataSourceIcon";
2727
import { genRandomKey } from "comps/utils/idGenerator";
2828
import { isPublicApplication } from "@lowcoder-ee/redux/selectors/applicationSelector";
29+
import { CurlImportModal } from "./CurlImport";
2930

3031
const Wrapper = styled.div<{ $placement: PageType }>`
3132
width: 100%;
@@ -230,6 +231,7 @@ export function ResCreatePanel(props: ResCreateModalProps) {
230231
const { onSelect, onClose, recentlyUsed, datasource, placement = "editor" } = props;
231232
const [isScrolling, setScrolling] = useState(false);
232233
const [visible, setVisible] = useState(false);
234+
const [curlModalVisible, setCurlModalVisible] = useState(false);
233235

234236
const isPublicApp = useSelector(isPublicApplication);
235237
const user = useSelector(getUser);
@@ -244,6 +246,14 @@ export function ResCreatePanel(props: ResCreateModalProps) {
244246
setScrolling(top > 0);
245247
}, 100);
246248

249+
const handleCurlImportSuccess = (parsedData: any) => {
250+
onSelect(BottomResTypeEnum.Query, {
251+
compType: "restApi",
252+
dataSourceId: QUICK_REST_API_ID,
253+
curlData: parsedData
254+
});
255+
};
256+
247257
return (
248258
<Wrapper $placement={placement}>
249259
<Title $shadow={isScrolling} $placement={placement}>
@@ -331,6 +341,10 @@ export function ResCreatePanel(props: ResCreateModalProps) {
331341
<ResButton size={buttonSize} identifier={"streamApi"} onSelect={onSelect} />
332342
<ResButton size={buttonSize} identifier={"alasql"} onSelect={onSelect} />
333343
<ResButton size={buttonSize} identifier={"graphql"} onSelect={onSelect} />
344+
<DataSourceButton size={buttonSize} onClick={() => setCurlModalVisible(true)}>
345+
<DataSourceIcon size="large" dataSourceType="restApi" />
346+
Import from cURL
347+
</DataSourceButton>
334348
</DataSourceListWrapper>
335349
</div>
336350

@@ -374,6 +388,11 @@ export function ResCreatePanel(props: ResCreateModalProps) {
374388
onCancel={() => setVisible(false)}
375389
onCreated={() => setVisible(false)}
376390
/>
391+
<CurlImportModal
392+
open={curlModalVisible}
393+
onCancel={() => setCurlModalVisible(false)}
394+
onSuccess={handleCurlImportSuccess}
395+
/>
377396
</Wrapper>
378397
);
379398
}

‎client/packages/lowcoder/src/comps/queries/queryComp.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
import { QueryContext } from "../../util/context/QueryContext";
6565
import { useFixedDelay } from "../../util/hooks";
6666
import { JSONObject, JSONValue } from "../../util/jsonTypes";
67+
import { processCurlData } from "../../util/curlUtils";
6768
import { BoolPureControl } from "../controls/boolControl";
6869
import { millisecondsControl } from "../controls/millisecondControl";
6970
import { paramsMillisecondsControl } from "../controls/paramsControl";
@@ -743,18 +744,42 @@ class QueryListComp extends QueryListTmpComp implements BottomResListComp {
743744
const name = this.genNewName(editorState);
744745
const compType = extraInfo?.compType || "js";
745746
const dataSourceId = extraInfo?.dataSourceId;
747+
const curlData = extraInfo?.curlData;
748+
console.log("CURL DATA", curlData);
749+
750+
// Build the basic payload
751+
let payload: any = {
752+
id: id,
753+
name: name,
754+
datasourceId: dataSourceId,
755+
compType,
756+
triggerType: manualTriggerResource.includes(compType) ? "manual" : "automatic",
757+
isNewCreate: true,
758+
order: Date.now(),
759+
};
760+
761+
// If this is a REST API created from cURL, pre-populate the HTTP query fields
762+
if (compType === "restApi" && curlData) {
763+
const curlConfig = processCurlData(curlData);
764+
if (curlConfig) {
765+
payload = {
766+
...payload,
767+
comp: {
768+
httpMethod: curlConfig.method,
769+
path: curlConfig.url,
770+
headers: curlConfig.headers,
771+
params: curlConfig.params,
772+
bodyType: curlConfig.bodyType,
773+
body: curlConfig.body,
774+
bodyFormData: curlConfig.bodyFormData,
775+
},
776+
};
777+
}
778+
}
746779

747780
this.dispatch(
748781
wrapActionExtraInfo(
749-
this.pushAction({
750-
id: id,
751-
name: name,
752-
datasourceId: dataSourceId,
753-
compType,
754-
triggerType: manualTriggerResource.includes(compType) ? "manual" : "automatic",
755-
isNewCreate: true,
756-
order: Date.now(),
757-
}),
782+
this.pushAction(payload),
758783
{
759784
compInfos: [
760785
{

‎client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"
4848
import { Helmet } from "react-helmet";
4949
import {fetchQLPaginationByOrg} from "@lowcoder-ee/util/pagination/axios";
5050
import { isEmpty } from "lodash";
51+
import { processCurlData } from "../../util/curlUtils";
5152

5253
const Wrapper = styled.div`
5354
display: flex;
@@ -199,17 +200,39 @@ export const QueryLibraryEditor = () => {
199200
const newName = nameGenerator.genItemName(trans("queryLibrary.unnamed"));
200201

201202
const handleAdd = (type: BottomResTypeEnum, extraInfo?: any) => {
203+
// Build basic query DSL
204+
let queryDSL: any = {
205+
triggerType: "manual",
206+
datasourceId: extraInfo?.dataSourceId,
207+
compType: extraInfo?.compType,
208+
};
209+
210+
// If it is a REST API created from cURL, pre-populate the HTTP query fields
211+
if (extraInfo?.compType === "restApi" && extraInfo?.curlData) {
212+
const curlConfig = processCurlData(extraInfo.curlData);
213+
if (curlConfig) {
214+
queryDSL = {
215+
...queryDSL,
216+
comp: {
217+
httpMethod: curlConfig.method,
218+
path: curlConfig.url,
219+
headers: curlConfig.headers,
220+
params: curlConfig.params,
221+
bodyType: curlConfig.bodyType,
222+
body: curlConfig.body,
223+
bodyFormData: curlConfig.bodyFormData,
224+
},
225+
};
226+
}
227+
}
228+
202229
dispatch(
203230
createQueryLibrary(
204231
{
205232
name: newName,
206233
organizationId: orgId,
207234
libraryQueryDSL: {
208-
query: {
209-
triggerType: "manual",
210-
datasourceId: extraInfo?.dataSourceId,
211-
compType: extraInfo?.compType,
212-
},
235+
query: queryDSL,
213236
},
214237
},
215238
(resp) => {
@@ -218,7 +241,6 @@ export const QueryLibraryEditor = () => {
218241
setModify(!modify);
219242
}, 200);
220243
setCurrentPage(Math.ceil(elements.total / pageSize));
221-
222244
},
223245
() => {}
224246
)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Utility to convert parsed cURL data from @bany/curl-to-json library
3+
* to the format expected by REST API query components
4+
*/
5+
6+
// Body type mapping to match the dropdown values in httpQuery.tsx
7+
const CONTENT_TYPE_TO_BODY_TYPE: Record<string, string> = {
8+
"application/json": "application/json",
9+
"text/plain": "text/plain",
10+
"text/html": "text/plain",
11+
"text/xml": "text/plain",
12+
"application/xml": "text/plain",
13+
"application/x-www-form-urlencoded": "application/x-www-form-urlencoded",
14+
"multipart/form-data": "multipart/form-data",
15+
};
16+
17+
/**
18+
* Parse URL-encoded form data - handles both string and object input
19+
*/
20+
function parseUrlEncodedData(data: string | object): Array<{ key: string; value: string; type: string }> {
21+
if (!data) {
22+
return [{ key: "", value: "", type: "text" }];
23+
}
24+
25+
try {
26+
let result: Array<{ key: string; value: string; type: string }> = [];
27+
28+
if (typeof data === 'object') {
29+
// @bany/curl-to-json already parsed it into an object
30+
Object.entries(data).forEach(([key, value]) => {
31+
result.push({
32+
key: key,
33+
value: decodeURIComponent(String(value).replace(/\+/g, ' ')), // Handle URL encoding
34+
type: "text"
35+
});
36+
});
37+
} else if (typeof data === 'string') {
38+
// Raw URL-encoded string - use URLSearchParams
39+
const params = new URLSearchParams(data);
40+
params.forEach((value, key) => {
41+
result.push({
42+
key: key,
43+
value: value,
44+
type: "text"
45+
});
46+
});
47+
}
48+
49+
return result.length > 0 ? result : [{ key: "", value: "", type: "text" }];
50+
} catch (error) {
51+
console.warn('Failed to parse URL-encoded data:', error);
52+
return [{ key: "", value: "", type: "text" }];
53+
}
54+
}
55+
56+
export function processCurlData(curlData: any) {
57+
if (!curlData) return null;
58+
59+
60+
// Convert headers object to key-value array format expected by UI
61+
const headers = curlData.header
62+
? Object.entries(curlData.header).map(([key, value]) => ({ key, value }))
63+
: [{ key: "", value: "" }];
64+
65+
// Convert query params object to key-value array format expected by UI
66+
const params = curlData.params
67+
? Object.entries(curlData.params).map(([key, value]) => ({ key, value }))
68+
: [{ key: "", value: "" }];
69+
70+
// Get request body - @bany/curl-to-json may use 'body' or 'data'
71+
const bodyContent = curlData.body !== undefined ? curlData.body : curlData.data;
72+
73+
// Determine body type based on Content-Type header or content structure
74+
let bodyType = "none";
75+
let bodyFormData = [{ key: "", value: "", type: "text" }];
76+
let processedBody = "";
77+
78+
if (bodyContent !== undefined && bodyContent !== "") {
79+
const contentTypeHeader = curlData.header?.["Content-Type"] || curlData.header?.["content-type"];
80+
81+
if (contentTypeHeader) {
82+
// Extract base content type (remove charset, boundary, etc.)
83+
const baseContentType = contentTypeHeader.split(';')[0].trim().toLowerCase();
84+
bodyType = CONTENT_TYPE_TO_BODY_TYPE[baseContentType] || "text/plain";
85+
} else {
86+
// Fallback: infer from content structure
87+
if (typeof bodyContent === "object") {
88+
bodyType = "application/json";
89+
} else {
90+
bodyType = "text/plain";
91+
}
92+
}
93+
94+
// Handle different body types
95+
if (bodyType === "application/x-www-form-urlencoded") {
96+
bodyFormData = parseUrlEncodedData(bodyContent);
97+
processedBody = ""; // Form data goes in bodyFormData, not body
98+
} else if (typeof bodyContent === "object") {
99+
processedBody = JSON.stringify(bodyContent, null, 2);
100+
} else {
101+
processedBody = bodyContent;
102+
}
103+
}
104+
105+
return {
106+
method: curlData.method || "GET",
107+
url: curlData.url || "",
108+
headers,
109+
params,
110+
bodyType,
111+
body: processedBody,
112+
bodyFormData,
113+
};
114+
}

0 commit comments

Comments
 (0)