diff --git a/app/client/.gitignore b/app/client/.gitignore index 3c972dd653..436ff4fe1b 100755 --- a/app/client/.gitignore +++ b/app/client/.gitignore @@ -24,5 +24,5 @@ yarn-debug.log* yarn-error.log* /out -/public/fonts -/src/assets/icon/fonts +/public/fonts/* +/src/assets/icons/fonts/* diff --git a/app/client/package.json b/app/client/package.json index d1e9623dc0..9cafe6b830 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -10,6 +10,7 @@ "@blueprintjs/core": "^3.11.0", "@blueprintjs/datetime": "^3.6.0", "@blueprintjs/icons": "^3.5.0", + "@blueprintjs/select": "^3.10.0", "@blueprintjs/table": "^3.4.0", "@sentry/browser": "^5.6.3", "@types/axios": "^0.14.0", @@ -52,7 +53,7 @@ "redux-saga": "^1.0.0", "styled-components": "^4.1.3", "ts-loader": "^6.0.4", - "typescript": "^3.2.4" + "typescript": "^3.6.3" }, "scripts": { "start": "react-scripts start", @@ -84,7 +85,8 @@ "eslint-plugin-prettier": "^3.1.0", "eslint-plugin-react": "^7.14.3", "icon-font-generator": "^2.1.10", - "redux-devtools": "^3.5.0" + "redux-devtools": "^3.5.0", + "redux-devtools-extension": "^2.13.8" }, "husky": { "hooks": { diff --git a/app/client/src/actions/controlActions.tsx b/app/client/src/actions/controlActions.tsx new file mode 100644 index 0000000000..5dc2fba3d2 --- /dev/null +++ b/app/client/src/actions/controlActions.tsx @@ -0,0 +1,25 @@ +import { + ReduxActionTypes, + ReduxAction, +} from "../constants/ReduxActionConstants"; + +export const updateWidgetProperty = ( + widgetId: string, + propertyName: string, + propertyValue: any, +): ReduxAction => { + return { + type: ReduxActionTypes.UPDATE_WIDGET_PROPERTY, + payload: { + widgetId, + propertyName, + propertyValue, + }, + }; +}; + +export interface UpdateWidgetPropertyPayload { + widgetId: string; + propertyName: string; + propertyValue: any; +} diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index ee835ce088..0ec62cd137 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -59,11 +59,11 @@ export const removeWidget = ( }; }; -export const loadCanvasWidgets = ( +export const updateCanvas = ( payload: LoadCanvasWidgetsPayload, ): ReduxAction => { return { - type: ReduxActionTypes.LOAD_CANVAS_WIDGETS, + type: ReduxActionTypes.UPDATE_CANVAS, payload, }; }; diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index c5c0b4059c..76568925b8 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -1,7 +1,7 @@ import API, { HttpMethod } from "./Api"; import { ApiResponse } from "./ApiResponses"; import { APIRequest } from "./ApiRequests"; -import _ from "lodash"; +import { mapToPropList } from "../utils/AppsmithUtils"; export interface CreateActionRequest extends APIRequest { resourceId: string; @@ -48,7 +48,7 @@ export interface ActionCreateUpdateResponse extends ApiResponse { export interface ExecuteActionRequest extends APIRequest { actionId: string; - dynamicBindingMap: Record; + dynamicBindingList?: Property[]; } export interface ExecuteActionResponse extends ApiResponse { @@ -67,12 +67,8 @@ class ActionAPI extends API { httpMethod: apiConfig.method, path: apiConfig.path, body: apiConfig.body, - headers: _.map(apiConfig.requestHeaders, (value, key) => { - return { key: key, value: value }; - }), - queryParameters: _.map(apiConfig.queryParams, (value, key) => { - return { key: key, value: value }; - }), + headers: mapToPropList(apiConfig.requestHeaders), + queryParameters: mapToPropList(apiConfig.queryParams), }, }; return API.post(ActionAPI.url, createAPI); @@ -87,12 +83,8 @@ class ActionAPI extends API { httpMethod: apiConfig.method, path: apiConfig.path, body: apiConfig.body, - headers: _.map(apiConfig.requestHeaders, (value, key) => { - return { key: key, value: value }; - }), - queryParameters: _.map(apiConfig.queryParams, (value, key) => { - return { key: key, value: value }; - }), + headers: mapToPropList(apiConfig.requestHeaders), + queryParameters: mapToPropList(apiConfig.queryParams), }, }; return API.post(ActionAPI.url, updateAPI); @@ -113,7 +105,7 @@ class ActionAPI extends API { static executeAction( executeAction: ExecuteActionRequest, ): Promise { - return API.post(ActionAPI.url, executeAction); + return API.post(ActionAPI.url + "/execute", executeAction); } } diff --git a/app/client/src/api/Api.tsx b/app/client/src/api/Api.tsx index 690287dab6..a08acc8827 100644 --- a/app/client/src/api/Api.tsx +++ b/app/client/src/api/Api.tsx @@ -53,7 +53,7 @@ class Api { ); } - static post(url: string, queryParams?: any, body?: any) { + static post(url: string, body?: any, queryParams?: any) { return axiosInstance.post( url + this.convertObjectToQueryParams(queryParams), body, diff --git a/app/client/src/api/PageApi.tsx b/app/client/src/api/PageApi.tsx index da0bd3162a..510970dd44 100644 --- a/app/client/src/api/PageApi.tsx +++ b/app/client/src/api/PageApi.tsx @@ -18,8 +18,8 @@ export interface SavePageRequest { export interface PageLayout { id: string; - dsl: ContainerWidgetProps; - actions?: PageAction[]; + dsl: Partial>; + actions: PageAction[]; } export type FetchPageResponse = ApiResponse & { @@ -36,9 +36,9 @@ export interface SavePageResponse { } class PageApi extends Api { - static url = "/pages"; + static url = "api/v1/pages"; static getLayoutUpdateURL = (pageId: string, layoutId: string) => { - return `/layouts/${layoutId}/pages/${pageId}`; + return `api/v1/layouts/${layoutId}/pages/${pageId}`; }; static fetchPage(pageRequest: FetchPageRequest): Promise { diff --git a/app/client/src/constants/ActionConstants.tsx b/app/client/src/constants/ActionConstants.tsx index 80d4f0bbc8..147baa1741 100644 --- a/app/client/src/constants/ActionConstants.tsx +++ b/app/client/src/constants/ActionConstants.tsx @@ -22,18 +22,11 @@ export type ActionType = | "DOWNLOAD"; export interface ActionPayload { + actionId: string; actionType: ActionType; contextParams: Record; } -export interface APIActionPayload extends ActionPayload { - apiId: string; -} - -export interface QueryActionPayload extends ActionPayload { - queryId: string; -} - export type NavigationType = "NEW_TAB" | "INLINE"; export interface NavigateActionPayload extends ActionPayload { @@ -71,5 +64,5 @@ export interface PageAction { actionId: string; actionType: ActionType; actionName: string; - dynamicBindings: string[]; + dynamicBindings?: string[]; } diff --git a/app/client/src/constants/ApiConstants.tsx b/app/client/src/constants/ApiConstants.tsx index 04924d604f..a28c8a4f2c 100644 --- a/app/client/src/constants/ApiConstants.tsx +++ b/app/client/src/constants/ApiConstants.tsx @@ -9,9 +9,9 @@ export type EncodingType = "gzip"; export const PROD_BASE_URL = "https://mobtools.com/api/"; export const MOCK_BASE_URL = "https://f78ff9dd-2c08-45f1-9bf9-8c670a1bb696.mock.pstmn.io"; -export const STAGE_BASE_URL = "https://appsmith-test.herokuapp.com/api/v1/"; +export const STAGE_BASE_URL = "https://appsmith-test.herokuapp.com"; export const BASE_URL = STAGE_BASE_URL; -export const REQUEST_TIMEOUT_MS = 5000; +export const REQUEST_TIMEOUT_MS = 2000; export const REQUEST_HEADERS: APIHeaders = { Accept: "application/json", "Content-Type": "application/json", diff --git a/app/client/src/constants/PropertyControlConstants.tsx b/app/client/src/constants/PropertyControlConstants.tsx new file mode 100644 index 0000000000..527133dd72 --- /dev/null +++ b/app/client/src/constants/PropertyControlConstants.tsx @@ -0,0 +1,17 @@ +export type ControlType = + | "INPUT_TEXT" + | "ICON_PICKER" + | "SEGMENT_CONTROL" + | "SWITCH" + | "CHECKBOX" + | "DATE_PICKER" + | "DROP_DOWN" + | "COLOR_PICKER" + | "TIMEZONE_PICKER" + | "ACTION_SELECTOR" + | "RECORD_ACTION_SELECTOR" + | "OPTION_INPUT" + | "IMAGE_PICKER" + | "SHAPE_PICKER" + | "VALIDATION_INPUT" + | "ZOOM"; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 0776d74363..4e29a739d3 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -1,7 +1,7 @@ import { WidgetProps, WidgetCardProps } from "../widgets/BaseWidget"; export const ReduxActionTypes: { [key: string]: string } = { - LOAD_CANVAS_WIDGETS: "LOAD_CANVAS_WIDGETS", + UPDATE_CANVAS: "UPDATE_CANVAS", FETCH_CANVAS: "FETCH_CANVAS", CLEAR_CANVAS: "CLEAR_CANVAS", FETCH_PAGE: "FETCH_PAGE", @@ -13,6 +13,7 @@ export const ReduxActionTypes: { [key: string]: string } = { UNDO_CANVAS_ACTION: "UNDO_CANVAS_ACTION", REDO_CANVAS_ACTION: "REDO_CANVAS_ACTION", LOAD_WIDGET_CONFIG: "LOAD_WIDGET_CONFIG", + LOAD_PROPERTY_CONFIG: "LOAD_PROPERTY_CONFIG", PUBLISH: "PUBLISH", FETCH_WIDGET_CARDS: "FETCH_WIDGET_CARDS", SUCCESS_FETCHING_WIDGET_CARDS: "SUCCESS_FETCHING_WIDGET_CARDS", @@ -33,6 +34,8 @@ export const ReduxActionTypes: { [key: string]: string } = { WIDGET_MOVE: "WIDGET_MOVE", WIDGET_RESIZE: "WIDGET_RESIZE", WIDGET_DELETE: "WIDGET_DELETE", + SHOW_PROPERTY_PANE: "SHOW_PROPERTY_PANE", + UPDATE_WIDGET_PROPERTY: "UPDATE_WIDGET_PROPERTY", }; export type ReduxActionType = (typeof ReduxActionTypes)[keyof typeof ReduxActionTypes]; @@ -48,8 +51,8 @@ export interface LoadCanvasWidgetsPayload { layoutId: string; } -export interface LoadWidgetConfigPayload { - [widgetId: string]: WidgetProps; +export interface ShowPropertyPanePayload { + widgetId: string; } // export interface LoadAPIResponsePayload extends ExecuteActionResponse {} diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 9b8839ab80..be21d05f84 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -10,8 +10,7 @@ export type WidgetType = | "CHECKBOX_WIDGET" | "RADIO_GROUP_WIDGET" | "INPUT_WIDGET" - | "SWITCH_WIDGET" - | "ALERT_WIDGET"; + | "SWITCH_WIDGET"; export const WidgetTypes: { [id: string]: WidgetType } = { BUTTON_WIDGET: "BUTTON_WIDGET", @@ -26,7 +25,6 @@ export const WidgetTypes: { [id: string]: WidgetType } = { DROP_DOWN_WIDGET: "DROP_DOWN_WIDGET", CHECKBOX_WIDGET: "CHECKBOX_WIDGET", RADIO_GROUP_WIDGET: "RADIO_GROUP_WIDGET", - ALERT_WIDGET: "ALERT_WIDGET", }; export type ContainerOrientation = "HORIZONTAL" | "VERTICAL"; diff --git a/app/client/src/index.tsx b/app/client/src/index.tsx index 75ab6ae070..da30db4e65 100755 --- a/app/client/src/index.tsx +++ b/app/client/src/index.tsx @@ -10,7 +10,6 @@ import * as serviceWorker from "./serviceWorker"; import { BrowserRouter, Route, Switch } from "react-router-dom"; import { createStore, applyMiddleware } from "redux"; import appReducer from "./reducers"; -import WidgetBuilderRegistry from "./utils/WidgetRegistry"; import { ThemeProvider, theme } from "./constants/DefaultTheme"; import createSagaMiddleware from "redux-saga"; import { rootSaga } from "./sagas"; @@ -18,11 +17,14 @@ import { DndProvider } from "react-dnd"; import HTML5Backend from "react-dnd-html5-backend"; import { appInitializer } from "./utils/AppsmithUtils"; import ProtectedRoute from "./pages/common/ProtectedRoute"; +import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction"; appInitializer(); -WidgetBuilderRegistry.registerWidgetBuilders(); const sagaMiddleware = createSagaMiddleware(); -const store = createStore(appReducer, applyMiddleware(sagaMiddleware)); +const store = createStore( + appReducer, + composeWithDevTools(applyMiddleware(sagaMiddleware)), +); sagaMiddleware.run(rootSaga); ReactDOM.render( diff --git a/app/client/src/mockResponses/PageMockResponse.tsx b/app/client/src/mockResponses/PageMockResponse.tsx new file mode 100644 index 0000000000..fc51e106b2 --- /dev/null +++ b/app/client/src/mockResponses/PageMockResponse.tsx @@ -0,0 +1,56 @@ +import { FetchPageResponse } from "../api/PageApi"; +import { generateReactKey } from "../utils/generators"; +import { WidgetType } from "../constants/WidgetConstants"; +import { ActionType } from "../constants/ActionConstants"; + +const PageMockResponse: FetchPageResponse = { + responseMeta: { + status: 200, + success: true, + }, + data: { + id: generateReactKey(), + applicationId: generateReactKey(), + name: "Mock Page", + layouts: [ + { + id: generateReactKey(), + dsl: { + widgetId: "0", + type: "CONTAINER_WIDGET" as WidgetType, + topRow: 2, + leftColumn: 2, + rightColumn: 10, + bottomRow: 10, + children: [ + { + widgetId: "1", + type: "BUTTON_WIDGET" as WidgetType, + topRow: 2, + leftColumn: 2, + text: "submit", + rightColumn: 10, + bottomRow: 10, + onClick: [ + { + actionId: "5d8082e2795dc6000482bc84", + actionType: "API", + }, + ], + }, + ], + }, + actions: [ + { + actionId: "5d8082e2795dc6000482bc84", + actionType: "API" as ActionType, + actionName: "getUsers", + dynamicBindings: ["$.apiData.0.name"], + }, + ], + }, + ], + }, +}; + +export default PageMockResponse; diff --git a/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx b/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx new file mode 100644 index 0000000000..57e07c31d0 --- /dev/null +++ b/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx @@ -0,0 +1,555 @@ +import { PropertyPaneConfigState } from "../reducers/entityReducers/propertyPaneConfigReducer"; + +const PropertyPaneConfigResponse: PropertyPaneConfigState = { + config: { + BUTTON_WIDGET: [ + { + sectionName: "General", + id: "1", + children: [ + { + id: "1.1", + propertyName: "text", + label: "Button Text", + controlType: "INPUT_TEXT", + placeholderText: "Enter button text here", + }, + { + id: "1.2", + propertyName: "buttonStyle", + label: "Button Style", + controlType: "DROP_DOWN", + options: [ + { label: "Primary Button", value: "PRIMARY_BUTTON" }, + { label: "Secondary Button", value: "SECONDARY_BUTTON" }, + ], + }, + { + id: "1.3", + propertyName: "isDisabled", + label: "Disabled", + controlType: "SWITCH", + }, + { + id: "1.4", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + { + sectionName: "Actions", + id: "2", + children: [ + { + id: "2.1", + propertyName: "onClick", + label: "onClick", + controlType: "ACTION_SELECTOR", + }, + ], + }, + ], + TEXT_WIDGET: [ + { + sectionName: "General", + id: "3", + children: [ + { + id: "3.1", + propertyName: "text", + label: "Text", + controlType: "INPUT_TEXT", + placeholderText: "Enter your text here", + }, + { + id: "3.2", + propertyName: "textStyle", + label: "Text Style", + controlType: "DROP_DOWN", + options: [ + { label: "Heading", value: "HEADING" }, + { label: "Label", value: "LABEL" }, + { label: "Body", value: "BODY" }, + { label: "Sub text", value: "SUB_TEXT" }, + ], + }, + { + id: "3.3", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + ], + IMAGE_WIDGET: [ + { + sectionName: "General", + id: "4", + children: [ + { + id: "4.1", + propertyName: "image", + label: "Image", + controlType: "IMAGE_PICKER", + }, + { + id: "4.2", + propertyName: "defaultImage", + label: "Default Image", + controlType: "IMAGE_PICKER", + }, + { + id: "4.3", + propertyName: "imageShape", + label: "Shape", + controlType: "SHAPE_PICKER", + }, + { + id: "4.4", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + ], + INPUT_WIDGET: [ + { + sectionName: "General", + id: "5", + children: [ + { + id: "5.1", + propertyName: "label", + label: "Label", + controlType: "INPUT_TEXT", + inputType: "TEXT", + placeholderText: "Label the widget", + }, + { + id: "5.2", + propertyName: "inputType", + label: "Data Type", + controlType: "DROP_DOWN", + options: [ + { label: "Text", value: "TEXT" }, + { label: "Number", value: "NUMBER" }, + { label: "Integer", value: "INTEGER" }, + { label: "Phone Number", value: "PHONE_NUMBER" }, + { label: "Email", value: "EMAIL" }, + { label: "Passwork", value: "PASSWORD" }, + { label: "Currency", value: "CURRENCY" }, + { label: "Search", value: "SEARCH" }, + ], + }, + { + id: "5.3", + propertyName: "placeholderText", + label: "Placeholder", + controlType: "INPUT_TEXT", + placeholderText: "Enter your text here", + }, + { + id: "5.4", + propertyName: "maxChars", + label: "Max Chars", + controlType: "INPUT_TEXT", + inputType: "INTEGER", + placeholderText: "Maximum character length", + }, + { + id: "5.5", + propertyName: "validators", + label: "Validators", + controlType: "VALIDATION_INPUT", + }, + { + id: "5.6", + children: [ + { + id: "5.6.1", + propertyName: "focusIndexx", + label: "Focus Index", + controlType: "INPUT_TEXT", + inputType: "INTEGER", + placeholderText: "Enter the order of tab focus", + }, + { + id: "5.6.2", + propertyName: "autoFocus", + label: "Auto Focus", + controlType: "SWITCH", + }, + ], + }, + { + id: "5.7", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + { + id: "5.8", + propertyName: "isDisabled", + label: "Disabled", + controlType: "SWITCH", + }, + ], + }, + ], + SWITCH_WIDGET: [ + { + sectionName: "General", + id: "6", + children: [ + { + id: "6.1", + propertyName: "label", + label: "Label", + controlType: "INPUT_TEXT", + inputType: "TEXT", + placeholderText: "Label the widget", + }, + { + id: "6.2", + propertyName: "isOn", + label: "Default State", + controlType: "SWITCH", + }, + { + id: "6.3", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + { + id: "6.4", + propertyName: "isDisabled", + label: "Disabled", + controlType: "SWITCH", + }, + ], + }, + ], + CONTAINER_WIDGET: [ + { + sectionName: "General", + id: "7", + children: [ + { + id: "7.1", + propertyName: "backgroundColor", + label: "Background Color", + controlType: "COLOR_PICKER", + }, + { + id: "6.3", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + ], + SPINNER_WIDGET: [ + { + sectionName: "General", + id: "8", + children: [ + { + id: "8.1", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + ], + DATE_PICKER_WIDGET: [ + { + sectionName: "General", + id: "9", + children: [ + { + id: "9.1", + propertyName: "datePickerType", + label: "Picker Type", + controlType: "DROP_DOWN", + options: [ + { label: "Single Date", value: "DATE_PICKER" }, + { label: "Date Range", value: "DATE_RANGE_PICKER" }, + ], + }, + { + id: "9.4", + propertyName: "label", + label: "Enter Date Label", + controlType: "INPUT_TEXT", + }, + { + id: "9.1", + propertyName: "defaultDate", + label: "Default Date", + controlType: "DATE_PICKER", + }, + { + id: "9.2", + propertyName: "defaultTimezone", + label: "Default Timezone", + controlType: "TIMEZONE_PICKER", + }, + { + id: "9.3", + propertyName: "enableTime", + label: "Enable Pick Time", + controlType: "SWITCH", + }, + { + id: "9.5", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + { + id: "9.6", + propertyName: "isDisabled", + label: "Disabled", + controlType: "SWITCH", + }, + ], + }, + { + sectionName: "Actions", + id: "10", + children: [ + { + id: "10.1", + propertyName: "onDateSelected", + label: "onDateSelected", + controlType: "ACTION_SELECTOR", + }, + { + id: "10.2", + propertyName: "onDateRangeSelected", + label: "onDateRangeSelected", + controlType: "ACTION_SELECTOR", + }, + ], + }, + ], + TABLE_WIDGET: [ + { + sectionName: "General", + id: "11", + children: [ + { + id: "11.1", + propertyName: "label", + label: "Enter Table Label", + controlType: "INPUT_TEXT", + }, + { + id: "11.2", + propertyName: "tableData", + label: "Enter data array", + controlType: "INPUT_TEXT", + }, + { + id: "11.3", + propertyName: "nextPageKey", + label: "Next Pagination Key", + controlType: "INPUT_TEXT", + }, + { + id: "11.4", + propertyName: "prevPageKey", + label: "Previous Pagination Key", + controlType: "INPUT_TEXT", + }, + { + id: "11.5", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + { + sectionName: "Actions", + id: "12", + children: [ + { + id: "12.1", + propertyName: "tableActions", + label: "Record action", + controlType: "RECORD_ACTION_SELECTOR", + }, + { + id: "12.2", + propertyName: "onRowSelected", + label: "onRowSelected", + controlType: "ACTION_SELECTOR", + }, + { + id: "12.3", + propertyName: "onPageChange", + label: "onPageChange", + controlType: "ACTION_SELECTOR", + }, + ], + }, + ], + DROP_DOWN_WIDGET: [ + { + sectionName: "General", + id: "13", + children: [ + { + id: "13.1", + propertyName: "type", + label: "Selection Type", + controlType: "DROP_DOWN", + options: [ + { label: "Single Select", value: "SINGLE_SELECT" }, + { label: "Multi Select", value: "MULTI_SELECT" }, + ], + }, + { + id: "13.2", + propertyName: "label", + label: "Label", + controlType: "INPUT_TEXT", + placeholderText: "Enter the label", + }, + { + id: "13.3", + propertyName: "placeholderText", + label: "Placeholder", + controlType: "INPUT_TEXT", + placeholderText: "Enter the placeholder", + }, + { + id: "13.4", + propertyName: "options", + label: "Options", + controlType: "OPTION_INPUT", + }, + { + id: "13.5", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + { + sectionName: "Actions", + id: "14", + children: [ + { + id: "14.1", + propertyName: "onOptionSelected", + label: "onOptionSelected", + controlType: "ACTION_SELECTOR", + }, + ], + }, + ], + CHECKBOX_WIDGET: [ + { + sectionName: "General", + id: "15", + children: [ + { + id: "15.1", + propertyName: "label", + label: "Label", + controlType: "INPUT_TEXT", + placeholderText: "Enter the label", + }, + { + id: "15.2", + propertyName: "defaultCheckedState", + label: "Default State", + controlType: "SWITCH", + }, + { + id: "13.5", + propertyName: "isDisabled", + label: "Disabled", + controlType: "SWITCH", + }, + { + id: "13.5", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + { + sectionName: "Actions", + id: "16", + children: [ + { + id: "16.1", + propertyName: "onCheckChange", + label: "onCheckChange", + controlType: "ACTION_SELECTOR", + }, + ], + }, + ], + RADIO_GROUP_WIDGET: [ + { + sectionName: "General", + id: "16", + children: [ + { + id: "16.1", + propertyName: "label", + label: "Label", + controlType: "INPUT_TEXT", + placeholderText: "Enter the label", + }, + { + id: "16.2", + propertyName: "defaultOptionValue", + label: "Default Selected Value", + controlType: "SWITCH", + }, + { + id: "16.3", + propertyName: "options", + label: "Options", + controlType: "OPTION_INPUT", + }, + { + id: "13.5", + propertyName: "isVisible", + label: "Visibile", + controlType: "SWITCH", + }, + ], + }, + { + sectionName: "Actions", + id: "17", + children: [ + { + id: "17.1", + propertyName: "onOptionSelected", + label: "onOptionSelected", + controlType: "ACTION_SELECTOR", + }, + ], + }, + ], + }, + configVersion: 1, +}; + +export default PropertyPaneConfigResponse; diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 27cb8b0d25..efff9f0c9f 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -1,89 +1,92 @@ -import { WidgetConfigReducerState } from "../reducers/entityReducers/widgetConfigReducer.tsx"; +import { WidgetConfigReducerState } from "../reducers/entityReducers/widgetConfigReducer"; const WidgetConfigResponse: WidgetConfigReducerState = { - BUTTON_WIDGET: { - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - rows: 1, - columns: 2, - }, - TEXT_WIDGET: { - text: "Not all labels are bad!", - textStyle: "LABEL", - rows: 1, - columns: 3, - }, - IMAGE_WIDGET: { - defaultImage: "", - imageShape: "RECTANGLE", - image: "", - rows: 3, - columns: 3, - }, - INPUT_WIDGET: { - inputType: "TEXT", - label: "Label me", - rows: 1, - columns: 3, - }, - SWITCH_WIDGET: { - isOn: false, - label: "Turn me on", - rows: 1, - columns: 4, - }, - CONTAINER_WIDGET: { - backgroundColor: "#FFFFFF", - rows: 1, - columns: 4, - }, - SPINNER_WIDGET: { - rows: 1, - columns: 1, - }, - DATE_PICKER_WIDGET: { - enableTime: false, - datePickerType: "DATE_PICKER", - rows: 1, - columns: 3, - label: "Date", - }, - TABLE_WIDGET: { - rows: 5, - columns: 7, - label: "Don't table me!", - }, - DROP_DOWN_WIDGET: { - rows: 1, - columns: 3, - selectionType: "SINGLE_SELECT", - label: "Pick me!", - }, - CHECKBOX_WIDGET: { - rows: 1, - columns: 3, - label: "Label - CHECK!", - defaultCheckedState: true, - }, - RADIO_GROUP_WIDGET: { - rows: 3, - columns: 3, - label: "Alpha - come in!", - options: [ - { label: "Alpha", value: "1" }, - { label: "Bravo", value: "2" }, - { label: "Charlie", value: "3" }, - ], - defaultOptionValue: "1", - }, - ALERT_WIDGET: { - alertType: "NOTIFICATION", - intent: "SUCCESS", - rows: 3, - columns: 3, - header: "", - message: "", + config: { + BUTTON_WIDGET: { + text: "Submit", + buttonStyle: "PRIMARY_BUTTON", + rows: 1, + columns: 2, + }, + TEXT_WIDGET: { + text: "Not all labels are bad!", + textStyle: "LABEL", + rows: 1, + columns: 3, + }, + IMAGE_WIDGET: { + defaultImage: "", + imageShape: "RECTANGLE", + image: "", + rows: 3, + columns: 3, + }, + INPUT_WIDGET: { + inputType: "TEXT", + label: "Label me", + rows: 1, + columns: 3, + }, + SWITCH_WIDGET: { + isOn: false, + label: "Turn me on", + rows: 1, + columns: 4, + }, + CONTAINER_WIDGET: { + backgroundColor: "#FFFFFF", + rows: 1, + columns: 4, + }, + SPINNER_WIDGET: { + rows: 1, + columns: 1, + }, + DATE_PICKER_WIDGET: { + enableTime: false, + datePickerType: "DATE_PICKER", + rows: 1, + columns: 3, + label: "Date", + }, + TABLE_WIDGET: { + rows: 5, + columns: 7, + label: "Don't table me!", + }, + DROP_DOWN_WIDGET: { + rows: 1, + columns: 3, + selectionType: "SINGLE_SELECT", + label: "Pick me!", + }, + CHECKBOX_WIDGET: { + rows: 1, + columns: 3, + label: "Label - CHECK!", + defaultCheckedState: true, + }, + RADIO_GROUP_WIDGET: { + rows: 3, + columns: 3, + label: "Alpha - come in!", + options: [ + { label: "Alpha", value: "1" }, + { label: "Bravo", value: "2" }, + { label: "Charlie", value: "3" }, + ], + defaultOptionValue: "1", + }, + ALERT_WIDGET: { + alertType: "NOTIFICATION", + intent: "SUCCESS", + rows: 3, + columns: 3, + header: "", + message: "", + }, }, + configVersion: 1, }; export default WidgetConfigResponse; diff --git a/app/client/src/pages/Editor/PropertyPane.tsx b/app/client/src/pages/Editor/PropertyPane.tsx new file mode 100644 index 0000000000..e75bfda162 --- /dev/null +++ b/app/client/src/pages/Editor/PropertyPane.tsx @@ -0,0 +1,121 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { AppState } from "../../reducers"; +import PropertyControlFactory from "../../utils/PropertyControlFactory"; +import _ from "lodash"; +import { ControlProps } from "../propertyControls/BaseControl"; +import { PropertySection } from "../../reducers/entityReducers/propertyPaneConfigReducer"; +import { updateWidgetProperty } from "../../actions/controlActions"; + +class PropertyPane extends Component< + PropertyPaneProps & PropertyPaneFunctions +> { + constructor(props: PropertyPaneProps & PropertyPaneFunctions) { + super(props); + this.onPropertyChange = this.onPropertyChange.bind(this); + } + + render() { + if (this.props.isVisible) { + return ( +
+ {!_.isNil(this.props.propertySections) + ? _.map( + this.props.propertySections, + (propertySection: PropertySection) => { + return this.renderPropertySection( + propertySection, + propertySection.id, + ); + }, + ) + : undefined} +
+ ); + } else { + return null; + } + } + + renderPropertySection(propertySection: PropertySection, key: string) { + return ( +
+ {!_.isNil(propertySection) ? ( +
{propertySection.sectionName}
+ ) : ( + undefined + )} +
+ {_.map( + propertySection.children, + (propertyControlOrSection: ControlProps | PropertySection) => { + if ("children" in propertyControlOrSection) { + return this.renderPropertySection( + propertyControlOrSection, + propertyControlOrSection.id, + ); + } else { + return PropertyControlFactory.createControl( + propertyControlOrSection, + { onPropertyChange: this.onPropertyChange }, + ); + } + }, + )} +
+
+ ); + } + + onPropertyChange(propertyName: string, propertyValue: any) { + this.props.updateWidgetProperty( + this.props.widgetId, + propertyName, + propertyValue, + ); + } +} + +const mapStateToProps = (state: AppState): PropertyPaneProps => { + let propertyConfig = undefined; + if (!_.isNil(state.ui.propertyPane.widgetId)) { + const widget = state.entities.canvasWidgets[state.ui.propertyPane.widgetId]; + propertyConfig = state.entities.propertyConfig.config[widget.type]; + } + return { + propertySections: propertyConfig, + widgetId: state.ui.propertyPane.widgetId, + isVisible: state.ui.propertyPane.isVisible, + }; +}; + +const mapDispatchToProps = (dispatch: any): PropertyPaneFunctions => { + return { + updateWidgetProperty: ( + widgetId: string, + propertyName: string, + propertyValue: any, + ) => dispatch(updateWidgetProperty(widgetId, propertyName, propertyValue)), + }; +}; + +export interface PropertyPaneProps { + propertySections?: PropertySection[]; + widgetId?: string; + isVisible: boolean; +} + +export interface PropertyPaneFunctions { + updateWidgetProperty: Function; +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(PropertyPane); diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index 0acc5b68ba..dbfadffb30 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -1,4 +1,5 @@ import React, { Component } from "react"; +import { Position, Toaster } from "@blueprintjs/core"; import { connect } from "react-redux"; import styled from "styled-components"; import Canvas from "./Canvas"; @@ -17,6 +18,11 @@ import { fetchPage, updateWidget, savePage } from "../../actions/pageActions"; import { RenderModes } from "../../constants/WidgetConstants"; import { executeAction } from "../../actions/widgetActions"; import { ActionPayload } from "../../constants/ActionConstants"; +import PropertyPane from "./PropertyPane"; + +const SaveToast = Toaster.create({ + position: Position.TOP, +}); const CanvasContainer = styled.section` height: 100%; @@ -58,6 +64,7 @@ type EditorProps = { page: string; currentPageId: string; currentLayoutId: string; + isSaving: boolean; }; class Editor extends Component { @@ -65,6 +72,19 @@ class Editor extends Component { this.props.fetchCanvasWidgets(this.props.currentPageId); } + componentDidUpdate(prevProps: EditorProps) { + if (this.props.isSaving && prevProps.isSaving !== this.props.isSaving) { + SaveToast.clear(); + SaveToast.show({ message: "Saving Page..." }); + } else if ( + !this.props.isSaving && + prevProps.isSaving !== this.props.isSaving + ) { + SaveToast.clear(); + SaveToast.show({ message: "Page Saved" }); + } + } + public render() { return ( @@ -82,6 +102,7 @@ class Editor extends Component { }} /> + ); @@ -99,6 +120,7 @@ const mapStateToProps = (state: AppState): EditorReduxState => { pageWidgetId: state.ui.editor.pageWidgetId, currentPageId: state.ui.editor.currentPageId, currentLayoutId: state.ui.editor.currentLayoutId, + isSaving: state.ui.editor.isSaving, }; }; diff --git a/app/client/src/pages/propertyControls/BaseControl.tsx b/app/client/src/pages/propertyControls/BaseControl.tsx new file mode 100644 index 0000000000..3aa271c104 --- /dev/null +++ b/app/client/src/pages/propertyControls/BaseControl.tsx @@ -0,0 +1,37 @@ +/*** + * Controls are rendered in the property panel from the property config + * Controls are higher order components that update a widgets property + */ +import { Component } from "react"; +import { ControlType } from "../../constants/PropertyControlConstants"; +import _ from "lodash"; + +abstract class BaseControl extends Component { + updateProperty(propertyName: string, propertyValue: any) { + if (!_.isNil(this.props.onPropertyChange)) + this.props.onPropertyChange(propertyName, propertyValue); + } + + abstract getControlType(): ControlType; +} + +export interface ControlBuilder { + buildPropertyControl(controlProps: T): JSX.Element; +} + +export interface ControlProps extends ControlData, ControlFunctions { + key?: string; +} + +export interface ControlData { + id: string; + label: string; + propertyName: string; + controlType: ControlType; +} + +export interface ControlFunctions { + onPropertyChange?: (propertyName: string, propertyValue: string) => void; +} + +export default BaseControl; diff --git a/app/client/src/pages/propertyControls/DropDownControl.tsx b/app/client/src/pages/propertyControls/DropDownControl.tsx new file mode 100644 index 0000000000..9adf46f911 --- /dev/null +++ b/app/client/src/pages/propertyControls/DropDownControl.tsx @@ -0,0 +1,74 @@ +import React, { SyntheticEvent } from "react"; +import BaseControl, { ControlProps } from "./BaseControl"; +import { ControlType } from "../../constants/PropertyControlConstants"; +import { Button, MenuItem } from "@blueprintjs/core"; +import { Select, IItemRendererProps } from "@blueprintjs/select"; + +class DropDownControl extends BaseControl { + constructor(props: DropDownControlProps) { + super(props); + this.onItemSelect = this.onItemSelect.bind(this); + } + + render() { + const DropDown = Select.ofType(); + return ( + } + > +