diff --git a/app/client/.lintstagedrc b/app/client/.lintstagedrc index f72b289722..7f509df656 100644 --- a/app/client/.lintstagedrc +++ b/app/client/.lintstagedrc @@ -1,3 +1,4 @@ { - "src/**/*.tsx": ["npx eslint --fix", "npx prettier --write", "git add"] + "src/**/*.tsx": ["npx eslint --fix", "npx prettier --write", "git add"], + "src/**/*.ts": ["npx eslint --fix", "npx prettier --write", "git add"], } diff --git a/app/client/package.json b/app/client/package.json index b6ebc6207b..74e4997bbf 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -42,17 +42,22 @@ "prettier": "^1.18.2", "re-reselect": "^3.4.0", "react": "^16.7.0", + "react-ace": "^8.0.0", "react-dnd": "^9.3.4", "react-dnd-html5-backend": "^9.3.4", "react-dnd-touch-backend": "^9.4.0", "react-dom": "^16.7.0", + "react-json-view": "^1.19.1", "react-netlify-identity": "^0.1.9", "react-redux": "^6.0.0", "react-rnd": "^10.1.1", "react-router": "^5.0.1", "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", "reselect": "^4.0.0", "styled-components": "^4.1.3", @@ -79,6 +84,9 @@ "not op_mini all" ], "devDependencies": { + "@types/react-select": "^3.0.5", + "@types/react-tabs": "^2.3.1", + "@types/redux-form": "^8.1.9", "@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/parser": "^2.0.0", "dotenv": "^8.1.0", diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts new file mode 100644 index 0000000000..f4f553b97c --- /dev/null +++ b/app/client/src/actions/actionActions.ts @@ -0,0 +1,58 @@ +import { ReduxActionTypes } from "../constants/ReduxActionConstants"; +import { RestAction } from "../api/ActionAPI"; + +export const createAction = (payload: RestAction) => { + return { + type: ReduxActionTypes.CREATE_ACTION, + payload, + }; +}; + +export const fetchActions = () => { + return { + type: ReduxActionTypes.FETCH_ACTIONS_INIT, + }; +}; + +export const selectAction = (payload: { id: string }) => { + return { + type: ReduxActionTypes.SELECT_ACTION, + payload, + }; +}; + +export const fetchApiConfig = (payload: { id: string }) => { + return { + type: ReduxActionTypes.FETCH_ACTION, + payload, + }; +}; + +export const runAction = (payload: { id: string }) => { + return { + type: ReduxActionTypes.RUN_ACTION, + payload, + }; +}; + +export const deleteAction = (payload: { id: string }) => { + return { + type: ReduxActionTypes.DELETE_ACTION, + payload, + }; +}; + +export const updateAction = (payload: { data: RestAction }) => { + return { + type: ReduxActionTypes.UPDATE_ACTION, + payload, + }; +}; + +export default { + createAction, + fetchActions, + fetchApiConfig, + runAction, + deleteAction, +}; diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index da00dbc06e..4f30b499d5 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -1,11 +1,11 @@ import API, { HttpMethod } from "./Api"; -import { ApiResponse } from "./ApiResponses"; +import { ApiResponse, GenericApiResponse } from "./ApiResponses"; import { APIRequest } from "../constants/ApiConstants"; -import { mapToPropList } from "../utils/AppsmithUtils"; export interface CreateActionRequest extends APIRequest { resourceId: string; - actionName: string; + pageId: string; + name: string; actionConfiguration: T; } @@ -15,9 +15,10 @@ export interface UpdateActionRequest extends CreateActionRequest { export interface APIConfig { resourceId: string; - actionName: string; + pageId: string; + name: string; requestHeaders: Record; - method: HttpMethod; + httpMethod: HttpMethod; path: string; body: JSON; queryParams: Record; @@ -31,9 +32,9 @@ export interface Property { export interface APIConfigRequest { headers: Property[]; - httpMethod: HttpMethod; + httpMethod: string; path: string; - body: JSON; + body: JSON | string; queryParameters: Property[]; } @@ -42,8 +43,17 @@ export interface QueryConfig { } export interface ActionCreateUpdateResponse extends ApiResponse { - actionId: string; - dynamicBindingMap: Record; + id: string; + jsonPathKeys: Record; +} + +export interface RestAction { + id: string; + name: string; + resourceId: string; + pluginId: string; + pageId: string; + actionConfiguration: APIConfigRequest; } export interface ExecuteActionRequest extends APIRequest { @@ -59,35 +69,26 @@ export interface ExecuteActionResponse extends ApiResponse { class ActionAPI extends API { static url = "v1/actions"; - static createAPI(apiConfig: APIConfig): Promise { - const createAPI: CreateActionRequest = { - resourceId: apiConfig.resourceId, - actionName: apiConfig.actionName, - actionConfiguration: { - httpMethod: apiConfig.method, - path: apiConfig.path, - body: apiConfig.body, - headers: mapToPropList(apiConfig.requestHeaders), - queryParameters: mapToPropList(apiConfig.queryParams), - }, - }; - return API.post(ActionAPI.url, createAPI); + static fetchAPI(id: string): Promise> { + return API.get(`${ActionAPI.url}/${id}`); } - static updateAPI(apiConfig: APIConfig): Promise { - const updateAPI: UpdateActionRequest = { - resourceId: apiConfig.resourceId, - actionName: apiConfig.actionName, - actionId: apiConfig.actionId, - actionConfiguration: { - httpMethod: apiConfig.method, - path: apiConfig.path, - body: apiConfig.body, - headers: mapToPropList(apiConfig.requestHeaders), - queryParameters: mapToPropList(apiConfig.queryParams), - }, - }; - return API.post(ActionAPI.url, updateAPI); + static createAPI(apiConfig: RestAction): Promise { + return API.post(ActionAPI.url, apiConfig); + } + + static fetchActions(): Promise> { + return API.get(ActionAPI.url); + } + + static updateAPI( + apiConfig: Partial, + ): Promise { + return API.put(`${ActionAPI.url}/${apiConfig.id}`, null, apiConfig); + } + + static deleteAction(id: string) { + return API.delete(`${ActionAPI.url}/${id}`); } static createQuery( diff --git a/app/client/src/api/Api.tsx b/app/client/src/api/Api.tsx index 42936c5092..9a31e53553 100644 --- a/app/client/src/api/Api.tsx +++ b/app/client/src/api/Api.tsx @@ -64,6 +64,12 @@ class Api { ); } + static delete(url: string, queryParams?: any) { + return axiosInstance.delete( + url + this.convertObjectToQueryParams(queryParams), + ); + } + static convertObjectToQueryParams(object: any): string { if (!_.isNil(object)) { const paramArray: string[] = _.map(_.keys(object), key => { diff --git a/app/client/src/api/ApiResponses.tsx b/app/client/src/api/ApiResponses.tsx index 7508c9ecba..a81f112eda 100644 --- a/app/client/src/api/ApiResponses.tsx +++ b/app/client/src/api/ApiResponses.tsx @@ -14,6 +14,11 @@ export type ApiResponse = { data: any; }; +export type GenericApiResponse = { + responseMeta: ResponseMeta; + data: T; +}; + // NO_RESOURCE_FOUND, 1000, "Unable to find {0} with id {1}" // INVALID_PARAMTER, 4000, "Invalid parameter {0} provided in the input" // PLUGIN_NOT_INSTALLED, 4001, "Plugin {0} not installed" diff --git a/app/client/src/assets/icons/form/trash.svg b/app/client/src/assets/icons/form/trash.svg new file mode 100755 index 0000000000..8fa3b294a1 --- /dev/null +++ b/app/client/src/assets/icons/form/trash.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/client/src/components/canvas/Button.tsx b/app/client/src/components/canvas/Button.tsx new file mode 100644 index 0000000000..2e7b8f96b1 --- /dev/null +++ b/app/client/src/components/canvas/Button.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import { Button, IButtonProps, MaybeElement } from "@blueprintjs/core"; +import styled, { css } from "styled-components"; +import { Container } from "../../editorComponents/ContainerComponent"; +import { TextComponentProps } from "./TextViewComponent"; + +const ButtonColorStyles = css` + color: ${props => { + if (props.filled) return props.theme.colors.textOnDarkBG; + if (props.styleName) { + if (props.styleName === "secondary") + return props.theme.colors.OXFORD_BLUE; + return props.theme.colors[props.styleName]; + } + }}; +`; + +const ButtonWrapper = styled(Button)` + && { + ${ButtonColorStyles}; + width: 100%; + height: 100%; + transition: background-color 0.2s; + background-color: ${props => + props.filled && props.styleName && props.theme.colors[props.styleName]}; + border: 1px solid + ${props => + props.styleName + ? props.theme.colors[props.styleName] + : props.theme.colors.secondary}; + border-radius: 4px; + font-weight: bold; + outline: none; + &&:hover, + &&:focus { + ${ButtonColorStyles}; + background-color: ${props => { + if (!props.filled) return props.theme.colors.secondaryDarker; + if (props.styleName !== "secondary") { + return props.theme.colors[`${props.styleName}Darker`]; + } + }}; + border-color: ${props => { + if (!props.filled) return; + if (props.styleName !== "secondary") { + return props.theme.colors[`${props.styleName}Darker`]; + } + }}; + } + &&:active { + ${ButtonColorStyles}; + background-color: ${props => { + if (!props.filled) return props.theme.colors.secondaryDarkest; + if (props.styleName !== "secondary") { + return props.theme.colors[`${props.styleName}Darkest`]; + } + }}; + border-color: ${props => { + if (!props.filled) return; + if (props.styleName !== "secondary") { + return props.theme.colors[`${props.styleName}Darkest`]; + } + }}; + } + } +`; + +type ButtonStyleProps = { + styleName?: "primary" | "secondary" | "error"; + filled?: boolean; +}; + +// To be used in any other part of the app +export const BaseButton = (props: IButtonProps & ButtonStyleProps) => { + return ; +}; + +BaseButton.defaultProps = { + styleName: "secondary", + text: "Button Text", + minimal: true, +}; + +interface ButtonContainerProps extends TextComponentProps { + icon?: MaybeElement; + onClick?: (event: React.MouseEvent) => void; +} + +// To be used with the canvas +const ButtonContainer = (props: ButtonContainerProps) => { + return ( + + + + ); +}; + +export default ButtonContainer; diff --git a/app/client/src/components/canvas/CreatableDropdown.tsx b/app/client/src/components/canvas/CreatableDropdown.tsx new file mode 100644 index 0000000000..1bfc17f615 --- /dev/null +++ b/app/client/src/components/canvas/CreatableDropdown.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import Creatable from "react-select/creatable"; + +type DropdownProps = { + options: Array<{ + value: string; + label: string; + }>; + placeholder: string; +}; + +const selectStyles = { + container: (styles: any) => ({ + ...styles, + flex: 1, + }), +}; + +class CreatableDropdown extends React.Component { + render() { + return ( + + ); + } +} + +export default CreatableDropdown; diff --git a/app/client/src/components/canvas/Dropdown.tsx b/app/client/src/components/canvas/Dropdown.tsx new file mode 100644 index 0000000000..5deeb69785 --- /dev/null +++ b/app/client/src/components/canvas/Dropdown.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import Select from "react-select"; +import { WrappedFieldInputProps } from "redux-form"; + +type DropdownProps = { + options: Array<{ + value: string; + label: string; + }>; + input: WrappedFieldInputProps; +}; + +const selectStyles = { + control: (styles: any) => ({ + ...styles, + width: 100, + }), +}; + +export const BaseDropdown = (props: DropdownProps) => { + const { input, options } = props; + return ( +