Convert Seammammals plugin to Sandy {emoji:1f389}

Summary:
Converted the Seammammals plugin to use Sandy plugin infra (but the old components). Also updated lock file.

Added unittests have been added as well. The UI snapshots in there are kinda overkill, but nice demo that it works.

This completes the full roundtrip of the new Sandy infra, so this will be the last diff of this stack. I promise. I think.

Reviewed By: nikoant

Differential Revision: D22308265

fbshipit-source-id: 260e91a1951d486f6689880fe25281e80a71806a
This commit is contained in:
Michel Weststrate
2020-07-01 08:58:40 -07:00
committed by Facebook GitHub Bot
parent 581ddafd18
commit babc88e472
5 changed files with 243 additions and 122 deletions

View File

@@ -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 <div className="DetailsSidebar">{props.children}</div>;
});
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(`
<div
class="css-ok7d66-View-FlexBox-FlexColumn"
data-testid="Turtle"
>
<div
class="css-vgz97s"
style="background-image: url(http://turtle.png);"
/>
<span
class="css-b2jb3d-Text"
>
Turtle
</span>
</div>
`);
// 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();
});

View File

@@ -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 (
<Panel floating={false} heading={'Extras'}>
@@ -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<Events, {}>) {
const rows = createState<PersistedState>({});
const selectedID = createState<string | null>(null);
static persistedStateReducer<PersistedState>(
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 (
<SeaMammals.Container>
{Object.entries(persistedState).map(([id, row]) => (
<Card
{...row}
onSelect={() => this.setState({selectedID: id})}
selected={id === selectedID}
key={id}
/>
))}
<DetailSidebar>
{selectedID && renderSidebar(persistedState[selectedID])}
</DetailSidebar>
</SeaMammals.Container>
);
}
}
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 (
<Card.Container
onClick={this.props.onSelect}
selected={this.props.selected}>
<Card.Image style={{backgroundImage: `url(${this.props.url || ''})`}} />
<Card.Title>{this.props.title}</Card.Title>
</Card.Container>
);
}
return (
<Container>
{Object.entries(rows).map(([id, row]) => (
<Card
row={row}
onSelect={instance.setSelection}
selected={id === selectedID}
key={id}
/>
))}
<DetailSidebar>
{selectedID && renderSidebar(rows[selectedID])}
</DetailSidebar>
</Container>
);
}
type CardProps = {
onSelect: (id: number) => void;
selected: boolean;
row: Row;
};
const Card = memo(({row, selected, onSelect}: CardProps) => {
return (
<CardContainer
data-testid={row.title}
onClick={() => onSelect(row.id)}
selected={selected}>
<Image style={{backgroundImage: `url(${row.url || ''})`}} />
<Title>{row.title}</Title>
</CardContainer>
);
});
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',
});