Introduce NUX element
Summary:
allow-large-files
This diff introces the `NUX` element that can be wrapped around any other element to give a first-time usage hint.
Hint dismissal is stored by taking a hash of the hint contents, and scoped per plugin.
Users can reset the 'read' status in the settings page
Example usage:
```
<NUX
title="Use bookmarks to directly navigate to a location in the app."
placement="right">
<Input addonAfter={<SettingOutlined />} defaultValue="mysite" />
</NUX>
```
Reviewed By: nikoant
Differential Revision: D24622276
fbshipit-source-id: 0265634f9ab50c32214b74f033f59482cd986f23
This commit is contained in:
committed by
Facebook GitHub Bot
parent
b8b9c4296a
commit
2b0e93a063
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import {FlexColumn, Button, styled, Text, FlexRow, Spacer} from '../ui';
|
||||
import React, {Component} from 'react';
|
||||
import React, {Component, useContext} from 'react';
|
||||
import {updateSettings, Action} from '../reducers/settings';
|
||||
import {
|
||||
Action as LauncherAction,
|
||||
@@ -28,6 +28,7 @@ import LauncherSettingsPanel from '../fb-stubs/LauncherSettingsPanel';
|
||||
import SandySettingsPanel from '../fb-stubs/SandySettingsPanel';
|
||||
import {reportUsage} from '../utils/metrics';
|
||||
import {Modal} from 'antd';
|
||||
import {Layout, NuxManagerContext} from 'flipper-plugin';
|
||||
|
||||
const Container = styled(FlexColumn)({
|
||||
padding: 20,
|
||||
@@ -311,6 +312,10 @@ class SettingsSheet extends Component<Props, State> {
|
||||
}}
|
||||
/>
|
||||
</ToggledSection>
|
||||
<Layout.Right center>
|
||||
<span>Reset all new user tooltips</span>
|
||||
<ResetTooltips />
|
||||
</Layout.Right>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -351,3 +356,16 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
|
||||
}),
|
||||
{updateSettings, updateLauncherSettings},
|
||||
)(SettingsSheet);
|
||||
|
||||
function ResetTooltips() {
|
||||
const nuxManager = useContext(NuxManagerContext);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
nuxManager.resetHints();
|
||||
}}>
|
||||
Reset
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {PopoverProvider} from './ui/components/PopoverProvider';
|
||||
import {initializeFlipperLibImplementation} from './utils/flipperLibImplementation';
|
||||
import {enableConsoleHook} from './chrome/ConsoleLogs';
|
||||
import {sideEffect} from './utils/sideEffect';
|
||||
import {NuxManagerContext, createNuxManager} from 'flipper-plugin';
|
||||
|
||||
if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') {
|
||||
// By default Node.JS has its internal certificate storage and doesn't use
|
||||
@@ -56,7 +57,9 @@ const AppFrame = () => (
|
||||
<ContextMenuProvider>
|
||||
<Provider store={store}>
|
||||
<CacheProvider value={cache}>
|
||||
<NuxManagerContext.Provider value={createNuxManager()}>
|
||||
<App logger={logger} />
|
||||
</NuxManagerContext.Provider>
|
||||
</CacheProvider>
|
||||
</Provider>
|
||||
</ContextMenuProvider>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
import React from 'react';
|
||||
import {Typography, Card, Table, Collapse, Button, Tabs} from 'antd';
|
||||
import {Layout} from '../ui';
|
||||
import {theme} from 'flipper-plugin';
|
||||
import {Layout, Link} from '../ui';
|
||||
import {NUX, theme} from 'flipper-plugin';
|
||||
import reactElementToJSXString from 'react-element-to-jsx-string';
|
||||
import {CodeOutlined} from '@ant-design/icons';
|
||||
|
||||
@@ -29,8 +29,8 @@ const demoStyle: Record<string, React.CSSProperties> = {
|
||||
|
||||
type PreviewProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
props: [string, string, string][];
|
||||
description?: React.ReactNode;
|
||||
props: [string, React.ReactNode, React.ReactNode][];
|
||||
demos: Record<string, React.ReactNode>;
|
||||
};
|
||||
|
||||
@@ -285,6 +285,31 @@ const demos: PreviewProps[] = [
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'NUX',
|
||||
description:
|
||||
'A component to provide a New-User-eXperience: Highlight new features to first time users. For tooltips that should stay available, use ToolTip from ANT design',
|
||||
props: [
|
||||
['title', 'string / React element', 'The tooltip contents'],
|
||||
[
|
||||
'placement',
|
||||
<>
|
||||
See{' '}
|
||||
<Link href="https://ant.design/components/tooltip/#components-tooltip-demo-placement">
|
||||
docs
|
||||
</Link>
|
||||
</>,
|
||||
'(optional) on which side to place the tooltip',
|
||||
],
|
||||
],
|
||||
demos: {
|
||||
'NUX example': (
|
||||
<NUX title="This button does something cool" placement="right">
|
||||
<Button>Hello world</Button>
|
||||
</NUX>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function ComponentPreview({title, demos, description, props}: PreviewProps) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {Alert, Input} from 'antd';
|
||||
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
|
||||
import {SettingOutlined} from '@ant-design/icons';
|
||||
import {Layout, Link, styled} from '../../ui';
|
||||
import {theme} from 'flipper-plugin';
|
||||
import {NUX, theme} from 'flipper-plugin';
|
||||
import {AppSelector} from './AppSelector';
|
||||
import {useStore} from '../../utils/useStore';
|
||||
import {PluginList} from './PluginList';
|
||||
@@ -49,7 +49,11 @@ export function AppInspect() {
|
||||
type="info"
|
||||
/>
|
||||
) : (
|
||||
<NUX
|
||||
title="Use bookmarks to directly navigate to a location in the app."
|
||||
placement="right">
|
||||
<Input addonAfter={<SettingOutlined />} defaultValue="mysite" />
|
||||
</NUX>
|
||||
)}
|
||||
{!isArchived && (
|
||||
<Toolbar gap>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {Badge, Button, Menu, Tooltip, Typography} from 'antd';
|
||||
import {InfoIcon, SidebarTitle} from '../LeftSidebar';
|
||||
import {PlusOutlined, MinusOutlined} from '@ant-design/icons';
|
||||
import {Glyph, Layout, styled} from '../../ui';
|
||||
import {theme} from 'flipper-plugin';
|
||||
import {theme, NUX} from 'flipper-plugin';
|
||||
import {useDispatch, useStore} from '../../utils/useStore';
|
||||
import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils';
|
||||
import {ClientPluginDefinition, DevicePluginDefinition} from '../../plugin';
|
||||
@@ -124,7 +124,10 @@ export const PluginList = memo(function PluginList() {
|
||||
</PluginGroup>
|
||||
|
||||
{!isArchived && (
|
||||
<PluginGroup key="metro" title="React Native">
|
||||
<PluginGroup
|
||||
key="metro"
|
||||
title="React Native"
|
||||
hint="The following plugins are exposed by the currently running Metro instance. Note that Metro might currently be connected to a different application or device than selected above.">
|
||||
{metroPlugins.map((plugin) => (
|
||||
<PluginEntry
|
||||
key={'metro' + plugin.id}
|
||||
@@ -163,7 +166,10 @@ export const PluginList = memo(function PluginList() {
|
||||
))}
|
||||
</PluginGroup>
|
||||
{!isArchived && (
|
||||
<PluginGroup key="disabled" title="Disabled">
|
||||
<PluginGroup
|
||||
key="disabled"
|
||||
title="Disabled"
|
||||
hint="This section shows the plugins that are currently disabled. If a pluign is enabled, you will be able to interact with it. If a plugin is disabled it won't consume resources in Flipper or in the connected application.">
|
||||
{disabledPlugins.map((plugin) => (
|
||||
<PluginEntry
|
||||
key={plugin.id}
|
||||
@@ -184,7 +190,10 @@ export const PluginList = memo(function PluginList() {
|
||||
</PluginGroup>
|
||||
)}
|
||||
{!isArchived && (
|
||||
<PluginGroup key="unavailable" title="Unavailable plugins">
|
||||
<PluginGroup
|
||||
key="unavailable"
|
||||
title="Unavailable plugins"
|
||||
hint="The plugins below are installed in Flipper, but not available for the selected device / application. Hover the plugin info box to find out why.">
|
||||
{unavailablePlugins.map(([plugin, reason]) => (
|
||||
<PluginEntry
|
||||
key={plugin.id}
|
||||
@@ -295,23 +304,40 @@ const PluginEntry = memo(function PluginEntry({
|
||||
const PluginGroup = memo(function PluginGroup({
|
||||
title,
|
||||
children,
|
||||
hint,
|
||||
...rest
|
||||
}: {title: string; children: React.ReactElement[]} & Record<string, any>) {
|
||||
}: {title: string; children: React.ReactElement[]; hint?: string} & Record<
|
||||
string,
|
||||
any
|
||||
>) {
|
||||
if (children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SubMenu
|
||||
{...rest}
|
||||
title={
|
||||
<Layout.Right center>
|
||||
<Text strong>{title}</Text>
|
||||
|
||||
let badge = (
|
||||
<Badge
|
||||
count={children.length}
|
||||
style={{
|
||||
marginRight: 20,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
if (hint) {
|
||||
badge = (
|
||||
<NUX title={hint} placement="right">
|
||||
{badge}
|
||||
</NUX>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SubMenu
|
||||
{...rest}
|
||||
title={
|
||||
<Layout.Right center>
|
||||
<Text strong>{title}</Text>
|
||||
|
||||
{badge}
|
||||
</Layout.Right>
|
||||
}>
|
||||
{children}
|
||||
|
||||
@@ -9,14 +9,20 @@
|
||||
"license": "MIT",
|
||||
"bugs": "https://github.com/facebook/flipper/issues",
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^7.26.3",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"immer": "^7.0.5"
|
||||
"react-element-to-jsx-string": "^14.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.3",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ant-design/icons": "^4.2.2",
|
||||
"@testing-library/dom": "^7.26.3",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"antd": "^4.8.0",
|
||||
"emotion": "^10.0.27",
|
||||
"immer": "^7.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"reset": "rimraf lib *.tsbuildinfo",
|
||||
"build": "tsc -b",
|
||||
|
||||
@@ -34,6 +34,7 @@ export {
|
||||
|
||||
export {theme} from './ui/theme';
|
||||
export {Layout} from './ui/Layout';
|
||||
export {NUX, NuxManagerContext, createNuxManager} from './ui/NUX';
|
||||
|
||||
// It's not ideal that this exists in flipper-plugin sources directly,
|
||||
// but is the least pain for plugin authors.
|
||||
|
||||
@@ -198,7 +198,7 @@ const SandySplitContainer = styled.div<{
|
||||
flex: 1,
|
||||
flexDirection: props.flexDirection,
|
||||
alignItems: props.center ? 'center' : 'stretch',
|
||||
overflow: 'hidden',
|
||||
overflow: props.center ? undefined : 'hidden', // only use overflow hidden in container mode, to avoid weird resizing issues
|
||||
'> :nth-child(1)': {
|
||||
flex: props.grow === 1 ? splitGrowStyle : splitFixedStyle,
|
||||
minWidth: props.grow === 1 ? 0 : undefined,
|
||||
|
||||
158
desktop/flipper-plugin/src/ui/NUX.tsx
Normal file
158
desktop/flipper-plugin/src/ui/NUX.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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 React, {createContext, useCallback, useContext} from 'react';
|
||||
import {Badge, Tooltip, Typography, Button} from 'antd';
|
||||
import styled from '@emotion/styled';
|
||||
import {SandyPluginInstance, theme} from 'flipper-plugin';
|
||||
import {keyframes} from 'emotion';
|
||||
import reactElementToJSXString from 'react-element-to-jsx-string';
|
||||
import {SandyPluginContext} from '../plugin/PluginContext';
|
||||
import {createState, useValue} from '../state/atom';
|
||||
import {SandyDevicePluginInstance} from '../plugin/DevicePlugin';
|
||||
import {Layout} from './Layout';
|
||||
import {BulbTwoTone} from '@ant-design/icons';
|
||||
import {createHash} from 'crypto';
|
||||
import type {TooltipPlacement} from 'antd/lib/tooltip';
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
type NuxManager = ReturnType<typeof createNuxManager>;
|
||||
|
||||
const storageKey = `FLIPPER_NUX_STATE`;
|
||||
|
||||
export function getNuxKey(
|
||||
elem: React.ReactNode,
|
||||
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
||||
) {
|
||||
return `${currentPlugin?.definition.id ?? 'flipper'}:${createHash('sha256')
|
||||
.update(reactElementToJSXString(elem))
|
||||
.digest('base64')}`;
|
||||
}
|
||||
|
||||
export function createNuxManager() {
|
||||
const ticker = createState(0);
|
||||
|
||||
let readMap: Record<string, boolean> = JSON.parse(
|
||||
window.localStorage.getItem(storageKey) || '{}',
|
||||
);
|
||||
|
||||
function save() {
|
||||
// trigger all Nux Elements to re-compute state
|
||||
ticker.set(ticker.get() + 1);
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(readMap, null, 2));
|
||||
}
|
||||
|
||||
return {
|
||||
markRead(
|
||||
elem: React.ReactNode,
|
||||
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
||||
): void {
|
||||
readMap[getNuxKey(elem, currentPlugin)] = true;
|
||||
save();
|
||||
},
|
||||
isRead(
|
||||
elem: React.ReactNode,
|
||||
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
||||
): boolean {
|
||||
return !!readMap[getNuxKey(elem, currentPlugin)];
|
||||
},
|
||||
resetHints(): void {
|
||||
readMap = {};
|
||||
save();
|
||||
},
|
||||
ticker,
|
||||
};
|
||||
}
|
||||
|
||||
const stubManager: NuxManager = {
|
||||
markRead() {},
|
||||
isRead() {
|
||||
return true;
|
||||
},
|
||||
resetHints() {},
|
||||
ticker: createState(0),
|
||||
};
|
||||
|
||||
export const NuxManagerContext = createContext<NuxManager>(stubManager);
|
||||
|
||||
/**
|
||||
* Creates a New-User-eXperience element; a lightbulb that will show the user new features
|
||||
*/
|
||||
export function NUX({
|
||||
children,
|
||||
title,
|
||||
placement,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
placement?: TooltipPlacement;
|
||||
}) {
|
||||
const manager = useContext(NuxManagerContext);
|
||||
const pluginInstance = useContext(SandyPluginContext);
|
||||
// changing the ticker will force `isRead` to be recomputed
|
||||
const _tick = useValue(manager.ticker);
|
||||
const isRead = manager.isRead(title, pluginInstance);
|
||||
const dismiss = useCallback(() => {
|
||||
manager.markRead(title, pluginInstance);
|
||||
}, [title, manager, pluginInstance]);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
count={
|
||||
isRead ? (
|
||||
0
|
||||
) : (
|
||||
<Tooltip
|
||||
placement={placement}
|
||||
color={theme.backgroundWash}
|
||||
title={
|
||||
<Layout.Container
|
||||
center
|
||||
gap
|
||||
pad
|
||||
style={{color: theme.textColorPrimary}}>
|
||||
<BulbTwoTone style={{fontSize: 24}} />
|
||||
<Text>{title}</Text>
|
||||
<Button size="small" type="default" onClick={dismiss}>
|
||||
Dismiss
|
||||
</Button>
|
||||
</Layout.Container>
|
||||
}>
|
||||
<Pulse />
|
||||
</Tooltip>
|
||||
)
|
||||
}>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const pulse = keyframes({
|
||||
'0%': {
|
||||
opacity: 0.2,
|
||||
},
|
||||
'100%': {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const Pulse = styled.div({
|
||||
cursor: 'pointer',
|
||||
background: theme.warningColor,
|
||||
animation: `${pulse} 2s infinite alternate`,
|
||||
borderRadius: 20,
|
||||
height: 12,
|
||||
width: 12,
|
||||
':hover': {
|
||||
opacity: `1 !important`,
|
||||
background: theme.errorColor,
|
||||
animationPlayState: 'paused',
|
||||
},
|
||||
});
|
||||
47
desktop/flipper-plugin/src/ui/__tests__/NUX.node.tsx
Normal file
47
desktop/flipper-plugin/src/ui/__tests__/NUX.node.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 {TestUtils} from 'flipper-plugin';
|
||||
import React from 'react';
|
||||
import {getNuxKey} from '../NUX';
|
||||
|
||||
test('nuxkey computation', () => {
|
||||
expect(getNuxKey('test')).toMatchInlineSnapshot(
|
||||
`"flipper:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`,
|
||||
);
|
||||
expect(getNuxKey('test')).toMatchInlineSnapshot(
|
||||
`"flipper:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`,
|
||||
);
|
||||
expect(getNuxKey('test2')).toMatchInlineSnapshot(
|
||||
`"flipper:YDA64iuZiGG847KPM+7BvnWKITyGyTwHbb6fVYwRx1I="`,
|
||||
);
|
||||
expect(getNuxKey(<div>bla</div>)).toMatchInlineSnapshot(
|
||||
`"flipper:myN0Mqqzs3fPwYDKGEQVG9XD9togJNWYJiy1VNQOf18="`,
|
||||
);
|
||||
expect(getNuxKey(<div>bla2</div>)).toMatchInlineSnapshot(
|
||||
`"flipper:B6kICeYCJMWeUThs5TWCLuiwCqzr5cWn67xXA4ET0bU="`,
|
||||
);
|
||||
});
|
||||
|
||||
test('nuxkey computation with plugin', () => {
|
||||
const res = TestUtils.startPlugin({
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
plugin() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
getNuxKey('test', (res as any)._backingInstance),
|
||||
).toMatchInlineSnapshot(
|
||||
`"TestPlugin:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`,
|
||||
);
|
||||
});
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
export const theme = {
|
||||
white: 'white', // use as counter color for primary
|
||||
black: 'black',
|
||||
primaryColor: 'var(--flipper-primary-color)',
|
||||
successColor: 'var(--flipper-success-color)',
|
||||
errorColor: 'var(--flipper-error-color)',
|
||||
|
||||
@@ -6803,11 +6803,6 @@ immer@^6.0.0:
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-6.0.9.tgz#b9dd69b8e69b3a12391e87db1e3ff535d1b26485"
|
||||
integrity sha512-SyCYnAuiRf67Lvk0VkwFvwtDoEiCMjeamnHvRfnVDyc7re1/rQrNxuL+jJ7lA3WvdC4uznrvbmm+clJ9+XXatg==
|
||||
|
||||
immer@^7.0.5:
|
||||
version "7.0.9"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.9.tgz#28e7552c21d39dd76feccd2b800b7bc86ee4a62e"
|
||||
integrity sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A==
|
||||
|
||||
immutable@^4.0.0-rc.12:
|
||||
version "4.0.0-rc.12"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.12.tgz#ca59a7e4c19ae8d9bf74a97bdf0f6e2f2a5d0217"
|
||||
@@ -7220,6 +7215,11 @@ is-plain-object@3.0.0:
|
||||
dependencies:
|
||||
isobject "^4.0.0"
|
||||
|
||||
is-plain-object@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b"
|
||||
integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==
|
||||
|
||||
is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
||||
@@ -10694,6 +10694,14 @@ react-element-to-jsx-string@^14.3.1:
|
||||
"@base2/pretty-print-object" "1.0.0"
|
||||
is-plain-object "3.0.0"
|
||||
|
||||
react-element-to-jsx-string@^14.3.2:
|
||||
version "14.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.2.tgz#c0000ed54d1f8b4371731b669613f2d4e0f63d5c"
|
||||
integrity sha512-WZbvG72cjLXAxV7VOuSzuHEaI3RHj10DZu8EcKQpkKcAj7+qAkG5XUeSdX5FXrA0vPrlx0QsnAzZEBJwzV0e+w==
|
||||
dependencies:
|
||||
"@base2/pretty-print-object" "1.0.0"
|
||||
is-plain-object "3.0.1"
|
||||
|
||||
react-inspector@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.0.tgz#45a325e15f33e595be5356ca2d3ceffb7d6b8c3a"
|
||||
|
||||
Reference in New Issue
Block a user