diff --git a/app/client/package.json b/app/client/package.json index 417bbae831..73b25b5b96 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -71,6 +71,7 @@ "react-router-dom": "^5.1.2", "react-scripts": "^3.1.1", "react-select": "^3.0.8", + "react-simple-tree-menu": "^1.1.9", "redux": "^4.0.1", "redux-form": "^8.2.6", "redux-saga": "^1.0.0", diff --git a/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx b/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx index 822717524c..3f04efd7e5 100644 --- a/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/TextInputComponent.tsx @@ -1,7 +1,13 @@ import React from "react"; import styled from "styled-components"; 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"; export const TextInput = styled(InputGroup)` @@ -45,6 +51,7 @@ const InputContainer = styled.div` display: flex; flex: 1; flex-direction: column; + position: relative; `; const ErrorText = styled.span` @@ -54,9 +61,10 @@ const ErrorText = styled.span` color: ${props => props.theme.colors.error}; `; -export interface TextInputProps { +export interface TextInputProps extends IInputGroupProps { /** TextInput Placeholder */ placeholder?: string; + intent?: IIntentProps["intent"]; input?: Partial; meta?: Partial; icon?: IconName | MaybeElement; @@ -64,17 +72,18 @@ export interface TextInputProps { showError?: boolean; /** Additional classname */ className?: string; + refHandler?: (ref: HTMLInputElement | null) => void; } export const BaseTextInput = (props: TextInputProps) => { - const { placeholder, input, meta, icon, showError, className } = props; + const { input, meta, showError, className, ...rest } = props; return ( {showError && ( diff --git a/app/client/src/components/editorComponents/DataTreeNode.tsx b/app/client/src/components/editorComponents/DataTreeNode.tsx new file mode 100644 index 0000000000..eb3d04e385 --- /dev/null +++ b/app/client/src/components/editorComponents/DataTreeNode.tsx @@ -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) => ( + + + + //{item.type} + +); + +export default DataTreeNode; diff --git a/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx b/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx new file mode 100644 index 0000000000..8bd13ebdcf --- /dev/null +++ b/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx @@ -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 { + 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): 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) => { + if (event.key === "ArrowDown") { + if ( + document.activeElement && + document.activeElement.tagName === "INPUT" + ) { + const tree = document.getElementById("tree"); + const container = + tree && tree.closest("[tabindex='0']"); + if (container) { + container.focus(); + } + } + } + }; + + render() { + const { input, ...rest } = this.props; + return ( + + + {this.state.showTree && this.state.tree.length && ( + + {({ search, items }) => ( + + {items.length === 0 ? ( + No results found + ) : ( + items.map((item: TreeMenuItem) => { + this.search = search; + return ; + }) + )} + + )} + + )} + + ); + } +} + +const mapStateToProps = (state: AppState): ReduxStateProps => ({ + dynamicData: getNameBindingsWithData(state), +}); + +export default connect(mapStateToProps)(DynamicAutocompleteInput); diff --git a/app/client/src/components/editorComponents/form/fields/DynamicTextField.tsx b/app/client/src/components/editorComponents/form/fields/DynamicTextField.tsx new file mode 100644 index 0000000000..106fc4c447 --- /dev/null +++ b/app/client/src/components/editorComponents/form/fields/DynamicTextField.tsx @@ -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 ; + } +} + +export default DynamicTextField; diff --git a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx index 3ea4574eab..d4253bd621 100644 --- a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx +++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx @@ -2,7 +2,7 @@ import React from "react"; import { FieldArray, WrappedFieldArrayProps } from "redux-form"; import { Icon } from "@blueprintjs/core"; import { FormIcons } from "icons/FormIcons"; -import TextField from "./TextField"; +import DynamicTextField from "./DynamicTextField"; import FormRow from "components/editorComponents/FormRow"; import FormLabel from "components/editorComponents/FormLabel"; import styled from "styled-components"; @@ -23,8 +23,8 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { {props.fields.map((field: any, index: number) => ( {index === 0 && {props.label}} - - + + {index === props.fields.length - 1 ? ( { render() { return ( - + + + ); } @@ -34,8 +39,11 @@ class InputTextControl extends BaseControl { } } - onTextChange = (event: React.ChangeEvent) => { - const value: string = event.target.value; + onTextChange = (event: React.ChangeEvent | string) => { + let value = event; + if (typeof event !== "string") { + value = event.target.value; + } this.updateProperty(this.props.propertyName, value); }; diff --git a/app/client/src/components/propertyControls/StyledControls.tsx b/app/client/src/components/propertyControls/StyledControls.tsx index d0b217e8ee..012b8bdca2 100644 --- a/app/client/src/components/propertyControls/StyledControls.tsx +++ b/app/client/src/components/propertyControls/StyledControls.tsx @@ -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)` & > input { placeholder-text: ${props => props.placeholder}; diff --git a/app/client/src/constants/BindingsConstants.ts b/app/client/src/constants/BindingsConstants.ts index 77b8aac1f4..2bd2e0df30 100644 --- a/app/client/src/constants/BindingsConstants.ts +++ b/app/client/src/constants/BindingsConstants.ts @@ -3,4 +3,5 @@ export type NamePathBindingMap = Record; export const DATA_BIND_REGEX = /(.*?){{(\s*(.*?)\s*)}}(.*?)/g; export const DATA_PATH_REGEX = /[\w\.\[\]\d]+/; +export const DATA_BIND_AUTOCOMPLETE = /({{)(.*)(}{0,2}?)/; /* eslint-enable no-useless-escape */ diff --git a/app/client/src/pages/Editor/APIEditor/Form.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx index d6673ba7cb..3eb282b1ee 100644 --- a/app/client/src/pages/Editor/APIEditor/Form.tsx +++ b/app/client/src/pages/Editor/APIEditor/Form.tsx @@ -7,6 +7,7 @@ import FormRow from "components/editorComponents/FormRow"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import { RestAction } from "api/ActionAPI"; 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 DatasourcesField from "components/editorComponents/form/fields/DatasourcesField"; import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray"; @@ -131,7 +132,7 @@ const ApiEditorForm: React.FC = (props: Props) => { options={HTTP_METHOD_OPTIONS} /> - { if (!pluginId) return null; const { isCreating, search, name } = this.state; 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; return ( diff --git a/app/client/src/pages/Editor/Popper.tsx b/app/client/src/pages/Editor/Popper.tsx index 58769b99f8..97a4599abd 100644 --- a/app/client/src/pages/Editor/Popper.tsx +++ b/app/client/src/pages/Editor/Popper.tsx @@ -16,7 +16,7 @@ const PopperWrapper = styled(PaneWrapper)` max-height: ${props => props.theme.propertyPane.height}px; width: ${props => props.theme.propertyPane.width}px; margin: ${props => props.theme.spaces[6]}px; - overflow-y: auto; + overflow-y: visible; `; /* eslint-disable react/display-name */ diff --git a/app/client/src/pages/Editor/WidgetsEditor.tsx b/app/client/src/pages/Editor/WidgetsEditor.tsx index c3f83aef9a..c81a1db519 100644 --- a/app/client/src/pages/Editor/WidgetsEditor.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor.tsx @@ -3,7 +3,6 @@ import { connect } from "react-redux"; import { useParams } from "react-router-dom"; import styled from "styled-components"; import Canvas from "./Canvas"; -import PropertyPane from "./PropertyPane"; import { AppState } from "reducers"; import { WidgetProps } from "widgets/BaseWidget"; import { savePage } from "actions/pageActions"; @@ -84,7 +83,6 @@ const WidgetsEditor = (props: EditorProps) => { {node} - ); diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 1c43be0fcf..7ee8311c06 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -39,7 +39,6 @@ import { evaluateDynamicBoundValue, getDynamicBindings, isDynamicValue, - NameBindingsWithData, } from "utils/DynamicBindingUtils"; import { validateResponse } from "./ErrorSagas"; import { getDataTree } from "selectors/entitiesSelector"; @@ -50,7 +49,10 @@ import { import { getFormData } from "selectors/formSelectors"; import { API_EDITOR_FORM_NAME } from "constants/forms"; import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton"; -import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector"; +import { + getNameBindingsWithData, + NameBindingsWithData, +} from "selectors/nameBindingsWithDataSelector"; export const getAction = ( state: AppState, diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 32d2fc14e9..fdc4067eb7 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -7,10 +7,7 @@ import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigRe import { WidgetCardProps } from "widgets/BaseWidget"; import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; -import { - enhanceWithDynamicValuesAndValidations, - NameBindingsWithData, -} from "utils/DynamicBindingUtils"; +import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; import { getDataTree } from "./entitiesSelector"; import { FlattenedWidgetProps, @@ -20,7 +17,10 @@ import { PageListReduxState } from "reducers/entityReducers/pageListReducer"; import { OccupiedSpace } from "constants/editorConstants"; import { WidgetTypes } from "constants/WidgetConstants"; -import { getNameBindingsWithData } from "./nameBindingsWithDataSelector"; +import { + getNameBindingsWithData, + NameBindingsWithData, +} from "./nameBindingsWithDataSelector"; const getEditorState = (state: AppState) => state.ui.editor; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 54bf9f2baa..9e7595ddd3 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -2,6 +2,9 @@ import { AppState, DataTree } from "reducers"; export const getDataTree = (state: AppState): DataTree => state.entities; +export const getDynamicNames = (state: AppState): DataTree["nameBindings"] => + state.entities.nameBindings; + export const getPluginIdOfName = ( state: AppState, name: string, diff --git a/app/client/src/selectors/nameBindingsWithDataSelector.ts b/app/client/src/selectors/nameBindingsWithDataSelector.ts index 55ae87f2d7..85ddf0d3fc 100644 --- a/app/client/src/selectors/nameBindingsWithDataSelector.ts +++ b/app/client/src/selectors/nameBindingsWithDataSelector.ts @@ -1,9 +1,9 @@ import { DataTree } from "reducers"; -import { NameBindingsWithData } from "utils/DynamicBindingUtils"; import { JSONPath } from "jsonpath-plus"; import { createSelector } from "reselect"; import { getDataTree } from "./entitiesSelector"; +export type NameBindingsWithData = Record; export const getNameBindingsWithData = createSelector( getDataTree, (dataTree: DataTree): NameBindingsWithData => { diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index 9dc1489779..6a5879ebf0 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -4,12 +4,12 @@ import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer" import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer"; -import { - enhanceWithDynamicValuesAndValidations, - NameBindingsWithData, -} from "utils/DynamicBindingUtils"; +import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; import { WidgetProps } from "widgets/BaseWidget"; -import { getNameBindingsWithData } from "./nameBindingsWithDataSelector"; +import { + getNameBindingsWithData, + NameBindingsWithData, +} from "./nameBindingsWithDataSelector"; const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => state.ui.propertyPane; diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 2d888a216d..1e88799d38 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -1,10 +1,25 @@ import _ from "lodash"; 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 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; export const isDynamicValue = (value: string): boolean => DATA_BIND_REGEX.test(value); diff --git a/app/client/src/utils/DynamicTreeAutoCompleteUtils.tsx b/app/client/src/utils/DynamicTreeAutoCompleteUtils.tsx new file mode 100644 index 0000000000..0b9c77e730 --- /dev/null +++ b/app/client/src/utils/DynamicTreeAutoCompleteUtils.tsx @@ -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, + 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 = ( +
+ {name}: + + {childType === "string" + ? `"${dataTree[name]}"` + : String(dataTree[name])} + +
+ ); + } + return { + key: name, + path: currentPath, + label: "", + labelRender, + nodes, + type: childType, + }; + }); +}; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index dd2f0b0e7b..b9dac73f7b 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -7944,6 +7944,11 @@ is-directory@^0.3.1: resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" 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: version "0.1.1" 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: 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: version "2.6.10" 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" 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: version "2.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"