Merge branch 'fix/api-pane-p0' into 'release'

Fix various api pane bugs

See merge request theappsmith/internal-tools-client!136
This commit is contained in:
Hetu Nandu 2019-11-13 07:34:59 +00:00
commit 5886bccbdc
25 changed files with 533 additions and 349 deletions

View File

@ -2,7 +2,7 @@ import { ReduxActionTypes } from "../constants/ReduxActionConstants";
import { RestAction } from "../api/ActionAPI";
import { ActionPayload } from "../constants/ActionConstants";
export const createActionRequest = (payload: RestAction) => {
export const createActionRequest = (payload: Partial<RestAction>) => {
return {
type: ReduxActionTypes.CREATE_ACTION_INIT,
payload,

View File

@ -5,7 +5,7 @@ import {
} from "../constants/ReduxActionConstants";
import { NamePathBindingMap } from "../constants/BindingsConstants";
export const createUpdateBindingsMap = (): ReduxActionWithoutPayload => ({
export const initBindingMapListener = (): ReduxActionWithoutPayload => ({
type: ReduxActionTypes.CREATE_UPDATE_BINDINGS_MAP_INIT,
});

View File

@ -6,7 +6,7 @@ import {
REQUEST_HEADERS,
AUTH_CREDENTIALS,
} from "../constants/ApiConstants";
import { ActionApiResponse, ActionResponse } from "./ActionAPI";
import { ActionApiResponse } from "./ActionAPI";
const axiosInstance = axios.create({
baseURL: BASE_URL,

View File

@ -1,24 +1,40 @@
import React from "react";
import styled, { css } from "styled-components";
import styled from "styled-components";
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
import { FormGroup, IconName, InputGroup, Intent } from "@blueprintjs/core";
import { ComponentProps } from "./BaseComponent";
const InputStyles = css`
padding: ${props => `${props.theme.spaces[3]}px ${props.theme.spaces[1]}px`};
const TextInput = styled(InputGroup)`
flex: 1;
border: 1px solid ${props => props.theme.colors.inputInactiveBorders};
border-radius: 4px;
height: 32px;
background-color: ${props => props.theme.colors.textOnDarkBG};
&:focus {
border-color: ${props => props.theme.colors.secondary};
input {
border: 1px solid ${props => props.theme.colors.inputInactiveBorders};
border-radius: 4px;
box-shadow: none;
height: 38px;
background-color: ${props => props.theme.colors.textOnDarkBG};
outline: 0;
&:focus {
border-color: ${props => props.theme.colors.secondary};
background-color: ${props => props.theme.colors.textOnDarkBG};
outline: 0;
}
}
.bp3-icon {
border-radius: 4px 0 0 4px;
margin: 0;
height: 38px;
width: 30px;
background-color: ${props => props.theme.colors.menuButtonBGInactive};
display: flex;
align-items: center;
justify-content: center;
svg {
height: 20px;
width: 20px;
path {
fill: ${props => props.theme.colors.textDefault};
}
}
}
`;
const Input = styled.input`
${InputStyles}
`;
const InputContainer = styled.div`
@ -27,32 +43,26 @@ const InputContainer = styled.div`
flex-direction: column;
`;
const Error = styled.span`
const ErrorText = styled.span`
height: 10px;
padding: 3px;
font-size: 10px;
color: ${props => props.theme.colors.error};
fontsize: ${props => props.theme.fontSizes[1]};
`;
const TextArea = styled.textarea`
${InputStyles}
height: 100px;
`;
export interface TextInputProps {
placeholderMessage?: string;
multiline?: boolean;
input?: WrappedFieldInputProps;
input?: Partial<WrappedFieldInputProps>;
meta?: WrappedFieldMetaProps;
icon?: IconName;
}
export const BaseTextInput = (props: TextInputProps) => {
const { placeholderMessage, multiline, input, meta } = props;
if (multiline) {
return <TextArea placeholder={placeholderMessage} {...input} />;
}
const { placeholderMessage, input, meta, icon } = props;
return (
<InputContainer>
<Input placeholder={placeholderMessage} {...input} />
{meta && meta.touched && meta.error && <Error>{meta.error}</Error>}
<TextInput {...input} placeholder={placeholderMessage} leftIcon={icon} />
<ErrorText>{meta && meta.touched && meta.error}</ErrorText>
</InputContainer>
);
};

View File

@ -3,16 +3,23 @@ import { AnchorButton, IButtonProps, MaybeElement } from "@blueprintjs/core";
import styled, { css } from "styled-components";
import { TextComponentProps } from "./TextComponent";
import { ButtonStyle } from "../../../widgets/ButtonWidget";
import { Theme } from "../../../constants/DefaultTheme";
const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => {
if (props.filled) return props.theme.colors.textOnDarkBG;
if (props.styleName) {
if (props.styleName === "secondary") {
return props.theme.colors.OXFORD_BLUE;
}
return props.theme.colors[props.styleName];
}
};
const ButtonColorStyles = css<ButtonStyleProps>`
color: ${props => {
if (props.filled) return props.theme.colors.textOnDarkBG;
if (props.styleName) {
if (props.styleName === "secondary")
return props.theme.colors.OXFORD_BLUE;
return props.theme.colors[props.styleName];
}
}};
color: ${getButtonColorStyles};
svg {
fill: ${getButtonColorStyles};
}
`;
const ButtonWrapper = styled(AnchorButton)<ButtonStyleProps>`

View File

@ -153,7 +153,7 @@ const ApiResponseView = (props: Props) => {
const mapStateToProps = (state: AppState): ReduxStateProps => ({
responses: state.entities.apiData,
isRunning: state.entities.actions.isRunning,
isRunning: state.ui.apiPane.isRunning,
});
export default connect(mapStateToProps)(withRouter(ApiResponseView));

View File

@ -7,7 +7,6 @@ import { DatasourceDataState } from "../../../reducers/entityReducers/datasource
import _ from "lodash";
import { createDatasource } from "../../../actions/datasourcesActions";
import { REST_PLUGIN_ID } from "../../../constants/ApiEditorConstants";
import { required } from "../../../utils/validation/common";
interface ReduxStateProps {
datasources: DatasourceDataState;
@ -41,7 +40,6 @@ const DatasourcesField = (
onCreateOption={props.createDatasource}
format={(value: string) => _.find(options, { value })}
parse={(option: { value: string }) => (option ? option.value : null)}
validate={required}
/>
);
};

View File

@ -3,6 +3,7 @@ import { RefObject } from "react";
export const ReduxActionTypes: { [key: string]: string } = {
INIT_EDITOR: "INIT_EDITOR",
INIT_SUCCESS: "INIT_SUCCESS",
REPORT_ERROR: "REPORT_ERROR",
FLUSH_ERRORS: "FLUSH_ERRORS",
UPDATE_CANVAS: "UPDATE_CANVAS",
@ -95,8 +96,10 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
PROPERTY_PANE_ERROR: "PROPERTY_PANE_ERROR",
FETCH_ACTIONS_ERROR: "FETCH_ACTIONS_ERROR",
UPDATE_WIDGET_PROPERTY_ERROR: "UPDATE_WIDGET_PROPERTY_ERROR",
CREATE_ACTION_ERROR: "CREATE_ACTION_ERROR",
UPDATE_ACTION_ERROR: "UPDATE_ACTION_ERROR",
DELETE_ACTION_ERROR: "DELETE_ACTION_ERROR",
EXECUTE_ACTION_ERROR: "EXECUTE_ACTION_ERROR",
FETCH_DATASOURCES_ERROR: "FETCH_DATASOURCES_ERROR",
CREATE_DATASOURCE_ERROR: "CREATE_DATASOURCE_ERROR",
FETCH_PUBLISHED_PAGE_ERROR: "FETCH_PUBLISHED_PAGE_ERROR",

View File

@ -40,23 +40,16 @@ const Form = styled.form`
}
`;
const MainConfiguration = styled.div`
padding-top: 10px;
`;
const SecondaryWrapper = styled.div`
display: flex;
height: 100%;
border-top: 1px solid #d0d7dd;
`;
const ForwardSlash = styled.div`
&& {
margin: 0 10px;
height: 27px;
width: 1px;
background-color: #d0d7dd;
transform: rotate(27deg);
align-self: center;
}
`;
const RequestParamsWrapper = styled.div`
flex: 5;
border-right: 1px solid #d0d7dd;
@ -103,48 +96,50 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
} = props;
return (
<Form onSubmit={handleSubmit}>
<FormRow>
<TextField
name="name"
placeholderMessage="API Name"
validate={required}
/>
<ActionButtons>
<ActionButton
text="Delete"
styleName="error"
onClick={onDeleteClick}
loading={isDeleting}
<MainConfiguration>
<FormRow>
<TextField
name="name"
placeholderMessage="API Name *"
validate={required}
/>
<ActionButton
text="Run"
styleName="secondary"
onClick={onRunClick}
loading={isRunning}
<ActionButtons>
<ActionButton
text="Delete"
styleName="error"
onClick={onDeleteClick}
loading={isDeleting}
/>
<ActionButton
text="Run"
styleName="secondary"
onClick={onRunClick}
loading={isRunning}
/>
<ActionButton
text="Save"
styleName="primary"
filled
onClick={onSaveClick}
loading={isSaving}
/>
</ActionButtons>
</FormRow>
<FormRow>
<DropdownField
placeholder="Method"
name="actionConfiguration.httpMethod"
options={HTTP_METHOD_OPTIONS}
/>
<ActionButton
text="Save"
styleName="primary"
filled
onClick={onSaveClick}
loading={isSaving}
<DatasourcesField name="datasource.id" />
<TextField
placeholderMessage="API Path"
name="actionConfiguration.path"
validate={[apiPathValidation]}
icon="slash"
/>
</ActionButtons>
</FormRow>
<FormRow>
<DropdownField
placeholder="Method"
name="actionConfiguration.httpMethod"
options={HTTP_METHOD_OPTIONS}
/>
<DatasourcesField name="datasource.id" />
<ForwardSlash />
<TextField
placeholderMessage="API Path"
name="actionConfiguration.path"
validate={[required, apiPathValidation]}
/>
</FormRow>
</FormRow>
</MainConfiguration>
<SecondaryWrapper>
<RequestParamsWrapper>
<KeyValueFieldArray

View File

@ -14,11 +14,13 @@ import { AppState } from "../../reducers";
import { RouteComponentProps } from "react-router";
import { API_EDITOR_URL } from "../../constants/routes";
import { API_EDITOR_FORM_NAME } from "../../constants/forms";
import { FORM_INITIAL_VALUES } from "../../constants/ApiEditorConstants";
import { ActionDataState } from "../../reducers/entityReducers/actionsReducer";
import { ApiPaneReduxState } from "../../reducers/uiReducers/apiPaneReducer";
import styled from "styled-components";
interface ReduxStateProps {
actions: ActionDataState;
apiPane: ApiPaneReduxState;
formData: any;
}
interface ReduxActionProps {
@ -36,6 +38,13 @@ type Props = ReduxActionProps &
ReduxStateProps &
RouteComponentProps<{ id: string }>;
const EmptyStateContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
`;
class ApiEditor extends React.Component<Props> {
componentDidMount(): void {
const currentId = this.props.match.params.id;
@ -52,9 +61,6 @@ class ApiEditor extends React.Component<Props> {
componentDidUpdate(prevProps: Readonly<Props>): void {
const currentId = this.props.match.params.id;
if (!currentId && prevProps.match.params.id) {
this.props.initialize(API_EDITOR_FORM_NAME, FORM_INITIAL_VALUES);
}
if (currentId && currentId !== prevProps.match.params.id) {
const data = this.props.actions.data.filter(
action => action.id === currentId,
@ -90,24 +96,36 @@ class ApiEditor extends React.Component<Props> {
render() {
const {
actions: { isSaving, isRunning, isDeleting },
apiPane: { isSaving, isRunning, isDeleting },
match: {
params: { id },
},
} = this.props;
return (
<ApiEditorForm
isSaving={isSaving}
isRunning={isRunning}
isDeleting={isDeleting}
onSubmit={this.handleSubmit}
onSaveClick={this.handleSaveClick}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
/>
<React.Fragment>
{id ? (
<ApiEditorForm
isSaving={isSaving}
isRunning={isRunning}
isDeleting={isDeleting}
onSubmit={this.handleSubmit}
onSaveClick={this.handleSaveClick}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
/>
) : (
<EmptyStateContainer>
{"Create an api select from the list"}
</EmptyStateContainer>
)}
</React.Fragment>
);
}
}
const mapStateToProps = (state: AppState): ReduxStateProps => ({
actions: state.entities.actions,
apiPane: state.ui.apiPane,
formData: getFormValues(API_EDITOR_FORM_NAME)(state),
});

View File

@ -8,6 +8,10 @@ import { API_EDITOR_ID_URL, API_EDITOR_URL } from "../../constants/routes";
import { BaseButton } from "../../components/designSystems/blueprint/ButtonComponent";
import { FormIcons } from "../../icons/FormIcons";
import { Spinner } from "@blueprintjs/core";
import { ApiPaneReduxState } from "../../reducers/uiReducers/apiPaneReducer";
import { BaseTextInput } from "../../components/designSystems/appsmith/TextInputComponent";
import { TICK } from "@blueprintjs/icons/lib/esm/generated/iconNames";
import { createActionRequest } from "../../actions/actionActions";
const ApiSidebarWrapper = styled.div`
height: 100%;
@ -81,24 +85,81 @@ const CreateNewButton = styled(BaseButton)`
}
`;
const CreateApiWrapper = styled.div`
display: grid;
grid-template-columns: 4fr 1fr;
grid-gap: 5px;
height: 40px;
`;
interface ReduxStateProps {
actions: ActionDataState;
apiPane: ApiPaneReduxState;
}
type Props = ReduxStateProps & RouteComponentProps<{ id: string }>;
interface ReduxDispatchProps {
createAction: (name: string) => void;
}
type Props = ReduxStateProps &
ReduxDispatchProps &
RouteComponentProps<{ id: string }>;
type State = {
isCreating: boolean;
name: string;
};
class ApiSidebar extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isCreating: false,
name: "",
};
}
componentDidUpdate(prevProps: Readonly<Props>): void {
if (!prevProps.match.params.id && this.props.match.params.id) {
this.setState({
isCreating: false,
name: "",
});
}
}
class ApiSidebar extends React.Component<Props> {
handleCreateNew = () => {
const { history } = this.props;
history.push(API_EDITOR_URL);
this.setState({
isCreating: true,
name: "",
});
};
saveAction = () => {
if (this.state.name) {
this.props.createAction(this.state.name);
} else {
this.setState({
isCreating: false,
});
}
};
handleNameChange = (e: React.ChangeEvent<{ value: string }>) => {
const value = e.target.value;
this.setState({
name: value,
});
};
render() {
const { actions, history, match } = this.props;
const { actions, apiPane, history, match } = this.props;
const { isCreating } = this.state;
const activeActionId = match.params.id;
return (
<ApiSidebarWrapper>
{actions.isFetching && <Spinner size={30} />}
{apiPane.isFetching && <Spinner size={30} />}
<ApiItemsWrapper>
{actions.data.map(action => (
<ApiItem
@ -112,18 +173,30 @@ class ApiSidebar extends React.Component<Props> {
<ActionName>{action.name}</ActionName>
</ApiItem>
))}
{!activeActionId && !actions.isFetching && (
<ApiItem isSelected>
<HTTPMethod method="" />
<ActionName>New Api</ActionName>
</ApiItem>
)}
</ApiItemsWrapper>
<CreateNewButton
text="Create new API"
icon={FormIcons.ADD_NEW_ICON()}
onClick={this.handleCreateNew}
/>
{isCreating ? (
<CreateApiWrapper>
<BaseTextInput
input={{
value: this.state.name,
onChange: this.handleNameChange,
}}
/>
<BaseButton
icon={TICK}
styleName="primary"
text=""
onClick={this.saveAction}
filled
/>
</CreateApiWrapper>
) : (
<CreateNewButton
text="Create new API"
icon={FormIcons.ADD_NEW_ICON()}
onClick={this.handleCreateNew}
/>
)}
</ApiSidebarWrapper>
);
}
@ -131,6 +204,19 @@ class ApiSidebar extends React.Component<Props> {
const mapStateToProps = (state: AppState): ReduxStateProps => ({
actions: state.entities.actions,
apiPane: state.ui.apiPane,
});
export default connect(mapStateToProps)(ApiSidebar);
const mapDispatchToProps = (dispatch: Function): ReduxDispatchProps => ({
createAction: (name: string) =>
dispatch(
createActionRequest({
name,
}),
),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ApiSidebar);

View File

@ -11,11 +11,12 @@ const PageSelector = styled(DropdownComponent)`
flex: 2;
`;
const NotificationText = styled.div`
const LoadingContainer = styled.div`
display: flex;
justify-content: space-evenly;
justify-content: flex-end;
align-items: center;
flex-grow: 1;
margin: 0 10px;
`;
const PreviewPublishSection = styled.div`
@ -37,7 +38,7 @@ const StretchedBreadCrumb = styled(Breadcrumbs)`
`;
type EditorHeaderProps = {
notificationText?: string;
isSaving?: boolean;
pageName: string;
onPublish: React.FormEventHandler;
onCreatePage: (name: string) => void;
@ -83,9 +84,9 @@ export const EditorHeader = (props: EditorHeaderProps) => {
}}
/>
)}
<NotificationText>
<span>{props.notificationText}</span>
</NotificationText>
<LoadingContainer>
{props.isSaving ? "Saving..." : "All changed Saved"}
</LoadingContainer>
<PreviewPublishSection>
<BaseButton
onClick={props.onPublish}

View File

@ -72,7 +72,7 @@ class Editor extends Component<EditorProps> {
return (
<div>
<EditorHeader
notificationText={this.props.isSaving ? "Saving page..." : undefined}
isSaving={this.props.isSaving}
pageName={this.props.currentPageName}
onPublish={this.handlePublish}
onCreatePage={this.handleCreatePage}

View File

@ -8,25 +8,13 @@ import { RestAction } from "../../api/ActionAPI";
const initialState: ActionDataState = {
data: [],
isFetching: false,
isRunning: false,
isSaving: false,
isDeleting: false,
};
export interface ActionDataState {
data: RestAction[];
isFetching: boolean;
isRunning: boolean;
isSaving: boolean;
isDeleting: boolean;
}
const actionsReducer = createReducer(initialState, {
[ReduxActionTypes.FETCH_ACTIONS_INIT]: (state: ActionDataState) => ({
...state,
isFetching: true,
}),
[ReduxActionTypes.FETCH_ACTIONS_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<RestAction[]>,
@ -38,11 +26,6 @@ const actionsReducer = createReducer(initialState, {
[ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: (state: ActionDataState) => ({
...state,
data: [],
isFetching: false,
}),
[ReduxActionTypes.CREATE_ACTION_INIT]: (state: ActionDataState) => ({
...state,
isSaving: true,
}),
[ReduxActionTypes.CREATE_ACTION_SUCCESS]: (
state: ActionDataState,
@ -50,11 +33,6 @@ const actionsReducer = createReducer(initialState, {
) => ({
...state,
data: state.data.concat([action.payload]),
isSaving: false,
}),
[ReduxActionTypes.UPDATE_ACTION_INIT]: (state: ActionDataState) => ({
...state,
isSaving: true,
}),
[ReduxActionTypes.UPDATE_ACTION_SUCCESS]: (
state: ActionDataState,
@ -65,19 +43,6 @@ const actionsReducer = createReducer(initialState, {
if (d.id === action.payload.data.id) return action.payload.data;
return d;
}),
isSaving: false,
}),
[ReduxActionTypes.EXECUTE_ACTION]: (state: ActionDataState) => ({
...state,
isRunning: true,
}),
[ReduxActionTypes.EXECUTE_ACTION_SUCCESS]: (state: ActionDataState) => ({
...state,
isRunning: false,
}),
[ReduxActionTypes.DELETE_ACTION_INIT]: (state: ActionDataState) => ({
...state,
isDeleting: true,
}),
[ReduxActionTypes.DELETE_ACTION_SUCCESS]: (
state: ActionDataState,
@ -85,7 +50,6 @@ const actionsReducer = createReducer(initialState, {
) => ({
...state,
data: state.data.filter(d => d.id !== action.payload.id),
isDeleting: false,
}),
});

View File

@ -16,6 +16,7 @@ import { DatasourceDataState } from "./entityReducers/datasourceReducer";
import { AppViewReduxState } from "./uiReducers/appViewReducer";
import { ApplicationsReduxState } from "./uiReducers/applicationsReducer";
import { BindingsDataState } from "./entityReducers/bindingsReducer";
import { ApiPaneReduxState } from "./uiReducers/apiPaneReducer";
const appReducer = combineReducers({
entities: entityReducer,
@ -33,6 +34,7 @@ export interface AppState {
errors: ErrorReduxState;
appView: AppViewReduxState;
applications: ApplicationsReduxState;
apiPane: ApiPaneReduxState;
};
entities: {
canvasWidgets: CanvasWidgetsReduxState;

View File

@ -0,0 +1,84 @@
import { createReducer } from "../../utils/AppsmithUtils";
import {
ReduxActionTypes,
ReduxActionErrorTypes,
} from "../../constants/ReduxActionConstants";
const initialState: ApiPaneReduxState = {
isFetching: false,
isRunning: false,
isSaving: false,
isDeleting: false,
};
export interface ApiPaneReduxState {
isFetching: boolean;
isRunning: boolean;
isSaving: boolean;
isDeleting: boolean;
}
const apiPaneReducer = createReducer(initialState, {
[ReduxActionTypes.FETCH_ACTIONS_INIT]: (state: ApiPaneReduxState) => ({
...state,
isFetching: true,
}),
[ReduxActionTypes.FETCH_ACTIONS_SUCCESS]: (state: ApiPaneReduxState) => ({
...state,
isFetching: false,
}),
[ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: (state: ApiPaneReduxState) => ({
...state,
isFetching: false,
}),
[ReduxActionTypes.CREATE_ACTION_INIT]: (state: ApiPaneReduxState) => ({
...state,
isSaving: true,
}),
[ReduxActionTypes.CREATE_ACTION_SUCCESS]: (state: ApiPaneReduxState) => ({
...state,
isSaving: false,
}),
[ReduxActionErrorTypes.CREATE_ACTION_ERROR]: (state: ApiPaneReduxState) => ({
...state,
isSaving: false,
}),
[ReduxActionTypes.EXECUTE_ACTION]: (state: ApiPaneReduxState) => ({
...state,
isRunning: true,
}),
[ReduxActionTypes.EXECUTE_ACTION_SUCCESS]: (state: ApiPaneReduxState) => ({
...state,
isRunning: false,
}),
[ReduxActionErrorTypes.EXECUTE_ACTION_ERROR]: (state: ApiPaneReduxState) => ({
...state,
isRunning: false,
}),
[ReduxActionTypes.UPDATE_ACTION_INIT]: (state: ApiPaneReduxState) => ({
...state,
isSaving: true,
}),
[ReduxActionTypes.UPDATE_ACTION_SUCCESS]: (state: ApiPaneReduxState) => ({
...state,
isSaving: false,
}),
[ReduxActionErrorTypes.UPDATE_ACTION_ERROR]: (state: ApiPaneReduxState) => ({
...state,
isSaving: false,
}),
[ReduxActionTypes.DELETE_ACTION_INIT]: (state: ApiPaneReduxState) => ({
...state,
isDeleting: true,
}),
[ReduxActionTypes.DELETE_ACTION_SUCCESS]: (state: ApiPaneReduxState) => ({
...state,
isDeleting: false,
}),
[ReduxActionErrorTypes.DELETE_ACTION_ERROR]: (state: ApiPaneReduxState) => ({
...state,
isDeleting: false,
}),
});
export default apiPaneReducer;

View File

@ -5,6 +5,7 @@ import propertyPaneReducer from "./propertyPaneReducer";
import appViewReducer from "./appViewReducer";
import applicationsReducer from "./applicationsReducer";
import { widgetSidebarReducer } from "./widgetSidebarReducer";
import apiPaneReducer from "./apiPaneReducer";
const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer,
@ -13,5 +14,6 @@ const uiReducer = combineReducers({
propertyPane: propertyPaneReducer,
appView: appViewReducer,
applications: applicationsReducer,
apiPane: apiPaneReducer,
});
export default uiReducer;

View File

@ -33,7 +33,7 @@ import {
import { API_EDITOR_ID_URL, API_EDITOR_URL } from "../constants/routes";
import { getDynamicBoundValue } from "../utils/DynamicBindingUtils";
import history from "../utils/history";
import { createUpdateBindingsMap } from "../actions/bindingActions";
import { validateResponse } from "./ErrorSagas";
const getDataTree = (state: AppState): DataTree => {
return state.entities;
@ -69,56 +69,70 @@ export function* evaluateJSONPathSaga(path: string): any {
}
export function* executeAPIQueryActionSaga(apiAction: ActionPayload) {
const api: PageAction = yield select(getAction, apiAction.actionId);
const executeActionRequest: ExecuteActionRequest = {
action: {
id: apiAction.actionId,
},
};
if (!_.isNil(api.jsonPathKeys)) {
const values: any = _.flatten(
yield all(
api.jsonPathKeys.map((jsonPath: string) => {
return call(evaluateJSONPathSaga, jsonPath);
}),
),
);
const dynamicBindings: Record<string, string> = {};
api.jsonPathKeys.forEach((key, i) => {
dynamicBindings[key] = values[i];
});
executeActionRequest.params = mapToPropList(dynamicBindings);
}
const response: ActionApiResponse = yield ActionAPI.executeAction(
executeActionRequest,
);
let payload = createActionResponse(response);
if (response.responseMeta && response.responseMeta.error) {
payload = createActionErrorResponse(response);
if (apiAction.onError) {
try {
const api: PageAction = yield select(getAction, apiAction.actionId);
if (!api) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION,
payload: apiAction.onError,
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: "No action selected",
});
return;
}
const executeActionRequest: ExecuteActionRequest = {
action: {
id: apiAction.actionId,
},
};
if (!_.isNil(api.jsonPathKeys)) {
const values: any = _.flatten(
yield all(
api.jsonPathKeys.map((jsonPath: string) => {
return call(evaluateJSONPathSaga, jsonPath);
}),
),
);
const dynamicBindings: Record<string, string> = {};
api.jsonPathKeys.forEach((key, i) => {
dynamicBindings[key] = values[i];
});
executeActionRequest.params = mapToPropList(dynamicBindings);
}
const response: ActionApiResponse = yield ActionAPI.executeAction(
executeActionRequest,
);
let payload = createActionResponse(response);
if (response.responseMeta && response.responseMeta.error) {
payload = createActionErrorResponse(response);
if (apiAction.onError) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION,
payload: apiAction.onError,
});
}
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: { [apiAction.actionId]: payload },
});
} else {
if (apiAction.onSuccess) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION,
payload: apiAction.onSuccess,
});
}
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_SUCCESS,
payload: { [apiAction.actionId]: payload },
});
}
return response;
} catch (error) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: { [apiAction.actionId]: payload },
});
} else {
if (apiAction.onSuccess) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION,
payload: apiAction.onSuccess,
});
}
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_SUCCESS,
payload: { [apiAction.actionId]: payload },
payload: { error },
});
}
return response;
}
// TODO(satbir): Refact this to not make this recursive.
@ -131,7 +145,10 @@ export function* executeActionSaga(actionPayloads: ActionPayload[]): any {
case "QUERY":
return call(executeAPIQueryActionSaga, actionPayload);
default:
return undefined;
return put({
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: "No action type defined",
});
}
}),
);
@ -140,6 +157,11 @@ export function* executeActionSaga(actionPayloads: ActionPayload[]): any {
export function* executeReduxActionSaga(action: ReduxAction<ActionPayload[]>) {
if (!_.isNil(action.payload)) {
yield call(executeActionSaga, action.payload);
} else {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: "No action payload",
});
}
}
@ -164,33 +186,43 @@ function* dryRunActionSaga(action: ReduxAction<RestAction>) {
}
export function* createActionSaga(actionPayload: ReduxAction<RestAction>) {
const response: ActionCreateUpdateResponse = yield ActionAPI.createAPI(
actionPayload.payload,
);
if (response.responseMeta.success) {
AppToaster.show({
message: `${actionPayload.payload.name} Action created`,
intent: Intent.SUCCESS,
try {
const response: ActionCreateUpdateResponse = yield ActionAPI.createAPI(
actionPayload.payload,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
AppToaster.show({
message: `${actionPayload.payload.name} Action created`,
intent: Intent.SUCCESS,
});
yield put(createActionSuccess(response.data));
history.push(API_EDITOR_ID_URL(response.data.id));
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.CREATE_ACTION_ERROR,
payload: { error },
});
yield put(createActionSuccess(response.data));
yield put(createUpdateBindingsMap());
history.push(API_EDITOR_ID_URL(response.data.id));
}
}
export function* fetchActionsSaga() {
const response: GenericApiResponse<
RestAction[]
> = yield ActionAPI.fetchActions();
if (response.responseMeta.success) {
yield put({
type: ReduxActionTypes.FETCH_ACTIONS_SUCCESS,
payload: response.data,
});
} else {
try {
const response: GenericApiResponse<
RestAction[]
> = yield ActionAPI.fetchActions();
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_ACTIONS_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.FETCH_ACTIONS_ERROR,
payload: response.responseMeta.status,
payload: { error },
});
}
}
@ -198,41 +230,45 @@ export function* fetchActionsSaga() {
export function* updateActionSaga(
actionPayload: ReduxAction<{ data: RestAction }>,
) {
const response: GenericApiResponse<RestAction> = yield ActionAPI.updateAPI(
actionPayload.payload.data,
);
if (response.responseMeta.success) {
AppToaster.show({
message: `${actionPayload.payload.data.name} Action updated`,
intent: Intent.SUCCESS,
});
yield put(updateActionSuccess({ data: response.data }));
yield put(createUpdateBindingsMap());
} else {
AppToaster.show({
message: "Error occurred when updating action",
intent: Intent.DANGER,
try {
const response: GenericApiResponse<RestAction> = yield ActionAPI.updateAPI(
actionPayload.payload.data,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
AppToaster.show({
message: `${actionPayload.payload.data.name} Action updated`,
intent: Intent.SUCCESS,
});
yield put(updateActionSuccess({ data: response.data }));
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.UPDATE_ACTION_ERROR,
payload: { error },
});
}
}
export function* deleteActionSaga(actionPayload: ReduxAction<{ id: string }>) {
const id = actionPayload.payload.id;
const response: GenericApiResponse<RestAction> = yield ActionAPI.deleteAction(
id,
);
if (response.responseMeta.success) {
AppToaster.show({
message: `${response.data.name} Action deleted`,
intent: Intent.SUCCESS,
});
yield put(deleteActionSuccess({ id }));
yield put(createUpdateBindingsMap());
history.push(API_EDITOR_URL);
} else {
AppToaster.show({
message: "Error occurred when deleting action",
intent: Intent.DANGER,
try {
const id = actionPayload.payload.id;
const response: GenericApiResponse<
RestAction
> = yield ActionAPI.deleteAction(id);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
AppToaster.show({
message: `${response.data.name} Action deleted`,
intent: Intent.SUCCESS,
});
yield put(deleteActionSuccess({ id }));
history.push(API_EDITOR_URL);
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.DELETE_ACTION_ERROR,
payload: { error },
});
}
}

View File

@ -1,4 +1,4 @@
import { all, select, takeLatest, put } from "redux-saga/effects";
import { all, select, takeLatest, put, call, take } from "redux-saga/effects";
import { ReduxActionTypes } from "../constants/ReduxActionConstants";
import { AppState } from "../reducers";
import { bindingsMapSuccess } from "../actions/bindingActions";
@ -16,11 +16,26 @@ function* createUpdateBindingsMapData() {
yield put(bindingsMapSuccess(map));
}
// The listener will keep track of any action
// that requires an update of the action and
// then call the update function again
function* initListener() {
while (true) {
// list all actions types here
yield take([
ReduxActionTypes.INIT_SUCCESS,
ReduxActionTypes.CREATE_ACTION_SUCCESS,
ReduxActionTypes.UPDATE_ACTION_SUCCESS,
ReduxActionTypes.DELETE_ACTION_SUCCESS,
ReduxActionTypes.UPDATE_CANVAS,
ReduxActionTypes.SAVE_PAGE_INIT,
]);
yield call(createUpdateBindingsMapData);
}
}
export default function* watchBindingsSagas() {
yield all([
takeLatest(
ReduxActionTypes.CREATE_UPDATE_BINDINGS_MAP_INIT,
createUpdateBindingsMapData,
),
takeLatest(ReduxActionTypes.CREATE_UPDATE_BINDINGS_MAP_INIT, initListener),
]);
}

View File

@ -11,20 +11,24 @@ import DatasourcesApi, {
Datasource,
} from "../api/DatasourcesApi";
import { API_EDITOR_FORM_NAME } from "../constants/forms";
import { validateResponse } from "./ErrorSagas";
function* fetchDatasourcesSaga() {
const response: GenericApiResponse<
Datasource[]
> = yield DatasourcesApi.fetchDatasources();
if (response.responseMeta.success) {
yield put({
type: ReduxActionTypes.FETCH_DATASOURCES_SUCCESS,
payload: response.data,
});
} else {
try {
const response: GenericApiResponse<
Datasource[]
> = yield DatasourcesApi.fetchDatasources();
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_DATASOURCES_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.FETCH_DATASOURCES_ERROR,
payload: response.responseMeta.status,
payload: { error },
});
}
}
@ -32,19 +36,23 @@ function* fetchDatasourcesSaga() {
function* createDatasourceSaga(
actionPayload: ReduxAction<CreateDatasourceConfig>,
) {
const response: GenericApiResponse<
Datasource
> = yield DatasourcesApi.createDatasource(actionPayload.payload);
if (response.responseMeta.success) {
try {
const response: GenericApiResponse<
Datasource
> = yield DatasourcesApi.createDatasource(actionPayload.payload);
if (response.responseMeta.success) {
yield put({
type: ReduxActionTypes.CREATE_DATASOURCE_SUCCESS,
payload: response.data,
});
yield put(
change(API_EDITOR_FORM_NAME, "datasource.id", response.data.id),
);
}
} catch (error) {
yield put({
type: ReduxActionTypes.CREATE_DATASOURCE_SUCCESS,
payload: response.data,
});
yield put(change(API_EDITOR_FORM_NAME, "datasource.id", response.data.id));
} else {
yield put({
type: ReduxActionTypes.CREATE_DATASOURCES_ERROR,
payload: response.responseMeta.error,
type: ReduxActionTypes.CREATE_DATASOURCE_ERROR,
payload: { error },
});
}
}

View File

@ -11,13 +11,14 @@ import { fetchEditorConfigs } from "../actions/configsActions";
import { fetchPage, fetchPageList } from "../actions/pageActions";
import { fetchActions } from "../actions/actionActions";
import { fetchDatasources } from "../actions/datasourcesActions";
import { createUpdateBindingsMap } from "../actions/bindingActions";
import { initBindingMapListener } from "../actions/bindingActions";
function* initializeEditorSaga() {
// Step 1: Start getting all the data needed by the app
const propertyPaneConfigsId = yield select(getPropertyPaneConfigsId);
const currentPageId = yield select(getCurrentPageId);
yield all([
put(initBindingMapListener()),
put(fetchPageList()),
put(fetchEditorConfigs({ propertyPaneConfigsId })),
put(fetchPage(currentPageId)),
@ -31,13 +32,16 @@ function* initializeEditorSaga() {
take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS),
take(ReduxActionTypes.FETCH_DATASOURCES_SUCCESS),
]);
// Step 3: Create the bindings map;
yield put(createUpdateBindingsMap());
// Step 3: Create the success;
yield put({
type: ReduxActionTypes.INIT_SUCCESS,
});
}
export function* initializeAppViewerSaga(
action: ReduxAction<{ pageId: string }>,
) {
yield put(initBindingMapListener());
yield all([put(fetchPageList()), put(fetchActions())]);
yield all([
take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS),
@ -48,7 +52,9 @@ export function* initializeAppViewerSaga(
payload: action.payload,
});
yield take(ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS);
yield put(createUpdateBindingsMap());
yield put({
type: ReduxActionTypes.INIT_SUCCESS,
});
}
export default function* watchInitSagas() {

View File

@ -31,9 +31,6 @@ import { getPageLayoutId } from "./selectors";
import { extractCurrentDSL } from "../utils/WidgetPropsUtils";
import { getEditorConfigs, getWidgets } from "./selectors";
import { validateResponse } from "./ErrorSagas";
import { createUpdateBindingsMap } from "../actions/bindingActions";
import { UpdateWidgetPropertyPayload } from "../actions/controlActions";
import { RenderModes } from "../constants/WidgetConstants";
export function* fetchPageListSaga() {
try {
@ -105,7 +102,6 @@ export function* fetchPageSaga(
if (isValidResponse) {
const canvasWidgetsPayload = getCanvasWidgetsPayload(fetchPageResponse);
yield put(updateCanvas(canvasWidgetsPayload));
yield put(createUpdateBindingsMap());
}
} catch (error) {
yield put({
@ -143,7 +139,6 @@ export function* fetchPublishedPageSaga(
});
const canvasWidgetsPayload = getAppViewWidgetsPayload(response);
yield put(updateCanvas(canvasWidgetsPayload));
yield put(createUpdateBindingsMap());
}
} catch (error) {
yield put({
@ -192,20 +187,15 @@ function getLayoutSavePayload(
};
}
export function* saveLayoutSaga(
updateLayoutAction: ReduxAction<{
widgets: { [widgetId: string]: FlattenedWidgetProps };
}>,
) {
export function* saveLayoutSaga() {
try {
const { widgets } = updateLayoutAction.payload;
const widgets = yield select(getWidgets);
const editorConfigs = yield select(getEditorConfigs) as any;
yield put({
type: ReduxActionTypes.SAVE_PAGE_INIT,
payload: getLayoutSavePayload(widgets, editorConfigs),
});
put(createUpdateBindingsMap());
} catch (error) {
yield put({
type: ReduxActionErrorTypes.SAVE_PAGE_ERROR,
@ -216,38 +206,6 @@ export function* saveLayoutSaga(
}
}
// TODO(abhinav): This has redundant code. The only thing different here is the lack of state update.
// For now, this is fire and forget.
export function* asyncSaveLayout(
actionPayload: ReduxAction<UpdateWidgetPropertyPayload>,
) {
if (actionPayload.payload.renderMode === RenderModes.PAGE) return;
try {
const widgets = yield select(getWidgets);
const editorConfigs = yield select(getEditorConfigs) as any;
const request: SavePageRequest = getLayoutSavePayload(
widgets,
editorConfigs,
);
const savePageResponse: SavePageResponse = yield call(
PageApi.savePage,
request,
);
if (!validateResponse(savePageResponse)) {
throw Error("Error when saving layout");
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.UPDATE_WIDGET_PROPERTY_ERROR,
payload: {
error,
},
});
}
}
export function* createPageSaga(
createPageAction: ReduxAction<CreatePageRequest>,
) {
@ -286,13 +244,8 @@ export default function* pageSagas() {
),
takeLatest(ReduxActionTypes.SAVE_PAGE_INIT, savePageSaga),
takeEvery(ReduxActionTypes.UPDATE_LAYOUT, saveLayoutSaga),
// No need to save layout everytime a property is updated.
// We save the latest request to update layout.
takeLatest(ReduxActionTypes.UPDATE_WIDGET_PROPERTY, asyncSaveLayout),
takeLatest(
ReduxActionTypes.UPDATE_WIDGET_DYNAMIC_PROPERTY,
asyncSaveLayout,
),
takeLatest(ReduxActionTypes.UPDATE_WIDGET_PROPERTY, saveLayoutSaga),
takeLatest(ReduxActionTypes.UPDATE_WIDGET_DYNAMIC_PROPERTY, saveLayoutSaga),
takeLatest(ReduxActionTypes.CREATE_PAGE_INIT, createPageSaga),
takeLatest(ReduxActionTypes.FETCH_PAGE_LIST_INIT, fetchPageListSaga),
]);

View File

@ -19,7 +19,6 @@ import { put, select, takeEvery, takeLatest, all } from "redux-saga/effects";
import { getNextWidgetName } from "../utils/AppsmithUtils";
import { UpdateWidgetPropertyPayload } from "../actions/controlActions";
import { DATA_BIND_REGEX } from "../constants/BindingsConstants";
import { createUpdateBindingsMap } from "../actions/bindingActions";
export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
try {
@ -57,8 +56,6 @@ export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
type: ReduxActionTypes.UPDATE_LAYOUT,
payload: { widgets },
});
// TODO might be a potential performance choke point.
yield put(createUpdateBindingsMap());
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
@ -84,7 +81,6 @@ export function* deleteSaga(deleteAction: ReduxAction<WidgetDelete>) {
type: ReduxActionTypes.UPDATE_LAYOUT,
payload: { widgets },
});
yield put(createUpdateBindingsMap());
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,

View File

@ -1,5 +1,5 @@
import { API_PATH_START_WITH_SLASH_ERROR } from "../../constants/validations";
export const apiPathValidation = (value: string) => {
if (value.startsWith("/")) return API_PATH_START_WITH_SLASH_ERROR;
if (value && value.startsWith("/")) return API_PATH_START_WITH_SLASH_ERROR;
};

View File

@ -65,7 +65,7 @@ abstract class BaseWidget<
*/
executeAction(actionPayloads?: ActionPayload[]): void {
const { executeAction } = this.context;
executeAction && executeAction(actionPayloads);
executeAction && !_.isNil(actionPayloads) && executeAction(actionPayloads);
}
updateWidgetProperty(