Skip to content

Commit 23ed87b

Browse files
fixes API editor query parameters not auto-updating URL
1 parent 65e6ab9 commit 23ed87b

File tree

4 files changed

+521
-65
lines changed

4 files changed

+521
-65
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
2+
const commonlocators = require("../../../../locators/commonlocators.json");
3+
import ApiEditor from "../../../../locators/ApiEditor";
4+
import { agHelper, entityExplorer } from "../../../../support/Objects/ObjectsCore";
5+
6+
describe("Bug 40045: API Query Parameters URL Update", function() {
7+
before(() => {
8+
// Visit the application
9+
cy.visit("/applications");
10+
cy.createWorkspace();
11+
cy.wait("@createWorkspace").then((interception) => {
12+
const newWorkspaceName = interception.response.body.data.name;
13+
cy.CreateAppForWorkspace(newWorkspaceName, newWorkspaceName);
14+
});
15+
});
16+
17+
it("1. API query parameters should auto-update the URL", function() {
18+
// Create a new API
19+
cy.get(commonlocators.AddBtn).click();
20+
cy.get("div.t--entity").contains("API").click();
21+
cy.wait("@createNewApi");
22+
23+
// Add URL with no query parameters
24+
cy.get(apiwidget.resourceUrl).clear().type("https://example.com/users");
25+
26+
// Navigate to the Params tab
27+
cy.get(ApiEditor.apiTab).contains("Params").click({ force: true });
28+
29+
// Verify the URL starts with no query params
30+
cy.get(apiwidget.resourceUrl).invoke("val").should("eq", "https://example.com/users");
31+
32+
// Enter query parameters
33+
cy.get(".t--actionConfiguration\\.queryParameters\\[0\\]\\.key")
34+
.first()
35+
.clear()
36+
.type("id");
37+
cy.get(".t--actionConfiguration\\.queryParameters\\[0\\]\\.value")
38+
.first()
39+
.clear()
40+
.type("123");
41+
42+
// Check that URL updates automatically
43+
cy.get(apiwidget.resourceUrl)
44+
.invoke("val")
45+
.should("eq", "https://example.com/users?id=123");
46+
47+
// Add another parameter
48+
cy.get(".t--add-field, .btn-add-more").first().click();
49+
cy.get(".t--actionConfiguration\\.queryParameters\\[1\\]\\.key")
50+
.clear()
51+
.type("name");
52+
cy.get(".t--actionConfiguration\\.queryParameters\\[1\\]\\.value")
53+
.clear()
54+
.type("test");
55+
56+
// Verify URL has both parameters
57+
cy.get(apiwidget.resourceUrl)
58+
.invoke("val")
59+
.should("eq", "https://example.com/users?id=123&name=test");
60+
61+
// Remove a parameter and verify URL updates
62+
cy.get(".t--delete-field").first().click();
63+
cy.get(apiwidget.resourceUrl)
64+
.invoke("val")
65+
.should("eq", "https://example.com/users?name=test");
66+
});
67+
68+
it("2. Editing a URL with query parameters should populate the params", function() {
69+
// Add URL with query parameters
70+
cy.get(apiwidget.resourceUrl)
71+
.clear()
72+
.type("https://example.com/users?id=456&status=active");
73+
74+
// Navigate to Params tab
75+
cy.get(ApiEditor.apiTab).contains("Params").click({ force: true });
76+
77+
// Verify parameters are extracted
78+
cy.get(".t--actionConfiguration\\.queryParameters\\[0\\]\\.key")
79+
.first()
80+
.invoke("val")
81+
.should("eq", "id");
82+
cy.get(".t--actionConfiguration\\.queryParameters\\[0\\]\\.value")
83+
.first()
84+
.invoke("val")
85+
.should("eq", "456");
86+
87+
cy.get(".t--actionConfiguration\\.queryParameters\\[1\\]\\.key")
88+
.invoke("val")
89+
.should("eq", "status");
90+
cy.get(".t--actionConfiguration\\.queryParameters\\[1\\]\\.value")
91+
.invoke("val")
92+
.should("eq", "active");
93+
});
94+
95+
it("3. Should persist query parameters after save and reload", function() {
96+
// Save the API
97+
cy.get(".t--apiFormSave").click();
98+
cy.wait("@saveAction");
99+
100+
// Reload the page
101+
cy.reload();
102+
cy.wait("@getActions");
103+
104+
// Verify URL still has query parameters
105+
cy.get(apiwidget.resourceUrl)
106+
.invoke("val")
107+
.should("eq", "https://example.com/users?id=456&status=active");
108+
});
109+
});

