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 ( - -