diff --git a/app/client/cypress/e2e/Regression/ClientSide/Debugger/Query_pane_navigation.ts b/app/client/cypress/e2e/Regression/ClientSide/Debugger/Query_pane_navigation.ts
new file mode 100644
index 0000000000..d2fddc1e9a
--- /dev/null
+++ b/app/client/cypress/e2e/Regression/ClientSide/Debugger/Query_pane_navigation.ts
@@ -0,0 +1,83 @@
+///
+
+import {
+ agHelper,
+ homePage,
+ dataSources,
+ entityExplorer,
+ entityItems,
+ debuggerHelper,
+} from "../../../../support/Objects/ObjectsCore";
+
+describe("excludeForAirgap", "Query pane navigation", () => {
+ let ds1Name: string;
+ let ds2Name: string;
+
+ before("Add dsl and create datasource from the", () => {
+ agHelper.GenerateUUID();
+ cy.get("@guid").then((uid) => {
+ homePage.CreateNewWorkspace("Workspace" + uid, true);
+ homePage.CreateAppInWorkspace("Workspace" + uid, "App" + uid);
+ });
+ dataSources.CreateDataSource("S3", true, false);
+ cy.get("@dsName").then(($dsName) => {
+ ds1Name = $dsName as unknown as string;
+ });
+ dataSources.CreateDataSource("Firestore", true, false);
+ cy.get("@dsName").then(($dsName) => {
+ ds2Name = $dsName as unknown as string;
+ });
+ });
+
+ it("1. Switching between S3 query and firestore query from the debugger", () => {
+ entityExplorer.CreateNewDsQuery(ds1Name);
+ agHelper.EnterValue("{{test}}", {
+ propFieldName:
+ ".t--actionConfiguration\\.formData\\.list\\.sortBy\\.data\\[0\\]\\.column",
+ directInput: true,
+ inputFieldName: "",
+ });
+ agHelper.UpdateCodeInput(
+ ".t--actionConfiguration\\.formData\\.bucket\\.data",
+ "test",
+ );
+ debuggerHelper.AssertErrorCount(1);
+
+ cy.get("@dsName").then(($dsName) => {
+ ds2Name = $dsName as unknown as string;
+ });
+ entityExplorer.CreateNewDsQuery(ds2Name);
+ agHelper.UpdateCodeInput(
+ ".t--actionConfiguration\\.formData\\.limitDocuments\\.data",
+ "{{test}}",
+ );
+ agHelper.UpdateCodeInput(
+ ".t--actionConfiguration\\.formData\\.path\\.data",
+ "test",
+ );
+ debuggerHelper.AssertErrorCount(2);
+
+ debuggerHelper.ClickDebuggerIcon();
+ debuggerHelper.ClicklogEntityLink();
+ agHelper.AssertElementVisibility(
+ ".t--actionConfiguration\\.formData\\.limitDocuments\\.data",
+ );
+
+ debuggerHelper.ClicklogEntityLink(true);
+ agHelper.AssertElementVisibility(
+ ".t--actionConfiguration\\.formData\\.list\\.sortBy\\.data\\[0\\]\\.column",
+ );
+
+ entityExplorer.ActionContextMenuByEntityName({
+ entityNameinLeftSidebar: "Query1",
+ entityType: entityItems.Query,
+ });
+ entityExplorer.ActionContextMenuByEntityName({
+ entityNameinLeftSidebar: "Query2",
+ entityType: entityItems.Query,
+ });
+
+ dataSources.DeleteDSFromEntityExplorer(ds1Name);
+ dataSources.DeleteDSFromEntityExplorer(ds2Name);
+ });
+});
diff --git a/app/client/cypress/support/Pages/AggregateHelper.ts b/app/client/cypress/support/Pages/AggregateHelper.ts
index eb8de841ad..9412efebbb 100644
--- a/app/client/cypress/support/Pages/AggregateHelper.ts
+++ b/app/client/cypress/support/Pages/AggregateHelper.ts
@@ -1395,6 +1395,17 @@ export class AggregateHelper extends ReusableHelper {
//return this.ScrollIntoView(selector, index, timeout).should("be.visible");//to find out why this is failing.
}
+ public AssertElementNotVisible(
+ selector: ElementType,
+ index = 0,
+ timeout = 20000,
+ ) {
+ return this.GetElement(selector, timeout)
+ .eq(index)
+ .scrollIntoView()
+ .should("not.be.visible");
+ }
+
public CheckForErrorToast(error: string) {
cy.get("body").then(($ele) => {
if ($ele.find(this.locator._toastMsg).length) {
diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx
index ea3d34b087..0dfc392dea 100644
--- a/app/client/src/ce/constants/ReduxActionConstants.tsx
+++ b/app/client/src/ce/constants/ReduxActionConstants.tsx
@@ -852,6 +852,7 @@ const ActionTypes = {
FETCH_PRODUCT_ALERT_INIT: "FETCH_PRODUCT_ALERT_INIT",
FETCH_PRODUCT_ALERT_SUCCESS: "FETCH_PRODUCT_ALERT_SUCCESS",
UPDATE_PRODUCT_ALERT_CONFIG: "UPDATE_PRODUCT_ALERT_CONFIG",
+ FORM_EVALUATION_EMPTY_BUFFER: "FORM_EVALUATION_EMPTY_BUFFER",
};
export const ReduxActionTypes = {
diff --git a/app/client/src/pages/Editor/EntityNavigation/ActionPane/ActionPaneNavigation.ts b/app/client/src/pages/Editor/EntityNavigation/ActionPane/ActionPaneNavigation.ts
index a2c92f68d3..1fe543eac5 100644
--- a/app/client/src/pages/Editor/EntityNavigation/ActionPane/ActionPaneNavigation.ts
+++ b/app/client/src/pages/Editor/EntityNavigation/ActionPane/ActionPaneNavigation.ts
@@ -5,13 +5,14 @@ import {
getPlugin,
getSettingConfig,
} from "selectors/entitiesSelector";
-import { call, delay, select } from "redux-saga/effects";
+import { call, delay, put, select } from "redux-saga/effects";
import PaneNavigation from "../PaneNavigation";
import type { Plugin } from "api/PluginApi";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
import history from "utils/history";
import { NAVIGATION_DELAY } from "../costants";
+import { setFocusableInputField } from "actions/editorContextActions";
export default class ActionPaneNavigation extends PaneNavigation {
action!: Action;
@@ -61,6 +62,8 @@ export default class ActionPaneNavigation extends PaneNavigation {
if (!url) return;
history.push(url);
yield delay(NAVIGATION_DELAY);
+ // Reset context switching field for the id, to allow scrolling to the error field
+ yield put(setFocusableInputField(id));
}
*scrollToView(propertyPath: string) {
diff --git a/app/client/src/pages/Editor/EntityNavigation/ActionPane/QueryPaneNavigation.ts b/app/client/src/pages/Editor/EntityNavigation/ActionPane/QueryPaneNavigation.ts
new file mode 100644
index 0000000000..5b66f8ac87
--- /dev/null
+++ b/app/client/src/pages/Editor/EntityNavigation/ActionPane/QueryPaneNavigation.ts
@@ -0,0 +1,83 @@
+import { call, delay, put, race, select, take } from "redux-saga/effects";
+import type { EntityInfo, IQueryPaneNavigationConfig } from "../types";
+import { ActionPaneNavigation } from "./exports";
+import { NAVIGATION_DELAY } from "../costants";
+import { setQueryPaneConfigSelectedTabIndex } from "actions/queryPaneActions";
+import { EDITOR_TABS } from "constants/QueryEditorConstants";
+import { getFormEvaluationState } from "selectors/formSelectors";
+import type { FormEvaluationState } from "reducers/evaluationReducers/formEvaluationReducer";
+import { isEmpty } from "lodash";
+import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
+import { isActionSaving } from "selectors/entitiesSelector";
+
+export default class QueryPaneNavigation extends ActionPaneNavigation {
+ constructor(entityInfo: EntityInfo) {
+ super(entityInfo);
+ this.getConfig = this.getConfig.bind(this);
+ this.navigate = this.navigate.bind(this);
+
+ this.getTab = this.getTab.bind(this);
+ this.waitForFormUpdate = this.waitForFormUpdate.bind(this);
+ }
+
+ *getConfig() {
+ let config: IQueryPaneNavigationConfig = {
+ tab: EDITOR_TABS.QUERY,
+ };
+ if (!this.entityInfo.propertyPath) return {};
+ const tab: string = yield call(this.getTab, this.entityInfo.propertyPath);
+
+ config = {
+ tab,
+ };
+ return config;
+ }
+
+ *navigate() {
+ const config: IQueryPaneNavigationConfig = yield call(this.getConfig);
+
+ yield call(this.navigateToUrl);
+ if (!this.entityInfo.propertyPath) return;
+
+ if (config.tab) {
+ yield put(setQueryPaneConfigSelectedTabIndex(config.tab));
+ }
+
+ yield call(this.waitForFormUpdate);
+ yield call(this.scrollToView, this.entityInfo.propertyPath);
+ }
+
+ *waitForFormUpdate() {
+ const formEvaluationState: FormEvaluationState = yield select(
+ getFormEvaluationState,
+ );
+ const isSaving: boolean = yield select(isActionSaving(this.action.id));
+ if (isEmpty(formEvaluationState[this.action.id]) || isSaving) {
+ // Wait till the form fields are computed
+ yield take(ReduxActionTypes.FORM_EVALUATION_EMPTY_BUFFER);
+ yield delay(NAVIGATION_DELAY);
+ } else {
+ yield race({
+ evaluation: take(ReduxActionTypes.FORM_EVALUATION_EMPTY_BUFFER),
+ timeout: delay(NAVIGATION_DELAY),
+ });
+ }
+ }
+
+ *getTab(propertyPath: string) {
+ let tab = EDITOR_TABS.QUERY;
+ const modifiedProperty = propertyPath.replace(
+ "config",
+ "actionConfiguration",
+ );
+ const inSettingsTab: boolean = yield call(
+ this.isInSettingsTab,
+ modifiedProperty,
+ );
+ if (inSettingsTab) {
+ tab = EDITOR_TABS.SETTINGS;
+ }
+
+ return tab;
+ }
+}
diff --git a/app/client/src/pages/Editor/EntityNavigation/ActionPane/exports.ts b/app/client/src/pages/Editor/EntityNavigation/ActionPane/exports.ts
index b3bdbde5b1..a6b6e31454 100644
--- a/app/client/src/pages/Editor/EntityNavigation/ActionPane/exports.ts
+++ b/app/client/src/pages/Editor/EntityNavigation/ActionPane/exports.ts
@@ -1,2 +1,3 @@
export { default as ActionPaneNavigation } from "./ActionPaneNavigation";
export { default as ApiPaneNavigation } from "./ApiPaneNavigation";
+export { default as QueryPaneNavigation } from "./QueryPaneNavigation";
diff --git a/app/client/src/pages/Editor/EntityNavigation/ActionPane/index.ts b/app/client/src/pages/Editor/EntityNavigation/ActionPane/index.ts
index 5ac9006f14..19b5dc7a2b 100644
--- a/app/client/src/pages/Editor/EntityNavigation/ActionPane/index.ts
+++ b/app/client/src/pages/Editor/EntityNavigation/ActionPane/index.ts
@@ -2,16 +2,23 @@ import { PluginType, type Action } from "entities/Action";
import type { EntityInfo } from "../types";
import { getAction } from "selectors/entitiesSelector";
import { select } from "redux-saga/effects";
-import { ActionPaneNavigation, ApiPaneNavigation } from "./exports";
+import {
+ ActionPaneNavigation,
+ ApiPaneNavigation,
+ QueryPaneNavigation,
+} from "./exports";
export default class ActionPaneNavigationFactory {
static *create(entityInfo: EntityInfo) {
const action: Action | undefined = yield select(getAction, entityInfo.id);
-
if (!action) throw Error(`Couldn't find action with id: ${entityInfo.id}`);
switch (action.pluginType) {
case PluginType.API:
return new ApiPaneNavigation(entityInfo);
+ case PluginType.DB:
+ case PluginType.SAAS:
+ case PluginType.REMOTE:
+ return new QueryPaneNavigation(entityInfo);
default:
return new ActionPaneNavigation(entityInfo);
}
diff --git a/app/client/src/pages/Editor/EntityNavigation/types.ts b/app/client/src/pages/Editor/EntityNavigation/types.ts
index fe6e7ed3a2..ed0c258cf0 100644
--- a/app/client/src/pages/Editor/EntityNavigation/types.ts
+++ b/app/client/src/pages/Editor/EntityNavigation/types.ts
@@ -30,3 +30,7 @@ export interface IMatchedSection {
export interface IApiPaneNavigationConfig {
tabIndex?: number;
}
+
+export interface IQueryPaneNavigationConfig {
+ tab: string;
+}
diff --git a/app/client/src/sagas/FormEvaluationSaga.ts b/app/client/src/sagas/FormEvaluationSaga.ts
index f8fe063d8f..5476bbded7 100644
--- a/app/client/src/sagas/FormEvaluationSaga.ts
+++ b/app/client/src/sagas/FormEvaluationSaga.ts
@@ -28,6 +28,7 @@ import {
extractQueueOfValuesToBeFetched,
} from "./helper";
import type { DatasourceConfiguration } from "entities/Datasource";
+import { buffers } from "redux-saga";
export type FormEvalActionPayload = {
formId: string;
@@ -260,9 +261,15 @@ function* fetchDynamicValueSaga(
}
function* formEvaluationChangeListenerSaga() {
+ const buffer = buffers.fixed();
const formEvalChannel: ActionPattern> =
- yield actionChannel(FORM_EVALUATION_REDUX_ACTIONS);
+ yield actionChannel(FORM_EVALUATION_REDUX_ACTIONS, buffer as any);
while (true) {
+ if (buffer.isEmpty()) {
+ yield put({
+ type: ReduxActionTypes.FORM_EVALUATION_EMPTY_BUFFER,
+ });
+ }
const action: ReduxAction = yield take(
formEvalChannel,
);