Merge branch 'fix/form' into 'release'

Drafts in API Pane

Closes: #285 #223 #142 #224 #231 #176 #198 #249 #98 #248 #237 #233 #276 #178 #281 

* #223, #142: Added feature to save drafts of apis which are indicated by an orange dot next to it
* #285 #224: Added better routing in the api pane to save last visited api and selecting by default
* #231: Fixed button disabled state
* #176: Adding 2 headers and param rows by default
* #198 #98: Adding a default name for api `Action{Number}`
* #249: Fixed multiple scrolls in the response view
* #248 #237 #233 #276 : Fixed validations
* #178: Disable post body in GET
* #281: Disable Code Editor context menu

See merge request theappsmith/internal-tools-client!158
This commit is contained in:
Hetu Nandu 2019-11-25 09:15:12 +00:00
commit ce2c6af4b4
31 changed files with 390 additions and 147 deletions

View File

@ -0,0 +1,15 @@
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
export const changeApi = (id: string): ReduxAction<{ id: string }> => {
return {
type: ReduxActionTypes.API_PANE_CHANGE_API,
payload: { id },
};
};
export const initApiPane = (urlId?: string): ReduxAction<{ id?: string }> => {
return {
type: ReduxActionTypes.INIT_API_PANE,
payload: { id: urlId },
};
};

View File

@ -0,0 +1,7 @@
import { RoutesParamsReducerState } from "reducers/uiReducers/routesParamsReducer";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
export const updateRouteParams = (payload: RoutesParamsReducerState) => ({
type: ReduxActionTypes.UPDATE_ROUTES_PARAMS,
payload,
});

View File

