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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy