From 107e1fd6d2bbc79d7bdf73da02a3cc0611243583 Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Mon, 30 Dec 2019 13:05:37 +0000 Subject: [PATCH 01/10] Api Pane select api fix --- app/client/src/sagas/ApiPaneSagas.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts index 08f80a96f3..0e98fbb91e 100644 --- a/app/client/src/sagas/ApiPaneSagas.ts +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -88,9 +88,9 @@ function* syncApiParamsSaga( } } else if (field.includes("actionConfiguration.queryParameters")) { const { values } = yield select(getFormData, API_EDITOR_FORM_NAME); - const path = values.actionConfiguration.path; + const path = values.actionConfiguration.path || ""; const pathHasParams = path.indexOf("?") > -1; - const currentPath = values.actionConfiguration.path.substring( + const currentPath = path.substring( 0, pathHasParams ? path.indexOf("?") : undefined, ); @@ -127,14 +127,16 @@ function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) { const data = _.isEmpty(draft) ? action : draft; yield put(initialize(API_EDITOR_FORM_NAME, data)); history.push(API_EDITOR_ID_URL(applicationId, pageId, id)); - // Sync the api params my mocking a change action - yield call(syncApiParamsSaga, { - type: ReduxFormActionTypes.ARRAY_REMOVE, - payload: data.actionConfiguration.queryParameters, - meta: { - field: "actionConfiguration.queryParameters", - }, - }); + if (data.actionConfiguration && data.actionConfiguration.queryParameters) { + // Sync the api params my mocking a change action + yield call(syncApiParamsSaga, { + type: ReduxFormActionTypes.ARRAY_REMOVE, + payload: data.actionConfiguration.queryParameters, + meta: { + field: "actionConfiguration.queryParameters", + }, + }); + } } function* updateDraftsSaga() { From 507b9bd3866bd480bf0ddcbe5642c628045bd13d Mon Sep 17 00:00:00 2001 From: Satbir Singh Date: Wed, 1 Jan 2020 07:53:03 +0000 Subject: [PATCH 02/10] Fixing spaces issue in dynamic binding for tabledata. --- .../jsExecution/JSExecutionManagerSingleton.ts | 2 +- app/client/src/jsExecution/RealmExecutor.ts | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/client/src/jsExecution/JSExecutionManagerSingleton.ts b/app/client/src/jsExecution/JSExecutionManagerSingleton.ts index f98bc07ef2..46bc0ea1a5 100644 --- a/app/client/src/jsExecution/JSExecutionManagerSingleton.ts +++ b/app/client/src/jsExecution/JSExecutionManagerSingleton.ts @@ -3,7 +3,7 @@ import moment from "moment-timezone"; export type JSExecutorGlobal = Record; export interface JSExecutor { - execute: (src: string, data: JSExecutorGlobal) => string; + execute: (src: string, data: JSExecutorGlobal) => any; registerLibrary: (accessor: string, lib: any) => void; unRegisterLibrary: (accessor: string) => void; } diff --git a/app/client/src/jsExecution/RealmExecutor.ts b/app/client/src/jsExecution/RealmExecutor.ts index d84417410b..fc397eee9f 100644 --- a/app/client/src/jsExecution/RealmExecutor.ts +++ b/app/client/src/jsExecution/RealmExecutor.ts @@ -3,7 +3,7 @@ declare let Realm: any; export default class RealmExecutor implements JSExecutor { rootRealm: any; - creaetSafeObject: any; + createSafeObject: any; extrinsics: any[] = []; createSafeFunction: (unsafeFn: Function) => Function; @@ -17,7 +17,7 @@ export default class RealmExecutor implements JSExecutor { } }) `); - this.creaetSafeObject = this.rootRealm.evaluate(` + this.createSafeObject = this.rootRealm.evaluate(` (function creaetSafeObject(unsafeObject) { return JSON.parse(JSON.stringify(unsafeObject)); }) @@ -29,14 +29,23 @@ export default class RealmExecutor implements JSExecutor { unRegisterLibrary(accessor: string) { this.rootRealm.global[accessor] = null; } + private convertToMainScope(result: any) { + if (typeof result === "object") { + if (Array.isArray(result)) { + return Object.assign([], result); + } + return Object.assign({}, result); + } + return result; + } execute(sourceText: string, data: JSExecutorGlobal) { - const safeData = this.creaetSafeObject(data); + const safeData = this.createSafeObject(data); let result; try { result = this.rootRealm.evaluate(sourceText, safeData); } catch (e) { //TODO(Satbir): Return an object with an error message. } - return result; + return this.convertToMainScope(result); } } From 3ecad24203d0be0737f405d061e92c7a001d9db9 Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Thu, 2 Jan 2020 13:36:35 +0000 Subject: [PATCH 03/10] Fixes for Dynamic Input styling --- .../appsmith/CreatableDropdown.tsx | 7 + .../designSystems/appsmith/Dropdown.tsx | 4 + .../editorComponents/ApiResponseView.tsx | 5 +- .../editorComponents/CodeEditor.tsx | 6 +- .../DynamicAutocompleteInput.tsx | 214 ++++++++++++++++-- .../editorComponents/ErrorTooltip.tsx | 2 + .../form/fields/DatasourcesField.tsx | 3 +- .../form/fields/DynamicTextField.tsx | 7 +- .../form/fields/JSONEditorField.tsx | 8 +- .../form/fields/KeyValueFieldArray.tsx | 12 +- .../propertyControls/CodeEditorControl.tsx | 8 +- .../propertyControls/InputTextControl.tsx | 12 +- app/client/src/constants/BindingsConstants.ts | 3 +- app/client/src/icons/FormIcons.tsx | 9 + app/client/src/index.css | 10 +- .../PropertyPaneConfigResponse.tsx | 10 +- .../src/pages/Editor/APIEditor/Form.tsx | 17 +- app/client/src/utils/DynamicBindingUtils.ts | 17 +- 18 files changed, 270 insertions(+), 84 deletions(-) diff --git a/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx b/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx index 5e4340bff1..9182c6e375 100644 --- a/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx +++ b/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx @@ -14,9 +14,14 @@ type DropdownProps = { meta: WrappedFieldMetaProps; onCreateOption: (inputValue: string) => void; formatCreateLabel?: (value: string) => React.ReactNode; + noOptionsMessage?: (obj: { inputValue: string }) => string; }; const selectStyles = { + placeholder: (provided: any) => ({ + ...provided, + color: "#a3b3bf", + }), singleValue: (provided: any) => ({ ...provided, backgroundColor: "rgba(104,113,239,0.1)", @@ -67,9 +72,11 @@ class CreatableDropdown extends React.Component { onCreateOption, input, formatCreateLabel, + noOptionsMessage, } = this.props; const optionalProps: Partial = {}; if (formatCreateLabel) optionalProps.formatCreateLabel = formatCreateLabel; + if (noOptionsMessage) optionalProps.noOptionsMessage = noOptionsMessage; return ( ({ + ...provided, + color: "#a3b3bf", + }), control: (styles: any, state: any) => ({ ...styles, width: 100, diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 7dea588b8f..b4a6a20b31 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -11,7 +11,7 @@ import { formatBytes } from "utils/helpers"; import { APIEditorRouteParams } from "constants/routes"; import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer"; import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen"; -import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput"; +import CodeEditor from "components/editorComponents/CodeEditor"; const ResponseWrapper = styled.div` position: relative; @@ -138,12 +138,13 @@ const ApiResponseView = (props: Props) => { key: "body", title: "Response Body", panelComponent: ( - ), }, diff --git a/app/client/src/components/editorComponents/CodeEditor.tsx b/app/client/src/components/editorComponents/CodeEditor.tsx index a5967da8f3..0fb14743a8 100644 --- a/app/client/src/components/editorComponents/CodeEditor.tsx +++ b/app/client/src/components/editorComponents/CodeEditor.tsx @@ -21,19 +21,19 @@ interface Props { class CodeEditor extends React.Component { textArea = React.createRef(); editor: any; - constructor(props: Props) { - super(props); - } componentDidMount(): void { if (this.textArea.current) { + const readOnly = !this.props.input.onChange; this.editor = cm.fromTextArea(this.textArea.current, { mode: { name: "javascript", json: true }, value: this.props.input.value, + readOnly, lineNumbers: true, tabSize: 2, indentWithTabs: true, lineWrapping: true, }); + this.editor.setSize(null, this.props.height); } } diff --git a/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx b/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx index cc8e29e842..eed461f523 100644 --- a/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx +++ b/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx @@ -1,43 +1,146 @@ import React, { Component } from "react"; import { connect } from "react-redux"; import { AppState } from "reducers"; -import styled from "styled-components"; -import CodeMirror, { EditorConfiguration } from "codemirror"; +import styled, { createGlobalStyle } from "styled-components"; +import CodeMirror, { EditorConfiguration, LineHandle } from "codemirror"; import "codemirror/lib/codemirror.css"; import "codemirror/theme/monokai.css"; import "codemirror/addon/hint/show-hint"; -import "codemirror/addon/hint/show-hint.css"; import "codemirror/addon/hint/javascript-hint"; +import "codemirror/addon/display/placeholder"; import { getNameBindingsWithData, NameBindingsWithData, } from "selectors/nameBindingsWithDataSelector"; +import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants"; +import ErrorTooltip from "components/editorComponents/ErrorTooltip"; +import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; +import _ from "lodash"; +import { parseDynamicString } from "utils/DynamicBindingUtils"; require("codemirror/mode/javascript/javascript"); -const Wrapper = styled.div<{ height?: number; theme?: "LIGHT" | "DARK" }>` - border: ${props => props.theme !== "DARK" && "1px solid #d0d7dd"}; +const HintStyles = createGlobalStyle` + .CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + margin: 0; + padding: 5px; + font-size: 90%; + font-family: monospace; + max-height: 20em; + width: 200px; + overflow-y: auto; + background: #FFFFFF; + border: 1px solid #EBEFF2; + box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14); + border-radius: 4px; + } + + .CodeMirror-hint { + height: 32px; + padding: 3px; + margin: 0; + white-space: pre; + color: #2E3D49; + cursor: pointer; + display: flex; + align-items: center; + font-size: 14px; + } + + li.CodeMirror-hint-active { + background: #E9FAF3; + border-radius: 4px; + } +`; + +const Wrapper = styled.div<{ + borderStyle?: THEME; + hasError: boolean; +}>` + border: 1px solid; + border-color: ${props => + props.hasError + ? props.theme.colors.error + : props.borderStyle !== THEMES.DARK + ? "#d0d7dd" + : "transparent"}; border-radius: 4px; display: flex; flex: 1; - flex-direction: column; + flex-direction: row; position: relative; text-transform: none; min-height: 32px; - height: ${props => (props.height ? `${props.height}px` : "32px")}; + overflow: hidden; + height: auto; + && { + .binding-highlight { + color: ${props => + props.borderStyle === THEMES.DARK ? "#f7c75b" : "#ffb100"}; + font-weight: 700; + } + .CodeMirror { + flex: 1; + line-height: 21px; + z-index: 0; + border-radius: 4px; + height: auto; + } + .CodeMirror pre.CodeMirror-placeholder { + color: #a3b3bf; + } + } `; +const IconContainer = styled.div` + .bp3-icon { + border-radius: 4px 0 0 4px; + margin: 0; + height: 32px; + width: 30px; + display: flex; + align-items: center; + justify-content: center; + background-color: #eef2f5; + svg { + height: 20px; + width: 20px; + path { + fill: #979797; + } + } + } +`; + +const THEMES = { + LIGHT: "LIGHT", + DARK: "DARK", +}; + +type THEME = "LIGHT" | "DARK"; + interface ReduxStateProps { dynamicData: NameBindingsWithData; } -type Props = ReduxStateProps & { - input: { - value: string; - onChange?: (value: string) => void; - }; - theme?: "LIGHT" | "DARK"; +export type DynamicAutocompleteInputProps = { + placeholder?: string; + leftIcon?: Function; + initialHeight: number; + theme?: THEME; + meta?: Partial; + showLineNumbers?: boolean; + allowTabIndent?: boolean; }; +type Props = ReduxStateProps & + DynamicAutocompleteInputProps & { + input: Partial; + }; + class DynamicAutocompleteInput extends Component { textArea = React.createRef(); editor: any; @@ -47,30 +150,40 @@ class DynamicAutocompleteInput extends Component { const options: EditorConfiguration = {}; if (this.props.theme === "DARK") options.theme = "monokai"; if (!this.props.input.onChange) options.readOnly = true; + if (this.props.showLineNumbers) options.lineNumbers = true; + const extraKeys: Record = { + "Ctrl-Space": "autocomplete", + }; + if (!this.props.allowTabIndent) extraKeys["Tab"] = false; this.editor = CodeMirror.fromTextArea(this.textArea.current, { mode: { name: "javascript", globalVars: true }, + viewportMargin: 10, value: this.props.input.value, tabSize: 2, indentWithTabs: true, lineWrapping: true, - extraKeys: { "Ctrl-Space": "autocomplete" }, showHint: true, + extraKeys, ...options, }); - this.editor.on("change", this.handleChange); - this.editor.on("keyup", this.handleAutocompleteVisibility); + this.editor.on("change", _.debounce(this.handleChange, 200)); + this.editor.on("cursorActivity", this.handleAutocompleteVisibility); this.editor.setOption("hintOptions", { completeSingle: false, globalScope: this.props.dynamicData, }); + this.editor.eachLine(this.highlightBindings); } } componentDidUpdate(): void { if (this.editor) { const editorValue = this.editor.getValue(); - const inputValue = this.props.input.value; - if (inputValue && inputValue !== editorValue) { + let inputValue = this.props.input.value; + if (typeof inputValue === "object") { + inputValue = JSON.stringify(inputValue, null, 2); + } + if (!!inputValue && inputValue !== editorValue) { this.editor.setValue(inputValue); } } @@ -81,21 +194,72 @@ class DynamicAutocompleteInput extends Component { if (this.props.input.onChange) { this.props.input.onChange(value); } + this.editor.eachLine(this.highlightBindings); }; - handleAutocompleteVisibility = (cm: any, event: any) => { - if (!cm.state.completionActive && event.keyCode !== 13) { + handleAutocompleteVisibility = (cm: any) => { + let cursorBetweenBinding = false; + const cursor = this.editor.getCursor(); + const value = this.editor.getValue(); + let cumulativeCharCount = 0; + parseDynamicString(value).forEach(segment => { + const start = cumulativeCharCount; + const dynamicStart = segment.indexOf("{{"); + const dynamicDoesStart = dynamicStart > -1; + const dynamicEnd = segment.indexOf("}}"); + const dynamicDoesEnd = dynamicEnd > -1; + const dynamicStartIndex = dynamicStart + start + 1; + const dynamicEndIndex = dynamicEnd + start + 1; + if ( + dynamicDoesStart && + cursor.ch > dynamicStartIndex && + ((dynamicDoesEnd && cursor.ch < dynamicEndIndex) || + (!dynamicDoesEnd && cursor.ch > dynamicStartIndex)) + ) { + cursorBetweenBinding = true; + } + cumulativeCharCount = start + segment.length; + }); + const shouldShow = cursorBetweenBinding && !cm.state.completionActive; + if (shouldShow) { cm.showHint(cm); } }; + highlightBindings = (line: LineHandle) => { + const lineNo = this.editor.getLineNumber(line); + let match; + while ((match = AUTOCOMPLETE_MATCH_REGEX.exec(line.text)) != null) { + const start = match.index; + const end = AUTOCOMPLETE_MATCH_REGEX.lastIndex; + this.editor.markText( + { ch: start, line: lineNo }, + { ch: end, line: lineNo }, + { + className: "binding-highlight", + }, + ); + } + }; + render() { - const { input, theme } = this.props; - const height = this.editor ? this.editor.doc.height + 20 : null; + const { input, meta, theme } = this.props; + const hasError = !!(meta && meta.error); return ( - -