@ -16,6 +16,7 @@ export const TextInput = styled(InputGroup)`
border-color: ${props => props.theme.colors.secondary};
background-color: ${props => props.theme.colors.textOnDarkBG};
outline: 0;
box-shadow: none;
}
}
&.bp3-input-group .bp3-input:not(:first-child) {
@ -54,7 +55,7 @@ const ErrorText = styled.span`
`;
export interface TextInputProps {
placeholderMessage?: string;
placeholder?: string;
input?: Partial<WrappedFieldInputProps>;
meta?: WrappedFieldMetaProps;
icon?: IconName | MaybeElement;
@ -63,11 +64,20 @@ export interface TextInputProps {
}
export const BaseTextInput = (props: TextInputProps) => {
const { placeholderMessage, input, meta, icon, showError, className } = props;
const { placeholder, input, meta, icon, showError, className } = props;
return (
<InputContainer className={className}>
<TextInput {...input} placeholder={placeholderMessage} leftIcon={icon} />
{showError && <ErrorText>{meta && meta.touched && meta.error}</ErrorText>}
<TextInput
{...input}
placeholder={placeholder}
leftIcon={icon}
autoComplete={"off"}
/>
{showError && (
<ErrorText>
{meta && (meta.touched || meta.active) && meta.error}
</ErrorText>
)}
</InputContainer>
);
};

View File

@ -70,6 +70,10 @@ const ButtonWrapper = styled(AnchorButton)<ButtonStyleProps>`
}
}};
}
&&.bp3-disabled {
background-color: #d0d7dd;
border: none;
}
}
`;
export type ButtonStyleName = "primary" | "secondary" | "error";

View File

@ -148,13 +148,14 @@ const ApiResponseView = (props: Props) => {
panelComponent: (
<CodeEditor
theme={"LIGHT"}
height={500}
height={600}
language={"json"}
input={{
value: response.body
? JSON.stringify(response.body, null, 2)
: "",
}}
lineNumbersMinChars={2}
/>
),
},

View File

@ -6,7 +6,6 @@ import { editor } from "monaco-editor";
const Wrapper = styled.div<{ height: number }>`
height: ${props => props.height}px;
overflow: auto;
color: white;
`;
@ -36,6 +35,8 @@ const CodeEditor = (props: Props) => {
lineNumbers: props.lineNumbers,
glyphMargin: props.glyphMargin,
folding: props.folding,
contextmenu: false,
scrollBeyondLastLine: false,
// // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
lineDecorationsWidth: props.lineDecorationsWidth,
lineNumbersMinChars: props.lineNumbersMinChars,

View File

@ -23,8 +23,8 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => {
{props.fields.map((field: any, index: number) => (
<FormRowWithLabel key={index}>
{index === 0 && <FormLabel>{props.label}</FormLabel>}
<TextField name={`${field}.key`} placeholderMessage="Key" />
<TextField name={`${field}.value`} placeholderMessage="Value" />
<TextField name={`${field}.key`} placeholder="Key" />
<TextField name={`${field}.value`} placeholder="Value" />
{index === props.fields.length - 1 ? (
<Icon
icon="plus"

View File

@ -82,6 +82,11 @@ export const ReduxActionTypes: { [key: string]: string } = {
"CREATE_UPDATE_ACTION_WIDGETIDS_MAP_SUCCESS",
UPDATE_WIDGET_PROPERTY_VALIDATION: "UPDATE_WIDGET_PROPERTY_VALIDATION",
HIDE_PROPERTY_PANE: "HIDE_PROPERTY_PANE",
INIT_API_PANE: "INIT_API_PANE",
API_PANE_CHANGE_API: "API_PANE_CHANGE_API",
UPDATE_API_DRAFT: "UPDATE_API_DRAFT",
DELETE_API_DRAFT: "DELETE_API_DRAFT",
UPDATE_ROUTES_PARAMS: "UPDATE_ROUTES_PARAMS",
};
export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];
@ -118,6 +123,11 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
CREATE_APPLICATION_ERROR: "CREATE_APPLICATION_ERROR",
};
export const ReduxFormActionTypes: { [key: string]: string } = {
VALUE_CHANGE: "@@redux-form/CHANGE",
UPDATE_FIELD_ERROR: "@@redux-form/UPDATE_SYNC_ERRORS",
};
export type ReduxActionErrorType = typeof ReduxActionErrorTypes[keyof typeof ReduxActionErrorTypes];
export interface ReduxAction<T> {
@ -127,6 +137,10 @@ export interface ReduxAction<T> {
export type ReduxActionWithoutPayload = Pick<ReduxAction<undefined>, "type">;
export interface ReduxActionWithMeta<T, M> extends ReduxAction<T> {
meta: M;
}
export interface ReduxActionErrorPayload {
message: string;
source?: string;

View File

@ -1,2 +0,0 @@
export const API_PATH_START_WITH_SLASH_ERROR = "Path cannot start with /";
export const FIELD_REQUIRED_ERROR = "This field is required";

View File

@ -34,7 +34,7 @@ export const PositionTypes: { [id: string]: string } = {
ABSOLUTE: "ABSOLUTE",
CONTAINER_DIREACTION: "CONTAINER_DIRECTION",
};
export type PositionType = (typeof PositionTypes)[keyof typeof PositionTypes];
export type PositionType = typeof PositionTypes[keyof typeof PositionTypes];
export type CSSUnit =
| "px"

View File

@ -1,5 +1,9 @@
export const ERROR_MESSAGE_SELECT_ACTION = "Please select an action";
export const ERROR_MESSAGE_SELECT_ACTION_TYPE = "Please select an action type";
export const ERROR_MESSAGE_CREATE_APPLICATION =
"We could not create the Application";
export const API_PATH_START_WITH_SLASH_ERROR = "Path cannot start with /";
export const FIELD_REQUIRED_ERROR = "This field is required";
export const VALID_FUNCTION_NAME_ERROR =
"Action name is not a valid function name";
export const UNIQUE_NAME_ERROR = "Action name must be unique";

View File

@ -5,7 +5,7 @@ import {
CreateApplicationFormValues,
createApplicationFormSubmitHandler,
} from "utils/formhelpers";
import TextField from "components/editorComponents/fields/TextField";
import TextField from "components/editorComponents/form/fields/TextField";
import { required } from "utils/validation/common";
import { FormGroup } from "@blueprintjs/core";
@ -18,7 +18,7 @@ export const CreateApplicationForm = (
<FormGroup intent={error ? "danger" : "none"} helperText={error}>
<TextField
name="applicationName"
placeholderMessage="Name"
placeholder="Name"
validate={required}
/>
</FormGroup>

View File

@ -1,21 +1,16 @@
import React from "react";
import { reduxForm, InjectedFormProps, FormSubmitHandler } from "redux-form";
import {
FORM_INITIAL_VALUES,
HTTP_METHOD_OPTIONS,
} from "constants/ApiEditorConstants";
import { HTTP_METHOD_OPTIONS } from "constants/ApiEditorConstants";
import styled from "styled-components";
import FormLabel from "components/editorComponents/FormLabel";
import FormRow from "components/editorComponents/FormRow";
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
import { RestAction } from "api/ActionAPI";
import TextField from "components/editorComponents/fields/TextField";
import DropdownField from "components/editorComponents/fields/DropdownField";
import DatasourcesField from "components/editorComponents/fields/DatasourcesField";
import KeyValueFieldArray from "components/editorComponents/fields/KeyValueFieldArray";
import JSONEditorField from "components/editorComponents/fields/JSONEditorField";
import { required } from "utils/validation/common";
import { apiPathValidation } from "utils/validation/ApiForm";
import TextField from "components/editorComponents/form/fields/TextField";
import DropdownField from "components/editorComponents/form/fields/DropdownField";
import DatasourcesField from "components/editorComponents/form/fields/DatasourcesField";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import JSONEditorField from "components/editorComponents/form/fields/JSONEditorField";
import ApiResponseView from "components/editorComponents/ApiResponseView";
import { API_EDITOR_FORM_NAME } from "constants/forms";
@ -74,6 +69,8 @@ const JSONEditorFieldWrapper = styled.div`
`;
interface APIFormProps {
allowSave: boolean;
allowPostBody: boolean;
onSubmit: FormSubmitHandler<RestAction>;
onSaveClick: () => void;
onRunClick: () => void;
@ -87,6 +84,8 @@ type Props = APIFormProps & InjectedFormProps<RestAction, APIFormProps>;
const ApiEditorForm: React.FC<Props> = (props: Props) => {
const {
allowSave,
allowPostBody,
onSaveClick,
onDeleteClick,
onRunClick,
@ -99,12 +98,7 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
<Form onSubmit={handleSubmit}>
<MainConfiguration>
<FormRow>
<TextField
name="name"
placeholderMessage="API Name *"
validate={required}
showError
/>
<TextField name="name" placeholder="API Name *" showError />
<ActionButtons>
<ActionButton
text="Delete"
@ -124,6 +118,7 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
filled
onClick={onSaveClick}
loading={isSaving}
disabled={!allowSave}
/>
</ActionButtons>
</FormRow>
@ -135,9 +130,8 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
/>
<DatasourcesField name="datasource.id" />
<TextField
placeholderMessage="API Path"
placeholder="API Path"
name="actionConfiguration.path"
validate={[apiPathValidation]}
icon="slash"
showError
/>
@ -153,10 +147,14 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
name="actionConfiguration.queryParameters"
label="Params"
/>
<FormLabel>{"Post Body"}</FormLabel>
<JSONEditorFieldWrapper>
<JSONEditorField name="actionConfiguration.body" />
</JSONEditorFieldWrapper>
{allowPostBody && (
<React.Fragment>
<FormLabel>{"Post Body"}</FormLabel>
<JSONEditorFieldWrapper>
<JSONEditorField name="actionConfiguration.body" />
</JSONEditorFieldWrapper>
</React.Fragment>
)}
</RequestParamsWrapper>
<ApiResponseView />
</SecondaryWrapper>
@ -167,5 +165,4 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
export default reduxForm<RestAction, APIFormProps>({
form: API_EDITOR_FORM_NAME,
enableReinitialize: true,
initialValues: FORM_INITIAL_VALUES,
})(ApiEditorForm);

View File

@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import { submit, initialize, getFormValues, destroy } from "redux-form";
import ApiEditorForm from "./APIEditor/ApiEditorForm";
import { submit, getFormValues } from "redux-form";
import ApiEditorForm from "./Form";
import {
createActionRequest,
runApiAction,
@ -11,17 +11,17 @@ import {
import { RestAction } from "api/ActionAPI";
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 { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer";
import styled from "styled-components";
import { FORM_INITIAL_VALUES } from "constants/ApiEditorConstants";
import { HTTP_METHODS } from "constants/ApiEditorConstants";
import _ from "lodash";
interface ReduxStateProps {
actions: ActionDataState;
apiPane: ApiPaneReduxState;
formData: any;
formData: RestAction;
}
interface ReduxActionProps {
submitForm: (name: string) => void;
@ -29,8 +29,6 @@ interface ReduxActionProps {
runAction: () => void;
deleteAction: (id: string) => void;
updateAction: (data: RestAction) => void;
initialize: (formName: string, data?: Partial<RestAction>) => void;
destroy: (formName: string) => void;
}
type Props = ReduxActionProps &
@ -46,42 +44,6 @@ const EmptyStateContainer = styled.div`
`;
class ApiEditor extends React.Component<Props> {
componentDidMount(): void {
const currentApiId = this.props.match.params.apiId;
const currentApplicationId = this.props.match.params.applicationId;
const currentPageId = this.props.match.params.pageId;
if (!currentApiId) return;
if (!this.props.actions.data.length) {
this.props.history.push(
API_EDITOR_URL(currentApplicationId, currentPageId),
);
return;
}
const data = this.props.actions.data.filter(
action => action.id === currentApiId,
)[0];
this.props.initialize(API_EDITOR_FORM_NAME, data);
}
componentDidUpdate(prevProps: Readonly<Props>): void {
const currentId = this.props.match.params.apiId;
if (currentId && currentId !== prevProps.match.params.apiId) {
const data = this.props.actions.data.filter(
action => action.id === currentId,
)[0];
this.props.destroy(API_EDITOR_FORM_NAME);
let initialData = data;
if (!initialData.actionConfiguration) {
initialData = {
...data,
...FORM_INITIAL_VALUES,
};
}
this.props.initialize(API_EDITOR_FORM_NAME, initialData);
}
}
handleSubmit = (values: RestAction) => {
const { formData } = this.props;
if (formData.id) {
@ -103,15 +65,19 @@ class ApiEditor extends React.Component<Props> {
render() {
const {
apiPane: { isSaving, isRunning, isDeleting },
apiPane: { isSaving, isRunning, isDeleting, drafts },
match: {
params: { apiId },
},
formData,
} = this.props;
const httpMethod = _.get(formData, "actionConfiguration.httpMethod");
return (
<React.Fragment>
{apiId ? (
<ApiEditorForm
allowSave={apiId in drafts}
allowPostBody={httpMethod && httpMethod !== HTTP_METHODS[0]}
isSaving={isSaving}
isRunning={isRunning}
isDeleting={isDeleting}
@ -133,7 +99,7 @@ class ApiEditor extends React.Component<Props> {
const mapStateToProps = (state: AppState): ReduxStateProps => ({
actions: state.entities.actions,
apiPane: state.ui.apiPane,
formData: getFormValues(API_EDITOR_FORM_NAME)(state),
formData: getFormValues(API_EDITOR_FORM_NAME)(state) as RestAction,
});
const mapDispatchToProps = (dispatch: any): ReduxActionProps => ({
@ -142,9 +108,6 @@ const mapDispatchToProps = (dispatch: any): ReduxActionProps => ({
runAction: () => dispatch(runApiAction()),
deleteAction: (id: string) => dispatch(deleteAction({ id })),
updateAction: (data: RestAction) => dispatch(updateAction({ data })),
initialize: (formName: string, data?: Partial<RestAction>) =>
dispatch(initialize(formName, data)),
destroy: (formName: string) => dispatch(destroy(formName)),
});
export default connect(mapStateToProps, mapDispatchToProps)(ApiEditor);

View File

@ -4,11 +4,7 @@ import { RouteComponentProps } from "react-router";
import styled from "styled-components";
import { AppState } from "reducers";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import {
API_EDITOR_ID_URL,
API_EDITOR_URL,
APIEditorRouteParams,
} from "constants/routes";
import { API_EDITOR_URL, APIEditorRouteParams } from "constants/routes";
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
import { FormIcons } from "icons/FormIcons";
import { Spinner } from "@blueprintjs/core";
@ -16,15 +12,13 @@ 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";
import { changeApi, initApiPane } from "actions/apiPaneActions";
import { RestAction } from "api/ActionAPI";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import Fuse from "fuse.js";
const LoadingContainer = styled.div`
const LoadingContainer = styled(CenteredWrapper)`
height: 50%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
`;
const ApiSidebarWrapper = styled.div`
@ -98,6 +92,14 @@ const ActionName = styled.span`
text-overflow: ellipsis;
`;
const DraftIconIndicator = styled.span<{ isHidden: boolean }>`
width: 8px;
height: 8px;
border-radius: 8px;
background-color: #f2994a;
opacity: ${({ isHidden }) => (isHidden ? 0 : 1)};
`;
const CreateNewButton = styled(BaseButton)`
&& {
border: none;
@ -135,6 +137,8 @@ interface ReduxStateProps {
interface ReduxDispatchProps {
createAction: (name: string) => void;
onApiChange: (id: string) => void;
initApiPane: (urlId?: string) => void;
}
type Props = ReduxStateProps &
@ -164,7 +168,12 @@ class ApiSidebar extends React.Component<Props, State> {
};
}
componentDidMount(): void {
this.props.initApiPane(this.props.match.params.apiId);
}
componentDidUpdate(prevProps: Readonly<Props>): void {
// If url has changed, hide the create input
if (!prevProps.match.params.apiId && this.props.match.params.apiId) {
this.setState({
isCreating: false,
@ -174,13 +183,13 @@ class ApiSidebar extends React.Component<Props, State> {
}
handleCreateNew = () => {
const { history } = this.props;
const { history, actions } = this.props;
const { pageId, applicationId } = this.props.match.params;
history.push(API_EDITOR_URL(applicationId, pageId));
this.setState({
isCreating: true,
name: "",
name: `action${actions.data.length}`,
});
};
@ -208,11 +217,13 @@ class ApiSidebar extends React.Component<Props, State> {
});
};
handleApiChange = (actionId: string) => {
this.props.onApiChange(actionId);
};
render() {
const { applicationId, pageId } = this.props.match.params;
const {
apiPane,
history,
apiPane: { isFetching, isSaving, drafts },
match,
actions: { data },
} = this.props;
@ -222,7 +233,7 @@ class ApiSidebar extends React.Component<Props, State> {
const actions: RestAction[] = search ? fuse.search(search) : data;
return (
<React.Fragment>
{apiPane.isFetching ? (
{isFetching ? (
<LoadingContainer>
<Spinner size={30} />
</LoadingContainer>
@ -235,16 +246,12 @@ class ApiSidebar extends React.Component<Props, State> {
value: search,
onChange: this.handleSearchChange,
}}
placeholderMessage="Search"
placeholder="Search"
/>
{actions.map(action => (
<ApiItem
key={action.id}
onClick={() =>
history.push(
API_EDITOR_ID_URL(applicationId, pageId, action.id),
)
}
onClick={() => this.handleApiChange(action.id)}
isSelected={activeActionId === action.id}
>
{action.actionConfiguration ? (
@ -255,13 +262,14 @@ class ApiSidebar extends React.Component<Props, State> {
<HTTPMethod />
)}
<ActionName>{action.name}</ActionName>
<DraftIconIndicator isHidden={!(action.id in drafts)} />
</ApiItem>
))}
</ApiItemsWrapper>
{isCreating ? (
<CreateApiWrapper>
<BaseTextInput
placeholderMessage="API name"
placeholder="API name"
input={{
value: name,
onChange: this.handleNameChange,
@ -273,12 +281,12 @@ class ApiSidebar extends React.Component<Props, State> {
text=""
onClick={this.saveAction}
filled
loading={apiPane.isSaving}
loading={isSaving}
/>
</CreateApiWrapper>
) : (
<React.Fragment>
{!apiPane.isFetching && (
{!isFetching && (
<CreateNewButton
text="Create new API"
icon={FormIcons.ADD_NEW_ICON()}
@ -301,6 +309,8 @@ const mapStateToProps = (state: AppState): ReduxStateProps => ({
const mapDispatchToProps = (dispatch: Function): ReduxDispatchProps => ({
createAction: (name: string) => dispatch(createActionRequest({ name })),
onApiChange: (actionId: string) => dispatch(changeApi(actionId)),
initApiPane: (urlId?: string) => dispatch(initApiPane(urlId)),
});
export default connect(mapStateToProps, mapDispatchToProps)(ApiSidebar);

View File

@ -28,6 +28,8 @@ import {
} from "constants/ReduxActionConstants";
import { Dialog, Classes, AnchorButton } from "@blueprintjs/core";
import { initEditor } from "actions/initActions";
import { updateRouteParams } from "actions/routeParamsActions";
import { RoutesParamsReducerState } from "reducers/uiReducers/routesParamsReducer";
type EditorProps = {
currentPageName?: string;
@ -39,6 +41,7 @@ type EditorProps = {
previewPage: Function;
initEditor: Function;
createPage: Function;
updateRouteParams: (params: RoutesParamsReducerState) => void;
pages: PageListPayload;
switchPage: (pageId: string) => void;
isPublishing: boolean;
@ -60,30 +63,7 @@ class Editor extends Component<EditorProps> {
}
}
componentDidUpdate(previously: EditorProps) {
// const currently = this.props;
// if (currently.publishedTime !== previously.publishedTime) {
// this.setState({
// isDialogOpen: true,
// });
// }
// if (
// currently.currentPageId &&
// previously.currentPageId !== currently.currentPageId &&
// currently.currentApplicationId
// ) {
// this.props.history.replace(
// BUILDER_PAGE_URL(
// currently.currentApplicationId,
// currently.currentPageId,
// ),
// );
// }
// if (
// previously.match.params.pageId !== currently.match.params.pageId &&
// currently.currentPageId !== currently.match.params.pageId
// ) {
// this.props.switchPage(currently.match.params.pageId);
// }
this.props.updateRouteParams(this.props.match.params);
}
handleDialogClose = () => {
@ -207,6 +187,8 @@ const mapDispatchToProps = (dispatch: any) => {
},
});
},
updateRouteParams: (params: RoutesParamsReducerState) =>
dispatch(updateRouteParams(params)),
};
};

View File

@ -5,7 +5,7 @@ import {
withRouter,
RouteComponentProps,
} from "react-router-dom";
import ApiEditor from "./ApiEditor";
import ApiEditor from "./APIEditor";
import {
API_EDITOR_ID_URL,
API_EDITOR_URL,

View File

@ -2,6 +2,7 @@ import { createReducer } from "utils/AppsmithUtils";
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import { ActionResponse } from "api/ActionAPI";
import { ActionDataState } from "./actionsReducer";
import _ from "lodash";
const initialState: APIDataState = {};
@ -16,6 +17,10 @@ const apiDataReducer = createReducer(initialState, {
state: ActionDataState,
action: ReduxAction<{ [id: string]: ActionResponse }>,
) => ({ ...state, ...action.payload }),
[ReduxActionTypes.DELETE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ id: string }>,
) => _.omit(state, action.payload.id),
});
export default apiDataReducer;

View File

@ -18,6 +18,7 @@ import { ApplicationsReduxState } from "./uiReducers/applicationsReducer";
import { BindingsDataState } from "./entityReducers/bindingsReducer";
import { PageListReduxState } from "./entityReducers/pageListReducer";
import { ApiPaneReduxState } from "./uiReducers/apiPaneReducer";
import { RoutesParamsReducerState } from "reducers/uiReducers/routesParamsReducer";
const appReducer = combineReducers({
entities: entityReducer,
@ -36,6 +37,7 @@ export interface AppState {
appView: AppViewReduxState;
applications: ApplicationsReduxState;
apiPane: ApiPaneReduxState;
routesParams: RoutesParamsReducerState;
};
entities: {
canvasWidgets: CanvasWidgetsReduxState;

View File

@ -2,9 +2,14 @@ import { createReducer } from "utils/AppsmithUtils";
import {
ReduxActionTypes,
ReduxActionErrorTypes,
ReduxAction,
} from "constants/ReduxActionConstants";
import { RestAction } from "api/ActionAPI";
import _ from "lodash";
const initialState: ApiPaneReduxState = {
lastUsed: "",
drafts: {},
isFetching: false,
isRunning: false,
isSaving: false,
@ -12,6 +17,8 @@ const initialState: ApiPaneReduxState = {
};
export interface ApiPaneReduxState {
lastUsed: string;
drafts: Record<string, RestAction>;
isFetching: boolean;
isRunning: boolean;
isSaving: boolean;
@ -79,6 +86,30 @@ const apiPaneReducer = createReducer(initialState, {
...state,
isDeleting: false,
}),
[ReduxActionTypes.UPDATE_API_DRAFT]: (
state: ApiPaneReduxState,
action: ReduxAction<{ id: string; draft: Partial<RestAction> }>,
) => ({
...state,
drafts: {
...state.drafts,
[action.payload.id]: action.payload.draft,
},
}),
[ReduxActionTypes.DELETE_API_DRAFT]: (
state: ApiPaneReduxState,
action: ReduxAction<{ id: string }>,
) => ({
...state,
drafts: _.omit(state.drafts, action.payload.id),
}),
[ReduxActionTypes.API_PANE_CHANGE_API]: (
state: ApiPaneReduxState,
action: ReduxAction<{ id: string }>,
) => ({
...state,
lastUsed: action.payload.id,
}),
});
export default apiPaneReducer;

View File

@ -6,6 +6,7 @@ import appViewReducer from "./appViewReducer";
import applicationsReducer from "./applicationsReducer";
import { widgetSidebarReducer } from "./widgetSidebarReducer";
import apiPaneReducer from "./apiPaneReducer";
import routesParamsReducer from "reducers/uiReducers/routesParamsReducer";
const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer,
@ -15,5 +16,6 @@ const uiReducer = combineReducers({
appView: appViewReducer,
applications: applicationsReducer,
apiPane: apiPaneReducer,
routesParams: routesParamsReducer,
});
export default uiReducer;

View File

@ -0,0 +1,23 @@
import { createReducer } from "utils/AppsmithUtils";
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
const initialState: RoutesParamsReducerState = {
applicationId: "",
pageId: "",
};
const routesParamsReducer = createReducer(initialState, {
[ReduxActionTypes.UPDATE_ROUTES_PARAMS]: (
state: RoutesParamsReducerState,
action: ReduxAction<RoutesParamsReducerState>,
) => {
return { ...action.payload };
},
});
export interface RoutesParamsReducerState {
applicationId: string;
pageId: string;
}
export default routesParamsReducer;

View File

@ -31,13 +31,11 @@ import {
deleteActionSuccess,
updateActionSuccess,
} from "actions/actionActions";
import { API_EDITOR_ID_URL, API_EDITOR_URL } from "constants/routes";
import {
extractDynamicBoundValue,
getDynamicBindings,
isDynamicValue,
} from "utils/DynamicBindingUtils";
import history from "utils/history";
import { validateResponse } from "./ErrorSagas";
import { getDataTree } from "selectors/entitiesSelector";
import {
@ -47,7 +45,7 @@ import {
import { getFormData } from "selectors/formSelectors";
import { API_EDITOR_FORM_NAME } from "constants/forms";
const getAction = (
export const getAction = (
state: AppState,
actionId: string,
): RestAction | undefined => {
@ -225,7 +223,6 @@ export function* createActionSaga(actionPayload: ReduxAction<RestAction>) {
intent: Intent.SUCCESS,
});
yield put(createActionSuccess(response.data));
history.push(API_EDITOR_ID_URL(response.data.id));
}
} catch (error) {
yield put({
@ -289,7 +286,6 @@ export function* deleteActionSaga(actionPayload: ReduxAction<{ id: string }>) {
intent: Intent.SUCCESS,
});
yield put(deleteActionSuccess({ id }));
history.push(API_EDITOR_URL);
}
} catch (error) {
yield put({

View File

@ -0,0 +1,181 @@
/**
* 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 } from "redux-saga/effects";
import {
ReduxAction,
ReduxActionTypes,
ReduxActionWithMeta,
ReduxFormActionTypes,
} from "constants/ReduxActionConstants";
import { getFormData } from "selectors/formSelectors";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import history from "utils/history";
import { API_EDITOR_ID_URL, API_EDITOR_URL } from "constants/routes";
import { destroy, initialize } from "redux-form";
import { getAction } from "./ActionSagas";
import { AppState } from "reducers";
import { RestAction } from "api/ActionAPI";
import { FORM_INITIAL_VALUES } from "constants/ApiEditorConstants";
import { changeApi } from "actions/apiPaneActions";
import {
API_PATH_START_WITH_SLASH_ERROR,
FIELD_REQUIRED_ERROR,
UNIQUE_NAME_ERROR,
VALID_FUNCTION_NAME_ERROR,
} from "constants/messages";
const getApiDraft = (state: AppState, id: string) => {
const drafts = state.ui.apiPane.drafts;
if (id in drafts) return drafts[id];
return {};
};
const getActions = (state: AppState) => state.entities.actions.data;
const getLastUsedAction = (state: AppState) => state.ui.apiPane.lastUsed;
const getRouterParams = (state: AppState) => state.ui.routesParams;
function* initApiPaneSaga(actionPayload: ReduxAction<{ id?: string }>) {
let actions = yield select(getActions);
while (!actions.length) {
yield take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS);
actions = yield select(getActions);
}
const urlId = actionPayload.payload.id;
const lastUsedId = yield select(getLastUsedAction);
let id = "";
if (urlId) {
id = urlId;
} else if (lastUsedId) {
id = lastUsedId;
}
yield put(changeApi(id));
}
function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) {
const { id } = actionPayload.payload;
const { applicationId, pageId } = yield select(getRouterParams);
const action = yield select(getAction, id);
if (!action) {
history.push(API_EDITOR_URL(applicationId, pageId));
return;
}
const draft = yield select(getApiDraft, id);
yield put(destroy(API_EDITOR_FORM_NAME));
const data = _.isEmpty(draft) ? action : draft;
yield put(initialize(API_EDITOR_FORM_NAME, data));
history.push(API_EDITOR_ID_URL(applicationId, pageId, id));
}
function* updateDraftsSaga() {
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(
actionPayload: ReduxActionWithMeta<string, { field: string; form: string }>,
) {
const errors = {};
const {
payload,
meta: { field },
} = actionPayload;
const actions: RestAction[] = yield select(getActions);
const sameNames = actions.filter(
(action: RestAction) => action.name === payload && action.id,
);
if (field === "name") {
if (!_.trim(payload)) {
_.set(errors, field, FIELD_REQUIRED_ERROR);
} else if (payload.indexOf(" ") !== -1) {
_.set(errors, field, VALID_FUNCTION_NAME_ERROR);
} else if (sameNames.length > 0) {
// TODO Check this
_.set(errors, field, UNIQUE_NAME_ERROR);
} else {
_.unset(errors, field);
}
}
if (field === "actionConfiguration.path") {
if (payload && payload.startsWith("/")) {
_.set(errors, field, API_PATH_START_WITH_SLASH_ERROR);
} else {
_.unset(errors, field);
}
}
yield put({
type: ReduxFormActionTypes.UPDATE_FIELD_ERROR,
meta: {
form: API_EDITOR_FORM_NAME,
},
payload: {
syncErrors: errors,
},
});
}
function* formValueChangeSaga(
actionPayload: ReduxActionWithMeta<string, { field: string; form: string }>,
) {
const { form } = actionPayload.meta;
if (form !== API_EDITOR_FORM_NAME) return;
yield all([call(validateInputSaga, actionPayload), call(updateDraftsSaga)]);
}
function* handleActionCreatedSaga(actionPayload: ReduxAction<RestAction>) {
const { id } = actionPayload.payload;
const action = yield select(getAction, id);
const data = {
...action,
...FORM_INITIAL_VALUES,
};
yield put(initialize(API_EDITOR_FORM_NAME, data));
const { applicationId, pageId } = yield select(getRouterParams);
history.push(API_EDITOR_ID_URL(applicationId, pageId, id));
}
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;
const { applicationId, pageId } = yield select(getRouterParams);
history.push(API_EDITOR_URL(applicationId, pageId));
yield put({
type: ReduxActionTypes.DELETE_API_DRAFT,
payload: { id },
});
}
export default function* root() {
yield all([
takeEvery(ReduxActionTypes.INIT_API_PANE, initApiPaneSaga),
takeEvery(ReduxActionTypes.API_PANE_CHANGE_API, changeApiSaga),
// Intercepting the redux-form change actionType
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleActionCreatedSaga),
takeEvery(ReduxActionTypes.UPDATE_ACTION_SUCCESS, handleActionUpdatedSaga),
takeEvery(ReduxActionTypes.DELETE_ACTION_SUCCESS, handleActionDeletedSaga),
]);
}

View File

@ -12,6 +12,7 @@ import bindingsSagas from "./BindingsSagas";
import watchActionWidgetMapSagas, {
watchPropertyAndBindingUpdate,
} from "./ActionWidgetMapSagas";
import apiPaneSagas from "./ApiPaneSagas";
export function* rootSaga() {
yield all([
@ -27,5 +28,6 @@ export function* rootSaga() {
spawn(bindingsSagas),
spawn(watchActionWidgetMapSagas),
spawn(watchPropertyAndBindingUpdate),
spawn(apiPaneSagas),
]);
}

View File

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

View File

@ -1,4 +1,4 @@
import { FIELD_REQUIRED_ERROR } from "constants/ValidationsMessages";
import { FIELD_REQUIRED_ERROR } from "constants/messages";
export const required = (value: any) => {
if (value === undefined || value === null || value === "") {