Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

[MM-22171] Move loadedPlugins into redux-store #5262

Merged
73 changes: 50 additions & 23 deletions actions/websocket_actions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ const mockState = {
},
},
websocket: {},
plugins: {
manifests: {}
}
};

jest.mock('stores/redux_store', () => {
Expand Down Expand Up @@ -779,6 +782,11 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {

expect(mockScript.onload).toBeUndefined();
handlePluginEnabled({data: {manifest}});
mockState.plugins.manifests = {[manifest.id]: manifest};

const dispatchArg = store.dispatch.mock.calls[0][0];
expect(dispatchArg.type).toBe(ActionTypes.ADD_WEBAPP_PLUGIN);
expect(dispatchArg.data).toBe(manifest);

expect(document.createElement).toHaveBeenCalledWith('script');
expect(document.getElementsByTagName).toHaveBeenCalledTimes(1);
Expand All @@ -792,19 +800,20 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
const mockComponent = 'mockRootComponent';
registery.registerRootComponent(mockComponent);

const dispatchArg = store.dispatch.mock.calls[0][0];
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
expect(dispatchArg.name).toBe('Root');
expect(dispatchArg.data.component).toBe(mockComponent);
expect(dispatchArg.data.pluginId).toBe(manifest.id);
const dispatchArg2 = store.dispatch.mock.calls[1][0];
expect(dispatchArg2.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
expect(dispatchArg2.name).toBe('Root');
expect(dispatchArg2.data.component).toBe(mockComponent);
expect(dispatchArg2.data.pluginId).toBe(manifest.id);

// Assert handlePluginEnabled is idempotent
mockScript.onload = undefined;
handlePluginEnabled({data: {manifest}});
expect(mockScript.onload).toBeUndefined();

expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledTimes(2);
expect(console.error).toHaveBeenCalledTimes(0);
mockState.plugins.manifests = {};
});

test('when a plugin is upgraded', () => {
Expand All @@ -831,6 +840,11 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {

expect(mockScript.onload).toBeUndefined();
handlePluginEnabled({data: {manifest}});
mockState.plugins.manifests = {[manifest.id]: manifest};

const dispatchReceivedArg = store.dispatch.mock.calls[0][0];
expect(dispatchReceivedArg.type).toBe(ActionTypes.ADD_WEBAPP_PLUGIN);
expect(dispatchReceivedArg.data).toBe(manifest);

expect(document.createElement).toHaveBeenCalledWith('script');
expect(document.getElementsByTagName).toHaveBeenCalledTimes(1);
Expand All @@ -844,15 +858,16 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
const mockComponent = 'mockRootComponent';
registry.registerRootComponent(mockComponent);

const dispatchReceivedArg = store.dispatch.mock.calls[0][0];
expect(dispatchReceivedArg.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
expect(dispatchReceivedArg.name).toBe('Root');
expect(dispatchReceivedArg.data.component).toBe(mockComponent);
expect(dispatchReceivedArg.data.pluginId).toBe(manifest.id);
const dispatchReceivedArg2 = store.dispatch.mock.calls[1][0];
expect(dispatchReceivedArg2.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
expect(dispatchReceivedArg2.name).toBe('Root');
expect(dispatchReceivedArg2.data.component).toBe(mockComponent);
expect(dispatchReceivedArg2.data.pluginId).toBe(manifest.id);

// Upgrade plugin
mockScript.onload = undefined;
handlePluginEnabled({data: {manifest: manifestv2}});
mockState.plugins.manifests = {[manifestv2.id]: manifestv2};

// Assert upgrade is idempotent
handlePluginEnabled({data: {manifest: manifestv2}});
Expand All @@ -867,18 +882,23 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
const mockComponent2 = 'mockRootComponent2';
registry2.registerRootComponent(mockComponent2);

expect(store.dispatch).toHaveBeenCalledTimes(3);
const dispatchRemovedArg = store.dispatch.mock.calls[1][0];
expect(dispatchRemovedArg.type).toBe(ActionTypes.REMOVED_WEBAPP_PLUGIN);
expect(store.dispatch).toHaveBeenCalledTimes(5);
const dispatchRemovedArg = store.dispatch.mock.calls[2][0];
expect(dispatchRemovedArg.type).toBe(ActionTypes.REMOVE_WEBAPP_PLUGIN);
expect(dispatchRemovedArg.data).toBe(manifestv2);

const dispatchReceivedArg2 = store.dispatch.mock.calls[2][0];
expect(dispatchReceivedArg2.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
expect(dispatchReceivedArg2.name).toBe('Root');
expect(dispatchReceivedArg2.data.component).toBe(mockComponent2);
expect(dispatchReceivedArg2.data.pluginId).toBe(manifest.id);
const dispatchReceivedArg3 = store.dispatch.mock.calls[3][0];
expect(dispatchReceivedArg3.type).toBe(ActionTypes.ADD_WEBAPP_PLUGIN);
expect(dispatchReceivedArg3.data).toBe(manifestv2);

const dispatchReceivedArg4 = store.dispatch.mock.calls[4][0];
expect(dispatchReceivedArg4.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
expect(dispatchReceivedArg4.name).toBe('Root');
expect(dispatchReceivedArg4.data.component).toBe(mockComponent2);
expect(dispatchReceivedArg4.data.pluginId).toBe(manifest.id);

expect(console.error).toHaveBeenCalledTimes(0);
mockState.plugins.manifests = {};
});
});

Expand Down Expand Up @@ -930,20 +950,27 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {

// Enable plugin
handlePluginEnabled({data: {manifest}});
mockState.plugins.manifests = {[manifest.id]: manifest};

const dispatchReceivedArg = store.dispatch.mock.calls[0][0];
expect(dispatchReceivedArg.type).toBe(ActionTypes.ADD_WEBAPP_PLUGIN);
expect(dispatchReceivedArg.data).toBe(manifest);

expect(document.createElement).toHaveBeenCalledWith('script');
expect(document.createElement).toHaveBeenCalledTimes(1);

// Disable plugin
handlePluginDisabled({data: {manifest}});
mockState.plugins.manifests = {};

const dispatchRemovedArg = store.dispatch.mock.calls[1][0];
expect(dispatchRemovedArg.type).toBe(ActionTypes.REMOVE_WEBAPP_PLUGIN);
expect(dispatchRemovedArg.data).toBe(manifest);

// Assert handlePluginDisabled is idempotent
handlePluginDisabled({data: {manifest}});

expect(store.dispatch).toHaveBeenCalledTimes(2);
const dispatchRemovedArg = store.dispatch.mock.calls[0][0];
expect(dispatchRemovedArg.type).toBe(ActionTypes.REMOVED_WEBAPP_PLUGIN);
expect(dispatchRemovedArg.data).toBe(manifest);
expect(store.dispatch).toHaveBeenCalledTimes(3);
expect(console.error).toHaveBeenCalledTimes(0);
});
});
Expand Down
47 changes: 18 additions & 29 deletions plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function initializePlugins() {
return;
}

const {data, error} = await getPlugins()(store.dispatch);
const {data, error} = await getPlugins();
if (error) {
console.error(error); //eslint-disable-line no-console
return;
Expand All @@ -54,24 +54,17 @@ export async function initializePlugins() {
}

// getPlugins queries the server for all enabled plugins
export function getPlugins() {
return async (dispatch) => {
let plugins;
try {
plugins = await Client4.getWebappPlugins();
} catch (error) {
return {error};
}

dispatch({type: ActionTypes.RECEIVED_WEBAPP_PLUGINS, data: plugins});
export async function getPlugins() {
let plugins;
try {
plugins = await Client4.getWebappPlugins();
} catch (error) {
return {error};
}

return {data: plugins};
};
return {data: plugins};
}

// loadedPlugins tracks which plugins have been added as script tags to the page
const loadedPlugins = {};

// describePlugin takes a manifest and spits out a string suitable for console.log messages.
const describePlugin = (manifest) => (
'plugin ' + manifest.id + ', version ' + manifest.version
Expand All @@ -82,15 +75,15 @@ const describePlugin = (manifest) => (
export function loadPlugin(manifest) {
return new Promise((resolve, reject) => {
// Don't load it again if previously loaded
const oldManifest = loadedPlugins[manifest.id];
const oldManifest = store.getState().plugins.manifests[manifest.id];
if (oldManifest && oldManifest.webapp.bundle_path === manifest.webapp.bundle_path) {
resolve();
return;
}

if (oldManifest) {
// upgrading, perform cleanup
store.dispatch({type: ActionTypes.REMOVED_WEBAPP_PLUGIN, data: manifest});
store.dispatch({type: ActionTypes.REMOVE_WEBAPP_PLUGIN, data: manifest});
}

function onLoad() {
Expand Down Expand Up @@ -119,7 +112,7 @@ export function loadPlugin(manifest) {
script.onerror = onError;

document.getElementsByTagName('head')[0].appendChild(script);
loadedPlugins[manifest.id] = manifest;
store.dispatch({type: ActionTypes.ADD_WEBAPP_PLUGIN, data: manifest});
});
}

Expand All @@ -138,14 +131,12 @@ function initializePlugin(manifest) {
// event handlers, and removes the plugin script from the DOM entirely. The plugin is responsible
// for removing any of its registered components.
export function removePlugin(manifest) {
if (!loadedPlugins[manifest.id]) {
if (!(store.getState().plugins.manifests[manifest.id])) {
return;
}
console.log('Removing ' + describePlugin(manifest)); //eslint-disable-line no-console

delete loadedPlugins[manifest.id];

store.dispatch({type: ActionTypes.REMOVED_WEBAPP_PLUGIN, data: manifest});
store.dispatch({type: ActionTypes.REMOVE_WEBAPP_PLUGIN, data: manifest});

const plugin = window.plugins[manifest.id];
if (plugin && plugin.uninitialize) {
Expand Down Expand Up @@ -174,18 +165,16 @@ export async function loadPluginsIfNecessary() {
return;
}

const oldManifests = store.getState().plugins.plugins;
const oldManifests = store.getState().plugins.manifests;

const {error} = await getPlugins()(store.dispatch);
const {data: newManifests, error} = await getPlugins();
if (error) {
console.error(error); //eslint-disable-line no-console
return;
}

const newManifests = store.getState().plugins.plugins;

// Get new plugins and update existing plugins if version changed
Object.values(newManifests).forEach((newManifest) => {
newManifests.forEach((newManifest) => {
const oldManifest = oldManifests[newManifest.id];
if (!oldManifest || oldManifest.version !== newManifest.version) {
loadPlugin(newManifest).catch((loadErr) => {
Expand All @@ -198,7 +187,7 @@ export async function loadPluginsIfNecessary() {
Object.keys(oldManifests).forEach((id) => {
if (!newManifests.hasOwnProperty(id)) {
const oldManifest = oldManifests[id];
store.dispatch({type: ActionTypes.REMOVED_WEBAPP_PLUGIN, data: oldManifest});
store.dispatch({type: ActionTypes.REMOVE_WEBAPP_PLUGIN, data: oldManifest});
removePlugin(oldManifest);
}
});
Expand Down
38 changes: 14 additions & 24 deletions reducers/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,27 +129,17 @@ function removePluginComponent(state, action) {
return state;
}

function plugins(state = {}, action) {
function manifests(state = {}, action) {
switch (action.type) {
case ActionTypes.RECEIVED_WEBAPP_PLUGINS: {
if (action.data) {
const nextState = {};
action.data.forEach((p) => {
nextState[p.id] = p;
});
return nextState;
}
return state;
}
case ActionTypes.RECEIVED_WEBAPP_PLUGIN: {
case ActionTypes.ADD_WEBAPP_PLUGIN: {
if (action.data) {
const nextState = {...state};
nextState[action.data.id] = action.data;
return nextState;
}
return state;
}
case ActionTypes.REMOVED_WEBAPP_PLUGIN: {
case ActionTypes.REMOVE_WEBAPP_PLUGIN: {
if (action.data && state[action.data.id]) {
const nextState = {...state};
Reflect.deleteProperty(nextState, action.data.id);
Expand Down Expand Up @@ -190,8 +180,8 @@ function components(state = {}, action) {
}
case ActionTypes.REMOVED_PLUGIN_COMPONENT:
return removePluginComponent(state, action);
case ActionTypes.RECEIVED_WEBAPP_PLUGIN:
case ActionTypes.REMOVED_WEBAPP_PLUGIN:
case ActionTypes.ADD_WEBAPP_PLUGIN:
case ActionTypes.REMOVE_WEBAPP_PLUGIN:
return removePluginComponents(state, action);
default:
return state;
Expand All @@ -217,8 +207,8 @@ function postTypes(state = {}, action) {
}
case ActionTypes.REMOVED_PLUGIN_POST_COMPONENT:
return removePostPluginComponent(state, action);
case ActionTypes.RECEIVED_WEBAPP_PLUGIN:
case ActionTypes.REMOVED_WEBAPP_PLUGIN:
case ActionTypes.ADD_WEBAPP_PLUGIN:
case ActionTypes.REMOVE_WEBAPP_PLUGIN:
return removePostPluginComponents(state, action);
default:
return state;
Expand All @@ -244,8 +234,8 @@ function postCardTypes(state = {}, action) {
}
case ActionTypes.REMOVED_PLUGIN_POST_CARD_COMPONENT:
return removePostPluginComponent(state, action);
case ActionTypes.RECEIVED_WEBAPP_PLUGIN:
case ActionTypes.REMOVED_WEBAPP_PLUGIN:
case ActionTypes.ADD_WEBAPP_PLUGIN:
case ActionTypes.REMOVE_WEBAPP_PLUGIN:
return removePostPluginComponents(state, action);
default:
return state;
Expand All @@ -270,8 +260,8 @@ function adminConsoleReducers(state = {}, action) {
}
return state;
}
case ActionTypes.RECEIVED_WEBAPP_PLUGIN:
case ActionTypes.REMOVED_WEBAPP_PLUGIN:
case ActionTypes.ADD_WEBAPP_PLUGIN:
case ActionTypes.REMOVE_WEBAPP_PLUGIN:
if (action.data) {
const nextState = {...state};
delete nextState[action.data.id];
Expand Down Expand Up @@ -303,8 +293,8 @@ function adminConsoleCustomComponents(state = {}, action) {

return nextState;
}
case ActionTypes.RECEIVED_WEBAPP_PLUGIN:
case ActionTypes.REMOVED_WEBAPP_PLUGIN: {
case ActionTypes.ADD_WEBAPP_PLUGIN:
case ActionTypes.REMOVE_WEBAPP_PLUGIN: {
if (!action.data || !state[action.data.id]) {
return state;
}
Expand All @@ -322,7 +312,7 @@ function adminConsoleCustomComponents(state = {}, action) {
export default combineReducers({

// object where every key is a plugin id and values are webapp plugin manifests
plugins,
manifests,

// object where every key is a component name and the values are arrays of
// components wrapped in an object that contains an id and plugin id
Expand Down
5 changes: 2 additions & 3 deletions utils/constants.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,8 @@ export const ActionTypes = keyMirror({
RECEIVED_PLUGIN_POST_CARD_COMPONENT: null,
REMOVED_PLUGIN_POST_COMPONENT: null,
REMOVED_PLUGIN_POST_CARD_COMPONENT: null,
RECEIVED_WEBAPP_PLUGINS: null,
RECEIVED_WEBAPP_PLUGIN: null,
REMOVED_WEBAPP_PLUGIN: null,
ADD_WEBAPP_PLUGIN: null,
REMOVE_WEBAPP_PLUGIN: null,
RECEIVED_ADMIN_CONSOLE_REDUCER: null,
REMOVED_ADMIN_CONSOLE_REDUCER: null,
RECEIVED_ADMIN_CONSOLE_CUSTOM_COMPONENT: null,
Expand Down