diff --git a/app/client/package.json b/app/client/package.json index 095aaed2be..3206ad5a25 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -28,6 +28,7 @@ "eslint": "^6.4.0", "flow-bin": "^0.91.0", "fontfaceobserver": "^2.1.0", + "history": "^4.10.1", "husky": "^3.0.5", "jsonpath-plus": "^1.0.0", "lint-staged": "^9.2.5", @@ -53,7 +54,6 @@ "react-router-dom": "^5.0.1", "react-scripts": "^3.1.1", "react-select": "^3.0.8", - "react-tabs": "^3.0.0", "redux": "^4.0.1", "redux-form": "^8.2.6", "redux-saga": "^1.0.0", diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index f4f553b97c..a59a73d6d6 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -3,7 +3,14 @@ import { RestAction } from "../api/ActionAPI"; export const createAction = (payload: RestAction) => { return { - type: ReduxActionTypes.CREATE_ACTION, + type: ReduxActionTypes.CREATE_ACTION_INIT, + payload, + }; +}; + +export const createActionSuccess = (payload: RestAction) => { + return { + type: ReduxActionTypes.CREATE_ACTION_SUCCESS, payload, }; }; @@ -14,13 +21,6 @@ export const fetchActions = () => { }; }; -export const selectAction = (payload: { id: string }) => { - return { - type: ReduxActionTypes.SELECT_ACTION, - payload, - }; -}; - export const fetchApiConfig = (payload: { id: string }) => { return { type: ReduxActionTypes.FETCH_ACTION, @@ -30,21 +30,35 @@ export const fetchApiConfig = (payload: { id: string }) => { export const runAction = (payload: { id: string }) => { return { - type: ReduxActionTypes.RUN_ACTION, - payload, - }; -}; - -export const deleteAction = (payload: { id: string }) => { - return { - type: ReduxActionTypes.DELETE_ACTION, + type: ReduxActionTypes.RUN_ACTION_INIT, payload, }; }; export const updateAction = (payload: { data: RestAction }) => { return { - type: ReduxActionTypes.UPDATE_ACTION, + type: ReduxActionTypes.UPDATE_ACTION_INIT, + payload, + }; +}; + +export const updateActionSuccess = (payload: { data: RestAction }) => { + return { + type: ReduxActionTypes.UPDATE_ACTION_SUCCESS, + payload, + }; +}; + +export const deleteAction = (payload: { id: string }) => { + return { + type: ReduxActionTypes.DELETE_ACTION_INIT, + payload, + }; +}; + +export const deleteActionSuccess = (payload: { id: string }) => { + return { + type: ReduxActionTypes.DELETE_ACTION_SUCCESS, payload, }; }; @@ -55,4 +69,7 @@ export default { fetchApiConfig, runAction, deleteAction, + deleteActionSuccess, + updateAction, + updateActionSuccess, }; diff --git a/app/client/src/assets/icons/form/add-new.svg b/app/client/src/assets/icons/form/add-new.svg new file mode 100644 index 0000000000..6edbd99663 --- /dev/null +++ b/app/client/src/assets/icons/form/add-new.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/components/canvas/CreatableDropdown.tsx b/app/client/src/components/canvas/CreatableDropdown.tsx index 59349221e0..98a698fb7b 100644 --- a/app/client/src/components/canvas/CreatableDropdown.tsx +++ b/app/client/src/components/canvas/CreatableDropdown.tsx @@ -38,7 +38,6 @@ class CreatableDropdown extends React.Component { onCreateOption={onCreateOption} {...input} onChange={value => input.onChange(value)} - onBlur={() => input.onBlur(input.value)} /> ); } diff --git a/app/client/src/components/canvas/Dropdown.tsx b/app/client/src/components/canvas/Dropdown.tsx index 5deeb69785..245825f15c 100644 --- a/app/client/src/components/canvas/Dropdown.tsx +++ b/app/client/src/components/canvas/Dropdown.tsx @@ -8,12 +8,13 @@ type DropdownProps = { label: string; }>; input: WrappedFieldInputProps; + placeholder: string; }; const selectStyles = { control: (styles: any) => ({ ...styles, - width: 100, + width: 120, }), }; @@ -21,12 +22,11 @@ export const BaseDropdown = (props: DropdownProps) => { const { input, options } = props; return ( input.onChange(value)} - onBlur={() => input.onBlur(input.value)} /> ); }; diff --git a/app/client/src/components/canvas/TabbedView.tsx b/app/client/src/components/canvas/TabbedView.tsx index 2810901c6e..300ad5f330 100644 --- a/app/client/src/components/canvas/TabbedView.tsx +++ b/app/client/src/components/canvas/TabbedView.tsx @@ -1,18 +1,21 @@ import React from "react"; -import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; -import "react-tabs/style/react-tabs.scss"; +import { Tab, Tabs } from "@blueprintjs/core"; import styled from "styled-components"; -const TabsWrapper = styled(Tabs)` - ul { - border-bottom-color: #d0d7dd; - li { - &.react-tabs__tab--selected { - border-color: #d0d7dd; - left: -1px; - border-radius: 0; - border-top: 5px solid ${props => props.theme.colors.primary}; - } +const TabsWrapper = styled.div` + padding: 0 5px; + .bp3-tab-indicator { + background-color: ${props => props.theme.colors.primary}; + } + .bp3-tab { + &[aria-selected="true"] { + color: ${props => props.theme.colors.primary}; + } + :hover { + color: ${props => props.theme.colors.primary}; + } + :focus { + outline: none; } } `; @@ -21,21 +24,23 @@ type TabbedViewComponentType = { tabs: Array<{ key: string; title: string; - panelComponent: () => React.ReactNode; + panelComponent: JSX.Element; }>; }; export const BaseTabbedView = (props: TabbedViewComponentType) => { return ( - + {props.tabs.map(tab => ( - {tab.title} + ))} - - {props.tabs.map(tab => ( - {tab.panelComponent()} - ))} + ); }; diff --git a/app/client/src/components/canvas/TextInputComponent.tsx b/app/client/src/components/canvas/TextInputComponent.tsx index 2a604ff4b3..cf5be43260 100644 --- a/app/client/src/components/canvas/TextInputComponent.tsx +++ b/app/client/src/components/canvas/TextInputComponent.tsx @@ -10,7 +10,7 @@ const InputStyles = css` border: 1px solid ${props => props.theme.colors.inputInactiveBorders}; border-radius: 4px; height: 32px; - background-color: ${props => props.theme.colors.inputInactiveBG}; + background-color: ${props => props.theme.colors.textOnDarkBG}; &:focus { border-color: ${props => props.theme.colors.secondary}; background-color: ${props => props.theme.colors.textOnDarkBG}; diff --git a/app/client/src/components/editor/ApiResponseView.tsx b/app/client/src/components/editor/ApiResponseView.tsx new file mode 100644 index 0000000000..63163a20c3 --- /dev/null +++ b/app/client/src/components/editor/ApiResponseView.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { connect } from "react-redux"; +import { withRouter, RouteComponentProps } from "react-router"; +import FormRow from "./FormRow"; +import { BaseText } from "../canvas/TextViewComponent"; +import { BaseTabbedView } from "../canvas/TabbedView"; +import JSONViewer from "./JSONViewer"; +import styled from "styled-components"; +import { AppState } from "../../reducers"; +import { ActionApiResponse } from "../../reducers/entityReducers/actionsReducer"; + +const ResponseWrapper = styled.div` + flex: 4; +`; +const ResponseMetaInfo = styled.div` + display: flex; + ${BaseText} { + color: #768896; + margin: 0 5px; + } +`; + +interface ReduxStateProps { + responses: { + [id: string]: ActionApiResponse; + }; +} + +const TableWrapper = styled.div` + &&& { + table { + table-layout: fixed; + width: 100%; + td { + font-size: 12px; + width: 50%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } +`; + +const ResponseHeadersView = (props: { + data: { [name: string]: Array }; +}) => { + if (!props.data) return ; + return ( + + + + + Key + Value + + + + {Object.keys(props.data).map(k => ( + + {k} + {props.data[k].join(", ")} + + ))} + + + + ); +}; + +type Props = ReduxStateProps & RouteComponentProps<{ id: string }>; + +const ApiResponseView = (props: Props) => { + const response = props.responses[props.match.params.id] || {}; + return ( + + + {response.statusCode} + + 300ms + 203 kb + + + , + }, + { + key: "headers", + title: "Response Headers", + panelComponent: , + }, + ]} + /> + + ); +}; + +const mapStateToProps = (state: AppState): ReduxStateProps => ({ + responses: state.entities.actions.responses, +}); + +export default connect(mapStateToProps)(withRouter(ApiResponseView)); diff --git a/app/client/src/components/editor/JSONEditor.tsx b/app/client/src/components/editor/JSONEditor.tsx index 4c03a0b75e..6bb5869430 100644 --- a/app/client/src/components/editor/JSONEditor.tsx +++ b/app/client/src/components/editor/JSONEditor.tsx @@ -19,11 +19,12 @@ const JSONEditor = (props: any) => { highlightActiveLine={true} width="100%" setOptions={{ - enableBasicAutocompletion: true, + enableBasicAutocompletion: false, enableLiveAutocompletion: false, enableSnippets: false, showLineNumbers: true, tabSize: 2, + useWorker: false, }} name={input.name} onBlur={aceOnBlur(input.onBlur)} diff --git a/app/client/src/components/editor/JSONViewer.tsx b/app/client/src/components/editor/JSONViewer.tsx index 26887be8e0..6eef67879d 100644 --- a/app/client/src/components/editor/JSONViewer.tsx +++ b/app/client/src/components/editor/JSONViewer.tsx @@ -5,10 +5,13 @@ import styled from "styled-components"; const JSONViewWrapper = styled.div` max-height: 600px; overflow-y: auto; + & > div { + font-size: ${props => props.theme.fontSizes[2]}px; + } `; const JSONViewer = (props: { data: JSON }) => { - if (!props.data) return null; + if (!props.data) return ; return ( { displayDataTypes={false} indentWidth={2} enableClipboard={false} - style={{ - fontSize: "10px", - }} /> ); diff --git a/app/client/src/components/editor/Sidebar.tsx b/app/client/src/components/editor/Sidebar.tsx index ef79175917..15e695c036 100644 --- a/app/client/src/components/editor/Sidebar.tsx +++ b/app/client/src/components/editor/Sidebar.tsx @@ -10,7 +10,7 @@ import WidgetSidebar from "../../pages/Editor/WidgetSidebar"; import ApiSidebar from "../../pages/Editor/ApiSidebar"; const SidebarWrapper = styled.div` - flex: 7; + flex: 9; background-color: ${props => props.theme.colors.paneBG}; padding: 5px 10px; color: ${props => props.theme.colors.textOnDarkBG}; diff --git a/app/client/src/components/fields/DropdownField.tsx b/app/client/src/components/fields/DropdownField.tsx index 4e303ccbe6..af760f50e9 100644 --- a/app/client/src/components/fields/DropdownField.tsx +++ b/app/client/src/components/fields/DropdownField.tsx @@ -9,6 +9,7 @@ interface DropdownFieldProps { label: string; value: string; }>; + placeholder: string; } const DropdownField = (props: DropdownFieldProps) => { @@ -17,6 +18,7 @@ const DropdownField = (props: DropdownFieldProps) => { name={props.name} component={BaseDropdown} options={props.options} + placeholder={props.placeholder} format={(value: string) => _.find(props.options, { value })} normalize={(option: { value: string }) => option.value} /> diff --git a/app/client/src/components/fields/ResourcesField.tsx b/app/client/src/components/fields/ResourcesField.tsx index caf5c15399..97b0f3fd77 100644 --- a/app/client/src/components/fields/ResourcesField.tsx +++ b/app/client/src/components/fields/ResourcesField.tsx @@ -32,6 +32,7 @@ const ResourcesField = ( component={CreatableDropdown} isLoading={props.resources.loading} options={options} + placeholder="Resource" onCreateOption={props.createResource} format={(value: string) => _.find(options, { value })} normalize={(option: { value: string }) => option.value} diff --git a/app/client/src/components/forms/ApiEditorForm.tsx b/app/client/src/components/forms/ApiEditorForm.tsx index 174b8a193f..afe095b3c6 100644 --- a/app/client/src/components/forms/ApiEditorForm.tsx +++ b/app/client/src/components/forms/ApiEditorForm.tsx @@ -7,8 +7,6 @@ import { HTTP_METHOD_OPTIONS, } from "../../constants/ApiEditorConstants"; import FormLabel from "../editor/FormLabel"; -import { BaseText } from "../canvas/TextViewComponent"; -import { BaseTabbedView } from "../canvas/TabbedView"; import styled from "styled-components"; import FormContainer from "../editor/FormContainer"; import { BaseButton } from "../canvas/Button"; @@ -16,7 +14,7 @@ import KeyValueFieldArray from "../fields/KeyValueFieldArray"; import JSONEditorField from "../fields/JSONEditorField"; import DropdownField from "../fields/DropdownField"; import { RestAction } from "../../api/ActionAPI"; -import JSONViewer from "../../components/editor/JSONViewer"; +import ApiResponseView from "../editor/ApiResponseView"; import { API_EDITOR_FORM_NAME } from "../../constants/forms"; import ResourcesField from "../fields/ResourcesField"; @@ -54,10 +52,11 @@ const ForwardSlash = styled.div` const RequestParamsWrapper = styled.div` flex: 5; border-right: 1px solid #d0d7dd; + overflow-y: scroll; `; -const ResponseWrapper = styled.div` - flex: 4; +const ActionButtons = styled.div` + flex: 1; `; const ActionButton = styled(BaseButton)` @@ -65,14 +64,6 @@ const ActionButton = styled(BaseButton)` margin: 0 5px; `; -const ResponseMetaInfo = styled.div` - display: flex; - ${BaseText} { - color: #768896; - margin: 0 5px; - } -`; - const JSONEditorFieldWrapper = styled.div` flex: 1; border: 1px solid #d0d7dd; @@ -84,33 +75,40 @@ interface APIFormProps { onSaveClick: () => void; onRunClick: () => void; onDeleteClick: () => void; - response: any; + isEdit: boolean; } type Props = APIFormProps & InjectedFormProps; class ApiEditorForm extends React.Component { render() { - const { onSaveClick, onDeleteClick, onRunClick } = this.props; + const { onSaveClick, onDeleteClick, onRunClick, isEdit } = this.props; return ( - - - + + + + + @@ -138,35 +136,7 @@ class ApiEditorForm extends React.Component { - - - - {this.props.response.statusCode} - - - 300ms - 203 kb - - - ( - - ), - }, - { - key: "headers", - title: "Response Headers", - panelComponent: () => ( - - ), - }, - ]} - /> - + ); diff --git a/app/client/src/constants/ApiEditorConstants.ts b/app/client/src/constants/ApiEditorConstants.ts index c66324ffe6..d15c0b9ed2 100644 --- a/app/client/src/constants/ApiEditorConstants.ts +++ b/app/client/src/constants/ApiEditorConstants.ts @@ -6,19 +6,26 @@ export const HTTP_METHOD_OPTIONS = HTTP_METHODS.map(method => ({ })); export const FORM_INITIAL_VALUES = { - resourceId: "5d808014795dc6000482bc83", actionConfiguration: { headers: [ { key: "", value: "", }, + { + key: "", + value: "", + }, ], queryParameters: [ { key: "", value: "", }, + { + key: "", + value: "", + }, ], }, }; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index ca08501d5d..42c37b1d67 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -110,7 +110,7 @@ export const theme: Theme = { color: Colors.GEYSER_LIGHT, }, ], - sidebarWidth: "300px", + sidebarWidth: "350px", headerHeight: "50px", }; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index dd211d45ab..3ebaee28f8 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -40,12 +40,17 @@ export const ReduxActionTypes: { [key: string]: string } = { FETCH_PROPERTY_PANE_CONFIGS_SUCCESS: "FETCH_PROPERTY_PANE_CONFIGS_SUCCESS", FETCH_CONFIGS_INIT: "FETCH_CONFIGS_INIT", ADD_WIDGET_REF: "ADD_WIDGET_REF", - CREATE_ACTION: "CREATE_ACTION", + CREATE_ACTION_INIT: "CREATE_ACTION_INIT", + CREATE_ACTION_SUCCESS: "CREATE_ACTION_SUCCESS", FETCH_ACTIONS_INIT: "FETCH_ACTIONS_INIT", FETCH_ACTIONS_SUCCESS: "FETCH_ACTIONS_SUCCESS", FETCH_ACTION: "FETCH_ACTION", - RUN_ACTION: "RUN_ACTION", + RUN_ACTION_INIT: "RUN_ACTION_INIT", RUN_ACTION_SUCCESS: "RUN_ACTION_SUCCESS", + UPDATE_ACTION_INIT: "UPDATE_ACTION_INIT", + UPDATE_ACTION_SUCCESS: "UPDATE_ACTION_SUCCESS", + DELETE_ACTION_INIT: "DELETE_ACTION_INIT", + DELETE_ACTION_SUCCESS: "DELETE_ACTION_SUCCESS", UPDATE_ACTION: "UPDATE_ACTION", DELETE_ACTION: "DELETE_ACTION", FETCH_RESOURCES_INIT: "FETCH_RESOURCES_INIT", @@ -71,6 +76,8 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { PROPERTY_PANE_ERROR: "PROPERTY_PANE_ERROR", FETCH_ACTIONS_ERROR: "FETCH_ACTIONS_ERROR", UPDATE_WIDGET_PROPERTY_ERROR: "UPDATE_WIDGET_PROPERTY_ERROR", + UPDATE_ACTION_ERROR: "UPDATE_ACTION_ERROR", + DELETE_ACTION_ERROR: "DELETE_ACTION_ERROR", FETCH_RESOURCES_ERROR: "FETCH_RESOURCES_ERROR", CREATE_RESOURCE_ERROR: "CREATE_RESOURCE_ERROR", }; diff --git a/app/client/src/icons/FormIcons.tsx b/app/client/src/icons/FormIcons.tsx index 5320016246..00e937041d 100644 --- a/app/client/src/icons/FormIcons.tsx +++ b/app/client/src/icons/FormIcons.tsx @@ -3,6 +3,7 @@ import { Icon } from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; import { IconProps, IconWrapper } from "../constants/IconConstants"; import { ReactComponent as DeleteIcon } from "../assets/icons/form/trash.svg"; +import { ReactComponent as AddNewIcon } from "../assets/icons/form/add-new.svg"; /* eslint-disable react/display-name */ @@ -14,6 +15,11 @@ export const FormIcons: { ), + ADD_NEW_ICON: (props: IconProps) => ( + + + + ), PLUS_ICON: (props: IconProps) => ( diff --git a/app/client/src/index.tsx b/app/client/src/index.tsx index 7a15fe1647..efe425f0df 100755 --- a/app/client/src/index.tsx +++ b/app/client/src/index.tsx @@ -7,8 +7,9 @@ import Editor from "./pages/Editor"; import PageNotFound from "./pages/common/PageNotFound"; import LoginPage from "./pages/common/LoginPage"; import * as serviceWorker from "./serviceWorker"; -import { BrowserRouter, Route, Switch } from "react-router-dom"; +import { Router, Route, Switch } from "react-router-dom"; import { createStore, applyMiddleware } from "redux"; +import history from "./utils/history"; import appReducer from "./reducers"; import { ThemeProvider, theme } from "./constants/DefaultTheme"; import createSagaMiddleware from "redux-saga"; @@ -32,14 +33,14 @@ ReactDOM.render( - + - + , diff --git a/app/client/src/normalizers/ApiFormNormalizer.ts b/app/client/src/normalizers/ApiFormNormalizer.ts new file mode 100644 index 0000000000..e45b8e71f2 --- /dev/null +++ b/app/client/src/normalizers/ApiFormNormalizer.ts @@ -0,0 +1,15 @@ +import { RestAction } from "../api/ActionAPI"; + +export const normalizeApiFormData = (formData: any): RestAction => { + return { + ...formData, + actionConfiguration: { + ...formData.actionConfiguration, + body: formData.actionConfiguration.body + ? typeof formData.actionConfiguration.body === "string" + ? JSON.parse(formData.actionConfiguration.body) + : formData.actionConfiguration.body + : null, + }, + }; +}; diff --git a/app/client/src/pages/Editor/ApiEditor.tsx b/app/client/src/pages/Editor/ApiEditor.tsx index 5b25ebd4b0..9dee731bd2 100644 --- a/app/client/src/pages/Editor/ApiEditor.tsx +++ b/app/client/src/pages/Editor/ApiEditor.tsx @@ -15,10 +15,11 @@ import { API_EDITOR_URL } from "../../constants/routes"; import { API_EDITOR_FORM_NAME } from "../../constants/forms"; import { ResourceDataState } from "../../reducers/entityReducers/resourcesReducer"; import { fetchResources } from "../../actions/resourcesActions"; +import { FORM_INITIAL_VALUES } from "../../constants/ApiEditorConstants"; +import { normalizeApiFormData } from "../../normalizers/ApiFormNormalizer"; interface ReduxStateProps { actions: RestAction[]; - response: any; formData: any; resources: ResourceDataState; } @@ -55,6 +56,9 @@ class ApiEditor extends React.Component { componentDidUpdate(prevProps: Readonly): void { const currentId = this.props.match.params.id; + if (!currentId && prevProps.match.params.id) { + this.props.initialize(API_EDITOR_FORM_NAME, FORM_INITIAL_VALUES); + } if (currentId && currentId !== prevProps.match.params.id) { const data = this.props.actions.filter( action => action.id === currentId, @@ -65,17 +69,7 @@ class ApiEditor extends React.Component { handleSubmit = (values: RestAction) => { const { formData } = this.props; - const data: RestAction = { - ...formData, - actionConfiguration: { - ...formData.actionConfiguration, - body: formData.actionConfiguration.body - ? typeof formData.actionConfiguration.body === "string" - ? JSON.parse(formData.actionConfiguration.body) - : formData.actionConfiguration.body - : null, - }, - }; + const data = normalizeApiFormData(formData); if (data.id) { this.props.updateAction(data); } else { @@ -100,7 +94,7 @@ class ApiEditor extends React.Component { onSaveClick={this.handleSaveClick} onDeleteClick={this.handleDeleteClick} onRunClick={this.handleRunClick} - response={this.props.response} + isEdit={!!this.props.match.params.id} /> ); } @@ -108,7 +102,6 @@ class ApiEditor extends React.Component { const mapStateToProps = (state: AppState): ReduxStateProps => ({ actions: state.entities.actions.data, - response: state.entities.actions.response, formData: getFormValues(API_EDITOR_FORM_NAME)(state), resources: state.entities.resources, }); diff --git a/app/client/src/pages/Editor/ApiSidebar.tsx b/app/client/src/pages/Editor/ApiSidebar.tsx index 0ae610d2b1..ccd682599d 100644 --- a/app/client/src/pages/Editor/ApiSidebar.tsx +++ b/app/client/src/pages/Editor/ApiSidebar.tsx @@ -5,15 +5,29 @@ import styled from "styled-components"; import { AppState } from "../../reducers"; import { fetchActions } from "../../actions/actionActions"; import { ActionDataState } from "../../reducers/entityReducers/actionsReducer"; -import { API_EDITOR_ID_URL } from "../../constants/routes"; +import { API_EDITOR_ID_URL, API_EDITOR_URL } from "../../constants/routes"; +import { BaseButton } from "../../components/canvas/Button"; +import { FormIcons } from "../../icons/FormIcons"; + +const ApiSidebarWrapper = styled.div` + display: flex; + height: 100%; + flex-direction: column; +`; +const ApiItemsWrapper = styled.div` + flex: 1; +`; const ApiItem = styled.div<{ isSelected: boolean }>` + height: 32px; width: 100%; - padding: 5px 10px; + padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; display: flex; + align-items: center; + margin-bottom: 2px; background-color: ${props => props.isSelected ? props.theme.colors.paneCard : props.theme.colors.paneBG} :hover { @@ -23,6 +37,7 @@ const ApiItem = styled.div<{ isSelected: boolean }>` const HTTPMethod = styled.span<{ method: string | undefined }>` flex: 1; + font-size: 12px; color: ${props => { switch (props.method) { case "GET": @@ -41,6 +56,29 @@ const HTTPMethod = styled.span<{ method: string | undefined }>` const ActionName = styled.span` flex: 3; + padding: 0 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +const CreateNewButton = styled(BaseButton)` + && { + border: none; + color: ${props => props.theme.colors.textOnDarkBG}; + height: 32px; + &:hover { + color: ${props => props.theme.colors.paneBG}; + svg { + path { + fill: ${props => props.theme.colors.paneBG}; + } + } + } + svg { + height: 12px; + } + } `; interface ReduxStateProps { @@ -58,28 +96,42 @@ type Props = ReduxStateProps & class ApiSidebar extends React.Component { componentDidMount(): void { - this.props.fetchActions(); + if (!this.props.actions.data.length) { + this.props.fetchActions(); + } } + handleCreateNew = () => { + const { history } = this.props; + history.push(API_EDITOR_URL); + }; + render() { const { actions, history, match } = this.props; const activeActionId = match.params.id; return ( - - {actions.loading && "Loading..."} - {actions.data.map(action => ( - history.push(API_EDITOR_ID_URL(action.id))} - isSelected={activeActionId === action.id} - > - - {action.actionConfiguration.httpMethod} - - {action.name} - - ))} - + + + {actions.data.map(action => ( + history.push(API_EDITOR_ID_URL(action.id))} + isSelected={activeActionId === action.id} + className={actions.loading ? "bp3-skeleton" : ""} + > + + {action.actionConfiguration.httpMethod} + + {action.name} + + ))} + + + ); } } diff --git a/app/client/src/reducers/entityReducers/actionsReducer.tsx b/app/client/src/reducers/entityReducers/actionsReducer.tsx index 7c5ecd289f..01486663de 100644 --- a/app/client/src/reducers/entityReducers/actionsReducer.tsx +++ b/app/client/src/reducers/entityReducers/actionsReducer.tsx @@ -11,23 +11,25 @@ import { RestAction } from "../../api/ActionAPI"; const initialState: ActionDataState = { list: {}, data: [], - response: { - body: null, - headers: null, - statusCode: "", - }, + responses: {}, loading: false, }; +export interface ActionApiResponse { + body: JSON; + headers: any; + statusCode: string; + timeTaken: number; + size: number; +} + export interface ActionDataState { list: { [name: string]: PageAction; }; data: RestAction[]; - response: { - body: any; - headers: any; - statusCode: string; + responses: { + [id: string]: ActionApiResponse; }; loading: boolean; } @@ -42,24 +44,51 @@ const actionsReducer = createReducer(initialState, { }); return { ...state, list: { ...actionMap } }; }, - [ReduxActionTypes.FETCH_ACTIONS_INIT]: (state: ActionDataState) => { - return { ...state, loading: true }; - }, + [ReduxActionTypes.FETCH_ACTIONS_INIT]: (state: ActionDataState) => ({ + ...state, + loading: true, + }), [ReduxActionTypes.FETCH_ACTIONS_SUCCESS]: ( state: ActionDataState, action: ReduxAction, - ) => { - return { ...state, data: action.payload, loading: false }; - }, - [ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: (state: ActionDataState) => { - return { ...state, data: [], loading: false }; - }, + ) => ({ + ...state, + data: action.payload, + loading: false, + }), + [ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: (state: ActionDataState) => ({ + ...state, + data: [], + loading: false, + }), [ReduxActionTypes.RUN_ACTION_SUCCESS]: ( state: ActionDataState, - action: ReduxAction, - ) => { - return { ...state, response: action.payload }; - }, + action: ReduxAction<{ [id: string]: ActionApiResponse }>, + ) => ({ ...state, responses: { ...state.responses, ...action.payload } }), + [ReduxActionTypes.CREATE_ACTION_SUCCESS]: ( + state: ActionDataState, + action: ReduxAction, + ) => ({ + ...state, + data: state.data.concat([action.payload]), + }), + [ReduxActionTypes.UPDATE_ACTION_SUCCESS]: ( + state: ActionDataState, + action: ReduxAction<{ data: RestAction }>, + ) => ({ + ...state, + data: state.data.map(d => { + if (d.id === action.payload.data.id) return action.payload.data; + return d; + }), + }), + [ReduxActionTypes.DELETE_ACTION_SUCCESS]: ( + state: ActionDataState, + action: ReduxAction<{ id: string }>, + ) => ({ + ...state, + data: state.data.filter(d => d.id !== action.payload.id), + }), }); export default actionsReducer; diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index ac465da586..7315dcd3b4 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -4,7 +4,14 @@ import { ReduxActionTypes, } from "../constants/ReduxActionConstants"; import { Intent } from "@blueprintjs/core"; -import { all, call, select, put, takeEvery } from "redux-saga/effects"; +import { + all, + call, + select, + put, + takeEvery, + takeLatest, +} from "redux-saga/effects"; import { initialize } from "redux-form"; import { ActionPayload, PageAction } from "../constants/ActionConstants"; import ActionAPI, { @@ -18,8 +25,14 @@ import _ from "lodash"; import { mapToPropList } from "../utils/AppsmithUtils"; import AppToaster from "../components/editor/ToastComponent"; import { GenericApiResponse } from "../api/ApiResponses"; -import { fetchActions } from "../actions/actionActions"; import { API_EDITOR_FORM_NAME } from "../constants/forms"; +import { + createActionSuccess, + deleteActionSuccess, + updateActionSuccess, +} from "../actions/actionActions"; +import { API_EDITOR_ID_URL, API_EDITOR_URL } from "../constants/routes"; +import history from "../utils/history"; const getDataTree = (state: AppState) => { return state.entities; @@ -82,7 +95,8 @@ export function* createActionSaga(actionPayload: ReduxAction) { message: `${actionPayload.payload.name} Action created`, intent: Intent.SUCCESS, }); - yield put(fetchActions()); + yield put(createActionSuccess(response.data)); + history.push(API_EDITOR_ID_URL(response.data.id)); } } @@ -116,7 +130,7 @@ export function* runActionSaga(actionPayload: ReduxAction<{ id: string }>) { const response: any = yield ActionAPI.executeAction({ actionId: id }); yield put({ type: ReduxActionTypes.RUN_ACTION_SUCCESS, - payload: response, + payload: { [id]: response }, }); } @@ -135,7 +149,7 @@ export function* updateActionSaga( message: `${actionPayload.payload.data.name} Action updated`, intent: Intent.SUCCESS, }); - yield put(fetchActions()); + yield put(updateActionSuccess({ data: response.data })); } else { AppToaster.show({ message: "Error occurred when updating action", @@ -154,7 +168,8 @@ export function* deleteActionSaga(actionPayload: ReduxAction<{ id: string }>) { message: `${response.data.name} Action deleted`, intent: Intent.SUCCESS, }); - yield put(fetchActions()); + yield put(deleteActionSuccess({ id })); + history.push(API_EDITOR_URL); } else { AppToaster.show({ message: "Error occurred when deleting action", @@ -166,11 +181,11 @@ export function* deleteActionSaga(actionPayload: ReduxAction<{ id: string }>) { export function* watchActionSagas() { yield all([ takeEvery(ReduxActionTypes.FETCH_ACTIONS_INIT, fetchActionsSaga), - takeEvery(ReduxActionTypes.EXECUTE_ACTION, executeActionSaga), - takeEvery(ReduxActionTypes.CREATE_ACTION, createActionSaga), + takeLatest(ReduxActionTypes.EXECUTE_ACTION, executeActionSaga), + takeLatest(ReduxActionTypes.CREATE_ACTION_INIT, createActionSaga), takeEvery(ReduxActionTypes.FETCH_ACTION, fetchActionSaga), - takeEvery(ReduxActionTypes.RUN_ACTION, runActionSaga), - takeEvery(ReduxActionTypes.UPDATE_ACTION, updateActionSaga), - takeEvery(ReduxActionTypes.DELETE_ACTION, deleteActionSaga), + takeLatest(ReduxActionTypes.RUN_ACTION_INIT, runActionSaga), + takeLatest(ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga), + takeLatest(ReduxActionTypes.DELETE_ACTION_INIT, deleteActionSaga), ]); } diff --git a/app/client/src/sagas/ResourcesSagas.ts b/app/client/src/sagas/ResourcesSagas.ts index 973dcf9d76..da7918b438 100644 --- a/app/client/src/sagas/ResourcesSagas.ts +++ b/app/client/src/sagas/ResourcesSagas.ts @@ -10,6 +10,7 @@ import ResourcesApi, { CreateResourceConfig, Resource, } from "../api/ResourcesApi"; +import { API_EDITOR_FORM_NAME } from "../constants/forms"; function* fetchResourcesSaga() { const response: GenericApiResponse< @@ -37,7 +38,7 @@ function* createResourceSaga(actionPayload: ReduxAction) { type: ReduxActionTypes.CREATE_RESOURCE_SUCCESS, payload: response.data, }); - yield put(change("ApiEditorForm", "resourceId", response.data.id)); + yield put(change(API_EDITOR_FORM_NAME, "resourceId", response.data.id)); } else { yield put({ type: ReduxActionTypes.CREATE_RESOURCE_ERROR, diff --git a/app/client/src/utils/history.ts b/app/client/src/utils/history.ts new file mode 100644 index 0000000000..84c862066d --- /dev/null +++ b/app/client/src/utils/history.ts @@ -0,0 +1,2 @@ +const createHistory = require("history").createBrowserHistory; +export default createHistory(); diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 2f0dd11311..7907ac238f 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -5587,7 +5587,7 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -history@^4.9.0: +history@^4.10.1, history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==