Fix embedded datasource path (#2)

* Refactor CodeMirror component to be more configurable and testable (hints, markings)
* Update the existing datasource path component
* Better text highlighting for JSON fields
* Case insensitive hinting in autocomplete
This commit is contained in:
Hetu Nandu 2020-07-01 15:31:07 +05:30 committed by GitHub
parent 224d0ee49c
commit 38aafb5027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1624 additions and 1353 deletions

View File

@ -7,4 +7,8 @@ module.exports = {
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node", "css"],
moduleDirectories: ["node_modules", "src"],
transformIgnorePatterns: ["<rootDir>/node_modules/(?!codemirror)"],
moduleNameMapper: {
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
"\\.svg$": "<rootDir>/test/__mocks__/svgMock.js",
},
};

View File

@ -46,7 +46,7 @@
"algoliasearch": "^4.2.0",
"axios": "^0.18.0",
"chance": "^1.1.3",
"codemirror": "^5.50.0",
"codemirror": "^5.55.0",
"eslint": "^6.4.0",
"fast-deep-equal": "^3.1.1",
"flow-bin": "^0.91.0",
@ -149,7 +149,7 @@
"@storybook/addons": "^5.2.6",
"@storybook/preset-create-react-app": "^1.3.1",
"@storybook/react": "^5.2.6",
"@types/codemirror": "^0.0.82",
"@types/codemirror": "^0.0.96",
"@types/jest": "^24.0.22",
"@types/react-beautiful-dnd": "^11.0.4",
"@types/react-select": "^3.0.5",

View File

@ -21,8 +21,8 @@ export interface Datasource {
headers?: Record<string, string>;
databaseName?: string;
};
invalids: string[];
isValid: boolean;
invalids?: string[];
isValid?: boolean;
}
export interface CreateDatasourceConfig {

View File

@ -3,7 +3,7 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import "react-tabs/style/react-tabs.css";
import styled from "styled-components";
const TabsWrapper = styled.div<{ overflow?: boolean }>`
const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
height: 100%;
.react-tabs {
height: 100%;
@ -16,7 +16,7 @@ const TabsWrapper = styled.div<{ overflow?: boolean }>`
border-bottom-color: #d0d7dd;
color: #a3b3bf;
${props =>
props.overflow &&
props.shouldOverflow &&
`
overflow-y: hidden;
overflow-x: auto;
@ -51,7 +51,7 @@ type TabbedViewComponentType = {
export const BaseTabbedView = (props: TabbedViewComponentType) => {
return (
<TabsWrapper overflow={props.overflow}>
<TabsWrapper shouldOverflow={props.overflow}>
<Tabs
selectedIndex={props.selectedIndex}
onSelect={(index: number) => {

View File

@ -10,7 +10,7 @@ import { ActionResponse } from "api/ActionAPI";
import { formatBytes } from "utils/helpers";
import { APIEditorRouteParams } from "constants/routes";
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
import CodeEditor from "components/editorComponents/CodeEditor";
import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
import { getActionResponses } from "selectors/entitiesSelector";
import { Colors } from "constants/Colors";
import _ from "lodash";
@ -201,7 +201,7 @@ const ApiResponseView = (props: Props) => {
</div>
</FailedMessageContainer>
)}
<CodeEditor
<ReadOnlyEditor
input={{
value: response.body
? JSON.stringify(response.body, null, 2)

View File

@ -1,66 +0,0 @@
import React, { ChangeEvent } from "react";
import cm from "codemirror";
import styled from "styled-components";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/monokai.css";
require("codemirror/mode/javascript/javascript");
const Wrapper = styled.div<{ height: number | string }>`
height: ${props =>
typeof props.height === "number" ? props.height + "px" : props.height};
color: white;
.CodeMirror {
height: 100%;
}
`;
interface Props {
input: {
value: string;
onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void;
};
height: number | string;
}
class CodeEditor extends React.Component<Props> {
textArea = React.createRef<HTMLTextAreaElement>();
editor: any;
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,
tabSize: 2,
indentWithTabs: true,
lineNumbers: true,
lineWrapping: true,
});
}
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<{}>,
snapshot?: any,
): void {
this.editor.setValue(this.props.input.value);
}
render(): React.ReactNode {
return (
<Wrapper height={this.props.height}>
<textarea
ref={this.textArea}
onChange={this.props.input.onChange}
defaultValue={this.props.input.value}
/>
</Wrapper>
);
}
}
export default CodeEditor;

View File

@ -0,0 +1,47 @@
import CodeMirror from "codemirror";
import { DataTree } from "entities/DataTree/dataTreeFactory";
export enum EditorModes {
TEXT = "text/plain",
SQL = "sql",
TEXT_WITH_BINDING = "text-js",
JSON = "application/json",
JSON_WITH_BINDING = "json-js",
SQL_WITH_BINDING = "sql-js",
}
export enum EditorTheme {
LIGHT = "LIGHT",
DARK = "DARK",
}
export enum TabBehaviour {
INPUT = "INPUT",
INDENT = "INDENT",
}
export enum EditorSize {
COMPACT = "COMPACT",
EXTENDED = "EXTENDED",
}
export type EditorConfig = {
theme: EditorTheme;
mode: EditorModes;
tabBehaviour: TabBehaviour;
size: EditorSize;
hinting: Array<HintHelper>;
marking: Array<MarkHelper>;
};
export const EditorThemes: Record<EditorTheme, string> = {
[EditorTheme.LIGHT]: "neat",
[EditorTheme.DARK]: "monokai",
};
export type HintHelper = (editor: CodeMirror.Editor, data: DataTree) => Hinter;
export type Hinter = {
showHint: (editor: CodeMirror.Editor) => void;
update?: (data: DataTree) => void;
};
export type MarkHelper = (editor: CodeMirror.Editor) => void;

View File

@ -3,7 +3,7 @@ import styled from "styled-components";
import _ from "lodash";
import Popper from "pages/Editor/Popper";
import ReactJson from "react-json-view";
import { EditorTheme } from "components/editorComponents/DynamicAutocompleteInput";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import { theme } from "constants/DefaultTheme";
import { Placement } from "popper.js";
@ -23,13 +23,13 @@ type ThemeConfig = {
type PopupTheme = Record<EditorTheme, ThemeConfig>;
const THEMES: PopupTheme = {
LIGHT: {
[EditorTheme.LIGHT]: {
backgroundColor: "#fff",
textColor: "#1E242B",
editorBackground: "#F4F4F4",
editorColor: "#1E242B",
},
DARK: {
[EditorTheme.DARK]: {
backgroundColor: "#23292e",
textColor: "#F4F4F4",
editorBackground: "#090a0f",
@ -123,7 +123,7 @@ const CurrentValueViewer = (props: {
Array.isArray(props.evaluatedValue)
) {
const reactJsonProps = {
theme: props.theme === "DARK" ? "monokai" : "rjv-default",
theme: props.theme === EditorTheme.DARK ? "monokai" : "rjv-default",
name: null,
enableClipboard: false,
displayObjectSize: false,

View File

@ -0,0 +1,87 @@
import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers";
import { MockCodemirrorEditor } from "../../../../test/__mocks__/CodeMirrorEditorMock";
import RealmExecutor from "jsExecution/RealmExecutor";
jest.mock("jsExecution/RealmExecutor");
describe("hint helpers", () => {
beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
RealmExecutor.mockClear();
});
describe("binding hint helper", () => {
it("is initialized correctly", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
const helper = bindingHint(MockCodemirrorEditor, {});
expect(MockCodemirrorEditor.setOption).toBeCalled();
expect(helper).toHaveProperty("showHint");
expect(helper).toHaveProperty("update");
});
it("opens hint correctly", () => {
// Setup
type Case = {
value: string;
cursor: { ch: number; line: number };
toCall: "closeHint" | "showHint";
getLine?: string[];
};
const cases: Case[] = [
{ value: "ABC", cursor: { ch: 3, line: 0 }, toCall: "closeHint" },
{ value: "{{ }}", cursor: { ch: 3, line: 0 }, toCall: "showHint" },
{
value: '{ name: "{{}}" }',
cursor: { ch: 11, line: 0 },
toCall: "showHint",
},
{
value: '{ name: "{{}}" }',
cursor: { ch: 12, line: 0 },
toCall: "closeHint",
},
{
value: "{somethingIsHere }}",
cursor: { ch: 18, line: 0 },
toCall: "closeHint",
},
{
value: `{\n\tname: "{{}}"\n}`,
getLine: ["{", `\tname: "{{}}`, "}"],
cursor: { ch: 10, line: 1 },
toCall: "showHint",
},
];
cases.forEach(testCase => {
MockCodemirrorEditor.getValue.mockReturnValueOnce(testCase.value);
MockCodemirrorEditor.getCursor.mockReturnValueOnce(testCase.cursor);
if (testCase.getLine) {
testCase.getLine.forEach(line => {
MockCodemirrorEditor.getLine.mockReturnValueOnce(line);
});
}
});
// Test
cases.forEach(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
const helper = bindingHint(MockCodemirrorEditor, {});
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
helper.showHint(MockCodemirrorEditor);
});
// Assert
const showHintCount = cases.filter(c => c.toCall === "showHint").length;
expect(MockCodemirrorEditor.showHint).toHaveBeenCalledTimes(
showHintCount,
);
const closeHintCount = cases.filter(c => c.toCall === "closeHint").length;
expect(MockCodemirrorEditor.closeHint).toHaveBeenCalledTimes(
closeHintCount,
);
});
});
});

View File

