New multi app supporting sidebar navigation

Summary:
This diff changes the sidebar navigation, fixing a bunch of issues:
It will be possible to quickly switch again between the same plugins in multiple apps
No need to expand-and-check the app dropdown until the app is connected
No need for ugly fallback selections if some app connects faster than another one

Reviewed By: nikoant

Differential Revision: D19272701

fbshipit-source-id: 10f5fab42391014ef4a4a4c91c529d93f8bfb125
This commit is contained in:
Michel Weststrate
2020-01-06 08:47:21 -08:00
committed by Facebook Github Bot
parent 8152085111
commit 8cfe06d530
9 changed files with 540 additions and 416 deletions

View File

@@ -135,6 +135,7 @@ export class App extends React.Component<Props> {
}; };
render() { render() {
const useNewSidebar = GK.get('flipper_sidebar2');
return ( return (
<FlexColumn grow={true}> <FlexColumn grow={true}>
<TitleBar version={version} /> <TitleBar version={version} />
@@ -143,7 +144,7 @@ export class App extends React.Component<Props> {
<Sheet>{this.getSheet}</Sheet> <Sheet>{this.getSheet}</Sheet>
<FlexRow grow={true}> <FlexRow grow={true}>
{this.props.leftSidebarVisible && {this.props.leftSidebarVisible &&
(GK.get('flipper_sidebar2') ? <MainSidebar2 /> : <MainSidebar />)} (useNewSidebar ? <MainSidebar2 /> : <MainSidebar />)}
{this.props.staticView != null ? ( {this.props.staticView != null ? (
React.createElement(this.props.staticView, { React.createElement(this.props.staticView, {
logger: this.props.logger, logger: this.props.logger,

View File

@@ -10,30 +10,24 @@
import BaseDevice from '../../devices/BaseDevice'; import BaseDevice from '../../devices/BaseDevice';
import Client from '../../Client'; import Client from '../../Client';
import {UninitializedClient} from '../../UninitializedClient'; import {UninitializedClient} from '../../UninitializedClient';
import {FlipperBasePlugin, sortPluginsByName} from '../../plugin'; import {sortPluginsByName} from '../../plugin';
import {PluginNotification} from '../../reducers/notifications'; import {PluginNotification} from '../../reducers/notifications';
import {ActiveSheet} from '../../reducers/application'; import {ActiveSheet} from '../../reducers/application';
import {State as Store} from '../../reducers'; import {State as Store} from '../../reducers';
import { import {
Sidebar, Sidebar,
colors, colors,
brandColors,
Glyph, Glyph,
styled, styled,
FlexColumn,
GK, GK,
FlipperPlugin, FlipperPlugin,
FlipperDevicePlugin, FlipperDevicePlugin,
LoadingIndicator,
Button, Button,
StarButton,
Heading,
Spacer,
ArchivedDevice, ArchivedDevice,
SmallText, SmallText,
Info, Info,
} from 'flipper'; } from 'flipper';
import React, {Component, PureComponent, Fragment} from 'react'; import React, {PureComponent, Fragment} from 'react';
import { import {
selectPlugin, selectPlugin,
starPlugin, starPlugin,
@@ -55,6 +49,12 @@ import {
PluginsByCategory, PluginsByCategory,
PluginName, PluginName,
PluginIcon, PluginIcon,
Plugins,
ErrorIndicator,
Spinner,
CategoryName,
PluginSidebarListItem,
NoDevices,
} from './sidebarUtils'; } from './sidebarUtils';
const SidebarButton = styled(Button)<{small?: boolean}>(({small}) => ({ const SidebarButton = styled(Button)<{small?: boolean}>(({small}) => ({
@@ -71,78 +71,6 @@ const SidebarButton = styled(Button)<{small?: boolean}>(({small}) => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
})); }));
const CategoryName = styled(PluginName)({
color: colors.macOSSidebarSectionTitle,
textTransform: 'uppercase',
fontSize: '0.9em',
});
const Plugins = styled(FlexColumn)({
flexGrow: 1,
overflow: 'auto',
});
class PluginSidebarListItem extends Component<{
onClick: () => void;
isActive: boolean;
plugin: typeof FlipperBasePlugin;
app?: string | null | undefined;
helpRef?: any;
provided?: any;
onFavorite?: () => void;
starred?: boolean;
}> {
render() {
const {isActive, plugin, onFavorite, starred} = this.props;
const app = this.props.app || 'Facebook';
let iconColor: string | undefined = (brandColors as any)[app];
if (!iconColor) {
const pluginColors = [
colors.seaFoam,
colors.teal,
colors.lime,
colors.lemon,
colors.orange,
colors.tomato,
colors.cherry,
colors.pink,
colors.grape,
];
iconColor = pluginColors[parseInt(app, 36) % pluginColors.length];
}
return (
<ListItem active={isActive} onClick={this.props.onClick}>
<PluginIcon
isActive={isActive}
name={plugin.icon || 'apps'}
backgroundColor={iconColor}
color={colors.white}
/>
<PluginName>{plugin.title || plugin.id}</PluginName>
{starred !== undefined && (
<StarButton onStar={onFavorite!} starred={starred} />
)}
</ListItem>
);
}
}
const Spinner = centerInSidebar(LoadingIndicator);
const ErrorIndicator = centerInSidebar(Glyph);
function centerInSidebar(component: any) {
return styled(component)({
marginTop: '10px',
marginBottom: '10px',
marginLeft: 'auto',
marginRight: 'auto',
});
}
type OwnProps = {}; type OwnProps = {};
type StateFromProps = { type StateFromProps = {
@@ -168,6 +96,7 @@ type DispatchFromProps = {
selectedPlugin: string | null; selectedPlugin: string | null;
selectedApp: string | null; selectedApp: string | null;
deepLinkPayload: string | null; deepLinkPayload: string | null;
selectedDevice: BaseDevice;
}) => void; }) => void;
selectClient: typeof selectClient; selectClient: typeof selectClient;
setActiveSheet: (activeSheet: ActiveSheet) => void; setActiveSheet: (activeSheet: ActiveSheet) => void;
@@ -217,6 +146,7 @@ class MainSidebar extends PureComponent<Props, State> {
selectedPlugin: plugin.id, selectedPlugin: plugin.id,
selectedApp: null, selectedApp: null,
deepLinkPayload: null, deepLinkPayload: null,
selectedDevice,
}) })
} }
plugin={plugin} plugin={plugin}
@@ -270,16 +200,7 @@ class MainSidebar extends PureComponent<Props, State> {
))} ))}
</> </>
) : ( ) : (
<ListItem <NoDevices />
style={{
textAlign: 'center',
marginTop: 50,
flexDirection: 'column',
}}>
<Glyph name="mobile" size={32} color={colors.red}></Glyph>
<Spacer style={{height: 20}} />
<Heading>Select a device to get started</Heading>
</ListItem>
)} )}
</Plugins> </Plugins>
<MainSidebarUtils /> <MainSidebarUtils />
@@ -331,7 +252,12 @@ class MainSidebar extends PureComponent<Props, State> {
starred: boolean, starred: boolean,
onFavorite: (pluginId: string) => void, onFavorite: (pluginId: string) => void,
) { ) {
const {selectedPlugin, selectedApp, selectPlugin} = this.props; const {
selectedPlugin,
selectedApp,
selectPlugin,
selectedDevice,
} = this.props;
return groupPluginsByCategory(plugins).map(([category, plugins]) => ( return groupPluginsByCategory(plugins).map(([category, plugins]) => (
<Fragment key={category}> <Fragment key={category}>
{category && ( {category && (
@@ -348,6 +274,7 @@ class MainSidebar extends PureComponent<Props, State> {
selectedPlugin: plugin.id, selectedPlugin: plugin.id,
selectedApp: client.id, selectedApp: client.id,
deepLinkPayload: null, deepLinkPayload: null,
selectedDevice: selectedDevice!,
}) })
} }
plugin={plugin} plugin={plugin}

View File

@@ -10,144 +10,128 @@
import BaseDevice from '../../devices/BaseDevice'; import BaseDevice from '../../devices/BaseDevice';
import Client from '../../Client'; import Client from '../../Client';
import {UninitializedClient} from '../../UninitializedClient'; import {UninitializedClient} from '../../UninitializedClient';
import {FlipperBasePlugin, sortPluginsByName} from '../../plugin'; import {sortPluginsByName} from '../../plugin';
import {PluginNotification} from '../../reducers/notifications'; import {PluginNotification} from '../../reducers/notifications';
import {ActiveSheet} from '../../reducers/application'; import {ActiveSheet} from '../../reducers/application';
import {State as Store} from '../../reducers'; import {State as Store} from '../../reducers';
import { import {
Sidebar, Sidebar,
colors, colors,
brandColors,
Glyph, Glyph,
styled, styled,
FlexColumn,
GK, GK,
FlipperPlugin, FlipperPlugin,
FlipperDevicePlugin, FlipperDevicePlugin,
LoadingIndicator,
Button,
StarButton,
Heading,
Spacer,
ArchivedDevice, ArchivedDevice,
SmallText, SmallText,
Info, Info,
HBox,
} from 'flipper'; } from 'flipper';
import React, {Component, PureComponent, Fragment} from 'react'; import React, {
PureComponent,
Fragment,
memo,
useCallback,
useState,
} from 'react';
import NotificationScreen from '../NotificationScreen';
import { import {
selectPlugin, selectPlugin,
starPlugin, starPlugin as starPluginAction,
StaticView, StaticView,
setStaticView, setStaticView,
selectClient,
getAvailableClients, getAvailableClients,
getClientById, canBeDefaultDevice,
} from '../../reducers/connections'; } from '../../reducers/connections';
import {setActiveSheet} from '../../reducers/application'; import {setActiveSheet} from '../../reducers/application';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import SupportRequestFormManager from '../../fb-stubs/SupportRequestFormManager'; import SupportRequestFormManager from '../../fb-stubs/SupportRequestFormManager';
import SupportRequestDetails from '../../fb-stubs/SupportRequestDetails'; import SupportRequestDetails from '../../fb-stubs/SupportRequestDetails';
import MainSidebarUtils from './MainSidebarUtilsSection'; import MainSidebarUtilsSection from './MainSidebarUtilsSection';
import { import {
ListItem, ListItem,
isStaticViewActive,
FlipperPlugins,
PluginsByCategory,
PluginName, PluginName,
Plugins,
CategoryName,
PluginIcon, PluginIcon,
PluginSidebarListItem,
ErrorIndicator,
NoClients,
Spinner,
NoDevices,
getColorByApp,
} from './sidebarUtils'; } from './sidebarUtils';
const SidebarButton = styled(Button)<{small?: boolean}>(({small}) => ({ type FlipperPlugins = typeof FlipperPlugin[];
fontWeight: 'bold', type PluginsByCategory = [string, FlipperPlugins][];
fontSize: small ? 11 : 14,
width: '100%', type SectionLevel = 1 | 2 | 3;
overflow: 'hidden',
marginTop: small ? 0 : 20, const SidebarSectionButton = styled('button')<{
pointer: 'cursor', level: SectionLevel;
color: string;
}>(({level, color}) => ({
fontWeight: level === 3 ? 'normal' : 'bold',
borderRadius: 0,
border: 'none', border: 'none',
background: 'none', background: level === 1 ? colors.sectionHeaderBorder : 'transparent',
padding: 0, textAlign: level === 3 ? 'center' : 'left',
justifyContent: 'left', width: '100%',
whiteSpace: 'nowrap', fontSize: level === 3 ? 11 : 14,
color,
padding: `${level === 3 ? 0 : 8}px 10px 8px 9px`,
})); }));
const CategoryName = styled(PluginName)({ const SidebarSectionBody = styled('div')<{
color: colors.macOSSidebarSectionTitle, level: SectionLevel;
textTransform: 'uppercase', collapsed: boolean;
fontSize: '0.9em', }>(({collapsed}) => ({
}); flexShrink: 0,
overflow: 'hidden',
maxHeight: collapsed ? 0 : 1000, // might need increase if too many plugins...
transition: 'max-height 0.5s ease',
}));
const Plugins = styled(FlexColumn)({ const SidebarSection: React.FC<{
flexGrow: 1, defaultCollapsed?: boolean;
overflow: 'auto', title: string | React.ReactNode | ((collapsed: boolean) => React.ReactNode);
}); level: SectionLevel;
color?: string;
class PluginSidebarListItem extends Component<{ }> = ({children, title, level, color, defaultCollapsed}) => {
onClick: () => void; const [collapsed, setCollapsed] = useState(!!defaultCollapsed);
isActive: boolean; color = color || colors.macOSTitleBarIconActive;
plugin: typeof FlipperBasePlugin;
app?: string | null | undefined;
helpRef?: any;
provided?: any;
onFavorite?: () => void;
starred?: boolean;
}> {
render() {
const {isActive, plugin, onFavorite, starred} = this.props;
const app = this.props.app || 'Facebook';
let iconColor: string | undefined = (brandColors as any)[app];
if (!iconColor) {
const pluginColors = [
colors.seaFoam,
colors.teal,
colors.lime,
colors.lemon,
colors.orange,
colors.tomato,
colors.cherry,
colors.pink,
colors.grape,
];
iconColor = pluginColors[parseInt(app, 36) % pluginColors.length];
}
return ( return (
<ListItem active={isActive} onClick={this.props.onClick}> <>
<PluginIcon <SidebarSectionButton
isActive={isActive} onClick={() => setCollapsed(s => !s)}
name={plugin.icon || 'apps'} level={level}
backgroundColor={iconColor} color={color}>
color={colors.white} <HBox grow="left">
{typeof title === 'function' ? title(collapsed) : title}
{level < 3 && (
<Glyph
name={collapsed ? 'chevron-down' : 'chevron-up'}
size={12}
color={color}
/> />
<PluginName>{plugin.title || plugin.id}</PluginName>
{starred !== undefined && (
<StarButton onStar={onFavorite!} starred={starred} />
)} )}
</ListItem> </HBox>
</SidebarSectionButton>
<SidebarSectionBody level={level} collapsed={collapsed}>
{level === 1 && <div style={{height: 8}} />}
{children}
</SidebarSectionBody>
</>
); );
} };
}
const Spinner = centerInSidebar(LoadingIndicator);
const ErrorIndicator = centerInSidebar(Glyph);
function centerInSidebar(component: any) {
return styled(component)({
marginTop: '10px',
marginBottom: '10px',
marginLeft: 'auto',
marginRight: 'auto',
});
}
type OwnProps = {}; type OwnProps = {};
type StateFromProps = { type StateFromProps = {
numNotifications: number; numNotifications: number;
windowIsFocused: boolean; windowIsFocused: boolean;
devices: BaseDevice[];
selectedDevice: BaseDevice | null | undefined; selectedDevice: BaseDevice | null | undefined;
staticView: StaticView; staticView: StaticView;
selectedPlugin: string | null | undefined; selectedPlugin: string | null | undefined;
@@ -163,101 +147,104 @@ type StateFromProps = {
clientPlugins: Map<string, typeof FlipperPlugin>; clientPlugins: Map<string, typeof FlipperPlugin>;
}; };
type DispatchFromProps = { type SelectPlugin = (payload: {
selectPlugin: (payload: {
selectedPlugin: string | null; selectedPlugin: string | null;
selectedApp: string | null; selectedApp?: string | null;
deepLinkPayload: string | null; deepLinkPayload: string | null;
}) => void; selectedDevice: BaseDevice;
selectClient: typeof selectClient; }) => void;
type DispatchFromProps = {
selectPlugin: SelectPlugin;
setActiveSheet: (activeSheet: ActiveSheet) => void; setActiveSheet: (activeSheet: ActiveSheet) => void;
setStaticView: (payload: StaticView) => void; setStaticView: (payload: StaticView) => void;
starPlugin: typeof starPlugin; starPlugin: typeof starPluginAction;
}; };
type Props = OwnProps & StateFromProps & DispatchFromProps; type Props = OwnProps & StateFromProps & DispatchFromProps;
type State = { type State = {
showSupportForm: boolean;
showWatchDebugRoot: boolean;
showAllPlugins: boolean; showAllPlugins: boolean;
}; };
class MainSidebar extends PureComponent<Props, State> { class MainSidebar2 extends PureComponent<Props, State> {
state: State = { state: State = {
showSupportForm: GK.get('support_requests_v2'),
showWatchDebugRoot: GK.get('watch_team_flipper_clientless_access'),
showAllPlugins: false, showAllPlugins: false,
}; };
static getDerivedStateFromProps(props: Props, state: State) {
if (
!state.showSupportForm &&
props.staticView === SupportRequestFormManager
) {
// Show SupportForm option even when GK is false and support form is shown.
// That means the user has used deeplink to open support form.
// Once the variable is true, it will be true for the whole session.
return {showSupportForm: true};
}
return state;
}
render() { render() {
const { const devices = this.props.devices
selectedDevice, .slice()
selectClient, .sort((a, b) => a.title.localeCompare(b.title));
selectedPlugin, const renderableDevices = devices.filter(canBeDefaultDevice);
selectedApp,
selectPlugin,
uninitializedClients,
} = this.props;
const clients = getAvailableClients(selectedDevice, this.props.clients);
const client: Client | undefined = getClientById(clients, selectedApp);
return ( return (
<Sidebar position="left" width={200} backgroundColor={colors.light02}> <Sidebar position="left" width={200} backgroundColor={colors.light02}>
<Plugins> <Plugins>
{selectedDevice ? ( {renderableDevices.length ? (
<> renderableDevices.map(device => this.renderDevice(device))
<ListItem> ) : (
<SidebarButton>{selectedDevice.title}</SidebarButton> <NoDevices />
</ListItem> )}
{this.showArchivedDeviceDetails(selectedDevice)} </Plugins>
{selectedDevice.devicePlugins.map(pluginName => { <MainSidebarUtilsSection />
</Sidebar>
);
}
renderDevice(device: BaseDevice) {
const {
selectedPlugin,
selectPlugin,
uninitializedClients,
clientPlugins,
starPlugin,
userStarredPlugins,
selectedApp,
selectedDevice,
} = this.props;
const clients = getAvailableClients(device, this.props.clients);
return (
<SidebarSection title={device.title} key={device.serial} level={1}>
{this.showArchivedDeviceDetails(device)}
<SidebarSection level={2} title="Device" defaultCollapsed={true}>
{device.devicePlugins.map(pluginName => {
const plugin = this.props.devicePlugins.get(pluginName)!; const plugin = this.props.devicePlugins.get(pluginName)!;
return ( return (
<PluginSidebarListItem <PluginSidebarListItem
key={plugin.id} key={plugin.id}
isActive={plugin.id === selectedPlugin} isActive={
plugin.id === selectedPlugin && selectedDevice === device
}
onClick={() => onClick={() =>
selectPlugin({ selectPlugin({
selectedPlugin: plugin.id, selectedPlugin: plugin.id,
selectedApp: null, selectedApp: null,
deepLinkPayload: null, deepLinkPayload: null,
selectedDevice: device,
}) })
} }
plugin={plugin} plugin={plugin}
/> />
); );
})} })}
<ListItem> </SidebarSection>
<SidebarButton
title="Select an app to see available plugins"
compact={true}
dropdown={clients.map(c => ({
checked: client === c,
label: c.query.app,
type: 'checkbox',
click: () => selectClient(c.id),
}))}>
{clients.length === 0 ? (
<>
<Glyph
name="mobile-engagement"
size={16}
color={colors.red}
style={{marginRight: 10}}
/>
No clients connected
</>
) : !client ? (
'Select client'
) : (
<>
{client.query.app}
<Glyph
size={12}
name="chevron-down"
style={{marginLeft: 8}}
/>
</>
)}
</SidebarButton>
</ListItem>
{this.renderClientPlugins(client)}
{uninitializedClients.map(entry => ( {uninitializedClients.map(entry => (
<ListItem key={JSON.stringify(entry.client)}> <ListItem key={JSON.stringify(entry.client)}>
{entry.client.appName} {entry.client.appName}
@@ -268,27 +255,29 @@ class MainSidebar extends PureComponent<Props, State> {
)} )}
</ListItem> </ListItem>
))} ))}
</> {clients.length === 0 ? (
<NoClients />
) : ( ) : (
<ListItem clients.map(client => (
style={{ <PluginList
textAlign: 'center', device={device}
marginTop: 50, key={client.id}
flexDirection: 'column', client={client}
}}> clientPlugins={clientPlugins}
<Glyph name="mobile" size={32} color={colors.red}></Glyph> starPlugin={starPlugin}
<Spacer style={{height: 20}} /> userStarredPlugins={userStarredPlugins}
<Heading>Select a device to get started</Heading> selectedPlugin={selectedPlugin}
</ListItem> selectedApp={selectedApp}
selectPlugin={selectPlugin}
/>
))
)} )}
</Plugins> </SidebarSection>
<MainSidebarUtils />
</Sidebar>
); );
} }
showArchivedDeviceDetails(selectedDevice: BaseDevice) { showArchivedDeviceDetails(device: BaseDevice) {
if (!selectedDevice.isArchived || !selectedDevice.source) { if (!device.isArchived || !device.source) {
return null; return null;
} }
const {staticView, setStaticView} = this.props; const {staticView, setStaticView} = this.props;
@@ -296,18 +285,15 @@ class MainSidebar extends PureComponent<Props, State> {
staticView, staticView,
SupportRequestDetails, SupportRequestDetails,
); );
const showSupportForm =
GK.get('support_requests_v2') ||
isStaticViewActive(staticView, SupportRequestFormManager);
return ( return (
<> <>
<ListItem> <ListItem>
<Info type="warning" small> <Info type="warning" small>
{selectedDevice.source ? 'Imported device' : 'Archived device'} {device.source ? 'Imported device' : 'Archived device'}
</Info> </Info>
</ListItem> </ListItem>
{showSupportForm && {this.state.showSupportForm &&
(selectedDevice as ArchivedDevice).supportRequestDetails && ( (device as ArchivedDevice).supportRequestDetails && (
<ListItem <ListItem
active={supportRequestDetailsactive} active={supportRequestDetailsactive}
onClick={() => setStaticView(SupportRequestDetails)}> onClick={() => setStaticView(SupportRequestDetails)}>
@@ -325,127 +311,42 @@ class MainSidebar extends PureComponent<Props, State> {
); );
} }
renderPluginsByCategory( renderNotificationsEntry() {
client: Client, if (GK.get('flipper_disable_notifications')) {
plugins: FlipperPlugins,
starred: boolean,
onFavorite: (pluginId: string) => void,
) {
const {selectedPlugin, selectedApp, selectPlugin} = this.props;
return groupPluginsByCategory(plugins).map(([category, plugins]) => (
<Fragment key={category}>
{category && (
<ListItem>
<CategoryName>{category}</CategoryName>
</ListItem>
)}
{plugins.map(plugin => (
<PluginSidebarListItem
key={plugin.id}
isActive={plugin.id === selectedPlugin && selectedApp === client.id}
onClick={() =>
selectPlugin({
selectedPlugin: plugin.id,
selectedApp: client.id,
deepLinkPayload: null,
})
}
plugin={plugin}
app={client.query.app}
onFavorite={() => onFavorite(plugin.id)}
starred={starred}
/>
))}
</Fragment>
));
}
renderClientPlugins(client?: Client) {
if (!client) {
return null; return null;
} }
const onFavorite = (plugin: string) => {
this.props.starPlugin({ const active = isStaticViewActive(
selectedApp: client.query.app, this.props.staticView,
selectedPlugin: plugin, NotificationScreen,
});
};
const allPlugins = Array.from(this.props.clientPlugins.values()).filter(
(p: typeof FlipperPlugin) => client.plugins.indexOf(p.id) > -1,
); );
const favoritePlugins: FlipperPlugins = getFavoritePlugins(
allPlugins,
this.props.userStarredPlugins[client.query.app],
true,
);
const showAllPlugins =
this.state.showAllPlugins ||
favoritePlugins.length === 0 ||
// If the plugin is part of the hidden section, make sure sidebar is expanded
(client.plugins.includes(this.props.selectedPlugin!) &&
!favoritePlugins.find(
plugin => plugin.id === this.props.selectedPlugin,
));
return ( return (
<> <ListItem
{favoritePlugins.length === 0 ? ( active={active}
<ListItem> onClick={() => this.props.setStaticView(NotificationScreen)}
<SmallText center>Star your favorite plugins!</SmallText>
</ListItem>
) : (
<>
{this.renderPluginsByCategory(
client,
favoritePlugins,
true,
onFavorite,
)}
<ListItem>
<SidebarButton
small
compact
onClick={() =>
this.setState(state => ({
...state,
showAllPlugins: !state.showAllPlugins,
}))
}>
{showAllPlugins ? 'Show less' : 'Show more'}
<Glyph
size={8}
name={showAllPlugins ? 'chevron-up' : 'chevron-down'}
style={{ style={{
marginLeft: 4, borderTop: `1px solid ${colors.blackAlpha10}`,
}}
/>
</SidebarButton>
</ListItem>
</>
)}
<div
style={{
flex: 'auto' /*scroll this region, not the entire thing*/,
overflow: 'auto',
height: 'auto',
}}> }}>
{showAllPlugins <PluginIcon
? this.renderPluginsByCategory( color={colors.light50}
client, name={this.props.numNotifications > 0 ? 'bell' : 'bell-null'}
getFavoritePlugins( isActive={active}
allPlugins, />
this.props.userStarredPlugins[client.query.app], <PluginName count={this.props.numNotifications} isActive={active}>
false, Notifications
), </PluginName>
false, </ListItem>
onFavorite,
)
: null}
</div>
</>
); );
} }
} }
function isStaticViewActive(
current: StaticView,
selected: StaticView,
): boolean {
return current && selected && current === selected;
}
function getFavoritePlugins( function getFavoritePlugins(
allPlugins: FlipperPlugins, allPlugins: FlipperPlugins,
starredPlugins: undefined | string[], starredPlugins: undefined | string[],
@@ -481,6 +382,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({ ({
application: {windowIsFocused}, application: {windowIsFocused},
connections: { connections: {
devices,
selectedDevice, selectedDevice,
selectedPlugin, selectedPlugin,
selectedApp, selectedApp,
@@ -499,6 +401,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
).length; ).length;
})(), })(),
windowIsFocused, windowIsFocused,
devices,
selectedDevice, selectedDevice,
staticView, staticView,
selectedPlugin, selectedPlugin,
@@ -511,9 +414,167 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
}), }),
{ {
selectPlugin, selectPlugin,
selectClient,
setStaticView, setStaticView,
setActiveSheet, setActiveSheet,
starPlugin, starPlugin: starPluginAction,
}, },
)(MainSidebar); )(MainSidebar2);
const PluginList = memo(function PluginList({
client,
device,
clientPlugins,
starPlugin,
userStarredPlugins,
selectedPlugin,
selectedApp,
selectPlugin,
}: {
client: Client;
device: BaseDevice;
clientPlugins: Map<string, typeof FlipperPlugin>;
starPlugin: typeof starPluginAction;
userStarredPlugins: Store['connections']['userStarredPlugins'];
selectedPlugin?: null | string;
selectPlugin: SelectPlugin;
selectedApp?: null | string;
}) {
const onFavorite = useCallback(
(plugin: string) => {
starPlugin({
selectedApp: client.query.app,
selectedPlugin: plugin,
});
},
[client],
);
const allPlugins = Array.from(clientPlugins.values()).filter(
(p: typeof FlipperPlugin) => client.plugins.indexOf(p.id) > -1,
);
const favoritePlugins: FlipperPlugins = getFavoritePlugins(
allPlugins,
userStarredPlugins[client.query.app],
true,
);
const selectedNonFavoritePlugin =
selectedApp === client.id &&
client.plugins.includes(selectedPlugin!) &&
!favoritePlugins.find(plugin => plugin.id === selectedPlugin);
const allPluginsStarred = favoritePlugins.length === allPlugins.length;
return (
<SidebarSection
level={2}
key={client.id}
title={client.query.app}
color={getColorByApp(client.query.app)}>
{favoritePlugins.length === 0 ? (
<ListItem>
<SmallText center>Star your favorite plugins!</SmallText>
</ListItem>
) : (
<PluginsByCategory
client={client}
device={device}
plugins={favoritePlugins}
starred={true}
onFavorite={onFavorite}
selectedPlugin={selectedPlugin}
selectedApp={selectedApp}
selectPlugin={selectPlugin}
/>
)}
{!allPluginsStarred && (
<SidebarSection
level={3}
color={colors.light20}
defaultCollapsed={
favoritePlugins.length > 0 && !selectedNonFavoritePlugin
}
title={collapsed => (
<div>
{collapsed ? 'All plugins…' : 'Show less'}
<Glyph
color={colors.light20}
size={8}
name={collapsed ? 'chevron-down' : 'chevron-up'}
style={{
marginLeft: 4,
}}
/>
</div>
)}>
<PluginsByCategory
client={client}
device={device}
plugins={getFavoritePlugins(
allPlugins,
userStarredPlugins[client.query.app],
false,
)}
starred={false}
onFavorite={onFavorite}
selectedPlugin={selectedPlugin}
selectedApp={selectedApp}
selectPlugin={selectPlugin}
/>
</SidebarSection>
)}
</SidebarSection>
);
});
const PluginsByCategory = memo(function PluginsByCategory({
client,
plugins,
starred,
onFavorite,
selectedPlugin,
selectedApp,
selectPlugin,
device,
}: {
client: Client;
device: BaseDevice;
plugins: FlipperPlugins;
starred: boolean;
selectedPlugin?: null | string;
selectedApp?: null | string;
onFavorite: (pluginId: string) => void;
selectPlugin: SelectPlugin;
}) {
return (
<>
{groupPluginsByCategory(plugins).map(([category, plugins]) => (
<Fragment key={category}>
{category && (
<ListItem>
<CategoryName>{category}</CategoryName>
</ListItem>
)}
{plugins.map(plugin => (
<PluginSidebarListItem
key={plugin.id}
isActive={
plugin.id === selectedPlugin && selectedApp === client.id
}
onClick={() =>
selectPlugin({
selectedPlugin: plugin.id,
selectedApp: client.id,
deepLinkPayload: null,
selectedDevice: device,
})
}
plugin={plugin}
app={client.query.app}
onFavorite={() => onFavorite(plugin.id)}
starred={starred}
/>
))}
</Fragment>
))}
</>
);
});

View File

@@ -7,8 +7,22 @@
* @format * @format
*/ */
import {FlexBox, colors, Text, Glyph, styled, FlipperPlugin} from 'flipper'; import {
import React from 'react'; FlexBox,
colors,
Text,
Glyph,
styled,
FlipperPlugin,
FlexColumn,
LoadingIndicator,
FlipperBasePlugin,
StarButton,
brandColors,
Spacer,
Heading,
} from 'flipper';
import React, {Component} from 'react';
import {StaticView} from '../../reducers/connections'; import {StaticView} from '../../reducers/connections';
import {BackgroundColorProperty} from 'csstype'; import {BackgroundColorProperty} from 'csstype';
@@ -96,3 +110,109 @@ export function isStaticViewActive(
): boolean { ): boolean {
return current && selected && current === selected; return current && selected && current === selected;
} }
export const CategoryName = styled(PluginName)({
color: colors.macOSSidebarSectionTitle,
textTransform: 'uppercase',
fontSize: '0.9em',
});
export const Plugins = styled(FlexColumn)({
flexGrow: 1,
overflow: 'auto',
});
export const Spinner = centerInSidebar(LoadingIndicator);
export const ErrorIndicator = centerInSidebar(Glyph);
export function centerInSidebar(component: any) {
return styled(component)({
marginTop: '10px',
marginBottom: '10px',
marginLeft: 'auto',
marginRight: 'auto',
});
}
export class PluginSidebarListItem extends Component<{
onClick: () => void;
isActive: boolean;
plugin: typeof FlipperBasePlugin;
app?: string | null | undefined;
helpRef?: any;
provided?: any;
onFavorite?: () => void;
starred?: boolean;
}> {
render() {
const {isActive, plugin, onFavorite, starred} = this.props;
const iconColor = getColorByApp(this.props.app);
return (
<ListItem active={isActive} onClick={this.props.onClick}>
<PluginIcon
isActive={isActive}
name={plugin.icon || 'apps'}
backgroundColor={iconColor}
color={colors.white}
/>
<PluginName>{plugin.title || plugin.id}</PluginName>
{starred !== undefined && (
<StarButton onStar={onFavorite!} starred={starred} />
)}
</ListItem>
);
}
}
export function getColorByApp(app?: string | null): string {
let iconColor: string | undefined = (brandColors as any)[app!];
if (!iconColor) {
if (!app) {
// Device plugin
iconColor = colors.macOSTitleBarIconBlur;
} else {
const pluginColors = [
colors.seaFoam,
colors.teal,
colors.lime,
colors.lemon,
colors.orange,
colors.tomato,
colors.cherry,
colors.pink,
colors.grape,
];
iconColor = pluginColors[parseInt(app, 36) % pluginColors.length];
}
}
return iconColor;
}
export const NoDevices = () => (
<ListItem
style={{
textAlign: 'center',
marginTop: 50,
flexDirection: 'column',
}}>
<Glyph name="mobile" size={32} color={colors.red}></Glyph>
<Spacer style={{height: 20}} />
<Heading>Select a device to get started</Heading>
</ListItem>
);
export const NoClients = () => (
<ListItem>
<Glyph
name="mobile-engagement"
size={16}
color={colors.red}
style={{marginRight: 10}}
/>
No clients connected
</ListItem>
);

View File

@@ -87,6 +87,7 @@ export type Action =
selectedPlugin: null | string; selectedPlugin: null | string;
selectedApp?: null | string; selectedApp?: null | string;
deepLinkPayload: null | string; deepLinkPayload: null | string;
selectedDevice?: null | BaseDevice;
}; };
} }
| { | {
@@ -224,6 +225,10 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
case 'SELECT_PLUGIN': { case 'SELECT_PLUGIN': {
const {payload} = action; const {payload} = action;
const {selectedPlugin, selectedApp} = payload; const {selectedPlugin, selectedApp} = payload;
const selectedDevice = payload.selectedDevice || state.selectedDevice;
if (!selectDevice) {
console.warn('Trying to select a plugin before a device was selected!');
}
if (selectedPlugin) { if (selectedPlugin) {
performance.mark(`activePlugin-${selectedPlugin}`); performance.mark(`activePlugin-${selectedPlugin}`);
} }
@@ -234,6 +239,10 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
selectedApp: selectedApp || null, selectedApp: selectedApp || null,
selectedPlugin, selectedPlugin,
userPreferredPlugin: selectedPlugin || state.userPreferredPlugin, userPreferredPlugin: selectedPlugin || state.userPreferredPlugin,
selectedDevice: selectedDevice!,
userPreferredDevice: selectedDevice
? selectedDevice.title
: state.userPreferredDevice,
}); });
} }
@@ -450,6 +459,7 @@ export const preferDevice = (payload: string): Action => ({
export const selectPlugin = (payload: { export const selectPlugin = (payload: {
selectedPlugin: null | string; selectedPlugin: null | string;
selectedApp?: null | string; selectedApp?: null | string;
selectedDevice?: BaseDevice | null;
deepLinkPayload: null | string; deepLinkPayload: null | string;
}): Action => ({ }): Action => ({
type: 'SELECT_PLUGIN', type: 'SELECT_PLUGIN',
@@ -517,7 +527,7 @@ export function getClientById(
return clients.find(client => client.id === clientId); return clients.find(client => client.id === clientId);
} }
function canBeDefaultDevice(device: BaseDevice) { export function canBeDefaultDevice(device: BaseDevice) {
return !DEFAULT_DEVICE_BLACKLIST.some( return !DEFAULT_DEVICE_BLACKLIST.some(
blacklistedDevice => device instanceof blacklistedDevice, blacklistedDevice => device instanceof blacklistedDevice,
); );

View File

@@ -111,6 +111,7 @@ export async function createMockFlipperWithPlugin(
selectedPlugin: pluginClazz.id, selectedPlugin: pluginClazz.id,
selectedApp: client.query.app, selectedApp: client.query.app,
deepLinkPayload: null, deepLinkPayload: null,
selectedDevice: device,
}), }),
); );

View File

@@ -63,6 +63,7 @@ function selectDeviceLogs(store: Store) {
selectedPlugin: 'DeviceLogs', selectedPlugin: 'DeviceLogs',
selectedApp: null, selectedApp: null,
deepLinkPayload: null, deepLinkPayload: null,
selectedDevice: store.getState().connections.selectedDevice!,
}), }),
); );
} }
@@ -73,6 +74,7 @@ function selectTestPlugin(store: Store, client: Client) {
selectedPlugin: TestPlugin.id, selectedPlugin: TestPlugin.id,
selectedApp: client.query.app, selectedApp: client.query.app,
deepLinkPayload: null, deepLinkPayload: null,
selectedDevice: store.getState().connections.selectedDevice!,
}), }),
); );
} }
@@ -279,6 +281,7 @@ test('queue - messages that arrive during processing will be queued', async () =
selectedPlugin: TestPlugin.id, selectedPlugin: TestPlugin.id,
selectedApp: client.id, selectedApp: client.id,
deepLinkPayload: null, deepLinkPayload: null,
selectedDevice: device,
}), }),
); );
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin'); expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');

View File

@@ -73,7 +73,7 @@ function buildIconURL(name, size, density) {
// Check if that icon actually exists! // Check if that icon actually exists!
fetch(url) fetch(url)
.then(res => { .then(res => {
if (res.status === 200) { if (res.status === 200 && !existing.includes(size)) {
// the icon exists // the icon exists
existing.push(size); existing.push(size);
existing.sort(); existing.sort();

View File

@@ -81,8 +81,9 @@
16 16
], ],
"chevron-up": [ "chevron-up": [
8, 12,
12 16,
8
], ],
"compose": [ "compose": [
12 12