convert Layout plugin
Summary: _typescript_ Reviewed By: passy Differential Revision: D17153997 fbshipit-source-id: 308a070b86430a9256beb93b4d3e5f8d5b6c6e52
This commit is contained in:
committed by
Facebook Github Bot
parent
705ba8eaa8
commit
ef2c6787fa
@@ -4,4 +4,15 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
export default [];
|
|
||||||
|
import {PluginClient, Client, ElementID} from 'flipper';
|
||||||
|
import {Logger} from 'src/fb-interfaces/Logger';
|
||||||
|
|
||||||
|
export default [] as Array<
|
||||||
|
(
|
||||||
|
client: PluginClient,
|
||||||
|
realClient: Client,
|
||||||
|
selectedNode: ElementID,
|
||||||
|
logger: Logger,
|
||||||
|
) => React.ReactNode
|
||||||
|
>;
|
||||||
|
|||||||
@@ -5,36 +5,36 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
ElementID,
|
ElementID,
|
||||||
Element,
|
Element,
|
||||||
PluginClient,
|
PluginClient,
|
||||||
|
ElementsInspector,
|
||||||
ElementSearchResultSet,
|
ElementSearchResultSet,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import {ElementsInspector} from 'flipper';
|
|
||||||
import {Component} from 'react';
|
import {Component} from 'react';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
import {PersistedState, ElementMap} from './';
|
||||||
import type {PersistedState, ElementMap} from './';
|
import React from 'react';
|
||||||
|
|
||||||
type GetNodesOptions = {
|
type GetNodesOptions = {
|
||||||
force?: boolean,
|
force?: boolean;
|
||||||
ax?: boolean,
|
ax?: boolean;
|
||||||
forAccessibilityEvent?: boolean,
|
forAccessibilityEvent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
ax?: boolean,
|
ax?: boolean;
|
||||||
client: PluginClient,
|
client: PluginClient;
|
||||||
showsSidebar: boolean,
|
showsSidebar: boolean;
|
||||||
inAlignmentMode?: boolean,
|
inAlignmentMode?: boolean;
|
||||||
selectedElement: ?ElementID,
|
selectedElement: ElementID | null | undefined;
|
||||||
selectedAXElement: ?ElementID,
|
selectedAXElement: ElementID | null | undefined;
|
||||||
onSelect: (ids: ?ElementID) => void,
|
onSelect: (ids: ElementID | null | undefined) => void;
|
||||||
onDataValueChanged: (path: Array<string>, value: any) => void,
|
onDataValueChanged: (path: Array<string>, value: any) => void;
|
||||||
setPersistedState: (state: $Shape<PersistedState>) => void,
|
setPersistedState: (state: Partial<PersistedState>) => void;
|
||||||
persistedState: PersistedState,
|
persistedState: PersistedState;
|
||||||
searchResults: ?ElementSearchResultSet,
|
searchResults: ElementSearchResultSet | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Inspector extends Component<Props> {
|
export default class Inspector extends Component<Props> {
|
||||||
@@ -76,8 +76,12 @@ export default class Inspector extends Component<Props> {
|
|||||||
const elements: Array<Element> = Object.values(
|
const elements: Array<Element> = Object.values(
|
||||||
this.props.persistedState.AXelements,
|
this.props.persistedState.AXelements,
|
||||||
);
|
);
|
||||||
return elements.find(i => i?.data?.Accessibility?.['accessibility-focused'])
|
const focusedElement = elements.find(i =>
|
||||||
?.id;
|
Boolean(
|
||||||
|
i.data.Accessibility && i.data.Accessibility['accessibility-focused'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return focusedElement ? focusedElement.id : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
getAXContextMenuExtensions = () =>
|
getAXContextMenuExtensions = () =>
|
||||||
@@ -106,7 +110,7 @@ export default class Inspector extends Component<Props> {
|
|||||||
({
|
({
|
||||||
nodes,
|
nodes,
|
||||||
}: {
|
}: {
|
||||||
nodes: Array<{id: ElementID, children: Array<ElementID>}>,
|
nodes: Array<{id: ElementID; children: Array<ElementID>}>;
|
||||||
}) => {
|
}) => {
|
||||||
const ids = nodes
|
const ids = nodes
|
||||||
.map(n => [n.id, ...(n.children || [])])
|
.map(n => [n.id, ...(n.children || [])])
|
||||||
@@ -154,7 +158,11 @@ export default class Inspector extends Component<Props> {
|
|||||||
selectedElement
|
selectedElement
|
||||||
];
|
];
|
||||||
if (newlySelectedElem) {
|
if (newlySelectedElem) {
|
||||||
this.props.onSelect(newlySelectedElem.extraInfo?.linkedNode);
|
this.props.onSelect(
|
||||||
|
newlySelectedElem.extraInfo
|
||||||
|
? newlySelectedElem.extraInfo.linkedNode
|
||||||
|
: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
!ax &&
|
!ax &&
|
||||||
@@ -166,7 +174,11 @@ export default class Inspector extends Component<Props> {
|
|||||||
selectedAXElement
|
selectedAXElement
|
||||||
];
|
];
|
||||||
if (newlySelectedAXElem) {
|
if (newlySelectedAXElem) {
|
||||||
this.props.onSelect(newlySelectedAXElem.extraInfo?.linkedNode);
|
this.props.onSelect(
|
||||||
|
newlySelectedAXElem.extraInfo
|
||||||
|
? newlySelectedAXElem.extraInfo.linkedNode
|
||||||
|
: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,11 +191,13 @@ export default class Inspector extends Component<Props> {
|
|||||||
(acc: ElementMap, element: Element) => {
|
(acc: ElementMap, element: Element) => {
|
||||||
acc[element.id] = {
|
acc[element.id] = {
|
||||||
...element,
|
...element,
|
||||||
expanded: this.elements()[element.id]?.expanded,
|
expanded: this.elements()[element.id]
|
||||||
|
? this.elements()[element.id].expanded
|
||||||
|
: false,
|
||||||
};
|
};
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
new Map(),
|
{},
|
||||||
);
|
);
|
||||||
this.props.setPersistedState({
|
this.props.setPersistedState({
|
||||||
[this.props.ax ? 'AXelements' : 'elements']: {
|
[this.props.ax ? 'AXelements' : 'elements']: {
|
||||||
@@ -193,17 +207,19 @@ export default class Inspector extends Component<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidate(ids: Array<ElementID>): Promise<Array<Element>> {
|
async invalidate(ids: Array<ElementID>): Promise<Array<Element>> {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
return this.getNodes(ids, {}).then((elements: Array<Element>) => {
|
const elements = await this.getNodes(ids, {});
|
||||||
const children = elements
|
const children = elements
|
||||||
.filter((element: Element) => this.elements()[element.id]?.expanded)
|
.filter(
|
||||||
.map((element: Element) => element.children)
|
(element: Element) =>
|
||||||
.reduce((acc, val) => acc.concat(val), []);
|
this.elements()[element.id] && this.elements()[element.id].expanded,
|
||||||
return this.invalidate(children);
|
)
|
||||||
});
|
.map((element: Element) => element.children)
|
||||||
|
.reduce((acc, val) => acc.concat(val), []);
|
||||||
|
return this.invalidate(children);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateElement(id: ElementID, data: Object) {
|
updateElement(id: ElementID, data: Object) {
|
||||||
@@ -225,7 +241,7 @@ export default class Inspector extends Component<Props> {
|
|||||||
// element has no children so we're as deep as we can be
|
// element has no children so we're as deep as we can be
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return this.getChildren(element.id, {}).then((elements: Array<Element>) => {
|
return this.getChildren(element.id, {}).then(() => {
|
||||||
if (element.children.length >= 2) {
|
if (element.children.length >= 2) {
|
||||||
// element has two or more children so we can stop expanding
|
// element has two or more children so we can stop expanding
|
||||||
return;
|
return;
|
||||||
@@ -245,32 +261,32 @@ export default class Inspector extends Component<Props> {
|
|||||||
return this.getNodes(this.elements()[id].children, options);
|
return this.getNodes(this.elements()[id].children, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodes(
|
async getNodes(
|
||||||
ids: Array<ElementID> = [],
|
ids: Array<ElementID> = [],
|
||||||
options: GetNodesOptions,
|
options: GetNodesOptions,
|
||||||
): Promise<Array<Element>> {
|
): Promise<Array<Element>> {
|
||||||
const {forAccessibilityEvent} = options;
|
|
||||||
|
|
||||||
if (ids.length > 0) {
|
if (ids.length > 0) {
|
||||||
return this.props.client
|
const {forAccessibilityEvent} = options;
|
||||||
.call(this.call().GET_NODES, {
|
const {
|
||||||
|
elements,
|
||||||
|
}: {elements: Array<Element>} = await this.props.client.call(
|
||||||
|
this.call().GET_NODES,
|
||||||
|
{
|
||||||
ids,
|
ids,
|
||||||
forAccessibilityEvent,
|
forAccessibilityEvent,
|
||||||
selected: false,
|
selected: false,
|
||||||
})
|
},
|
||||||
.then(({elements}) => {
|
);
|
||||||
elements.forEach(e => this.updateElement(e.id, e));
|
elements.forEach(e => this.updateElement(e.id, e));
|
||||||
return elements;
|
return elements;
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve([]);
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAndExpandPath(path: Array<ElementID>) {
|
async getAndExpandPath(path: Array<ElementID>) {
|
||||||
return Promise.all(path.map(id => this.getChildren(id, {}))).then(() => {
|
await Promise.all(path.map(id => this.getChildren(id, {})));
|
||||||
this.onElementSelected(path[path.length - 1]);
|
this.onElementSelected(path[path.length - 1]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onElementSelected = debounce((selectedKey: ElementID) => {
|
onElementSelected = debounce((selectedKey: ElementID) => {
|
||||||
@@ -278,7 +294,7 @@ export default class Inspector extends Component<Props> {
|
|||||||
this.props.onSelect(selectedKey);
|
this.props.onSelect(selectedKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
onElementHovered = debounce((key: ?ElementID) =>
|
onElementHovered = debounce((key: ElementID | null | undefined) =>
|
||||||
this.props.client.call(this.call().SET_HIGHLIGHTED, {
|
this.props.client.call(this.call().SET_HIGHLIGHTED, {
|
||||||
id: key,
|
id: key,
|
||||||
isAlignmentMode: this.props.inAlignmentMode,
|
isAlignmentMode: this.props.inAlignmentMode,
|
||||||
@@ -5,22 +5,21 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Element} from 'flipper';
|
|
||||||
import type {PluginClient} from 'flipper';
|
|
||||||
import type Client from '../../Client.tsx';
|
|
||||||
import type {Logger} from '../../fb-interfaces/Logger.tsx';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ManagedDataInspector,
|
ManagedDataInspector,
|
||||||
Panel,
|
Panel,
|
||||||
FlexCenter,
|
FlexCenter,
|
||||||
styled,
|
styled,
|
||||||
colors,
|
colors,
|
||||||
|
PluginClient,
|
||||||
SidebarExtensions,
|
SidebarExtensions,
|
||||||
|
Element,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
|
import Client from '../../Client';
|
||||||
|
import {Logger} from '../../fb-interfaces/Logger';
|
||||||
import {Component} from 'react';
|
import {Component} from 'react';
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
const deepEqual = require('deep-equal');
|
import React from 'react';
|
||||||
|
|
||||||
const NoData = styled(FlexCenter)({
|
const NoData = styled(FlexCenter)({
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -30,10 +29,10 @@ const NoData = styled(FlexCenter)({
|
|||||||
type OnValueChanged = (path: Array<string>, val: any) => void;
|
type OnValueChanged = (path: Array<string>, val: any) => void;
|
||||||
|
|
||||||
type InspectorSidebarSectionProps = {
|
type InspectorSidebarSectionProps = {
|
||||||
data: any,
|
data: any;
|
||||||
id: string,
|
id: string;
|
||||||
onValueChanged: ?OnValueChanged,
|
onValueChanged: OnValueChanged | null;
|
||||||
tooltips?: Object,
|
tooltips?: Object;
|
||||||
};
|
};
|
||||||
|
|
||||||
class InspectorSidebarSection extends Component<InspectorSidebarSectionProps> {
|
class InspectorSidebarSection extends Component<InspectorSidebarSectionProps> {
|
||||||
@@ -51,7 +50,7 @@ class InspectorSidebarSection extends Component<InspectorSidebarSectionProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
extractValue = (val: any, depth: number) => {
|
extractValue = (val: any, _depth: number) => {
|
||||||
if (val && val.__type__) {
|
if (val && val.__type__) {
|
||||||
return {
|
return {
|
||||||
mutable: Boolean(val.__mutable__),
|
mutable: Boolean(val.__mutable__),
|
||||||
@@ -84,14 +83,14 @@ class InspectorSidebarSection extends Component<InspectorSidebarSectionProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {|
|
type Props = {
|
||||||
element: ?Element,
|
element: Element | null;
|
||||||
tooltips?: Object,
|
tooltips?: Object;
|
||||||
onValueChanged: ?OnValueChanged,
|
onValueChanged: OnValueChanged | null;
|
||||||
client: PluginClient,
|
client: PluginClient;
|
||||||
realClient: Client,
|
realClient: Client;
|
||||||
logger: Logger,
|
logger: Logger;
|
||||||
|};
|
};
|
||||||
|
|
||||||
export default class Sidebar extends Component<Props> {
|
export default class Sidebar extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
@@ -115,12 +114,13 @@ export default class Sidebar extends Component<Props> {
|
|||||||
for (const key in element.data) {
|
for (const key in element.data) {
|
||||||
if (key === 'Extra Sections') {
|
if (key === 'Extra Sections') {
|
||||||
for (const extraSection in element.data[key]) {
|
for (const extraSection in element.data[key]) {
|
||||||
let data = element.data[key][extraSection];
|
const section = element.data[key][extraSection];
|
||||||
|
let data = {};
|
||||||
|
|
||||||
// data might be sent as stringified JSON, we want to parse it for a nicer persentation.
|
// data might be sent as stringified JSON, we want to parse it for a nicer persentation.
|
||||||
if (typeof data === 'string') {
|
if (typeof section === 'string') {
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(section);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// data was not a valid JSON, type is required to be an object
|
// data was not a valid JSON, type is required to be an object
|
||||||
console.error(
|
console.error(
|
||||||
@@ -128,6 +128,8 @@ export default class Sidebar extends Component<Props> {
|
|||||||
);
|
);
|
||||||
data = {};
|
data = {};
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
data = section;
|
||||||
}
|
}
|
||||||
sections.push(
|
sections.push(
|
||||||
<InspectorSidebarSection
|
<InspectorSidebarSection
|
||||||
@@ -5,15 +5,18 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Element, ElementID} from 'flipper';
|
import {Element} from 'flipper';
|
||||||
import type {PersistedState} from './index';
|
import {PersistedState} from './index';
|
||||||
import type {SearchResultTree} from './Search';
|
import {SearchResultTree} from './Search';
|
||||||
// $FlowFixMe
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
const propsForPersistedState = (
|
const propsForPersistedState = (
|
||||||
AXMode: boolean,
|
AXMode: boolean,
|
||||||
): {ROOT: string, ELEMENTS: string, ELEMENT: string} => {
|
): {
|
||||||
|
ROOT: 'rootAXElement' | 'rootElement';
|
||||||
|
ELEMENTS: 'AXelements' | 'elements';
|
||||||
|
ELEMENT: 'axElement' | 'element';
|
||||||
|
} => {
|
||||||
return {
|
return {
|
||||||
ROOT: AXMode ? 'rootAXElement' : 'rootElement',
|
ROOT: AXMode ? 'rootAXElement' : 'rootElement',
|
||||||
ELEMENTS: AXMode ? 'AXelements' : 'elements',
|
ELEMENTS: AXMode ? 'AXelements' : 'elements',
|
||||||
@@ -25,14 +28,14 @@ function constructSearchResultTree(
|
|||||||
node: Element,
|
node: Element,
|
||||||
isMatch: boolean,
|
isMatch: boolean,
|
||||||
children: Array<SearchResultTree>,
|
children: Array<SearchResultTree>,
|
||||||
AXMode: boolean,
|
_AXMode: boolean,
|
||||||
AXNode: ?Element,
|
AXNode: Element | null,
|
||||||
): SearchResultTree {
|
): SearchResultTree {
|
||||||
const searchResult = {
|
const searchResult = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
isMatch,
|
isMatch,
|
||||||
hasChildren: children.length > 0,
|
hasChildren: children.length > 0,
|
||||||
children: children.length > 0 ? children : null,
|
children: children.length > 0 ? children : [],
|
||||||
element: node,
|
element: node,
|
||||||
axElement: AXNode,
|
axElement: AXNode,
|
||||||
};
|
};
|
||||||
@@ -49,7 +52,7 @@ export function searchNodes(
|
|||||||
query: string,
|
query: string,
|
||||||
AXMode: boolean,
|
AXMode: boolean,
|
||||||
state: PersistedState,
|
state: PersistedState,
|
||||||
): ?SearchResultTree {
|
): SearchResultTree | null {
|
||||||
// Even if the axMode is true, we will have to search the normal elements too.
|
// Even if the axMode is true, we will have to search the normal elements too.
|
||||||
// The AXEelements will automatically populated in constructSearchResultTree
|
// The AXEelements will automatically populated in constructSearchResultTree
|
||||||
const elements = state[propsForPersistedState(false).ELEMENTS];
|
const elements = state[propsForPersistedState(false).ELEMENTS];
|
||||||
@@ -83,20 +86,19 @@ class ProxyArchiveClient {
|
|||||||
this.persistedState = cloneDeep(persistedState);
|
this.persistedState = cloneDeep(persistedState);
|
||||||
}
|
}
|
||||||
persistedState: PersistedState;
|
persistedState: PersistedState;
|
||||||
subscribe(method: string, callback: (params: any) => void): void {
|
subscribe(_method: string, _callback: (params: any) => void): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsMethod(method: string): Promise<boolean> {
|
supportsMethod(_method: string): Promise<boolean> {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
send(method: string, params?: Object): void {
|
send(_method: string, _params?: Object): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
call(method: string, params?: Object): Promise<any> {
|
call(method: string, paramaters?: {[key: string]: any}): Promise<any> {
|
||||||
const paramaters = params;
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'getRoot': {
|
case 'getRoot': {
|
||||||
const {rootElement} = this.persistedState;
|
const {rootElement} = this.persistedState;
|
||||||
@@ -118,7 +120,7 @@ class ProxyArchiveClient {
|
|||||||
}
|
}
|
||||||
const {ids} = paramaters;
|
const {ids} = paramaters;
|
||||||
const arr: Array<Element> = [];
|
const arr: Array<Element> = [];
|
||||||
for (const id: ElementID of ids) {
|
for (const id of ids) {
|
||||||
arr.push(this.persistedState.elements[id]);
|
arr.push(this.persistedState.elements[id]);
|
||||||
}
|
}
|
||||||
return Promise.resolve({elements: arr});
|
return Promise.resolve({elements: arr});
|
||||||
@@ -129,7 +131,7 @@ class ProxyArchiveClient {
|
|||||||
}
|
}
|
||||||
const {ids} = paramaters;
|
const {ids} = paramaters;
|
||||||
const arr: Array<Element> = [];
|
const arr: Array<Element> = [];
|
||||||
for (const id: ElementID of ids) {
|
for (const id of ids) {
|
||||||
arr.push(this.persistedState.AXelements[id]);
|
arr.push(this.persistedState.AXelements[id]);
|
||||||
}
|
}
|
||||||
return Promise.resolve({elements: arr});
|
return Promise.resolve({elements: arr});
|
||||||
@@ -148,7 +150,7 @@ class ProxyArchiveClient {
|
|||||||
new Error('query is not passed as a params to getSearchResults'),
|
new Error('query is not passed as a params to getSearchResults'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let element = {};
|
let element: Element;
|
||||||
if (axEnabled) {
|
if (axEnabled) {
|
||||||
if (!rootAXElement) {
|
if (!rootAXElement) {
|
||||||
return Promise.reject(new Error('rootAXElement is undefined'));
|
return Promise.reject(new Error('rootAXElement is undefined'));
|
||||||
@@ -5,10 +5,11 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {PluginClient, ElementSearchResultSet, Element} from 'flipper';
|
import {PersistedState, ElementMap} from './';
|
||||||
import type {PersistedState, ElementMap} from './';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
PluginClient,
|
||||||
|
ElementSearchResultSet,
|
||||||
|
Element,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
SearchBox,
|
SearchBox,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
@@ -17,28 +18,29 @@ import {
|
|||||||
colors,
|
colors,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import {Component} from 'react';
|
import {Component} from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
export type SearchResultTree = {|
|
export type SearchResultTree = {
|
||||||
id: string,
|
id: string;
|
||||||
isMatch: boolean,
|
isMatch: boolean;
|
||||||
hasChildren: boolean,
|
hasChildren: boolean;
|
||||||
children: ?Array<SearchResultTree>,
|
children: Array<SearchResultTree>;
|
||||||
element: Element,
|
element: Element;
|
||||||
axElement: ?Element, // Not supported in iOS
|
axElement: Element | null; // Not supported in iOS
|
||||||
|};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
client: PluginClient,
|
client: PluginClient;
|
||||||
inAXMode: boolean,
|
inAXMode: boolean;
|
||||||
onSearchResults: (searchResults: ElementSearchResultSet) => void,
|
onSearchResults: (searchResults: ElementSearchResultSet) => void;
|
||||||
setPersistedState: (state: $Shape<PersistedState>) => void,
|
setPersistedState: (state: Partial<PersistedState>) => void;
|
||||||
persistedState: PersistedState,
|
persistedState: PersistedState;
|
||||||
initialQuery: ?string,
|
initialQuery: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
value: string,
|
value: string;
|
||||||
outstandingSearchQuery: ?string,
|
outstandingSearchQuery: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoadingSpinner = styled(LoadingIndicator)({
|
const LoadingSpinner = styled(LoadingIndicator)({
|
||||||
@@ -53,16 +55,18 @@ export default class Search extends Component<Props, State> {
|
|||||||
outstandingSearchQuery: null,
|
outstandingSearchQuery: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
timer: TimeoutID;
|
timer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
onChange = (e: SyntheticInputEvent<>) => {
|
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
clearTimeout(this.timer);
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
const {value} = e.target;
|
const {value} = e.target;
|
||||||
this.setState({value});
|
this.setState({value});
|
||||||
this.timer = setTimeout(() => this.performSearch(value), 200);
|
this.timer = setTimeout(() => this.performSearch(value), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyDown = (e: SyntheticKeyboardEvent<>) => {
|
onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
this.performSearch(this.state.value);
|
this.performSearch(this.state.value);
|
||||||
}
|
}
|
||||||
@@ -73,7 +77,9 @@ export default class Search extends Component<Props, State> {
|
|||||||
const queryString = this.props.initialQuery
|
const queryString = this.props.initialQuery
|
||||||
? this.props.initialQuery
|
? this.props.initialQuery
|
||||||
: '';
|
: '';
|
||||||
clearTimeout(this.timer);
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
this.timer = setTimeout(() => this.performSearch(queryString), 200);
|
this.timer = setTimeout(() => this.performSearch(queryString), 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,8 +108,8 @@ export default class Search extends Component<Props, State> {
|
|||||||
results,
|
results,
|
||||||
query,
|
query,
|
||||||
}: {
|
}: {
|
||||||
results: ?SearchResultTree,
|
results: SearchResultTree | null;
|
||||||
query: string,
|
query: string;
|
||||||
},
|
},
|
||||||
axMode: boolean,
|
axMode: boolean,
|
||||||
) {
|
) {
|
||||||
@@ -159,13 +165,14 @@ export default class Search extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getElementsFromSearchResultTree(
|
getElementsFromSearchResultTree(
|
||||||
tree: ?SearchResultTree,
|
tree: SearchResultTree | null,
|
||||||
): Array<SearchResultTree> {
|
): Array<SearchResultTree> {
|
||||||
if (!tree) {
|
if (!tree) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let elements = [
|
let elements = [
|
||||||
{
|
{
|
||||||
|
children: [] as Array<SearchResultTree>,
|
||||||
id: tree.id,
|
id: tree.id,
|
||||||
isMatch: tree.isMatch,
|
isMatch: tree.isMatch,
|
||||||
hasChildren: Boolean(tree.children),
|
hasChildren: Boolean(tree.children),
|
||||||
@@ -6,13 +6,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Glyph, styled, colors} from 'flipper';
|
import {Glyph, styled, colors} from 'flipper';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {|
|
type Props = {
|
||||||
title: string,
|
title: string;
|
||||||
icon: string,
|
icon: string;
|
||||||
active: boolean,
|
active: boolean;
|
||||||
onClick: () => void,
|
onClick: () => void;
|
||||||
|};
|
};
|
||||||
|
|
||||||
const ToolbarIcon = styled('div')({
|
const ToolbarIcon = styled('div')({
|
||||||
marginRight: 9,
|
marginRight: 9,
|
||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
default as ProxyArchiveClient,
|
default as ProxyArchiveClient,
|
||||||
searchNodes,
|
searchNodes,
|
||||||
} from '../ProxyArchiveClient';
|
} from '../ProxyArchiveClient';
|
||||||
import type {PersistedState} from '../index';
|
import {PersistedState, ElementMap} from '../index';
|
||||||
import type {ElementID, Element} from 'flipper';
|
import {ElementID, Element} from 'flipper';
|
||||||
import type {SearchResultTree} from '../Search';
|
import {SearchResultTree} from '../Search';
|
||||||
|
|
||||||
function constructElement(
|
function constructElement(
|
||||||
id: string,
|
id: string,
|
||||||
@@ -49,7 +49,7 @@ function constructPersistedState(axMode: boolean): PersistedState {
|
|||||||
let state = constructPersistedState(false);
|
let state = constructPersistedState(false);
|
||||||
|
|
||||||
function populateChildren(state: PersistedState, axMode: boolean) {
|
function populateChildren(state: PersistedState, axMode: boolean) {
|
||||||
const elements = {};
|
const elements: ElementMap = {};
|
||||||
elements['root'] = constructElement('root', 'root view', [
|
elements['root'] = constructElement('root', 'root view', [
|
||||||
'child0',
|
'child0',
|
||||||
'child1',
|
'child1',
|
||||||
@@ -95,7 +95,7 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('test the searchNode for root in axMode false', async () => {
|
test('test the searchNode for root in axMode false', async () => {
|
||||||
const searchResult: ?SearchResultTree = await searchNodes(
|
const searchResult: SearchResultTree | null = await searchNodes(
|
||||||
state.elements['root'],
|
state.elements['root'],
|
||||||
'root',
|
'root',
|
||||||
false,
|
false,
|
||||||
@@ -106,7 +106,7 @@ test('test the searchNode for root in axMode false', async () => {
|
|||||||
id: 'root',
|
id: 'root',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.elements['root'],
|
element: state.elements['root'],
|
||||||
axElement: null,
|
axElement: null,
|
||||||
});
|
});
|
||||||
@@ -115,7 +115,7 @@ test('test the searchNode for root in axMode false', async () => {
|
|||||||
test('test the searchNode for root in axMode true', async () => {
|
test('test the searchNode for root in axMode true', async () => {
|
||||||
state = constructPersistedState(true);
|
state = constructPersistedState(true);
|
||||||
populateChildren(state, true);
|
populateChildren(state, true);
|
||||||
const searchResult: ?SearchResultTree = await searchNodes(
|
const searchResult: SearchResultTree | null = await searchNodes(
|
||||||
state.AXelements['root'],
|
state.AXelements['root'],
|
||||||
'RoOT',
|
'RoOT',
|
||||||
true,
|
true,
|
||||||
@@ -126,14 +126,14 @@ test('test the searchNode for root in axMode true', async () => {
|
|||||||
id: 'root',
|
id: 'root',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.AXelements['root'], // Even though AXElement exists, normal element will exist too
|
element: state.AXelements['root'], // Even though AXElement exists, normal element will exist too
|
||||||
axElement: state.AXelements['root'],
|
axElement: state.AXelements['root'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test the searchNode which matches just one child', async () => {
|
test('test the searchNode which matches just one child', async () => {
|
||||||
const searchResult: ?SearchResultTree = await searchNodes(
|
const searchResult: SearchResultTree | null = await searchNodes(
|
||||||
state.elements['root'],
|
state.elements['root'],
|
||||||
'child0_child0',
|
'child0_child0',
|
||||||
false,
|
false,
|
||||||
@@ -154,7 +154,7 @@ test('test the searchNode which matches just one child', async () => {
|
|||||||
id: 'child0_child0',
|
id: 'child0_child0',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.elements['child0_child0'],
|
element: state.elements['child0_child0'],
|
||||||
axElement: null,
|
axElement: null,
|
||||||
},
|
},
|
||||||
@@ -169,7 +169,7 @@ test('test the searchNode which matches just one child', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('test the searchNode for which matches multiple child', async () => {
|
test('test the searchNode for which matches multiple child', async () => {
|
||||||
const searchResult: ?SearchResultTree = await searchNodes(
|
const searchResult: SearchResultTree | null = await searchNodes(
|
||||||
state.elements['root'],
|
state.elements['root'],
|
||||||
'child0',
|
'child0',
|
||||||
false,
|
false,
|
||||||
@@ -190,7 +190,7 @@ test('test the searchNode for which matches multiple child', async () => {
|
|||||||
id: 'child0_child0',
|
id: 'child0_child0',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.elements['child0_child0'],
|
element: state.elements['child0_child0'],
|
||||||
axElement: null,
|
axElement: null,
|
||||||
},
|
},
|
||||||
@@ -198,7 +198,7 @@ test('test the searchNode for which matches multiple child', async () => {
|
|||||||
id: 'child0_child1',
|
id: 'child0_child1',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.elements['child0_child1'],
|
element: state.elements['child0_child1'],
|
||||||
axElement: null,
|
axElement: null,
|
||||||
},
|
},
|
||||||
@@ -215,7 +215,7 @@ test('test the searchNode for which matches multiple child', async () => {
|
|||||||
id: 'child1_child0',
|
id: 'child1_child0',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.elements['child1_child0'],
|
element: state.elements['child1_child0'],
|
||||||
axElement: null,
|
axElement: null,
|
||||||
},
|
},
|
||||||
@@ -231,7 +231,7 @@ test('test the searchNode for which matches multiple child', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('test the searchNode, it should not be case sensitive', async () => {
|
test('test the searchNode, it should not be case sensitive', async () => {
|
||||||
const searchResult: ?SearchResultTree = await searchNodes(
|
const searchResult: SearchResultTree | null = await searchNodes(
|
||||||
state.elements['root'],
|
state.elements['root'],
|
||||||
'ChIlD0',
|
'ChIlD0',
|
||||||
false,
|
false,
|
||||||
@@ -252,7 +252,7 @@ test('test the searchNode, it should not be case sensitive', async () => {
|
|||||||
id: 'child0_child0',
|
id: 'child0_child0',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.elements['child0_child0'],
|
element: state.elements['child0_child0'],
|
||||||
axElement: null,
|
axElement: null,
|
||||||
},
|
},
|
||||||
@@ -260,7 +260,7 @@ test('test the searchNode, it should not be case sensitive', async () => {
|
|||||||
id: 'child0_child1',
|
id: 'child0_child1',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.elements['child0_child1'],
|
element: state.elements['child0_child1'],
|
||||||
axElement: null,
|
axElement: null,
|
||||||
},
|
},
|
||||||
@@ -277,7 +277,7 @@ test('test the searchNode, it should not be case sensitive', async () => {
|
|||||||
id: 'child1_child0',
|
id: 'child1_child0',
|
||||||
isMatch: true,
|
isMatch: true,
|
||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
children: null,
|
children: [],
|
||||||
element: state.elements['child1_child0'],
|
element: state.elements['child1_child0'],
|
||||||
axElement: null,
|
axElement: null,
|
||||||
},
|
},
|
||||||
@@ -293,7 +293,7 @@ test('test the searchNode, it should not be case sensitive', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('test the searchNode for non existent query', async () => {
|
test('test the searchNode for non existent query', async () => {
|
||||||
const searchResult: ?SearchResultTree = await searchNodes(
|
const searchResult: SearchResultTree | null = await searchNodes(
|
||||||
state.elements['root'],
|
state.elements['root'],
|
||||||
'Unknown query',
|
'Unknown query',
|
||||||
false,
|
false,
|
||||||
@@ -5,20 +5,16 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
ElementID,
|
ElementID,
|
||||||
Element,
|
Element,
|
||||||
ElementSearchResultSet,
|
ElementSearchResultSet,
|
||||||
MiddlewareAPI,
|
MiddlewareAPI,
|
||||||
PluginClient,
|
PluginClient,
|
||||||
} from 'flipper';
|
|
||||||
|
|
||||||
import {
|
|
||||||
FlexColumn,
|
FlexColumn,
|
||||||
FlexRow,
|
FlexRow,
|
||||||
FlipperPlugin,
|
FlipperPlugin,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Sidebar,
|
|
||||||
DetailSidebar,
|
DetailSidebar,
|
||||||
VerticalRule,
|
VerticalRule,
|
||||||
Button,
|
Button,
|
||||||
@@ -29,37 +25,42 @@ import ToolbarIcon from './ToolbarIcon';
|
|||||||
import InspectorSidebar from './InspectorSidebar';
|
import InspectorSidebar from './InspectorSidebar';
|
||||||
import Search from './Search';
|
import Search from './Search';
|
||||||
import ProxyArchiveClient from './ProxyArchiveClient';
|
import ProxyArchiveClient from './ProxyArchiveClient';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
type State = {|
|
type State = {
|
||||||
init: boolean,
|
init: boolean;
|
||||||
inTargetMode: boolean,
|
inTargetMode: boolean;
|
||||||
inAXMode: boolean,
|
inAXMode: boolean;
|
||||||
inAlignmentMode: boolean,
|
inAlignmentMode: boolean;
|
||||||
selectedElement: ?ElementID,
|
selectedElement: ElementID | null | undefined;
|
||||||
selectedAXElement: ?ElementID,
|
selectedAXElement: ElementID | null | undefined;
|
||||||
searchResults: ?ElementSearchResultSet,
|
searchResults: ElementSearchResultSet | null;
|
||||||
|};
|
};
|
||||||
|
|
||||||
export type ElementMap = {[key: ElementID]: Element};
|
export type ElementMap = {[key: string]: Element};
|
||||||
|
|
||||||
export type PersistedState = {|
|
export type PersistedState = {
|
||||||
rootElement: ?ElementID,
|
rootElement: ElementID | null;
|
||||||
rootAXElement: ?ElementID,
|
rootAXElement: ElementID | null;
|
||||||
elements: ElementMap,
|
elements: ElementMap;
|
||||||
AXelements: ElementMap,
|
AXelements: ElementMap;
|
||||||
|};
|
};
|
||||||
|
|
||||||
export default class Layout extends FlipperPlugin<State, void, PersistedState> {
|
export default class Layout extends FlipperPlugin<State, any, PersistedState> {
|
||||||
static exportPersistedState = (
|
static exportPersistedState = async (
|
||||||
callClient: (string, ?Object) => Promise<Object>,
|
callClient: (
|
||||||
persistedState: ?PersistedState,
|
method: 'getAllNodes',
|
||||||
store: ?MiddlewareAPI,
|
) => Promise<{
|
||||||
): Promise<?PersistedState> => {
|
allNodes: PersistedState;
|
||||||
const defaultPromise = Promise.resolve(persistedState);
|
}>,
|
||||||
|
persistedState: PersistedState | undefined,
|
||||||
|
store: MiddlewareAPI | undefined,
|
||||||
|
): Promise<PersistedState | undefined> => {
|
||||||
if (!store) {
|
if (!store) {
|
||||||
return defaultPromise;
|
return persistedState;
|
||||||
}
|
}
|
||||||
return callClient('getAllNodes').then(({allNodes}) => allNodes);
|
const {allNodes} = await callClient('getAllNodes');
|
||||||
|
return allNodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultPersistedState = {
|
static defaultPersistedState = {
|
||||||
@@ -69,7 +70,7 @@ export default class Layout extends FlipperPlugin<State, void, PersistedState> {
|
|||||||
AXelements: {},
|
AXelements: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state: State = {
|
||||||
init: false,
|
init: false,
|
||||||
inTargetMode: false,
|
inTargetMode: false,
|
||||||
inAXMode: false,
|
inAXMode: false,
|
||||||
@@ -154,13 +155,12 @@ export default class Layout extends FlipperPlugin<State, void, PersistedState> {
|
|||||||
searchResults: this.state.searchResults,
|
searchResults: this.state.searchResults,
|
||||||
};
|
};
|
||||||
|
|
||||||
let element;
|
let element: Element | null = null;
|
||||||
if (this.state.inAXMode && this.state.selectedAXElement) {
|
const {selectedAXElement, selectedElement, inAXMode} = this.state;
|
||||||
element = this.props.persistedState.AXelements[
|
if (inAXMode && selectedAXElement) {
|
||||||
this.state.selectedAXElement
|
element = this.props.persistedState.AXelements[selectedAXElement];
|
||||||
];
|
} else if (selectedElement) {
|
||||||
} else if (this.state.selectedElement) {
|
element = this.props.persistedState.elements[selectedElement];
|
||||||
element = this.props.persistedState.elements[this.state.selectedElement];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const inspector = (
|
const inspector = (
|
||||||
@@ -247,7 +247,7 @@ export default class Layout extends FlipperPlugin<State, void, PersistedState> {
|
|||||||
compact={true}
|
compact={true}
|
||||||
style={{marginTop: 8, marginRight: 12}}
|
style={{marginTop: 8, marginRight: 12}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.props.selectPlugin('YogaPerformance', element.id);
|
this.props.selectPlugin('YogaPerformance', element!.id);
|
||||||
}}>
|
}}>
|
||||||
Analyze Yoga Performance
|
Analyze Yoga Performance
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Inspector",
|
"name": "Inspector",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.tsx",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
@@ -13,5 +13,8 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"email": "oncall+flipper@xmail.facebook.com",
|
"email": "oncall+flipper@xmail.facebook.com",
|
||||||
"url": "https://fb.workplace.com/groups/230455004101832/"
|
"url": "https://fb.workplace.com/groups/230455004101832/"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash.clonedeep": "^4.5.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,18 @@
|
|||||||
# yarn lockfile v1
|
# 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@^1.0.1:
|
deep-equal@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "system",
|
"module": "system",
|
||||||
"lib": ["es7", "dom"],
|
"lib": ["es7", "dom", "es2017"],
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user