diff --git a/desktop/app/src/chrome/ConsoleLogs.tsx b/desktop/app/src/chrome/ConsoleLogs.tsx index 84e9c76fa..62abb9e62 100644 --- a/desktop/app/src/chrome/ConsoleLogs.tsx +++ b/desktop/app/src/chrome/ConsoleLogs.tsx @@ -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( + const [logLevels, setLogLevels] = useLocalStorageState( 'console-logs-loglevels', defaultLogLevels, ); diff --git a/desktop/app/src/index.tsx b/desktop/app/src/index.tsx index 0493b82a0..b944453f0 100644 --- a/desktop/app/src/index.tsx +++ b/desktop/app/src/index.tsx @@ -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'; diff --git a/desktop/app/src/sandy-chrome/SandyWelcomeScreen.tsx b/desktop/app/src/sandy-chrome/SandyWelcomeScreen.tsx index 911e8955c..b71b4df00 100644 --- a/desktop/app/src/sandy-chrome/SandyWelcomeScreen.tsx +++ b/desktop/app/src/sandy-chrome/SandyWelcomeScreen.tsx @@ -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, ); diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index 76f1d265e..db7958e63 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -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", diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 72d960219..7c1e1c6df 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -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 { diff --git a/desktop/flipper-plugin/src/ui/Panel.tsx b/desktop/flipper-plugin/src/ui/Panel.tsx new file mode 100644 index 000000000..cf14b9c4d --- /dev/null +++ b/desktop/flipper-plugin/src/ui/Panel.tsx @@ -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 ( + + + + {props.children} + + + + ); +}; + +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, + }, +}); diff --git a/desktop/flipper-plugin/src/ui/Tracked.tsx b/desktop/flipper-plugin/src/ui/Tracked.tsx index 80c3eb90a..88274b181 100644 --- a/desktop/flipper-plugin/src/ui/Tracked.tsx +++ b/desktop/flipper-plugin/src/ui/Tracked.tsx @@ -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; diff --git a/desktop/app/src/utils/__tests__/useLocalStorage.node.tsx b/desktop/flipper-plugin/src/ui/__tests__/useLocalStorage.node.tsx similarity index 79% rename from desktop/app/src/utils/__tests__/useLocalStorage.node.tsx rename to desktop/flipper-plugin/src/ui/__tests__/useLocalStorage.node.tsx index 79de77e2e..7b2d2051d 100644 --- a/desktop/app/src/utils/__tests__/useLocalStorage.node.tsx +++ b/desktop/flipper-plugin/src/ui/__tests__/useLocalStorage.node.tsx @@ -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 (
@@ -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(); 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"`, ); }); diff --git a/desktop/flipper-plugin/src/utils/useAssertStableRef.tsx b/desktop/flipper-plugin/src/utils/useAssertStableRef.tsx index cf72cdac6..a6271a778 100644 --- a/desktop/flipper-plugin/src/utils/useAssertStableRef.tsx +++ b/desktop/flipper-plugin/src/utils/useAssertStableRef.tsx @@ -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) { diff --git a/desktop/app/src/utils/useLocalStorage.tsx b/desktop/flipper-plugin/src/utils/useLocalStorageState.tsx similarity index 76% rename from desktop/app/src/utils/useLocalStorage.tsx rename to desktop/flipper-plugin/src/utils/useLocalStorageState.tsx index 45259c41a..463956687 100644 --- a/desktop/app/src/utils/useLocalStorage.tsx +++ b/desktop/flipper-plugin/src/utils/useLocalStorageState.tsx @@ -8,25 +8,24 @@ */ import {useState, useCallback} from 'react'; +import {useCurrentScopeName} from '../ui/Tracked'; +import {useAssertStableRef} from './useAssertStableRef'; -export function useLocalStorage( +export function useLocalStorageState( 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(() => { 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( 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]; diff --git a/docs/extending/flipper-plugin.mdx b/docs/extending/flipper-plugin.mdx index c5ddd814b..228bb2f89 100644 --- a/docs/extending/flipper-plugin.mdx +++ b/docs/extending/flipper-plugin.mdx @@ -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. diff --git a/docs/extending/sandy-migration.mdx b/docs/extending/sandy-migration.mdx index 3ca43095b..17427fa32 100644 --- a/docs/extending/sandy-migration.mdx +++ b/docs/extending/sandy-migration.mdx @@ -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.