Summary: This diff lands improved sidebar navigation. The old functionality to order plugins based on last-recently-used, and cropping at 5 items has been removed. Instead, items can be starred and their position will be fixed. Together with the app switcher introduced this should lead to a cleaner, stabler, and more customizable UI. Reviewed By: jknoxville Differential Revision: D18299401 fbshipit-source-id: 29b7eb3a4130933c637f7c81834558bf738d5bf0
626 lines
17 KiB
TypeScript
626 lines
17 KiB
TypeScript
/**
|
|
* 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 config from '../fb-stubs/config';
|
|
import BaseDevice from '../devices/BaseDevice';
|
|
import Client from '../Client';
|
|
import {UninitializedClient} from '../UninitializedClient';
|
|
import {FlipperBasePlugin} from '../plugin';
|
|
import {PluginNotification} from '../reducers/notifications';
|
|
import {ActiveSheet, ACTIVE_SHEET_PLUGINS} from '../reducers/application';
|
|
import {State as Store} from '../reducers';
|
|
import {
|
|
Sidebar,
|
|
FlexBox,
|
|
colors,
|
|
brandColors,
|
|
Text,
|
|
Glyph,
|
|
styled,
|
|
FlexColumn,
|
|
GK,
|
|
FlipperPlugin,
|
|
FlipperDevicePlugin,
|
|
LoadingIndicator,
|
|
Button,
|
|
StarButton,
|
|
} from 'flipper';
|
|
import React, {Component, PureComponent, Fragment} from 'react';
|
|
import NotificationsHub from '../NotificationsHub';
|
|
import {
|
|
selectPlugin,
|
|
starPlugin,
|
|
StaticView,
|
|
setStaticView,
|
|
} from '../reducers/connections';
|
|
import {setActiveSheet} from '../reducers/application';
|
|
import UserAccount from './UserAccount';
|
|
import {connect} from 'react-redux';
|
|
import {BackgroundColorProperty} from 'csstype';
|
|
import {StyledOtherComponent} from 'create-emotion-styled';
|
|
import SupportRequestFormManager from '../fb-stubs/SupportRequestFormManager';
|
|
|
|
type FlipperPlugins = (typeof FlipperPlugin)[];
|
|
type PluginsByCategory = [string, FlipperPlugins][];
|
|
|
|
const ListItem = styled('div')(({active}: {active?: boolean}) => ({
|
|
paddingLeft: 10,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
marginBottom: 2,
|
|
flexShrink: 0,
|
|
backgroundColor: active ? colors.macOSTitleBarIconSelected : 'none',
|
|
color: active ? colors.white : colors.macOSSidebarSectionItem,
|
|
lineHeight: '25px',
|
|
padding: '0 10px',
|
|
'&[disabled]': {
|
|
color: 'rgba(0, 0, 0, 0.5)',
|
|
},
|
|
}));
|
|
|
|
const SidebarButton = styled(Button)(({small}: {small?: boolean}) => ({
|
|
fontWeight: 'bold',
|
|
fontSize: small ? 11 : 14,
|
|
width: '100%',
|
|
overflow: 'hidden',
|
|
marginTop: small ? 0 : 20,
|
|
pointer: 'cursor',
|
|
border: 'none',
|
|
background: 'none',
|
|
padding: 0,
|
|
justifyContent: 'left',
|
|
}));
|
|
|
|
const PluginShape = styled(FlexBox)(
|
|
({backgroundColor}: {backgroundColor?: BackgroundColorProperty}) => ({
|
|
marginRight: 5,
|
|
backgroundColor,
|
|
borderRadius: 3,
|
|
flexShrink: 0,
|
|
width: 18,
|
|
height: 18,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
}),
|
|
);
|
|
|
|
const PluginName = styled(Text)(
|
|
(props: {isActive?: boolean; count?: number}) => ({
|
|
minWidth: 0,
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
overflow: 'hidden',
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
flexGrow: 1,
|
|
'::after': {
|
|
fontSize: 12,
|
|
display: props.count ? 'inline-block' : 'none',
|
|
padding: '0 8px',
|
|
lineHeight: '17px',
|
|
height: 17,
|
|
alignSelf: 'center',
|
|
content: `"${props.count}"`,
|
|
borderRadius: '999em',
|
|
color: props.isActive ? colors.macOSTitleBarIconSelected : colors.white,
|
|
backgroundColor: props.isActive
|
|
? colors.white
|
|
: colors.macOSTitleBarIconSelected,
|
|
fontWeight: 500,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const CategoryName = styled(PluginName)({
|
|
color: colors.macOSSidebarSectionTitle,
|
|
textTransform: 'uppercase',
|
|
fontSize: '0.9em',
|
|
});
|
|
|
|
const Plugins = styled(FlexColumn)({
|
|
flexGrow: 1,
|
|
overflow: 'auto',
|
|
});
|
|
|
|
function PluginIcon({
|
|
isActive,
|
|
backgroundColor,
|
|
name,
|
|
color,
|
|
}: {
|
|
isActive: boolean;
|
|
backgroundColor?: string;
|
|
name: string;
|
|
color: string;
|
|
}) {
|
|
return (
|
|
<PluginShape backgroundColor={backgroundColor}>
|
|
<Glyph size={12} name={name} color={isActive ? colors.white : color} />
|
|
</PluginShape>
|
|
);
|
|
}
|
|
|
|
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: StyledOtherComponent<any, {}, any> | React.ComponentType<any>,
|
|
) {
|
|
return styled(component)({
|
|
marginTop: '10px',
|
|
marginBottom: '10px',
|
|
marginLeft: 'auto',
|
|
marginRight: 'auto',
|
|
});
|
|
}
|
|
|
|
type OwnProps = {};
|
|
|
|
type StateFromProps = {
|
|
numNotifications: number;
|
|
windowIsFocused: boolean;
|
|
selectedDevice: BaseDevice | null | undefined;
|
|
staticView: StaticView;
|
|
selectedPlugin: string | null | undefined;
|
|
selectedApp: string | null | undefined;
|
|
userStarredPlugins: Store['connections']['userStarredPlugins'];
|
|
clients: Array<Client>;
|
|
uninitializedClients: Array<{
|
|
client: UninitializedClient;
|
|
deviceId?: string;
|
|
errorMessage?: string;
|
|
}>;
|
|
devicePlugins: Map<string, typeof FlipperDevicePlugin>;
|
|
clientPlugins: Map<string, typeof FlipperPlugin>;
|
|
};
|
|
|
|
type DispatchFromProps = {
|
|
selectPlugin: (payload: {
|
|
selectedPlugin: string | null;
|
|
selectedApp: string | null;
|
|
deepLinkPayload: string | null;
|
|
}) => void;
|
|
setActiveSheet: (activeSheet: ActiveSheet) => void;
|
|
setStaticView: (payload: StaticView) => void;
|
|
starPlugin: typeof starPlugin;
|
|
};
|
|
|
|
type Props = OwnProps & StateFromProps & DispatchFromProps;
|
|
type State = {
|
|
showSupportForm: boolean;
|
|
selectedClientIndex: number;
|
|
showAllPlugins: boolean;
|
|
};
|
|
class MainSidebar extends PureComponent<Props, State> {
|
|
state: State = {
|
|
showSupportForm: GK.get('flipper_support_requests'),
|
|
// Not to be confused with selectedApp prop, this one only used to remember the client drowdown selector
|
|
selectedClientIndex: 0,
|
|
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,
|
|
selectedPlugin,
|
|
staticView,
|
|
selectPlugin,
|
|
setStaticView,
|
|
windowIsFocused,
|
|
numNotifications,
|
|
} = this.props;
|
|
let {clients, uninitializedClients} = this.props;
|
|
clients = clients
|
|
.filter(
|
|
(client: Client) =>
|
|
(selectedDevice &&
|
|
selectedDevice.supportsOS(client.query.os) &&
|
|
client.query.device_id === selectedDevice.serial) ||
|
|
// Old android sdk versions don't know their device_id
|
|
// Display their plugins under all selected devices until they die out
|
|
client.query.device_id === 'unknown',
|
|
)
|
|
.sort((a, b) => (a.query.app || '').localeCompare(b.query.app));
|
|
const client: Client | null =
|
|
clients[this.state.selectedClientIndex] || null;
|
|
|
|
return (
|
|
<Sidebar
|
|
position="left"
|
|
width={200}
|
|
backgroundColor={
|
|
process.platform === 'darwin' && windowIsFocused ? 'transparent' : ''
|
|
}>
|
|
<Plugins>
|
|
{selectedDevice && (
|
|
<ListItem>
|
|
<SidebarButton>{selectedDevice.title}</SidebarButton>
|
|
</ListItem>
|
|
)}
|
|
{selectedDevice &&
|
|
Array.from(this.props.devicePlugins.values())
|
|
.filter(plugin => plugin.supportsDevice(selectedDevice))
|
|
.sort(sortPluginsByName)
|
|
.map((plugin: typeof FlipperDevicePlugin) => (
|
|
<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, index) => ({
|
|
checked: client === c,
|
|
label: c.query.app,
|
|
type: 'checkbox',
|
|
click: () => this.setState({selectedClientIndex: index}),
|
|
}))}>
|
|
{clients.length === 0 ? (
|
|
'(Not connected to app)'
|
|
) : this.state.selectedClientIndex >= clients.length ? (
|
|
'(Select app)'
|
|
) : (
|
|
<>
|
|
{client.query.app}
|
|
{clients.length > 1 && (
|
|
<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>
|
|
))}
|
|
</Plugins>
|
|
{!GK.get('flipper_disable_notifications') && (
|
|
<ListItem
|
|
active={selectedPlugin === 'notifications'}
|
|
onClick={() =>
|
|
selectPlugin({
|
|
selectedPlugin: 'notifications',
|
|
selectedApp: null,
|
|
deepLinkPayload: null,
|
|
})
|
|
}
|
|
style={{
|
|
borderTop: `1px solid ${colors.blackAlpha10}`,
|
|
}}>
|
|
<PluginIcon
|
|
color={colors.light50}
|
|
name={
|
|
numNotifications > 0
|
|
? NotificationsHub.icon || 'bell'
|
|
: 'bell-null'
|
|
}
|
|
isActive={selectedPlugin === NotificationsHub.id}
|
|
/>
|
|
<PluginName
|
|
count={numNotifications}
|
|
isActive={selectedPlugin === NotificationsHub.id}>
|
|
{NotificationsHub.title}
|
|
</PluginName>
|
|
</ListItem>
|
|
)}
|
|
{this.state.showSupportForm && (
|
|
<ListItem
|
|
active={
|
|
staticView != null && staticView === SupportRequestFormManager
|
|
}
|
|
onClick={() => setStaticView(SupportRequestFormManager)}>
|
|
<PluginIcon
|
|
color={colors.light50}
|
|
name={'app-dailies'}
|
|
isActive={
|
|
staticView != null && staticView === SupportRequestFormManager
|
|
}
|
|
/>
|
|
<PluginName
|
|
isActive={
|
|
staticView != null && staticView === SupportRequestFormManager
|
|
}>
|
|
Litho Support Request
|
|
</PluginName>
|
|
</ListItem>
|
|
)}
|
|
<ListItem
|
|
onClick={() => this.props.setActiveSheet(ACTIVE_SHEET_PLUGINS)}>
|
|
<PluginIcon
|
|
name="question-circle"
|
|
color={colors.light50}
|
|
isActive={false}
|
|
/>
|
|
Manage Plugins
|
|
</ListItem>
|
|
{config.showLogin && <UserAccount />}
|
|
</Sidebar>
|
|
);
|
|
}
|
|
|
|
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 | null) {
|
|
if (!client) {
|
|
return null;
|
|
}
|
|
const onFavorite = (plugin: string) => {
|
|
this.props.starPlugin({
|
|
selectedApp: client.id,
|
|
selectedPlugin: plugin,
|
|
});
|
|
};
|
|
const allPlugins = Array.from(this.props.clientPlugins.values());
|
|
const favoritePlugins: FlipperPlugins = getFavoritePlugins(
|
|
client,
|
|
allPlugins,
|
|
this.props.userStarredPlugins,
|
|
true,
|
|
);
|
|
return (
|
|
<>
|
|
{favoritePlugins.length === 0 ? (
|
|
<ListItem>
|
|
<div style={{textAlign: 'center', width: '100%'}}>
|
|
Star some plugins!
|
|
<hr style={{width: '100%'}} />
|
|
</div>
|
|
</ListItem>
|
|
) : (
|
|
<>
|
|
{this.renderPluginsByCategory(
|
|
client,
|
|
favoritePlugins,
|
|
true,
|
|
onFavorite,
|
|
)}
|
|
<ListItem>
|
|
<SidebarButton
|
|
small
|
|
compact
|
|
onClick={() =>
|
|
this.setState(state => ({
|
|
...state,
|
|
showAllPlugins: !state.showAllPlugins,
|
|
}))
|
|
}>
|
|
{this.state.showAllPlugins ? 'Show less' : 'Show more'}
|
|
<Glyph
|
|
size={8}
|
|
name={
|
|
this.state.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',
|
|
}}>
|
|
{this.state.showAllPlugins || favoritePlugins.length === 0
|
|
? this.renderPluginsByCategory(
|
|
client,
|
|
getFavoritePlugins(
|
|
client,
|
|
allPlugins,
|
|
this.props.userStarredPlugins,
|
|
false,
|
|
),
|
|
false,
|
|
onFavorite,
|
|
)
|
|
: null}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
function getFavoritePlugins(
|
|
client: Client,
|
|
allPlugins: FlipperPlugins,
|
|
userStarredPlugins: Props['userStarredPlugins'],
|
|
favorite: boolean,
|
|
): FlipperPlugins {
|
|
const appName = client.id;
|
|
return allPlugins.filter(plugin => {
|
|
const idx = userStarredPlugins[appName]
|
|
? userStarredPlugins[appName].indexOf(plugin.id)
|
|
: -1;
|
|
return idx === -1 ? !favorite : favorite;
|
|
});
|
|
}
|
|
|
|
function groupPluginsByCategory(plugins: FlipperPlugins): PluginsByCategory {
|
|
const sortedPlugins = plugins.slice().sort(sortPluginsByName);
|
|
const byCategory: {[cat: string]: FlipperPlugins} = {};
|
|
const res: PluginsByCategory = [];
|
|
sortedPlugins.forEach(plugin => {
|
|
const category = plugin.category || '';
|
|
(byCategory[category] || (byCategory[category] = [])).push(plugin);
|
|
});
|
|
// Sort categories
|
|
Object.keys(byCategory)
|
|
.sort()
|
|
.forEach(category => {
|
|
res.push([category, byCategory[category]]);
|
|
});
|
|
return res;
|
|
}
|
|
|
|
function sortPluginsByName(
|
|
a: typeof FlipperBasePlugin,
|
|
b: typeof FlipperBasePlugin,
|
|
): number {
|
|
return (a.title || a.id) > (b.title || b.id) ? 1 : -1;
|
|
}
|
|
|
|
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
|
|
({
|
|
application: {windowIsFocused},
|
|
connections: {
|
|
selectedDevice,
|
|
selectedPlugin,
|
|
selectedApp,
|
|
userStarredPlugins,
|
|
clients,
|
|
uninitializedClients,
|
|
staticView,
|
|
},
|
|
notifications: {activeNotifications, blacklistedPlugins},
|
|
plugins: {devicePlugins, clientPlugins},
|
|
}) => ({
|
|
numNotifications: (() => {
|
|
const blacklist = new Set(blacklistedPlugins);
|
|
return activeNotifications.filter(
|
|
(n: PluginNotification) => !blacklist.has(n.pluginId),
|
|
).length;
|
|
})(),
|
|
windowIsFocused,
|
|
selectedDevice,
|
|
staticView,
|
|
selectedPlugin,
|
|
selectedApp,
|
|
userStarredPlugins,
|
|
clients,
|
|
uninitializedClients,
|
|
devicePlugins,
|
|
clientPlugins,
|
|
}),
|
|
{
|
|
selectPlugin,
|
|
setStaticView,
|
|
setActiveSheet,
|
|
starPlugin,
|
|
},
|
|
)(MainSidebar);
|