Data Tree Autocomplete
This commit is contained in:
parent
85fa63d654
commit
f45d2b9135
|
|
@ -71,6 +71,7 @@
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "^3.1.1",
|
"react-scripts": "^3.1.1",
|
||||||
"react-select": "^3.0.8",
|
"react-select": "^3.0.8",
|
||||||
|
"react-simple-tree-menu": "^1.1.9",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
"redux-form": "^8.2.6",
|
"redux-form": "^8.2.6",
|
||||||
"redux-saga": "^1.0.0",
|
"redux-saga": "^1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
|
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
|
||||||
import { IconName, InputGroup, MaybeElement } from "@blueprintjs/core";
|
import {
|
||||||
|
IconName,
|
||||||
|
IInputGroupProps,
|
||||||
|
IIntentProps,
|
||||||
|
InputGroup,
|
||||||
|
MaybeElement,
|
||||||
|
} from "@blueprintjs/core";
|
||||||
import { ComponentProps } from "./BaseComponent";
|
import { ComponentProps } from "./BaseComponent";
|
||||||
|
|
||||||
export const TextInput = styled(InputGroup)`
|
export const TextInput = styled(InputGroup)`
|
||||||
|
|
@ -45,6 +51,7 @@ const InputContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ErrorText = styled.span`
|
const ErrorText = styled.span`
|
||||||
|
|
@ -54,9 +61,10 @@ const ErrorText = styled.span`
|
||||||
color: ${props => props.theme.colors.error};
|
color: ${props => props.theme.colors.error};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export interface TextInputProps {
|
export interface TextInputProps extends IInputGroupProps {
|
||||||
/** TextInput Placeholder */
|
/** TextInput Placeholder */
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
intent?: IIntentProps["intent"];
|
||||||
input?: Partial<WrappedFieldInputProps>;
|
input?: Partial<WrappedFieldInputProps>;
|
||||||
meta?: Partial<WrappedFieldMetaProps>;
|
meta?: Partial<WrappedFieldMetaProps>;
|
||||||
icon?: IconName | MaybeElement;
|
icon?: IconName | MaybeElement;
|
||||||
|
|
@ -64,17 +72,18 @@ export interface TextInputProps {
|
||||||
showError?: boolean;
|
showError?: boolean;
|
||||||
/** Additional classname */
|
/** Additional classname */
|
||||||
className?: string;
|
className?: string;
|
||||||
|
refHandler?: (ref: HTMLInputElement | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseTextInput = (props: TextInputProps) => {
|
export const BaseTextInput = (props: TextInputProps) => {
|
||||||
const { placeholder, input, meta, icon, showError, className } = props;
|
const { input, meta, showError, className, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<InputContainer className={className}>
|
<InputContainer className={className}>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
inputRef={props.refHandler}
|
||||||
{...input}
|
{...input}
|
||||||
placeholder={placeholder}
|
|
||||||
leftIcon={icon}
|
|
||||||
autoComplete={"off"}
|
autoComplete={"off"}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{showError && (
|
{showError && (
|
||||||
<ErrorText>
|
<ErrorText>
|
||||||
|
|
|
||||||
62
app/client/src/components/editorComponents/DataTreeNode.tsx
Normal file
62
app/client/src/components/editorComponents/DataTreeNode.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React from "react";
|
||||||
|
import { TreeMenuItem } from "react-simple-tree-menu";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { Icon } from "@blueprintjs/core";
|
||||||
|
|
||||||
|
const NodeWrapper = styled.li<{ level: number; isActive?: boolean }>`
|
||||||
|
& {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 32px;
|
||||||
|
padding-left: ${props => props.level * 2}em;
|
||||||
|
background-color: ${props => (props.isActive ? "#e9faf3" : "white")};
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #e9faf3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CaretIcon = styled(Icon)`
|
||||||
|
color: #a3b3bf;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Label = styled.div`
|
||||||
|
color: ${props => props.theme.colors.textDefault}
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex: none;
|
||||||
|
max-width: 70%;
|
||||||
|
padding-right: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Type = styled.span`
|
||||||
|
color: #737e8a;
|
||||||
|
flex: 2;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: TreeMenuItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DataTreeNode = ({ item }: Props) => (
|
||||||
|
<NodeWrapper
|
||||||
|
onClick={item.hasNodes && item.toggleNode ? item.toggleNode : item.onClick}
|
||||||
|
level={item.level}
|
||||||
|
isActive={item.focused}
|
||||||
|
>
|
||||||
|
<CaretIcon
|
||||||
|
icon={
|
||||||
|
item.hasNodes ? (item.isOpen ? "caret-down" : "caret-right") : "dot"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label>{item.labelRender}</Label>
|
||||||
|
<Type>//{item.type}</Type>
|
||||||
|
</NodeWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DataTreeNode;
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
import React, { ChangeEvent, Component, KeyboardEvent } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { AppState } from "reducers";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
getDynamicAutocompleteSearchTerm,
|
||||||
|
getDynamicBindings,
|
||||||
|
} from "utils/DynamicBindingUtils";
|
||||||
|
import {
|
||||||
|
BaseTextInput,
|
||||||
|
TextInputProps,
|
||||||
|
} from "components/designSystems/appsmith/TextInputComponent";
|
||||||
|
import {
|
||||||
|
getNameBindingsWithData,
|
||||||
|
NameBindingsWithData,
|
||||||
|
} from "selectors/nameBindingsWithDataSelector";
|
||||||
|
import TreeMenu, {
|
||||||
|
MatchSearchFunction,
|
||||||
|
TreeMenuItem,
|
||||||
|
TreeNodeInArray,
|
||||||
|
} from "react-simple-tree-menu";
|
||||||
|
import { DATA_BIND_AUTOCOMPLETE } from "constants/BindingsConstants";
|
||||||
|
import DataTreeNode from "components/editorComponents/DataTreeNode";
|
||||||
|
import { transformToTreeStructure } from "utils/DynamicTreeAutoCompleteUtils";
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DataTreeWrapper = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 33px;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 10px;
|
||||||
|
max-height: 400px;
|
||||||
|
width: 450px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ebeff2;
|
||||||
|
box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NoResultsMessage = styled.p`
|
||||||
|
color: ${props => props.theme.colors.textDefault};
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ReduxStateProps {
|
||||||
|
dynamicData: NameBindingsWithData;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = ReduxStateProps & TextInputProps;
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
tree: TreeNodeInArray[];
|
||||||
|
showTree: boolean;
|
||||||
|
focusedNode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DynamicAutocompleteInput extends Component<Props, State> {
|
||||||
|
private input: HTMLInputElement | null = null;
|
||||||
|
private search: Function | undefined;
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tree: [],
|
||||||
|
showTree: true,
|
||||||
|
focusedNode: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
componentDidMount(): void {
|
||||||
|
this.updateTree();
|
||||||
|
}
|
||||||
|
componentDidUpdate(prevProps: Readonly<Props>): void {
|
||||||
|
if (prevProps.dynamicData !== this.props.dynamicData) {
|
||||||
|
this.updateTree();
|
||||||
|
}
|
||||||
|
this.updateTreeVisibility();
|
||||||
|
}
|
||||||
|
updateTreeVisibility = () => {
|
||||||
|
const { showTree } = this.state;
|
||||||
|
const { input } = this.props;
|
||||||
|
let value;
|
||||||
|
let hasIncomplete = 0;
|
||||||
|
if (input && input.value) {
|
||||||
|
value = input.value;
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
const { bindings, paths } = getDynamicBindings(value);
|
||||||
|
bindings.forEach((binding, i) => {
|
||||||
|
if (binding.indexOf("{{") > -1 && paths[i] === "") {
|
||||||
|
hasIncomplete++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTree) {
|
||||||
|
if (hasIncomplete === 0) {
|
||||||
|
this.setState({ showTree: false });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasIncomplete > 0) {
|
||||||
|
this.setState({ showTree: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleNodeSearch: MatchSearchFunction = ({ path, searchTerm }) => {
|
||||||
|
const lowerCasePath = path.toLowerCase();
|
||||||
|
const lowerCaseSearchTerm = searchTerm.toLowerCase();
|
||||||
|
const matchPath = lowerCasePath.substr(0, searchTerm.length);
|
||||||
|
return matchPath === lowerCaseSearchTerm;
|
||||||
|
};
|
||||||
|
updateTree = () => {
|
||||||
|
const { dynamicData } = this.props;
|
||||||
|
const filters = Object.keys(dynamicData).map(name => ({ name }));
|
||||||
|
const tree = transformToTreeStructure(
|
||||||
|
dynamicData,
|
||||||
|
filters.map(f => f.name),
|
||||||
|
);
|
||||||
|
this.setState({ tree });
|
||||||
|
};
|
||||||
|
handleNodeSelected = (node: any) => {
|
||||||
|
if (this.props.input && this.props.input.value) {
|
||||||
|
const currentValue = String(this.props.input.value);
|
||||||
|
const path = node.path;
|
||||||
|
const { bindings, paths } = getDynamicBindings(currentValue);
|
||||||
|
const autoComplete = bindings.map((binding, i) => {
|
||||||
|
if (binding.indexOf("{{") > -1 && paths[i] === "") {
|
||||||
|
return binding.replace(DATA_BIND_AUTOCOMPLETE, `{{${path}}}`);
|
||||||
|
}
|
||||||
|
return binding;
|
||||||
|
});
|
||||||
|
this.props.input.onChange &&
|
||||||
|
this.props.input.onChange(autoComplete.join(""));
|
||||||
|
this.input && this.input.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setInputRef = (ref: HTMLInputElement | null) => {
|
||||||
|
if (ref) {
|
||||||
|
this.input = ref;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleInputChange = (e: ChangeEvent<{ value: string }>) => {
|
||||||
|
if (this.props.input && this.props.input.onChange) {
|
||||||
|
this.props.input.onChange(e);
|
||||||
|
}
|
||||||
|
const value = e.target.value;
|
||||||
|
if (this.search) {
|
||||||
|
const { bindings, paths } = getDynamicBindings(value);
|
||||||
|
bindings.forEach((binding, i) => {
|
||||||
|
if (binding.indexOf("{{") > -1 && paths[i] === "") {
|
||||||
|
const query = getDynamicAutocompleteSearchTerm(binding);
|
||||||
|
this.search && this.search(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
if (
|
||||||
|
document.activeElement &&
|
||||||
|
document.activeElement.tagName === "INPUT"
|
||||||
|
) {
|
||||||
|
const tree = document.getElementById("tree");
|
||||||
|
const container =
|
||||||
|
tree && tree.closest<HTMLDivElement>("[tabindex='0']");
|
||||||
|
if (container) {
|
||||||
|
container.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { input, ...rest } = this.props;
|
||||||
|
return (
|
||||||
|
<Wrapper onKeyDown={this.handleKeyDown}>
|
||||||
|
<BaseTextInput
|
||||||
|
refHandler={this.setInputRef}
|
||||||
|
input={{
|
||||||
|
...input,
|
||||||
|
onChange: this.handleInputChange,
|
||||||
|
}}
|
||||||
|
{..._.omit(rest, ["dynamicData", "dispatch"])}
|
||||||
|
/>
|
||||||
|
{this.state.showTree && this.state.tree.length && (
|
||||||
|
<TreeMenu
|
||||||
|
data={this.state.tree}
|
||||||
|
matchSearch={this.handleNodeSearch}
|
||||||
|
onClickItem={this.handleNodeSelected}
|
||||||
|
initialFocusKey={this.state.tree[0].key}
|
||||||
|
disableKeyboard={false}
|
||||||
|
>
|
||||||
|
{({ search, items }) => (
|
||||||
|
<DataTreeWrapper id="tree">
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<NoResultsMessage>No results found</NoResultsMessage>
|
||||||
|
) : (
|
||||||
|
items.map((item: TreeMenuItem) => {
|
||||||
|
this.search = search;
|
||||||
|
return <DataTreeNode key={item.key} item={item} />;
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</DataTreeWrapper>
|
||||||
|
)}
|
||||||
|
</TreeMenu>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState): ReduxStateProps => ({
|
||||||
|
dynamicData: getNameBindingsWithData(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(DynamicAutocompleteInput);
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Field, BaseFieldProps } from "redux-form";
|
||||||
|
import { TextInputProps } from "components/designSystems/appsmith/TextInputComponent";
|
||||||
|
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
|
||||||
|
|
||||||
|
class DynamicTextField extends React.Component<
|
||||||
|
BaseFieldProps & TextInputProps
|
||||||
|
> {
|
||||||
|
render() {
|
||||||
|
return <Field component={DynamicAutocompleteInput} {...this.props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicTextField;
|
||||||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
||||||
import { FieldArray, WrappedFieldArrayProps } from "redux-form";
|
import { FieldArray, WrappedFieldArrayProps } from "redux-form";
|
||||||
import { Icon } from "@blueprintjs/core";
|
import { Icon } from "@blueprintjs/core";
|
||||||
import { FormIcons } from "icons/FormIcons";
|
import { FormIcons } from "icons/FormIcons";
|
||||||
import TextField from "./TextField";
|
import DynamicTextField from "./DynamicTextField";
|
||||||
import FormRow from "components/editorComponents/FormRow";
|
import FormRow from "components/editorComponents/FormRow";
|
||||||
import FormLabel from "components/editorComponents/FormLabel";
|
import FormLabel from "components/editorComponents/FormLabel";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
@ -23,8 +23,8 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => {
|
||||||
{props.fields.map((field: any, index: number) => (
|
{props.fields.map((field: any, index: number) => (
|
||||||
<FormRowWithLabel key={index}>
|
<FormRowWithLabel key={index}>
|
||||||
{index === 0 && <FormLabel>{props.label}</FormLabel>}
|
{index === 0 && <FormLabel>{props.label}</FormLabel>}
|
||||||
<TextField name={`${field}.key`} placeholder="Key" />
|
<DynamicTextField name={`${field}.key`} placeholder="Key" />
|
||||||
<TextField name={`${field}.value`} placeholder="Value" />
|
<DynamicTextField name={`${field}.value`} placeholder="Value" />
|
||||||
{index === props.fields.length - 1 ? (
|
{index === props.fields.length - 1 ? (
|
||||||
<Icon
|
<Icon
|
||||||
icon="plus"
|
icon="plus"
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import BaseControl, { ControlProps } from "./BaseControl";
|
import BaseControl, { ControlProps } from "./BaseControl";
|
||||||
import { ControlWrapper, StyledInputGroup } from "./StyledControls";
|
import { ControlWrapper, StyledDynamicInput } from "./StyledControls";
|
||||||
import { InputType } from "widgets/InputWidget";
|
import { InputType } from "widgets/InputWidget";
|
||||||
import { ControlType } from "constants/PropertyControlConstants";
|
import { ControlType } from "constants/PropertyControlConstants";
|
||||||
import { Intent } from "@blueprintjs/core";
|
import { Intent } from "@blueprintjs/core";
|
||||||
|
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
|
||||||
|
|
||||||
class InputTextControl extends BaseControl<InputControlProps> {
|
class InputTextControl extends BaseControl<InputControlProps> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<ControlWrapper>
|
<ControlWrapper>
|
||||||
<label>{this.props.label}</label>
|
<label>{this.props.label}</label>
|
||||||
<StyledInputGroup
|
<StyledDynamicInput>
|
||||||
intent={this.props.isValid ? Intent.NONE : Intent.DANGER}
|
<DynamicAutocompleteInput
|
||||||
type={this.isNumberType() ? "number" : "text"}
|
intent={this.props.isValid ? Intent.NONE : Intent.DANGER}
|
||||||
onChange={this.onTextChange}
|
type={this.isNumberType() ? "number" : "text"}
|
||||||
placeholder={this.props.placeholderText}
|
input={{
|
||||||
defaultValue={this.props.propertyValue}
|
value: this.props.propertyValue,
|
||||||
/>
|
onChange: this.onTextChange,
|
||||||
|
}}
|
||||||
|
placeholder={this.props.placeholderText}
|
||||||
|
/>
|
||||||
|
</StyledDynamicInput>
|
||||||
</ControlWrapper>
|
</ControlWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -34,8 +39,11 @@ class InputTextControl extends BaseControl<InputControlProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
onTextChange = (event: React.ChangeEvent<HTMLInputElement> | string) => {
|
||||||
const value: string = event.target.value;
|
let value = event;
|
||||||
|
if (typeof event !== "string") {
|
||||||
|
value = event.target.value;
|
||||||
|
}
|
||||||
this.updateProperty(this.props.propertyName, value);
|
this.updateProperty(this.props.propertyName, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,21 @@ export const StyledSwitch = styled(Switch)`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const StyledDynamicInput = styled.div`
|
||||||
|
&&& {
|
||||||
|
input {
|
||||||
|
border: none;
|
||||||
|
color: ${props => props.theme.colors.textOnDarkBG};
|
||||||
|
background: ${props => props.theme.colors.paneInputBG};
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
color: ${props => props.theme.colors.textOnDarkBG};
|
||||||
|
background: ${props => props.theme.colors.paneInputBG};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const StyledInputGroup = styled(InputGroup)`
|
export const StyledInputGroup = styled(InputGroup)`
|
||||||
& > input {
|
& > input {
|
||||||
placeholder-text: ${props => props.placeholder};
|
placeholder-text: ${props => props.placeholder};
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@
|
||||||
export type NamePathBindingMap = Record<string, string>;
|
export type NamePathBindingMap = Record<string, string>;
|
||||||
export const DATA_BIND_REGEX = /(.*?){{(\s*(.*?)\s*)}}(.*?)/g;
|
export const DATA_BIND_REGEX = /(.*?){{(\s*(.*?)\s*)}}(.*?)/g;
|
||||||
export const DATA_PATH_REGEX = /[\w\.\[\]\d]+/;
|
export const DATA_PATH_REGEX = /[\w\.\[\]\d]+/;
|
||||||
|
export const DATA_BIND_AUTOCOMPLETE = /({{)(.*)(}{0,2}?)/;
|
||||||
/* eslint-enable no-useless-escape */
|
/* eslint-enable no-useless-escape */
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import FormRow from "components/editorComponents/FormRow";
|
||||||
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
|
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
|
||||||
import { RestAction } from "api/ActionAPI";
|
import { RestAction } from "api/ActionAPI";
|
||||||
import TextField from "components/editorComponents/form/fields/TextField";
|
import TextField from "components/editorComponents/form/fields/TextField";
|
||||||
|
import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField";
|
||||||
import DropdownField from "components/editorComponents/form/fields/DropdownField";
|
import DropdownField from "components/editorComponents/form/fields/DropdownField";
|
||||||
import DatasourcesField from "components/editorComponents/form/fields/DatasourcesField";
|
import DatasourcesField from "components/editorComponents/form/fields/DatasourcesField";
|
||||||
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
|
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
|
||||||
|
|
@ -131,7 +132,7 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
|
||||||
options={HTTP_METHOD_OPTIONS}
|
options={HTTP_METHOD_OPTIONS}
|
||||||
/>
|
/>
|
||||||
<DatasourcesField name="datasource.id" pluginId={pluginId} />
|
<DatasourcesField name="datasource.id" pluginId={pluginId} />
|
||||||
<TextField
|
<DynamicTextField
|
||||||
placeholder="API Path"
|
placeholder="API Path"
|
||||||
name="actionConfiguration.path"
|
name="actionConfiguration.path"
|
||||||
icon="slash"
|
icon="slash"
|
||||||
|
|
|
||||||
|
|
@ -153,11 +153,12 @@ type State = {
|
||||||
search: string;
|
search: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fuseOptions = {
|
const FUSE_OPTIONS = {
|
||||||
shouldSort: true,
|
shouldSort: true,
|
||||||
threshold: 0.1,
|
threshold: 0.5,
|
||||||
location: 0,
|
location: 0,
|
||||||
minMatchCharLength: 3,
|
minMatchCharLength: 3,
|
||||||
|
findAllMatches: true,
|
||||||
keys: ["name"],
|
keys: ["name"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -234,7 +235,7 @@ class ApiSidebar extends React.Component<Props, State> {
|
||||||
if (!pluginId) return null;
|
if (!pluginId) return null;
|
||||||
const { isCreating, search, name } = this.state;
|
const { isCreating, search, name } = this.state;
|
||||||
const activeActionId = match.params.apiId;
|
const activeActionId = match.params.apiId;
|
||||||
const fuse = new Fuse(data, fuseOptions);
|
const fuse = new Fuse(data, FUSE_OPTIONS);
|
||||||
const actions: RestAction[] = search ? fuse.search(search) : data;
|
const actions: RestAction[] = search ? fuse.search(search) : data;
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const PopperWrapper = styled(PaneWrapper)`
|
||||||
max-height: ${props => props.theme.propertyPane.height}px;
|
max-height: ${props => props.theme.propertyPane.height}px;
|
||||||
width: ${props => props.theme.propertyPane.width}px;
|
width: ${props => props.theme.propertyPane.width}px;
|
||||||
margin: ${props => props.theme.spaces[6]}px;
|
margin: ${props => props.theme.spaces[6]}px;
|
||||||
overflow-y: auto;
|
overflow-y: visible;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { connect } from "react-redux";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Canvas from "./Canvas";
|
import Canvas from "./Canvas";
|
||||||
import PropertyPane from "./PropertyPane";
|
|
||||||
import { AppState } from "reducers";
|
import { AppState } from "reducers";
|
||||||
import { WidgetProps } from "widgets/BaseWidget";
|
import { WidgetProps } from "widgets/BaseWidget";
|
||||||
import { savePage } from "actions/pageActions";
|
import { savePage } from "actions/pageActions";
|
||||||
|
|
@ -84,7 +83,6 @@ const WidgetsEditor = (props: EditorProps) => {
|
||||||
<EditorContextProvider>
|
<EditorContextProvider>
|
||||||
<EditorWrapper>
|
<EditorWrapper>
|
||||||
<CanvasContainer>{node}</CanvasContainer>
|
<CanvasContainer>{node}</CanvasContainer>
|
||||||
<PropertyPane />
|
|
||||||
</EditorWrapper>
|
</EditorWrapper>
|
||||||
</EditorContextProvider>
|
</EditorContextProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ import {
|
||||||
evaluateDynamicBoundValue,
|
evaluateDynamicBoundValue,
|
||||||
getDynamicBindings,
|
getDynamicBindings,
|
||||||
isDynamicValue,
|
isDynamicValue,
|
||||||
NameBindingsWithData,
|
|
||||||
} from "utils/DynamicBindingUtils";
|
} from "utils/DynamicBindingUtils";
|
||||||
import { validateResponse } from "./ErrorSagas";
|
import { validateResponse } from "./ErrorSagas";
|
||||||
import { getDataTree } from "selectors/entitiesSelector";
|
import { getDataTree } from "selectors/entitiesSelector";
|
||||||
|
|
@ -50,7 +49,10 @@ import {
|
||||||
import { getFormData } from "selectors/formSelectors";
|
import { getFormData } from "selectors/formSelectors";
|
||||||
import { API_EDITOR_FORM_NAME } from "constants/forms";
|
import { API_EDITOR_FORM_NAME } from "constants/forms";
|
||||||
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
|
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
|
||||||
import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
|
import {
|
||||||
|
getNameBindingsWithData,
|
||||||
|
NameBindingsWithData,
|
||||||
|
} from "selectors/nameBindingsWithDataSelector";
|
||||||
|
|
||||||
export const getAction = (
|
export const getAction = (
|
||||||
state: AppState,
|
state: AppState,
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@ import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigRe
|
||||||
import { WidgetCardProps } from "widgets/BaseWidget";
|
import { WidgetCardProps } from "widgets/BaseWidget";
|
||||||
import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer";
|
import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer";
|
||||||
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
|
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
|
||||||
import {
|
import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils";
|
||||||
enhanceWithDynamicValuesAndValidations,
|
|
||||||
NameBindingsWithData,
|
|
||||||
} from "utils/DynamicBindingUtils";
|
|
||||||
import { getDataTree } from "./entitiesSelector";
|
import { getDataTree } from "./entitiesSelector";
|
||||||
import {
|
import {
|
||||||
FlattenedWidgetProps,
|
FlattenedWidgetProps,
|
||||||
|
|
@ -20,7 +17,10 @@ import { PageListReduxState } from "reducers/entityReducers/pageListReducer";
|
||||||
|
|
||||||
import { OccupiedSpace } from "constants/editorConstants";
|
import { OccupiedSpace } from "constants/editorConstants";
|
||||||
import { WidgetTypes } from "constants/WidgetConstants";
|
import { WidgetTypes } from "constants/WidgetConstants";
|
||||||
import { getNameBindingsWithData } from "./nameBindingsWithDataSelector";
|
import {
|
||||||
|
getNameBindingsWithData,
|
||||||
|
NameBindingsWithData,
|
||||||
|
} from "./nameBindingsWithDataSelector";
|
||||||
|
|
||||||
const getEditorState = (state: AppState) => state.ui.editor;
|
const getEditorState = (state: AppState) => state.ui.editor;
|
||||||
const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig;
|
const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ import { AppState, DataTree } from "reducers";
|
||||||
|
|
||||||
export const getDataTree = (state: AppState): DataTree => state.entities;
|
export const getDataTree = (state: AppState): DataTree => state.entities;
|
||||||
|
|
||||||
|
export const getDynamicNames = (state: AppState): DataTree["nameBindings"] =>
|
||||||
|
state.entities.nameBindings;
|
||||||
|
|
||||||
export const getPluginIdOfName = (
|
export const getPluginIdOfName = (
|
||||||
state: AppState,
|
state: AppState,
|
||||||
name: string,
|
name: string,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { DataTree } from "reducers";
|
import { DataTree } from "reducers";
|
||||||
import { NameBindingsWithData } from "utils/DynamicBindingUtils";
|
|
||||||
import { JSONPath } from "jsonpath-plus";
|
import { JSONPath } from "jsonpath-plus";
|
||||||
import { createSelector } from "reselect";
|
import { createSelector } from "reselect";
|
||||||
import { getDataTree } from "./entitiesSelector";
|
import { getDataTree } from "./entitiesSelector";
|
||||||
|
|
||||||
|
export type NameBindingsWithData = Record<string, object>;
|
||||||
export const getNameBindingsWithData = createSelector(
|
export const getNameBindingsWithData = createSelector(
|
||||||
getDataTree,
|
getDataTree,
|
||||||
(dataTree: DataTree): NameBindingsWithData => {
|
(dataTree: DataTree): NameBindingsWithData => {
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer"
|
||||||
import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer";
|
import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer";
|
||||||
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||||
import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer";
|
import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer";
|
||||||
import {
|
import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils";
|
||||||
enhanceWithDynamicValuesAndValidations,
|
|
||||||
NameBindingsWithData,
|
|
||||||
} from "utils/DynamicBindingUtils";
|
|
||||||
import { WidgetProps } from "widgets/BaseWidget";
|
import { WidgetProps } from "widgets/BaseWidget";
|
||||||
import { getNameBindingsWithData } from "./nameBindingsWithDataSelector";
|
import {
|
||||||
|
getNameBindingsWithData,
|
||||||
|
NameBindingsWithData,
|
||||||
|
} from "./nameBindingsWithDataSelector";
|
||||||
|
|
||||||
const getPropertyPaneState = (state: AppState): PropertyPaneReduxState =>
|
const getPropertyPaneState = (state: AppState): PropertyPaneReduxState =>
|
||||||
state.ui.propertyPane;
|
state.ui.propertyPane;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,25 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { WidgetProps } from "widgets/BaseWidget";
|
import { WidgetProps } from "widgets/BaseWidget";
|
||||||
import { DATA_BIND_REGEX } from "constants/BindingsConstants";
|
import {
|
||||||
|
DATA_BIND_AUTOCOMPLETE,
|
||||||
|
DATA_BIND_REGEX,
|
||||||
|
} from "constants/BindingsConstants";
|
||||||
import ValidationFactory from "./ValidationFactory";
|
import ValidationFactory from "./ValidationFactory";
|
||||||
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
|
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
|
||||||
|
import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
|
||||||
|
|
||||||
|
export const isDynamicAutocompleteMatch = (value: string): boolean =>
|
||||||
|
DATA_BIND_AUTOCOMPLETE.test(value);
|
||||||
|
|
||||||
|
export const getDynamicAutocompleteSearchTerm = (value: string): string => {
|
||||||
|
const bindings = value.match(DATA_BIND_AUTOCOMPLETE) || [];
|
||||||
|
if (bindings.length > 0) {
|
||||||
|
return bindings[2];
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type NameBindingsWithData = Record<string, object>;
|
|
||||||
export const isDynamicValue = (value: string): boolean =>
|
export const isDynamicValue = (value: string): boolean =>
|
||||||
DATA_BIND_REGEX.test(value);
|
DATA_BIND_REGEX.test(value);
|
||||||
|
|
||||||
|
|
|
||||||
78
app/client/src/utils/DynamicTreeAutoCompleteUtils.tsx
Normal file
78
app/client/src/utils/DynamicTreeAutoCompleteUtils.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { TreeNodeInArray } from "react-simple-tree-menu";
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Key = styled.span`
|
||||||
|
color: #768896;
|
||||||
|
padding-right: 5px;
|
||||||
|
`;
|
||||||
|
const Value = styled.span<{ type: string }>`
|
||||||
|
color: ${props => {
|
||||||
|
switch (props.type) {
|
||||||
|
case "string":
|
||||||
|
return "#2E3D49";
|
||||||
|
case "number":
|
||||||
|
return props.theme.colors.error;
|
||||||
|
case "boolean":
|
||||||
|
return props.theme.colors.primary;
|
||||||
|
default:
|
||||||
|
return "#2E3D49";
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const transformToTreeStructure = (
|
||||||
|
dataTree: Record<string, any>,
|
||||||
|
names: string[],
|
||||||
|
parentPath?: string,
|
||||||
|
): TreeNodeInArray[] => {
|
||||||
|
return names.map(name => {
|
||||||
|
const currentPath = parentPath ? `${parentPath}.${name}` : name;
|
||||||
|
let nodes: TreeNodeInArray["nodes"] = [];
|
||||||
|
const child = dataTree[name];
|
||||||
|
let childType: string = typeof child;
|
||||||
|
let labelRender: React.ReactNode = name;
|
||||||
|
if (childType === "object") {
|
||||||
|
if (!Array.isArray(child)) {
|
||||||
|
nodes = transformToTreeStructure(
|
||||||
|
child,
|
||||||
|
Object.keys(child),
|
||||||
|
currentPath,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
nodes = child.map((c, i) => ({
|
||||||
|
key: `[${i}]`,
|
||||||
|
path: `${currentPath}[${i}]`,
|
||||||
|
label: "",
|
||||||
|
labelRender: i.toString(),
|
||||||
|
nodes: transformToTreeStructure(
|
||||||
|
c,
|
||||||
|
Object.keys(c),
|
||||||
|
`${currentPath}[${i}]`,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
childType = "Array";
|
||||||
|
labelRender = `${name} {${child.length}}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
labelRender = (
|
||||||
|
<div>
|
||||||
|
<Key>{name}: </Key>
|
||||||
|
<Value type={childType}>
|
||||||
|
{childType === "string"
|
||||||
|
? `"${dataTree[name]}"`
|
||||||
|
: String(dataTree[name])}
|
||||||
|
</Value>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: name,
|
||||||
|
path: currentPath,
|
||||||
|
label: "",
|
||||||
|
labelRender,
|
||||||
|
nodes,
|
||||||
|
type: childType,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -7944,6 +7944,11 @@ is-directory@^0.3.1:
|
||||||
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
|
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
|
||||||
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
|
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
|
||||||
|
|
||||||
|
is-empty@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-empty/-/is-empty-1.2.0.tgz#de9bb5b278738a05a0b09a57e1fb4d4a341a9f6b"
|
||||||
|
integrity sha1-3pu1snhzigWgsJpX4ftNSjQan2s=
|
||||||
|
|
||||||
is-extendable@^0.1.0, is-extendable@^0.1.1:
|
is-extendable@^0.1.0, is-extendable@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
|
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
|
||||||
|
|
@ -12318,6 +12323,16 @@ react-side-effect@^1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shallowequal "^1.0.1"
|
shallowequal "^1.0.1"
|
||||||
|
|
||||||
|
react-simple-tree-menu@^1.1.9:
|
||||||
|
version "1.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-simple-tree-menu/-/react-simple-tree-menu-1.1.9.tgz#0a3364e34cdbd40469b93a327e2b4371b8dbc76d"
|
||||||
|
integrity sha512-CFUHDiIYBOanuRDyUHZxIH/lBXecAo/uTQM8ZsSn31uDFn98tD6vPztTYZZIFe2wpW+50ydO680lm5oUfFJU1A==
|
||||||
|
dependencies:
|
||||||
|
classnames "^2.2.6"
|
||||||
|
fast-memoize "^2.5.1"
|
||||||
|
is-empty "^1.2.0"
|
||||||
|
tiny-debounce "^0.1.1"
|
||||||
|
|
||||||
react-sizeme@^2.6.7:
|
react-sizeme@^2.6.7:
|
||||||
version "2.6.10"
|
version "2.6.10"
|
||||||
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.10.tgz#9993dcb5e67fab94a8e5d078a0d3820609010f17"
|
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.10.tgz#9993dcb5e67fab94a8e5d078a0d3820609010f17"
|
||||||
|
|
@ -14338,6 +14353,11 @@ timsort@^0.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||||
|
|
||||||
|
tiny-debounce@^0.1.1:
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-debounce/-/tiny-debounce-0.1.1.tgz#e7759447ca1d48830d3b302ce94666a0ca537b1d"
|
||||||
|
integrity sha1-53WUR8odSIMNOzAs6UZmoMpTex0=
|
||||||
|
|
||||||
tiny-emitter@^2.0.0:
|
tiny-emitter@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user