Summary: This diff is a refinement of D18780965, which fixed plugin preferences to be stored per device. Instead of storing plugin preferences globally, we now store them per app, so that every app can have their own favorites, which are shared regardless the device Note that the current favorite selection will be lost. Reviewed By: nikoant Differential Revision: D19018169 fbshipit-source-id: acfa05ece8516840bb91aee4059886365b346582
669 lines
19 KiB
TypeScript
669 lines
19 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, sortPluginsByName} 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,
|
|
Heading,
|
|
Spacer,
|
|
ArchivedDevice,
|
|
SmallText,
|
|
Info,
|
|
} from 'flipper';
|
|
import React, {Component, PureComponent, Fragment} from 'react';
|
|
import NotificationScreen from '../chrome/NotificationScreen';
|
|
import {
|
|
selectPlugin,
|
|
starPlugin,
|
|
StaticView,
|
|
setStaticView,
|
|
selectClient,
|
|
getAvailableClients,
|
|
getClientById,
|
|
} from '../reducers/connections';
|
|
import {setActiveSheet} from '../reducers/application';
|
|
import UserAccount from './UserAccount';
|
|
import {connect} from 'react-redux';
|
|
import {BackgroundColorProperty} from 'csstype';
|
|
import SupportRequestFormManager from '../fb-stubs/SupportRequestFormManager';
|
|
import SupportRequestDetails from '../fb-stubs/SupportRequestDetails';
|
|
import SupportRequestFormV2 from '../fb-stubs/SupportRequestFormV2';
|
|
|
|
type FlipperPlugins = typeof FlipperPlugin[];
|
|
type PluginsByCategory = [string, FlipperPlugins][];
|
|
|
|
const ListItem = styled.div<{active?: boolean}>(({active}) => ({
|
|
paddingLeft: 10,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
marginBottom: 6,
|
|
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?: boolean}>(({small}) => ({
|
|
fontWeight: 'bold',
|
|
fontSize: small ? 11 : 14,
|
|
width: '100%',
|
|
overflow: 'hidden',
|
|
marginTop: small ? 0 : 20,
|
|
pointer: 'cursor',
|
|
border: 'none',
|
|
background: 'none',
|
|
padding: 0,
|
|
justifyContent: 'left',
|
|
whiteSpace: 'nowrap',
|
|
}));
|
|
|
|
const PluginShape = styled(FlexBox)<{
|
|
backgroundColor?: BackgroundColorProperty;
|
|
}>(({backgroundColor}) => ({
|
|
marginRight: 8,
|
|
backgroundColor,
|
|
borderRadius: 3,
|
|
flexShrink: 0,
|
|
width: 18,
|
|
height: 18,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
top: '-1px',
|
|
}));
|
|
|
|
const PluginName = styled(Text)<{isActive?: boolean; count?: number}>(
|
|
props => ({
|
|
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: 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;
|
|
selectClient: typeof selectClient;
|
|
setActiveSheet: (activeSheet: ActiveSheet) => void;
|
|
setStaticView: (payload: StaticView) => void;
|
|
starPlugin: typeof starPlugin;
|
|
};
|
|
|
|
type Props = OwnProps & StateFromProps & DispatchFromProps;
|
|
type State = {
|
|
showSupportForm: boolean;
|
|
showAllPlugins: boolean;
|
|
};
|
|
class MainSidebar extends PureComponent<Props, State> {
|
|
state: State = {
|
|
showSupportForm: GK.get('support_requests_v2'),
|
|
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,
|
|
staticView,
|
|
selectPlugin,
|
|
setStaticView,
|
|
uninitializedClients,
|
|
} = this.props;
|
|
const clients = getAvailableClients(selectedDevice, this.props.clients);
|
|
const client: Client | undefined = getClientById(clients, selectedApp);
|
|
|
|
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>
|
|
))}
|
|
</>
|
|
) : (
|
|
<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>
|
|
)}
|
|
</Plugins>
|
|
{this.renderNotificationsEntry()}
|
|
{this.state.showSupportForm &&
|
|
(function() {
|
|
const active = isStaticViewActive(staticView, SupportRequestFormV2);
|
|
return (
|
|
<ListItem
|
|
active={active}
|
|
onClick={() => setStaticView(SupportRequestFormV2)}>
|
|
<PluginIcon
|
|
color={colors.light50}
|
|
name={'app-dailies'}
|
|
isActive={active}
|
|
/>
|
|
<PluginName isActive={active}>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>
|
|
);
|
|
}
|
|
|
|
showArchivedDeviceDetails(selectedDevice: BaseDevice) {
|
|
if (!selectedDevice.isArchived || !selectedDevice.source) {
|
|
return null;
|
|
}
|
|
const {staticView, setStaticView} = this.props;
|
|
const supportRequestDetailsactive = isStaticViewActive(
|
|
staticView,
|
|
SupportRequestDetails,
|
|
);
|
|
return (
|
|
<>
|
|
<ListItem>
|
|
<Info type="warning" small>
|
|
{selectedDevice.source ? 'Imported device' : 'Archived device'}
|
|
</Info>
|
|
</ListItem>
|
|
{this.state.showSupportForm &&
|
|
(selectedDevice as ArchivedDevice).supportRequestDetails && (
|
|
<ListItem
|
|
active={supportRequestDetailsactive}
|
|
onClick={() => setStaticView(SupportRequestDetails)}>
|
|
<PluginIcon
|
|
color={colors.light50}
|
|
name={'app-dailies'}
|
|
isActive={supportRequestDetailsactive}
|
|
/>
|
|
<PluginName isActive={supportRequestDetailsactive}>
|
|
Support Request Details
|
|
</PluginName>
|
|
</ListItem>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
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) {
|
|
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 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>
|
|
</>
|
|
);
|
|
}
|
|
|
|
renderNotificationsEntry() {
|
|
if (GK.get('flipper_disable_notifications')) {
|
|
return null;
|
|
}
|
|
|
|
const active = isStaticViewActive(
|
|
this.props.staticView,
|
|
NotificationScreen,
|
|
);
|
|
return (
|
|
<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[],
|
|
favorite: boolean,
|
|
): FlipperPlugins {
|
|
if (!starredPlugins || !starredPlugins.length) {
|
|
return favorite ? [] : allPlugins;
|
|
}
|
|
return allPlugins.filter(plugin => {
|
|
const idx = starredPlugins.indexOf(plugin.id);
|
|
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;
|
|
}
|
|
|
|
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,
|
|
selectClient,
|
|
setStaticView,
|
|
setActiveSheet,
|
|
starPlugin,
|
|
},
|
|
)(MainSidebar);
|