app/client/cypress/locators/ApiEditor.js

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default {
1111
ApiHomePage: ".t--apiHomePage",
1212
formActionButtons: ".t--formActionButtons",
1313
dataSourceField: ".t--dataSourceField",
14+
resourceUrl: ".t--dataSourceField",
1415
responseBody: ".CodeMirror-code span.cm-string.cm-property",
1516
ApiVerb: ".t--apiFormHttpMethod div",
1617
apiPaginationNextText: ".t--apiFormPaginationNext",

app/client/src/sagas/ApiPaneSagas.ts

+98-57
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ import { convertToBasePageIdSelector } from "selectors/pageListSelectors";
7777
import type { ApplicationPayload } from "entities/Application";
7878
import { klonaLiteWithTelemetry } from "utils/helpers";
7979

80-
function* syncApiParamsSaga(
80+
// Export for testing
81+
export function* syncApiParamsSaga(
8182
actionPayload: ReduxActionWithMeta<string, { field: string }>,
8283
actionId: string,
8384
) {
@@ -110,21 +111,108 @@ function* syncApiParamsSaga(
110111
const path = values.actionConfiguration.path || "";
111112
const matchGroups = path.match(queryParamsRegEx) || [];
112113
const currentPath = matchGroups[1] || "";
113-
const paramsString = values.actionConfiguration.queryParameters
114-
.filter((p: Property) => p.key)
114+
115+
// Get all query parameters that have a key
116+
const validParams = values.actionConfiguration.queryParameters.filter(
117+
(p: Property) => p.key,
118+
);
119+
120+
// Create the query parameters string representation
121+
const paramsString = validParams
115122
.map(
116123
(p: Property, i: number) => `${i === 0 ? "?" : "&"}${p.key}=${p.value}`,
117124
)
118125
.join("");
119126

120-
yield put(
121-
autofill(
122-
API_EDITOR_FORM_NAME,
123-
"actionConfiguration.path",
124-
`${currentPath}${paramsString}`,
125-
),
126-
);
127+
// Only update if we have a currentPath to avoid removing the whole URL
128+
if (currentPath) {
129+
yield put(
130+
autofill(
131+
API_EDITOR_FORM_NAME,
132+
"actionConfiguration.path",
133+
`${currentPath}${paramsString}`,
134+
),
135+
);
136+
137+
// Also update the action property to ensure consistency
138+
yield put(
139+
setActionProperty({
140+
actionId: actionId,
141+
propertyName: "actionConfiguration.path",
142+
value: `${currentPath}${paramsString}`,
143+
}),
144+
);
145+
}
146+
}
147+
}
148+
149+
// Export for testing
150+
export function* changeApiSaga(
151+
actionPayload: ReduxAction<{
152+
id: string;
153+
isSaas: boolean;
154+
action?: Action;
155+
}>,
156+
) {
157+
const { id, isSaas } = actionPayload.payload;
158+
let { action } = actionPayload.payload;
159+
160+
if (!action) action = yield select(getAction, id);
161+
162+
if (!action) return;
163+
164+
if (isSaas) {
165+
yield put(initialize(QUERY_EDITOR_FORM_NAME, action));
166+
} else {
167+
yield put(initialize(API_EDITOR_FORM_NAME, action));
168+
169+
if (
170+
action.actionConfiguration &&
171+
action.actionConfiguration.queryParameters?.length
172+
) {
173+
// Sync the api params by mocking a change action
174+
yield call(
175+
syncApiParamsSaga,
176+
{
177+
type: ReduxFormActionTypes.ARRAY_REMOVE,
178+
payload: action.actionConfiguration.queryParameters,
179+
meta: {
180+
field: "actionConfiguration.queryParameters",
181+
},
182+
},
183+
id,
184+
);
185+
186+
// Force a re-sync to ensure the URL is updated with query parameters
187+
const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
188+
189+
if (
190+
values &&
191+
values.actionConfiguration &&
192+
values.actionConfiguration.queryParameters?.length
193+
) {
194+
yield call(
195+
syncApiParamsSaga,
196+
{
197+
type: ReduxFormActionTypes.ARRAY_REMOVE,
198+
payload: values.actionConfiguration.queryParameters,
199+
meta: {
200+
field: "actionConfiguration.queryParameters",
201+
},
202+
},
203+
id,
204+
);
205+
}
206+
}
127207
}
208+
209+
//Retrieve form data with synced query params to start tracking change history.
210+
const { values: actionPostProcess } = yield select(
211+
getFormData,
212+
API_EDITOR_FORM_NAME,
213+
);
214+
215+
yield put(updateReplayEntity(id, actionPostProcess, ENTITY_TYPE.ACTION));
128216
}
129217

130218
function* handleUpdateBodyContentType(contentType: string) {
@@ -216,53 +304,6 @@ function* handleUpdateBodyContentType(contentType: string) {
216304
}
217305
}
218306

219-
function* changeApiSaga(
220-
actionPayload: ReduxAction<{
221-
id: string;
222-
isSaas: boolean;
223-
action?: Action;
224-
}>,
225-
) {
226-
const { id, isSaas } = actionPayload.payload;
227-
let { action } = actionPayload.payload;
228-
229-
if (!action) action = yield select(getAction, id);
230-
231-
if (!action) return;
232-
233-
if (isSaas) {
234-
yield put(initialize(QUERY_EDITOR_FORM_NAME, action));
235-
} else {
236-
yield put(initialize(API_EDITOR_FORM_NAME, action));
237-
238-
if (
239-
action.actionConfiguration &&
240-
action.actionConfiguration.queryParameters?.length
241-
) {
242-
// Sync the api params my mocking a change action
243-
yield call(
244-
syncApiParamsSaga,
245-
{
246-
type: ReduxFormActionTypes.ARRAY_REMOVE,
247-
payload: action.actionConfiguration.queryParameters,
248-
meta: {
249-
field: "actionConfiguration.queryParameters",
250-
},
251-
},
252-
id,
253-
);
254-
}
255-
}
256-
257-
//Retrieve form data with synced query params to start tracking change history.
258-
const { values: actionPostProcess } = yield select(
259-
getFormData,
260-
API_EDITOR_FORM_NAME,
261-
);
262-
263-
yield put(updateReplayEntity(id, actionPostProcess, ENTITY_TYPE.ACTION));
264-
}
265-
266307
function* setApiBodyTabHeaderFormat(apiContentType?: string) {
267308
let displayFormat;
268309

0 commit comments

Comments
 (0)