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

View File

@ -148,13 +148,14 @@ const ApiResponseView = (props: Props) => {
panelComponent: ( panelComponent: (
<CodeEditor <CodeEditor
theme={"LIGHT"} theme={"LIGHT"}
height={500} height={600}
language={"json"} language={"json"}
input={{ input={{
value: response.body value: response.body
? JSON.stringify(response.body, null, 2) ? 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 }>` const Wrapper = styled.div<{ height: number }>`
height: ${props => props.height}px; height: ${props => props.height}px;
overflow: auto;
color: white; color: white;
`; `;
@ -36,6 +35,8 @@ const CodeEditor = (props: Props) => {
lineNumbers: props.lineNumbers, lineNumbers: props.lineNumbers,
glyphMargin: props.glyphMargin, glyphMargin: props.glyphMargin,
folding: props.folding, folding: props.folding,
contextmenu: false,
scrollBeyondLastLine: false,
// // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882 // // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
lineDecorationsWidth: props.lineDecorationsWidth, lineDecorationsWidth: props.lineDecorationsWidth,
lineNumbersMinChars: props.lineNumbersMinChars, lineNumbersMinChars: props.lineNumbersMinChars,

View File

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

View File

@ -82,6 +82,11 @@ export const ReduxActionTypes: { [key: string]: string } = {
"CREATE_UPDATE_ACTION_WIDGETIDS_MAP_SUCCESS", "CREATE_UPDATE_ACTION_WIDGETIDS_MAP_SUCCESS",
UPDATE_WIDGET_PROPERTY_VALIDATION: "UPDATE_WIDGET_PROPERTY_VALIDATION", UPDATE_WIDGET_PROPERTY_VALIDATION: "UPDATE_WIDGET_PROPERTY_VALIDATION",
HIDE_PROPERTY_PANE: "HIDE_PROPERTY_PANE", 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]; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];
@ -118,6 +123,11 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
CREATE_APPLICATION_ERROR: "CREATE_APPLICATION_ERROR", 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 type ReduxActionErrorType = typeof ReduxActionErrorTypes[keyof typeof ReduxActionErrorTypes];
export interface ReduxAction<T> { export interface ReduxAction<T> {
@ -127,6 +137,10 @@ export interface ReduxAction<T> {
export type ReduxActionWithoutPayload = Pick<ReduxAction<undefined>, "type">; export type ReduxActionWithoutPayload = Pick<ReduxAction<undefined>, "type">;
export interface ReduxActionWithMeta<T, M> extends ReduxAction<T> {
meta: M;
}
export interface ReduxActionErrorPayload { export interface ReduxActionErrorPayload {
message: string; message: string;
source?: 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", ABSOLUTE: "ABSOLUTE",
CONTAINER_DIREACTION: "CONTAINER_DIRECTION", CONTAINER_DIREACTION: "CONTAINER_DIRECTION",
}; };
export type PositionType = (typeof PositionTypes)[keyof typeof PositionTypes]; export type PositionType = typeof PositionTypes[keyof typeof PositionTypes];
export type CSSUnit = export type CSSUnit =
| "px" | "px"

View File

@ -1,5 +1,9 @@
export const ERROR_MESSAGE_SELECT_ACTION = "Please select an action"; 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_SELECT_ACTION_TYPE = "Please select an action type";
export const ERROR_MESSAGE_CREATE_APPLICATION = export const ERROR_MESSAGE_CREATE_APPLICATION =
"We could not create the 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, CreateApplicationFormValues,
createApplicationFormSubmitHandler, createApplicationFormSubmitHandler,
} from "utils/formhelpers"; } 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 { required } from "utils/validation/common";
import { FormGroup } from "@blueprintjs/core"; import { FormGroup } from "@blueprintjs/core";
@ -18,7 +18,7 @@ export const CreateApplicationForm = (
<FormGroup intent={error ? "danger" : "none"} helperText={error}> <FormGroup intent={error ? "danger" : "none"} helperText={error}>
<TextField <TextField
name="applicationName" name="applicationName"
placeholderMessage="Name" placeholder="Name"
validate={required} validate={required}
/> />
</FormGroup> </FormGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,14 @@ import { createReducer } from "utils/AppsmithUtils";
import { import {
ReduxActionTypes, ReduxActionTypes,
ReduxActionErrorTypes, ReduxActionErrorTypes,
ReduxAction,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { RestAction } from "api/ActionAPI";
import _ from "lodash";
const initialState: ApiPaneReduxState = { const initialState: ApiPaneReduxState = {
lastUsed: "",
drafts: {},
isFetching: false, isFetching: false,
isRunning: false, isRunning: false,
isSaving: false, isSaving: false,
@ -12,6 +17,8 @@ const initialState: ApiPaneReduxState = {
}; };
export interface ApiPaneReduxState { export interface ApiPaneReduxState {
lastUsed: string;
drafts: Record<string, RestAction>;
isFetching: boolean; isFetching: boolean;
isRunning: boolean; isRunning: boolean;
isSaving: boolean; isSaving: boolean;
@ -79,6 +86,30 @@ const apiPaneReducer = createReducer(initialState, {
...state, ...state,
isDeleting: false, 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; export default apiPaneReducer;

View File

@ -6,6 +6,7 @@ import appViewReducer from "./appViewReducer";
import applicationsReducer from "./applicationsReducer"; import applicationsReducer from "./applicationsReducer";
import { widgetSidebarReducer } from "./widgetSidebarReducer"; import { widgetSidebarReducer } from "./widgetSidebarReducer";
import apiPaneReducer from "./apiPaneReducer"; import apiPaneReducer from "./apiPaneReducer";
import routesParamsReducer from "reducers/uiReducers/routesParamsReducer";
const uiReducer = combineReducers({ const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer, widgetSidebar: widgetSidebarReducer,
@ -15,5 +16,6 @@ const uiReducer = combineReducers({
appView: appViewReducer, appView: appViewReducer,
applications: applicationsReducer, applications: applicationsReducer,
apiPane: apiPaneReducer, apiPane: apiPaneReducer,
routesParams: routesParamsReducer,
}); });
export default uiReducer; 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, deleteActionSuccess,
updateActionSuccess, updateActionSuccess,
} from "actions/actionActions"; } from "actions/actionActions";
import { API_EDITOR_ID_URL, API_EDITOR_URL } from "constants/routes";
import { import {
extractDynamicBoundValue, extractDynamicBoundValue,
getDynamicBindings, getDynamicBindings,
isDynamicValue, isDynamicValue,
} from "utils/DynamicBindingUtils"; } from "utils/DynamicBindingUtils";
import history from "utils/history";
import { validateResponse } from "./ErrorSagas"; import { validateResponse } from "./ErrorSagas";
import { getDataTree } from "selectors/entitiesSelector"; import { getDataTree } from "selectors/entitiesSelector";
import { import {
@ -47,7 +45,7 @@ import {
import { getFormData } from "selectors/formSelectors"; import { getFormData } from "selectors/formSelectors";
import { API_EDITOR_FORM_NAME } from "constants/forms"; import { API_EDITOR_FORM_NAME } from "constants/forms";
const getAction = ( export const getAction = (
state: AppState, state: AppState,
actionId: string, actionId: string,
): RestAction | undefined => { ): RestAction | undefined => {
@ -225,7 +223,6 @@ export function* createActionSaga(actionPayload: ReduxAction<RestAction>) {
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
yield put(createActionSuccess(response.data)); yield put(createActionSuccess(response.data));
history.push(API_EDITOR_ID_URL(response.data.id));
} }
} catch (error) { } catch (error) {
yield put({ yield put({
@ -289,7 +286,6 @@ export function* deleteActionSaga(actionPayload: ReduxAction<{ id: string }>) {
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
yield put(deleteActionSuccess({ id })); yield put(deleteActionSuccess({ id }));
history.push(API_EDITOR_URL);
} }
} catch (error) { } catch (error) {
yield put({ 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, { import watchActionWidgetMapSagas, {
watchPropertyAndBindingUpdate, watchPropertyAndBindingUpdate,
} from "./ActionWidgetMapSagas"; } from "./ActionWidgetMapSagas";
import apiPaneSagas from "./ApiPaneSagas";
export function* rootSaga() { export function* rootSaga() {
yield all([ yield all([
@ -27,5 +28,6 @@ export function* rootSaga() {
spawn(bindingsSagas), spawn(bindingsSagas),
spawn(watchActionWidgetMapSagas), spawn(watchActionWidgetMapSagas),
spawn(watchPropertyAndBindingUpdate), 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) => { export const required = (value: any) => {
if (value === undefined || value === null || value === "") { if (value === undefined || value === null || value === "") {