Move plugins to "sonar/desktop/plugins"
Summary: Plugins moved from "sonar/desktop/src/plugins" to "sonar/desktop/plugins". Fixed all the paths after moving. New "desktop" folder structure: - `src` - Flipper desktop app JS code executing in Electron Renderer (Chrome) process. - `static` - Flipper desktop app JS code executing in Electron Main (Node.js) process. - `plugins` - Flipper desktop JS plugins. - `pkg` - Flipper packaging lib and CLI tool. - `doctor` - Flipper diagnostics lib and CLI tool. - `scripts` - Build scripts for Flipper desktop app. - `headless` - Headless version of Flipper desktop app. - `headless-tests` - Integration tests running agains Flipper headless version. Reviewed By: mweststrate Differential Revision: D20344186 fbshipit-source-id: d020da970b2ea1e001f9061a8782bfeb54e31ba0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
beb5c85e69
commit
10d990c32c
343
desktop/plugins/layout/Inspector.tsx
Normal file
343
desktop/plugins/layout/Inspector.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
ElementID,
|
||||
Element,
|
||||
PluginClient,
|
||||
ElementsInspector,
|
||||
ElementSearchResultSet,
|
||||
} from 'flipper';
|
||||
import {Component} from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import {PersistedState, ElementMap} from './';
|
||||
import React from 'react';
|
||||
|
||||
type GetNodesOptions = {
|
||||
force?: boolean;
|
||||
ax?: boolean;
|
||||
forAccessibilityEvent?: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
ax?: boolean;
|
||||
client: PluginClient;
|
||||
showsSidebar: boolean;
|
||||
inAlignmentMode?: boolean;
|
||||
selectedElement: ElementID | null | undefined;
|
||||
selectedAXElement: ElementID | null | undefined;
|
||||
onSelect: (ids: ElementID | null | undefined) => void;
|
||||
onDataValueChanged: (path: Array<string>, value: any) => void;
|
||||
setPersistedState: (state: Partial<PersistedState>) => void;
|
||||
persistedState: PersistedState;
|
||||
searchResults: ElementSearchResultSet | null;
|
||||
};
|
||||
|
||||
export default class Inspector extends Component<Props> {
|
||||
call() {
|
||||
return {
|
||||
GET_ROOT: this.props.ax ? 'getAXRoot' : 'getRoot',
|
||||
INVALIDATE: this.props.ax ? 'invalidateAX' : 'invalidate',
|
||||
GET_NODES: this.props.ax ? 'getAXNodes' : 'getNodes',
|
||||
SET_HIGHLIGHTED: 'setHighlighted',
|
||||
SELECT: this.props.ax ? 'selectAX' : 'select',
|
||||
INVALIDATE_WITH_DATA: this.props.ax
|
||||
? 'invalidateWithDataAX'
|
||||
: 'invalidateWithData',
|
||||
};
|
||||
}
|
||||
|
||||
selected = () => {
|
||||
return this.props.ax
|
||||
? this.props.selectedAXElement
|
||||
: this.props.selectedElement;
|
||||
};
|
||||
|
||||
root = () => {
|
||||
return this.props.ax
|
||||
? this.props.persistedState.rootAXElement
|
||||
: this.props.persistedState.rootElement;
|
||||
};
|
||||
|
||||
elements = () => {
|
||||
return this.props.ax
|
||||
? this.props.persistedState.AXelements
|
||||
: this.props.persistedState.elements;
|
||||
};
|
||||
|
||||
focused = () => {
|
||||
if (!this.props.ax) {
|
||||
return null;
|
||||
}
|
||||
const elements: Array<Element> = Object.values(
|
||||
this.props.persistedState.AXelements,
|
||||
);
|
||||
const focusedElement = elements.find(i =>
|
||||
Boolean(
|
||||
i.data.Accessibility && i.data.Accessibility['accessibility-focused'],
|
||||
),
|
||||
);
|
||||
return focusedElement ? focusedElement.id : null;
|
||||
};
|
||||
|
||||
getAXContextMenuExtensions = () =>
|
||||
this.props.ax
|
||||
? [
|
||||
{
|
||||
label: 'Focus',
|
||||
click: (id: ElementID) => {
|
||||
this.props.client.call('onRequestAXFocus', {id});
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
componentDidMount() {
|
||||
this.props.client.call(this.call().GET_ROOT).then((root: Element) => {
|
||||
this.props.setPersistedState({
|
||||
[this.props.ax ? 'rootAXElement' : 'rootElement']: root.id,
|
||||
});
|
||||
this.updateElement(root.id, {...root, expanded: true});
|
||||
this.performInitialExpand(root);
|
||||
});
|
||||
|
||||
this.props.client.subscribe(
|
||||
this.call().INVALIDATE,
|
||||
({
|
||||
nodes,
|
||||
}: {
|
||||
nodes: Array<{id: ElementID; children: Array<ElementID>}>;
|
||||
}) => {
|
||||
const ids = nodes
|
||||
.map(n => [n.id, ...(n.children || [])])
|
||||
.reduce((acc, cv) => acc.concat(cv), []);
|
||||
this.invalidate(ids);
|
||||
},
|
||||
);
|
||||
|
||||
this.props.client.subscribe(
|
||||
this.call().INVALIDATE_WITH_DATA,
|
||||
(obj: {nodes: Array<Element>}) => {
|
||||
const {nodes} = obj;
|
||||
this.invalidateWithData(nodes);
|
||||
},
|
||||
);
|
||||
|
||||
this.props.client.subscribe(
|
||||
this.call().SELECT,
|
||||
({path}: {path: Array<ElementID>}) => {
|
||||
this.getAndExpandPath(path);
|
||||
},
|
||||
);
|
||||
|
||||
if (this.props.ax) {
|
||||
this.props.client.subscribe('axFocusEvent', () => {
|
||||
// update all nodes, to find new focused node
|
||||
this.getNodes(Object.keys(this.props.persistedState.AXelements), {
|
||||
force: true,
|
||||
ax: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const {ax, selectedElement, selectedAXElement} = this.props;
|
||||
|
||||
if (
|
||||
ax &&
|
||||
selectedElement &&
|
||||
selectedElement !== prevProps.selectedElement
|
||||
) {
|
||||
// selected element in non-AX tree changed, find linked element in AX tree
|
||||
const newlySelectedElem = this.props.persistedState.elements[
|
||||
selectedElement
|
||||
];
|
||||
if (newlySelectedElem) {
|
||||
this.props.onSelect(
|
||||
newlySelectedElem.extraInfo
|
||||
? newlySelectedElem.extraInfo.linkedNode
|
||||
: null,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
!ax &&
|
||||
selectedAXElement &&
|
||||
selectedAXElement !== prevProps.selectedAXElement
|
||||
) {
|
||||
// selected element in AX tree changed, find linked element in non-AX tree
|
||||
const newlySelectedAXElem = this.props.persistedState.AXelements[
|
||||
selectedAXElement
|
||||
];
|
||||
if (newlySelectedAXElem) {
|
||||
this.props.onSelect(
|
||||
newlySelectedAXElem.extraInfo
|
||||
? newlySelectedAXElem.extraInfo.linkedNode
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalidateWithData(elements: Array<Element>): void {
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
const updatedElements: ElementMap = elements.reduce(
|
||||
(acc: ElementMap, element: Element) => {
|
||||
acc[element.id] = {
|
||||
...element,
|
||||
expanded: this.elements()[element.id]
|
||||
? this.elements()[element.id].expanded
|
||||
: false,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
this.props.setPersistedState({
|
||||
[this.props.ax ? 'AXelements' : 'elements']: {
|
||||
...this.elements(),
|
||||
...updatedElements,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async invalidate(ids: Array<ElementID>): Promise<Array<Element>> {
|
||||
if (ids.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const elements = await this.getNodes(ids, {});
|
||||
const children = elements
|
||||
.filter(
|
||||
(element: Element) =>
|
||||
this.elements()[element.id] && this.elements()[element.id].expanded,
|
||||
)
|
||||
.map((element: Element) => element.children)
|
||||
.reduce((acc, val) => acc.concat(val), []);
|
||||
return this.invalidate(children);
|
||||
}
|
||||
|
||||
updateElement(id: ElementID, data: Object) {
|
||||
this.props.setPersistedState({
|
||||
[this.props.ax ? 'AXelements' : 'elements']: {
|
||||
...this.elements(),
|
||||
[id]: {
|
||||
...this.elements()[id],
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// When opening the inspector for the first time, expand all elements that
|
||||
// contain only 1 child recursively.
|
||||
async performInitialExpand(element: Element): Promise<void> {
|
||||
if (!element.children.length) {
|
||||
// element has no children so we're as deep as we can be
|
||||
return;
|
||||
}
|
||||
return this.getChildren(element.id, {}).then(() => {
|
||||
if (element.children.length >= 2) {
|
||||
// element has two or more children so we can stop expanding
|
||||
return;
|
||||
}
|
||||
return this.performInitialExpand(this.elements()[element.children[0]]);
|
||||
});
|
||||
}
|
||||
|
||||
async getChildren(
|
||||
id: ElementID,
|
||||
options: GetNodesOptions,
|
||||
): Promise<Array<Element>> {
|
||||
if (!this.elements()[id]) {
|
||||
await this.getNodes([id], options);
|
||||
}
|
||||
this.updateElement(id, {expanded: true});
|
||||
return this.getNodes(this.elements()[id].children, options);
|
||||
}
|
||||
|
||||
async getNodes(
|
||||
ids: Array<ElementID> = [],
|
||||
options: GetNodesOptions,
|
||||
): Promise<Array<Element>> {
|
||||
if (ids.length > 0) {
|
||||
const {forAccessibilityEvent} = options;
|
||||
const {
|
||||
elements,
|
||||
}: {elements: Array<Element>} = await this.props.client.call(
|
||||
this.call().GET_NODES,
|
||||
{
|
||||
ids,
|
||||
forAccessibilityEvent,
|
||||
selected: false,
|
||||
},
|
||||
);
|
||||
elements.forEach(e => this.updateElement(e.id, e));
|
||||
return elements;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getAndExpandPath(path: Array<ElementID>) {
|
||||
await Promise.all(path.map(id => this.getChildren(id, {})));
|
||||
this.onElementSelected(path[path.length - 1]);
|
||||
}
|
||||
|
||||
onElementSelected = debounce((selectedKey: ElementID) => {
|
||||
this.onElementHovered(selectedKey);
|
||||
this.props.onSelect(selectedKey);
|
||||
});
|
||||
|
||||
onElementHovered = debounce((key: ElementID | null | undefined) => {
|
||||
this.props.client.call(this.call().SET_HIGHLIGHTED, {
|
||||
id: key,
|
||||
isAlignmentMode: this.props.inAlignmentMode,
|
||||
});
|
||||
});
|
||||
|
||||
onElementExpanded = (
|
||||
id: ElementID,
|
||||
deep: boolean,
|
||||
forceExpand: boolean = false,
|
||||
) => {
|
||||
const shouldExpand = forceExpand || !this.elements()[id].expanded;
|
||||
if (shouldExpand) {
|
||||
this.updateElement(id, {expanded: shouldExpand});
|
||||
}
|
||||
this.getChildren(id, {}).then(children => {
|
||||
if (deep) {
|
||||
children.forEach(child =>
|
||||
this.onElementExpanded(child.id, deep, shouldExpand),
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!shouldExpand) {
|
||||
this.updateElement(id, {expanded: shouldExpand});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.root() ? (
|
||||
<ElementsInspector
|
||||
onElementSelected={this.onElementSelected}
|
||||
onElementHovered={this.onElementHovered}
|
||||
onElementExpanded={this.onElementExpanded}
|
||||
onValueChanged={this.props.onDataValueChanged}
|
||||
searchResults={this.props.searchResults}
|
||||
selected={this.selected()}
|
||||
root={this.root()}
|
||||
elements={this.elements()}
|
||||
focused={this.focused()}
|
||||
contextMenuExtensions={this.getAXContextMenuExtensions()}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
175
desktop/plugins/layout/InspectorSidebar.tsx
Normal file
175
desktop/plugins/layout/InspectorSidebar.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
ManagedDataInspector,
|
||||
Panel,
|
||||
FlexCenter,
|
||||
styled,
|
||||
colors,
|
||||
PluginClient,
|
||||
SidebarExtensions,
|
||||
Element,
|
||||
Client,
|
||||
Logger,
|
||||
} from 'flipper';
|
||||
import {Component} from 'react';
|
||||
import deepEqual from 'deep-equal';
|
||||
import React from 'react';
|
||||
import {useMemo, useEffect} from 'react';
|
||||
import {kebabCase} from 'lodash';
|
||||
|
||||
const NoData = styled(FlexCenter)({
|
||||
fontSize: 18,
|
||||
color: colors.macOSTitleBarIcon,
|
||||
});
|
||||
|
||||
type OnValueChanged = (path: Array<string>, val: any) => void;
|
||||
|
||||
type InspectorSidebarSectionProps = {
|
||||
data: any;
|
||||
id: string;
|
||||
onValueChanged: OnValueChanged | null;
|
||||
tooltips?: Object;
|
||||
};
|
||||
|
||||
class InspectorSidebarSection extends Component<InspectorSidebarSectionProps> {
|
||||
setValue = (path: Array<string>, value: any) => {
|
||||
if (this.props.onValueChanged) {
|
||||
this.props.onValueChanged([this.props.id, ...path], value);
|
||||
}
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: InspectorSidebarSectionProps) {
|
||||
return (
|
||||
!deepEqual(nextProps, this.props) ||
|
||||
this.props.id !== nextProps.id ||
|
||||
this.props.onValueChanged !== nextProps.onValueChanged
|
||||
);
|
||||
}
|
||||
|
||||
extractValue = (val: any, _depth: number) => {
|
||||
if (val && val.__type__) {
|
||||
return {
|
||||
mutable: Boolean(val.__mutable__),
|
||||
type: val.__type__ === 'auto' ? typeof val.value : val.__type__,
|
||||
value: val.value,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
mutable: typeof val === 'object',
|
||||
type: typeof val,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {id} = this.props;
|
||||
return (
|
||||
<Panel heading={id} floating={false} grow={false}>
|
||||
<ManagedDataInspector
|
||||
data={this.props.data}
|
||||
setValue={this.props.onValueChanged ? this.setValue : undefined}
|
||||
extractValue={this.extractValue}
|
||||
expandRoot={true}
|
||||
collapsed={true}
|
||||
tooltips={this.props.tooltips}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
element: Element | null;
|
||||
tooltips?: Object;
|
||||
onValueChanged: OnValueChanged | null;
|
||||
client: PluginClient;
|
||||
realClient: Client;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
const Sidebar: React.FC<Props> = (props: Props) => {
|
||||
const {element} = props;
|
||||
if (!element || !element.data) {
|
||||
return <NoData grow>No data</NoData>;
|
||||
}
|
||||
|
||||
const [sectionDefs, sectionKeys] = useMemo(() => {
|
||||
const sectionKeys = [];
|
||||
const sectionDefs = [];
|
||||
|
||||
for (const key in element.data) {
|
||||
if (key === 'Extra Sections') {
|
||||
for (const extraSection in element.data[key]) {
|
||||
const section = element.data[key][extraSection];
|
||||
let data = {};
|
||||
|
||||
// data might be sent as stringified JSON, we want to parse it for a nicer persentation.
|
||||
if (typeof section === 'string') {
|
||||
try {
|
||||
data = JSON.parse(section);
|
||||
} catch (e) {
|
||||
// data was not a valid JSON, type is required to be an object
|
||||
console.error(
|
||||
`ElementsInspector unable to parse extra section: ${extraSection}`,
|
||||
);
|
||||
data = {};
|
||||
}
|
||||
} else {
|
||||
data = section;
|
||||
}
|
||||
sectionKeys.push(kebabCase(extraSection));
|
||||
sectionDefs.push({
|
||||
key: extraSection,
|
||||
id: extraSection,
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
sectionKeys.push(kebabCase(key));
|
||||
sectionDefs.push({
|
||||
key,
|
||||
id: key,
|
||||
data: element.data[key],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [sectionDefs, sectionKeys];
|
||||
}, [props.element]);
|
||||
|
||||
const sections: Array<React.ReactNode> = (
|
||||
(SidebarExtensions &&
|
||||
SidebarExtensions.map(ext =>
|
||||
ext(props.client, props.realClient, element.id, props.logger),
|
||||
)) ||
|
||||
[]
|
||||
).concat(
|
||||
sectionDefs.map(def => (
|
||||
<InspectorSidebarSection
|
||||
tooltips={props.tooltips}
|
||||
key={def.key}
|
||||
id={def.id}
|
||||
data={def.data}
|
||||
onValueChanged={props.onValueChanged}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
sectionKeys.map(key =>
|
||||
props.logger.track('usage', `layout-sidebar-extension:${key}:loaded`),
|
||||
);
|
||||
}, [props.element?.data]);
|
||||
return <>{sections}</>;
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
194
desktop/plugins/layout/ProxyArchiveClient.tsx
Normal file
194
desktop/plugins/layout/ProxyArchiveClient.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Element} from 'flipper';
|
||||
import {PersistedState} from './index';
|
||||
import {SearchResultTree} from './Search';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
const propsForPersistedState = (
|
||||
AXMode: boolean,
|
||||
): {
|
||||
ROOT: 'rootAXElement' | 'rootElement';
|
||||
ELEMENTS: 'AXelements' | 'elements';
|
||||
ELEMENT: 'axElement' | 'element';
|
||||
} => {
|
||||
return {
|
||||
ROOT: AXMode ? 'rootAXElement' : 'rootElement',
|
||||
ELEMENTS: AXMode ? 'AXelements' : 'elements',
|
||||
ELEMENT: AXMode ? 'axElement' : 'element',
|
||||
};
|
||||
};
|
||||
|
||||
function constructSearchResultTree(
|
||||
node: Element,
|
||||
isMatch: boolean,
|
||||
children: Array<SearchResultTree>,
|
||||
_AXMode: boolean,
|
||||
AXNode: Element | null,
|
||||
): SearchResultTree {
|
||||
const searchResult = {
|
||||
id: node.id,
|
||||
isMatch,
|
||||
hasChildren: children.length > 0,
|
||||
children: children.length > 0 ? children : [],
|
||||
element: node,
|
||||
axElement: AXNode,
|
||||
};
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
function isMatch(element: Element, query: string): boolean {
|
||||
const nameMatch = element.name.toLowerCase().includes(query.toLowerCase());
|
||||
return nameMatch || element.id === query;
|
||||
}
|
||||
|
||||
export function searchNodes(
|
||||
node: Element,
|
||||
query: string,
|
||||
AXMode: boolean,
|
||||
state: PersistedState,
|
||||
): SearchResultTree | null {
|
||||
// Even if the axMode is true, we will have to search the normal elements too.
|
||||
// The AXEelements will automatically populated in constructSearchResultTree
|
||||
const elements = state[propsForPersistedState(false).ELEMENTS];
|
||||
const children: Array<SearchResultTree> = [];
|
||||
const match = isMatch(node, query);
|
||||
|
||||
for (const childID of node.children) {
|
||||
const child = elements[childID];
|
||||
const tree = searchNodes(child, query, AXMode, state);
|
||||
if (tree) {
|
||||
children.push(tree);
|
||||
}
|
||||
}
|
||||
|
||||
if (match || children.length > 0) {
|
||||
return cloneDeep(
|
||||
constructSearchResultTree(
|
||||
node,
|
||||
match,
|
||||
children,
|
||||
AXMode,
|
||||
AXMode ? state.AXelements[node.id] : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class ProxyArchiveClient {
|
||||
constructor(
|
||||
persistedState: PersistedState,
|
||||
onElementHighlighted?: (id: string) => void,
|
||||
) {
|
||||
this.persistedState = cloneDeep(persistedState);
|
||||
this.onElementHighlighted = onElementHighlighted;
|
||||
}
|
||||
persistedState: PersistedState;
|
||||
onElementHighlighted: ((id: string) => void) | undefined;
|
||||
subscribe(_method: string, _callback: (params: any) => void): void {
|
||||
return;
|
||||
}
|
||||
|
||||
supportsMethod(_method: string): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
send(_method: string, _params?: Object): void {
|
||||
return;
|
||||
}
|
||||
|
||||
call(method: string, paramaters?: {[key: string]: any}): Promise<any> {
|
||||
switch (method) {
|
||||
case 'getRoot': {
|
||||
const {rootElement} = this.persistedState;
|
||||
if (!rootElement) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.resolve(this.persistedState.elements[rootElement]);
|
||||
}
|
||||
case 'getAXRoot': {
|
||||
const {rootAXElement} = this.persistedState;
|
||||
if (!rootAXElement) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.resolve(this.persistedState.AXelements[rootAXElement]);
|
||||
}
|
||||
case 'getNodes': {
|
||||
if (!paramaters) {
|
||||
return Promise.reject(new Error('Called getNodes with no params'));
|
||||
}
|
||||
const {ids} = paramaters;
|
||||
const arr: Array<Element> = [];
|
||||
for (const id of ids) {
|
||||
arr.push(this.persistedState.elements[id]);
|
||||
}
|
||||
return Promise.resolve({elements: arr});
|
||||
}
|
||||
case 'getAXNodes': {
|
||||
if (!paramaters) {
|
||||
return Promise.reject(new Error('Called getAXNodes with no params'));
|
||||
}
|
||||
const {ids} = paramaters;
|
||||
const arr: Array<Element> = [];
|
||||
for (const id of ids) {
|
||||
arr.push(this.persistedState.AXelements[id]);
|
||||
}
|
||||
return Promise.resolve({elements: arr});
|
||||
}
|
||||
case 'getSearchResults': {
|
||||
const {rootElement, rootAXElement} = this.persistedState;
|
||||
|
||||
if (!paramaters) {
|
||||
return Promise.reject(
|
||||
new Error('Called getSearchResults with no params'),
|
||||
);
|
||||
}
|
||||
const {query, axEnabled} = paramaters;
|
||||
if (!query) {
|
||||
return Promise.reject(
|
||||
new Error('query is not passed as a params to getSearchResults'),
|
||||
);
|
||||
}
|
||||
let element: Element;
|
||||
if (axEnabled) {
|
||||
if (!rootAXElement) {
|
||||
return Promise.reject(new Error('rootAXElement is undefined'));
|
||||
}
|
||||
element = this.persistedState.AXelements[rootAXElement];
|
||||
} else {
|
||||
if (!rootElement) {
|
||||
return Promise.reject(new Error('rootElement is undefined'));
|
||||
}
|
||||
element = this.persistedState.elements[rootElement];
|
||||
}
|
||||
const output = searchNodes(
|
||||
element,
|
||||
query,
|
||||
axEnabled,
|
||||
this.persistedState,
|
||||
);
|
||||
return Promise.resolve({results: output, query});
|
||||
}
|
||||
case 'isConsoleEnabled': {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
case 'setHighlighted': {
|
||||
const id = paramaters?.id;
|
||||
this.onElementHighlighted && this.onElementHighlighted(id);
|
||||
return Promise.resolve();
|
||||
}
|
||||
default: {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export default ProxyArchiveClient;
|
||||
211
desktop/plugins/layout/Search.tsx
Normal file
211
desktop/plugins/layout/Search.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {PersistedState, ElementMap} from './';
|
||||
import {
|
||||
PluginClient,
|
||||
ElementSearchResultSet,
|
||||
Element,
|
||||
SearchInput,
|
||||
SearchBox,
|
||||
SearchIcon,
|
||||
LoadingIndicator,
|
||||
styled,
|
||||
colors,
|
||||
} from 'flipper';
|
||||
import {Component} from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export type SearchResultTree = {
|
||||
id: string;
|
||||
isMatch: boolean;
|
||||
hasChildren: boolean;
|
||||
children: Array<SearchResultTree>;
|
||||
element: Element;
|
||||
axElement: Element | null; // Not supported in iOS
|
||||
};
|
||||
|
||||
type Props = {
|
||||
client: PluginClient;
|
||||
inAXMode: boolean;
|
||||
onSearchResults: (searchResults: ElementSearchResultSet) => void;
|
||||
setPersistedState: (state: Partial<PersistedState>) => void;
|
||||
persistedState: PersistedState;
|
||||
initialQuery: string | null;
|
||||
};
|
||||
|
||||
type State = {
|
||||
value: string;
|
||||
outstandingSearchQuery: string | null;
|
||||
};
|
||||
|
||||
const LoadingSpinner = styled(LoadingIndicator)({
|
||||
marginRight: 4,
|
||||
marginLeft: 3,
|
||||
marginTop: -1,
|
||||
});
|
||||
|
||||
export default class Search extends Component<Props, State> {
|
||||
state = {
|
||||
value: '',
|
||||
outstandingSearchQuery: null,
|
||||
};
|
||||
|
||||
timer: NodeJS.Timeout | undefined;
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
const {value} = e.target;
|
||||
this.setState({value});
|
||||
this.timer = setTimeout(() => this.performSearch(value), 200);
|
||||
};
|
||||
|
||||
onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.performSearch(this.state.value);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.initialQuery) {
|
||||
const queryString = this.props.initialQuery
|
||||
? this.props.initialQuery
|
||||
: '';
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
this.timer = setTimeout(() => this.performSearch(queryString), 200);
|
||||
}
|
||||
}
|
||||
|
||||
performSearch(query: string) {
|
||||
this.setState({
|
||||
outstandingSearchQuery: query,
|
||||
});
|
||||
|
||||
if (!query) {
|
||||
this.displaySearchResults(
|
||||
{query: '', results: null},
|
||||
this.props.inAXMode,
|
||||
);
|
||||
} else {
|
||||
this.props.client
|
||||
.call('getSearchResults', {query, axEnabled: this.props.inAXMode})
|
||||
.then(response =>
|
||||
this.displaySearchResults(response, this.props.inAXMode),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
displaySearchResults(
|
||||
{
|
||||
results,
|
||||
query,
|
||||
}: {
|
||||
results: SearchResultTree | null;
|
||||
query: string;
|
||||
},
|
||||
axMode: boolean,
|
||||
) {
|
||||
this.setState({
|
||||
outstandingSearchQuery:
|
||||
query === this.state.outstandingSearchQuery
|
||||
? null
|
||||
: this.state.outstandingSearchQuery,
|
||||
});
|
||||
|
||||
const searchResults = this.getElementsFromSearchResultTree(results);
|
||||
const searchResultIDs = new Set(searchResults.map(r => r.element.id));
|
||||
const elements: ElementMap = searchResults.reduce(
|
||||
(acc: ElementMap, {element}: SearchResultTree) => ({
|
||||
...acc,
|
||||
[element.id]: {
|
||||
...element,
|
||||
// expand all search results, that we have have children for
|
||||
expanded: element.children.some(c => searchResultIDs.has(c)),
|
||||
},
|
||||
}),
|
||||
this.props.persistedState.elements,
|
||||
);
|
||||
|
||||
let {AXelements} = this.props.persistedState;
|
||||
if (axMode) {
|
||||
AXelements = searchResults.reduce(
|
||||
(acc: ElementMap, {axElement}: SearchResultTree) => {
|
||||
if (!axElement) {
|
||||
return acc;
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[axElement.id]: {
|
||||
...axElement,
|
||||
// expand all search results, that we have have children for
|
||||
expanded: axElement.children.some(c => searchResultIDs.has(c)),
|
||||
},
|
||||
};
|
||||
},
|
||||
this.props.persistedState.AXelements,
|
||||
);
|
||||
}
|
||||
|
||||
this.props.setPersistedState({elements, AXelements});
|
||||
|
||||
this.props.onSearchResults({
|
||||
matches: new Set(
|
||||
searchResults.filter(x => x.isMatch).map(x => x.element.id),
|
||||
),
|
||||
query: query,
|
||||
});
|
||||
}
|
||||
|
||||
getElementsFromSearchResultTree(
|
||||
tree: SearchResultTree | null,
|
||||
): Array<SearchResultTree> {
|
||||
if (!tree) {
|
||||
return [];
|
||||
}
|
||||
let elements = [
|
||||
{
|
||||
children: [] as Array<SearchResultTree>,
|
||||
id: tree.id,
|
||||
isMatch: tree.isMatch,
|
||||
hasChildren: Boolean(tree.children),
|
||||
element: tree.element,
|
||||
axElement: tree.axElement,
|
||||
},
|
||||
];
|
||||
if (tree.children) {
|
||||
for (const child of tree.children) {
|
||||
elements = elements.concat(this.getElementsFromSearchResultTree(child));
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SearchBox tabIndex={-1}>
|
||||
<SearchIcon
|
||||
name="magnifying-glass"
|
||||
color={colors.macOSTitleBarIcon}
|
||||
size={16}
|
||||
/>
|
||||
<SearchInput
|
||||
placeholder={'Search'}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
value={this.state.value}
|
||||
/>
|
||||
{this.state.outstandingSearchQuery && <LoadingSpinner size={16} />}
|
||||
</SearchBox>
|
||||
);
|
||||
}
|
||||
}
|
||||
41
desktop/plugins/layout/ToolbarIcon.tsx
Normal file
41
desktop/plugins/layout/ToolbarIcon.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Glyph, styled, colors} from 'flipper';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
icon: string;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const ToolbarIcon = styled.div({
|
||||
marginRight: 9,
|
||||
marginTop: -3,
|
||||
marginLeft: 4,
|
||||
position: 'relative', // for settings popover positioning
|
||||
});
|
||||
|
||||
export default function(props: Props) {
|
||||
return (
|
||||
<ToolbarIcon onClick={props.onClick} title={props.title}>
|
||||
<Glyph
|
||||
name={props.icon}
|
||||
size={16}
|
||||
color={
|
||||
props.active
|
||||
? colors.macOSTitleBarIconSelected
|
||||
: colors.macOSTitleBarIconActive
|
||||
}
|
||||
/>
|
||||
</ToolbarIcon>
|
||||
);
|
||||
}
|
||||
415
desktop/plugins/layout/__tests__/ProxyArchiveClient.node.tsx
Normal file
415
desktop/plugins/layout/__tests__/ProxyArchiveClient.node.tsx
Normal file
@@ -0,0 +1,415 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
default as ProxyArchiveClient,
|
||||
searchNodes,
|
||||
} from '../ProxyArchiveClient';
|
||||
import {PersistedState, ElementMap} from '../index';
|
||||
import {ElementID, Element} from 'flipper';
|
||||
import {SearchResultTree} from '../Search';
|
||||
|
||||
function constructElement(
|
||||
id: string,
|
||||
name: string,
|
||||
children: Array<ElementID>,
|
||||
): Element {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
expanded: false,
|
||||
children,
|
||||
attributes: [],
|
||||
data: {},
|
||||
decoration: 'decoration',
|
||||
extraInfo: {},
|
||||
};
|
||||
}
|
||||
|
||||
function constructPersistedState(axMode: boolean): PersistedState {
|
||||
if (!axMode) {
|
||||
return {
|
||||
rootElement: 'root',
|
||||
rootAXElement: null,
|
||||
elements: {},
|
||||
AXelements: {},
|
||||
};
|
||||
}
|
||||
return {
|
||||
rootElement: null,
|
||||
rootAXElement: 'root',
|
||||
elements: {},
|
||||
AXelements: {},
|
||||
};
|
||||
}
|
||||
let state = constructPersistedState(false);
|
||||
|
||||
function populateChildren(state: PersistedState, axMode: boolean) {
|
||||
const elements: ElementMap = {};
|
||||
elements['root'] = constructElement('root', 'root view', [
|
||||
'child0',
|
||||
'child1',
|
||||
]);
|
||||
|
||||
elements['child0'] = constructElement('child0', 'child0 view', [
|
||||
'child0_child0',
|
||||
'child0_child1',
|
||||
]);
|
||||
elements['child1'] = constructElement('child1', 'child1 view', [
|
||||
'child1_child0',
|
||||
'child1_child1',
|
||||
]);
|
||||
elements['child0_child0'] = constructElement(
|
||||
'child0_child0',
|
||||
'child0_child0 view',
|
||||
[],
|
||||
);
|
||||
elements['child0_child1'] = constructElement(
|
||||
'child0_child1',
|
||||
'child0_child1 view',
|
||||
[],
|
||||
);
|
||||
elements['child1_child0'] = constructElement(
|
||||
'child1_child0',
|
||||
'child1_child0 view',
|
||||
[],
|
||||
);
|
||||
elements['child1_child1'] = constructElement(
|
||||
'child1_child1',
|
||||
'child1_child1 view',
|
||||
[],
|
||||
);
|
||||
state.elements = elements;
|
||||
if (axMode) {
|
||||
state.AXelements = elements;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state = constructPersistedState(false);
|
||||
populateChildren(state, false);
|
||||
});
|
||||
|
||||
test('test the searchNode for root in axMode false', async () => {
|
||||
const searchResult: SearchResultTree | null = await searchNodes(
|
||||
state.elements['root'],
|
||||
'root',
|
||||
false,
|
||||
state,
|
||||
);
|
||||
expect(searchResult).toBeDefined();
|
||||
expect(searchResult).toEqual({
|
||||
id: 'root',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.elements['root'],
|
||||
axElement: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('test the searchNode for root in axMode true', async () => {
|
||||
state = constructPersistedState(true);
|
||||
populateChildren(state, true);
|
||||
const searchResult: SearchResultTree | null = await searchNodes(
|
||||
state.AXelements['root'],
|
||||
'RoOT',
|
||||
true,
|
||||
state,
|
||||
);
|
||||
expect(searchResult).toBeDefined();
|
||||
expect(searchResult).toEqual({
|
||||
id: 'root',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.AXelements['root'], // Even though AXElement exists, normal element will exist too
|
||||
axElement: state.AXelements['root'],
|
||||
});
|
||||
});
|
||||
|
||||
test('test the searchNode which matches just one child', async () => {
|
||||
const searchResult: SearchResultTree | null = await searchNodes(
|
||||
state.elements['root'],
|
||||
'child0_child0',
|
||||
false,
|
||||
state,
|
||||
);
|
||||
expect(searchResult).toBeDefined();
|
||||
expect(searchResult).toEqual({
|
||||
id: 'root',
|
||||
isMatch: false,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{
|
||||
id: 'child0',
|
||||
isMatch: false,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{
|
||||
id: 'child0_child0',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.elements['child0_child0'],
|
||||
axElement: null,
|
||||
},
|
||||
],
|
||||
element: state.elements['child0'],
|
||||
axElement: null,
|
||||
},
|
||||
],
|
||||
element: state.elements['root'],
|
||||
axElement: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('test the searchNode for which matches multiple child', async () => {
|
||||
const searchResult: SearchResultTree | null = await searchNodes(
|
||||
state.elements['root'],
|
||||
'child0',
|
||||
false,
|
||||
state,
|
||||
);
|
||||
expect(searchResult).toBeDefined();
|
||||
const expectedSearchResult = {
|
||||
id: 'root',
|
||||
isMatch: false,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{
|
||||
id: 'child0',
|
||||
isMatch: true,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{
|
||||
id: 'child0_child0',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.elements['child0_child0'],
|
||||
axElement: null,
|
||||
},
|
||||
{
|
||||
id: 'child0_child1',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.elements['child0_child1'],
|
||||
axElement: null,
|
||||
},
|
||||
],
|
||||
element: state.elements['child0'],
|
||||
axElement: null,
|
||||
},
|
||||
{
|
||||
id: 'child1',
|
||||
isMatch: false,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{
|
||||
id: 'child1_child0',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.elements['child1_child0'],
|
||||
axElement: null,
|
||||
},
|
||||
],
|
||||
element: state.elements['child1'],
|
||||
axElement: null,
|
||||
},
|
||||
],
|
||||
element: state.elements['root'],
|
||||
axElement: null,
|
||||
};
|
||||
expect(searchResult).toEqual(expectedSearchResult);
|
||||
});
|
||||
|
||||
test('test the searchNode, it should not be case sensitive', async () => {
|
||||
const searchResult: SearchResultTree | null = await searchNodes(
|
||||
state.elements['root'],
|
||||
'ChIlD0',
|
||||
false,
|
||||
state,
|
||||
);
|
||||
expect(searchResult).toBeDefined();
|
||||
const expectedSearchResult = {
|
||||
id: 'root',
|
||||
isMatch: false,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{
|
||||
id: 'child0',
|
||||
isMatch: true,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{
|
||||
id: 'child0_child0',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.elements['child0_child0'],
|
||||
axElement: null,
|
||||
},
|
||||
{
|
||||
id: 'child0_child1',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.elements['child0_child1'],
|
||||
axElement: null,
|
||||
},
|
||||
],
|
||||
element: state.elements['child0'],
|
||||
axElement: null,
|
||||
},
|
||||
{
|
||||
id: 'child1',
|
||||
isMatch: false,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{
|
||||
id: 'child1_child0',
|
||||
isMatch: true,
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
element: state.elements['child1_child0'],
|
||||
axElement: null,
|
||||
},
|
||||
],
|
||||
element: state.elements['child1'],
|
||||
axElement: null,
|
||||
},
|
||||
],
|
||||
element: state.elements['root'],
|
||||
axElement: null,
|
||||
};
|
||||
expect(searchResult).toEqual(expectedSearchResult);
|
||||
});
|
||||
|
||||
test('test the searchNode for non existent query', async () => {
|
||||
const searchResult: SearchResultTree | null = await searchNodes(
|
||||
state.elements['root'],
|
||||
'Unknown query',
|
||||
false,
|
||||
state,
|
||||
);
|
||||
expect(searchResult).toBeNull();
|
||||
});
|
||||
|
||||
test('test the call method with getRoot', async () => {
|
||||
const proxyClient = new ProxyArchiveClient(state);
|
||||
const root: Element = await proxyClient.call('getRoot');
|
||||
expect(root).toEqual(state.elements['root']);
|
||||
});
|
||||
|
||||
test('test the call method with getAXRoot', async () => {
|
||||
state = constructPersistedState(true);
|
||||
populateChildren(state, true);
|
||||
const proxyClient = new ProxyArchiveClient(state);
|
||||
const root: Element = await proxyClient.call('getAXRoot');
|
||||
expect(root).toEqual(state.AXelements['root']);
|
||||
});
|
||||
|
||||
test('test the call method with getNodes', async () => {
|
||||
const proxyClient = new ProxyArchiveClient(state);
|
||||
const nodes: Array<Element> = await proxyClient.call('getNodes', {
|
||||
ids: ['child0_child1', 'child1_child0'],
|
||||
});
|
||||
expect(nodes).toEqual({
|
||||
elements: [
|
||||
{
|
||||
id: 'child0_child1',
|
||||
name: 'child0_child1 view',
|
||||
expanded: false,
|
||||
children: [],
|
||||
attributes: [],
|
||||
data: {},
|
||||
decoration: 'decoration',
|
||||
extraInfo: {},
|
||||
},
|
||||
{
|
||||
id: 'child1_child0',
|
||||
name: 'child1_child0 view',
|
||||
expanded: false,
|
||||
children: [],
|
||||
attributes: [],
|
||||
data: {},
|
||||
decoration: 'decoration',
|
||||
extraInfo: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('test the call method with getAXNodes', async () => {
|
||||
state = constructPersistedState(true);
|
||||
populateChildren(state, true);
|
||||
const proxyClient = new ProxyArchiveClient(state);
|
||||
const nodes: Array<Element> = await proxyClient.call('getAXNodes', {
|
||||
ids: ['child0_child1', 'child1_child0'],
|
||||
});
|
||||
expect(nodes).toEqual({
|
||||
elements: [
|
||||
{
|
||||
id: 'child0_child1',
|
||||
name: 'child0_child1 view',
|
||||
expanded: false,
|
||||
children: [],
|
||||
attributes: [],
|
||||
data: {},
|
||||
decoration: 'decoration',
|
||||
extraInfo: {},
|
||||
},
|
||||
{
|
||||
id: 'child1_child0',
|
||||
name: 'child1_child0 view',
|
||||
expanded: false,
|
||||
children: [],
|
||||
attributes: [],
|
||||
data: {},
|
||||
decoration: 'decoration',
|
||||
extraInfo: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('test different methods of calls with no params', async () => {
|
||||
const proxyClient = new ProxyArchiveClient(state);
|
||||
await expect(proxyClient.call('getNodes')).rejects.toThrow(
|
||||
new Error('Called getNodes with no params'),
|
||||
);
|
||||
await expect(proxyClient.call('getAXNodes')).rejects.toThrow(
|
||||
new Error('Called getAXNodes with no params'),
|
||||
);
|
||||
// let result: Error = await proxyClient.call('getSearchResults');
|
||||
await expect(proxyClient.call('getSearchResults')).rejects.toThrow(
|
||||
new Error('Called getSearchResults with no params'),
|
||||
);
|
||||
await expect(
|
||||
proxyClient.call('getSearchResults', {
|
||||
query: 'random',
|
||||
axEnabled: true,
|
||||
}),
|
||||
).rejects.toThrow(new Error('rootAXElement is undefined'));
|
||||
await expect(
|
||||
proxyClient.call('getSearchResults', {
|
||||
axEnabled: false,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new Error('query is not passed as a params to getSearchResults'),
|
||||
);
|
||||
});
|
||||
|
||||
test('test call method isConsoleEnabled', () => {
|
||||
const proxyClient = new ProxyArchiveClient(state);
|
||||
return expect(proxyClient.call('isConsoleEnabled')).resolves.toBe(false);
|
||||
});
|
||||
451
desktop/plugins/layout/index.tsx
Normal file
451
desktop/plugins/layout/index.tsx
Normal file
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
ElementID,
|
||||
Element,
|
||||
ElementSearchResultSet,
|
||||
PluginClient,
|
||||
FlexColumn,
|
||||
FlexRow,
|
||||
FlipperPlugin,
|
||||
Toolbar,
|
||||
DetailSidebar,
|
||||
VerticalRule,
|
||||
Button,
|
||||
GK,
|
||||
Idler,
|
||||
Text,
|
||||
styled,
|
||||
colors,
|
||||
SupportRequestFormV2,
|
||||
constants,
|
||||
ReduxState,
|
||||
ArchivedDevice,
|
||||
} from 'flipper';
|
||||
import Inspector from './Inspector';
|
||||
import ToolbarIcon from './ToolbarIcon';
|
||||
import InspectorSidebar from './InspectorSidebar';
|
||||
import Search from './Search';
|
||||
import ProxyArchiveClient from './ProxyArchiveClient';
|
||||
import React from 'react';
|
||||
import {VisualizerPortal} from 'flipper';
|
||||
import {getFlipperMediaCDN} from 'flipper';
|
||||
|
||||
type State = {
|
||||
init: boolean;
|
||||
inTargetMode: boolean;
|
||||
inAXMode: boolean;
|
||||
inAlignmentMode: boolean;
|
||||
selectedElement: ElementID | null | undefined;
|
||||
selectedAXElement: ElementID | null | undefined;
|
||||
highlightedElement: ElementID | null;
|
||||
searchResults: ElementSearchResultSet | null;
|
||||
visualizerWindow: Window | null;
|
||||
visualizerScreenshot: string | null;
|
||||
screenDimensions: {width: number; height: number} | null;
|
||||
};
|
||||
|
||||
export type ElementMap = {[key: string]: Element};
|
||||
|
||||
export type PersistedState = {
|
||||
rootElement: ElementID | null;
|
||||
rootAXElement: ElementID | null;
|
||||
elements: ElementMap;
|
||||
AXelements: ElementMap;
|
||||
};
|
||||
|
||||
const FlipperADBarContainer = styled(FlexRow)({
|
||||
backgroundColor: colors.warningTint,
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
});
|
||||
|
||||
const FlipperADText = styled(Text)({
|
||||
padding: 10,
|
||||
});
|
||||
|
||||
const FlipperADButton = styled(Button)({
|
||||
margin: 10,
|
||||
});
|
||||
|
||||
export default class Layout extends FlipperPlugin<State, any, PersistedState> {
|
||||
FlipperADBar() {
|
||||
return (
|
||||
<FlipperADBarContainer>
|
||||
<FlipperADText>
|
||||
You can now submit support requests to Litho Group from Flipper. This
|
||||
automatically attaches critical information for reproducing your issue
|
||||
with just a single click.
|
||||
</FlipperADText>
|
||||
<FlipperADButton
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
this.props.setStaticView(SupportRequestFormV2);
|
||||
}}>
|
||||
Try it out
|
||||
</FlipperADButton>
|
||||
</FlipperADBarContainer>
|
||||
);
|
||||
}
|
||||
|
||||
static exportPersistedState = async (
|
||||
callClient: (
|
||||
method: 'getAllNodes',
|
||||
) => Promise<{
|
||||
allNodes: PersistedState;
|
||||
}>,
|
||||
persistedState: PersistedState | undefined,
|
||||
store: ReduxState | undefined,
|
||||
): Promise<PersistedState | undefined> => {
|
||||
if (!store) {
|
||||
return persistedState;
|
||||
}
|
||||
const {allNodes} = await callClient('getAllNodes');
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
static serializePersistedState: (
|
||||
persistedState: PersistedState,
|
||||
statusUpdate?: (msg: string) => void,
|
||||
idler?: Idler,
|
||||
) => Promise<string> = (
|
||||
persistedState: PersistedState,
|
||||
statusUpdate?: (msg: string) => void,
|
||||
_idler?: Idler,
|
||||
) => {
|
||||
statusUpdate && statusUpdate('Serializing Inspector Plugin...');
|
||||
return Promise.resolve(JSON.stringify(persistedState));
|
||||
};
|
||||
|
||||
static deserializePersistedState: (
|
||||
serializedString: string,
|
||||
) => PersistedState = (serializedString: string) => {
|
||||
return JSON.parse(serializedString);
|
||||
};
|
||||
|
||||
teardown() {
|
||||
this.state.visualizerWindow?.close();
|
||||
}
|
||||
|
||||
static defaultPersistedState = {
|
||||
rootElement: null,
|
||||
rootAXElement: null,
|
||||
elements: {},
|
||||
AXelements: {},
|
||||
};
|
||||
|
||||
state: State = {
|
||||
init: false,
|
||||
inTargetMode: false,
|
||||
inAXMode: false,
|
||||
inAlignmentMode: false,
|
||||
selectedElement: null,
|
||||
selectedAXElement: null,
|
||||
searchResults: null,
|
||||
visualizerWindow: null,
|
||||
highlightedElement: null,
|
||||
visualizerScreenshot: null,
|
||||
screenDimensions: null,
|
||||
};
|
||||
|
||||
init() {
|
||||
if (!this.props.persistedState) {
|
||||
// If the selected plugin from the previous session was layout, then while importing the flipper trace, the redux store doesn't get updated in the first render, due to which the plugin crashes, as it has no persisted state
|
||||
this.props.setPersistedState(this.constructor.defaultPersistedState);
|
||||
}
|
||||
// persist searchActive state when moving between plugins to prevent multiple
|
||||
// TouchOverlayViews since we can't edit the view heirarchy in onDisconnect
|
||||
this.client.call('isSearchActive').then(({isSearchActive}) => {
|
||||
this.setState({inTargetMode: isSearchActive});
|
||||
});
|
||||
|
||||
// disable target mode after
|
||||
this.client.subscribe('select', () => {
|
||||
if (this.state.inTargetMode) {
|
||||
this.onToggleTargetMode();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.props.isArchivedDevice) {
|
||||
this.getDevice()
|
||||
.then(d => {
|
||||
const handle = (d as ArchivedDevice).getArchivedScreenshotHandle();
|
||||
if (!handle) {
|
||||
throw new Error('No screenshot attached.');
|
||||
}
|
||||
return handle;
|
||||
})
|
||||
.then(handle => getFlipperMediaCDN(handle, 'Image'))
|
||||
.then(url => this.setState({visualizerScreenshot: url}))
|
||||
.catch(_ => {
|
||||
// Not all exports have screenshots. This is ok.
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
init: true,
|
||||
selectedElement: this.props.deepLinkPayload
|
||||
? this.props.deepLinkPayload
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
onToggleTargetMode = () => {
|
||||
const inTargetMode = !this.state.inTargetMode;
|
||||
this.setState({inTargetMode});
|
||||
this.client.send('setSearchActive', {active: inTargetMode});
|
||||
};
|
||||
|
||||
onToggleAXMode = () => {
|
||||
this.setState({inAXMode: !this.state.inAXMode});
|
||||
};
|
||||
|
||||
getClient(): PluginClient {
|
||||
return this.props.isArchivedDevice
|
||||
? new ProxyArchiveClient(this.props.persistedState, (id: string) => {
|
||||
this.setState({highlightedElement: id});
|
||||
})
|
||||
: this.client;
|
||||
}
|
||||
onToggleAlignmentMode = () => {
|
||||
if (this.state.selectedElement) {
|
||||
this.client.send('setHighlighted', {
|
||||
id: this.state.selectedElement,
|
||||
inAlignmentMode: !this.state.inAlignmentMode,
|
||||
});
|
||||
}
|
||||
this.setState({inAlignmentMode: !this.state.inAlignmentMode});
|
||||
};
|
||||
|
||||
onToggleVisualizer = () => {
|
||||
if (this.state.visualizerWindow) {
|
||||
this.state.visualizerWindow.close();
|
||||
} else {
|
||||
const screenDimensions = this.state.screenDimensions;
|
||||
if (!screenDimensions) {
|
||||
return;
|
||||
}
|
||||
const visualizerWindow = window.open(
|
||||
'',
|
||||
'visualizer',
|
||||
`width=${screenDimensions.width},height=${screenDimensions.height}`,
|
||||
);
|
||||
if (!visualizerWindow) {
|
||||
return;
|
||||
}
|
||||
visualizerWindow.onunload = () => {
|
||||
this.setState({visualizerWindow: null});
|
||||
};
|
||||
visualizerWindow.onresize = () => {
|
||||
this.setState({visualizerWindow: visualizerWindow});
|
||||
};
|
||||
visualizerWindow.onload = () => {
|
||||
this.setState({visualizerWindow: visualizerWindow});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
onDataValueChanged = (path: Array<string>, value: any) => {
|
||||
const id = this.state.inAXMode
|
||||
? this.state.selectedAXElement
|
||||
: this.state.selectedElement;
|
||||
this.client.call('setData', {
|
||||
id,
|
||||
path,
|
||||
value,
|
||||
ax: this.state.inAXMode,
|
||||
});
|
||||
};
|
||||
showFlipperADBar: boolean = false;
|
||||
|
||||
getScreenDimensions(): {width: number; height: number} | null {
|
||||
if (this.state.screenDimensions) {
|
||||
return this.state.screenDimensions;
|
||||
}
|
||||
|
||||
requestIdleCallback(() => {
|
||||
// Walk the layout tree from root node down until a node with width and height is found.
|
||||
// Assume these are the dimensions of the screen.
|
||||
let elementId = this.props.persistedState.rootElement;
|
||||
while (elementId != null) {
|
||||
const element = this.props.persistedState.elements[elementId];
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
if (element.data.View?.width) {
|
||||
break;
|
||||
}
|
||||
elementId = element.children[0];
|
||||
}
|
||||
if (elementId == null) {
|
||||
return null;
|
||||
}
|
||||
const element = this.props.persistedState.elements[elementId];
|
||||
if (
|
||||
element == null ||
|
||||
typeof element.data.View?.width != 'object' ||
|
||||
typeof element.data.View?.height != 'object'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const screenDimensions = {
|
||||
width: element.data.View?.width.value,
|
||||
height: element.data.View?.height.value,
|
||||
};
|
||||
this.setState({screenDimensions});
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const inspectorProps = {
|
||||
client: this.getClient(),
|
||||
inAlignmentMode: this.state.inAlignmentMode,
|
||||
selectedElement: this.state.selectedElement,
|
||||
selectedAXElement: this.state.selectedAXElement,
|
||||
setPersistedState: this.props.setPersistedState,
|
||||
persistedState: this.props.persistedState,
|
||||
onDataValueChanged: this.onDataValueChanged,
|
||||
searchResults: this.state.searchResults,
|
||||
};
|
||||
|
||||
let element: Element | null = null;
|
||||
const {selectedAXElement, selectedElement, inAXMode} = this.state;
|
||||
if (inAXMode && selectedAXElement) {
|
||||
element = this.props.persistedState.AXelements[selectedAXElement];
|
||||
} else if (selectedElement) {
|
||||
element = this.props.persistedState.elements[selectedElement];
|
||||
}
|
||||
if (!constants.IS_PUBLIC_BUILD && !this.showFlipperADBar) {
|
||||
this.showFlipperADBar = element != null && element.decoration === 'litho';
|
||||
}
|
||||
const inspector = (
|
||||
<Inspector
|
||||
{...inspectorProps}
|
||||
onSelect={selectedElement => this.setState({selectedElement})}
|
||||
showsSidebar={!this.state.inAXMode}
|
||||
/>
|
||||
);
|
||||
|
||||
const axInspector = this.state.inAXMode && (
|
||||
<Inspector
|
||||
{...inspectorProps}
|
||||
onSelect={selectedAXElement => this.setState({selectedAXElement})}
|
||||
showsSidebar={true}
|
||||
ax
|
||||
/>
|
||||
);
|
||||
|
||||
const divider = this.state.inAXMode && <VerticalRule />;
|
||||
|
||||
const showAnalyzeYogaPerformanceButton = GK.get('flipper_yogaperformance');
|
||||
|
||||
const screenDimensions = this.getScreenDimensions();
|
||||
|
||||
return (
|
||||
<FlexColumn grow={true}>
|
||||
{this.state.init && (
|
||||
<>
|
||||
<Toolbar>
|
||||
{!this.props.isArchivedDevice && (
|
||||
<ToolbarIcon
|
||||
onClick={this.onToggleTargetMode}
|
||||
title="Toggle target mode"
|
||||
icon="target"
|
||||
active={this.state.inTargetMode}
|
||||
/>
|
||||
)}
|
||||
{this.realClient.query.os === 'Android' && (
|
||||
<ToolbarIcon
|
||||
onClick={this.onToggleAXMode}
|
||||
title="Toggle to see the accessibility hierarchy"
|
||||
icon="accessibility"
|
||||
active={this.state.inAXMode}
|
||||
/>
|
||||
)}
|
||||
{!this.props.isArchivedDevice && (
|
||||
<ToolbarIcon
|
||||
onClick={this.onToggleAlignmentMode}
|
||||
title="Toggle AlignmentMode to show alignment lines"
|
||||
icon="borders"
|
||||
active={this.state.inAlignmentMode}
|
||||
/>
|
||||
)}
|
||||
{this.props.isArchivedDevice &&
|
||||
this.state.visualizerScreenshot && (
|
||||
<ToolbarIcon
|
||||
onClick={this.onToggleVisualizer}
|
||||
title="Toggle visual recreation of layout"
|
||||
icon="mobile"
|
||||
active={!!this.state.visualizerWindow}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Search
|
||||
client={this.getClient()}
|
||||
setPersistedState={this.props.setPersistedState}
|
||||
persistedState={this.props.persistedState}
|
||||
onSearchResults={searchResults =>
|
||||
this.setState({searchResults})
|
||||
}
|
||||
inAXMode={this.state.inAXMode}
|
||||
initialQuery={this.props.deepLinkPayload}
|
||||
/>
|
||||
</Toolbar>
|
||||
<FlexRow grow={true}>
|
||||
{inspector}
|
||||
{divider}
|
||||
{axInspector}
|
||||
</FlexRow>
|
||||
{this.showFlipperADBar && this.FlipperADBar()}
|
||||
<DetailSidebar>
|
||||
<InspectorSidebar
|
||||
client={this.getClient()}
|
||||
realClient={this.realClient}
|
||||
element={element}
|
||||
onValueChanged={this.onDataValueChanged}
|
||||
logger={this.props.logger}
|
||||
/>
|
||||
{showAnalyzeYogaPerformanceButton &&
|
||||
element &&
|
||||
element.decoration === 'litho' ? (
|
||||
<Button
|
||||
icon={'share-external'}
|
||||
compact={true}
|
||||
style={{marginTop: 8, marginRight: 12}}
|
||||
onClick={() => {
|
||||
this.props.selectPlugin('YogaPerformance', element!.id);
|
||||
}}>
|
||||
Analyze Yoga Performance
|
||||
</Button>
|
||||
) : null}
|
||||
</DetailSidebar>
|
||||
{this.state.visualizerWindow &&
|
||||
screenDimensions &&
|
||||
(this.state.visualizerScreenshot ? (
|
||||
<VisualizerPortal
|
||||
container={this.state.visualizerWindow.document.body}
|
||||
elements={this.props.persistedState.elements}
|
||||
highlightedElement={this.state.highlightedElement}
|
||||
screenshotURL={this.state.visualizerScreenshot}
|
||||
screenDimensions={screenDimensions}
|
||||
/>
|
||||
) : (
|
||||
'Loading...'
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
24
desktop/plugins/layout/package.json
Normal file
24
desktop/plugins/layout/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "Inspector",
|
||||
"version": "1.0.0",
|
||||
"main": "index.tsx",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"flipper-plugin"
|
||||
],
|
||||
"dependencies": {
|
||||
"deep-equal": "^2.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.debounce": "^4.0.8"
|
||||
},
|
||||
"title": "Layout",
|
||||
"icon": "target",
|
||||
"bugs": {
|
||||
"email": "oncall+flipper@xmail.facebook.com",
|
||||
"url": "https://fb.workplace.com/groups/flippersupport/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.clonedeep": "^4.5.6"
|
||||
}
|
||||
}
|
||||
279
desktop/plugins/layout/yarn.lock
Normal file
279
desktop/plugins/layout/yarn.lock
Normal file
@@ -0,0 +1,279 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/lodash.clonedeep@^4.5.6":
|
||||
version "4.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz#3b6c40a0affe0799a2ce823b440a6cf33571d32b"
|
||||
integrity sha512-cE1jYr2dEg1wBImvXlNtp0xDoS79rfEdGozQVgliDZj1uERH4k+rmEMTudP9b4VQ8O6nRb5gPqft0QzEQGMQgA==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*":
|
||||
version "4.14.138"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.138.tgz#34f52640d7358230308344e579c15b378d91989e"
|
||||
integrity sha512-A4uJgHz4hakwNBdHNPdxOTkYmXNgmUAKLbXZ7PKGslgeV0Mb8P3BlbYfPovExek1qnod4pDfRbxuzcVs3dlFLg==
|
||||
|
||||
deep-equal@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.1.tgz#fc12bbd6850e93212f21344748682ccc5a8813cf"
|
||||
integrity sha512-7Et6r6XfNW61CPPCIYfm1YPGSmh6+CliYeL4km7GWJcpX5LTAflGF8drLLR+MZX+2P3NZfAfSduutBbSWqER4g==
|
||||
dependencies:
|
||||
es-abstract "^1.16.3"
|
||||
es-get-iterator "^1.0.1"
|
||||
is-arguments "^1.0.4"
|
||||
is-date-object "^1.0.1"
|
||||
is-regex "^1.0.4"
|
||||
isarray "^2.0.5"
|
||||
object-is "^1.0.1"
|
||||
object-keys "^1.1.1"
|
||||
regexp.prototype.flags "^1.2.0"
|
||||
side-channel "^1.0.1"
|
||||
which-boxed-primitive "^1.0.1"
|
||||
which-collection "^1.0.0"
|
||||
|
||||
define-properties@^1.1.2, define-properties@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
|
||||
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
|
||||
dependencies:
|
||||
object-keys "^1.0.12"
|
||||
|
||||
es-abstract@^1.16.2, es-abstract@^1.16.3:
|
||||
version "1.16.3"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.3.tgz#52490d978f96ff9f89ec15b5cf244304a5bca161"
|
||||
integrity sha512-WtY7Fx5LiOnSYgF5eg/1T+GONaGmpvpPdCpSnYij+U2gDTL0UPfWrhDw7b2IYb+9NQJsYpCA0wOQvZfsd6YwRw==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
is-callable "^1.1.4"
|
||||
is-regex "^1.0.4"
|
||||
object-inspect "^1.7.0"
|
||||
object-keys "^1.1.1"
|
||||
string.prototype.trimleft "^2.1.0"
|
||||
string.prototype.trimright "^2.1.0"
|
||||
|
||||
es-abstract@^1.17.0-next.1:
|
||||
version "1.17.0-next.1"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.0-next.1.tgz#94acc93e20b05a6e96dacb5ab2f1cb3a81fc2172"
|
||||
integrity sha512-7MmGr03N7Rnuid6+wyhD9sHNE2n4tFSwExnU2lQl3lIo2ShXWGePY80zYaoMOmILWv57H0amMjZGHNzzGG70Rw==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
is-callable "^1.1.4"
|
||||
is-regex "^1.0.4"
|
||||
object-inspect "^1.7.0"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
string.prototype.trimleft "^2.1.0"
|
||||
string.prototype.trimright "^2.1.0"
|
||||
|
||||
es-get-iterator@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.0.2.tgz#bc99065aa8c98ce52bc86ab282dedbba4120e0b3"
|
||||
integrity sha512-ZHb4fuNK3HKHEOvDGyHPKf5cSWh/OvAMskeM/+21NMnTuvqFvz8uHatolu+7Kf6b6oK9C+3Uo1T37pSGPWv0MA==
|
||||
dependencies:
|
||||
es-abstract "^1.17.0-next.1"
|
||||
has-symbols "^1.0.1"
|
||||
is-arguments "^1.0.4"
|
||||
is-map "^2.0.0"
|
||||
is-set "^2.0.0"
|
||||
is-string "^1.0.4"
|
||||
isarray "^2.0.5"
|
||||
|
||||
es-to-primitive@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
|
||||
integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
|
||||
dependencies:
|
||||
is-callable "^1.1.4"
|
||||
is-date-object "^1.0.1"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
has-symbols@^1.0.0, has-symbols@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
|
||||
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
|
||||
|
||||
has@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
||||
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
is-arguments@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
|
||||
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
|
||||
|
||||
is-bigint@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
|
||||
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
|
||||
|
||||
is-boolean-object@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
|
||||
integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=
|
||||
|
||||
is-callable@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
|
||||
integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
|
||||
|
||||
is-date-object@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
|
||||
integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
|
||||
|
||||
is-map@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
|
||||
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
|
||||
|
||||
is-number-object@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
|
||||
integrity sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=
|
||||
|
||||
is-regex@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
|
||||
integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
|
||||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
is-set@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
|
||||
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
|
||||
|
||||
is-string@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
|
||||
integrity sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=
|
||||
|
||||
is-symbol@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
||||
integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
|
||||
dependencies:
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-weakmap@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
|
||||
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
|
||||
|
||||
is-weakset@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
|
||||
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
|
||||
|
||||
isarray@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
||||
|
||||
lodash.clonedeep@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
|
||||
|
||||
lodash@^4.17.15:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
object-inspect@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
|
||||
integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
|
||||
|
||||
object-is@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4"
|
||||
integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==
|
||||
|
||||
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
|
||||
object.assign@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
|
||||
integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
|
||||
dependencies:
|
||||
define-properties "^1.1.2"
|
||||
function-bind "^1.1.1"
|
||||
has-symbols "^1.0.0"
|
||||
object-keys "^1.0.11"
|
||||
|
||||
regexp.prototype.flags@^1.2.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
|
||||
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.0-next.1"
|
||||
|
||||
side-channel@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.1.tgz#4fb6c60e13bf4a69baf1b219c50b7feb87cf5c30"
|
||||
integrity sha512-KhfWUIMFxTnJ1HTWiHhzPZL6CVZubPUFWcaIWY4Fc/551CazpDodWWTVTeJI8AjsC/JpH4fW6hmDa10Dnd4lRg==
|
||||
dependencies:
|
||||
es-abstract "^1.16.2"
|
||||
object-inspect "^1.7.0"
|
||||
|
||||
string.prototype.trimleft@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
|
||||
integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
function-bind "^1.1.1"
|
||||
|
||||
string.prototype.trimright@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
|
||||
integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
function-bind "^1.1.1"
|
||||
|
||||
which-boxed-primitive@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
|
||||
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
|
||||
dependencies:
|
||||
is-bigint "^1.0.0"
|
||||
is-boolean-object "^1.0.0"
|
||||
is-number-object "^1.0.3"
|
||||
is-string "^1.0.4"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
which-collection@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.0.tgz#303d38022473f4b7048b529b45f6c842d8814269"
|
||||
integrity sha512-mG4RtFHE+17N2AxRNvBQ488oBjrhaOaI/G+soUaRJwdyDbu5zmqoAKPYBlY7Zd+QTwpfvInRLKo40feo2si1yA==
|
||||
dependencies:
|
||||
is-map "^2.0.0"
|
||||
is-set "^2.0.0"
|
||||
is-weakmap "^2.0.0"
|
||||
is-weakset "^2.0.0"
|
||||
Reference in New Issue
Block a user