diff --git a/desktop/flipper-plugin/src/__tests__/TestPlugin.tsx b/desktop/flipper-plugin/src/__tests__/TestPlugin.tsx index 53cd5d7bf..41a601c43 100644 --- a/desktop/flipper-plugin/src/__tests__/TestPlugin.tsx +++ b/desktop/flipper-plugin/src/__tests__/TestPlugin.tsx @@ -77,6 +77,5 @@ export function Component() { // @ts-expect-error api.bla; - // TODO N.b.: state updates won't be visible return

Hi from test plugin {count}

; } diff --git a/desktop/plugins/seamammals/package.json b/desktop/plugins/seamammals/package.json index 4dca8562a..ed89335c7 100644 --- a/desktop/plugins/seamammals/package.json +++ b/desktop/plugins/seamammals/package.json @@ -23,7 +23,8 @@ "prepack": "flipper-pkg lint && flipper-pkg bundle --production" }, "peerDependencies": { - "flipper": "0.49.0" + "flipper": "0.49.0", + "flipper-plugin": "0.49.0" }, "devDependencies": { "flipper": "0.49.0", diff --git a/desktop/plugins/seamammals/src/__tests__/seammammals.node.tsx b/desktop/plugins/seamammals/src/__tests__/seammammals.node.tsx new file mode 100644 index 000000000..41a0394a8 --- /dev/null +++ b/desktop/plugins/seamammals/src/__tests__/seammammals.node.tsx @@ -0,0 +1,122 @@ +/** + * 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 * as Flipper from 'flipper'; +// eslint-disable-next-line +import {act} from '@testing-library/react'; + +{ + // These mocks are needed because seammammals still uses Flipper in its UI implementation, + // so we need to mock some things + + // @ts-ignore + jest.spyOn(Flipper.DetailSidebar, 'type').mockImplementation((props) => { + return
{props.children}
; + }); + + const origRequestIdleCallback = window.requestIdleCallback; + const origCancelIdleCallback = window.cancelIdleCallback; + // @ts-ignore + window.requestIdleCallback = (fn: () => void) => { + // the synchronous implementation forces DataInspector to render in sync + act(fn); + }; + // @ts-ignore + window.cancelIdleCallback = clearImmediate; + afterAll(() => { + window.requestIdleCallback = origRequestIdleCallback; + window.cancelIdleCallback = origCancelIdleCallback; + }); +} + +import {TestUtils} from 'flipper-plugin'; +import * as MammalsPlugin from '../'; + +test('It can store rows', () => { + const {instance, ...plugin} = TestUtils.startPlugin(MammalsPlugin); + + expect(instance.rows.get()).toEqual({}); + expect(instance.selectedID.get()).toBeNull(); + + plugin.sendEvent('newRow', { + id: 1, + title: 'Dolphin', + url: 'http://dolphin.png', + }); + plugin.sendEvent('newRow', { + id: 2, + title: 'Turtle', + url: 'http://turtle.png', + }); + + expect(instance.rows.get()).toMatchInlineSnapshot(` + Object { + "1": Object { + "id": 1, + "title": "Dolphin", + "url": "http://dolphin.png", + }, + "2": Object { + "id": 2, + "title": "Turtle", + "url": "http://turtle.png", + }, + } + `); +}); + +test('It can have selection and render details', async () => { + const {instance, renderer, act, ...plugin} = TestUtils.renderPlugin( + MammalsPlugin, + ); + + expect(instance.rows.get()).toEqual({}); + expect(instance.selectedID.get()).toBeNull(); + + plugin.sendEvent('newRow', { + id: 1, + title: 'Dolphin', + url: 'http://dolphin.png', + }); + plugin.sendEvent('newRow', { + id: 2, + title: 'Turtle', + url: 'http://turtle.png', + }); + + // Dolphin card should now be visible + expect(await renderer.findByTestId('Dolphin')).not.toBeNull(); + // Let's assert the structure of the Turtle card as well + expect(await renderer.findByTestId('Turtle')).toMatchInlineSnapshot(` +
+
+ + Turtle + +
+ `); + // Nothing selected, so we should not have a sidebar + expect(renderer.queryAllByText('Extras').length).toBe(0); + + act(() => { + instance.setSelection(2); + }); + + // Sidebar should be visible now + expect(await renderer.findByText('Extras')).not.toBeNull(); +}); diff --git a/desktop/plugins/seamammals/src/index.tsx b/desktop/plugins/seamammals/src/index.tsx index 559a37a5e..174befdd4 100644 --- a/desktop/plugins/seamammals/src/index.tsx +++ b/desktop/plugins/seamammals/src/index.tsx @@ -11,14 +11,15 @@ import { Text, Panel, ManagedDataInspector, - FlipperPlugin, DetailSidebar, FlexRow, FlexColumn, styled, colors, } from 'flipper'; -import React from 'react'; +import React, {memo} from 'react'; + +import {FlipperClient, usePlugin, createState, useValue} from 'flipper-plugin'; type Id = number; @@ -28,6 +29,10 @@ type Row = { url: string; }; +type Events = { + newRow: Row; +}; + function renderSidebar(row: Row) { return ( @@ -44,105 +49,99 @@ type PersistedState = { [key: string]: Row; }; -export default class SeaMammals extends FlipperPlugin< - State, - any, - PersistedState -> { - static defaultPersistedState = {}; +export function plugin(client: FlipperClient) { + const rows = createState({}); + const selectedID = createState(null); - static persistedStateReducer( - persistedState: PersistedState, - method: string, - payload: Row, - ) { - if (method === 'newRow') { - return Object.assign({}, persistedState, { - [payload.id]: payload, - }); - } - return persistedState; - } - - static Container = styled(FlexRow)({ - backgroundColor: colors.macOSTitleBarBackgroundBlur, - flexWrap: 'wrap', - alignItems: 'flex-start', - alignContent: 'flex-start', - flexGrow: 1, - overflow: 'scroll', + client.onMessage('newRow', (row) => { + rows.update((draft) => { + draft[row.id] = row; + }); }); - state = { - selectedID: null as string | null, + function setSelection(id: number) { + selectedID.set('' + id); + } + + return { + rows, + selectedID, + setSelection, }; - - render() { - const {selectedID} = this.state; - const {persistedState} = this.props; - - return ( - - {Object.entries(persistedState).map(([id, row]) => ( - this.setState({selectedID: id})} - selected={id === selectedID} - key={id} - /> - ))} - - {selectedID && renderSidebar(persistedState[selectedID])} - - - ); - } } -class Card extends React.Component< - { - onSelect: () => void; - selected: boolean; - } & Row -> { - static Container = styled(FlexColumn)<{selected?: boolean}>((props) => ({ - margin: 10, - borderRadius: 5, - border: '2px solid black', - backgroundColor: colors.white, - borderColor: props.selected - ? colors.macOSTitleBarIconSelected - : colors.white, - padding: 0, - width: 150, - overflow: 'hidden', - boxShadow: '1px 1px 4px rgba(0,0,0,0.1)', - cursor: 'pointer', - })); +export function Component() { + const instance = usePlugin(plugin); + const selectedID = useValue(instance.selectedID); + const rows = useValue(instance.rows); - static Image = styled.div({ - backgroundSize: 'cover', - width: '100%', - paddingTop: '100%', - }); - - static Title = styled(Text)({ - fontSize: 14, - fontWeight: 'bold', - padding: '10px 5px', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - }); - - render() { - return ( - - - {this.props.title} - - ); - } + return ( + + {Object.entries(rows).map(([id, row]) => ( + + ))} + + {selectedID && renderSidebar(rows[selectedID])} + + + ); } + +type CardProps = { + onSelect: (id: number) => void; + selected: boolean; + row: Row; +}; +const Card = memo(({row, selected, onSelect}: CardProps) => { + return ( + onSelect(row.id)} + selected={selected}> + + {row.title} + + ); +}); + +const Container = styled(FlexRow)({ + backgroundColor: colors.macOSTitleBarBackgroundBlur, + flexWrap: 'wrap', + alignItems: 'flex-start', + alignContent: 'flex-start', + flexGrow: 1, + overflow: 'scroll', +}); + +const CardContainer = styled(FlexColumn)<{selected?: boolean}>((props) => ({ + margin: 10, + borderRadius: 5, + border: '2px solid black', + backgroundColor: colors.white, + borderColor: props.selected ? colors.macOSTitleBarIconSelected : colors.white, + padding: 0, + width: 150, + overflow: 'hidden', + boxShadow: '1px 1px 4px rgba(0,0,0,0.1)', + cursor: 'pointer', +})); + +const Image = styled.div({ + backgroundSize: 'cover', + width: '100%', + paddingTop: '100%', +}); + +const Title = styled(Text)({ + fontSize: 14, + fontWeight: 'bold', + padding: '10px 5px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); diff --git a/iOS/Tutorial/Podfile.lock b/iOS/Tutorial/Podfile.lock index 94117f405..0a5571670 100644 --- a/iOS/Tutorial/Podfile.lock +++ b/iOS/Tutorial/Podfile.lock @@ -5,7 +5,7 @@ PODS: - ComponentKit (0.30): - RenderCore (= 0.30) - Yoga (~> 1.14) - - Flipper (0.48.0): + - Flipper (0.49.0): - Flipper-Folly (~> 2.2) - Flipper-RSocket (~> 1.1) - Flipper-DoubleConversion (1.1.7) @@ -19,41 +19,41 @@ PODS: - Flipper-PeerTalk (0.0.4) - Flipper-RSocket (1.1.0): - Flipper-Folly (~> 2.2) - - FlipperKit (0.48.0): - - FlipperKit/Core (= 0.48.0) - - FlipperKit/Core (0.48.0): - - Flipper (~> 0.48.0) + - FlipperKit (0.49.0): + - FlipperKit/Core (= 0.49.0) + - FlipperKit/Core (0.49.0): + - Flipper (~> 0.49.0) - FlipperKit/CppBridge - FlipperKit/FBCxxFollyDynamicConvert - FlipperKit/FBDefines - FlipperKit/FKPortForwarding - - FlipperKit/CppBridge (0.48.0): - - Flipper (~> 0.48.0) - - FlipperKit/FBCxxFollyDynamicConvert (0.48.0): + - FlipperKit/CppBridge (0.49.0): + - Flipper (~> 0.49.0) + - FlipperKit/FBCxxFollyDynamicConvert (0.49.0): - Flipper-Folly (~> 2.2) - - FlipperKit/FBDefines (0.48.0) - - FlipperKit/FKPortForwarding (0.48.0): + - FlipperKit/FBDefines (0.49.0) + - FlipperKit/FKPortForwarding (0.49.0): - CocoaAsyncSocket (~> 7.6) - Flipper-PeerTalk (~> 0.0.4) - - FlipperKit/FlipperKitHighlightOverlay (0.48.0) - - FlipperKit/FlipperKitLayoutComponentKitSupport (0.48.0): + - FlipperKit/FlipperKitHighlightOverlay (0.49.0) + - FlipperKit/FlipperKitLayoutComponentKitSupport (0.49.0): - ComponentKit (~> 0.30) - FlipperKit/Core - FlipperKit/FlipperKitHighlightOverlay - FlipperKit/FlipperKitLayoutPlugin - FlipperKit/FlipperKitLayoutTextSearchable - RenderCore (~> 0.30) - - FlipperKit/FlipperKitLayoutPlugin (0.48.0): + - FlipperKit/FlipperKitLayoutPlugin (0.49.0): - FlipperKit/Core - FlipperKit/FlipperKitHighlightOverlay - FlipperKit/FlipperKitLayoutTextSearchable - YogaKit (~> 1.18) - - FlipperKit/FlipperKitLayoutTextSearchable (0.48.0) - - FlipperKit/FlipperKitNetworkPlugin (0.48.0): + - FlipperKit/FlipperKitLayoutTextSearchable (0.49.0) + - FlipperKit/FlipperKitNetworkPlugin (0.49.0): - FlipperKit/Core - - FlipperKit/FlipperKitUserDefaultsPlugin (0.48.0): + - FlipperKit/FlipperKitUserDefaultsPlugin (0.49.0): - FlipperKit/Core - - FlipperKit/SKIOSNetworkPlugin (0.48.0): + - FlipperKit/SKIOSNetworkPlugin (0.49.0): - FlipperKit/Core - FlipperKit/FlipperKitNetworkPlugin - OpenSSL-Universal (1.0.2.19): @@ -65,10 +65,10 @@ PODS: - Yoga (~> 1.14) DEPENDENCIES: - - FlipperKit (~> 0.48.0) - - FlipperKit/FlipperKitLayoutComponentKitSupport (~> 0.48.0) - - FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.48.0) - - FlipperKit/SKIOSNetworkPlugin (~> 0.48.0) + - FlipperKit (~> 0.49.0) + - FlipperKit/FlipperKitLayoutComponentKitSupport (~> 0.49.0) + - FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.49.0) + - FlipperKit/SKIOSNetworkPlugin (~> 0.49.0) SPEC REPOS: trunk: @@ -93,18 +93,18 @@ SPEC CHECKSUMS: CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845 CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f ComponentKit: c34da1ab3515cf18db68a4ba22c6599568d1de74 - Flipper: ff9243fb80717f8310290396bd9f79be7c1a8016 + Flipper: 78dc06980f2a5f7dbf28371a2800955430a95302 Flipper-DoubleConversion: 38631e41ef4f9b12861c67d17cb5518d06badc41 Flipper-Folly: c12092ea368353b58e992843a990a3225d4533c3 Flipper-Glog: 1dfd6abf1e922806c52ceb8701a3599a79a200a6 Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9 Flipper-RSocket: 64e7431a55835eb953b0bf984ef3b90ae9fdddd7 - FlipperKit: a6208c63e203ceda87d99414755cc53615b749f3 + FlipperKit: 26980c82fa2710f7ccc09eba394d1837258bd065 OpenSSL-Universal: 8b48cc0d10c1b2923617dfe5c178aa9ed2689355 RenderCore: d779c47622b313ce2d51bb36d084517af38b0dc1 Yoga: cff67a400f6b74dc38eb0bad4f156673d9aa980c YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: db8648db517152c1225d37f97f0083975dfe8623 +PODFILE CHECKSUM: eee0bd8fb136f35ceb29ff650a6d04bfb903dad1 COCOAPODS: 1.9.3