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, );