@ -0,0 +1,75 @@
import CodeMirror from "codemirror";
import TernServer from "utils/autocomplete/TernServer";
import KeyboardShortcuts from "constants/KeyboardShortcuts";
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
import { HintHelper } from "components/editorComponents/CodeEditor/EditorConfig";
import AnalyticsUtil from "utils/AnalyticsUtil";
export const bindingHint: HintHelper = (editor, data) => {
const ternServer = new TernServer(data);
editor.setOption("extraKeys", {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
...editor.options.extraKeys,
[KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (cm: CodeMirror.Editor) =>
ternServer.complete(cm),
[KeyboardShortcuts.CodeEditor.ShowTypeAndInfo]: (cm: CodeMirror.Editor) => {
ternServer.showType(cm);
},
[KeyboardShortcuts.CodeEditor.OpenDocsLink]: (cm: CodeMirror.Editor) => {
ternServer.showDocs(cm);
},
});
return {
update: (data: DataTree) => {
const dataTreeDef = dataTreeTypeDefCreator(data);
ternServer.updateDef("dataTree", dataTreeDef);
},
showHint: (editor: CodeMirror.Editor) => {
let cursorBetweenBinding = false;
const cursor = editor.getCursor();
const value = editor.getValue();
let cursorIndex = cursor.ch;
if (cursor.line > 0) {
for (let lineIndex = 0; lineIndex < cursor.line; lineIndex++) {
const line = editor.getLine(lineIndex);
// Add line length + 1 for new line character
cursorIndex = cursorIndex + line.length + 1;
}
}
const stringSegments = getDynamicStringSegments(value);
// count of chars processed
let cumulativeCharCount = 0;
stringSegments.forEach((segment: string) => {
const start = cumulativeCharCount;
const dynamicStart = segment.indexOf("{{");
const dynamicDoesStart = dynamicStart > -1;
const dynamicEnd = segment.indexOf("}}");
const dynamicDoesEnd = dynamicEnd > -1;
const dynamicStartIndex = dynamicStart + start + 2;
const dynamicEndIndex = dynamicEnd + start;
if (
dynamicDoesStart &&
cursorIndex >= dynamicStartIndex &&
((dynamicDoesEnd && cursorIndex <= dynamicEndIndex) ||
(!dynamicDoesEnd && cursorIndex >= dynamicStartIndex))
) {
cursorBetweenBinding = true;
}
cumulativeCharCount = start + segment.length;
});
const shouldShow = cursorBetweenBinding;
if (shouldShow) {
AnalyticsUtil.logEvent("AUTO_COMPELTE_SHOW", {});
ternServer.complete(editor);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
editor.closeHint();
}
},
};
};

View File

@ -0,0 +1,371 @@
import React, { Component, lazy, Suspense } from "react";
import { connect } from "react-redux";
import { AppState } from "reducers";
import CodeMirror, { EditorConfiguration } from "codemirror";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/monokai.css";
import "codemirror/theme/neat.css";
import "codemirror/addon/hint/show-hint";
import "codemirror/addon/display/placeholder";
import "codemirror/addon/edit/closebrackets";
import "codemirror/addon/display/autorefresh";
import "codemirror/addon/mode/multiplex";
import "codemirror/addon/tern/tern.css";
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup";
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
import _ from "lodash";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { Skin } from "constants/DefaultTheme";
import AnalyticsUtil from "utils/AnalyticsUtil";
import "components/editorComponents/CodeEditor/modes";
import {
EditorConfig,
EditorSize,
EditorTheme,
EditorThemes,
Hinter,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
import {
DynamicAutocompleteInputWrapper,
EditorWrapper,
HintStyles,
IconContainer,
} from "components/editorComponents/CodeEditor/styledComponents";
import { bindingMarker } from "components/editorComponents/CodeEditor/markHelpers";
import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers";
const LightningMenu = lazy(() =>
import("components/editorComponents/LightningMenu"),
);
const AUTOCOMPLETE_CLOSE_KEY_CODES = ["Enter", "Tab", "Escape"];
interface ReduxStateProps {
dynamicData: DataTree;
}
export type EditorStyleProps = {
placeholder?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
height?: string | number;
meta?: Partial<WrappedFieldMetaProps>;
showLineNumbers?: boolean;
className?: string;
leftImage?: string;
disabled?: boolean;
link?: string;
showLightningMenu?: boolean;
dataTreePath?: string;
evaluatedValue?: any;
expected?: string;
borderLess?: boolean;
};
export type EditorProps = EditorStyleProps &
EditorConfig & {
input: Partial<WrappedFieldInputProps>;
};
type Props = ReduxStateProps & EditorProps;
type State = {
isFocused: boolean;
isOpened: boolean;
autoCompleteVisible: boolean;
};
class CodeEditor extends Component<Props, State> {
static defaultProps = {
marking: [bindingMarker],
hinting: [bindingHint],
};
textArea = React.createRef<HTMLTextAreaElement>();
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
editor: CodeMirror.Editor;
hinters: Hinter[] = [];
constructor(props: Props) {
super(props);
this.state = {
isFocused: false,
isOpened: false,
autoCompleteVisible: false,
};
this.updatePropertyValue = this.updatePropertyValue.bind(this);
}
componentDidMount(): void {
if (this.textArea.current) {
const options: EditorConfiguration = {
mode: this.props.mode,
theme: EditorThemes[this.props.theme],
viewportMargin: 10,
tabSize: 2,
autoCloseBrackets: true,
indentWithTabs: this.props.tabBehaviour === TabBehaviour.INDENT,
lineWrapping: this.props.size !== EditorSize.COMPACT,
lineNumbers: this.props.showLineNumbers,
addModeClass: true,
};
if (!this.props.input.onChange || this.props.disabled) {
options.readOnly = true;
options.scrollbarStyle = "null";
}
options.extraKeys = {};
if (this.props.tabBehaviour === TabBehaviour.INPUT) {
options.extraKeys["Tab"] = false;
}
this.editor = CodeMirror.fromTextArea(this.textArea.current, options);
this.editor.on("change", _.debounce(this.handleChange, 300));
this.editor.on("change", this.handleAutocompleteVisibility);
this.editor.on("keyup", this.handleAutocompleteHide);
this.editor.on("focus", this.handleEditorFocus);
this.editor.on("blur", this.handleEditorBlur);
if (this.props.height) {
this.editor.setSize(0, this.props.height);
} else {
this.editor.setSize(0, "auto");
}
// Set value of the editor
let inputValue = this.props.input.value || "";
if (typeof inputValue === "object") {
inputValue = JSON.stringify(inputValue, null, 2);
} else if (
typeof inputValue === "number" ||
typeof inputValue === "string"
) {
inputValue += "";
}
this.editor.setValue(inputValue);
this.updateMarkings();
this.startAutocomplete();
}
}
componentDidUpdate(prevProps: Props): void {
this.editor.refresh();
if (!this.state.isFocused) {
const currentMode = this.editor.getOption("mode");
const editorValue = this.editor.getValue();
let inputValue = this.props.input.value;
// Safe update of value of the editor when value updated outside the editor
if (typeof inputValue === "object") {
inputValue = JSON.stringify(inputValue, null, 2);
} else if (
typeof inputValue === "number" ||
typeof inputValue === "string"
) {
inputValue += "";
}
if ((!!inputValue || inputValue === "") && inputValue !== editorValue) {
this.editor.setValue(inputValue);
}
if (currentMode !== this.props.mode) {
this.editor.setOption("mode", this.props?.mode);
}
} else {
// Update the dynamic bindings for autocomplete
if (prevProps.dynamicData !== this.props.dynamicData) {
this.hinters.forEach(
hinter => hinter.update && hinter.update(this.props.dynamicData),
);
}
}
}
startAutocomplete() {
this.hinters = this.props.hinting.map(helper => {
return helper(this.editor, this.props.dynamicData);
});
}
handleEditorFocus = () => {
this.setState({ isFocused: true });
this.editor.refresh();
if (this.props.size === EditorSize.COMPACT) {
this.editor.setOption("lineWrapping", true);
}
};
handleEditorBlur = () => {
this.handleChange();
this.setState({ isFocused: false });
if (this.props.size === EditorSize.COMPACT) {
this.editor.setOption("lineWrapping", false);
}
};
handleChange = (instance?: any, changeObj?: any) => {
const value = this.editor.getValue();
if (changeObj && changeObj.origin === "complete") {
AnalyticsUtil.logEvent("AUTO_COMPLETE_SELECT", {
searchString: changeObj.text[0],
});
}
const inputValue = this.props.input.value;
if (this.props.input.onChange && value !== inputValue) {
this.props.input.onChange(value);
}
this.updateMarkings();
};
handleAutocompleteVisibility = (cm: CodeMirror.Editor) => {
this.hinters.forEach(hinter => hinter.showHint(cm));
};
handleAutocompleteHide = (cm: any, event: KeyboardEvent) => {
if (AUTOCOMPLETE_CLOSE_KEY_CODES.includes(event.code)) {
cm.closeHint();
}
};
updateMarkings = () => {
this.props.marking.forEach(helper => this.editor && helper(this.editor));
};
updatePropertyValue(value: string, cursor?: number) {
if (value) {
this.editor.setValue(value);
}
this.editor.focus();
if (cursor === undefined) {
if (value) {
cursor = value.length - 2;
} else {
cursor = 1;
}
}
this.editor.setCursor({
line: 0,
ch: cursor,
});
this.setState({ isFocused: true }, () => {
this.handleAutocompleteVisibility(this.editor);
});
}
render() {
const {
input,
meta,
theme,
disabled,
className,
showLightningMenu,
dataTreePath,
dynamicData,
expected,
size,
evaluatedValue,
height,
borderLess,
} = this.props;
const hasError = !!(meta && meta.error);
let evaluated = evaluatedValue;
if (dataTreePath) {
evaluated = _.get(dynamicData, dataTreePath);
}
const showEvaluatedValue =
this.state.isFocused &&
("evaluatedValue" in this.props ||
("dataTreePath" in this.props && !!this.props.dataTreePath));
return (
<DynamicAutocompleteInputWrapper
theme={this.props.theme}
skin={this.props.theme === EditorTheme.DARK ? Skin.DARK : Skin.LIGHT}
isActive={(this.state.isFocused && !hasError) || this.state.isOpened}
isNotHover={this.state.isFocused || this.state.isOpened}
>
{showLightningMenu !== false && (
<Suspense fallback={<div />}>
<LightningMenu
skin={
this.props.theme === EditorTheme.DARK ? Skin.DARK : Skin.LIGHT
}
updateDynamicInputValue={this.updatePropertyValue}
isFocused={this.state.isFocused}
isOpened={this.state.isOpened}
onOpenLightningMenu={() => {
this.setState({ isOpened: true });
}}
onCloseLightningMenu={() => {
this.setState({ isOpened: false });
}}
/>
</Suspense>
)}
<EvaluatedValuePopup
theme={theme || EditorTheme.LIGHT}
isOpen={showEvaluatedValue}
evaluatedValue={evaluated}
expected={expected}
hasError={hasError}
>
<EditorWrapper
editorTheme={theme}
hasError={hasError}
size={size}
isFocused={this.state.isFocused}
disabled={disabled}
className={className}
height={height}
borderLess={borderLess}
>
<HintStyles editorTheme={theme || EditorTheme.LIGHT} />
{this.props.leftIcon && (
<IconContainer>{this.props.leftIcon}</IconContainer>
)}
{this.props.leftImage && (
<img
src={this.props.leftImage}
alt="img"
className="leftImageStyles"
/>
)}
<textarea
ref={this.textArea}
{..._.omit(this.props.input, ["onChange", "value"])}
defaultValue={input.value}
placeholder={this.props.placeholder}
/>
{this.props.link && (
<React.Fragment>
<a
href={this.props.link}
target="_blank"
className="linkStyles"
rel="noopener noreferrer"
>
API documentation
</a>
</React.Fragment>
)}
{this.props.rightIcon && (
<IconContainer>{this.props.rightIcon}</IconContainer>
)}
</EditorWrapper>
</EvaluatedValuePopup>
</DynamicAutocompleteInputWrapper>
);
}
}
const mapStateToProps = (state: AppState): ReduxStateProps => ({
dynamicData: getDataTreeForAutocomplete(state),
});
export default connect(mapStateToProps)(CodeEditor);

View File

@ -0,0 +1,21 @@
import CodeMirror from "codemirror";
import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants";
import { MarkHelper } from "components/editorComponents/CodeEditor/EditorConfig";
export const bindingMarker: MarkHelper = (editor: CodeMirror.Editor) => {
editor.eachLine((line: CodeMirror.LineHandle) => {
const lineNo = editor.getLineNumber(line) || 0;
let match;
while ((match = AUTOCOMPLETE_MATCH_REGEX.exec(line.text)) != null) {
const start = match.index;
const end = AUTOCOMPLETE_MATCH_REGEX.lastIndex;
editor.markText(
{ ch: start, line: lineNo },
{ ch: end, line: lineNo },
{
className: "binding-highlight",
},
);
}
});
};

View File

@ -0,0 +1,51 @@
import CodeMirror from "codemirror";
import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig";
import "codemirror/addon/mode/multiplex";
import "codemirror/mode/javascript/javascript";
import "codemirror/mode/sql/sql";
import "codemirror/addon/hint/sql-hint";
CodeMirror.defineMode(EditorModes.TEXT_WITH_BINDING, function(config) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, EditorModes.TEXT),
{
open: "{{",
close: "}}",
mode: CodeMirror.getMode(config, {
name: "javascript",
}),
},
);
});
CodeMirror.defineMode(EditorModes.JSON_WITH_BINDING, function(config) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, { name: "javascript", json: true }),
{
open: "{{",
close: "}}",
mode: CodeMirror.getMode(config, {
name: "javascript",
}),
},
);
});
CodeMirror.defineMode(EditorModes.SQL_WITH_BINDING, function(config) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, EditorModes.SQL),
{
open: "{{",
close: "}}",
mode: CodeMirror.getMode(config, {
name: "javascript",
}),
},
);
});

