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:
parent
224d0ee49c
commit
38aafb5027
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ export interface Datasource {
|
|||
headers?: Record<string, string>;
|
||||
databaseName?: string;
|
||||
};
|
||||
invalids: string[];
|
||||
isValid: boolean;
|
||||
invalids?: string[];
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateDatasourceConfig {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
371
app/client/src/components/editorComponents/CodeEditor/index.tsx
Normal file
371
app/client/src/components/editorComponents/CodeEditor/index.tsx
Normal 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);
|
||||
|
|
@ -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",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -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",
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -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
|
||||
: ""};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
13
app/client/src/entities/Datasource/index.ts
Normal file
13
app/client/src/entities/Datasource/index.ts
Normal 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,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
11
app/client/test/__mocks__/CodeMirrorEditorMock.ts
Normal file
11
app/client/test/__mocks__/CodeMirrorEditorMock.ts
Normal 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(),
|
||||
};
|
||||
1
app/client/test/__mocks__/styleMock.js
Normal file
1
app/client/test/__mocks__/styleMock.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = {};
|
||||
9
app/client/test/__mocks__/svgMock.js
Normal file
9
app/client/test/__mocks__/svgMock.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
process() {
|
||||
return "module.exports = {};";
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return "svgTransform";
|
||||
},
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user