Summary: Changelog: Improved plugin / device / app selection handing. During some refactorings I discovered that the `connetions.selectedApp` field contained sometimes an application id, and sometimes just the name. This caused inconsistent behavior especially in unit tests. I've cleaned that up, and renamed it to `selectedAppId` where applicable, to make the distinction more clear. And, in contrast, userPreferredApp now always has a name, not an id. During refactoring our existing selection update logic was quite in the way, which was overcomplicated still, since during the sandy chrome migration, the reducers needed to be able to handle both the old UI, and the new application selection UI. That logic has been simplified now, and a lot of tests were added. As a further simplification the preferredApp/Device/Plugin are now only read and used when updating selection, but not when running selectors. Reviewed By: timur-valiev Differential Revision: D31305180 fbshipit-source-id: 2dbd9f9c33950227cc63aa29cc4a98bdd0db8e7a
260 lines
6.2 KiB
TypeScript
260 lines
6.2 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 {Notification} from 'flipper-plugin';
|
|
import {Actions} from './';
|
|
import React from 'react';
|
|
import {getStringFromErrorLike} from '../utils/errors';
|
|
|
|
export const GLOBAL_NOTIFICATION_PLUGIN_ID = 'Flipper';
|
|
|
|
export type PluginNotification = {
|
|
notification: Notification;
|
|
pluginId: string;
|
|
client: null | string; // id
|
|
};
|
|
|
|
export type PluginNotificationReference = {
|
|
notificationId: string;
|
|
pluginId: string;
|
|
client: null | string; // id
|
|
};
|
|
|
|
export type State = {
|
|
activeNotifications: Array<PluginNotification>;
|
|
invalidatedNotifications: Array<PluginNotification>;
|
|
blocklistedPlugins: Array<string>;
|
|
blocklistedCategories: Array<string>;
|
|
clearedNotifications: Set<string>;
|
|
};
|
|
|
|
type ActiveNotificationsAction = {
|
|
type: 'SET_ACTIVE_NOTIFICATIONS';
|
|
payload: {
|
|
notifications: Array<Notification>;
|
|
client: null | string;
|
|
pluginId: string;
|
|
};
|
|
};
|
|
|
|
export type Action =
|
|
| {
|
|
type: 'CLEAR_ALL_NOTIFICATIONS';
|
|
}
|
|
| {
|
|
type: 'SET_ACTIVE_NOTIFICATIONS';
|
|
payload: {
|
|
notifications: Array<Notification>;
|
|
client: null | string;
|
|
pluginId: string;
|
|
};
|
|
}
|
|
| {
|
|
type: 'UPDATE_PLUGIN_BLOCKLIST';
|
|
payload: Array<string>;
|
|
}
|
|
| {
|
|
type: 'UPDATE_CATEGORY_BLOCKLIST';
|
|
payload: Array<string>;
|
|
}
|
|
| {
|
|
type: 'ADD_NOTIFICATION';
|
|
payload: PluginNotification;
|
|
}
|
|
| {
|
|
type: 'REMOVE_NOTIFICATION';
|
|
payload: PluginNotificationReference;
|
|
};
|
|
|
|
const INITIAL_STATE: State = {
|
|
activeNotifications: [],
|
|
invalidatedNotifications: [],
|
|
blocklistedPlugins: [],
|
|
blocklistedCategories: [],
|
|
clearedNotifications: new Set(),
|
|
};
|
|
|
|
export default function reducer(
|
|
state: State = INITIAL_STATE,
|
|
action: Actions,
|
|
): State {
|
|
switch (action.type) {
|
|
case 'SET_ACTIVE_NOTIFICATIONS': {
|
|
return activeNotificationsReducer(state, action);
|
|
}
|
|
case 'CLEAR_ALL_NOTIFICATIONS':
|
|
const markAsCleared = ({
|
|
pluginId,
|
|
notification: {id},
|
|
}: PluginNotification) =>
|
|
state.clearedNotifications.add(`${pluginId}#${id}`);
|
|
|
|
state.activeNotifications.forEach(markAsCleared);
|
|
state.invalidatedNotifications.forEach(markAsCleared);
|
|
// Q: Should this actually delete them, or just invalidate them?
|
|
return {
|
|
...state,
|
|
activeNotifications: [],
|
|
invalidatedNotifications: [],
|
|
};
|
|
case 'UPDATE_PLUGIN_BLOCKLIST':
|
|
return {
|
|
...state,
|
|
blocklistedPlugins: action.payload,
|
|
};
|
|
case 'UPDATE_CATEGORY_BLOCKLIST':
|
|
return {
|
|
...state,
|
|
blocklistedCategories: action.payload,
|
|
};
|
|
case 'ADD_NOTIFICATION':
|
|
return {
|
|
...state,
|
|
// while adding notifications, remove old duplicates
|
|
activeNotifications: [
|
|
...state.activeNotifications.filter(
|
|
(notif) =>
|
|
notif.client !== action.payload.client ||
|
|
notif.pluginId !== action.payload.pluginId ||
|
|
notif.notification.id !== action.payload.notification.id,
|
|
),
|
|
action.payload,
|
|
],
|
|
};
|
|
case 'REMOVE_NOTIFICATION':
|
|
return {
|
|
...state,
|
|
activeNotifications: [
|
|
...state.activeNotifications.filter(
|
|
(notif) =>
|
|
notif.client !== action.payload.client ||
|
|
notif.pluginId !== action.payload.pluginId ||
|
|
notif.notification.id !== action.payload.notificationId,
|
|
),
|
|
],
|
|
};
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
function activeNotificationsReducer(
|
|
state: State,
|
|
action: ActiveNotificationsAction,
|
|
): State {
|
|
const {payload} = action;
|
|
const newActiveNotifications = [];
|
|
const newInactivatedNotifications = state.invalidatedNotifications.slice();
|
|
|
|
const newIDs = new Set(payload.notifications.map((n: Notification) => n.id));
|
|
|
|
for (const activeNotification of state.activeNotifications) {
|
|
if (activeNotification.pluginId !== payload.pluginId) {
|
|
newActiveNotifications.push(activeNotification);
|
|
continue;
|
|
}
|
|
|
|
if (!newIDs.has(activeNotification.notification.id)) {
|
|
newInactivatedNotifications.push(activeNotification);
|
|
}
|
|
}
|
|
|
|
payload.notifications
|
|
.filter(
|
|
({id}: Notification) =>
|
|
!state.clearedNotifications.has(`${payload.pluginId}#${id}`),
|
|
)
|
|
.forEach((notification: Notification) => {
|
|
newActiveNotifications.push({
|
|
pluginId: payload.pluginId,
|
|
client: payload.client,
|
|
notification,
|
|
});
|
|
});
|
|
|
|
return {
|
|
...state,
|
|
activeNotifications: newActiveNotifications,
|
|
invalidatedNotifications: newInactivatedNotifications,
|
|
};
|
|
}
|
|
|
|
export function addNotification(payload: PluginNotification): Action {
|
|
return {
|
|
type: 'ADD_NOTIFICATION',
|
|
payload,
|
|
};
|
|
}
|
|
|
|
export function removeNotification(
|
|
payload: PluginNotificationReference,
|
|
): Action {
|
|
return {
|
|
type: 'REMOVE_NOTIFICATION',
|
|
payload,
|
|
};
|
|
}
|
|
|
|
export function addErrorNotification(
|
|
title: string,
|
|
message: string | React.ReactNode,
|
|
error?: any,
|
|
): Action {
|
|
// TODO: use this method for https://github.com/facebook/flipper/pull/1478/files as well
|
|
console.warn(title, message, error);
|
|
return addNotification({
|
|
client: null,
|
|
pluginId: GLOBAL_NOTIFICATION_PLUGIN_ID,
|
|
notification: {
|
|
id: title,
|
|
title,
|
|
message: error ? (
|
|
<>
|
|
<p>{message}</p>
|
|
<p>{getStringFromErrorLike(error)}</p>
|
|
</>
|
|
) : (
|
|
message
|
|
),
|
|
severity: 'error',
|
|
},
|
|
});
|
|
}
|
|
|
|
export function setActiveNotifications(payload: {
|
|
notifications: Array<Notification>;
|
|
client: null | string;
|
|
pluginId: string;
|
|
}): Action {
|
|
return {
|
|
type: 'SET_ACTIVE_NOTIFICATIONS',
|
|
payload,
|
|
};
|
|
}
|
|
|
|
export function clearAllNotifications(): Action {
|
|
return {
|
|
type: 'CLEAR_ALL_NOTIFICATIONS',
|
|
};
|
|
}
|
|
|
|
export function updatePluginBlocklist(payload: Array<string>): Action {
|
|
return {
|
|
type: 'UPDATE_PLUGIN_BLOCKLIST',
|
|
payload,
|
|
};
|
|
}
|
|
|
|
export function updateCategoryBlocklist(payload: Array<string>): Action {
|
|
return {
|
|
type: 'UPDATE_CATEGORY_BLOCKLIST',
|
|
payload,
|
|
};
|
|
}
|