Data Tree Autocomplete

This commit is contained in:
Hetu Nandu 2019-12-06 13:16:08 +00:00
parent 85fa63d654
commit f45d2b9135
21 changed files with 492 additions and 40 deletions

View File

@ -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",

View File

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

View 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;

View File

@ -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);

View File

@ -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;

View File

@ -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"

View File

@ -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);
}; };

View File

@ -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};

View File

@ -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 */

View File

@ -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"

View File

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

View File

@ -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 */

View File

@ -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>
); );

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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 => {

View File

@ -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;

View File

@ -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);

View 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,
};
});
};

View File

@ -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"