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:
committed by
Facebook GitHub Bot
parent
faf8588097
commit
c005753018
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
85
desktop/flipper-plugin/src/ui/Panel.tsx
Normal file
85
desktop/flipper-plugin/src/ui/Panel.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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"`,
|
||||
);
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user