View File

@ -0,0 +1,256 @@
import styled, { createGlobalStyle } from "styled-components";
import {
EditorSize,
EditorTheme,
} from "components/editorComponents/CodeEditor/EditorConfig";
import { Skin, Theme } from "constants/DefaultTheme";
import { Colors } from "constants/Colors";
export const HintStyles = createGlobalStyle<{ editorTheme: EditorTheme }>`
.CodeMirror-hints {
position: absolute;
z-index: 20;
overflow: hidden;
list-style: none;
margin: 0;
padding: 5px;
font-size: 90%;
font-family: monospace;
max-height: 20em;
width: 300px;
overflow-y: auto;
background: ${props =>
props.editorTheme === EditorTheme.DARK ? "#090A0F" : "#ffffff"};
border: 1px solid;
border-color: ${props =>
props.editorTheme === EditorTheme.DARK ? "#535B62" : "#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: ${props =>
props.editorTheme === EditorTheme.DARK ? "#F4F4F4" : "#1E242B"};
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
}
li.CodeMirror-hint-active {
background: ${props =>
props.editorTheme === EditorTheme.DARK
? "rgba(244,244,244,0.1)"
: "rgba(128,136,141,0.1)"};
border-radius: 4px;
}
.CodeMirror-Tern-completion {
padding-left: 22px !important;
}
.CodeMirror-Tern-completion:before {
left: 4px !important;
bottom: 7px !important;
line-height: 15px !important;
}
.CodeMirror-Tern-tooltip {
z-index: 20 !important;
}
.CodeMirror-Tern-hint-doc {
background-color: ${props =>
props.editorTheme === EditorTheme.DARK ? "#23292e" : "#fff"} !important;
color: ${props =>
props.editorTheme === EditorTheme.DARK
? "#F4F4F4"
: "#1E242B"} !important;
max-height: 150px;
width: 250px;
padding: 12px !important;
border: 1px solid !important;
border-color: ${props =>
props.editorTheme === EditorTheme.DARK
? "#23292e"
: "#DEDEDE"} !important;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.12) !important;
overflow: scroll;
}
`;
const getBorderStyle = (
props: { theme: Theme } & {
editorTheme?: EditorTheme;
hasError: boolean;
size: EditorSize;
isFocused: boolean;
disabled?: boolean;
},
) => {
if (props.hasError) return props.theme.colors.error;
if (props.editorTheme !== EditorTheme.DARK) {
if (props.isFocused) return props.theme.colors.inputActiveBorder;
return props.theme.colors.border;
}
return "transparent";
};
export const EditorWrapper = styled.div<{
editorTheme?: EditorTheme;
hasError: boolean;
isFocused: boolean;
disabled?: boolean;
size: EditorSize;
height?: string | number;
borderLess?: boolean;
}>`
width: 100%;
${props =>
props.size === EditorSize.COMPACT && props.isFocused
? `
z-index: 5;
position: absolute;
right: 0;
left: 0;
top: 0;
`
: `z-index: 0; position: relative;`}
min-height: 32px;
height: ${props => props.height || "auto"};
background-color: ${props =>
props.editorTheme === EditorTheme.DARK ? "#272822" : "#fff"};
background-color: ${props => props.disabled && "#eef2f5"};
${props =>
!props.borderLess &&
`
border: 1px solid;
border-radius: 4px;
`}
border-color: ${getBorderStyle};
display: flex;
flex: 1;
flex-direction: row;
text-transform: none;
&& {
.binding-highlight {
color: ${props =>
props.editorTheme === EditorTheme.DARK
? props.theme.colors.bindingTextDark
: props.theme.colors.bindingText};
font-weight: 700;
}
.datasource-highlight {
background-color: rgba(104, 113, 239, 0.1);
border: 1px solid rgba(104, 113, 239, 0.5);
padding: 2px;
border-radius: 2px;
margin-right: 2px;
}
.CodeMirror {
flex: 1;
line-height: 21px;
z-index: 0;
border-radius: 4px;
height: auto;
}
${props =>
props.disabled &&
`
.CodeMirror-cursor {
display: none !important;
}
`}
.CodeMirror pre.CodeMirror-placeholder {
color: #a3b3bf;
}
${props =>
props.size === EditorSize.COMPACT &&
`
.CodeMirror-hscrollbar {
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
`}
}
&& {
.CodeMirror-lines {
background-color: ${props => props.disabled && "#eef2f5"};
cursor: ${props => (props.disabled ? "not-allowed" : "text")};
}
}
.bp3-popover-target {
padding-right: 10px;
padding-top: 5px;
}
.leftImageStyles {
width: 20px;
height: 20px;
margin: 5px;
}
.linkStyles {
margin: 5px;
margin-right: 11px;
}
`;
export const IconContainer = styled.div`
border-radius: 4px 0 0 4px;
margin: 0;
height: 30px;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
background-color: #eef2f5;
svg {
height: 20px;
width: 20px;
path {
fill: #979797;
}
}
`;
export const DynamicAutocompleteInputWrapper = styled.div<{
skin: Skin;
theme: Theme;
isActive: boolean;
isNotHover: boolean;
}>`
width: 100%;
height: 100%;
flex: 1;
position: relative;
border: ${props => (props.skin === Skin.DARK ? "1px solid" : "none")};
border-radius: 2px;
border-color: ${props =>
props.isActive && props.skin === Skin.DARK
? Colors.ALABASTER
: "transparent"};
&:hover {
border: ${props =>
props.skin === Skin.DARK ? "1px solid " + Colors.ALABASTER : "none"};
.lightning-menu {
background: ${props =>
!props.isNotHover
? props.skin === Skin.DARK
? Colors.ALABASTER
: Colors.BLUE_CHARCOAL
: ""};
svg {
path,
circle {
fill: ${props =>
!props.isNotHover
? props.skin === Skin.DARK
? Colors.BLUE_CHARCOAL
: Colors.WHITE
: ""};
}
}
}
}
`;

View File

@ -1,762 +0,0 @@
import React, { Component, lazy, Suspense } from "react";
import { connect } from "react-redux";
import { AppState } from "reducers";
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/display/placeholder";
import "codemirror/addon/edit/closebrackets";
import "codemirror/addon/display/autorefresh";
import "codemirror/addon/mode/multiplex";
import "codemirror/addon/tern/tern.css";
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants";
import HelperTooltip from "components/editorComponents/HelperTooltip";
import EvaluatedValuePopup from "components/editorComponents/EvaluatedValuePopup";
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
import _ from "lodash";
import { getDynamicStringSegments } from "utils/DynamicBindingUtils";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { Theme, Skin } from "constants/DefaultTheme";
import { Colors } from "constants/Colors";
import AnalyticsUtil from "utils/AnalyticsUtil";
import TernServer from "utils/autocomplete/TernServer";
import KeyboardShortcuts from "constants/KeyboardShortcuts";
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
const LightningMenu = lazy(() =>
import("components/editorComponents/LightningMenu"),
);
require("codemirror/mode/javascript/javascript");
require("codemirror/mode/sql/sql");
require("codemirror/addon/hint/sql-hint");
CodeMirror.defineMode("sql-js", function(config) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, "text/x-sql"),
{
open: "{{",
close: "}}",
mode: CodeMirror.getMode(config, {
name: "javascript",
globalVars: true,
}),
},
// .. more multiplexed styles can follow here
);
});
CodeMirror.defineMode("js-js", function(config) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, { name: "javascript", json: true }),
{
open: "{{",
close: "}}",
mode: CodeMirror.getMode(config, {
name: "javascript",
globalVars: true,
}),
},
// .. more multiplexed styles can follow here
);
});
const getBorderStyle = (
props: { theme: Theme } & {
editorTheme?: EditorTheme;
hasError: boolean;
singleLine: boolean;
isFocused: boolean;
disabled?: boolean;
},
) => {
if (props.hasError) return props.theme.colors.error;
if (props.editorTheme !== THEMES.DARK) {
if (props.isFocused) return props.theme.colors.inputActiveBorder;
return props.theme.colors.border;
}
return "transparent";
};
const HintStyles = createGlobalStyle<{ editorTheme: EditorTheme }>`
.CodeMirror-hints {
position: absolute;
z-index: 20;
overflow: hidden;
list-style: none;
margin: 0;
padding: 5px;
font-size: 90%;
font-family: monospace;
max-height: 20em;
width: 200px;
overflow-y: auto;
::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
background: ${props =>
props.editorTheme === "DARK" ? "#090A0F" : "#ffffff"};
border: 1px solid;
border-color: ${props =>
props.editorTheme === "DARK" ? "#535B62" : "#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: ${props => (props.editorTheme === "DARK" ? "#F4F4F4" : "#1E242B")};
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
}
li.CodeMirror-hint-active {
background: ${props =>
props.editorTheme === "DARK"
? "rgba(244,244,244,0.1)"
: "rgba(128,136,141,0.1)"};
border-radius: 4px;
}
.CodeMirror-Tern-completion {
padding-left: 22px !important;
}
.CodeMirror-Tern-completion:before {
left: 4px !important;
bottom: 7px !important;
line-height: 15px !important;
}
.CodeMirror-Tern-tooltip {
z-index: 20 !important;
}
.CodeMirror-Tern-hint-doc {
background-color: ${props =>
props.editorTheme === "DARK" ? "#23292e" : "#fff"} !important;
color: ${props =>
props.editorTheme === "DARK" ? "#F4F4F4" : "#1E242B"} !important;
max-height: 150px;
width: 250px;
padding: 12px !important;
border: 1px solid !important;
border-color: ${props =>
props.editorTheme === "DARK" ? "#23292e" : "#DEDEDE"} !important;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.12) !important;
overflow: scroll;
::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
}
`;
const EditorWrapper = styled.div<{
editorTheme?: EditorTheme;
hasError: boolean;
singleLine: boolean;
isFocused: boolean;
disabled?: boolean;
setMaxHeight?: boolean;
}>`
width: 100%;
${props =>
props.singleLine && props.isFocused
? `
z-index: 5;
position: absolute;
right: 0;
left: 0;
top: 0;
`
: `z-index: 0; position: relative;`}
background-color: ${props =>
props.editorTheme === THEMES.DARK ? "#272822" : "#fff"};
background-color: ${props => props.disabled && "#eef2f5"};
border: 1px solid;
border-color: ${getBorderStyle};
border-radius: 4px;
display: flex;
flex: 1;
flex-direction: row;
text-transform: none;
min-height: 32px;
height: auto;
${props =>
props.setMaxHeight &&
props.isFocused &&
`
z-index: 5;
position: absolute;
right: 0;
left: 0;
top: 0;
`}
${props => props.setMaxHeight && !props.isFocused && `max-height: 30px;`}
&& {
.binding-highlight {
color: ${props =>
props.editorTheme === THEMES.DARK
? props.theme.colors.bindingTextDark
: props.theme.colors.bindingText};
font-weight: 700;
}
.CodeMirror {
flex: 1;
line-height: 21px;
z-index: 0;
border-radius: 4px;
height: auto;
}
${props =>
props.disabled &&
`
.CodeMirror-cursor {
display: none !important;
}
`}
.CodeMirror pre.CodeMirror-placeholder {
color: #a3b3bf;
}
${props =>
props.singleLine &&
`
.CodeMirror-hscrollbar {
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
`}
}
&& {
.CodeMirror-lines {
background-color: ${props => props.disabled && "#eef2f5"};
cursor: ${props => (props.disabled ? "not-allowed" : "text")}
}
}
.bp3-popover-target {
padding-right: 10px;
padding-top: 5px;
}
.leftImageStyles {
width: 20px;
height: 20px;
margin: 5px;
}
.linkStyles {
margin: 5px;
margin-right: 11px;
}
`;
const IconContainer = styled.div`
.bp3-icon {
border-radius: 4px 0 0 4px;
margin: 0;
height: 30px;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
background-color: #eef2f5;
svg {
height: 20px;
width: 20px;
path {
fill: #979797;
}
}
}
.bp3-popover-target {
padding-right: 10px;
}
`;
const DynamicAutocompleteInputWrapper = styled.div<{
skin: Skin;
theme: Theme;
isActive: boolean;
isNotHover: boolean;
}>`
width: 100%;
height: 100%;
flex: 1;
position: relative;
border: ${props => (props.skin === Skin.DARK ? "1px solid" : "none")};
border-radius: 2px;
border-color: ${props =>
props.isActive && props.skin === Skin.DARK
? Colors.ALABASTER
: "transparent"};
&:hover {
border: ${props =>
props.skin === Skin.DARK ? "1px solid " + Colors.ALABASTER : "none"};
.lightning-menu {
background: ${props =>
!props.isNotHover
? props.skin === Skin.DARK
? Colors.ALABASTER
: Colors.BLUE_CHARCOAL
: ""};
svg {
path,
circle {
fill: ${props =>
!props.isNotHover
? props.skin === Skin.DARK
? Colors.BLUE_CHARCOAL
: Colors.WHITE
: ""};
}
}
}
}
`;
const THEMES: Record<string, EditorTheme> = {
LIGHT: "LIGHT",
DARK: "DARK",
};
export type EditorTheme = "LIGHT" | "DARK";
const AUTOCOMPLETE_CLOSE_KEY_CODES = ["Enter", "Tab", "Escape"];
interface ReduxStateProps {
dynamicData: DataTree;
}
export type DynamicAutocompleteInputProps = {
placeholder?: string;
leftIcon?: Function;
rightIcon?: Function;
description?: string;
height?: number;
theme?: EditorTheme;
meta?: Partial<WrappedFieldMetaProps>;
showLineNumbers?: boolean;
allowTabIndent?: boolean;
singleLine: boolean;
mode?: string | object;
className?: string;
leftImage?: string;
disabled?: boolean;
link?: string;
baseMode?: string | object;
setMaxHeight?: boolean;
showLightningMenu?: boolean;
dataTreePath?: string;
evaluatedValue?: any;
expected?: string;
};
type Props = ReduxStateProps &
DynamicAutocompleteInputProps & {
input: Partial<WrappedFieldInputProps>;
};
type State = {
isFocused: boolean;
isOpened: boolean;
autoCompleteVisible: boolean;
};
class DynamicAutocompleteInput extends Component<Props, State> {
textArea = React.createRef<HTMLTextAreaElement>();
editor: any;
ternServer?: TernServer = undefined;
constructor(props: Props) {
super(props);
this.state = {
isFocused: false,
isOpened: false,
autoCompleteVisible: false,
};
this.updatePropertyValue = this.updatePropertyValue.bind(this);
}
componentDidMount(): void {
if (this.textArea.current) {
const options: EditorConfiguration = {};
if (this.props.theme === "DARK") options.theme = "monokai";
if (!this.props.input.onChange || this.props.disabled) {
options.readOnly = true;
options.scrollbarStyle = "null";
}
if (this.props.showLineNumbers) options.lineNumbers = true;
const extraKeys: Record<string, any> = {};
if (!this.props.allowTabIndent) extraKeys["Tab"] = false;
this.editor = CodeMirror.fromTextArea(this.textArea.current, {
mode: this.props.mode || { name: "javascript", globalVars: true },
viewportMargin: 10,
tabSize: 2,
indentWithTabs: !!this.props.allowTabIndent,
lineWrapping: !this.props.singleLine,
extraKeys,
autoCloseBrackets: true,
...options,
});
this.editor.on("change", _.debounce(this.handleChange, 300));
this.editor.on("change", this.handleAutocompleteVisibility);
this.editor.on("keyup", this.handleAutocompleteHide);
this.editor.on("focus", this.handleEditorFocus);
this.editor.on("blur", this.handleEditorBlur);
if (this.props.height) {
this.editor.setSize(0, this.props.height);
} else {
this.editor.setSize(0, "auto");
}
this.editor.eachLine(this.highlightBindings);
// Set value of the editor
let inputValue = this.props.input.value || "";
if (typeof inputValue === "object") {
inputValue = JSON.stringify(inputValue, null, 2);
} else if (
typeof inputValue === "number" ||
typeof inputValue === "string"
) {
inputValue += "";
}
this.editor.setValue(inputValue);
this.startAutocomplete();
}
}
componentDidUpdate(prevProps: Props): void {
if (this.editor) {
this.editor.refresh();
if (!this.state.isFocused) {
const currentMode = this.editor.getOption("mode");
const editorValue = this.editor.getValue();
let inputValue = this.props.input.value;
// Safe update of value of the editor when value updated outside the editor
if (typeof inputValue === "object") {
inputValue = JSON.stringify(inputValue, null, 2);
} else if (
typeof inputValue === "number" ||
typeof inputValue === "string"
) {
inputValue += "";
}
if ((!!inputValue || inputValue === "") && inputValue !== editorValue) {
this.editor.setValue(inputValue);
}
if (currentMode !== this.props.mode) {
this.editor.setOption("mode", this.props?.mode);
}
} else {
// Update the dynamic bindings for autocomplete
if (prevProps.dynamicData !== this.props.dynamicData) {
if (this.ternServer) {
const dataTreeDef = dataTreeTypeDefCreator(this.props.dynamicData);
this.ternServer.updateDef("dataTree", dataTreeDef);
} else {
this.editor.setOption("hintOptions", {
completeSingle: false,
globalScope: this.props.dynamicData,
});
}
}
}
}
}
startAutocomplete() {
try {
this.ternServer = new TernServer(this.props.dynamicData);
} catch (e) {
console.error(e);
}
if (this.ternServer) {
this.editor.setOption("extraKeys", {
...this.editor.options.extraKeys,
[KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (
cm: CodeMirror.Editor,
) => {
if (this.ternServer) this.ternServer.complete(cm);
},
[KeyboardShortcuts.CodeEditor.ShowTypeAndInfo]: (cm: any) => {
if (this.ternServer) this.ternServer.showType(cm);
},
[KeyboardShortcuts.CodeEditor.OpenDocsLink]: (cm: any) => {
if (this.ternServer) this.ternServer.showDocs(cm);
},
});
} else {
// start normal autocomplete
this.editor.setOption("extraKeys", {
...this.editor.options.extraKeys,
[KeyboardShortcuts.CodeEditor.OpenAutocomplete]: "autocomplete",
});
this.editor.setOption("showHint", true);
this.editor.setOption("hintOptions", {
completeSingle: false,
globalScope: this.props.dynamicData,
});
}
}
handleEditorFocus = () => {
this.setState({ isFocused: true });
this.editor.refresh();
if (this.props.singleLine) {
this.editor.setOption("lineWrapping", true);
}
};
handleEditorBlur = () => {
this.handleChange();
this.setState({ isFocused: false });
if (this.props.singleLine) {
this.editor.setOption("lineWrapping", false);
}
};
handleChange = (instance?: any, changeObj?: any) => {
const value = this.editor.getValue();
if (changeObj && changeObj.origin === "complete") {
AnalyticsUtil.logEvent("AUTO_COMPLETE_SELECT", {
searchString: changeObj.text[0],
});
}
const inputValue = this.props.input.value;
if (this.props.input.onChange && value !== inputValue) {
this.props.input.onChange(value);
}
this.editor.eachLine(this.highlightBindings);
};
handleAutocompleteVisibility = (cm: any) => {
if (this.state.isFocused) {
let cursorBetweenBinding = false;
const cursor = this.editor.getCursor();
const value = this.editor.getValue();
let cursorIndex = cursor.ch;
if (cursor.line > 0) {
for (let lineIndex = 0; lineIndex < cursor.line; lineIndex++) {
const line = this.editor.getLine(lineIndex);
// Add line length + 1 for new line character
cursorIndex = cursorIndex + line.length + 1;
}
}
const stringSegments = getDynamicStringSegments(value);
// count of chars processed
let cumulativeCharCount = 0;
stringSegments.forEach((segment: string) => {
const start = cumulativeCharCount;
const dynamicStart = segment.indexOf("{{");
const dynamicDoesStart = dynamicStart > -1;
const dynamicEnd = segment.indexOf("}}");
const dynamicDoesEnd = dynamicEnd > -1;
const dynamicStartIndex = dynamicStart + start + 2;
const dynamicEndIndex = dynamicEnd + start;
if (
dynamicDoesStart &&
cursorIndex >= dynamicStartIndex &&
((dynamicDoesEnd && cursorIndex <= dynamicEndIndex) ||
(!dynamicDoesEnd && cursorIndex >= dynamicStartIndex))
) {
cursorBetweenBinding = true;
}
cumulativeCharCount = start + segment.length;
});
const shouldShow = cursorBetweenBinding;
if (this.props.baseMode) {
// https://github.com/codemirror/CodeMirror/issues/5249#issue-295565980
cm.doc.modeOption = this.props.baseMode;
}
if (shouldShow) {
AnalyticsUtil.logEvent("AUTO_COMPELTE_SHOW", {});
this.setState({
autoCompleteVisible: true,
});
if (this.ternServer) {
this.ternServer.complete(cm);
} else {
cm.showHint(cm);
}
} else {
this.setState({
autoCompleteVisible: false,
});
cm.closeHint();
}
}
};
handleAutocompleteHide = (cm: any, event: KeyboardEvent) => {
if (AUTOCOMPLETE_CLOSE_KEY_CODES.includes(event.code)) {
cm.closeHint();
}
};
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",
},
);
}
};
updatePropertyValue(value: string, cursor?: number) {
if (value) {
this.editor.setValue(value);
}
this.editor.focus();
if (cursor === undefined) {
if (value) {
cursor = value.length - 2;
} else {
cursor = 1;
}
}
this.editor.setCursor({
line: 0,
ch: cursor,
});
this.setState({ isFocused: true }, () => {
this.handleAutocompleteVisibility(this.editor);
});
}
render() {
const {
input,
meta,
theme,
singleLine,
disabled,
className,
setMaxHeight,
showLightningMenu,
dataTreePath,
dynamicData,
expected,
evaluatedValue,
} = this.props;
const hasError = !!(meta && meta.error);
let evaluated = evaluatedValue;
if (dataTreePath) {
evaluated = _.get(dynamicData, dataTreePath);
}
const showEvaluatedValue =
this.state.isFocused &&
("evaluatedValue" in this.props ||
("dataTreePath" in this.props && !!this.props.dataTreePath));
return (
<DynamicAutocompleteInputWrapper
theme={this.props.theme}
skin={this.props.theme === "DARK" ? Skin.DARK : Skin.LIGHT}
isActive={(this.state.isFocused && !hasError) || this.state.isOpened}
isNotHover={this.state.isFocused || this.state.isOpened}
>
{showLightningMenu !== false && (
<Suspense fallback={<div />}>
<LightningMenu
skin={this.props.theme === "DARK" ? Skin.DARK : Skin.LIGHT}
updateDynamicInputValue={this.updatePropertyValue}
isFocused={this.state.isFocused}
isOpened={this.state.isOpened}
onOpenLightningMenu={() => {
this.setState({ isOpened: true });
}}
onCloseLightningMenu={() => {
this.setState({ isOpened: false });
}}
/>
</Suspense>
)}
<EvaluatedValuePopup
theme={theme || THEMES.LIGHT}
isOpen={showEvaluatedValue}
evaluatedValue={evaluated}
expected={expected}
hasError={hasError}
>
<EditorWrapper
editorTheme={theme}
hasError={hasError}
singleLine={singleLine}
isFocused={this.state.isFocused}
disabled={disabled}
className={className}
setMaxHeight={setMaxHeight}
>
<HintStyles editorTheme={theme || THEMES.LIGHT} />
<IconContainer>
{this.props.leftIcon && <this.props.leftIcon />}
</IconContainer>
{this.props.leftImage && (
<img
src={this.props.leftImage}
alt="img"
className="leftImageStyles"
/>
)}
<textarea
ref={this.textArea}
{..._.omit(this.props.input, ["onChange", "value"])}
defaultValue={input.value}
placeholder={this.props.placeholder}
/>
{this.props.link && (
<React.Fragment>
<a
href={this.props.link}
target="_blank"
className="linkStyles"
rel="noopener noreferrer"
>
API documentation
</a>
</React.Fragment>
)}
{this.props.rightIcon && (
<div
style={{ zIndex: 100, position: "absolute", right: "-36px" }}
>
<HelperTooltip
description={this.props.description}
rightIcon={this.props.rightIcon}
/>
</div>
)}
</EditorWrapper>
</EvaluatedValuePopup>
</DynamicAutocompleteInputWrapper>
);
}
}
const mapStateToProps = (state: AppState): ReduxStateProps => ({
dynamicData: getDataTreeForAutocomplete(state),
});
export default connect(mapStateToProps)(DynamicAutocompleteInput);

