From 1825fd7a19a2b32d6037b26f48b0337d85c28bf5 Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Mon, 25 Nov 2019 09:15:11 +0000 Subject: [PATCH] Drafts in API Pane --- app/client/src/actions/apiPaneActions.ts | 15 ++ app/client/src/actions/routeParamsActions.ts | 7 + .../appsmith/TextInputComponent.tsx | 18 +- .../blueprint/ButtonComponent.tsx | 4 + .../editorComponents/ApiResponseView.tsx | 3 +- .../editorComponents/CodeEditor.tsx | 3 +- .../{ => form}/fields/DatasourcesField.tsx | 0 .../{ => form}/fields/DropdownField.tsx | 0 .../{ => form}/fields/JSONEditorField.tsx | 0 .../{ => form}/fields/KeyValueFieldArray.tsx | 4 +- .../{ => form}/fields/TextField.tsx | 0 .../src/constants/ReduxActionConstants.tsx | 14 ++ .../src/constants/ValidationsMessages.ts | 2 - app/client/src/constants/WidgetConstants.tsx | 2 +- app/client/src/constants/messages.ts | 6 +- .../Applications/CreateApplicationForm.tsx | 4 +- .../APIEditor/{ApiEditorForm.tsx => Form.tsx} | 45 ++--- .../{ApiEditor.tsx => APIEditor/index.tsx} | 59 ++---- app/client/src/pages/Editor/ApiSidebar.tsx | 60 +++--- app/client/src/pages/Editor/index.tsx | 30 +-- app/client/src/pages/Editor/routes.tsx | 2 +- .../entityReducers/apiDataReducer.tsx | 5 + app/client/src/reducers/index.tsx | 2 + .../src/reducers/uiReducers/apiPaneReducer.ts | 31 +++ app/client/src/reducers/uiReducers/index.tsx | 2 + .../uiReducers/routesParamsReducer.ts | 23 +++ app/client/src/sagas/ActionSagas.ts | 6 +- app/client/src/sagas/ApiPaneSagas.ts | 181 ++++++++++++++++++ app/client/src/sagas/index.tsx | 2 + app/client/src/utils/validation/ApiForm.ts | 5 - app/client/src/utils/validation/common.ts | 2 +- 31 files changed, 390 insertions(+), 147 deletions(-) create mode 100644 app/client/src/actions/apiPaneActions.ts create mode 100644 app/client/src/actions/routeParamsActions.ts rename app/client/src/components/editorComponents/{ => form}/fields/DatasourcesField.tsx (100%) rename app/client/src/components/editorComponents/{ => form}/fields/DropdownField.tsx (100%) rename app/client/src/components/editorComponents/{ => form}/fields/JSONEditorField.tsx (100%) rename app/client/src/components/editorComponents/{ => form}/fields/KeyValueFieldArray.tsx (91%) rename app/client/src/components/editorComponents/{ => form}/fields/TextField.tsx (100%) delete mode 100644 app/client/src/constants/ValidationsMessages.ts rename app/client/src/pages/Editor/APIEditor/{ApiEditorForm.tsx => Form.tsx} (77%) rename app/client/src/pages/Editor/{ApiEditor.tsx => APIEditor/index.tsx} (61%) create mode 100644 app/client/src/reducers/uiReducers/routesParamsReducer.ts create mode 100644 app/client/src/sagas/ApiPaneSagas.ts delete mode 100644 app/client/src/utils/validation/ApiForm.ts diff --git a/app/client/src/actions/apiPaneActions.ts b/app/client/src/actions/apiPaneActions.ts new file mode 100644 index 0000000000..36cb77d357 --- /dev/null +++ b/app/client/src/actions/apiPaneActions.ts @@ -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 }, + }; +}; diff --git a/app/client/src/actions/routeParamsActions.ts b/app/client/src/actions/routeParamsActions.ts new file mode 100644 index 0000000000..285d818beb --- /dev/null +++ b/app/client/src/actions/routeParamsActions.ts @@ -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, +}); diff --git a/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx b/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx index b390b08eb9..d911b5dfd2 100644 --- a/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx @@ -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; 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 ( - - {showError && {meta && meta.touched && meta.error}} + + {showError && ( + + {meta && (meta.touched || meta.active) && meta.error} + + )} ); }; diff --git a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx index ca60d0a2b2..293a02f3b8 100644 --- a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx @@ -70,6 +70,10 @@ const ButtonWrapper = styled(AnchorButton)` } }}; } + &&.bp3-disabled { + background-color: #d0d7dd; + border: none; + } } `; export type ButtonStyleName = "primary" | "secondary" | "error"; diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 35c99081c7..01b715b57a 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -148,13 +148,14 @@ const ApiResponseView = (props: Props) => { panelComponent: ( ), }, diff --git a/app/client/src/components/editorComponents/CodeEditor.tsx b/app/client/src/components/editorComponents/CodeEditor.tsx index a312de6dbb..0050f303ee 100644 --- a/app/client/src/components/editorComponents/CodeEditor.tsx +++ b/app/client/src/components/editorComponents/CodeEditor.tsx @@ -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, diff --git a/app/client/src/components/editorComponents/fields/DatasourcesField.tsx b/app/client/src/components/editorComponents/form/fields/DatasourcesField.tsx similarity index 100% rename from app/client/src/components/editorComponents/fields/DatasourcesField.tsx rename to app/client/src/components/editorComponents/form/fields/DatasourcesField.tsx diff --git a/app/client/src/components/editorComponents/fields/DropdownField.tsx b/app/client/src/components/editorComponents/form/fields/DropdownField.tsx similarity index 100% rename from app/client/src/components/editorComponents/fields/DropdownField.tsx rename to app/client/src/components/editorComponents/form/fields/DropdownField.tsx diff --git a/app/client/src/components/editorComponents/fields/JSONEditorField.tsx b/app/client/src/components/editorComponents/form/fields/JSONEditorField.tsx similarity index 100% rename from app/client/src/components/editorComponents/fields/JSONEditorField.tsx rename to app/client/src/components/editorComponents/form/fields/JSONEditorField.tsx diff --git a/app/client/src/components/editorComponents/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx similarity index 91% rename from app/client/src/components/editorComponents/fields/KeyValueFieldArray.tsx rename to app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx index aa29070b70..3ea4574eab 100644 --- a/app/client/src/components/editorComponents/fields/KeyValueFieldArray.tsx +++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx @@ -23,8 +23,8 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { {props.fields.map((field: any, index: number) => ( {index === 0 && {props.label}} - - + + {index === props.fields.length - 1 ? ( { @@ -127,6 +137,10 @@ export interface ReduxAction { export type ReduxActionWithoutPayload = Pick, "type">; +export interface ReduxActionWithMeta extends ReduxAction { + meta: M; +} + export interface ReduxActionErrorPayload { message: string; source?: string; diff --git a/app/client/src/constants/ValidationsMessages.ts b/app/client/src/constants/ValidationsMessages.ts deleted file mode 100644 index 14df923c1b..0000000000 --- a/app/client/src/constants/ValidationsMessages.ts +++ /dev/null @@ -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"; diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index f2e49b07e4..25ad6a23e5 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -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" diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index 868ea10724..a4d21215ed 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -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"; diff --git a/app/client/src/pages/Applications/CreateApplicationForm.tsx b/app/client/src/pages/Applications/CreateApplicationForm.tsx index 37ce325cde..18a55dc021 100644 --- a/app/client/src/pages/Applications/CreateApplicationForm.tsx +++ b/app/client/src/pages/Applications/CreateApplicationForm.tsx @@ -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 = ( diff --git a/app/client/src/pages/Editor/APIEditor/ApiEditorForm.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx similarity index 77% rename from app/client/src/pages/Editor/APIEditor/ApiEditorForm.tsx rename to app/client/src/pages/Editor/APIEditor/Form.tsx index 463e110ec4..64f38fe46a 100644 --- a/app/client/src/pages/Editor/APIEditor/ApiEditorForm.tsx +++ b/app/client/src/pages/Editor/APIEditor/Form.tsx @@ -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; onSaveClick: () => void; onRunClick: () => void; @@ -87,6 +84,8 @@ type Props = APIFormProps & InjectedFormProps; const ApiEditorForm: React.FC = (props: Props) => { const { + allowSave, + allowPostBody, onSaveClick, onDeleteClick, onRunClick, @@ -99,12 +98,7 @@ const ApiEditorForm: React.FC = (props: Props) => {
- + = (props: Props) => { filled onClick={onSaveClick} loading={isSaving} + disabled={!allowSave} /> @@ -135,9 +130,8 @@ const ApiEditorForm: React.FC = (props: Props) => { /> @@ -153,10 +147,14 @@ const ApiEditorForm: React.FC = (props: Props) => { name="actionConfiguration.queryParameters" label="Params" /> - {"Post Body"} - - - + {allowPostBody && ( + + {"Post Body"} + + + + + )} @@ -167,5 +165,4 @@ const ApiEditorForm: React.FC = (props: Props) => { export default reduxForm({ form: API_EDITOR_FORM_NAME, enableReinitialize: true, - initialValues: FORM_INITIAL_VALUES, })(ApiEditorForm); diff --git a/app/client/src/pages/Editor/ApiEditor.tsx b/app/client/src/pages/Editor/APIEditor/index.tsx similarity index 61% rename from app/client/src/pages/Editor/ApiEditor.tsx rename to app/client/src/pages/Editor/APIEditor/index.tsx index 0d71c111fd..cf7a6fa1ff 100644 --- a/app/client/src/pages/Editor/ApiEditor.tsx +++ b/app/client/src/pages/Editor/APIEditor/index.tsx @@ -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) => void; - destroy: (formName: string) => void; } type Props = ReduxActionProps & @@ -46,42 +44,6 @@ const EmptyStateContainer = styled.div` `; class ApiEditor extends React.Component { - 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): 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 { render() { const { - apiPane: { isSaving, isRunning, isDeleting }, + apiPane: { isSaving, isRunning, isDeleting, drafts }, match: { params: { apiId }, }, + formData, } = this.props; + const httpMethod = _.get(formData, "actionConfiguration.httpMethod"); return ( {apiId ? ( { 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) => - dispatch(initialize(formName, data)), - destroy: (formName: string) => dispatch(destroy(formName)), }); export default connect(mapStateToProps, mapDispatchToProps)(ApiEditor); diff --git a/app/client/src/pages/Editor/ApiSidebar.tsx b/app/client/src/pages/Editor/ApiSidebar.tsx index 0cbd67c0e6..1b99c604d6 100644 --- a/app/client/src/pages/Editor/ApiSidebar.tsx +++ b/app/client/src/pages/Editor/ApiSidebar.tsx @@ -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 { }; } + componentDidMount(): void { + this.props.initApiPane(this.props.match.params.apiId); + } + componentDidUpdate(prevProps: Readonly): 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 { } 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 { }); }; + 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 { const actions: RestAction[] = search ? fuse.search(search) : data; return ( - {apiPane.isFetching ? ( + {isFetching ? ( @@ -235,16 +246,12 @@ class ApiSidebar extends React.Component { value: search, onChange: this.handleSearchChange, }} - placeholderMessage="Search" + placeholder="Search" /> {actions.map(action => ( - 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 { )} {action.name} + ))} {isCreating ? ( { text="" onClick={this.saveAction} filled - loading={apiPane.isSaving} + loading={isSaving} /> ) : ( - {!apiPane.isFetching && ( + {!isFetching && ( ({ 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); diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index 699ccc6e64..e891793dad 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -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 { } } 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)), }; }; diff --git a/app/client/src/pages/Editor/routes.tsx b/app/client/src/pages/Editor/routes.tsx index 4bf5801915..91527225e2 100644 --- a/app/client/src/pages/Editor/routes.tsx +++ b/app/client/src/pages/Editor/routes.tsx @@ -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, diff --git a/app/client/src/reducers/entityReducers/apiDataReducer.tsx b/app/client/src/reducers/entityReducers/apiDataReducer.tsx index cca1da29f5..ed0971815f 100644 --- a/app/client/src/reducers/entityReducers/apiDataReducer.tsx +++ b/app/client/src/reducers/entityReducers/apiDataReducer.tsx @@ -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; diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx index 93648193fc..9b4d83a76a 100644 --- a/app/client/src/reducers/index.tsx +++ b/app/client/src/reducers/index.tsx @@ -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; diff --git a/app/client/src/reducers/uiReducers/apiPaneReducer.ts b/app/client/src/reducers/uiReducers/apiPaneReducer.ts index 89abda0ee6..c243e6f25d 100644 --- a/app/client/src/reducers/uiReducers/apiPaneReducer.ts +++ b/app/client/src/reducers/uiReducers/apiPaneReducer.ts @@ -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; 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 }>, + ) => ({ + ...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; diff --git a/app/client/src/reducers/uiReducers/index.tsx b/app/client/src/reducers/uiReducers/index.tsx index 8778624712..6415ac19b8 100644 --- a/app/client/src/reducers/uiReducers/index.tsx +++ b/app/client/src/reducers/uiReducers/index.tsx @@ -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; diff --git a/app/client/src/reducers/uiReducers/routesParamsReducer.ts b/app/client/src/reducers/uiReducers/routesParamsReducer.ts new file mode 100644 index 0000000000..0eca7a3787 --- /dev/null +++ b/app/client/src/reducers/uiReducers/routesParamsReducer.ts @@ -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, + ) => { + return { ...action.payload }; + }, +}); + +export interface RoutesParamsReducerState { + applicationId: string; + pageId: string; +} + +export default routesParamsReducer; diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 0fa0f759e0..fdd46b65b2 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -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) { 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({ diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts new file mode 100644 index 0000000000..3eb8d4d5df --- /dev/null +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -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, +) { + 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, +) { + const { form } = actionPayload.meta; + if (form !== API_EDITOR_FORM_NAME) return; + yield all([call(validateInputSaga, actionPayload), call(updateDraftsSaga)]); +} +function* handleActionCreatedSaga(actionPayload: ReduxAction) { + 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), + ]); +} diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index 3067e75d6e..ce587ca1a4 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -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), ]); } diff --git a/app/client/src/utils/validation/ApiForm.ts b/app/client/src/utils/validation/ApiForm.ts deleted file mode 100644 index adef4e87dd..0000000000 --- a/app/client/src/utils/validation/ApiForm.ts +++ /dev/null @@ -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; -}; diff --git a/app/client/src/utils/validation/common.ts b/app/client/src/utils/validation/common.ts index 2356cbbdf4..f6a2a15390 100644 --- a/app/client/src/utils/validation/common.ts +++ b/app/client/src/utils/validation/common.ts @@ -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 === "") {