Skip to content

Commit cdb2a95

Browse files
authored
feat: add search params list to resource UI (#5384)
Ref #3691 Editing search params separately from url can improve experience working with external CMS or databases like baserow. Also made headers list more compact and aligned with search params. Validation is simplified, empty pairs will be just ignored without blocking user with validation errors. https://github.com/user-attachments/assets/3102d2c8-a902-4e86-8011-d71346fb5491
1 parent 2c461b4 commit cdb2a95

File tree

12 files changed

+463
-176
lines changed

12 files changed

+463
-176
lines changed

apps/builder/app/builder/features/settings-panel/curl.test.ts

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { generateCurl, parseCurl, type CurlRequest } from "./curl";
44
test("support url", () => {
55
const result = {
66
url: "https://my-url/hello-world",
7+
searchParams: [],
78
method: "get",
89
headers: [],
910
};
@@ -19,6 +20,7 @@ test("support multiline command with backslashes", () => {
1920
`)
2021
).toEqual({
2122
url: "https://my-url/hello-world",
23+
searchParams: [],
2224
method: "get",
2325
headers: [],
2426
});
@@ -27,6 +29,7 @@ test("support multiline command with backslashes", () => {
2729
test("forgive missing closed quotes", () => {
2830
expect(parseCurl(`curl "https://my-url/hello-world`)).toEqual({
2931
url: "https://my-url/hello-world",
32+
searchParams: [],
3033
method: "get",
3134
headers: [],
3235
});
@@ -43,6 +46,7 @@ test("skip when invalid", () => {
4346
test("support method with --request and -X flags", () => {
4447
const result = {
4548
url: "https://my-url/hello-world",
49+
searchParams: [],
4650
method: "post",
4751
headers: [],
4852
};
@@ -62,19 +66,41 @@ test("support --get and -G flags", () => {
6266
expect(
6367
parseCurl(`curl --get https://my-url --data limit=3 --data first=0`)
6468
).toEqual({
65-
url: "https://my-url?limit=3&first=0",
69+
url: "https://my-url/",
70+
searchParams: [
71+
{ name: "limit", value: "3" },
72+
{ name: "first", value: "0" },
73+
],
6674
method: "get",
6775
headers: [],
6876
});
6977
expect(parseCurl(`curl -G https://my-url -d limit=3 -d first=0`)).toEqual({
70-
url: "https://my-url?limit=3&first=0",
78+
url: "https://my-url/",
79+
searchParams: [
80+
{ name: "limit", value: "3" },
81+
{ name: "first", value: "0" },
82+
],
7183
method: "get",
7284
headers: [],
7385
});
7486
expect(
7587
parseCurl(`curl -G https://my-url?filter=1 -d limit=3 -d first=0`)
7688
).toEqual({
77-
url: "https://my-url?filter=1&limit=3&first=0",
89+
url: "https://my-url/",
90+
searchParams: [
91+
{ name: "filter", value: "1" },
92+
{ name: "limit", value: "3" },
93+
{ name: "first", value: "0" },
94+
],
95+
method: "get",
96+
headers: [],
97+
});
98+
expect(parseCurl(`curl -G https://my-url?filter -d limit`)).toEqual({
99+
url: "https://my-url/",
100+
searchParams: [
101+
{ name: "filter", value: "" },
102+
{ name: "limit", value: "" },
103+
],
78104
method: "get",
79105
headers: [],
80106
});
@@ -85,12 +111,14 @@ test("support headers with --header and -H flags", () => {
85111
parseCurl(`curl https://my-url/hello-world --header "name: value"`)
86112
).toEqual({
87113
url: "https://my-url/hello-world",
114+
searchParams: [],
88115
method: "get",
89116
headers: [{ name: "name", value: "value" }],
90117
});
91118
expect(parseCurl(`curl https://my-url/hello-world -H "name: value"`)).toEqual(
92119
{
93120
url: "https://my-url/hello-world",
121+
searchParams: [],
94122
method: "get",
95123
headers: [{ name: "name", value: "value" }],
96124
}
@@ -101,6 +129,7 @@ test("support headers with --header and -H flags", () => {
101129
)
102130
).toEqual({
103131
url: "https://my-url/hello-world",
132+
searchParams: [],
104133
method: "get",
105134
headers: [
106135
{ name: "name", value: "value1" },
@@ -119,7 +148,8 @@ test("default to post method and urlencoded header when data is specified", () =
119148
--data-raw param=4
120149
`)
121150
).toEqual({
122-
url: "https://my-url",
151+
url: "https://my-url/",
152+
searchParams: [],
123153
method: "post",
124154
headers: [
125155
{ name: "content-type", value: "application/x-www-form-urlencoded" },
@@ -132,7 +162,8 @@ test("encode data for get request", () => {
132162
expect(
133163
parseCurl(`curl -G https://my-url --data-urlencode param=привет`)
134164
).toEqual({
135-
url: "https://my-url?param=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82",
165+
url: "https://my-url/",
166+
searchParams: [{ name: "param", value: "привет" }],
136167
method: "get",
137168
headers: [],
138169
});
@@ -142,7 +173,8 @@ test("encode data for post request", () => {
142173
expect(
143174
parseCurl(`curl https://my-url --data-urlencode param=привет`)
144175
).toEqual({
145-
url: "https://my-url",
176+
url: "https://my-url/",
177+
searchParams: [],
146178
method: "post",
147179
headers: [
148180
{ name: "content-type", value: "application/x-www-form-urlencoded" },
@@ -160,6 +192,7 @@ test("support text body", () => {
160192
`)
161193
).toEqual({
162194
url: "https://my-url/hello-world",
195+
searchParams: [],
163196
method: "post",
164197
headers: [{ name: "content-type", value: "plain/text" }],
165198
body: `{"param":"value"}`,
@@ -170,6 +203,7 @@ test("support text body", () => {
170203
)
171204
).toEqual({
172205
url: "https://my-url/hello-world",
206+
searchParams: [],
173207
method: "post",
174208
headers: [{ name: "content-type", value: "plain/text" }],
175209
body: `{"param":"value"}`,
@@ -186,6 +220,7 @@ test("support text body with explicit method", () => {
186220
`)
187221
).toEqual({
188222
url: "https://my-url/hello-world",
223+
searchParams: [],
189224
method: "put",
190225
headers: [{ name: "content-type", value: "plain/text" }],
191226
body: `{"param":"value"}`,
@@ -199,6 +234,7 @@ test("support json body", () => {
199234
)
200235
).toEqual({
201236
url: "https://my-url/hello-world",
237+
searchParams: [],
202238
method: "post",
203239
headers: [{ name: "content-type", value: "application/json" }],
204240
body: { param: "value" },
@@ -213,12 +249,13 @@ test("generate curl with json body", () => {
213249
expect(
214250
generateCurl({
215251
url: "https://my-url.com",
252+
searchParams: [],
216253
method: "post",
217254
headers: [{ name: "content-type", value: "application/json" }],
218255
body: { param: "value" },
219256
})
220257
).toMatchInlineSnapshot(`
221-
"curl "https://my-url.com" \\
258+
"curl "https://my-url.com/" \\
222259
--request post \\
223260
--header "content-type: application/json" \\
224261
--data "{\\"param\\":\\"value\\"}""
@@ -229,12 +266,13 @@ test("generate curl with text body", () => {
229266
expect(
230267
generateCurl({
231268
url: "https://my-url.com",
269+
searchParams: [],
232270
method: "post",
233271
headers: [],
234272
body: "my data",
235273
})
236274
).toMatchInlineSnapshot(`
237-
"curl "https://my-url.com" \\
275+
"curl "https://my-url.com/" \\
238276
--request post \\
239277
--data "my data""
240278
`);
@@ -244,18 +282,38 @@ test("generate curl without body", () => {
244282
expect(
245283
generateCurl({
246284
url: "https://my-url.com",
285+
searchParams: [],
247286
method: "post",
248287
headers: [],
249288
})
250289
).toMatchInlineSnapshot(`
251-
"curl "https://my-url.com" \\
290+
"curl "https://my-url.com/" \\
252291
--request post"
253292
`);
254293
});
255294

295+
test("generate curl with search params", () => {
296+
expect(
297+
generateCurl({
298+
url: "https://my-url.com",
299+
searchParams: [
300+
{ name: "search", value: "term1" },
301+
{ name: "search", value: "term2" },
302+
{ name: "filter", value: "привет" },
303+
],
304+
method: "get",
305+
headers: [],
306+
})
307+
).toMatchInlineSnapshot(`
308+
"curl "https://my-url.com/?search=term1&search=term2&filter=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82" \\
309+
--request get"
310+
`);
311+
});
312+
256313
test("multiline graphql is idempotent", () => {
257314
const request: CurlRequest = {
258315
url: "https://eu-central-1-shared-euc1-02.cdn.hygraph.com/content/clorhpxi8qx7r01t6hfp1b5f6/master",
316+
searchParams: [],
259317
method: "post",
260318
headers: [{ name: "Content-Type", value: "application/json" }],
261319
body: {
@@ -276,7 +334,8 @@ test("multiline graphql is idempotent", () => {
276334

277335
test("support basic http authentication", () => {
278336
expect(parseCurl(`curl https://my-url.com -u "user:password"`)).toEqual({
279-
url: "https://my-url.com",
337+
url: "https://my-url.com/",
338+
searchParams: [],
280339
method: "get",
281340
headers: [
282341
{

apps/builder/app/builder/features/settings-panel/curl.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const getMethod = (value: string): ResourceRequest["method"] => {
2626

2727
export type CurlRequest = Pick<
2828
ResourceRequest,
29-
"url" | "method" | "headers" | "body"
29+
"url" | "searchParams" | "method" | "headers" | "body"
3030
>;
3131

3232
const encodeSearchParams = (data: string[]) => {
@@ -84,12 +84,18 @@ export const parseCurl = (curl: string): undefined | CurlRequest => {
8484
return;
8585
}
8686
// curl url
87-
let url = args._[1].toString();
87+
const url = new URL(args._[1].toString());
8888
const defaultMethod = args.data ? "post" : "get";
8989
const method: CurlRequest["method"] = args.get
9090
? "get"
9191
: getMethod(args.request ?? defaultMethod);
9292
let contentType: undefined | string;
93+
const searchParams: NonNullable<ResourceRequest["searchParams"]> = [];
94+
for (const [name, value] of url.searchParams) {
95+
searchParams.push({ name, value });
96+
}
97+
// remove all search params from url
98+
url.search = "";
9399
const headers: ResourceRequest["headers"] = (
94100
(args.header as string[]) ?? []
95101
).map((header) => {
@@ -105,9 +111,10 @@ export const parseCurl = (curl: string): undefined | CurlRequest => {
105111
}
106112
let body: undefined | unknown;
107113
if (args.get && args.data) {
108-
const separator = url.includes("?") ? "&" : "?";
109-
const search = encodeSearchParams(args.data);
110-
url = `${url}${separator}${search}`;
114+
for (const pair of args.data) {
115+
const [name, value = ""] = pair.split("=");
116+
searchParams.push({ name, value });
117+
}
111118
} else if (args.data) {
112119
body = args.data[0];
113120
if (contentType === "application/json") {
@@ -126,18 +133,20 @@ export const parseCurl = (curl: string): undefined | CurlRequest => {
126133
}
127134
}
128135
return {
129-
url: url as string,
136+
url: url.toString(),
137+
searchParams,
130138
method,
131139
headers,
132140
body,
133141
};
134142
};
135143

136144
export const generateCurl = (request: CurlRequest) => {
137-
const args = [
138-
`curl ${JSON.stringify(request.url)}`,
139-
`--request ${request.method}`,
140-
];
145+
const url = new URL(request.url);
146+
for (const { name, value } of request.searchParams) {
147+
url.searchParams.append(name, value);
148+
}
149+
const args = [`curl ${JSON.stringify(url)}`, `--request ${request.method}`];
141150
for (const header of request.headers) {
142151
args.push(`--header "${header.name}: ${header.value}"`);
143152
}

0 commit comments

Comments
 (0)