Expose Panel and useLocalStorageState

Summary: Expose a Panel api from Sandy, which is quite similar to the old one, except that it uses Antd, and it will remember the users closed / open preference through sessions, a much requested feature.

Reviewed By: nikoant

Differential Revision: D27966607

fbshipit-source-id: 9b18df377215c1e6c5844d0bf972058c8c574cbb
This commit is contained in:
Michel Weststrate
2021-04-23 09:28:45 -07:00
committed by Facebook GitHub Bot
parent faf8588097
commit c005753018
12 changed files with 135 additions and 27 deletions

View File

@@ -14,7 +14,7 @@ import {Console, Hook} from 'console-feed';
import type {Methods} from 'console-feed/lib/definitions/Methods';
import type {Styles} from 'console-feed/lib/definitions/Styles';
import {createState, useValue} from 'flipper-plugin';
import {useLocalStorage} from '../utils/useLocalStorage';
import {useLocalStorageState} from 'flipper-plugin';
import {theme} from 'flipper-plugin';
import {useIsDarkMode} from '../utils/useIsDarkMode';
@@ -66,7 +66,7 @@ const defaultLogLevels: Methods[] = ['warn', 'error', 'table', 'assert'];
export function ConsoleLogs() {
const isDarkMode = useIsDarkMode();
const logs = useValue(logsAtom);
const [logLevels, setLogLevels] = useLocalStorage<Methods[]>(
const [logLevels, setLogLevels] = useLocalStorageState<Methods[]>(
'console-logs-loglevels',
defaultLogLevels,
);

View File

@@ -193,7 +193,7 @@ export {Rect} from './utils/geometry';
export {Logger} from './fb-interfaces/Logger';
export {getInstance as getLogger} from './fb-stubs/Logger';
export {callVSCode, getVSCodeUrl} from './utils/vscodeUtils';
export {useLocalStorage} from './utils/useLocalStorage';
export {useLocalStorageState as useLocalStorage} from 'flipper-plugin';
export {checkIdbIsInstalled} from './utils/iOSContainerUtility';
export {IDEFileResolver, IDEType} from './fb-stubs/IDEFileResolver';
export {renderMockFlipperWithPlugin} from './test-utils/createMockFlipperWithPlugin';

View File

@@ -12,13 +12,13 @@ import {Modal, Button, Checkbox, Typography} from 'antd';
import React, {useState} from 'react';
import constants from '../fb-stubs/constants';
import {NUX, Layout, theme} from 'flipper-plugin';
import {useLocalStorage} from '../utils/useLocalStorage';
import {useLocalStorageState} from 'flipper-plugin';
const {Title, Text, Link} = Typography;
export function SandyWelcomeScreen() {
const [dismissed, setDismissed] = useState(false);
const [showWelcomeScreen, setShowWelcomeScreen] = useLocalStorage(
const [showWelcomeScreen, setShowWelcomeScreen] = useLocalStorageState(
'flipper-sandy-show-welcome-screen',
true,
);

View File

@@ -38,6 +38,7 @@ test('Correct top level API exposed', () => {
"Layout",
"MarkerTimeline",
"NUX",
"Panel",
"TestUtils",
"Tracked",
"TrackingScope",
@@ -49,6 +50,7 @@ test('Correct top level API exposed', () => {
"sleep",
"styled",
"theme",
"useLocalStorageState",
"useLogger",
"useMemoize",
"usePlugin",

View File

@@ -89,6 +89,8 @@ export {
Interactive as _Interactive,
InteractiveProps as _InteractiveProps,
} from './ui/Interactive';
export {Panel} from './ui/Panel';
export {useLocalStorageState} from './utils/useLocalStorageState';
export {HighlightManager} from './ui/Highlight';
export {

View File

@@ -0,0 +1,85 @@
/**
* 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 * as React from 'react';
import {Collapse} from 'antd';
import {TrackingScope} from './Tracked';
import {useLocalStorageState} from '../utils/useLocalStorageState';
import {useCallback} from 'react';
import styled from '@emotion/styled';
import {Spacing, theme} from './theme';
import {Layout} from './Layout';
export const Panel: React.FC<{
title: string;
/**
* Whether the panel can be collapsed. Defaults to true
*/
collapsible?: boolean;
/**
* Initial state for panel if it is collapsable
*/
collapsed?: boolean;
pad: Spacing;
}> = (props) => {
const [collapsed, setCollapsed] = useLocalStorageState(
`panel:${props.title}:collapsed`,
props.collapsed,
);
const toggle = useCallback(() => {
console.log('click');
setCollapsed((c) => !c);
}, [setCollapsed]);
return (
<TrackingScope scope={props.title}>
<StyledCollapse
bordered={false}
activeKey={collapsed ? undefined : props.title}
onChange={toggle}>
<Collapse.Panel
key={props.title}
collapsible={props.collapsible ? undefined : 'disabled'}
header={props.title}>
<Layout.Container pad={props.pad}>{props.children}</Layout.Container>
</Collapse.Panel>
</StyledCollapse>
</TrackingScope>
);
};
Panel.defaultProps = {
collapsed: false,
collapsible: true,
};
const StyledCollapse = styled(Collapse)({
background: theme.backgroundDefault,
borderRadius: 0,
'& > .ant-collapse-item .ant-collapse-header': {
background: theme.backgroundWash,
paddingTop: theme.space.tiny,
paddingBottom: theme.space.tiny,
paddingLeft: 26,
fontWeight: 'bold',
'> .anticon': {
padding: `5px 0px`,
left: 8,
fontSize: '10px',
fontWeight: 'bold',
},
},
'& > .ant-collapse-item': {
borderBottom: 'none',
},
'& > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box': {
padding: 0,
},
});

View File

@@ -57,6 +57,14 @@ export function TrackingScope({
);
}
/**
* Gives the name of the current scope that is currently rendering.
* Typically the current plugin id, but can be further refined by using TrackingScopes
*/
export function useCurrentScopeName(): string {
return useContext(TrackingScopeContext);
}
export function Tracked({
events = 'onClick',
children,
@@ -73,7 +81,7 @@ export function Tracked({
action?: string;
children: React.ReactNode;
}): React.ReactElement {
const scope = useContext(TrackingScopeContext);
const scope = useCurrentScopeName();
return Children.map(children, (child: any) => {
if (!child || typeof child !== 'object') {
return child;

View File

@@ -10,7 +10,7 @@
import * as React from 'react';
import {render, fireEvent, act} from '@testing-library/react';
import {useLocalStorage} from '../useLocalStorage';
import {useLocalStorageState} from '../../utils/useLocalStorageState';
function TestComponent({
storageKey,
@@ -19,7 +19,7 @@ function TestComponent({
storageKey: string;
value: number;
}) {
const [current, setCurrent] = useLocalStorage(storageKey, value);
const [current, setCurrent] = useLocalStorageState(storageKey, value);
return (
<div>
@@ -67,13 +67,13 @@ test('it can store values', async () => {
expect((await res.findByTestId('value')).textContent).toEqual('2');
expect(storage).toMatchInlineSnapshot(`
Object {
"[useLocalStorage]x": "2",
"[useLocalStorage][Flipper]x": "2",
}
`);
});
test('it can read default from storage', async () => {
storage['[useLocalStorage]x'] = '3';
storage['[useLocalStorage][Flipper]x'] = '3';
const res = render(<TestComponent storageKey="x" value={1} />);
expect((await res.findByTestId('value')).textContent).toEqual('3');
@@ -84,7 +84,7 @@ test('it can read default from storage', async () => {
expect((await res.findByTestId('value')).textContent).toEqual('4');
expect(storage).toMatchInlineSnapshot(`
Object {
"[useLocalStorage]x": "4",
"[useLocalStorage][Flipper]x": "4",
}
`);
});
@@ -102,6 +102,6 @@ test('it does not allow changing key', async () => {
console.error = orig;
}
}).toThrowErrorMatchingInlineSnapshot(
`"The key passed to useLocalStorage should not be changed, 'x' -> 'y'"`,
`"[useAssertStableRef] An unstable reference was passed to this component as property 'key'. For optimization purposes we expect that this prop doesn't change over time. You might want to create the value passed to this prop outside the render closure, store it in useCallback / useMemo / useState, or set a key on the parent component"`,
);
});

View File

@@ -15,7 +15,7 @@ import {useRef} from 'react';
* (intentionally or accidentally)
*/
export const useAssertStableRef =
process.env.NODE_ENV === 'development'
process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'
? function useAssertStableRef(value: any, prop: string) {
const ref = useRef(value);
if (ref.current !== value) {

View File

@@ -8,25 +8,24 @@
*/
import {useState, useCallback} from 'react';
import {useCurrentScopeName} from '../ui/Tracked';
import {useAssertStableRef} from './useAssertStableRef';
export function useLocalStorage<T>(
export function useLocalStorageState<T>(
key: string,
initialValue: (() => T) | T,
): [T, (newState: T | ((current: T) => T)) => void] {
const [storedKey] = useState(key);
if (storedKey !== key) {
throw new Error(
`The key passed to useLocalStorage should not be changed, '${storedKey}' -> '${key}'`,
);
}
// Based on https://usehooks.com/useLocalStorage/ (with minor adaptions)
useAssertStableRef(key, 'key');
const scope = useCurrentScopeName();
const storageKey = `[useLocalStorage][${scope}]${key}`;
// Based on https://usehooks.com/useLocalStorage/ (with minor adaptions)
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem('[useLocalStorage]' + key);
const item = window.localStorage.getItem(storageKey);
// Parse stored json or if none return initialValue
return item
? JSON.parse(item)
@@ -48,14 +47,11 @@ export function useLocalStorage<T>(
const nextValue =
typeof value === 'function' ? value(storedValue) : value;
// Save to local storage
window.localStorage.setItem(
'[useLocalStorage]' + key,
JSON.stringify(nextValue),
);
window.localStorage.setItem(storageKey, JSON.stringify(nextValue));
return nextValue;
});
},
[key],
[storageKey],
);
return [storedValue, setValue];

View File

@@ -772,6 +772,19 @@ export function findMetroDevice(findMetroDevice, deviceList) {
```
### useLocalStorageState
Like `useState`, but the value will be stored in local storage under the given key, and read back upon initialization.
The hook signature is similar to `useState`, except that the first argument is the storage key.
The storage key will be scoped automatically to the current plugin and any additional tracking scopes. (See [`TrackingScope`](#trackingscope)).
```typescript
const [showWhitespace, setShowWhitespace] = useLocalStorageState(
`showWhitespace`,
true
);
```
## UI components
### Layout.*
@@ -792,6 +805,7 @@ See `View > Flipper Style Guide` inside the Flipper application for more details
### ElementSearchResultSet
### ElementsInspectorElement
### ElementsInspectorProps
### Panel
Coming soon.

View File

@@ -137,6 +137,7 @@ For conversion, the following table maps the old components to the new ones:
| `ManagedDataTable` | `DataTable` | `flipper-plugin` | Requires state to be provided by a [`createDataSource`](flipper-plugin.mdx#createdatasource) |
| `ManagedDataInspector` / `DataInspector` | `DataInspector` | `flipper-plugin` ||
| `ManagedElementInspector` / `ElementInspector` | `ElementInspector` | `flipper-plugin` ||
| `Panel` | `Panel` | `flipper-plugin` ||
Most other components, like `select` elements, tabs, date-pickers, etc etc can all be found in the Ant documentaiton.