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
This commit is contained in:
Hetu Nandu 2020-07-03 14:28:58 +05:30 committed by GitHub
parent 10b1e40acd
commit 4a6717889c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 769 additions and 1027 deletions

View File

@ -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();
});
});

View File

@ -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");

View File

@ -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}}", {

View File

@ -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")

View File

@ -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"
}
}

View File

@ -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");

View File

@ -182,4 +182,4 @@
"pre-commit": "lint-staged"
}
}
}
}

View File

@ -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<Action>) => {
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,

View File

@ -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,

View File

@ -155,7 +155,7 @@ class CodeEditor extends Component<Props, State> {
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<Props, State> {
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) {

View File

@ -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<Props> {
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<Props> {
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(

View File

@ -30,7 +30,7 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => {
}, [props.fields, props.pushFields]);
return (
<React.Fragment>
{typeof props.fields.getAll() === "object" && (
{props.fields.length && (
<React.Fragment>
{props.fields.map((field: any, index: number) => {
const otherProps: Record<string, any> = {};

View File

@ -25,7 +25,7 @@ export const DEFAULT_API_ACTION: Partial<RestAction> = {
},
};
export const API_CONSTANT = "API";
export const PLUGIN_TYPE_API = "API";
export const DEFAULT_PROVIDER_OPTION = "Business Software";
export const CONTENT_TYPE = "content-type";

View File

@ -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",

View File

@ -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<T> = {
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 (

View File

@ -2,7 +2,10 @@ import { Datasource } from "api/DatasourcesApi";
export type EmbeddedDatasource = Omit<Datasource, "id">;
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,
});

View File

@ -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

View File

@ -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 })),

View File

@ -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<Props> {
this.props.initApiPane(this.props.match.params.apiId);
}
shouldComponentUpdate(nextProps: Readonly<Props>): 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<Props> {
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 (
<EditorSidebar
isLoading={isFetching}
list={data}
selectedItemId={apiId}
draftIds={Object.keys(actionDrafts)}
itemRender={this.renderItem}
onItemCreateClick={this.handleCreateNewApiClick}
onItemSelected={this.handleApiChange}
@ -225,7 +213,6 @@ class ApiSidebar extends React.Component<Props> {
const mapStateToProps = (state: AppState): ReduxStateProps => ({
actions: state.entities.actions,
actionDrafts: state.entities.actionDrafts,
apiPane: state.ui.apiPane,
pages: state.entities.pageList.pages,
});

View File

@ -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)<IIconProps>`
&&& {
outline: none;
@ -192,7 +186,6 @@ type EditorSidebarComponentProps = {
isLoading: boolean;
list: Array<Item>;
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<Props, State> {
isLoading,
itemRender,
selectedItemId,
draftIds,
location,
createButtonTitle,
} = this.props;
@ -404,11 +396,6 @@ class EditorSidebar extends React.Component<Props, State> {
{this.state.itemDragging !==
item.id && (
<React.Fragment>
<DraftIconIndicator
isHidden={
draftIds.indexOf(item.id) === -1
}
/>
<TreeDropdown
defaultText=""
onSelect={() => {

View File

@ -35,6 +35,10 @@ class JSONOutput extends React.Component<Props> {
collapsed: 1,
};
if (typeof src !== "object") {
return <OutputContainer>{src}</OutputContainer>;
}
if (!src.length) {
return (
<OutputContainer>

View File

@ -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<string> | undefined;
submitForm: (name: string) => void;
@ -97,7 +95,6 @@ class QueryEditor extends React.Component<Props> {
pluginIds,
executedQueryData,
selectedPluginPackage,
actionDrafts,
isCreating,
runErrorMessage,
} = this.props;
@ -130,7 +127,7 @@ class QueryEditor extends React.Component<Props> {
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));
},

View File

@ -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<Props> {
this.props.initQueryPane(QUERY_CONSTANT, this.props.match.params.queryId);
}
shouldComponentUpdate(nextProps: Readonly<Props>): 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<Props> {
render() {
const {
actionDrafts,
apiPane: { isFetching },
match: {
params: { queryId },
@ -196,7 +183,6 @@ class QuerySidebar extends React.Component<Props> {
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<Props> {
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),

View File

@ -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<string, Action>;
const initialState: ActionDraftsState = {};
const actionDraftsReducer = createReducer(initialState, {
[ReduxActionTypes.UPDATE_API_DRAFT]: (
state: ApiPaneReduxState,
action: ReduxAction<{ id: string; draft: Partial<RestAction> }>,
) => ({
...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;

View File

@ -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<UpdateActionPropertyActionPayload>,
) =>
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<string>,
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 =>

View File

@ -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,

View File

@ -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;

View File

@ -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<string, boolean>;
isSaving: Record<string, boolean>;
isDeleting: Record<string, boolean>;
isDirty: Record<string, boolean>;
currentCategory: string;
lastUsedEditorPage: string;
datasourceFieldText: Record<string, string>;
@ -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<UpdateActionPropertyActionPayload>,
) => ({
...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,

View File

@ -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,
},
};
},

View File

@ -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<string, string> },
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<string, string> = {};
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<any>,
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<ExecuteActionPayload>) {
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<PageAction[][]>) {
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,
),
]);
}

View File

@ -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<string, string> = {};
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<string, string> },
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<any>,
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<ExecuteActionPayload>) {
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<RestAction>) {
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<PageAction[][]>) {
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<SetActionPropertyPayload>) {
const { actionId, value, propertyName } = action.payload;
if (!actionId) return;
const actionObj = yield select(getAction, actionId);
const effects: Record<string, any> = {};
// 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(

View File

@ -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<string, { field: string }>,
) {
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<string, { field: string }>,
) {
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<string, { field: string; form: string }>,
) {
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<RestAction>) {
}
}
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),

View File

@ -24,6 +24,10 @@ const BATCH_PRIORITY = {
priority: 1,
needsSaga: true,
},
[ReduxActionTypes.UPDATE_ACTION_PROPERTY]: {
priority: 1,
needsSaga: false,
},
};
const batches: ReduxAction<any>[][] = [];

View File

@ -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<RestAction> = 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

View File

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

View File

@ -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,

View File

@ -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<string, ActionResponse | undefined> = {};
@ -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];
});

View File

@ -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) => {

View File

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