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

View File

@ -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<WrappedFieldInputProps>;
meta?: Partial<WrappedFieldMetaProps>;
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 (
<InputContainer className={className}>
<TextInput
inputRef={props.refHandler}
{...input}
placeholder={placeholder}
leftIcon={icon}
autoComplete={"off"}
{...rest}
/>
{showError && (
<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 { 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) => (
<FormRowWithLabel key={index}>
{index === 0 && <FormLabel>{props.label}</FormLabel>}
<TextField name={`${field}.key`} placeholder="Key" />
<TextField name={`${field}.value`} placeholder="Value" />
<DynamicTextField name={`${field}.key`} placeholder="Key" />
<DynamicTextField name={`${field}.value`} placeholder="Value" />
{index === props.fields.length - 1 ? (
<Icon
icon="plus"

View File

@ -1,22 +1,27 @@
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlWrapper, StyledInputGroup } from "./StyledControls";
import { ControlWrapper, StyledDynamicInput } from "./StyledControls";
import { InputType } from "widgets/InputWidget";
import { ControlType } from "constants/PropertyControlConstants";
import { Intent } from "@blueprintjs/core";
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
class InputTextControl extends BaseControl<InputControlProps> {
render() {
return (
<ControlWrapper>
<label>{this.props.label}</label>
<StyledInputGroup
intent={this.props.isValid ? Intent.NONE : Intent.DANGER}
type={this.isNumberType() ? "number" : "text"}
onChange={this.onTextChange}
placeholder={this.props.placeholderText}
defaultValue={this.props.propertyValue}
/>
<StyledDynamicInput>
<DynamicAutocompleteInput
intent={this.props.isValid ? Intent.NONE : Intent.DANGER}
type={this.isNumberType() ? "number" : "text"}
input={{
value: this.props.propertyValue,
onChange: this.onTextChange,
}}
placeholder={this.props.placeholderText}
/>
</StyledDynamicInput>
</ControlWrapper>
);
}
@ -34,8 +39,11 @@ class InputTextControl extends BaseControl<InputControlProps> {
}
}
onTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value: string = event.target.value;
onTextChange = (event: React.ChangeEvent<HTMLInputElement> | string) => {
let value = event;
if (typeof event !== "string") {
value = event.target.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)`
& > input {
placeholder-text: ${props => props.placeholder};

View File

@ -3,4 +3,5 @@
export type NamePathBindingMap = Record<string, string>;
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 */

View File

@ -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: Props) => {
options={HTTP_METHOD_OPTIONS}
/>
<DatasourcesField name="datasource.id" pluginId={pluginId} />
<TextField
<DynamicTextField
placeholder="API Path"
name="actionConfiguration.path"
icon="slash"

View File

@ -153,11 +153,12 @@ type State = {
search: string;
};
const fuseOptions = {
const FUSE_OPTIONS = {
shouldSort: true,
threshold: 0.1,
threshold: 0.5,
location: 0,
minMatchCharLength: 3,
findAllMatches: true,
keys: ["name"],
};
@ -234,7 +235,7 @@ class ApiSidebar extends React.Component<Props, State> {
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 (
<React.Fragment>

View File

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

View File

@ -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) => {
<EditorContextProvider>
<EditorWrapper>
<CanvasContainer>{node}</CanvasContainer>
<PropertyPane />
</EditorWrapper>
</EditorContextProvider>
);

View File

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

View File

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

View File

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

View File

@ -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<string, object>;
export const getNameBindingsWithData = createSelector(
getDataTree,
(dataTree: DataTree): NameBindingsWithData => {

View File

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

View File

@ -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<string, object>;
export const isDynamicValue = (value: string): boolean =>
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"
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"