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

View File

@@ -10,30 +10,24 @@
import BaseDevice from '../../devices/BaseDevice';
import Client from '../../Client';
import {UninitializedClient} from '../../UninitializedClient';
import {FlipperBasePlugin, sortPluginsByName} from '../../plugin';
import {sortPluginsByName} from '../../plugin';
import {PluginNotification} from '../../reducers/notifications';
import {ActiveSheet} from '../../reducers/application';
import {State as Store} from '../../reducers';
import {
Sidebar,
colors,
brandColors,
Glyph,
styled,
FlexColumn,
GK,
FlipperPlugin,
FlipperDevicePlugin,
LoadingIndicator,
Button,
StarButton,
Heading,
Spacer,
ArchivedDevice,
SmallText,
Info,
} from 'flipper';
import React, {Component, PureComponent, Fragment} from 'react';
import React, {PureComponent, Fragment} from 'react';
import {
selectPlugin,
starPlugin,
@@ -55,6 +49,12 @@ import {
PluginsByCategory,
PluginName,
PluginIcon,
Plugins,
ErrorIndicator,
Spinner,
CategoryName,
PluginSidebarListItem,
NoDevices,
} from './sidebarUtils';
const SidebarButton = styled(Button)<{small?: boolean}>(({small}) => ({
@@ -71,78 +71,6 @@ const SidebarButton = styled(Button)<{small?: boolean}>(({small}) => ({
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 StateFromProps = {
@@ -168,6 +96,7 @@ type DispatchFromProps = {
selectedPlugin: string | null;
selectedApp: string | null;
deepLinkPayload: string | null;
selectedDevice: BaseDevice;
}) => void;
selectClient: typeof selectClient;
setActiveSheet: (activeSheet: ActiveSheet) => void;
@@ -217,6 +146,7 @@ class MainSidebar extends PureComponent<Props, State> {
selectedPlugin: plugin.id,
selectedApp: null,
deepLinkPayload: null,
selectedDevice,
})
}
plugin={plugin}
@@ -270,16 +200,7 @@ class MainSidebar extends PureComponent<Props, State> {
))}
</>
) : (
<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>
<NoDevices />
)}
</Plugins>
<MainSidebarUtils />
@@ -331,7 +252,12 @@ class MainSidebar extends PureComponent<Props, State> {
starred: boolean,
onFavorite: (pluginId: string) => void,
) {
const {selectedPlugin, selectedApp, selectPlugin} = this.props;
const {
selectedPlugin,
selectedApp,
selectPlugin,
selectedDevice,
} = this.props;
return groupPluginsByCategory(plugins).map(([category, plugins]) => (
<Fragment key={category}>
{category && (
@@ -348,6 +274,7 @@ class MainSidebar extends PureComponent<Props, State> {
selectedPlugin: plugin.id,
selectedApp: client.id,
deepLinkPayload: null,
selectedDevice: selectedDevice!,
})
}
plugin={plugin}

View File

@@ -10,144 +10,128 @@
import BaseDevice from '../../devices/BaseDevice';
import Client from '../../Client';
import {UninitializedClient} from '../../UninitializedClient';
import {FlipperBasePlugin, sortPluginsByName} from '../../plugin';
import {sortPluginsByName} from '../../plugin';
import {PluginNotification} from '../../reducers/notifications';
import {ActiveSheet} from '../../reducers/application';
import {State as Store} from '../../reducers';
import {
Sidebar,
colors,
brandColors,
Glyph,
styled,
FlexColumn,
GK,
FlipperPlugin,
FlipperDevicePlugin,
LoadingIndicator,
Button,
StarButton,
Heading,
Spacer,
ArchivedDevice,
SmallText,
Info,
HBox,
} from 'flipper';
import React, {Component, PureComponent, Fragment} from 'react';
import React, {
PureComponent,
Fragment,
memo,
useCallback,
useState,
} from 'react';
import NotificationScreen from '../NotificationScreen';
import {
selectPlugin,
starPlugin,
starPlugin as starPluginAction,
StaticView,
setStaticView,
selectClient,
getAvailableClients,
getClientById,
canBeDefaultDevice,
} from '../../reducers/connections';
import {setActiveSheet} from '../../reducers/application';
import {connect} from 'react-redux';
import SupportRequestFormManager from '../../fb-stubs/SupportRequestFormManager';
import SupportRequestDetails from '../../fb-stubs/SupportRequestDetails';
import MainSidebarUtils from './MainSidebarUtilsSection';
import MainSidebarUtilsSection from './MainSidebarUtilsSection';
import {
ListItem,
isStaticViewActive,
FlipperPlugins,
PluginsByCategory,
PluginName,
Plugins,
CategoryName,
PluginIcon,
PluginSidebarListItem,
ErrorIndicator,
NoClients,
Spinner,
NoDevices,
getColorByApp,
} from './sidebarUtils';
const SidebarButton = styled(Button)<{small?: boolean}>(({small}) => ({
fontWeight: 'bold',
fontSize: small ? 11 : 14,
width: '100%',
overflow: 'hidden',
marginTop: small ? 0 : 20,
pointer: 'cursor',
type FlipperPlugins = typeof FlipperPlugin[];
type PluginsByCategory = [string, FlipperPlugins][];
type SectionLevel = 1 | 2 | 3;
const SidebarSectionButton = styled('button')<{
level: SectionLevel;
color: string;
}>(({level, color}) => ({
fontWeight: level === 3 ? 'normal' : 'bold',
borderRadius: 0,
border: 'none',
background: 'none',
padding: 0,
justifyContent: 'left',
whiteSpace: 'nowrap',
background: level === 1 ? colors.sectionHeaderBorder : 'transparent',
textAlign: level === 3 ? 'center' : 'left',
width: '100%',
fontSize: level === 3 ? 11 : 14,
color,
padding: `${level === 3 ? 0 : 8}px 10px 8px 9px`,
}));
const CategoryName = styled(PluginName)({
color: colors.macOSSidebarSectionTitle,
textTransform: 'uppercase',
fontSize: '0.9em',
});
const SidebarSectionBody = styled('div')<{
level: SectionLevel;
collapsed: boolean;
}>(({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)({
flexGrow: 1,
overflow: 'auto',
});
const SidebarSection: React.FC<{
defaultCollapsed?: boolean;
title: string | React.ReactNode | ((collapsed: boolean) => React.ReactNode);
level: SectionLevel;
color?: string;
}> = ({children, title, level, color, defaultCollapsed}) => {
const [collapsed, setCollapsed] = useState(!!defaultCollapsed);
color = color || colors.macOSTitleBarIconActive;
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',
});
}
return (
<>
<SidebarSectionButton
onClick={() => setCollapsed(s => !s)}
level={level}
color={color}>
<HBox grow="left">
{typeof title === 'function' ? title(collapsed) : title}
{level < 3 && (
<Glyph
name={collapsed ? 'chevron-down' : 'chevron-up'}
size={12}
color={color}
/>
)}
</HBox>
</SidebarSectionButton>
<SidebarSectionBody level={level} collapsed={collapsed}>
{level === 1 && <div style={{height: 8}} />}
{children}
</SidebarSectionBody>
</>
);
};
type OwnProps = {};
type StateFromProps = {
numNotifications: number;
windowIsFocused: boolean;
devices: BaseDevice[];
selectedDevice: BaseDevice | null | undefined;
staticView: StaticView;
selectedPlugin: string | null | undefined;
@@ -163,132 +147,137 @@ type StateFromProps = {
clientPlugins: Map<string, typeof FlipperPlugin>;
};
type SelectPlugin = (payload: {
selectedPlugin: string | null;
selectedApp?: string | null;
deepLinkPayload: string | null;
selectedDevice: BaseDevice;
}) => void;
type DispatchFromProps = {
selectPlugin: (payload: {
selectedPlugin: string | null;
selectedApp: string | null;
deepLinkPayload: string | null;
}) => void;
selectClient: typeof selectClient;
selectPlugin: SelectPlugin;
setActiveSheet: (activeSheet: ActiveSheet) => void;
setStaticView: (payload: StaticView) => void;
starPlugin: typeof starPlugin;
starPlugin: typeof starPluginAction;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
type State = {
showSupportForm: boolean;
showWatchDebugRoot: boolean;
showAllPlugins: boolean;
};
class MainSidebar extends PureComponent<Props, State> {
class MainSidebar2 extends PureComponent<Props, State> {
state: State = {
showSupportForm: GK.get('support_requests_v2'),
showWatchDebugRoot: GK.get('watch_team_flipper_clientless_access'),
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() {
const {
selectedDevice,
selectClient,
selectedPlugin,
selectedApp,
selectPlugin,
uninitializedClients,
} = this.props;
const clients = getAvailableClients(selectedDevice, this.props.clients);
const client: Client | undefined = getClientById(clients, selectedApp);
const devices = this.props.devices
.slice()
.sort((a, b) => a.title.localeCompare(b.title));
const renderableDevices = devices.filter(canBeDefaultDevice);
return (
<Sidebar position="left" width={200} backgroundColor={colors.light02}>
<Plugins>
{selectedDevice ? (
<>
<ListItem>
<SidebarButton>{selectedDevice.title}</SidebarButton>
</ListItem>
{this.showArchivedDeviceDetails(selectedDevice)}
{selectedDevice.devicePlugins.map(pluginName => {
const plugin = this.props.devicePlugins.get(pluginName)!;
return (
<PluginSidebarListItem
key={plugin.id}
isActive={plugin.id === selectedPlugin}
onClick={() =>
selectPlugin({
selectedPlugin: plugin.id,
selectedApp: null,
deepLinkPayload: null,
})
}
plugin={plugin}
/>
);
})}
<ListItem>
<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 => (
<ListItem key={JSON.stringify(entry.client)}>
{entry.client.appName}
{entry.errorMessage ? (
<ErrorIndicator name={'mobile-cross'} size={16} />
) : (
<Spinner size={16} />
)}
</ListItem>
))}
</>
{renderableDevices.length ? (
renderableDevices.map(device => this.renderDevice(device))
) : (
<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>
<NoDevices />
)}
</Plugins>
<MainSidebarUtils />
<MainSidebarUtilsSection />
</Sidebar>
);
}
showArchivedDeviceDetails(selectedDevice: BaseDevice) {
if (!selectedDevice.isArchived || !selectedDevice.source) {
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)!;
return (
<PluginSidebarListItem
key={plugin.id}
isActive={
plugin.id === selectedPlugin && selectedDevice === device
}
onClick={() =>
selectPlugin({
selectedPlugin: plugin.id,
selectedApp: null,
deepLinkPayload: null,
selectedDevice: device,
})
}
plugin={plugin}
/>
);
})}
</SidebarSection>
{uninitializedClients.map(entry => (
<ListItem key={JSON.stringify(entry.client)}>
{entry.client.appName}
{entry.errorMessage ? (
<ErrorIndicator name={'mobile-cross'} size={16} />
) : (
<Spinner size={16} />
)}
</ListItem>
))}
{clients.length === 0 ? (
<NoClients />
) : (
clients.map(client => (
<PluginList
device={device}
key={client.id}
client={client}
clientPlugins={clientPlugins}
starPlugin={starPlugin}
userStarredPlugins={userStarredPlugins}
selectedPlugin={selectedPlugin}
selectedApp={selectedApp}
selectPlugin={selectPlugin}
/>
))
)}
</SidebarSection>
);
}
showArchivedDeviceDetails(device: BaseDevice) {
if (!device.isArchived || !device.source) {
return null;
}
const {staticView, setStaticView} = this.props;
@@ -296,18 +285,15 @@ class MainSidebar extends PureComponent<Props, State> {
staticView,
SupportRequestDetails,
);
const showSupportForm =
GK.get('support_requests_v2') ||
isStaticViewActive(staticView, SupportRequestFormManager);
return (
<>
<ListItem>
<Info type="warning" small>
{selectedDevice.source ? 'Imported device' : 'Archived device'}
{device.source ? 'Imported device' : 'Archived device'}
</Info>
</ListItem>
{showSupportForm &&
(selectedDevice as ArchivedDevice).supportRequestDetails && (
{this.state.showSupportForm &&
(device as ArchivedDevice).supportRequestDetails && (
<ListItem
active={supportRequestDetailsactive}
onClick={() => setStaticView(SupportRequestDetails)}>
@@ -325,127 +311,42 @@ class MainSidebar extends PureComponent<Props, State> {
);
}
renderPluginsByCategory(
client: Client,
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) {
renderNotificationsEntry() {
if (GK.get('flipper_disable_notifications')) {
return null;
}
const onFavorite = (plugin: string) => {
this.props.starPlugin({
selectedApp: client.query.app,
selectedPlugin: plugin,
});
};
const allPlugins = Array.from(this.props.clientPlugins.values()).filter(
(p: typeof FlipperPlugin) => client.plugins.indexOf(p.id) > -1,
const active = isStaticViewActive(
this.props.staticView,
NotificationScreen,
);
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 (
<>
{favoritePlugins.length === 0 ? (
<ListItem>
<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={{
marginLeft: 4,
}}
/>
</SidebarButton>
</ListItem>
</>
)}
<div
style={{
flex: 'auto' /*scroll this region, not the entire thing*/,
overflow: 'auto',
height: 'auto',
}}>
{showAllPlugins
? this.renderPluginsByCategory(
client,
getFavoritePlugins(
allPlugins,
this.props.userStarredPlugins[client.query.app],
false,
),
false,
onFavorite,
)
: null}
</div>
</>
<ListItem
active={active}
onClick={() => this.props.setStaticView(NotificationScreen)}
style={{
borderTop: `1px solid ${colors.blackAlpha10}`,
}}>
<PluginIcon
color={colors.light50}
name={this.props.numNotifications > 0 ? 'bell' : 'bell-null'}
isActive={active}
/>
<PluginName count={this.props.numNotifications} isActive={active}>
Notifications
</PluginName>
</ListItem>
);
}
}
function isStaticViewActive(
current: StaticView,
selected: StaticView,
): boolean {
return current && selected && current === selected;
}
function getFavoritePlugins(
allPlugins: FlipperPlugins,
starredPlugins: undefined | string[],
@@ -481,6 +382,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
application: {windowIsFocused},
connections: {
devices,
selectedDevice,
selectedPlugin,
selectedApp,
@@ -499,6 +401,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
).length;
})(),
windowIsFocused,
devices,
selectedDevice,
staticView,
selectedPlugin,
@@ -511,9 +414,167 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
}),
{
selectPlugin,
selectClient,
setStaticView,
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
*/
import {FlexBox, colors, Text, Glyph, styled, FlipperPlugin} from 'flipper';
import React from 'react';
import {
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 {BackgroundColorProperty} from 'csstype';
@@ -96,3 +110,109 @@ export function isStaticViewActive(
): boolean {
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;
selectedApp?: null | string;
deepLinkPayload: null | string;
selectedDevice?: null | BaseDevice;
};
}
| {
@@ -224,6 +225,10 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
case 'SELECT_PLUGIN': {
const {payload} = action;
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) {
performance.mark(`activePlugin-${selectedPlugin}`);
}
@@ -234,6 +239,10 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
selectedApp: selectedApp || null,
selectedPlugin,
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: {
selectedPlugin: null | string;
selectedApp?: null | string;
selectedDevice?: BaseDevice | null;
deepLinkPayload: null | string;
}): Action => ({
type: 'SELECT_PLUGIN',
@@ -517,7 +527,7 @@ export function getClientById(
return clients.find(client => client.id === clientId);
}
function canBeDefaultDevice(device: BaseDevice) {
export function canBeDefaultDevice(device: BaseDevice) {
return !DEFAULT_DEVICE_BLACKLIST.some(
blacklistedDevice => device instanceof blacklistedDevice,
);

View File

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

View File

@@ -63,6 +63,7 @@ function selectDeviceLogs(store: Store) {
selectedPlugin: 'DeviceLogs',
selectedApp: null,
deepLinkPayload: null,
selectedDevice: store.getState().connections.selectedDevice!,
}),
);
}
@@ -73,6 +74,7 @@ function selectTestPlugin(store: Store, client: Client) {
selectedPlugin: TestPlugin.id,
selectedApp: client.query.app,
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,
selectedApp: client.id,
deepLinkPayload: null,
selectedDevice: device,
}),
);
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');

View File

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

View File

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