From 4a6717889ca5b9ccff2a12c2871991ccf30d35eb Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Fri, 3 Jul 2020 14:28:58 +0530 Subject: [PATCH] Streamline action save with widgets (#10) - Remove drafts from actions - Direct update action from forms - Debounced saving of actions - Add org id in default datasource - Merge query and api run saga --- .../ApiPaneTests/API_Edit_spec.js | 4 - .../ApiPaneTests/API_Mustache_spec.js | 7 +- .../Binding/Bind_TableTextPagination_spec.js | 8 +- .../Binding/Bind_tableApi_spec.js | 4 +- .../cypress/locators/apiWidgetslocator.json | 4 +- app/client/cypress/support/commands.js | 16 +- app/client/package.json | 2 +- app/client/src/actions/actionActions.ts | 46 +- app/client/src/actions/queryPaneActions.ts | 10 - .../editorComponents/CodeEditor/index.tsx | 9 +- .../fields/EmbeddedDatasourcePathField.tsx | 8 +- .../form/fields/KeyValueFieldArray.tsx | 2 +- .../src/constants/ApiEditorConstants.ts | 2 +- .../src/constants/ReduxActionConstants.tsx | 13 +- .../src/entities/DataTree/dataTreeFactory.ts | 9 +- app/client/src/entities/Datasource/index.ts | 6 +- app/client/src/jsExecution/RealmExecutor.ts | 4 +- .../src/pages/Editor/APIEditor/index.tsx | 11 +- app/client/src/pages/Editor/ApiSidebar.tsx | 21 +- app/client/src/pages/Editor/EditorSidebar.tsx | 13 - .../pages/Editor/QueryEditor/JSONViewer.tsx | 4 + .../src/pages/Editor/QueryEditor/index.tsx | 12 +- app/client/src/pages/Editor/QuerySidebar.tsx | 15 - .../entityReducers/actionDraftsReducer.ts | 25 - .../entityReducers/actionsReducer.tsx | 36 +- .../src/reducers/entityReducers/index.tsx | 2 - app/client/src/reducers/index.tsx | 2 - .../src/reducers/uiReducers/apiPaneReducer.ts | 23 +- .../reducers/uiReducers/queryPaneReducer.ts | 54 +- app/client/src/sagas/ActionExecutionSagas.ts | 454 ++++++++++++++ app/client/src/sagas/ActionSagas.ts | 556 ++---------------- app/client/src/sagas/ApiPaneSagas.ts | 191 ++---- app/client/src/sagas/BatchSagas.tsx | 4 + app/client/src/sagas/QueryPaneSagas.ts | 135 +---- app/client/src/sagas/index.tsx | 2 + app/client/src/selectors/dataTreeSelectors.ts | 6 +- app/client/src/selectors/entitiesSelector.ts | 67 ++- app/client/src/selectors/formSelectors.ts | 6 +- app/client/src/utils/DynamicBindingUtils.ts | 3 +- 39 files changed, 769 insertions(+), 1027 deletions(-) delete mode 100644 app/client/src/reducers/entityReducers/actionDraftsReducer.ts create mode 100644 app/client/src/sagas/ActionExecutionSagas.ts diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js index c18a6bdc62..4592be3e3f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js @@ -23,10 +23,6 @@ describe("API Panel Test Functionality", function() { cy.EditApiName("SecondAPI"); cy.ClearSearch(); cy.SearchAPIandClick("SecondAPI"); - //invalid api end point check - cy.EditSourceDetail(testdata.baseUrl, testdata.invalidPath); - cy.RunAPI(); - cy.ResponseStatusCheck("404 NOT_FOUND"); cy.DeleteAPI(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js index a0bce1150b..bda5c0c1a5 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js @@ -15,12 +15,7 @@ describe("Moustache test Functionality", function() { cy.log("Navigation to API Panel screen successful"); cy.CreateAPI("TestAPINew"); cy.log("Creation of API Action successful"); - cy.EnterSourceDetailsWithHeader( - testdata.baseUrl2, - testdata.moustacheMethod, - testdata.headerKey, - testdata.headerValue, - ); + cy.enterDatasourceAndPath(testdata.baseUrl2, testdata.moustacheMethod); cy.RunAPI(); cy.ResponseStatusCheck(testdata.successStatusCode); cy.log("Response code check successful"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js index c65c958612..448d90b405 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js @@ -10,10 +10,8 @@ describe("Test Create Api and Bind to Table widget", function() { }); it("Test_Add Paginate with Table Page No and Execute the Api", function() { - cy.NavigateToApiEditor(); - cy.testCreateApiButton(); /**Create an Api1 of Paginate with Table Page No */ - cy.createApi(this.data.paginationUrl, this.data.paginationParam); + cy.createAndFillApi(this.data.paginationUrl, this.data.paginationParam); cy.RunAPI(); }); @@ -56,10 +54,8 @@ describe("Test Create Api and Bind to Table widget", function() { }); it("Test_Add Paginate with Response URL and Execute the Api", function() { - cy.NavigateToApiEditor(); - cy.testCreateApiButton(); /** Create Api2 of Paginate with Response URL*/ - cy.createApi(this.data.paginationUrl, "pokemon"); + cy.createAndFillApi(this.data.paginationUrl, "pokemon"); cy.RunAPI(); cy.NavigateToPaginationTab(); cy.get(apiPage.apiPaginationNextText).type("{{Api2.data.next}}", { diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js index 6ca66548db..3aec5f4a93 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js @@ -11,9 +11,7 @@ describe("Test Create Api and Bind to Table widget", function() { }); it("Test_Add users api and execute api", function() { - cy.NavigateToApiEditor(); - cy.testCreateApiButton(); - cy.createApi(this.data.userApi, "users"); + cy.createAndFillApi(this.data.userApi, "users"); cy.RunAPI(); cy.get(apiPage.responseBody) .contains("name") diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index 0af2d3a12a..4d94ca1b2a 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -9,7 +9,7 @@ "home": ".single-select >div:contains('Page1')", "delete": ".single-select >div:contains('Delete')", "path": ".t--path >div textarea", - "editResourceUrl": ".t--dataSourceField input", + "editResourceUrl": ".t--dataSourceField", "autoSuggest": "//div[contains(@id,'react-select')]", "headerKey": "(//div[contains(@class,'t--actionConfiguration.headers[0].key.0')]//textarea)[2]", "headerValue": "(//div[contains(@class,'t--actionConfiguration.headers[0].value.0')]//textarea)[2]", @@ -35,4 +35,4 @@ "TestPreUrl": ".t--apiFormPaginationPrevTest", "EditApiName": "img[alt='Edit pen']", "ApiName": ".t--action-name-edit-field span" -} \ No newline at end of file +} diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 8c0fd35f17..702d7c6511 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -177,15 +177,14 @@ Cypress.Commands.add("CreateAPI", apiname => { .click({ force: true }); cy.get(apiwidget.createapi).click({ force: true }); cy.wait("@createNewApi"); - cy.wait("@postSave"); cy.get(apiwidget.resourceUrl).should("be.visible"); - cy.wait("@postexe"); cy.get(apiwidget.EditApiName).should("be.visible"); cy.get(apiwidget.EditApiName).click(); cy.get(apiwidget.apiTxt) .clear() .type(apiname) - .should("have.value", apiname); + .should("have.value", apiname) + .blur(); //cy.WaitAutoSave(); // Added because api name edit takes some time to // reflect in api sidebar after the call passes. @@ -216,13 +215,14 @@ Cypress.Commands.add("EditApiName", apiname => { }); Cypress.Commands.add("WaitAutoSave", () => { + // wait for save query to trigger + cy.wait(200); cy.wait("@saveQuery"); //cy.wait("@postExecute"); }); Cypress.Commands.add("RunAPI", () => { cy.get(ApiEditor.ApiRunBtn).click({ force: true }); - // cy.wait('@postTrack'); cy.wait("@postExecute"); }); @@ -258,7 +258,7 @@ Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => { .first() .click({ force: true }) .type(datasource); - /* + /* cy.xpath(apiwidget.autoSuggest) .first() .click({ force: true }); @@ -1017,7 +1017,9 @@ Cypress.Commands.add("closePropertyPane", () => { cy.get(commonlocators.editPropCrossButton).click(); }); -Cypress.Commands.add("createApi", (url, parameters) => { +Cypress.Commands.add("createAndFillApi", (url, parameters) => { + cy.NavigateToApiEditor(); + cy.testCreateApiButton(); cy.get("@createNewApi").then(response => { cy.get(ApiEditor.ApiNameField).should("be.visible"); cy.expect(response.response.body.responseMeta.success).to.eq(true); @@ -1131,10 +1133,8 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("POST", "/api/v1/applications/publish/*").as("publishApp"); cy.route("PUT", "/api/v1/layouts/*/pages/*").as("updateLayout"); - cy.route("POST", "/v1/t").as("postSave"); cy.route("PUT", "/api/v1/actions/*").as("putActions"); cy.route("POST", "/track/*").as("postTrack"); - cy.route("POST", "/v1/m").as("postexe"); cy.route("POST", "/api/v1/actions/execute").as("postExecute"); cy.route("POST", "/api/v1/actions").as("postaction"); diff --git a/app/client/package.json b/app/client/package.json index b7be55130b..c80e72ef41 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -182,4 +182,4 @@ "pre-commit": "lint-staged" } } -} \ No newline at end of file +} diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index b3cfad49b4..8ceee2b17d 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -5,6 +5,7 @@ import { ReduxActionErrorTypes, } from "constants/ReduxActionConstants"; import { Action, RestAction } from "entities/Action"; +import { batchAction } from "actions/batchActions"; export const createActionRequest = (payload: Partial) => { return { @@ -47,12 +48,12 @@ export const fetchActionsForPageSuccess = (actions: RestAction[]) => { }; }; -export const runApiAction = (id: string, paginationField?: PaginationField) => { +export const runAction = (id: string, paginationField?: PaginationField) => { return { - type: ReduxActionTypes.RUN_API_REQUEST, + type: ReduxActionTypes.RUN_ACTION_REQUEST, payload: { - id: id, - paginationField: paginationField, + id, + paginationField, }, }; }; @@ -163,24 +164,35 @@ export const saveApiName = (payload: { id: string; name: string }) => ({ payload: payload, }); -export const updateApiNameDraft = (payload: { - id: string; - draft?: { - value: string; - validation: { - isValid: boolean; - validationMessage: string; - }; - }; -}) => ({ - type: ReduxActionTypes.UPDATE_API_NAME_DRAFT, - payload: payload, +export type SetActionPropertyPayload = { + actionId: string; + propertyName: string; + value: string; +}; + +export const setActionProperty = (payload: SetActionPropertyPayload) => ({ + type: ReduxActionTypes.SET_ACTION_PROPERTY, + payload, }); +export type UpdateActionPropertyActionPayload = { + id: string; + field: string; + value: any; +}; + +export const updateActionProperty = ( + payload: UpdateActionPropertyActionPayload, +) => + batchAction({ + type: ReduxActionTypes.UPDATE_ACTION_PROPERTY, + payload, + }); + export default { createAction: createActionRequest, fetchActions, - runAction: runApiAction, + runAction: runAction, deleteAction, deleteActionSuccess, updateAction, diff --git a/app/client/src/actions/queryPaneActions.ts b/app/client/src/actions/queryPaneActions.ts index 66647af349..4fa33e5c39 100644 --- a/app/client/src/actions/queryPaneActions.ts +++ b/app/client/src/actions/queryPaneActions.ts @@ -22,16 +22,6 @@ export const deleteQuerySuccess = (payload: { id: string }) => { }; }; -export const executeQuery = (payload: { - action: RestAction; - actionId: string; -}) => { - return { - type: ReduxActionTypes.EXECUTE_QUERY_REQUEST, - payload, - }; -}; - export const initQueryPane = ( pluginType: string, urlId?: string, diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index f5403d96c0..888de82f34 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -155,7 +155,7 @@ class CodeEditor extends Component { componentDidUpdate(prevProps: Props): void { this.editor.refresh(); if (!this.state.isFocused) { - const currentMode = this.editor.getOption("mode"); + // const currentMode = this.editor.getOption("mode"); const editorValue = this.editor.getValue(); let inputValue = this.props.input.value; // Safe update of value of the editor when value updated outside the editor @@ -170,10 +170,11 @@ class CodeEditor extends Component { if ((!!inputValue || inputValue === "") && inputValue !== editorValue) { this.editor.setValue(inputValue); } + this.updateMarkings(); - if (currentMode !== this.props.mode) { - this.editor.setOption("mode", this.props?.mode); - } + // if (currentMode !== this.props.mode) { + // this.editor.setOption("mode", this.props?.mode); + // } } else { // Update the dynamic bindings for autocomplete if (prevProps.dynamicData !== this.props.dynamicData) { diff --git a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx index 4c7c541417..ad9b0b4253 100644 --- a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx +++ b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx @@ -27,6 +27,7 @@ import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers" import StoreAsDatasource from "components/editorComponents/StoreAsDatasource"; type ReduxStateProps = { + orgId: string; datasource: Datasource | EmbeddedDatasource; datasourceList: Datasource[]; apiName: string; @@ -47,13 +48,13 @@ const fullPathRegexExp = /(https?:\/{2}\S+)(\/\S*?)$/; class EmbeddedDatasourcePathComponent extends React.Component { handleDatasourceUrlUpdate = (datasourceUrl: string) => { - const { datasource, pluginId, datasourceList } = this.props; + const { datasource, pluginId, orgId, datasourceList } = this.props; const urlHasUpdated = datasourceUrl !== datasource.datasourceConfiguration?.url; if (urlHasUpdated) { if ("id" in datasource && datasource.id) { this.props.updateDatasource({ - ...DEFAULT_DATASOURCE(pluginId), + ...DEFAULT_DATASOURCE(pluginId, orgId), datasourceConfiguration: { ...datasource.datasourceConfiguration, url: datasourceUrl, @@ -68,7 +69,7 @@ class EmbeddedDatasourcePathComponent extends React.Component { this.props.updateDatasource(matchesExistingDatasource); } else { this.props.updateDatasource({ - ...DEFAULT_DATASOURCE(pluginId), + ...DEFAULT_DATASOURCE(pluginId, orgId), datasourceConfiguration: { ...datasource.datasourceConfiguration, url: datasourceUrl, @@ -249,6 +250,7 @@ const mapStateToProps = ( ownProps: { pluginId: string }, ): ReduxStateProps => { return { + orgId: state.ui.orgs.currentOrgId, apiName: apiFormValueSelector(state, "name"), datasource: apiFormValueSelector(state, "datasource"), datasourceList: state.entities.datasources.list.filter( diff --git a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx index d0c2a82578..cd84bdde1e 100644 --- a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx +++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx @@ -30,7 +30,7 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { }, [props.fields, props.pushFields]); return ( - {typeof props.fields.getAll() === "object" && ( + {props.fields.length && ( {props.fields.map((field: any, index: number) => { const otherProps: Record = {}; diff --git a/app/client/src/constants/ApiEditorConstants.ts b/app/client/src/constants/ApiEditorConstants.ts index 1d0f5082c7..c1454c08b2 100644 --- a/app/client/src/constants/ApiEditorConstants.ts +++ b/app/client/src/constants/ApiEditorConstants.ts @@ -25,7 +25,7 @@ export const DEFAULT_API_ACTION: Partial = { }, }; -export const API_CONSTANT = "API"; +export const PLUGIN_TYPE_API = "API"; export const DEFAULT_PROVIDER_OPTION = "Business Software"; export const CONTENT_TYPE = "content-type"; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 5f4cd6b752..d05f19238b 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -27,10 +27,10 @@ export const ReduxActionTypes: { [key: string]: string } = { REMOVE_PAGE_WIDGET: "REMOVE_PAGE_WIDGET", LOAD_API_RESPONSE: "LOAD_API_RESPONSE", LOAD_QUERY_RESPONSE: "LOAD_QUERY_RESPONSE", - RUN_API_REQUEST: "RUN_API_REQUEST", + RUN_ACTION_REQUEST: "RUN_ACTION_REQUEST", + RUN_ACTION_SUCCESS: "RUN_ACTION_SUCCESS", INIT_API_PANE: "INIT_API_PANE", API_PANE_CHANGE_API: "API_PANE_CHANGE_API", - RUN_API_SUCCESS: "RUN_API_SUCCESS", EXECUTE_ACTION: "EXECUTE_ACTION", EXECUTE_ACTION_SUCCESS: "EXECUTE_ACTION_SUCCESS", LOAD_CANVAS_ACTIONS: "LOAD_CANVAS_ACTIONS", @@ -99,8 +99,6 @@ export const ReduxActionTypes: { [key: string]: string } = { HIDE_PROPERTY_PANE: "HIDE_PROPERTY_PANE", INIT_DATASOURCE_PANE: "INIT_DATASOURCE_PANE", INIT_QUERY_PANE: "INIT_QUERY_PANE", - UPDATE_API_DRAFT: "UPDATE_API_DRAFT", - DELETE_API_DRAFT: "DELETE_API_DRAFT", QUERY_PANE_CHANGE: "QUERY_PANE_CHANGE", UPDATE_ROUTES_PARAMS: "UPDATE_ROUTES_PARAMS", SET_EXTRA_FORMDATA: "SET_EXTRA_FORMDATA", @@ -190,8 +188,6 @@ export const ReduxActionTypes: { [key: string]: string } = { ADD_API_TO_PAGE_SUCCESS: "ADD_API_TO_PAGE_SUCCESS", DELETE_QUERY_INIT: "DELETE_QUERY_INIT", DELETE_QUERY_SUCCESS: "DELETE_QUERY_SUCCESS", - EXECUTE_QUERY_REQUEST: "EXECUTE_QUERY_REQUEST", - RUN_QUERY_SUCCESS: "RUN_QUERY_SUCCESS", CLEAR_PREVIOUSLY_EXECUTED_QUERY: "CLEAR_PREVIOUSLY_EXECUTED_QUERY", FETCH_PROVIDERS_CATEGORIES_INIT: "FETCH_PROVIDERS_CATEGORIES_INIT", FETCH_PROVIDERS_CATEGORIES_SUCCESS: "FETCH_PROVIDERS_CATEGORIES_SUCCESS", @@ -237,6 +233,8 @@ export const ReduxActionTypes: { [key: string]: string } = { SAVE_API_NAME: "SAVE_API_NAME", SAVE_API_NAME_SUCCESS: "SAVE_API_NAME_SUCCESS", UPDATE_API_NAME_DRAFT: "UPDATE_API_NAME_DRAFT", + SET_ACTION_PROPERTY: "SET_ACTION_PROPERTY", + UPDATE_ACTION_PROPERTY: "UPDATE_ACTION_PROPERTY", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; @@ -261,7 +259,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { CREATE_ACTION_ERROR: "CREATE_ACTION_ERROR", UPDATE_ACTION_ERROR: "UPDATE_ACTION_ERROR", DELETE_ACTION_ERROR: "DELETE_ACTION_ERROR", - RUN_API_ERROR: "RUN_API_ERROR", + RUN_ACTION_ERROR: "RUN_ACTION_ERROR", EXECUTE_ACTION_ERROR: "EXECUTE_ACTION_ERROR", FETCH_DATASOURCES_ERROR: "FETCH_DATASOURCES_ERROR", SEARCH_APIORPROVIDERS_ERROR: "SEARCH_APIORPROVIDERS_ERROR", @@ -299,7 +297,6 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { MOVE_ACTION_ERROR: "MOVE_ACTION_ERROR", COPY_ACTION_ERROR: "COPY_ACTION_ERROR", DELETE_PAGE_ERROR: "DELETE_PAGE_ERROR", - RUN_QUERY_ERROR: "RUN_QUERY_ERROR", DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR", SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR", CREATE_ORGANIZATION_ERROR: "CREATE_ORGANIZATION_ERROR", diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index 0b99c58e3b..901d8b2f5d 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -8,8 +8,7 @@ import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsRe import { MetaState } from "reducers/entityReducers/metaReducer"; import { PageListPayload } from "constants/ReduxActionConstants"; import WidgetFactory from "utils/WidgetFactory"; -import { ActionDraftsState } from "reducers/entityReducers/actionDraftsReducer"; -import { Property, ActionConfig } from "entities/Action"; +import { ActionConfig, Property } from "entities/Action"; export type ActionDescription = { type: string; @@ -68,7 +67,6 @@ export type DataTree = { type DataTreeSeed = { actions: ActionDataState; - actionDrafts: ActionDraftsState; widgets: CanvasWidgetsReduxState; widgetsMeta: MetaState; pageList: PageListPayload; @@ -77,7 +75,7 @@ type DataTreeSeed = { export class DataTreeFactory { static create( - { actions, actionDrafts, widgets, widgetsMeta, pageList }: DataTreeSeed, + { actions, widgets, widgetsMeta, pageList }: DataTreeSeed, // TODO(hetu) // temporary fix for not getting functions while normal evals which crashes the app // need to remove this after we get a proper solve @@ -86,8 +84,7 @@ export class DataTreeFactory { const dataTree: DataTree = {}; const actionPaths = []; actions.forEach(a => { - const config = - a.config.id in actionDrafts ? actionDrafts[a.config.id] : a.config; + const config = a.config; let dynamicBindingPathList: Property[] = []; // update paths if ( diff --git a/app/client/src/entities/Datasource/index.ts b/app/client/src/entities/Datasource/index.ts index 90a4aecf74..04d71509d6 100644 --- a/app/client/src/entities/Datasource/index.ts +++ b/app/client/src/entities/Datasource/index.ts @@ -2,7 +2,10 @@ import { Datasource } from "api/DatasourcesApi"; export type EmbeddedDatasource = Omit; -export const DEFAULT_DATASOURCE = (pluginId: string): EmbeddedDatasource => ({ +export const DEFAULT_DATASOURCE = ( + pluginId: string, + organizationId: string, +): EmbeddedDatasource => ({ name: "DEFAULT_REST_DATASOURCE", datasourceConfiguration: { url: "", @@ -10,4 +13,5 @@ export const DEFAULT_DATASOURCE = (pluginId: string): EmbeddedDatasource => ({ invalids: [], isValid: true, pluginId, + organizationId, }); diff --git a/app/client/src/jsExecution/RealmExecutor.ts b/app/client/src/jsExecution/RealmExecutor.ts index 2e92cf4724..f21f1b4e48 100644 --- a/app/client/src/jsExecution/RealmExecutor.ts +++ b/app/client/src/jsExecution/RealmExecutor.ts @@ -40,7 +40,9 @@ export default class RealmExecutor implements JSExecutor { safeObject.actionPaths.forEach(path => { const action = _.get(safeObject, path); const entity = _.get(safeObject, path.split(".")[0]) - _.set(safeObject, path, pusher.bind(safeObject, action.bind(entity))) + if(action) { + _.set(safeObject, path, pusher.bind(safeObject, action.bind(entity))) + } }) } return safeObject diff --git a/app/client/src/pages/Editor/APIEditor/index.tsx b/app/client/src/pages/Editor/APIEditor/index.tsx index 4645fb5a8c..7929256060 100644 --- a/app/client/src/pages/Editor/APIEditor/index.tsx +++ b/app/client/src/pages/Editor/APIEditor/index.tsx @@ -4,11 +4,7 @@ import { getFormValues, submit } from "redux-form"; import ApiEditorForm from "./Form"; import RapidApiEditorForm from "./RapidApiEditorForm"; import ApiHomeScreen from "./ApiHomeScreen"; -import { - runApiAction, - deleteAction, - updateAction, -} from "actions/actionActions"; +import { runAction, deleteAction, updateAction } from "actions/actionActions"; import { PaginationField } from "api/ActionAPI"; import { AppState } from "reducers"; import { RouteComponentProps } from "react-router"; @@ -235,8 +231,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { const apiName = getApiName(state, props.match.params.apiId); const { isDeleting, isRunning, isCreating } = state.ui.apiPane; - const actionDrafts = state.entities.actionDrafts; - const allowSave = !!(apiAction && apiAction.id in actionDrafts); + const allowSave = true; const datasourceFieldText = state.ui.apiPane.datasourceFieldText[formData?.id ?? ""] || ""; @@ -261,7 +256,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { const mapDispatchToProps = (dispatch: any): ReduxActionProps => ({ submitForm: (name: string) => dispatch(submit(name)), runAction: (id: string, paginationField?: PaginationField) => - dispatch(runApiAction(id, paginationField)), + dispatch(runAction(id, paginationField)), deleteAction: (id: string, name: string) => dispatch(deleteAction({ id, name })), updateAction: (data: RestAction) => dispatch(updateAction({ data })), diff --git a/app/client/src/pages/Editor/ApiSidebar.tsx b/app/client/src/pages/Editor/ApiSidebar.tsx index d7740969b0..bcc3108601 100644 --- a/app/client/src/pages/Editor/ApiSidebar.tsx +++ b/app/client/src/pages/Editor/ApiSidebar.tsx @@ -24,7 +24,6 @@ import { getNextEntityName } from "utils/AppsmithUtils"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { Page } from "constants/ReduxActionConstants"; import { RestAction } from "entities/Action"; -import { ActionDraftsState } from "reducers/entityReducers/actionDraftsReducer"; const HTTPMethod = styled.span<{ method?: string }>` flex: 1; @@ -51,6 +50,7 @@ const ActionItem = styled.div` flex: 1; display: flex; align-items: center; + max-width: 90%; `; const ActionName = styled.span` @@ -59,12 +59,10 @@ const ActionName = styled.span` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 100px; `; interface ReduxStateProps { actions: ActionDataState; - actionDrafts: ActionDraftsState; apiPane: ApiPaneReduxState; pages: Page[]; } @@ -92,16 +90,6 @@ class ApiSidebar extends React.Component { this.props.initApiPane(this.props.match.params.apiId); } - shouldComponentUpdate(nextProps: Readonly): boolean { - if ( - Object.keys(nextProps.actionDrafts) !== - Object.keys(this.props.actionDrafts) - ) { - return true; - } - return nextProps.actions !== this.props.actions; - } - handleApiChange = (actionId: string) => { this.props.onApiChange(actionId); }; @@ -197,20 +185,20 @@ class ApiSidebar extends React.Component { render() { const { - actionDrafts, apiPane: { isFetching }, match: { params: { apiId }, }, actions, } = this.props; - const data = actions.map(a => a.config).filter(a => a.pluginType === "API"); + const data = actions + .filter(a => a.config?.pluginType === "API") + .map(a => a.config); return ( { const mapStateToProps = (state: AppState): ReduxStateProps => ({ actions: state.entities.actions, - actionDrafts: state.entities.actionDrafts, apiPane: state.ui.apiPane, pages: state.entities.pageList.pages, }); diff --git a/app/client/src/pages/Editor/EditorSidebar.tsx b/app/client/src/pages/Editor/EditorSidebar.tsx index 9667a52d31..083b3b1b56 100644 --- a/app/client/src/pages/Editor/EditorSidebar.tsx +++ b/app/client/src/pages/Editor/EditorSidebar.tsx @@ -21,7 +21,6 @@ import TreeDropdown from "components/editorComponents/actioncreator/TreeDropdown import { theme } from "constants/DefaultTheme"; import { Colors } from "constants/Colors"; import { ControlIcons } from "icons/ControlIcons"; -import NotificationIcon from "components/designSystems/appsmith/NotificationIcon"; const LoadingContainer = styled(CenteredWrapper)` height: 50%; @@ -165,11 +164,6 @@ const ItemContainer = styled.div<{ } `; -const DraftIconIndicator = styled(NotificationIcon)<{ isHidden: boolean }>` - margin: 0 5px; - opacity: ${({ isHidden }) => (isHidden ? 0 : 1)}; -`; - const StyledAddButton = styled(Button)` &&& { outline: none; @@ -192,7 +186,6 @@ type EditorSidebarComponentProps = { isLoading: boolean; list: Array; selectedItemId?: string; - draftIds: string[]; itemRender: (item: any) => JSX.Element; onItemCreateClick: (pageId: string) => void; onItemSelected: (itemId: string, itemPageId: string) => void; @@ -306,7 +299,6 @@ class EditorSidebar extends React.Component { isLoading, itemRender, selectedItemId, - draftIds, location, createButtonTitle, } = this.props; @@ -404,11 +396,6 @@ class EditorSidebar extends React.Component { {this.state.itemDragging !== item.id && ( - { diff --git a/app/client/src/pages/Editor/QueryEditor/JSONViewer.tsx b/app/client/src/pages/Editor/QueryEditor/JSONViewer.tsx index 546cfa462a..441f47a26d 100644 --- a/app/client/src/pages/Editor/QueryEditor/JSONViewer.tsx +++ b/app/client/src/pages/Editor/QueryEditor/JSONViewer.tsx @@ -35,6 +35,10 @@ class JSONOutput extends React.Component { collapsed: 1, }; + if (typeof src !== "object") { + return {src}; + } + if (!src.length) { return ( diff --git a/app/client/src/pages/Editor/QueryEditor/index.tsx b/app/client/src/pages/Editor/QueryEditor/index.tsx index 72230335c0..9aa10905e3 100644 --- a/app/client/src/pages/Editor/QueryEditor/index.tsx +++ b/app/client/src/pages/Editor/QueryEditor/index.tsx @@ -12,8 +12,8 @@ import styled from "styled-components"; import { QueryEditorRouteParams } from "constants/routes"; import QueryEditorForm from "./Form"; import QueryHomeScreen from "./QueryHomeScreen"; -import { updateAction } from "actions/actionActions"; -import { deleteQuery, executeQuery } from "actions/queryPaneActions"; +import { runAction, updateAction } from "actions/actionActions"; +import { deleteQuery } from "actions/queryPaneActions"; import { AppState } from "reducers"; import { getDataSources } from "selectors/editorSelectors"; import { QUERY_EDITOR_FORM_NAME } from "constants/forms"; @@ -32,7 +32,6 @@ import { import { getCurrentApplication } from "selectors/applicationSelectors"; import { QueryAction, RestAction } from "entities/Action"; import { getPluginImage } from "pages/Editor/QueryEditor/helpers"; -import { ActionDraftsState } from "reducers/entityReducers/actionDraftsReducer"; const EmptyStateContainer = styled.div` display: flex; @@ -46,7 +45,6 @@ type QueryPageProps = { queryPane: QueryPaneReduxState; formData: RestAction; isCreating: boolean; - actionDrafts: ActionDraftsState; initialValues: RestAction; pluginIds: Array | undefined; submitForm: (name: string) => void; @@ -97,7 +95,6 @@ class QueryEditor extends React.Component { pluginIds, executedQueryData, selectedPluginPackage, - actionDrafts, isCreating, runErrorMessage, } = this.props; @@ -130,7 +127,7 @@ class QueryEditor extends React.Component { location={this.props.location} applicationId={applicationId} pageId={pageId} - allowSave={queryId in actionDrafts} + allowSave={true} isSaving={isSaving[queryId]} isRunning={isRunning[queryId]} isDeleting={isDeleting[queryId]} @@ -175,7 +172,6 @@ const mapStateToProps = (state: AppState): any => { return { plugins: getPlugins(state), runErrorMessage, - actionDrafts: state.entities.actionDrafts, pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS), dataSources: getDataSources(state), executedQueryData: state.ui.queryPane.runQuerySuccessData, @@ -193,7 +189,7 @@ const mapDispatchToProps = (dispatch: any): any => ({ updateAction: (data: RestAction) => dispatch(updateAction({ data })), deleteAction: (id: string) => dispatch(deleteQuery({ id })), runAction: (action: RestAction, actionId: string) => - dispatch(executeQuery({ action, actionId })), + dispatch(runAction(actionId)), createTemplate: (template: any) => { dispatch(change(QUERY_EDITOR_FORM_NAME, QUERY_BODY_FIELD, template)); }, diff --git a/app/client/src/pages/Editor/QuerySidebar.tsx b/app/client/src/pages/Editor/QuerySidebar.tsx index de6fc5c021..804ea7b680 100644 --- a/app/client/src/pages/Editor/QuerySidebar.tsx +++ b/app/client/src/pages/Editor/QuerySidebar.tsx @@ -27,7 +27,6 @@ import { getDataSources } from "selectors/editorSelectors"; import { QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID } from "constants/routes"; import { RestAction } from "entities/Action"; import { Colors } from "constants/Colors"; -import { ActionDraftsState } from "reducers/entityReducers/actionDraftsReducer"; const ActionItem = styled.div` flex: 1; @@ -60,7 +59,6 @@ interface ReduxStateProps { plugins: Plugin[]; queries: ActionDataState; apiPane: ApiPaneReduxState; - actionDrafts: ActionDraftsState; actions: ActionDataState; dataSources: Datasource[]; } @@ -88,16 +86,6 @@ class QuerySidebar extends React.Component { this.props.initQueryPane(QUERY_CONSTANT, this.props.match.params.queryId); } - shouldComponentUpdate(nextProps: Readonly): boolean { - if ( - Object.keys(nextProps.actionDrafts) !== - Object.keys(this.props.actionDrafts) - ) { - return true; - } - return nextProps.actions !== this.props.actions; - } - handleCreateNew = () => { const { actions } = this.props; const { pageId } = this.props.match.params; @@ -182,7 +170,6 @@ class QuerySidebar extends React.Component { render() { const { - actionDrafts, apiPane: { isFetching }, match: { params: { queryId }, @@ -196,7 +183,6 @@ class QuerySidebar extends React.Component { isLoading={isFetching} list={data} selectedItemId={queryId} - draftIds={Object.keys(actionDrafts)} itemRender={this.renderItem} onItemCreateClick={this.handleCreateNewQueryClick} onItemSelected={this.handleQueryChange} @@ -212,7 +198,6 @@ class QuerySidebar extends React.Component { const mapStateToProps = (state: AppState): ReduxStateProps => ({ plugins: getPlugins(state), queries: getQueryActions(state), - actionDrafts: state.entities.actionDrafts, apiPane: state.ui.apiPane, actions: state.entities.actions, dataSources: getDataSources(state), diff --git a/app/client/src/reducers/entityReducers/actionDraftsReducer.ts b/app/client/src/reducers/entityReducers/actionDraftsReducer.ts deleted file mode 100644 index 370e7401b9..0000000000 --- a/app/client/src/reducers/entityReducers/actionDraftsReducer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createReducer } from "utils/AppsmithUtils"; -import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; -import { Action, RestAction } from "entities/Action"; -import _ from "lodash"; -import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer"; - -export type ActionDraftsState = Record; - -const initialState: ActionDraftsState = {}; - -const actionDraftsReducer = createReducer(initialState, { - [ReduxActionTypes.UPDATE_API_DRAFT]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string; draft: Partial }>, - ) => ({ - ...state, - [action.payload.id]: action.payload.draft, - }), - [ReduxActionTypes.DELETE_API_DRAFT]: ( - state: ApiPaneReduxState, - action: ReduxAction<{ id: string }>, - ) => _.omit(state, action.payload.id), -}); - -export default actionDraftsReducer; diff --git a/app/client/src/reducers/entityReducers/actionsReducer.tsx b/app/client/src/reducers/entityReducers/actionsReducer.tsx index c97a93b508..b22e892c3b 100644 --- a/app/client/src/reducers/entityReducers/actionsReducer.tsx +++ b/app/client/src/reducers/entityReducers/actionsReducer.tsx @@ -8,6 +8,7 @@ import { ActionResponse } from "api/ActionAPI"; import { ExecuteErrorPayload } from "constants/ActionConstants"; import _ from "lodash"; import { RapidApiAction, RestAction } from "entities/Action"; +import { UpdateActionPropertyActionPayload } from "actions/actionActions"; export interface ActionData { isLoading: boolean; config: RestAction | RapidApiAction; @@ -87,6 +88,16 @@ const actionsReducer = createReducer(initialState, { return { ...a, config: action.payload.data }; return a; }), + [ReduxActionTypes.UPDATE_ACTION_PROPERTY]: ( + state: ActionDataState, + action: ReduxAction, + ) => + state.map(a => { + if (a.config.id === action.payload.id) { + return _.set(a, `config.${action.payload.field}`, action.payload.value); + } + return a; + }), [ReduxActionTypes.DELETE_ACTION_SUCCESS]: ( state: ActionDataState, action: ReduxAction<{ id: string }>, @@ -126,17 +137,17 @@ const actionsReducer = createReducer(initialState, { ): ActionDataState => state.map(a => { if (a.config.id === action.payload.actionId) { - return { ...a, isLoading: false }; + return { ...a, isLoading: false, data: action.payload.error }; } return a; }), - [ReduxActionTypes.RUN_API_REQUEST]: ( + [ReduxActionTypes.RUN_ACTION_REQUEST]: ( state: ActionDataState, - action: ReduxAction, + action: ReduxAction<{ id: string }>, ): ActionDataState => state.map(a => { - if (action.payload === a.config.id) { + if (action.payload.id === a.config.id) { return { ...a, isLoading: true, @@ -145,7 +156,7 @@ const actionsReducer = createReducer(initialState, { return a; }), - [ReduxActionTypes.RUN_API_SUCCESS]: ( + [ReduxActionTypes.RUN_ACTION_SUCCESS]: ( state: ActionDataState, action: ReduxAction<{ [id: string]: ActionResponse }>, ): ActionDataState => { @@ -157,20 +168,7 @@ const actionsReducer = createReducer(initialState, { return a; }); }, - [ReduxActionTypes.RUN_QUERY_SUCCESS]: ( - state: ActionDataState, - action: ReduxAction<{ actionId: string; data: ActionResponse }>, - ): ActionDataState => { - const actionId: string = action.payload.actionId; - - return state.map(a => { - if (a.config.id === actionId) { - return { ...a, isLoading: false, data: action.payload.data }; - } - return a; - }); - }, - [ReduxActionErrorTypes.RUN_API_ERROR]: ( + [ReduxActionErrorTypes.RUN_ACTION_ERROR]: ( state: ActionDataState, action: ReduxAction<{ id: string }>, ): ActionDataState => diff --git a/app/client/src/reducers/entityReducers/index.tsx b/app/client/src/reducers/entityReducers/index.tsx index 641d586211..3fb0ad4412 100644 --- a/app/client/src/reducers/entityReducers/index.tsx +++ b/app/client/src/reducers/entityReducers/index.tsx @@ -9,14 +9,12 @@ import pageListReducer from "./pageListReducer"; import jsExecutionsReducer from "./jsExecutionsReducer"; import pluginsReducer from "reducers/entityReducers/pluginsReducer"; import metaReducer from "./metaReducer"; -import actionDraftsReducer from "reducers/entityReducers/actionDraftsReducer"; const entityReducer = combineReducers({ canvasWidgets: canvasWidgetsReducer, queryData: queryDataReducer, widgetConfig: widgetConfigReducer, actions: actionsReducer, - actionDrafts: actionDraftsReducer, propertyConfig: propertyPaneConfigReducer, datasources: datasourceReducer, pageList: pageListReducer, diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx index 497e97c6a9..2b38bd7208 100644 --- a/app/client/src/reducers/index.tsx +++ b/app/client/src/reducers/index.tsx @@ -27,7 +27,6 @@ import { ImportedCollectionsReduxState } from "reducers/uiReducers/importedColle import { ProvidersReduxState } from "reducers/uiReducers/providerReducer"; import { MetaState } from "./entityReducers/metaReducer"; import { ImportReduxState } from "reducers/uiReducers/importReducer"; -import { ActionDraftsState } from "reducers/entityReducers/actionDraftsReducer"; import { HelpReduxState } from "./uiReducers/helpReducer"; import { ApiNameReduxState } from "./uiReducers/apiNameReducer"; @@ -64,7 +63,6 @@ export interface AppState { canvasWidgets: CanvasWidgetsReduxState; queryData: QueryDataState; actions: ActionDataState; - actionDrafts: ActionDraftsState; propertyConfig: PropertyPaneConfigState; widgetConfig: WidgetConfigReducerState; datasources: DatasourceDataState; diff --git a/app/client/src/reducers/uiReducers/apiPaneReducer.ts b/app/client/src/reducers/uiReducers/apiPaneReducer.ts index 2e9e5a45ce..1c2b094aa3 100644 --- a/app/client/src/reducers/uiReducers/apiPaneReducer.ts +++ b/app/client/src/reducers/uiReducers/apiPaneReducer.ts @@ -5,6 +5,7 @@ import { ReduxAction, } from "constants/ReduxActionConstants"; import { RestAction } from "entities/Action"; +import { UpdateActionPropertyActionPayload } from "actions/actionActions"; const initialState: ApiPaneReduxState = { lastUsed: "", @@ -13,6 +14,7 @@ const initialState: ApiPaneReduxState = { isRunning: {}, isSaving: {}, isDeleting: {}, + isDirty: {}, currentCategory: "", lastUsedEditorPage: "", lastSelectedPage: "", @@ -27,6 +29,7 @@ export interface ApiPaneReduxState { isRunning: Record; isSaving: Record; isDeleting: Record; + isDirty: Record; currentCategory: string; lastUsedEditorPage: string; datasourceFieldText: Record; @@ -65,7 +68,7 @@ const apiPaneReducer = createReducer(initialState, { ...state, isCreating: false, }), - [ReduxActionTypes.RUN_API_REQUEST]: ( + [ReduxActionTypes.RUN_ACTION_REQUEST]: ( state: ApiPaneReduxState, action: ReduxAction<{ id: string }>, ) => ({ @@ -75,7 +78,7 @@ const apiPaneReducer = createReducer(initialState, { [action.payload.id]: true, }, }), - [ReduxActionTypes.RUN_API_SUCCESS]: ( + [ReduxActionTypes.RUN_ACTION_SUCCESS]: ( state: ApiPaneReduxState, action: ReduxAction<{ [id: string]: any }>, ) => { @@ -88,7 +91,7 @@ const apiPaneReducer = createReducer(initialState, { }, }; }, - [ReduxActionErrorTypes.RUN_API_ERROR]: ( + [ReduxActionErrorTypes.RUN_ACTION_ERROR]: ( state: ApiPaneReduxState, action: ReduxAction<{ id: string }>, ) => ({ @@ -98,6 +101,16 @@ const apiPaneReducer = createReducer(initialState, { [action.payload.id]: false, }, }), + [ReduxActionTypes.UPDATE_ACTION_PROPERTY]: ( + state: ApiPaneReduxState, + action: ReduxAction, + ) => ({ + ...state, + isDirty: { + ...state.isDirty, + [action.payload.id]: true, + }, + }), [ReduxActionTypes.UPDATE_ACTION_INIT]: ( state: ApiPaneReduxState, action: ReduxAction<{ data: RestAction }>, @@ -117,6 +130,10 @@ const apiPaneReducer = createReducer(initialState, { ...state.isSaving, [action.payload.data.id]: false, }, + isDirty: { + ...state.isDirty, + [action.payload.data.id]: false, + }, }), [ReduxActionErrorTypes.UPDATE_ACTION_ERROR]: ( state: ApiPaneReduxState, diff --git a/app/client/src/reducers/uiReducers/queryPaneReducer.ts b/app/client/src/reducers/uiReducers/queryPaneReducer.ts index bbee40210c..c564c1711e 100644 --- a/app/client/src/reducers/uiReducers/queryPaneReducer.ts +++ b/app/client/src/reducers/uiReducers/queryPaneReducer.ts @@ -6,6 +6,7 @@ import { } from "constants/ReduxActionConstants"; import _ from "lodash"; import { RestAction } from "entities/Action"; +import { ActionResponse } from "api/ActionAPI"; const initialState: QueryPaneReduxState = { isFetching: false, @@ -115,15 +116,15 @@ const queryPaneReducer = createReducer(initialState, { [action.payload.id]: false, }, }), - [ReduxActionTypes.EXECUTE_QUERY_REQUEST]: ( + [ReduxActionTypes.RUN_ACTION_REQUEST]: ( state: any, - action: ReduxAction<{ action: RestAction; actionId: string }>, + action: ReduxAction<{ id: string }>, ) => { return { ...state, isRunning: { ...state.isRunning, - [action.payload.actionId]: true, + [action.payload.id]: true, }, runQuerySuccessData: [], }; @@ -133,40 +134,39 @@ const queryPaneReducer = createReducer(initialState, { runQuerySuccessData: [], }), - [ReduxActionTypes.RUN_QUERY_SUCCESS]: ( + [ReduxActionTypes.RUN_ACTION_SUCCESS]: ( state: any, - action: ReduxAction<{ actionId: string; data: object }>, + action: ReduxAction<{ [id: string]: ActionResponse }>, ) => { - const { actionId } = action.payload; - - return { - ...state, - isRunning: { - ...state.isRunning, - [action.payload.actionId]: false, - }, - runQuerySuccessData: { - ...state.runQuerySuccessData, - [action.payload.actionId]: action.payload.data, - }, - runErrorMessage: _.omit(state.runErrorMessage, [actionId]), - }; - }, - [ReduxActionErrorTypes.RUN_QUERY_ERROR]: ( - state: any, - action: ReduxAction<{ actionId: string; message: string }>, - ) => { - const { actionId, message } = action.payload; - + const actionId = Object.keys(action.payload)[0]; return { ...state, isRunning: { ...state.isRunning, [actionId]: false, }, + runQuerySuccessData: { + ...state.runQuerySuccessData, + ...action.payload, + }, + runErrorMessage: _.omit(state.runErrorMessage, [actionId]), + }; + }, + [ReduxActionErrorTypes.RUN_ACTION_ERROR]: ( + state: any, + action: ReduxAction<{ id: string; error: Error }>, + ) => { + const { id, error } = action.payload; + + return { + ...state, + isRunning: { + ...state.isRunning, + [id]: false, + }, runErrorMessage: { ...state.runError, - [actionId]: message, + [id]: error.message, }, }; }, diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts new file mode 100644 index 0000000000..50771375b3 --- /dev/null +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -0,0 +1,454 @@ +import { + ReduxAction, + ReduxActionErrorTypes, + ReduxActionTypes, +} from "constants/ReduxActionConstants"; +import { + EventType, + ExecuteActionPayload, + ExecuteActionPayloadEvent, + PageAction, +} from "constants/ActionConstants"; +import * as log from "loglevel"; +import { + all, + call, + put, + select, + take, + takeEvery, + takeLatest, +} from "redux-saga/effects"; +import { evaluateDataTree } from "selectors/dataTreeSelectors"; +import { + getDynamicBindings, + getDynamicValue, + isDynamicValue, +} from "utils/DynamicBindingUtils"; +import { + ActionDescription, + RunActionPayload, +} from "entities/DataTree/dataTreeFactory"; +import { AppToaster } from "components/editorComponents/ToastComponent"; +import { executeAction, executeActionError } from "actions/widgetActions"; +import { + getCurrentApplicationId, + getPageList, +} from "selectors/editorSelectors"; +import _ from "lodash"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +import history from "utils/history"; +import { + BUILDER_PAGE_URL, + getApplicationViewerPageURL, +} from "constants/routes"; +import { + executeApiActionRequest, + executeApiActionSuccess, + updateAction, +} from "actions/actionActions"; +import { Action, RestAction } from "entities/Action"; +import ActionAPI, { + ActionApiResponse, + ActionResponse, + ExecuteActionRequest, + PaginationField, + Property, +} from "api/ActionAPI"; +import { + getAction, + getCurrentPageNameByActionId, + isActionDirty, + isActionSaving, +} from "selectors/entitiesSelector"; +import { AppState } from "reducers"; +import { mapToPropList } from "utils/AppsmithUtils"; +import { validateResponse } from "sagas/ErrorSagas"; +import { ToastType } from "react-toastify"; +import { PLUGIN_TYPE_API } from "constants/ApiEditorConstants"; + +function* navigateActionSaga( + action: { pageNameOrUrl: string; params: Record }, + event: ExecuteActionPayloadEvent, +) { + const pageList = yield select(getPageList); + const applicationId = yield select(getCurrentApplicationId); + const page = _.find(pageList, { pageName: action.pageNameOrUrl }); + if (page) { + AnalyticsUtil.logEvent("NAVIGATE", { + pageName: action.pageNameOrUrl, + pageParams: action.params, + }); + // TODO need to make this check via RENDER_MODE; + const path = + history.location.pathname.indexOf("/edit") !== -1 + ? BUILDER_PAGE_URL(applicationId, page.pageId, action.params) + : getApplicationViewerPageURL( + applicationId, + page.pageId, + action.params, + ); + history.push(path); + if (event.callback) event.callback({ success: true }); + } else { + AnalyticsUtil.logEvent("NAVIGATE", { + navUrl: action.pageNameOrUrl, + }); + // Add a default protocol if it doesn't exist. + let url = action.pageNameOrUrl; + if (url.indexOf("://") === -1) { + url = "https://" + url; + } + window.location.assign(url); + } +} + +export const getActionTimeout = ( + state: AppState, + actionId: string, +): number | undefined => { + const action = _.find(state.entities.actions, a => a.config.id === actionId); + if (action) { + const timeout = action.config.actionConfiguration.timeoutInMillisecond; + if (timeout) { + // Extra timeout padding to account for network calls + return timeout + 5000; + } + return undefined; + } + return undefined; +}; +const createActionExecutionResponse = ( + response: ActionApiResponse, +): ActionResponse => ({ + ...response.data, + ...response.clientMeta, +}); +const isErrorResponse = (response: ActionApiResponse) => { + return !response.data.isExecutionSuccess; +}; + +export function* evaluateDynamicBoundValueSaga(path: string): any { + log.debug("Evaluating data tree to get action binding value"); + const tree = yield select(evaluateDataTree(false)); + const dynamicResult = getDynamicValue(`{{${path}}}`, tree); + return dynamicResult.result; +} + +export function* getActionParams(jsonPathKeys: string[] | undefined) { + if (_.isNil(jsonPathKeys)) return []; + const values: any = yield all( + jsonPathKeys.map((jsonPath: string) => { + return call(evaluateDynamicBoundValueSaga, jsonPath); + }), + ); + const dynamicBindings: Record = {}; + jsonPathKeys.forEach((key, i) => { + let value = values[i]; + if (typeof value === "object") value = JSON.stringify(value); + dynamicBindings[key] = value; + }); + return mapToPropList(dynamicBindings); +} + +export function extractBindingsFromAction(action: Action) { + const bindings: string[] = []; + action.dynamicBindingPathList.forEach(a => { + const value = _.get(action, a.key); + if (isDynamicValue(value)) { + const { jsSnippets } = getDynamicBindings(value); + bindings.push(...jsSnippets.filter(jsSnippet => !!jsSnippet)); + } + }); + return bindings; +} + +export function* executeActionSaga( + apiAction: RunActionPayload, + event: ExecuteActionPayloadEvent, +) { + const { actionId, onSuccess, onError } = apiAction; + try { + yield put(executeApiActionRequest({ id: apiAction.actionId })); + const api: RestAction = yield select(getAction, actionId); + const params: Property[] = yield call(getActionParams, api.jsonPathKeys); + const pagination = + event.type === EventType.ON_NEXT_PAGE + ? "NEXT" + : event.type === EventType.ON_PREV_PAGE + ? "PREV" + : undefined; + const executeActionRequest: ExecuteActionRequest = { + action: { id: actionId }, + params, + paginationField: pagination, + }; + const timeout = yield select(getActionTimeout, actionId); + const response: ActionApiResponse = yield ActionAPI.executeAction( + executeActionRequest, + timeout, + ); + const payload = createActionExecutionResponse(response); + yield put( + executeApiActionSuccess({ + id: actionId, + response: payload, + }), + ); + if (isErrorResponse(response)) { + if (onError) { + yield put( + executeAction({ + dynamicString: onError, + event: { + ...event, + type: EventType.ON_ERROR, + }, + responseData: payload, + }), + ); + } else { + if (event.callback) { + event.callback({ success: false }); + } + } + } else { + if (onSuccess) { + yield put( + executeAction({ + dynamicString: onSuccess, + event: { + ...event, + type: EventType.ON_SUCCESS, + }, + responseData: payload, + }), + ); + } else { + if (event.callback) { + event.callback({ success: true }); + } + } + } + return response; + } catch (error) { + yield put( + executeActionError({ + actionId: actionId, + error, + }), + ); + AppToaster.show({ + message: "Action execution failed", + type: "error", + }); + if (onError) { + yield put( + executeAction({ + dynamicString: `{{${onError}}}`, + event: { + ...event, + type: EventType.ON_ERROR, + }, + responseData: {}, + }), + ); + } else { + if (event.callback) { + event.callback({ success: false }); + } + } + } +} + +function* executeActionTriggers( + trigger: ActionDescription, + event: ExecuteActionPayloadEvent, +) { + try { + switch (trigger.type) { + case "RUN_ACTION": + yield call(executeActionSaga, trigger.payload, event); + break; + case "NAVIGATE_TO": + yield call(navigateActionSaga, trigger.payload, event); + break; + case "SHOW_ALERT": + AppToaster.show({ + message: trigger.payload.message, + type: trigger.payload.style, + }); + if (event.callback) event.callback({ success: true }); + break; + case "SHOW_MODAL_BY_NAME": + yield put(trigger); + if (event.callback) event.callback({ success: true }); + break; + case "CLOSE_MODAL": + yield put(trigger); + if (event.callback) event.callback({ success: true }); + break; + default: + yield put( + executeActionError({ + error: "Trigger type unknown", + actionId: "", + }), + ); + } + } catch (e) { + yield put( + executeActionError({ + error: "Failed to execute action", + actionId: "", + }), + ); + if (event.callback) event.callback({ success: false }); + } +} + +function* executeAppAction(action: ReduxAction) { + const { dynamicString, event, responseData } = action.payload; + log.debug("Evaluating data tree to get action trigger"); + log.debug({ dynamicString }); + const tree = yield select(evaluateDataTree(true)); + log.debug({ tree }); + const { triggers } = getDynamicValue(dynamicString, tree, responseData, true); + log.debug({ triggers }); + if (triggers && triggers.length) { + yield all( + triggers.map(trigger => call(executeActionTriggers, trigger, event)), + ); + } else { + if (event.callback) event.callback({ success: true }); + } +} + +function* runActionSaga( + reduxAction: ReduxAction<{ + id: string; + paginationField: PaginationField; + }>, +) { + try { + const actionId = reduxAction.payload.id; + const isSaving = yield select(isActionSaving(actionId)); + const isDirty = yield select(isActionDirty(actionId)); + if (isSaving || isDirty) { + if (isDirty && !isSaving) { + const actionObject = yield select(getAction, actionId); + yield put(updateAction({ data: actionObject })); + } + yield take(ReduxActionTypes.UPDATE_ACTION_SUCCESS); + } + const actionObject = yield select(getAction, actionId); + const action: ExecuteActionRequest["action"] = { id: actionId }; + const jsonPathKeys = actionObject.jsonPathKeys; + + const { paginationField } = reduxAction.payload; + + const params = yield call(getActionParams, jsonPathKeys); + const timeout = yield select(getActionTimeout, actionId); + const response: ActionApiResponse = yield ActionAPI.executeAction( + { + action, + params, + paginationField, + }, + timeout, + ); + const isValidResponse = yield validateResponse(response); + + if (isValidResponse) { + const payload = createActionExecutionResponse(response); + + const pageName = yield select(getCurrentPageNameByActionId, actionId); + const eventName = + actionObject.pluginType === PLUGIN_TYPE_API ? "RUN_API" : "RUN_QUERY"; + + AnalyticsUtil.logEvent(eventName, { + actionId, + actionName: actionObject.name, + pageName: pageName, + responseTime: response.clientMeta.duration, + apiType: "INTERNAL", + }); + + yield put({ + type: ReduxActionTypes.RUN_ACTION_SUCCESS, + payload: { [actionId]: payload }, + }); + AppToaster.show({ + message: "Action ran successfully", + type: ToastType.SUCCESS, + }); + } else { + let error = "An unexpected error occurred"; + if (response.data.body) { + error = response.data.body.toString(); + } + yield put({ + type: ReduxActionErrorTypes.RUN_ACTION_ERROR, + payload: { error, id: reduxAction.payload.id }, + }); + } + } catch (error) { + console.error(error); + yield put({ + type: ReduxActionErrorTypes.RUN_ACTION_ERROR, + payload: { error, id: reduxAction.payload.id }, + }); + } +} + +function* executePageLoadAction(pageAction: PageAction) { + yield put(executeApiActionRequest({ id: pageAction.id })); + const params: Property[] = yield call( + getActionParams, + pageAction.jsonPathKeys, + ); + const executeActionRequest: ExecuteActionRequest = { + action: { id: pageAction.id }, + params, + }; + const response: ActionApiResponse = yield ActionAPI.executeAction( + executeActionRequest, + pageAction.timeoutInMillisecond, + ); + + if (isErrorResponse(response)) { + yield put( + executeActionError({ + actionId: pageAction.id, + error: response.responseMeta.error, + }), + ); + } else { + const payload = createActionExecutionResponse(response); + yield put( + executeApiActionSuccess({ + id: pageAction.id, + response: payload, + }), + ); + } +} + +function* executePageLoadActionsSaga(action: ReduxAction) { + const pageActions = action.payload; + for (const actionSet of pageActions) { + // Load all sets in parallel + yield* yield all(actionSet.map(a => call(executePageLoadAction, a))); + } +} + +export function* watchActionExecutionSagas() { + yield all([ + takeEvery(ReduxActionTypes.EXECUTE_ACTION, executeAppAction), + takeLatest(ReduxActionTypes.RUN_ACTION_REQUEST, runActionSaga), + takeLatest( + ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS, + executePageLoadActionsSaga, + ), + ]); +} diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index d89faf07fb..df0869b247 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -11,23 +11,8 @@ import { takeEvery, takeLatest, } from "redux-saga/effects"; -import { - EventType, - ExecuteActionPayload, - ExecuteActionPayloadEvent, - PageAction, -} from "constants/ActionConstants"; -import ActionAPI, { - ActionApiResponse, - ActionCreateUpdateResponse, - ActionResponse, - ExecuteActionRequest, - PaginationField, - Property, -} from "api/ActionAPI"; -import { AppState } from "reducers"; +import ActionAPI, { ActionCreateUpdateResponse, Property } from "api/ActionAPI"; import _ from "lodash"; -import { mapToPropList } from "utils/AppsmithUtils"; import { AppToaster } from "components/editorComponents/ToastComponent"; import { GenericApiResponse } from "api/ApiResponses"; import PageApi from "api/PageApi"; @@ -37,366 +22,32 @@ import { copyActionSuccess, createActionSuccess, deleteActionSuccess, - executeApiActionRequest, - executeApiActionSuccess, + fetchActionsForPage, fetchActionsForPageSuccess, FetchActionsPayload, moveActionError, moveActionSuccess, + SetActionPropertyPayload, + updateActionProperty, updateActionSuccess, - fetchActionsForPage, } from "actions/actionActions"; import { - getDynamicBindings, - getDynamicValue, isDynamicValue, - removeBindingsFromObject, + removeBindingsFromActionObject, } from "utils/DynamicBindingUtils"; import { validateResponse } from "./ErrorSagas"; -import { getFormData } from "selectors/formSelectors"; -import { API_EDITOR_FORM_NAME } from "constants/forms"; -import { executeAction, executeActionError } from "actions/widgetActions"; -import { evaluateDataTree } from "selectors/dataTreeSelectors"; import { transformRestAction } from "transformers/RestActionTransformer"; -import { - ActionDescription, - RunActionPayload, -} from "entities/DataTree/dataTreeFactory"; -import { - getCurrentApplicationId, - getPageList, - getCurrentPageId, -} from "selectors/editorSelectors"; -import history from "utils/history"; -import { - BUILDER_PAGE_URL, - getApplicationViewerPageURL, -} from "constants/routes"; +import { getCurrentPageId } from "selectors/editorSelectors"; import { ToastType } from "react-toastify"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import * as log from "loglevel"; import { QUERY_CONSTANT } from "constants/QueryEditorConstants"; import { Action, RestAction } from "entities/Action"; import { ActionData } from "reducers/entityReducers/actionsReducer"; -import { getActions } from "selectors/entitiesSelector"; - -export const getAction = ( - state: AppState, - actionId: string, -): RestAction | undefined => { - const action = _.find(state.entities.actions, a => a.config.id === actionId); - return action ? action.config : undefined; -}; - -export const getActionTimeout = ( - state: AppState, - actionId: string, -): number | undefined => { - const action = _.find(state.entities.actions, a => a.config.id === actionId); - if (action) { - const timeout = action.config.actionConfiguration.timeoutInMillisecond; - if (timeout) { - // Extra timeout padding to account for network calls - return timeout + 5000; - } - return undefined; - } - return undefined; -}; - -const createActionSuccessResponse = ( - response: ActionApiResponse, -): ActionResponse => ({ - ...response.data, - ...response.clientMeta, -}); - -const isErrorResponse = (response: ActionApiResponse) => { - return ( - (response.responseMeta && response.responseMeta.error) || - !response.data.isExecutionSuccess - ); -}; - -function getCurrentPageNameByActionId( - state: AppState, - actionId: string, -): string { - const action = state.entities.actions.find(action => { - return action.config.id === actionId; - }); - const pageId = action ? action.config.pageId : ""; - return getPageNameByPageId(state, pageId); -} - -function getPageNameByPageId(state: AppState, pageId: string): string { - const page = state.entities.pageList.pages.find( - page => page.pageId === pageId, - ); - return page ? page.pageName : ""; -} - -const createActionErrorResponse = ( - response: ActionApiResponse, -): ActionResponse => ({ - body: response.responseMeta.error || { error: "Error" }, - statusCode: response.responseMeta.error - ? response.responseMeta.error.code.toString() - : "Error", - headers: {}, - request: { - headers: {}, - body: {}, - httpMethod: "", - url: "", - }, - duration: "0", - size: "0", -}); - -export function* evaluateDynamicBoundValueSaga(path: string): any { - log.debug("Evaluating data tree to get action binding value"); - const tree = yield select(evaluateDataTree(true)); - const dynamicResult = getDynamicValue(`{{${path}}}`, tree); - return dynamicResult.result; -} - -export function* getActionParams(jsonPathKeys: string[] | undefined) { - if (_.isNil(jsonPathKeys)) return []; - const values: any = yield all( - jsonPathKeys.map((jsonPath: string) => { - return call(evaluateDynamicBoundValueSaga, jsonPath); - }), - ); - const dynamicBindings: Record = {}; - jsonPathKeys.forEach((key, i) => { - let value = values[i]; - if (typeof value === "object") value = JSON.stringify(value); - dynamicBindings[key] = value; - }); - return mapToPropList(dynamicBindings); -} - -// function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) { -// const tree = yield select(getParsedDataTree); -// const result = JSExecutionManagerSingleton.evaluateSync( -// jsAction.jsFunction, -// tree, -// ); -// -// yield put({ -// type: ReduxActionTypes.SAVE_JS_EXECUTION_RECORD, -// payload: { -// [jsAction.jsFunctionId]: result, -// }, -// }); -// } - -export function* executeActionSaga( - apiAction: RunActionPayload, - event: ExecuteActionPayloadEvent, -) { - const { actionId, onSuccess, onError } = apiAction; - try { - yield put(executeApiActionRequest({ id: apiAction.actionId })); - const api: RestAction = yield select(getAction, actionId); - const params: Property[] = yield call(getActionParams, api.jsonPathKeys); - const pagination = - event.type === EventType.ON_NEXT_PAGE - ? "NEXT" - : event.type === EventType.ON_PREV_PAGE - ? "PREV" - : undefined; - const executeActionRequest: ExecuteActionRequest = { - action: { id: actionId }, - params, - paginationField: pagination, - }; - const timeout = yield select(getActionTimeout, actionId); - const response: ActionApiResponse = yield ActionAPI.executeAction( - executeActionRequest, - timeout, - ); - if (isErrorResponse(response)) { - const payload = createActionErrorResponse(response); - if (_.isNil(response.responseMeta.error)) { - AppToaster.show({ - message: api.name + " execution failed", - type: "error", - }); - } - if (onError) { - yield put( - executeAction({ - dynamicString: onError, - event: { - ...event, - type: EventType.ON_ERROR, - }, - responseData: payload, - }), - ); - } else { - if (event.callback) { - event.callback({ success: false }); - } - } - yield put( - executeActionError({ - actionId, - error: response.responseMeta.error, - }), - ); - } else { - const payload = createActionSuccessResponse(response); - yield put( - executeApiActionSuccess({ - id: apiAction.actionId, - response: payload, - }), - ); - if (onSuccess) { - yield put( - executeAction({ - dynamicString: onSuccess, - event: { - ...event, - type: EventType.ON_SUCCESS, - }, - responseData: payload, - }), - ); - } else { - if (event.callback) { - event.callback({ success: true }); - } - } - } - return response; - } catch (error) { - yield put( - executeActionError({ - actionId: actionId, - error, - }), - ); - if (onError) { - yield put( - executeAction({ - dynamicString: `{{${onError}}}`, - event: { - ...event, - type: EventType.ON_ERROR, - }, - responseData: {}, - }), - ); - } else { - if (event.callback) { - event.callback({ success: false }); - } - } - } -} - -function* navigateActionSaga( - action: { pageNameOrUrl: string; params: Record }, - event: ExecuteActionPayloadEvent, -) { - const pageList = yield select(getPageList); - const applicationId = yield select(getCurrentApplicationId); - const page = _.find(pageList, { pageName: action.pageNameOrUrl }); - if (page) { - AnalyticsUtil.logEvent("NAVIGATE", { - pageName: action.pageNameOrUrl, - pageParams: action.params, - }); - // TODO need to make this check via RENDER_MODE; - const path = - history.location.pathname.indexOf("/edit") !== -1 - ? BUILDER_PAGE_URL(applicationId, page.pageId, action.params) - : getApplicationViewerPageURL( - applicationId, - page.pageId, - action.params, - ); - history.push(path); - if (event.callback) event.callback({ success: true }); - } else { - AnalyticsUtil.logEvent("NAVIGATE", { - navUrl: action.pageNameOrUrl, - }); - // Add a default protocol if it doesn't exist. - let url = action.pageNameOrUrl; - if (url.indexOf("://") === -1) { - url = "https://" + url; - } - window.location.assign(url); - } -} - -export function* executeActionTriggers( - trigger: ActionDescription, - event: ExecuteActionPayloadEvent, -) { - try { - switch (trigger.type) { - case "RUN_ACTION": - yield call(executeActionSaga, trigger.payload, event); - break; - case "NAVIGATE_TO": - yield call(navigateActionSaga, trigger.payload, event); - break; - case "SHOW_ALERT": - AppToaster.show({ - message: trigger.payload.message, - type: trigger.payload.style, - }); - if (event.callback) event.callback({ success: true }); - break; - case "SHOW_MODAL_BY_NAME": - yield put(trigger); - if (event.callback) event.callback({ success: true }); - break; - case "CLOSE_MODAL": - yield put(trigger); - if (event.callback) event.callback({ success: true }); - break; - default: - yield put( - executeActionError({ - error: "Trigger type unknown", - actionId: "", - }), - ); - } - } catch (e) { - yield put( - executeActionError({ - error: "Failed to execute action", - actionId: "", - }), - ); - if (event.callback) event.callback({ success: false }); - } -} - -export function* executeAppAction(action: ReduxAction) { - const { dynamicString, event, responseData } = action.payload; - log.debug("Evaluating data tree to get action trigger"); - log.debug({ dynamicString }); - const tree = yield select(evaluateDataTree(true)); - log.debug({ tree }); - const { triggers } = getDynamicValue(dynamicString, tree, responseData, true); - log.debug({ triggers }); - if (triggers && triggers.length) { - yield all( - triggers.map(trigger => call(executeActionTriggers, trigger, event)), - ); - } else { - if (event.callback) event.callback({ success: true }); - } -} +import { + getAction, + getCurrentPageNameByActionId, + getPageNameByPageId, +} from "selectors/entitiesSelector"; export function* createActionSaga(actionPayload: ReduxAction) { try { @@ -555,126 +206,6 @@ export function* deleteActionSaga( } } -export function extractBindingsFromAction(action: Action) { - const bindings: string[] = []; - action.dynamicBindingPathList.forEach(a => { - const value = _.get(action, a.key); - if (isDynamicValue(value)) { - const { jsSnippets } = getDynamicBindings(value); - bindings.push(...jsSnippets.filter(jsSnippet => !!jsSnippet)); - } - }); - return bindings; -} - -export function* runApiActionSaga( - reduxAction: ReduxAction<{ - id: string; - paginationField: PaginationField; - }>, -) { - try { - const { - values, - dirty, - valid, - }: { - values: RestAction; - dirty: boolean; - valid: boolean; - } = yield select(getFormData, API_EDITOR_FORM_NAME); - const actionObject: PageAction = yield select(getAction, values.id); - let action: ExecuteActionRequest["action"] = { id: values.id }; - let jsonPathKeys = actionObject.jsonPathKeys; - if (!valid) { - console.error("Form error"); - return; - } - if (dirty) { - action = _.omit(transformRestAction(values), "id") as RestAction; - jsonPathKeys = extractBindingsFromAction(action as RestAction); - } - const { paginationField } = reduxAction.payload; - - const params = yield call(getActionParams, jsonPathKeys); - const timeout = yield select(getActionTimeout, values.id); - const response: ActionApiResponse = yield ActionAPI.executeAction( - { - action, - params, - paginationField, - }, - timeout, - ); - let payload = createActionSuccessResponse(response); - if (response.responseMeta && response.responseMeta.error) { - payload = createActionErrorResponse(response); - } - const id = values.id || "DRY_RUN"; - - const pageName = yield select(getCurrentPageNameByActionId, values.id); - - AnalyticsUtil.logEvent("RUN_API", { - apiId: values.id, - apiName: values.name, - pageName: pageName, - responseTime: response.clientMeta.duration, - apiType: "INTERNAL", - }); - - yield put({ - type: ReduxActionTypes.RUN_API_SUCCESS, - payload: { [id]: payload }, - }); - } catch (error) { - yield put({ - type: ReduxActionErrorTypes.RUN_API_ERROR, - payload: { error, id: reduxAction.payload.id }, - }); - } -} - -function* executePageLoadAction(pageAction: PageAction) { - yield put(executeApiActionRequest({ id: pageAction.id })); - const params: Property[] = yield call( - getActionParams, - pageAction.jsonPathKeys, - ); - const executeActionRequest: ExecuteActionRequest = { - action: { id: pageAction.id }, - params, - }; - const response: ActionApiResponse = yield ActionAPI.executeAction( - executeActionRequest, - pageAction.timeoutInMillisecond, - ); - - if (isErrorResponse(response)) { - yield put( - executeActionError({ - actionId: pageAction.id, - error: response.responseMeta.error, - }), - ); - } else { - const payload = createActionSuccessResponse(response); - yield put( - executeApiActionSuccess({ - id: pageAction.id, - response: payload, - }), - ); - } -} - -function* executePageLoadActionsSaga(action: ReduxAction) { - const pageActions = action.payload; - for (const actionSet of pageActions) { - // Load all sets in parallel - yield* yield all(actionSet.map(a => call(executePageLoadAction, a))); - } -} - function* moveActionSaga( action: ReduxAction<{ id: string; @@ -683,12 +214,8 @@ function* moveActionSaga( name: string; }>, ) { - const drafts = yield select(state => state.ui.apiPane.drafts); - const dirty = action.payload.id in drafts; - const actionObject: RestAction = dirty - ? drafts[action.payload.id] - : yield select(getAction, action.payload.id); - const withoutBindings = removeBindingsFromObject(actionObject); + const actionObject: RestAction = yield select(getAction, action.payload.id); + const withoutBindings = removeBindingsFromActionObject(actionObject); try { const response = yield ActionAPI.moveAction({ action: { @@ -729,13 +256,9 @@ function* moveActionSaga( function* copyActionSaga( action: ReduxAction<{ id: string; destinationPageId: string; name: string }>, ) { - const drafts = yield select(state => state.ui.apiPane.drafts); - const dirty = action.payload.id in drafts; - let actionObject = dirty - ? drafts[action.payload.id] - : yield select(getAction, action.payload.id); + let actionObject: RestAction = yield select(getAction, action.payload.id); if (action.payload.destinationPageId !== actionObject.pageId) { - actionObject = removeBindingsFromObject(actionObject); + actionObject = removeBindingsFromActionObject(actionObject); } try { const copyAction = { @@ -842,19 +365,56 @@ function* saveApiNameSaga(action: ReduxAction<{ id: string; name: string }>) { } } +function getDynamicBindingsChangesSaga( + action: Action, + value: string, + field: string, +) { + const bindingField = field.replace("actionConfiguration.", ""); + const isDynamic = isDynamicValue(value); + let dynamicBindings: Property[] = action.dynamicBindingPathList || []; + const fieldExists = _.some(dynamicBindings, { key: bindingField }); + + if (!isDynamic && fieldExists) { + dynamicBindings = dynamicBindings.filter(d => d.key !== bindingField); + } + if (isDynamic && !fieldExists) { + dynamicBindings.push({ key: bindingField }); + } + if (dynamicBindings !== action.dynamicBindingPathList) { + return dynamicBindings; + } + return action.dynamicBindingPathList; +} + +function* setActionPropertySaga(action: ReduxAction) { + const { actionId, value, propertyName } = action.payload; + if (!actionId) return; + const actionObj = yield select(getAction, actionId); + const effects: Record = {}; + // Value change effect + effects[propertyName] = value; + // Bindings change effect + effects.dynamicBindingPathList = getDynamicBindingsChangesSaga( + actionObj, + value, + propertyName, + ); + yield all( + Object.keys(effects).map(field => + put(updateActionProperty({ id: actionId, field, value: effects[field] })), + ), + ); +} + export function* watchActionSagas() { yield all([ + takeEvery(ReduxActionTypes.SET_ACTION_PROPERTY, setActionPropertySaga), takeEvery(ReduxActionTypes.FETCH_ACTIONS_INIT, fetchActionsSaga), - takeEvery(ReduxActionTypes.EXECUTE_ACTION, executeAppAction), - takeLatest(ReduxActionTypes.RUN_API_REQUEST, runApiActionSaga), takeEvery(ReduxActionTypes.CREATE_ACTION_INIT, createActionSaga), takeLatest(ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga), takeLatest(ReduxActionTypes.DELETE_ACTION_INIT, deleteActionSaga), takeLatest(ReduxActionTypes.SAVE_API_NAME, saveApiNameSaga), - takeLatest( - ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS, - executePageLoadActionsSaga, - ), takeLatest(ReduxActionTypes.MOVE_ACTION_INIT, moveActionSaga), takeLatest(ReduxActionTypes.COPY_ACTION_INIT, copyActionSaga), takeLatest( diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts index 965ca7d2f9..d1f3ad4398 100644 --- a/app/client/src/sagas/ApiPaneSagas.ts +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -2,17 +2,7 @@ * Handles the Api pane ui state. It looks into the routing based on actions too * */ import _ from "lodash"; -import { - all, - select, - put, - takeEvery, - take, - call, - race, - delay, -} from "redux-saga/effects"; -import { getFormSyncErrors } from "redux-form"; +import { all, select, put, takeEvery, take, call } from "redux-saga/effects"; import { ReduxAction, ReduxActionErrorTypes, @@ -46,36 +36,20 @@ import { getDataSources, } from "selectors/editorSelectors"; import { initialize, autofill, change } from "redux-form"; -import { getAction } from "./ActionSagas"; import { AppState } from "reducers"; import { Property } from "api/ActionAPI"; import { changeApi, setDatasourceFieldText } from "actions/apiPaneActions"; -import { - FIELD_REQUIRED_ERROR, - UNIQUE_NAME_ERROR, - VALID_FUNCTION_NAME_ERROR, -} from "constants/messages"; import { createNewApiName, getNextEntityName } from "utils/AppsmithUtils"; import { getPluginIdOfPackageName } from "sagas/selectors"; -import { getActions, getPlugins } from "selectors/entitiesSelector"; +import { getAction, getActions, getPlugins } from "selectors/entitiesSelector"; import { ActionData } from "reducers/entityReducers/actionsReducer"; -import { createActionRequest } from "actions/actionActions"; +import { createActionRequest, setActionProperty } from "actions/actionActions"; import { Datasource } from "api/DatasourcesApi"; import { Plugin } from "api/PluginApi"; import { PLUGIN_PACKAGE_DBS } from "constants/QueryEditorConstants"; import { RestAction } from "entities/Action"; -import { isDynamicValue } from "utils/DynamicBindingUtils"; import { getCurrentOrgId } from "selectors/organizationSelectors"; -const getApiDraft = (state: AppState, id: string) => { - const drafts = state.entities.actionDrafts; - if (id in drafts) return drafts[id]; - return {}; -}; - -const getActionConfigs = (state: AppState): ActionData["config"][] => - state.entities.actions.map(a => a.config); - const getLastUsedAction = (state: AppState) => state.ui.apiPane.lastUsed; const getLastUsedEditorPage = (state: AppState) => state.ui.apiPane.lastUsedEditorPage; @@ -229,25 +203,20 @@ function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) { } const action = yield select(getAction, id); if (!action) return; - const draft = yield select(getApiDraft, id); - let data; - if (_.isEmpty(draft)) { - data = action; - } else { - data = draft; - } - - yield put(initialize(API_EDITOR_FORM_NAME, data)); + yield put(initialize(API_EDITOR_FORM_NAME, action)); history.push(API_EDITOR_ID_URL(applicationId, pageId, id)); yield call(initializeExtraFormDataSaga); - if (data.actionConfiguration && data.actionConfiguration.queryParameters) { + if ( + action.actionConfiguration && + action.actionConfiguration.queryParameters?.length + ) { // Sync the api params my mocking a change action yield call(syncApiParamsSaga, { type: ReduxFormActionTypes.ARRAY_REMOVE, - payload: data.actionConfiguration.queryParameters, + payload: action.actionConfiguration.queryParameters, meta: { field: "actionConfiguration.queryParameters", }, @@ -255,82 +224,15 @@ function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) { } } -function* updateDraftsSaga() { - // debounce - // TODO check for save - const result = yield race({ - change: take(ReduxFormActionTypes.VALUE_CHANGE), - timeout: delay(300), - }); - if (result.timeout) { - const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); - if (!values.id) return; - const action = yield select(getAction, values.id); - - if (_.isEqual(values, action)) { - yield put({ - type: ReduxActionTypes.DELETE_API_DRAFT, - payload: { id: values.id }, - }); - } else { - yield put({ - type: ReduxActionTypes.UPDATE_API_DRAFT, - payload: { id: values.id, draft: values }, - }); - } - } -} - -function* validateInputSaga() { - const errors = {}; - const existingErrors = yield select(getFormSyncErrors); - const actions: RestAction[] = yield select(getActionConfigs); - const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); - - // Name field validation - let hasSameName = false; - const sameNames = actions.filter( - (action: RestAction) => action.name === values.name && action.id, - ); - if ( - sameNames.length > 1 || - (sameNames.length === 1 && sameNames[0].id !== values.id) - ) { - hasSameName = true; - } - if (!_.trim(values.name)) { - _.set(errors, "name", FIELD_REQUIRED_ERROR); - } else if (values.name.indexOf(" ") !== -1) { - _.set(errors, "name", VALID_FUNCTION_NAME_ERROR); - } else if (hasSameName) { - _.set(errors, "name", UNIQUE_NAME_ERROR); - } else { - _.unset(errors, "name"); - } - - if (existingErrors !== errors) { - yield put({ - type: ReduxFormActionTypes.UPDATE_FIELD_ERROR, - meta: { - form: API_EDITOR_FORM_NAME, - }, - payload: { - syncErrors: errors, - }, - }); - } -} - function* updateFormFields( actionPayload: ReduxActionWithMeta, ) { const field = actionPayload.meta.field; const value = actionPayload.payload; - const formData = yield select(getFormData, API_EDITOR_FORM_NAME); + const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); if (field === "actionConfiguration.httpMethod") { if (value !== "GET") { - const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); const { actionConfiguration } = values; const actionConfigurationHeaders = actionConfiguration.headers; let contentType; @@ -354,12 +256,11 @@ function* updateFormFields( } } } else if (field.includes("actionConfiguration.headers")) { - const formValues = formData.values; const actionConfigurationHeaders = _.get( - formValues, + values, "actionConfiguration.headers", ); - const apiId = _.get(formValues, "id"); + const apiId = _.get(values, "id"); let displayFormat; if (actionConfigurationHeaders) { @@ -389,41 +290,35 @@ function* updateFormFields( } } -function* updateDynamicBindingsSaga( - actionPayload: ReduxActionWithMeta, -) { - const field = actionPayload.meta.field.replace("actionConfiguration.", ""); - const value = actionPayload.payload; - const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); - if (!values.id) return; - - const isDynamic = isDynamicValue(value); - let dynamicBindings: Property[] = values.dynamicBindingPathList || []; - const fieldExists = _.some(dynamicBindings, { key: field }); - - if (!isDynamic && fieldExists) { - dynamicBindings = dynamicBindings.filter(d => d.key !== field); - } - if (isDynamic && !fieldExists) { - dynamicBindings.push({ key: field }); - } - if (dynamicBindings !== values.dynamicBindingPathList) { - yield put( - change(API_EDITOR_FORM_NAME, "dynamicBindingPathList", dynamicBindings), - ); - } -} - function* formValueChangeSaga( actionPayload: ReduxActionWithMeta, ) { const { form, field } = actionPayload.meta; if (form !== API_EDITOR_FORM_NAME) return; if (field === "dynamicBindingPathList") return; + const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); + if (!values.id) return; + if ( + actionPayload.type === ReduxFormActionTypes.ARRAY_REMOVE || + actionPayload.type === ReduxFormActionTypes.ARRAY_PUSH + ) { + const value = _.get(values, field); + setActionProperty({ + actionId: values.id, + propertyName: field, + value, + }); + } else { + yield put( + setActionProperty({ + actionId: values.id, + propertyName: field, + value: actionPayload.payload, + }), + ); + } + yield all([ - call(updateDynamicBindingsSaga, actionPayload), - call(validateInputSaga), - call(updateDraftsSaga), call(syncApiParamsSaga, actionPayload), call(updateFormFields, actionPayload), ]); @@ -442,25 +337,10 @@ function* handleActionCreatedSaga(actionPayload: ReduxAction) { } } -function* handleActionUpdatedSaga( - actionPayload: ReduxAction<{ data: RestAction }>, -) { - const { id } = actionPayload.payload.data; - yield put({ - type: ReduxActionTypes.DELETE_API_DRAFT, - payload: { id }, - }); -} - -function* handleActionDeletedSaga(actionPayload: ReduxAction<{ id: string }>) { - const { id } = actionPayload.payload; +function* handleActionDeletedSaga() { const applicationId = yield select(getCurrentApplicationId); const pageId = yield select(getCurrentPageId); history.push(API_EDITOR_URL(applicationId, pageId)); - yield put({ - type: ReduxActionTypes.DELETE_API_DRAFT, - payload: { id }, - }); } function* handleMoveOrCopySaga(actionPayload: ReduxAction<{ id: string }>) { @@ -571,7 +451,6 @@ export default function* root() { takeEvery(ReduxActionTypes.INIT_API_PANE, initApiPaneSaga), takeEvery(ReduxActionTypes.API_PANE_CHANGE_API, changeApiSaga), takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleActionCreatedSaga), - takeEvery(ReduxActionTypes.UPDATE_ACTION_SUCCESS, handleActionUpdatedSaga), takeEvery(ReduxActionTypes.DELETE_ACTION_SUCCESS, handleActionDeletedSaga), takeEvery(ReduxActionTypes.MOVE_ACTION_SUCCESS, handleMoveOrCopySaga), takeEvery(ReduxActionTypes.COPY_ACTION_SUCCESS, handleMoveOrCopySaga), diff --git a/app/client/src/sagas/BatchSagas.tsx b/app/client/src/sagas/BatchSagas.tsx index ab50dbd3d0..8afcc5379a 100644 --- a/app/client/src/sagas/BatchSagas.tsx +++ b/app/client/src/sagas/BatchSagas.tsx @@ -24,6 +24,10 @@ const BATCH_PRIORITY = { priority: 1, needsSaga: true, }, + [ReduxActionTypes.UPDATE_ACTION_PROPERTY]: { + priority: 1, + needsSaga: false, + }, }; const batches: ReduxAction[][] = []; diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts index dcc3bd9f49..2a8d61e7cd 100644 --- a/app/client/src/sagas/QueryPaneSagas.ts +++ b/app/client/src/sagas/QueryPaneSagas.ts @@ -27,38 +27,20 @@ import { getCurrentPageId, } from "selectors/editorSelectors"; import { change, initialize } from "redux-form"; -import { - extractBindingsFromAction, - getAction, - getActionParams, - getActionTimeout, -} from "./ActionSagas"; import { AppState } from "reducers"; -import ActionAPI, { - PaginationField, - ExecuteActionRequest, - ActionApiResponse, - Property, -} from "api/ActionAPI"; +import ActionAPI, { Property } from "api/ActionAPI"; import { QUERY_CONSTANT } from "constants/QueryEditorConstants"; import { changeQuery, deleteQuerySuccess } from "actions/queryPaneActions"; import { AppToaster } from "components/editorComponents/ToastComponent"; import { ToastType } from "react-toastify"; -import { PageAction } from "constants/ActionConstants"; import { isDynamicValue } from "utils/DynamicBindingUtils"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { GenericApiResponse } from "api/ApiResponses"; import { validateResponse } from "./ErrorSagas"; -import { getQueryName } from "selectors/entitiesSelector"; -import { QueryAction, RestAction } from "entities/Action"; +import { getAction, getQueryName } from "selectors/entitiesSelector"; +import { RestAction } from "entities/Action"; import { updateAction } from "actions/actionActions"; -const getQueryDraft = (state: AppState, id: string) => { - const drafts = state.entities.actionDrafts; - if (id in drafts) return drafts[id]; - return {}; -}; - const getActions = (state: AppState) => state.entities.actions.map(a => a.config); @@ -117,34 +99,19 @@ function* changeQuerySaga( return; } - const draft = yield select(getQueryDraft, id); - const data = _.isEmpty(draft) ? action : draft; const URL = QUERIES_EDITOR_ID_URL(applicationId, pageId, id); - yield put(initialize(QUERY_EDITOR_FORM_NAME, data)); + yield put(initialize(QUERY_EDITOR_FORM_NAME, action)); history.push(URL); } function* saveQueryAction() { const { values } = yield select(getFormData, QUERY_EDITOR_FORM_NAME); if (!values.id) return; - const action = yield select(getAction, values.id); - if (_.isEqual(values, action)) { - yield put({ - type: ReduxActionTypes.DELETE_API_DRAFT, - payload: { id: values.id }, - }); - } else { - yield put({ - type: ReduxActionTypes.UPDATE_API_DRAFT, - payload: { id: values.id, draft: values }, - }); - - yield put( - updateAction({ - data: values, - }), - ); - } + yield put( + updateAction({ + data: values, + }), + ); } function* updateDynamicBindingsSaga( @@ -223,88 +190,7 @@ function* handleMoveOrCopySaga(actionPayload: ReduxAction<{ id: string }>) { } } -export function* executeQuerySaga( - actionPayload: ReduxAction<{ - action: QueryAction; - actionId: string; - paginationField: PaginationField; - }>, -) { - try { - const { - values, - dirty, - }: { - values: QueryAction; - dirty: boolean; - valid: boolean; - } = yield select(getFormData, QUERY_EDITOR_FORM_NAME); - const actionObject: PageAction = yield select(getAction, values.id); - let action: ExecuteActionRequest["action"] = { id: values.id }; - let jsonPathKeys = actionObject.jsonPathKeys; - - if (dirty) { - action = _.omit(values, "id") as QueryAction; - jsonPathKeys = extractBindingsFromAction(action as QueryAction); - } - - const { paginationField } = actionPayload.payload; - - const params = yield call(getActionParams, jsonPathKeys); - const timeout = yield select(getActionTimeout, values.id); - const response: ActionApiResponse = yield ActionAPI.executeAction( - { - action, - params, - paginationField, - }, - timeout, - ); - const isValidResponse = yield validateResponse(response); - const isExecutionSuccess = response.data.isExecutionSuccess; - - if (!isExecutionSuccess) { - throw Error(response.data.body.toString()); - } - - if (!response.data.body) { - throw Error("An unexpected error occurred."); - } - - if (isValidResponse) { - yield put({ - type: ReduxActionTypes.RUN_QUERY_SUCCESS, - payload: { - data: response.data, - actionId: actionPayload.payload.actionId, - }, - }); - AppToaster.show({ - message: "Query ran successfully", - type: ToastType.SUCCESS, - }); - AnalyticsUtil.logEvent("RUN_QUERY", { - queryName: actionPayload.payload.action.name, - }); - } - } catch (error) { - yield put({ - type: ReduxActionErrorTypes.RUN_QUERY_ERROR, - payload: { - actionId: actionPayload.payload.actionId, - message: error.message, - show: false, - }, - }); - - AppToaster.show({ - message: error.message, - type: ToastType.ERROR, - }); - } -} - -export function* deleteQuerySaga(actionPayload: ReduxAction<{ id: string }>) { +function* deleteQuerySaga(actionPayload: ReduxAction<{ id: string }>) { try { const id = actionPayload.payload.id; const response: GenericApiResponse = yield ActionAPI.deleteAction( @@ -337,7 +223,6 @@ export default function* root() { takeEvery(ReduxActionTypes.DELETE_QUERY_SUCCESS, handleQueryDeletedSaga), takeEvery(ReduxActionTypes.MOVE_ACTION_SUCCESS, handleMoveOrCopySaga), takeEvery(ReduxActionTypes.COPY_ACTION_SUCCESS, handleMoveOrCopySaga), - takeLatest(ReduxActionTypes.EXECUTE_QUERY_REQUEST, executeQuerySaga), takeEvery(ReduxActionTypes.QUERY_PANE_CHANGE, changeQuerySaga), takeEvery(ReduxActionTypes.INIT_QUERY_PANE, initQueryPaneSaga), // Intercepting the redux-form change actionType diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index 483481fefe..2fcbc815cb 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -2,6 +2,7 @@ import { all, spawn } from "redux-saga/effects"; import pageSagas from "sagas/PageSagas"; import { fetchWidgetCardsSaga } from "./WidgetSidebarSagas"; import { watchActionSagas } from "./ActionSagas"; +import { watchActionExecutionSagas } from "sagas/ActionExecutionSagas"; import widgetOperationSagas from "./WidgetOperationSagas"; import errorSagas from "./ErrorSagas"; import configsSagas from "./ConfigsSagas"; @@ -25,6 +26,7 @@ export function* rootSaga() { spawn(pageSagas), spawn(fetchWidgetCardsSaga), spawn(watchActionSagas), + spawn(watchActionExecutionSagas), spawn(widgetOperationSagas), spawn(errorSagas), spawn(configsSagas), diff --git a/app/client/src/selectors/dataTreeSelectors.ts b/app/client/src/selectors/dataTreeSelectors.ts index ee05562d9c..fb41df809a 100644 --- a/app/client/src/selectors/dataTreeSelectors.ts +++ b/app/client/src/selectors/dataTreeSelectors.ts @@ -1,5 +1,5 @@ import { createSelector } from "reselect"; -import { getActionDrafts, getActionsForCurrentPage } from "./entitiesSelector"; +import { getActionsForCurrentPage } from "./entitiesSelector"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; import { getEvaluatedDataTree } from "utils/DynamicBindingUtils"; import { DataTree, DataTreeFactory } from "entities/DataTree/dataTreeFactory"; @@ -40,16 +40,14 @@ import { getPageList } from "./appViewSelectors"; export const getUnevaluatedDataTree = (withFunctions?: boolean) => createSelector( getActionsForCurrentPage, - getActionDrafts, getWidgets, getWidgetsMeta, getPageList, - (actions, actionDrafts, widgets, widgetsMeta, pageListPayload) => { + (actions, widgets, widgetsMeta, pageListPayload) => { const pageList = pageListPayload || []; return DataTreeFactory.create( { actions, - actionDrafts, widgets, widgetsMeta, pageList, diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 3c4a10c640..151d9afc11 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -1,14 +1,16 @@ import { AppState } from "reducers"; import { - ActionDataState, ActionData, + ActionDataState, } from "reducers/entityReducers/actionsReducer"; import { ActionResponse } from "api/ActionAPI"; import { QUERY_CONSTANT } from "constants/QueryEditorConstants"; -import { API_CONSTANT } from "constants/ApiEditorConstants"; +import { PLUGIN_TYPE_API } from "constants/ApiEditorConstants"; import { createSelector } from "reselect"; import { Page } from "constants/ReduxActionConstants"; import { Datasource } from "api/DatasourcesApi"; +import { Action } from "entities/Action"; +import { find } from "lodash"; export const getEntities = (state: AppState): AppState["entities"] => state.entities; @@ -124,12 +126,6 @@ export const getDatasourceDraft = (state: AppState, id: string) => { export const getPlugins = (state: AppState) => state.entities.plugins.list; -export const getApiActions = (state: AppState): ActionDataState => { - return state.entities.actions.filter((action: ActionData) => { - return action.config.pluginType === API_CONSTANT; - }); -}; - export const getQueryName = (state: AppState, actionId: string): string => { const action = state.entities.actions.find((action: ActionData) => { return action.config.id === actionId; @@ -138,14 +134,6 @@ export const getQueryName = (state: AppState, actionId: string): string => { return action?.config.name ?? ""; }; -export const getPageName = (state: AppState, pageId: string): string => { - const page = state.entities.pageList.pages.find((page: Page) => { - return page.pageId === pageId; - }); - - return page?.pageName ?? ""; -}; - export const getQueryActions = (state: AppState): ActionDataState => { return state.entities.actions.filter((action: ActionData) => { return action.config.pluginType === QUERY_CONSTANT; @@ -167,8 +155,6 @@ export const getActionsForCurrentPage = createSelector( }, ); -export const getActionDrafts = (state: AppState) => state.entities.actionDrafts; - export const getActionResponses = createSelector(getActions, actions => { const responses: Record = {}; @@ -178,3 +164,48 @@ export const getActionResponses = createSelector(getActions, actions => { return responses; }); +export const getAction = ( + state: AppState, + actionId: string, +): Action | undefined => { + const action = find(state.entities.actions, a => a.config.id === actionId); + return action ? action.config : undefined; +}; + +export function getCurrentPageNameByActionId( + state: AppState, + actionId: string, +): string { + const action = state.entities.actions.find(action => { + return action.config.id === actionId; + }); + const pageId = action ? action.config.pageId : ""; + return getPageNameByPageId(state, pageId); +} + +export function getPageNameByPageId(state: AppState, pageId: string): string { + const page = state.entities.pageList.pages.find( + page => page.pageId === pageId, + ); + return page ? page.pageName : ""; +} + +const getQueryPaneSavingMap = (state: AppState) => state.ui.queryPane.isSaving; +const getApiPaneSavingMap = (state: AppState) => state.ui.apiPane.isSaving; +const getActionDirtyState = (state: AppState) => state.ui.apiPane.isDirty; + +export const isActionSaving = (id: string) => + createSelector( + [getQueryPaneSavingMap, getApiPaneSavingMap], + (querySavingMap, apiSavingsMap) => { + return ( + (id in querySavingMap && querySavingMap[id]) || + (id in apiSavingsMap && apiSavingsMap[id]) + ); + }, + ); + +export const isActionDirty = (id: string) => + createSelector([getActionDirtyState], actionDirtyMap => { + return id in actionDirtyMap && actionDirtyMap[id]; + }); diff --git a/app/client/src/selectors/formSelectors.ts b/app/client/src/selectors/formSelectors.ts index 87b62f6d69..7a7fcccb63 100644 --- a/app/client/src/selectors/formSelectors.ts +++ b/app/client/src/selectors/formSelectors.ts @@ -6,15 +6,13 @@ import { ActionData } from "reducers/entityReducers/actionsReducer"; type GetFormData = ( state: AppState, formName: string, -) => { values: object; dirty: boolean; valid: boolean }; +) => { values: object; valid: boolean }; export const getFormData: GetFormData = (state, formName) => { const initialValues = getFormInitialValues(formName)(state) as RestAction; const values = getFormValues(formName)(state) as RestAction; - const drafts = state.entities.actionDrafts; - const dirty = values.id in drafts; const valid = isValid(formName)(state); - return { initialValues, values, dirty, valid }; + return { initialValues, values, valid }; }; export const getApiName = (state: AppState, id: string) => { diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 92fd03c2cf..fea780637d 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -20,8 +20,9 @@ import equal from "fast-deep-equal/es6"; import WidgetFactory from "utils/WidgetFactory"; import { AppToaster } from "components/editorComponents/ToastComponent"; import { ToastType } from "react-toastify"; +import { Action } from "entities/Action"; -export const removeBindingsFromObject = (obj: object) => { +export const removeBindingsFromActionObject = (obj: Action) => { const string = JSON.stringify(obj); const withBindings = string.replace(DATA_BIND_REGEX_GLOBAL, "{{ }}"); return JSON.parse(withBindings);