View File

@ -0,0 +1,37 @@
import React, { ChangeEvent } from "react";
import CodeEditor, {
EditorProps,
} from "components/editorComponents/CodeEditor";
import {
EditorModes,
EditorSize,
EditorTheme,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
interface Props {
input: {
value: string;
onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void;
};
height: string;
}
const ReadOnlyEditor = (props: Props) => {
const editorProps: EditorProps = {
hinting: [],
input: props.input,
marking: [],
mode: EditorModes.JSON_WITH_BINDING,
size: EditorSize.EXTENDED,
tabBehaviour: TabBehaviour.INDENT,
theme: EditorTheme.LIGHT,
height: props.height,
showLightningMenu: false,
showLineNumbers: true,
borderLess: true,
};
return <CodeEditor {...editorProps} />;
};
export default ReadOnlyEditor;

View File

@ -1,7 +1,7 @@
import { ITreeNode, Classes, Tree } from "@blueprintjs/core";
import React, { useState } from "react";
import styled from "styled-components";
import CodeEditor from "components/editorComponents/CodeEditor";
import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
const StyledKey = styled.span`
font-family: DM Sans;
@ -167,7 +167,7 @@ export function RequestView(props: {
{
id: 1,
label: (
<CodeEditor
<ReadOnlyEditor
input={{
value: props.requestBody,
}}

View File

@ -0,0 +1,85 @@
import { Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
import { ControlIcons } from "icons/ControlIcons";
import { theme } from "constants/DefaultTheme";
import React from "react";
import styled, { createGlobalStyle } from "styled-components";
import { IconWrapper } from "constants/IconConstants";
import { ReactComponent as StorageIcon } from "assets/icons/menu/storage.svg";
import { storeAsDatasource } from "actions/datasourceActions";
import { useDispatch } from "react-redux";
type Props = {};
const StyledMenu = styled(Menu)`
&&&&.bp3-menu {
padding: 8px;
background: #ffffff;
border: 1px solid #ebeff2;
box-sizing: border-box;
box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14);
border-radius: 4px;
}
`;
const StyledMenuItem = styled(MenuItem)`
&&&&.bp3-menu-item {
align-items: center;
width: 202px;
justify-content: center;
}
`;
const TooltipStyles = createGlobalStyle`
.helper-tooltip{
.bp3-popover {
margin-right: 10px;
margin-top: 5px;
}
}
`;
const StoreAsDatasource = (props: Props) => {
const dispatch = useDispatch();
const MenuContainer = (
<StyledMenu>
<StyledMenuItem
icon={
<IconWrapper
width={theme.fontSizes[4]}
height={theme.fontSizes[4]}
color={"#535B62"}
>
<StorageIcon />
</IconWrapper>
}
text="Store as datasource"
onClick={() => dispatch(storeAsDatasource())}
/>
</StyledMenu>
);
return (
<>
<TooltipStyles />
<Popover
content={MenuContainer}
position={Position.BOTTOM_LEFT}
usePortal
portalClassName="helper-tooltip"
>
<div
onMouseDown={e => {
e.stopPropagation();
}}
>
<ControlIcons.MORE_HORIZONTAL_CONTROL
width={theme.fontSizes[4]}
height={theme.fontSizes[4]}
color="#C4C4C4"
/>
</div>
</Popover>
</>
);
};
export default StoreAsDatasource;

View File

@ -1,315 +0,0 @@
import React, { useState, useEffect } from "react";
import CreatableDropdown from "components/designSystems/appsmith/CreatableDropdown";
import { connect } from "react-redux";
import { Field, formValueSelector, change } from "redux-form";
import { AppState } from "reducers";
import { ReactComponent as StorageIcon } from "assets/icons/menu/storage.svg";
import { DatasourceDataState } from "reducers/entityReducers/datasourceReducer";
import { Plugin } from "api/PluginApi";
import { getDatasourcePlugins } from "selectors/entitiesSelector";
import _ from "lodash";
import { createDatasource, storeAsDatasource } from "actions/datasourceActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { Datasource, CreateDatasourceConfig } from "api/DatasourcesApi";
import styled, { createGlobalStyle } from "styled-components";
import { MenuItem, Menu, Popover, Position } from "@blueprintjs/core";
import { IconWrapper } from "constants/IconConstants";
import { theme } from "constants/DefaultTheme";
import { ControlIcons } from "icons/ControlIcons";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import { InputActionMeta } from "react-select";
import { setDatasourceFieldText } from "actions/apiPaneActions";
import { getCurrentOrgId } from "selectors/organizationSelectors";
interface ReduxStateProps {
datasources: DatasourceDataState;
validDatasourcePlugins: Plugin[];
apiId: string;
value: Datasource;
organizationId: string;
}
interface ReduxActionProps {
createDatasource: (value: string) => void;
storeAsDatasource: () => void;
changeDatasource: (value: Datasource | CreateDatasourceConfig) => void;
changePath: (value: string) => void;
setDatasourceFieldText: (apiId: string, value: string) => void;
}
interface ComponentProps {
name: string;
pluginId: string;
appName: string;
datasourceFieldText: string;
}
const StyledMenuItem = styled(MenuItem)`
&&&&.bp3-menu-item {
align-items: center;
width: 202px;
justify-content: center;
}
`;
const StyledMenu = styled(Menu)`
&&&&.bp3-menu {
padding: 8px;
background: #ffffff;
border: 1px solid #ebeff2;
box-sizing: border-box;
box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14);
border-radius: 4px;
}
`;
const TooltipStyles = createGlobalStyle`
.helper-tooltip{
.bp3-popover {
margin-right: 10px;
margin-top: 5px;
}
}
`;
const DatasourcesField = (
props: ReduxActionProps & ReduxStateProps & ComponentProps,
) => {
const [inputValue, setValue] = useState(props.datasourceFieldText);
useEffect(() => {
setValue(props.datasourceFieldText);
}, [props.datasourceFieldText]);
const options = React.useMemo(() => {
return props.datasources.list
.filter(r => r.pluginId === props.pluginId)
.filter(r => {
return props.validDatasourcePlugins.some(
plugin => plugin.id === r.pluginId,
);
})
.filter(r => r.datasourceConfiguration)
.filter(r => r.datasourceConfiguration.url)
.map(r => ({
label: r.datasourceConfiguration?.url,
value: r.id,
}));
}, [props.datasources.list, props.validDatasourcePlugins, props.pluginId]);
const { storeAsDatasource } = props;
let isEmbeddedDatasource = true;
if (props.value && props.value.id) {
isEmbeddedDatasource = false;
} else if (props.value && props.value.datasourceConfiguration) {
isEmbeddedDatasource = true;
}
const DropdownIndicator = (props: any) => {
if (props.hasValue || !props.selectProps.inputValue) return null;
const MenuContainer = (
<StyledMenu>
<StyledMenuItem
icon={
<IconWrapper
width={theme.fontSizes[4]}
height={theme.fontSizes[4]}
color={"#535B62"}
>
<StorageIcon />
</IconWrapper>
}
text="Store as datasource"
onClick={storeAsDatasource}
/>
</StyledMenu>
);
return (
<>
<TooltipStyles />
<Popover
content={MenuContainer}
position={Position.BOTTOM_LEFT}
usePortal
portalClassName="helper-tooltip"
>
<div
style={{
padding: "8px 13px 3px 13px",
}}
onMouseDown={e => {
e.stopPropagation();
}}
>
<ControlIcons.MORE_HORIZONTAL_CONTROL
width={theme.fontSizes[4]}
height={theme.fontSizes[4]}
color="#C4C4C4"
/>
</div>
</Popover>
</>
);
};
return (
<Field
name={props.name}
component={CreatableDropdown}
isLoading={props.datasources.loading}
options={options}
components={{
ClearIndicator: () => null,
IndicatorSeparator: () => null,
DropdownIndicator,
}}
placeholder="https://<base-url>.com"
onInputChange={(value: string, actionMeta: InputActionMeta) => {
const { action } = actionMeta;
if (action === "input-blur") {
props.setDatasourceFieldText(props.apiId, inputValue);
return value;
} else if (action === "set-value") {
setValue("");
return "";
} else if (action === "menu-close") {
return value;
}
setValue(value);
if (isEmbeddedDatasource) {
let datasourcePayload: {
name: string;
datasourceConfiguration: { url: string };
};
let pathPayload: string;
const defaultDatasourcePayload = {
pluginId: props.pluginId,
appName: props.appName,
organizationId: props.organizationId,
};
try {
const url = new URL(value);
const path = url.pathname === "/" ? "" : url.pathname;
const params = url.search;
const baseUrl = url.origin;
datasourcePayload = {
name: baseUrl || "DEFAULT_REST_DATASOURCE",
datasourceConfiguration: {
url: baseUrl,
},
};
pathPayload = path + params;
} catch (e) {
datasourcePayload = {
name: value || "DEFAULT_REST_DATASOURCE",
datasourceConfiguration: {
url: value,
},
};
pathPayload = "";
}
const updateValues = _.debounce(() => {
props.changeDatasource({
...defaultDatasourcePayload,
...datasourcePayload,
});
props.changePath(pathPayload);
}, 50);
updateValues();
} else {
const updatePath = _.debounce(() => {
props.changePath(value);
}, 50);
updatePath();
}
}}
format={(value: Datasource) => {
if (!value || !value.datasourceConfiguration) return "";
if (!value.id) {
return "";
}
const option = _.find(options, { value: value.id });
return option ? [option] : "";
}}
parse={(option: { value: string }[]) => {
if (!option) return null;
if (option.length) {
const datasources = props.datasources.list;
return datasources.find(
datasource => datasource.id === option[0].value,
);
}
}}
inputValue={inputValue}
noOptionsMessage={() => null}
/>
);
};
const mapStateToProps = (state: AppState): ReduxStateProps => {
const selector = formValueSelector(API_EDITOR_FORM_NAME);
const apiId = selector(state, "id");
const datasource = selector(state, "datasource");
const organizationId = getCurrentOrgId(state);
return {
datasources: state.entities.datasources,
validDatasourcePlugins: getDatasourcePlugins(state),
apiId,
value: datasource,
organizationId,
};
};
const mapDispatchToProps = (
dispatch: any,
ownProps: ComponentProps,
): ReduxActionProps => ({
createDatasource: (value: string) => {
AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", {
appName: ownProps.appName,
dataSource: value,
});
// "https://example.com/ "
// "https://example.com "
const trimmedValue = value.trim();
dispatch(
createDatasource({
// Datasource name should not end with /
name: trimmedValue.endsWith("/")
? trimmedValue.slice(0, -1)
: trimmedValue,
datasourceConfiguration: {
// Datasource url should end with /
url: trimmedValue.endsWith("/") ? trimmedValue : `${trimmedValue}/`,
},
pluginId: ownProps.pluginId,
appName: ownProps.appName,
}),
);
},
storeAsDatasource: () => dispatch(storeAsDatasource()),
changeDatasource: value => {
dispatch(change(API_EDITOR_FORM_NAME, "datasource", value));
},
changePath: (value: string) => {
dispatch(change(API_EDITOR_FORM_NAME, "actionConfiguration.path", value));
},
setDatasourceFieldText: (apiId, value) => {
dispatch(setDatasourceFieldText(apiId, value));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DatasourcesField);

View File

@ -1,14 +1,31 @@
import React from "react";
import { Field, BaseFieldProps } from "redux-form";
import DynamicAutocompleteInput, {
DynamicAutocompleteInputProps,
} from "components/editorComponents/DynamicAutocompleteInput";
import CodeEditor, {
EditorStyleProps,
} from "components/editorComponents/CodeEditor";
import {
EditorModes,
EditorSize,
EditorTheme,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
class DynamicTextField extends React.Component<
BaseFieldProps & DynamicAutocompleteInputProps
BaseFieldProps &
EditorStyleProps & {
size?: EditorSize;
tabBehaviour?: TabBehaviour;
mode?: EditorModes;
}
> {
render() {
return <Field component={DynamicAutocompleteInput} {...this.props} />;
const editorProps = {
mode: this.props.mode || EditorModes.TEXT_WITH_BINDING,
tabBehaviour: this.props.tabBehaviour || TabBehaviour.INPUT,
theme: EditorTheme.LIGHT,
size: this.props.size || EditorSize.COMPACT,
};
return <Field component={CodeEditor} {...this.props} {...editorProps} />;
}
}

View File

@ -0,0 +1,278 @@
import React, { ChangeEvent } from "react";
import {
BaseFieldProps,
change,
Field,
formValueSelector,
WrappedFieldInputProps,
} from "redux-form";
import CodeEditor, {
EditorProps,
} from "components/editorComponents/CodeEditor";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import { AppState } from "reducers";
import { connect } from "react-redux";
import { Datasource } from "api/DatasourcesApi";
import _ from "lodash";
import { DEFAULT_DATASOURCE, EmbeddedDatasource } from "entities/Datasource";
import CodeMirror from "codemirror";
import {
EditorModes,
EditorTheme,
TabBehaviour,
EditorSize,
} from "components/editorComponents/CodeEditor/EditorConfig";
import { bindingMarker } from "components/editorComponents/CodeEditor/markHelpers";
import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers";
import StoreAsDatasource from "components/editorComponents/StoreAsDatasource";
type ReduxStateProps = {
datasource: Datasource | EmbeddedDatasource;
datasourceList: Datasource[];
apiName: string;
};
type ReduxDispatchProps = {
updateDatasource: (datasource: Datasource | EmbeddedDatasource) => void;
};
type Props = EditorProps &
ReduxStateProps &
ReduxDispatchProps & {
input: Partial<WrappedFieldInputProps>;
pluginId: string;
};
const fullPathRegexExp = /(https?:\/{2}\S+)(\/\S*?)$/;
class EmbeddedDatasourcePathComponent extends React.Component<Props> {
handleDatasourceUrlUpdate = (datasourceUrl: string) => {
const { datasource, pluginId, datasourceList } = this.props;
const urlHasUpdated =
datasourceUrl !== datasource.datasourceConfiguration?.url;
if (urlHasUpdated) {
if ("id" in datasource && datasource.id) {
this.props.updateDatasource({
...DEFAULT_DATASOURCE(pluginId),
datasourceConfiguration: {
...datasource.datasourceConfiguration,
url: datasourceUrl,
},
});
} else {
const matchesExistingDatasource = _.find(
datasourceList,
d => d.datasourceConfiguration?.url === datasourceUrl,
);
if (matchesExistingDatasource) {
this.props.updateDatasource(matchesExistingDatasource);
} else {
this.props.updateDatasource({
...DEFAULT_DATASOURCE(pluginId),
datasourceConfiguration: {
...datasource.datasourceConfiguration,
url: datasourceUrl,
},
});
}
}
}
};
handlePathUpdate = (path: string) => {
const { value, onChange } = this.props.input;
if (onChange && value !== path) {
onChange(path);
}
};
parseInputValue = (
value: string,
): { datasourceUrl: string; path: string } => {
const { datasource } = this.props;
if (value === "") {
return {
datasourceUrl: "",
path: "",
};
}
if ("id" in datasource && datasource.id) {
const datasourceUrl = datasource.datasourceConfiguration.url;
if (value.includes(datasourceUrl)) {
return {
datasourceUrl,
path: value.replace(datasourceUrl, ""),
};
}
}
let datasourceUrl = "";
let path = "";
const isFullPath = fullPathRegexExp.test(value);
if (isFullPath) {
const matches = value.match(fullPathRegexExp);
if (matches && matches.length) {
datasourceUrl = `${matches[1]}`;
path = matches[2];
}
} else {
datasourceUrl = value;
}
return {
datasourceUrl,
path,
};
};
handleOnChange = (valueOrEvent: ChangeEvent<any> | string) => {
const value: string =
typeof valueOrEvent === "string"
? valueOrEvent
: valueOrEvent.target.value;
const { path, datasourceUrl } = this.parseInputValue(value);
this.handlePathUpdate(path);
this.handleDatasourceUrlUpdate(datasourceUrl);
};
handleDatasourceHighlight = () => {
const { datasource } = this.props;
return (editorInstance: CodeMirror.Doc) => {
if (
editorInstance.lineCount() === 1 &&
datasource &&
"id" in datasource &&
datasource.id
) {
const end = datasource.datasourceConfiguration.url.length;
editorInstance.markText(
{ ch: 0, line: 0 },
{ ch: end, line: 0 },
{
className: "datasource-highlight",
atomic: true,
inclusiveRight: false,
},
);
}
};
};
handleDatasourceHint = () => {
const { datasourceList } = this.props;
return () => {
return {
showHint: (editor: CodeMirror.Editor) => {
const value = editor.getValue();
const parsed = this.parseInputValue(value);
if (
parsed.path === "" &&
!!value &&
this.props.datasource &&
!("id" in this.props.datasource)
) {
editor.showHint({
completeSingle: false,
hint: () => {
const list = datasourceList
.filter(datasource =>
datasource.datasourceConfiguration.url.includes(
parsed.datasourceUrl,
),
)
.map(datasource => ({
text: datasource.datasourceConfiguration.url,
data: datasource,
}));
const hints = {
list,
from: { ch: 0, line: 0 },
to: editor.getCursor(),
};
CodeMirror.on(
hints,
"pick",
(selected: { text: string; data: Datasource }) => {
this.props.updateDatasource(selected.data);
},
);
return hints;
},
});
}
},
};
};
};
render() {
const {
datasource,
input: { value },
} = this.props;
const datasourceUrl = _.get(datasource, "datasourceConfiguration.url", "");
const displayValue = `${datasourceUrl}${value}`;
const input = {
...this.props.input,
value: displayValue,
onChange: this.handleOnChange,
};
const props: EditorProps = {
...this.props,
input,
mode: EditorModes.TEXT_WITH_BINDING,
theme: EditorTheme.LIGHT,
tabBehaviour: TabBehaviour.INPUT,
size: EditorSize.COMPACT,
marking: [bindingMarker, this.handleDatasourceHighlight()],
hinting: [bindingHint, this.handleDatasourceHint()],
showLightningMenu: false,
dataTreePath: `${this.props.apiName}.config.path`,
};
if (datasource && !("id" in datasource) && !!displayValue) {
props.rightIcon = <StoreAsDatasource />;
}
return (
<React.Fragment>
<CodeEditor {...props} />
</React.Fragment>
);
}
}
const apiFormValueSelector = formValueSelector(API_EDITOR_FORM_NAME);
const mapStateToProps = (
state: AppState,
ownProps: { pluginId: string },
): ReduxStateProps => {
return {
apiName: apiFormValueSelector(state, "name"),
datasource: apiFormValueSelector(state, "datasource"),
datasourceList: state.entities.datasources.list.filter(
d => d.pluginId === ownProps.pluginId && d.isValid,
),
};
};
const mapDispatchToProps = (dispatch: Function): ReduxDispatchProps => ({
updateDatasource: datasource =>
dispatch(change(API_EDITOR_FORM_NAME, "datasource", datasource)),
});
const EmbeddedDatasourcePathConnectedComponent = connect(
mapStateToProps,
mapDispatchToProps,
)(EmbeddedDatasourcePathComponent);
const EmbeddedDatasourcePathField = (
props: BaseFieldProps & { pluginId: string },
) => {
return (
<Field component={EmbeddedDatasourcePathConnectedComponent} {...props} />
);
};
export default EmbeddedDatasourcePathField;

View File

@ -7,6 +7,7 @@ import DynamicTextField from "./DynamicTextField";
import FormRow from "components/editorComponents/FormRow";
import FormLabel from "components/editorComponents/FormLabel";
import FIELD_VALUES from "constants/FieldExpectedValue";
import HelperTooltip from "components/editorComponents/HelperTooltip";
const FormRowWithLabel = styled(FormRow)`
flex-wrap: wrap;
@ -31,95 +32,103 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => {
<React.Fragment>
{typeof props.fields.getAll() === "object" && (
<React.Fragment>
{props.fields.map((field: any, index: number) => (
<FormRowWithLabel key={index}>
{index === 0 && props.label !== "" && (
<FormLabel>{props.label}</FormLabel>
)}
<DynamicTextField
className={`t--${field}.key.${index}`}
name={`${field}.key`}
placeholder="Key"
singleLine
setMaxHeight
showLightningMenu={false}
dataTreePath={`${props.dataTreePath}[${index}].key`}
expected={FIELD_VALUES.API_ACTION.params}
/>
{!props.actionConfig && (
<DynamicTextField
className={`t--${field}.value.${index}`}
name={`${field}.value`}
placeholder="Value"
singleLine
setMaxHeight
dataTreePath={`${props.dataTreePath}[${index}].value`}
expected={FIELD_VALUES.API_ACTION.params}
{props.fields.map((field: any, index: number) => {
const otherProps: Record<string, any> = {};
if (
props.actionConfig &&
props.actionConfig[index].description &&
props.rightIcon
) {
otherProps.rightIcon = (
<HelperTooltip
description={props.actionConfig[index].description}
rightIcon={props.rightIcon}
/>
)}
);
}
{props.actionConfig && props.actionConfig[index] && (
<React.Fragment>
return (
<FormRowWithLabel key={index}>
{index === 0 && props.label !== "" && (
<FormLabel>{props.label}</FormLabel>
)}
<DynamicTextField
className={`t--${field}.key.${index}`}
name={`${field}.key`}
placeholder="Key"
showLightningMenu={false}
dataTreePath={`${props.dataTreePath}[${index}].key`}
/>
{!props.actionConfig && (
<DynamicTextField
className={`t--${field}.value.${index}`}
name={`${field}.value`}
placeholder="Value"
dataTreePath={`${props.dataTreePath}[${index}].value`}
setMaxHeight
expected={FIELD_VALUES.API_ACTION.params}
placeholder={
props.placeholder
? props.placeholder
: props.actionConfig[index].mandatory &&
props.actionConfig[index].type
? `${props.actionConfig[index].type}`
: props.actionConfig[index].type
? `${props.actionConfig[index].type} (Optional)`
: `(Optional)`
}
singleLine
rightIcon={
props.actionConfig[index].description && props.rightIcon
}
description={props.actionConfig[index].description}
disabled={
props.actionConfig[index].editable ||
props.actionConfig[index].editable === undefined
? false
: true
}
showLightningMenu={
props.actionConfig[index].editable ||
props.actionConfig[index].editable === undefined
? true
: false
}
/>
</React.Fragment>
)}
{props.addOrDeleteFields !== false && (
<React.Fragment>
{index === props.fields.length - 1 ? (
<Icon
icon="plus"
className="t--addApiHeader"
iconSize={20}
onClick={() => props.fields.push({ key: "", value: "" })}
color={"#A3B3BF"}
style={{ alignSelf: "center" }}
)}
{props.actionConfig && props.actionConfig[index] && (
<React.Fragment>
<DynamicTextField
className={`t--${field}.value.${index}`}
name={`${field}.value`}
dataTreePath={`${props.dataTreePath}[${index}].value`}
expected={FIELD_VALUES.API_ACTION.params}
placeholder={
props.placeholder
? props.placeholder
: props.actionConfig[index].mandatory &&
props.actionConfig[index].type
? `${props.actionConfig[index].type}`
: props.actionConfig[index].type
? `${props.actionConfig[index].type} (Optional)`
: `(Optional)`
}
disabled={
!(
props.actionConfig[index].editable ||
props.actionConfig[index].editable === undefined
)
}
showLightningMenu={
!!(
props.actionConfig[index].editable ||
props.actionConfig[index].editable === undefined
)
}
{...otherProps}
/>
) : (
<FormIcons.DELETE_ICON
height={20}
width={20}
color={"#A3B3BF"}
onClick={() => props.fields.remove(index)}
style={{ alignSelf: "center" }}
/>
)}
</React.Fragment>
)}
</FormRowWithLabel>
))}
</React.Fragment>
)}
{props.addOrDeleteFields !== false && (
<React.Fragment>
{index === props.fields.length - 1 ? (
<Icon
icon="plus"
className="t--addApiHeader"
iconSize={20}
onClick={() =>
props.fields.push({ key: "", value: "" })
}
color={"#A3B3BF"}
style={{ alignSelf: "center" }}
/>
) : (
<FormIcons.DELETE_ICON
height={20}
width={20}
color={"#A3B3BF"}
onClick={() => props.fields.remove(index)}
style={{ alignSelf: "center" }}
/>
)}
</React.Fragment>
)}
</FormRowWithLabel>
);
})}
</React.Fragment>
)}
</React.Fragment>

View File

@ -9,6 +9,7 @@ import { ControlType } from "constants/PropertyControlConstants";
import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField";
import FormLabel from "components/editorComponents/FormLabel";
import { InputType } from "widgets/InputWidget";
import HelperTooltip from "components/editorComponents/HelperTooltip";
const FormRowWithLabel = styled.div`
display: flex;
@ -61,66 +62,79 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => {
<React.Fragment>
{typeof props.fields.getAll() === "object" && (
<React.Fragment>
{props.fields.map((field: any, index: number) => (
<FormRowWithLabel key={index} style={{ marginTop: 16 }}>
<div style={{ width: "50vh" }}>
<FormLabel>
{extraData && extraData[0].label} {isRequired && "*"}
</FormLabel>
<TextField name={`${field}.${keyName[1]}`} />
</div>
{!props.actionConfig && (
<div style={{ marginLeft: 16 }}>
<FormLabel>
{extraData && extraData[1].label} {isRequired && "*"}
</FormLabel>
<div style={{ display: "flex", flexDirection: "row" }}>
<div style={{ marginRight: 14, width: 72 }}>
<StyledTextField
name={`${field}.${valueName[1]}`}
type={valueDataType}
/>
</div>
{index === props.fields.length - 1 ? (
<Icon
icon="plus"
iconSize={20}
onClick={() =>
props.fields.push({ key: "", value: "" })
}
color={"#A3B3BF"}
style={{ alignSelf: "center" }}
/>
) : (
<FormIcons.DELETE_ICON
height={20}
width={20}
onClick={() => props.fields.remove(index)}
style={{ alignSelf: "center" }}
/>
)}
</div>
</div>
)}
{props.actionConfig && (
<DynamicTextField
name={`${field}.value`}
placeholder={
props.actionConfig[index].mandatory &&
props.actionConfig[index].type
? `Value (Type: ${props.actionConfig[index].type})`
: `Value (optional)`
}
singleLine
{props.fields.map((field: any, index: number) => {
const otherProps: Record<string, any> = {};
if (
props.actionConfig &&
props.actionConfig[index].description &&
props.rightIcon
) {
otherProps.rightIcon = (
<HelperTooltip
description={props.actionConfig[index].description}
rightIcon={
props.actionConfig[index].description && props.rightIcon
}
description={props.actionConfig[index].description}
/>
)}
</FormRowWithLabel>
))}
);
}
return (
<FormRowWithLabel key={index} style={{ marginTop: 16 }}>
<div style={{ width: "50vh" }}>
<FormLabel>
{extraData && extraData[0].label} {isRequired && "*"}
</FormLabel>
<TextField name={`${field}.${keyName[1]}`} />
</div>
{!props.actionConfig && (
<div style={{ marginLeft: 16 }}>
<FormLabel>
{extraData && extraData[1].label} {isRequired && "*"}
</FormLabel>
<div style={{ display: "flex", flexDirection: "row" }}>
<div style={{ marginRight: 14, width: 72 }}>
<StyledTextField
name={`${field}.${valueName[1]}`}
type={valueDataType}
/>
</div>
{index === props.fields.length - 1 ? (
<Icon
icon="plus"
iconSize={20}
onClick={() =>
props.fields.push({ key: "", value: "" })
}
color={"#A3B3BF"}
style={{ alignSelf: "center" }}
/>
) : (
<FormIcons.DELETE_ICON
height={20}
width={20}
onClick={() => props.fields.remove(index)}
style={{ alignSelf: "center" }}
/>
)}
</div>
</div>
)}
{props.actionConfig && (
<DynamicTextField
name={`${field}.value`}
placeholder={
props.actionConfig[index].mandatory &&
props.actionConfig[index].type
? `Value (Type: ${props.actionConfig[index].type})`
: `Value (optional)`
}
{...otherProps}
/>
)}
</FormRowWithLabel>
);
})}
</React.Fragment>
)}
</React.Fragment>

View File

@ -5,7 +5,13 @@ import { ControlWrapper, StyledPropertyPaneButton } from "./StyledControls";
import styled from "constants/DefaultTheme";
import { FormIcons } from "icons/FormIcons";
import { AnyStyledComponent } from "styled-components";
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
import CodeEditor from "components/editorComponents/CodeEditor";
import {
EditorModes,
EditorSize,
EditorTheme,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
const StyledOptionControlWrapper = styled(ControlWrapper)`
display: flex;
@ -67,7 +73,7 @@ function DataControlComponent(props: RenderComponentProps) {
return (
<StyledOptionControlWrapper orientation={"VERTICAL"}>
<StyledOptionControlWrapper orientation={"HORIZONTAL"}>
<DynamicAutocompleteInput
<CodeEditor
expected={"string"}
input={{
value: item.seriesName,
@ -82,8 +88,10 @@ function DataControlComponent(props: RenderComponentProps) {
},
}}
evaluatedValue={evaluated?.seriesName}
theme={"DARK"}
singleLine={false}
theme={EditorTheme.DARK}
size={EditorSize.EXTENDED}
mode={EditorModes.TEXT_WITH_BINDING}
tabBehaviour={TabBehaviour.INPUT}
placeholder="Series Name"
/>
{length > 1 && (
@ -99,7 +107,7 @@ function DataControlComponent(props: RenderComponentProps) {
<StyledDynamicInput
className={"t--property-control-chart-series-data-control"}
>
<DynamicAutocompleteInput
<CodeEditor
expected={`Array<x:string, y:number>`}
input={{
value: item.data,
@ -118,8 +126,10 @@ function DataControlComponent(props: RenderComponentProps) {
error: isValid ? "" : "There is an error",
touched: true,
}}
theme={"DARK"}
singleLine={false}
theme={EditorTheme.DARK}
size={EditorSize.EXTENDED}
mode={EditorModes.JSON_WITH_BINDING}
tabBehaviour={TabBehaviour.INPUT}
placeholder=""
/>
</StyledDynamicInput>

View File

@ -1,7 +1,14 @@
import React, { ChangeEvent } from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
import CodeEditor from "components/editorComponents/CodeEditor";
import { EventOrValueHandler } from "redux-form";
import {
EditorModes,
EditorSize,
EditorTheme,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
class CodeEditorControl extends BaseControl<ControlProps> {
render() {
const {
@ -14,8 +21,8 @@ class CodeEditorControl extends BaseControl<ControlProps> {
} = this.props;
return (
<DynamicAutocompleteInput
theme={"DARK"}
<CodeEditor
theme={EditorTheme.DARK}
input={{ value: propertyValue, onChange: this.onChange }}
dataTreePath={dataTreePath}
expected={expected}
@ -24,7 +31,9 @@ class CodeEditorControl extends BaseControl<ControlProps> {
error: isValid ? "" : validationMessage,
touched: true,
}}
singleLine={false}
size={EditorSize.EXTENDED}
mode={EditorModes.TEXT_WITH_BINDING}
tabBehaviour={TabBehaviour.INDENT}
/>
);
}

View File

@ -2,7 +2,13 @@ import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { StyledDynamicInput } from "./StyledControls";
import { InputType } from "widgets/InputWidget";
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
import CodeEditor from "components/editorComponents/CodeEditor";
import {
EditorModes,
EditorSize,
EditorTheme,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
export function InputText(props: {
label: string;
@ -27,7 +33,7 @@ export function InputText(props: {
} = props;
return (
<StyledDynamicInput>
<DynamicAutocompleteInput
<CodeEditor
input={{
value: value,
onChange: onChange,
@ -39,8 +45,10 @@ export function InputText(props: {
error: isValid ? "" : errorMessage,
touched: true,
}}
theme={"DARK"}
singleLine={false}
theme={EditorTheme.DARK}
mode={EditorModes.TEXT_WITH_BINDING}
tabBehaviour={TabBehaviour.INDENT}
size={EditorSize.EXTENDED}
placeholder={placeholder}
/>
</StyledDynamicInput>

View File

@ -0,0 +1,13 @@
import { Datasource } from "api/DatasourcesApi";
export type EmbeddedDatasource = Omit<Datasource, "id">;
export const DEFAULT_DATASOURCE = (pluginId: string): EmbeddedDatasource => ({
name: "DEFAULT_REST_DATASOURCE",
datasourceConfiguration: {
url: "",
},
invalids: [],
isValid: true,
pluginId,
});

View File

@ -17,7 +17,6 @@ import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
import { PaginationField } from "api/ActionAPI";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import DropdownField from "components/editorComponents/form/fields/DropdownField";
import DatasourcesField from "components/editorComponents/form/fields/DatasourcesField";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView";
import Pagination from "./Pagination";
@ -28,6 +27,7 @@ import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleH
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import PostBodyData from "./PostBodyData";
import ApiResponseView from "components/editorComponents/ApiResponseView";
import EmbeddedDatasourcePathField from "components/editorComponents/form/fields/EmbeddedDatasourcePathField";
import { AppState } from "reducers";
import { getApiName } from "selectors/formSelectors";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
@ -210,12 +210,9 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
options={HTTP_METHOD_OPTIONS}
/>
<DatasourceWrapper className="t--dataSourceField">
<DatasourcesField
key={apiId}
name="datasource"
<EmbeddedDatasourcePathField
name="actionConfiguration.path"
pluginId={pluginId}
datasourceFieldText={props.datasourceFieldText}
appName={props.appName}
/>
</DatasourceWrapper>
</FormRow>

View File

@ -86,7 +86,6 @@ export default function Pagination(props: PaginationProps) {
<StyledDynamicTextField
className="t--apiFormPaginationPrev"
name="actionConfiguration.prev"
singleLine
/>
<TestButton
className="t--apiFormPaginationPrevTest"
@ -103,7 +102,6 @@ export default function Pagination(props: PaginationProps) {
<StyledDynamicTextField
className="t--apiFormPaginationNext"
name="actionConfiguration.next"
singleLine
/>
<TestButton
className="t--apiFormPaginationNextTest"

View File

@ -1,13 +1,13 @@
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { formValueSelector, change } from "redux-form";
import { change, formValueSelector } from "redux-form";
import Select from "react-select";
import {
POST_BODY_FORMAT_OPTIONS,
POST_BODY_FORMATS,
CONTENT_TYPE,
POST_BODY_FORMAT_OPTIONS,
POST_BODY_FORMAT_OPTIONS_NO_MULTI_PART,
POST_BODY_FORMATS,
} from "constants/ApiEditorConstants";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import FormLabel from "components/editorComponents/FormLabel";
@ -16,6 +16,11 @@ import DynamicTextField from "components/editorComponents/form/fields/DynamicTex
import { AppState } from "reducers";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import FIELD_VALUES from "constants/FieldExpectedValue";
import {
EditorModes,
EditorSize,
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
const DropDownContainer = styled.div`
width: 300px;
@ -115,8 +120,9 @@ const PostBodyData = (props: Props) => {
name="actionConfiguration.body"
expected={FIELD_VALUES.API_ACTION.body}
showLineNumbers
allowTabIndent
singleLine={false}
tabBehaviour={TabBehaviour.INDENT}
size={EditorSize.EXTENDED}
mode={EditorModes.JSON_WITH_BINDING}
placeholder={
'{\n "name":"{{ inputName.property }}",\n "preference":"{{ dropdownName.property }}"\n}\n\n\\\\Take widget inputs using {{ }}'
}
@ -149,8 +155,8 @@ const PostBodyData = (props: Props) => {
<DynamicTextField
name="actionConfiguration.body"
height={300}
allowTabIndent
singleLine={false}
tabBehaviour={TabBehaviour.INDENT}
size={EditorSize.EXTENDED}
dataTreePath={`${dataTreePath}.body`}
/>
</JSONEditorFieldWrapper>

View File

@ -143,6 +143,7 @@ const RapidApiEditorForm: React.FC<Props> = (props: Props) => {
const postbodyResponsePresent =
templateId &&
actionConfiguration &&
actionConfigurationBodyFormData &&
actionConfigurationBodyFormData.length > 0;
// let credentialStepsData;
@ -211,7 +212,6 @@ const RapidApiEditorForm: React.FC<Props> = (props: Props) => {
<DynamicTextField
placeholder="Provider name"
name="provider.name"
singleLine
leftImage={providerImage}
disabled={true}
showLightningMenu={false}
@ -220,7 +220,6 @@ const RapidApiEditorForm: React.FC<Props> = (props: Props) => {
placeholder="v1/method"
name="actionConfiguration.path"
leftIcon={FormIcons.SLASH_ICON}
singleLine
disabled={true}
showLightningMenu={false}
/>

View File

@ -1,23 +1,21 @@
import React, { useState, useRef, useEffect } from "react";
import React, { useEffect, useRef, useState } from "react";
import {
reduxForm,
InjectedFormProps,
Field,
FormSubmitHandler,
formValueSelector,
InjectedFormProps,
reduxForm,
} from "redux-form";
import CheckboxField from "components/editorComponents/form/fields/CheckboxField";
import styled, { createGlobalStyle } from "styled-components";
import { Popover, Icon } from "@blueprintjs/core";
import { Icon, Popover } from "@blueprintjs/core";
import {
components,
MenuListComponentProps,
SingleValueProps,
OptionTypeBase,
OptionProps,
OptionTypeBase,
SingleValueProps,
} from "react-select";
import history from "utils/history";
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
import { DATA_SOURCES_EDITOR_URL } from "constants/routes";
import TemplateMenu from "./TemplateMenu";
import Button from "components/editorComponents/Button";
@ -34,6 +32,8 @@ import { RestAction } from "entities/Action";
import { connect } from "react-redux";
import { AppState } from "reducers";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField";
import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig";
const QueryFormContainer = styled.div`
font-size: 20px;
@ -420,21 +420,18 @@ const QueryEditorForm: React.FC<Props> = (props: Props) => {
selectedPluginPackage={selectedPluginPackage}
/>
) : isSQL ? (
<Field
<DynamicTextField
name="actionConfiguration.body"
dataTreePath={`${props.actionName}.config.body`}
component={DynamicAutocompleteInput}
className="textAreaStyles"
mode="sql-js"
baseMode="text/x-sql"
mode={EditorModes.SQL_WITH_BINDING}
/>
) : (
<Field
<DynamicTextField
name="actionConfiguration.body"
dataTreePath={`${props.actionName}.config.body`}
component={DynamicAutocompleteInput}
className="textAreaStyles"
mode="js-js"
mode={EditorModes.JSON_WITH_BINDING}
/>
)}
<StyledCheckbox

View File

@ -190,7 +190,7 @@ function* testDatasourceSaga(actionPayload: ReduxAction<Datasource>) {
if (isValidResponse) {
const responseData = response.data;
if (responseData.invalids.length) {
if (responseData.invalids && responseData.invalids.length) {
AppToaster.show({
message: responseData.invalids[0],
type: ToastType.ERROR,

View File

@ -80,6 +80,7 @@ class TernServer {
docs: true,
urls: true,
origins: true,
caseInsensitive: true,
},
(error, data) => {
if (error) return this.showError(cm, error);
@ -197,6 +198,7 @@ class TernServer {
docs?: boolean;
urls?: boolean;
origins?: boolean;
caseInsensitive?: boolean;
preferFunction?: boolean;
end?: CodeMirror.Position;
},

View File

@ -0,0 +1,11 @@
export const MockCodemirrorEditor = {
setOption: jest.fn(),
options: {
extraKeys: {},
},
getValue: jest.fn(),
getCursor: jest.fn(),
showHint: jest.fn(),
getLine: jest.fn(),
closeHint: jest.fn(),
};

View File

@ -0,0 +1 @@
module.exports = {};

View File

@ -0,0 +1,9 @@
module.exports = {
process() {
return "module.exports = {};";
},
getCacheKey() {
// The output is always the same.
return "svgTransform";
},
};

View File

@ -2560,9 +2560,10 @@
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.8.tgz#48e0a5648f54487a9128915fe090154fb1a2f348"
"@types/codemirror@^0.0.82":
version "0.0.82"
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.82.tgz#6d0c50036a023980318a6ed9f85ff5cc6b24172a"
"@types/codemirror@^0.0.96":
version "0.0.96"
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.96.tgz#73b52e784a246cebef31d544fef45ea764de5bad"
integrity sha512-GTswEV26Bl1byRxpD3sKd1rT2AISr0rK9ImlJgEzfvqhcVWeu4xQKFQI6UgSC95NT5swNG4st/oRMeGVZgPj9w==
dependencies:
"@types/tern" "*"
@ -4878,9 +4879,10 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
codemirror@^5.50.0:
version "5.51.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.51.0.tgz#7746caaf5223e68f5c55ea11e2f3cc82a9a3929e"
codemirror@^5.55.0:
version "5.55.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.55.0.tgz#23731f641288f202a6858fdc878f3149e0e04363"
integrity sha512-TumikSANlwiGkdF/Blnu/rqovZ0Y3Jh8yy9TqrPbSM0xxSucq3RgnpVDQ+mD9q6JERJEIT2FMuF/fBGfkhIR/g==
collapse-white-space@^1.0.0, collapse-white-space@^1.0.2:
version "1.0.6"