Add Extra Action for Each Notification
Summary: Previously, notifications can be hidden by category or plugin. This diff ports that functionality to Sandy Reviewed By: mweststrate Differential Revision: D25021697 fbshipit-source-id: 28bdd1c169fcef4db79c2452db8cbb5f0bce6312
This commit is contained in:
committed by
Facebook GitHub Bot
parent
04de290b27
commit
f68cef3046
@@ -54,6 +54,8 @@ import {getInstance} from '../fb-stubs/Logger';
|
|||||||
import {isStaticViewActive} from '../chrome/mainsidebar/sidebarUtils';
|
import {isStaticViewActive} from '../chrome/mainsidebar/sidebarUtils';
|
||||||
import {getUser} from '../fb-stubs/user';
|
import {getUser} from '../fb-stubs/user';
|
||||||
import {SandyRatingButton} from '../chrome/RatingButton';
|
import {SandyRatingButton} from '../chrome/RatingButton';
|
||||||
|
import {filterNotifications} from './notification/notificationUtils';
|
||||||
|
import {useMemoize} from '../utils/useMemoize';
|
||||||
|
|
||||||
const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({
|
const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({
|
||||||
width: kind === 'small' ? 32 : 36,
|
width: kind === 'small' ? 32 : 36,
|
||||||
@@ -213,15 +215,18 @@ function NotificationButton({
|
|||||||
toplevelSelection,
|
toplevelSelection,
|
||||||
setToplevelSelection,
|
setToplevelSelection,
|
||||||
}: ToplevelProps) {
|
}: ToplevelProps) {
|
||||||
const notificationCount = useStore(
|
const notifications = useStore((state) => state.notifications);
|
||||||
(state) => state.notifications.activeNotifications.length,
|
const activeNotifications = useMemoize(filterNotifications, [
|
||||||
);
|
notifications.activeNotifications,
|
||||||
|
notifications.blacklistedPlugins,
|
||||||
|
notifications.blacklistedCategories,
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<LeftRailButton
|
<LeftRailButton
|
||||||
icon={<BellOutlined />}
|
icon={<BellOutlined />}
|
||||||
title="Notifications"
|
title="Notifications"
|
||||||
selected={toplevelSelection === 'notification'}
|
selected={toplevelSelection === 'notification'}
|
||||||
count={notificationCount}
|
count={activeNotifications.length}
|
||||||
onClick={() => setToplevelSelection('notification')}
|
onClick={() => setToplevelSelection('notification')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import React, {useCallback, useMemo, useState} from 'react';
|
import React, {useCallback, useMemo, useState} from 'react';
|
||||||
import {Layout, theme} from 'flipper-plugin';
|
import {Layout, theme} from 'flipper-plugin';
|
||||||
import {styled, Glyph} from '../../ui';
|
import {styled, Glyph} from '../../ui';
|
||||||
import {Input, Typography, Button, Collapse} from 'antd';
|
import {Input, Typography, Button, Collapse, Dropdown, Menu} from 'antd';
|
||||||
import {
|
import {
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
UpOutlined,
|
UpOutlined,
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
EllipsisOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {LeftSidebar, SidebarTitle} from '../LeftSidebar';
|
import {LeftSidebar, SidebarTitle} from '../LeftSidebar';
|
||||||
import {Notification as NotificationData} from '../../plugin';
|
import {Notification as NotificationData} from '../../plugin';
|
||||||
@@ -25,10 +26,18 @@ import {useStore, useDispatch} from '../../utils/useStore';
|
|||||||
import {ClientQuery} from '../../Client';
|
import {ClientQuery} from '../../Client';
|
||||||
import {deconstructClientId} from '../../utils/clientUtils';
|
import {deconstructClientId} from '../../utils/clientUtils';
|
||||||
import {selectPlugin} from '../../reducers/connections';
|
import {selectPlugin} from '../../reducers/connections';
|
||||||
import {clearAllNotifications} from '../../reducers/notifications';
|
import {
|
||||||
|
clearAllNotifications,
|
||||||
|
updateCategoryBlacklist,
|
||||||
|
updatePluginBlacklist,
|
||||||
|
} from '../../reducers/notifications';
|
||||||
|
import {filterNotifications} from './notificationUtils';
|
||||||
|
import {useMemoize} from '../../utils/useMemoize';
|
||||||
|
|
||||||
type NotificationExtra = {
|
type NotificationExtra = {
|
||||||
onOpen: () => void;
|
onOpen: () => void;
|
||||||
|
onHideSimilar: (() => void) | null;
|
||||||
|
onHidePlugin: () => void;
|
||||||
clientName: string | undefined;
|
clientName: string | undefined;
|
||||||
appName: string | undefined;
|
appName: string | undefined;
|
||||||
pluginName: string;
|
pluginName: string;
|
||||||
@@ -49,6 +58,11 @@ const CollapseContainer = styled.div({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ItemContainer = styled(Layout.Container)({
|
||||||
|
'.notification-item-action': {visibility: 'hidden'},
|
||||||
|
':hover': {'.notification-item-action': {visibility: 'visible'}},
|
||||||
|
});
|
||||||
|
|
||||||
function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
|
function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
|
||||||
const detailView =
|
const detailView =
|
||||||
typeof detail === 'string' ? (
|
typeof detail === 'string' ? (
|
||||||
@@ -92,6 +106,8 @@ function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
|
|||||||
function NotificationEntry({notification}: {notification: PluginNotification}) {
|
function NotificationEntry({notification}: {notification: PluginNotification}) {
|
||||||
const {
|
const {
|
||||||
onOpen,
|
onOpen,
|
||||||
|
onHideSimilar,
|
||||||
|
onHidePlugin,
|
||||||
message,
|
message,
|
||||||
title,
|
title,
|
||||||
clientName,
|
clientName,
|
||||||
@@ -100,17 +116,43 @@ function NotificationEntry({notification}: {notification: PluginNotification}) {
|
|||||||
iconName,
|
iconName,
|
||||||
} = notification;
|
} = notification;
|
||||||
|
|
||||||
|
const actions = useMemo(
|
||||||
|
() => (
|
||||||
|
<Layout.Horizontal className="notification-item-action">
|
||||||
|
<Dropdown
|
||||||
|
overlay={
|
||||||
|
<Menu>
|
||||||
|
{onHideSimilar && (
|
||||||
|
<Menu.Item key="hide_similar" onClick={onHideSimilar}>
|
||||||
|
Hide Similar
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item key="hide_plugin" onClick={onHidePlugin}>
|
||||||
|
Hide {pluginName}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
}>
|
||||||
|
<Button type="text" size="small" icon={<EllipsisOutlined />} />
|
||||||
|
</Dropdown>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
),
|
||||||
|
[onHideSimilar, onHidePlugin, pluginName],
|
||||||
|
);
|
||||||
|
|
||||||
const icon = iconName ? (
|
const icon = iconName ? (
|
||||||
<Glyph name={iconName} size={16} color={theme.primaryColor} />
|
<Glyph name={iconName} size={16} color={theme.primaryColor} />
|
||||||
) : (
|
) : (
|
||||||
<ExclamationCircleOutlined style={{color: theme.primaryColor}} />
|
<ExclamationCircleOutlined style={{color: theme.primaryColor}} />
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Layout.Container gap="small" pad="medium">
|
<ItemContainer gap="small" pad="medium">
|
||||||
|
<Layout.Right center>
|
||||||
<Layout.Horizontal gap="tiny" center>
|
<Layout.Horizontal gap="tiny" center>
|
||||||
{icon}
|
{icon}
|
||||||
<Text style={{fontSize: theme.fontSize.smallBody}}>{pluginName}</Text>
|
<Text style={{fontSize: theme.fontSize.smallBody}}>{pluginName}</Text>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
|
{actions}
|
||||||
|
</Layout.Right>
|
||||||
<Title level={4} ellipsis={{rows: 2}}>
|
<Title level={4} ellipsis={{rows: 2}}>
|
||||||
{title}
|
{title}
|
||||||
</Title>
|
</Title>
|
||||||
@@ -123,7 +165,7 @@ function NotificationEntry({notification}: {notification: PluginNotification}) {
|
|||||||
Open {pluginName}
|
Open {pluginName}
|
||||||
</Button>
|
</Button>
|
||||||
<DetailCollapse detail={message} />
|
<DetailCollapse detail={message} />
|
||||||
</Layout.Container>
|
</ItemContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,25 +213,16 @@ export function Notification() {
|
|||||||
[clientPlugins, devicePlugins],
|
[clientPlugins, devicePlugins],
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeNotifications = useStore(
|
const notifications = useStore((state) => state.notifications);
|
||||||
(state) => state.notifications.activeNotifications,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const activeNotifications = useMemoize(filterNotifications, [
|
||||||
|
notifications.activeNotifications,
|
||||||
|
notifications.blacklistedPlugins,
|
||||||
|
notifications.blacklistedCategories,
|
||||||
|
]);
|
||||||
const displayedNotifications: Array<PluginNotification> = useMemo(
|
const displayedNotifications: Array<PluginNotification> = useMemo(
|
||||||
() =>
|
() =>
|
||||||
activeNotifications
|
activeNotifications.map((noti) => {
|
||||||
.filter(
|
|
||||||
(noti) =>
|
|
||||||
noti.notification.title
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes(searchString.toLocaleLowerCase()) ||
|
|
||||||
(typeof noti.notification.message === 'string'
|
|
||||||
? noti.notification.message
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes(searchString.toLocaleLowerCase())
|
|
||||||
: false),
|
|
||||||
)
|
|
||||||
.map((noti) => {
|
|
||||||
const plugin = getPlugin(noti.pluginId);
|
const plugin = getPlugin(noti.pluginId);
|
||||||
const client = getClientQuery(noti.client);
|
const client = getClientQuery(noti.client);
|
||||||
return {
|
return {
|
||||||
@@ -202,13 +235,29 @@ export function Notification() {
|
|||||||
deepLinkPayload: noti.notification.action,
|
deepLinkPayload: noti.notification.action,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
onHideSimilar: noti.notification.category
|
||||||
|
? () =>
|
||||||
|
dispatch(
|
||||||
|
updateCategoryBlacklist([
|
||||||
|
...notifications.blacklistedCategories,
|
||||||
|
noti.notification.category!,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onHidePlugin: () =>
|
||||||
|
dispatch(
|
||||||
|
updatePluginBlacklist([
|
||||||
|
...notifications.blacklistedPlugins,
|
||||||
|
noti.pluginId,
|
||||||
|
]),
|
||||||
|
),
|
||||||
clientName: client?.device_id,
|
clientName: client?.device_id,
|
||||||
appName: client?.app,
|
appName: client?.app,
|
||||||
pluginName: plugin?.title ?? noti.pluginId,
|
pluginName: plugin?.title ?? noti.pluginId,
|
||||||
iconName: plugin?.icon,
|
iconName: plugin?.icon,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
[activeNotifications, getPlugin, getClientQuery, searchString, dispatch],
|
[activeNotifications, notifications, getPlugin, getClientQuery, dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* 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 {PluginNotification} from '../../../reducers/notifications';
|
||||||
|
import {filterNotifications} from '../notificationUtils';
|
||||||
|
|
||||||
|
const PLUGIN_COUNT = 3;
|
||||||
|
const CLIENT_COUNT = 2;
|
||||||
|
const CATEGORY_LABEL_COUNT = 2 * PLUGIN_COUNT * CLIENT_COUNT;
|
||||||
|
const CATEGORY_LABEL = 'some category';
|
||||||
|
|
||||||
|
const unfilteredNotifications: Array<PluginNotification> = [...Array(20)].map(
|
||||||
|
(_, idx) => ({
|
||||||
|
notification: {
|
||||||
|
id: `${idx}`,
|
||||||
|
title: `title ${idx}`,
|
||||||
|
message: `message ${idx}`,
|
||||||
|
severity: 'warning',
|
||||||
|
category: idx % CATEGORY_LABEL_COUNT ? undefined : CATEGORY_LABEL,
|
||||||
|
},
|
||||||
|
pluginId: `plugin_${idx % PLUGIN_COUNT}`,
|
||||||
|
client: `client_${idx % CLIENT_COUNT}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
test('Filter nothing', async () => {
|
||||||
|
const filteredNotifications = filterNotifications(
|
||||||
|
unfilteredNotifications.slice(),
|
||||||
|
);
|
||||||
|
expect(filteredNotifications.length).toBe(unfilteredNotifications.length);
|
||||||
|
expect(filteredNotifications).toEqual(unfilteredNotifications);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Filter by single pluginId', async () => {
|
||||||
|
const blockedPluginId = 'plugin_0';
|
||||||
|
const filteredNotifications = filterNotifications(
|
||||||
|
unfilteredNotifications.slice(),
|
||||||
|
[blockedPluginId],
|
||||||
|
);
|
||||||
|
const expectedNotification = unfilteredNotifications
|
||||||
|
.slice()
|
||||||
|
.filter((_, idx) => idx % PLUGIN_COUNT);
|
||||||
|
|
||||||
|
expect(filteredNotifications.length).toBe(expectedNotification.length);
|
||||||
|
expect(filteredNotifications).toEqual(expectedNotification);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Filter by multiple pluginId', async () => {
|
||||||
|
const blockedPluginIds = ['plugin_1', 'plugin_2'];
|
||||||
|
const filteredNotifications = filterNotifications(
|
||||||
|
unfilteredNotifications.slice(),
|
||||||
|
blockedPluginIds,
|
||||||
|
);
|
||||||
|
const expectedNotification = unfilteredNotifications
|
||||||
|
.slice()
|
||||||
|
.filter((_, idx) => !(idx % PLUGIN_COUNT));
|
||||||
|
|
||||||
|
expect(filteredNotifications.length).toBe(expectedNotification.length);
|
||||||
|
expect(filteredNotifications).toEqual(expectedNotification);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Filter by category', async () => {
|
||||||
|
const blockedCategory = CATEGORY_LABEL;
|
||||||
|
const filteredNotifications = filterNotifications(
|
||||||
|
unfilteredNotifications.slice(),
|
||||||
|
[],
|
||||||
|
[blockedCategory],
|
||||||
|
);
|
||||||
|
const expectedNotification = unfilteredNotifications
|
||||||
|
.slice()
|
||||||
|
.filter((_, idx) => idx % CATEGORY_LABEL_COUNT);
|
||||||
|
|
||||||
|
expect(filteredNotifications.length).toBe(expectedNotification.length);
|
||||||
|
expect(filteredNotifications).toEqual(expectedNotification);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Filter by pluginId and category', async () => {
|
||||||
|
const blockedCategory = CATEGORY_LABEL;
|
||||||
|
const blockedPluginId = 'plugin_1';
|
||||||
|
const filteredNotifications = filterNotifications(
|
||||||
|
unfilteredNotifications.slice(),
|
||||||
|
[blockedPluginId],
|
||||||
|
[blockedCategory],
|
||||||
|
);
|
||||||
|
const expectedNotification = unfilteredNotifications
|
||||||
|
.slice()
|
||||||
|
.filter((_, idx) => idx % CATEGORY_LABEL_COUNT && idx % PLUGIN_COUNT !== 1);
|
||||||
|
|
||||||
|
expect(filteredNotifications.length).toBe(expectedNotification.length);
|
||||||
|
expect(filteredNotifications).toEqual(expectedNotification);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Filter by string searching', async () => {
|
||||||
|
const searchString = 'age 5';
|
||||||
|
const filteredNotifications = filterNotifications(
|
||||||
|
unfilteredNotifications.slice(),
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
searchString,
|
||||||
|
);
|
||||||
|
const expectedNotification = [unfilteredNotifications[5]];
|
||||||
|
|
||||||
|
expect(filteredNotifications.length).toBe(expectedNotification.length);
|
||||||
|
expect(filteredNotifications).toEqual(expectedNotification);
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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 {PluginNotification} from '../../reducers/notifications';
|
||||||
|
|
||||||
|
export function filterNotifications(
|
||||||
|
notifications: Array<PluginNotification>,
|
||||||
|
blocklistedPlugins?: Array<string>,
|
||||||
|
blocklistedCategories?: Array<string>,
|
||||||
|
searchString?: string,
|
||||||
|
): Array<PluginNotification> {
|
||||||
|
return notifications
|
||||||
|
.filter((noti) =>
|
||||||
|
blocklistedPlugins ? !blocklistedPlugins.includes(noti.pluginId) : true,
|
||||||
|
)
|
||||||
|
.filter((noti) =>
|
||||||
|
blocklistedCategories && noti.notification.category
|
||||||
|
? !blocklistedCategories?.includes(noti.notification.category)
|
||||||
|
: true,
|
||||||
|
)
|
||||||
|
.filter((noti) =>
|
||||||
|
searchString
|
||||||
|
? noti.notification.title
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(searchString.toLocaleLowerCase()) ||
|
||||||
|
(typeof noti.notification.message === 'string'
|
||||||
|
? noti.notification.message
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(searchString.toLocaleLowerCase())
|
||||||
|
: false)
|
||||||
|
: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user