[MM-34233] Only refresh App bindings if apps plugin is enabled (#5673)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
This commit is contained in:
Michael Kochell 2022-03-28 13:45:29 -04:00 committed by GitHub
parent e086b19d1e
commit 0a4ec73ee3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 119 additions and 36 deletions

View file

@ -232,7 +232,6 @@ export function handleSelectChannel(channelId) {
dispatch(batchActions(actions, 'BATCH_SWITCH_CHANNEL'));
if (appsEnabled(state)) {
//TODO improve sync method
dispatch(fetchAppBindings(currentUserId, channelId));
}
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line

View file

@ -166,6 +166,9 @@ describe('Actions.Views.Channel', () => {
[currentTeamId]: {},
},
},
apps: {
pluginEnabled: true,
},
general: {
config: {
EnableLegacySidebar: 'true',

View file

@ -1,27 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fetchAppBindings, fetchThreadAppBindings} from '@mm-redux/actions/apps';
import {getThreadAppsBindingsChannelId} from '@mm-redux/selectors/entities/apps';
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import AppsTypes from '@mm-redux/action_types/apps';
import {refreshAppBindings} from '@mm-redux/actions/apps';
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
import {appsEnabled} from '@utils/apps';
export function handleRefreshAppsBindings() {
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
const state = getState();
if (!appsEnabled(state)) {
return {data: true};
}
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
const threadChannelID = getThreadAppsBindingsChannelId(state);
if (threadChannelID) {
dispatch(fetchThreadAppBindings(getCurrentUserId(state), threadChannelID));
if (appsEnabled(getState())) {
dispatch(refreshAppBindings());
}
return {data: true};
};
}
export function handleAppsPluginEnabled() {
return {
type: AppsTypes.APPS_PLUGIN_ENABLED,
};
}
export function handleAppsPluginDisabled() {
return {
type: AppsTypes.APPS_PLUGIN_DISABLED,
};
}

View file

@ -7,6 +7,7 @@ import {loadMe} from '@actions/views/user';
import {Client4} from '@client/rest';
import {WebsocketEvents} from '@constants';
import {ChannelTypes, GeneralTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
import {refreshAppBindings} from '@mm-redux/actions/apps';
import {getThreads} from '@mm-redux/actions/threads';
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
import {General} from '@mm-redux/constants';
@ -37,11 +38,12 @@ import {
handleCallScreenOn,
handleCallScreenOff, handleCallUserRaiseHand, handleCallUserUnraiseHand,
} from '@mmproducts/calls/store/actions/websockets';
import {appsConfiguredAsEnabled} from '@utils/apps';
import {getChannelSinceValue} from '@utils/channels';
import {semverFromServerVersion} from '@utils/general';
import websocketClient from '@websocket';
import {handleRefreshAppsBindings} from './apps';
import {handleAppsPluginDisabled, handleAppsPluginEnabled, handleRefreshAppsBindings} from './apps';
import {handleSidebarCategoryCreated, handleSidebarCategoryDeleted, handleSidebarCategoryOrderUpdated, handleSidebarCategoryUpdated} from './categories';
import {
handleChannelConvertedEvent,
@ -158,6 +160,10 @@ export function doReconnect(now: number) {
setChannelRetryFailed(false),
], 'BATCH_WS_SUCCESS'));
if (appsConfiguredAsEnabled(state)) {
dispatch(refreshAppBindings());
}
try {
const {data: me}: any = await dispatch(loadMe(null, null, true));
@ -430,6 +436,10 @@ function handleEvent(msg: WebSocketMessage) {
return dispatch(handleThreadFollowChanged(msg));
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS:
return dispatch(handleRefreshAppsBindings());
case WebsocketEvents.APPS_FRAMEWORK_PLUGIN_ENABLED:
return dispatch(handleAppsPluginEnabled());
case WebsocketEvents.APPS_FRAMEWORK_PLUGIN_DISABLED:
return dispatch(handleAppsPluginDisabled());
case WebsocketEvents.SIDEBAR_CATEGORY_CREATED:
return dispatch(handleSidebarCategoryCreated(msg));
case WebsocketEvents.SIDEBAR_CATEGORY_UPDATED:

View file

@ -84,6 +84,7 @@ describe('Actions.Websocket doReconnect', () => {
const channel2 = TestHelper.fakeChannelWithId(team.id);
const cMember1 = TestHelper.fakeChannelMember(me.id, channel1.id);
const cMember2 = TestHelper.fakeChannelMember(me.id, channel2.id);
const post1 = TestHelper.fakePost(channel1.id);
const currentTeamId = team.id;
const currentUserId = me.id;
@ -92,7 +93,9 @@ describe('Actions.Websocket doReconnect', () => {
const initialState = {
entities: {
general: {
config: {},
config: {
FeatureFlagAppsEnabled: 'false',
},
},
teams: {
currentTeamId,
@ -106,7 +109,7 @@ describe('Actions.Websocket doReconnect', () => {
channels: {
currentChannelId,
channels: {
currentChannelId: channel1,
currentChannelId: channel1.id,
},
},
users: {
@ -119,8 +122,20 @@ describe('Actions.Websocket doReconnect', () => {
myPreferences: {},
},
posts: {
posts: {},
postsInChannel: {},
posts: {
[post1.id]: post1,
},
postsInChannel: {
[channel1.id]: [
{
recent: true,
order: [post1.id],
},
],
},
},
apps: {
pluginEnabled: true,
},
},
websocket: {
@ -128,6 +143,13 @@ describe('Actions.Websocket doReconnect', () => {
lastConnectAt: 0,
lastDisconnectAt: 0,
},
views: {
channel: {
lastGetPosts: {
[channel1.id]: 1,
},
},
},
};
beforeAll(async () => {
@ -170,14 +192,12 @@ describe('Actions.Websocket doReconnect', () => {
await TestHelper.tearDown();
});
it('handle doReconnect', async () => {
it('handle doReconnect base case', async () => {
const state = {...initialState};
const testStore = await mockStore(state);
const timestamp = 1000;
const expectedActions = [
'BATCH_WS_SUCCESS',
];
const expectedMissingActions = [
'BATCH_WS_RECONNECT',
];
@ -197,7 +217,6 @@ describe('Actions.Websocket doReconnect', () => {
await TestHelper.wait(300);
const actionTypes = testStore.getActions().map((a) => a.type);
expect(actionTypes).toEqual(expectedActions);
expect(actionTypes).not.toEqual(expect.arrayContaining(expectedMissingActions));
});
it('handle doReconnect after the current channel was archived or the user left it', async () => {
@ -260,8 +279,6 @@ describe('Actions.Websocket doReconnect', () => {
const timestamp = 1000;
const expectedActions = [
'BATCH_WS_SUCCESS',
];
const expectedMissingActions = [
'BATCH_WS_RECONNECT',
];
@ -282,7 +299,6 @@ describe('Actions.Websocket doReconnect', () => {
const actions = testStore.getActions().map((a) => a.type);
expect(actions).toEqual(expect.arrayContaining(expectedActions));
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
});
it('handle doReconnect after the current channel was archived and setting is off', async () => {

View file

@ -36,6 +36,7 @@ describe('AppCommandParser', () => {
bindings,
threadBindings: bindings,
threadBindingsForms: {},
pluginEnabled: true,
},
},
} as any;

View file

@ -32,6 +32,7 @@ const makeStore = async (bindings: AppBinding[]) => {
bindingsForms: {},
threadBindings: bindings,
threadBindingsForms: {},
pluginEnabled: true,
},
},
} as any;

View file

@ -28,7 +28,10 @@ const makeStore = async (bindings: AppBinding[]) => {
...reduxTestState,
entities: {
...reduxTestState.entities,
apps: {bindings},
apps: {
bindings,
pluginEnabled: true,
},
},
} as any;
const testStore = await mockStore(initialState);

View file

@ -47,6 +47,8 @@ const WebsocketEvents = {
THREAD_FOLLOW_CHANGED: 'thread_follow_changed',
THREAD_READ_CHANGED: 'thread_read_changed',
APPS_FRAMEWORK_REFRESH_BINDINGS: 'custom_com.mattermost.apps_refresh_bindings',
APPS_FRAMEWORK_PLUGIN_ENABLED: 'custom_com.mattermost.apps_plugin_enabled',
APPS_FRAMEWORK_PLUGIN_DISABLED: 'custom_com.mattermost.apps_plugin_disabled',
SIDEBAR_CATEGORY_CREATED: 'sidebar_category_created',
SIDEBAR_CATEGORY_UPDATED: 'sidebar_category_updated',
SIDEBAR_CATEGORY_DELETED: 'sidebar_category_deleted',

View file

@ -5,8 +5,10 @@ import keyMirror from '@mm-redux/utils/key_mirror';
export default keyMirror({
RECEIVED_APP_BINDINGS: null,
RECEIVED_THREAD_APP_BINDINGS: null,
CLEAR_APP_BINDINGS: null,
FAILED_TO_FETCH_APP_BINDINGS: null,
CLEAR_THREAD_APP_BINDINGS: null,
RECEIVED_APP_COMMAND_FORM: null,
RECEIVED_APP_RHS_COMMAND_FORM: null,
APPS_PLUGIN_ENABLED: null,
APPS_PLUGIN_DISABLED: null,
});

View file

@ -3,20 +3,38 @@
import {Client4} from '@client/rest';
import {AppsTypes} from '@mm-redux/action_types';
import {getThreadAppsBindingsChannelId} from '@mm-redux/selectors/entities/apps';
import {getChannel} from '@mm-redux/selectors/entities/channels';
import {getCurrentChannelId, getCurrentTeamId, getCurrentUserId} from '@mm-redux/selectors/entities/common';
import {ActionFunc, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
import {bindClientFunc} from './helpers';
export function refreshAppBindings(): ActionFunc {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
const threadChannelID = getThreadAppsBindingsChannelId(state);
if (threadChannelID) {
dispatch(fetchThreadAppBindings(getCurrentUserId(state), threadChannelID));
}
return {data: true};
};
}
export function fetchAppBindings(userID: string, channelID: string): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const channel = getChannel(getState(), channelID);
const teamID = channel?.team_id || '';
const state = getState();
const channel = getChannel(state, channelID);
const teamID = channel?.team_id || getCurrentTeamId(state);
return dispatch(bindClientFunc({
clientFunc: () => Client4.getAppsBindings(userID, channelID, teamID),
onSuccess: AppsTypes.RECEIVED_APP_BINDINGS,
onFailure: AppsTypes.CLEAR_APP_BINDINGS,
onFailure: AppsTypes.FAILED_TO_FETCH_APP_BINDINGS,
}));
};
}
@ -33,6 +51,7 @@ export function fetchThreadAppBindings(userID: string, channelID: string): Actio
},
onSuccess: AppsTypes.RECEIVED_THREAD_APP_BINDINGS,
onRequest: AppsTypes.CLEAR_THREAD_APP_BINDINGS,
onFailure: AppsTypes.FAILED_TO_FETCH_APP_BINDINGS,
}));
};
}

View file

@ -17,7 +17,8 @@ export function bindings(state: AppBinding[] = [], action: GenericAction): AppBi
}
return newBindings;
}
case AppsTypes.CLEAR_APP_BINDINGS:
case AppsTypes.FAILED_TO_FETCH_APP_BINDINGS:
case AppsTypes.APPS_PLUGIN_DISABLED:
if (state.length > 0) {
return [];
}
@ -42,12 +43,11 @@ export function bindingsForms(state: AppCommandFormMap = {}, action: GenericActi
};
return newState;
}
case AppsTypes.CLEAR_APP_BINDINGS: {
case AppsTypes.FAILED_TO_FETCH_APP_BINDINGS:
if (Object.keys(state).length) {
return {};
}
return state;
}
default:
return state;
}
@ -108,10 +108,26 @@ export function threadBindingsForms(state: AppCommandFormMap = {}, action: Gener
}
}
export function pluginEnabled(state = true, action: GenericAction): boolean {
switch (action.type) {
case AppsTypes.APPS_PLUGIN_ENABLED:
return true;
case AppsTypes.APPS_PLUGIN_DISABLED:
return false;
case AppsTypes.RECEIVED_APP_BINDINGS:
return true;
case AppsTypes.FAILED_TO_FETCH_APP_BINDINGS:
return false;
default:
return state;
}
}
export default (combineReducers({
bindings,
bindingsForms,
threadBindings,
threadBindingsForms,
threadBindingsChannelId,
pluginEnabled,
}) as (b: AppsState, a: GenericAction) => AppsState);

View file

@ -21,6 +21,7 @@ export type AppsState = {
threadBindings: AppBinding[];
threadBindingsForms: AppCommandFormMap;
threadBindingsChannelId: string;
pluginEnabled: boolean;
};
export type AppBinding = {

View file

@ -6,11 +6,19 @@ import {AppBinding, AppCall, AppCallRequest, AppCallResponse, AppCallValues, App
import {Config} from '@mm-redux/types/config';
import {GlobalState} from '@mm-redux/types/store';
export function appsEnabled(state: GlobalState): boolean { // eslint-disable-line @typescript-eslint/no-unused-vars
export function appsConfiguredAsEnabled(state: GlobalState): boolean { // eslint-disable-line @typescript-eslint/no-unused-vars
const enabled = getConfig(state)?.['FeatureFlagAppsEnabled' as keyof Partial<Config>];
return enabled === 'true';
}
export function appsPluginIsEnabled(state: GlobalState): boolean {
return state.entities.apps.pluginEnabled;
}
export function appsEnabled(state: GlobalState): boolean {
return appsConfiguredAsEnabled(state) && appsPluginIsEnabled(state);
}
export function cleanBinding(binding: AppBinding, topLocation: string): AppBinding {
return cleanBindingRec(binding, topLocation, 0);
}