Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
16
webapp/channels/src/selectors/actions_menu.ts
Обычный файл
16
webapp/channels/src/selectors/actions_menu.ts
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
export function showActionsDropdownPulsatingDot(state: GlobalState): boolean {
|
||||
if (getIsMobileView(state)) {
|
||||
return false;
|
||||
}
|
||||
const actionsMenuTutorialState = get(state, Preferences.CATEGORY_ACTIONS_MENU, Preferences.NAME_ACTIONS_MENU_TUTORIAL_STATE, false);
|
||||
const modalAlreadyViewed = actionsMenuTutorialState && JSON.parse(actionsMenuTutorialState)[Preferences.ACTIONS_MENU_VIEWED];
|
||||
return !modalAlreadyViewed;
|
||||
}
|
||||
52
webapp/channels/src/selectors/admin_console.jsx
Обычный файл
52
webapp/channels/src/selectors/admin_console.jsx
Обычный файл
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {cloneDeep} from 'lodash';
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getMySystemPermissions} from 'mattermost-redux/selectors/entities/roles_helpers';
|
||||
import {ResourceToSysConsolePermissionsTable, RESOURCE_KEYS} from 'mattermost-redux/constants/permissions_sysconsole';
|
||||
|
||||
import AdminDefinition from 'components/admin_console/admin_definition.jsx';
|
||||
|
||||
export const getAdminDefinition = createSelector(
|
||||
'getAdminDefinition',
|
||||
() => AdminDefinition,
|
||||
(state) => state.plugins.adminConsoleReducers,
|
||||
(adminDefinition, reducers) => {
|
||||
let result = cloneDeep(AdminDefinition);
|
||||
for (const reducer of Object.values(reducers)) {
|
||||
result = reducer(result);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
export const getAdminConsoleCustomComponents = (state, pluginId) =>
|
||||
state.plugins.adminConsoleCustomComponents[pluginId] || {};
|
||||
|
||||
export const getConsoleAccess = createSelector(
|
||||
'getConsoleAccess',
|
||||
getAdminDefinition,
|
||||
getMySystemPermissions,
|
||||
(adminDefinition, mySystemPermissions) => {
|
||||
const consoleAccess = {read: {}, write: {}};
|
||||
const addEntriesForKey = (entryKey) => {
|
||||
const permissions = ResourceToSysConsolePermissionsTable[entryKey].filter((x) => mySystemPermissions.has(x));
|
||||
consoleAccess.read[entryKey] = permissions.length !== 0;
|
||||
consoleAccess.write[entryKey] = permissions.some((permission) => permission.startsWith('sysconsole_write_'));
|
||||
};
|
||||
const mapAccessValuesForKey = ([key]) => {
|
||||
if (typeof RESOURCE_KEYS[key.toUpperCase()] === 'object') {
|
||||
Object.values(RESOURCE_KEYS[key.toUpperCase()]).forEach((entry) => {
|
||||
addEntriesForKey(entry);
|
||||
});
|
||||
} else {
|
||||
addEntriesForKey(key);
|
||||
}
|
||||
};
|
||||
Object.entries(adminDefinition).forEach(mapAccessValuesForKey);
|
||||
return consoleAccess;
|
||||
},
|
||||
);
|
||||
57
webapp/channels/src/selectors/admin_console.test.jsx
Обычный файл
57
webapp/channels/src/selectors/admin_console.test.jsx
Обычный файл
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AdminDefinition from 'components/admin_console/admin_definition.jsx';
|
||||
|
||||
import {getAdminDefinition} from 'selectors/admin_console.jsx';
|
||||
|
||||
describe('Selectors.AdminConsole', () => {
|
||||
describe('get admin definitions', () => {
|
||||
it('should return the default admin definition if there is not plugins', () => {
|
||||
const state = {plugins: {adminConsoleReducers: {}}};
|
||||
expect(getAdminDefinition(state)).toEqual(AdminDefinition);
|
||||
});
|
||||
|
||||
it('should allow to remove everything with a plugin', () => {
|
||||
const result = getAdminDefinition({
|
||||
plugins: {
|
||||
adminConsoleReducers: {clean: () => ({})},
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should allow to add a value to the existing definition', () => {
|
||||
const result = getAdminDefinition({
|
||||
plugins: {
|
||||
adminConsoleReducers: {
|
||||
'add-something': (data) => {
|
||||
data.something = 'test';
|
||||
return data;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.something).toEqual('test');
|
||||
});
|
||||
|
||||
it('should allow to use multiple plugins', () => {
|
||||
const result = getAdminDefinition({
|
||||
plugins: {
|
||||
adminConsoleReducers: {
|
||||
'add-something': (data) => {
|
||||
data.something = 'test';
|
||||
return data;
|
||||
},
|
||||
'add-other-thing': (data) => {
|
||||
data.otherThing = 'other-thing';
|
||||
return data;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.something).toEqual('test');
|
||||
expect(result.otherThing).toEqual('other-thing');
|
||||
});
|
||||
});
|
||||
});
|
||||
86
webapp/channels/src/selectors/cloud.ts
Обычный файл
86
webapp/channels/src/selectors/cloud.ts
Обычный файл
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Invoice, Subscription} from '@mattermost/types/cloud';
|
||||
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export enum InquiryType {
|
||||
Technical = 'technical',
|
||||
Sales = 'sales',
|
||||
Billing = 'billing',
|
||||
}
|
||||
|
||||
export enum TechnicalInquiryIssue {
|
||||
AdminConsole = 'admin_console',
|
||||
MattermostMessaging = 'mm_messaging',
|
||||
DataExport = 'data_export',
|
||||
Other = 'other',
|
||||
}
|
||||
|
||||
export enum SalesInquiryIssue {
|
||||
AboutPurchasing = 'about_purchasing',
|
||||
CancelAccount = 'cancel_account',
|
||||
PurchaseNonprofit = 'purchase_nonprofit',
|
||||
TrialQuestions = 'trial_questions',
|
||||
UpgradeEnterprise = 'upgrade_enterprise',
|
||||
SomethingElse = 'something_else',
|
||||
}
|
||||
|
||||
type Issue = SalesInquiryIssue | TechnicalInquiryIssue
|
||||
|
||||
export const getCloudContactUsLink: (state: GlobalState) => (inquiry: InquiryType, inquiryIssue?: Issue) => string = createSelector(
|
||||
'getCloudContactUsLink',
|
||||
getConfig,
|
||||
getCurrentUser,
|
||||
(config, user) => {
|
||||
// cloud/contact-us with query params for name, email and inquiry
|
||||
const cwsUrl = config.CWSURL;
|
||||
const fullName = `${user.first_name} ${user.last_name}`;
|
||||
return (inquiry: InquiryType, inquiryIssue?: Issue) => {
|
||||
const inquiryIssueQuery = inquiryIssue ? `&inquiry-issue=${inquiryIssue}` : '';
|
||||
|
||||
return `${cwsUrl}/cloud/contact-us?email=${encodeURIComponent(user.email)}&name=${encodeURIComponent(fullName)}&inquiry=${inquiry}${inquiryIssueQuery}`;
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const getExpandSeatsLink: (state: GlobalState) => (licenseId: string) => string = createSelector(
|
||||
'getExpandSeatsLink',
|
||||
getConfig,
|
||||
(config) => {
|
||||
const cwsUrl = config.CWSURL;
|
||||
return (licenseId: string) => {
|
||||
return `${cwsUrl}/subscribe/expand?licenseId=${licenseId}`;
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const getCloudDelinquentInvoices = createSelector(
|
||||
'getCloudDelinquentInvoices',
|
||||
(state: GlobalState) => state.entities.cloud.invoices as Record<string, Invoice>,
|
||||
(invoices: Record<string, Invoice>) => {
|
||||
if (!invoices) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.values(invoices || []).filter((invoice) => invoice.status !== 'paid' && invoice.total > 0);
|
||||
},
|
||||
);
|
||||
|
||||
export const isCloudDelinquencyGreaterThan90Days = createSelector(
|
||||
'isCloudDelinquencyGreaterThan90Days',
|
||||
(state: GlobalState) => state.entities.cloud.subscription as Subscription,
|
||||
(subscription: Subscription) => {
|
||||
if (!subscription || !subscription.delinquent_since) {
|
||||
return false;
|
||||
}
|
||||
const now = new Date();
|
||||
const delinquentDate = new Date(subscription.delinquent_since * 1000);
|
||||
return (Math.floor((now.getTime() - delinquentDate.getTime()) / (1000 * 60 * 60 * 24)) >= 90);
|
||||
},
|
||||
);
|
||||
157
webapp/channels/src/selectors/drafts.test.ts
Обычный файл
157
webapp/channels/src/selectors/drafts.test.ts
Обычный файл
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {StoragePrefixes} from 'utils/constants';
|
||||
|
||||
import {makeGetDrafts, makeGetDraftsByPrefix, makeGetDraftsCount} from './drafts';
|
||||
|
||||
const currentUserId = 'currentUserId';
|
||||
const currentChannelId = 'channelId';
|
||||
const rootId = 'rootId';
|
||||
const currentTeamId = 'teamId';
|
||||
const initialState = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {
|
||||
currentUserId: {
|
||||
id: currentUserId,
|
||||
roles: 'system_role',
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: {id: currentChannelId, team_id: currentTeamId},
|
||||
},
|
||||
channelsInTeam: {
|
||||
currentTeamId: [currentChannelId],
|
||||
},
|
||||
myMembers: {
|
||||
currentChannelId: {
|
||||
channel_id: currentChannelId,
|
||||
user_id: currentUserId,
|
||||
roles: 'channel_role',
|
||||
mention_count: 1,
|
||||
msg_count: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId,
|
||||
teams: {
|
||||
currentTeamId: {
|
||||
id: currentTeamId,
|
||||
name: 'team-1',
|
||||
displayName: 'Team 1',
|
||||
},
|
||||
},
|
||||
myMembers: {
|
||||
currentTeamId: {roles: 'team_role'},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
const commentDrafts = [
|
||||
{
|
||||
message: 'comment_draft',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
channelId: currentChannelId,
|
||||
rootId,
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
|
||||
const channelDrafts = [
|
||||
{
|
||||
message: 'channel_draft',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
channelId: currentChannelId,
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
|
||||
const state = mergeObjects(initialState, {
|
||||
storage: {
|
||||
storage: {
|
||||
[`${StoragePrefixes.COMMENT_DRAFT}${rootId}`]: {
|
||||
value: commentDrafts[0],
|
||||
timestamp: new Date('2022-11-20T23:21:53.552Z'),
|
||||
},
|
||||
[`${StoragePrefixes.DRAFT}${currentChannelId}`]: {
|
||||
value: channelDrafts[0],
|
||||
timestamp: new Date('2022-11-20T23:21:53.552Z'),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const expectedCommentDrafts = [
|
||||
{
|
||||
id: rootId,
|
||||
key: `${StoragePrefixes.COMMENT_DRAFT}${rootId}`,
|
||||
type: 'thread',
|
||||
timestamp: new Date('2022-11-20T23:21:53.552Z'),
|
||||
value: commentDrafts[0],
|
||||
},
|
||||
];
|
||||
|
||||
const expectedChannelDrafts = [
|
||||
{
|
||||
id: currentChannelId,
|
||||
key: `${StoragePrefixes.DRAFT}${currentChannelId}`,
|
||||
type: 'channel',
|
||||
timestamp: new Date('2022-11-20T23:21:53.552Z'),
|
||||
value: channelDrafts[0],
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
|
||||
getMyActiveChannelIds: () => currentChannelId,
|
||||
}));
|
||||
|
||||
describe('makeGetDraftsByPrefix', () => {
|
||||
it('should return comment drafts when given the comment_draft prefix', () => {
|
||||
const getDraftsByPrefix = makeGetDraftsByPrefix(StoragePrefixes.COMMENT_DRAFT);
|
||||
const drafts = getDraftsByPrefix(state);
|
||||
|
||||
expect(drafts).toEqual(expectedCommentDrafts);
|
||||
});
|
||||
|
||||
it('should return channel drafts when given the comment_draft prefix', () => {
|
||||
const getDraftsByPrefix = makeGetDraftsByPrefix(StoragePrefixes.DRAFT);
|
||||
const drafts = getDraftsByPrefix(state);
|
||||
|
||||
expect(drafts).toEqual(expectedChannelDrafts);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeGetDrafts', () => {
|
||||
it('should return all drafts', () => {
|
||||
const getDrafts = makeGetDrafts();
|
||||
const drafts = getDrafts(state);
|
||||
|
||||
expect(drafts).toEqual([...expectedChannelDrafts, ...expectedCommentDrafts]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeGetDraftsCount', () => {
|
||||
it('should return drafts count', () => {
|
||||
const getDraftsCount = makeGetDraftsCount();
|
||||
const draftCount = getDraftsCount(state);
|
||||
|
||||
expect(draftCount).toEqual([...expectedChannelDrafts, ...expectedCommentDrafts].length);
|
||||
});
|
||||
});
|
||||
108
webapp/channels/src/selectors/drafts.ts
Обычный файл
108
webapp/channels/src/selectors/drafts.ts
Обычный файл
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getMyActiveChannelIds} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {get, onboardingTourTipsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {DraftInfo, PostDraft} from 'types/store/draft';
|
||||
|
||||
import {StoragePrefixes} from 'utils/constants';
|
||||
import {getDraftInfoFromKey} from 'utils/storage_utils';
|
||||
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
export type Draft = DraftInfo & {
|
||||
key: keyof GlobalState['storage']['storage'];
|
||||
value: PostDraft;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export type DraftSelector = (state: GlobalState) => Draft[];
|
||||
export type DraftCountSelector = (state: GlobalState) => number;
|
||||
|
||||
export function showDraftsPulsatingDotAndTourTip(state: GlobalState): boolean {
|
||||
if (!onboardingTourTipsEnabled(state) || getIsMobileView(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const draftsTourTipShowed = get(state, Preferences.CATEGORY_DRAFTS, Preferences.DRAFTS_TOUR_TIP_SHOWED, '');
|
||||
const draftsAlreadyViewed = draftsTourTipShowed && JSON.parse(draftsTourTipShowed)[Preferences.DRAFTS_TOUR_TIP_SHOWED];
|
||||
|
||||
return !draftsAlreadyViewed;
|
||||
}
|
||||
|
||||
export function makeGetDraftsByPrefix(prefix: string): DraftSelector {
|
||||
return createSelector(
|
||||
'makeGetDraftsByPrefix',
|
||||
(state: GlobalState) => state.storage?.storage,
|
||||
(storage) => {
|
||||
if (!storage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(storage).flatMap((key) => {
|
||||
const item = storage[key];
|
||||
if (
|
||||
key.startsWith(prefix) &&
|
||||
item != null &&
|
||||
item.value != null &&
|
||||
(item.value.message || item.value.fileInfos?.length > 0) &&
|
||||
item.value.show
|
||||
) {
|
||||
const info = getDraftInfoFromKey(key, prefix);
|
||||
|
||||
if (info === null || !info.id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
key,
|
||||
id: info.id,
|
||||
type: info.type,
|
||||
};
|
||||
}
|
||||
return [];
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all local drafts in storage.
|
||||
* @param excludeInactive determines if we filter drafts based on active channels.
|
||||
*/
|
||||
export function makeGetDrafts(excludeInactive = true): DraftSelector {
|
||||
const getChannelDrafts = makeGetDraftsByPrefix(StoragePrefixes.DRAFT);
|
||||
const getRHSDrafts = makeGetDraftsByPrefix(StoragePrefixes.COMMENT_DRAFT);
|
||||
|
||||
return createSelector(
|
||||
'makeGetDrafts',
|
||||
getChannelDrafts,
|
||||
getRHSDrafts,
|
||||
getMyActiveChannelIds,
|
||||
(channelDrafts, rhsDrafts, myChannels) => (
|
||||
[...channelDrafts, ...rhsDrafts]
|
||||
).
|
||||
filter((draft) => (excludeInactive ? myChannels.indexOf(draft.value.channelId) !== -1 : true)).
|
||||
sort((a, b) => b.value.updateAt - a.value.updateAt),
|
||||
);
|
||||
}
|
||||
|
||||
export function makeGetDraftsCount(): DraftCountSelector {
|
||||
const getChannelDrafts = makeGetDraftsByPrefix(StoragePrefixes.DRAFT);
|
||||
const getRHSDrafts = makeGetDraftsByPrefix(StoragePrefixes.COMMENT_DRAFT);
|
||||
return createSelector(
|
||||
'makeGetDraftsCount',
|
||||
getChannelDrafts,
|
||||
getRHSDrafts,
|
||||
getMyActiveChannelIds,
|
||||
(channelDrafts, rhsDrafts, myChannels) => [...channelDrafts, ...rhsDrafts].
|
||||
filter((draft) => myChannels.indexOf(draft.value.channelId) !== -1).length,
|
||||
);
|
||||
}
|
||||
416
webapp/channels/src/selectors/emojis.test.js
Обычный файл
416
webapp/channels/src/selectors/emojis.test.js
Обычный файл
@@ -0,0 +1,416 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import mergeObjects from 'mattermost-redux/test/merge_objects';
|
||||
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
import Constants, {Preferences} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import * as Selectors from './emojis';
|
||||
|
||||
function makeRecentEmojisPreferences(recentEmojis) {
|
||||
const userId = 'currentUserId';
|
||||
return {
|
||||
[getPreferenceKey(Constants.Preferences.RECENT_EMOJIS, userId)]: {
|
||||
category: Constants.Preferences.RECENT_EMOJIS,
|
||||
name: userId,
|
||||
user_id: userId,
|
||||
value: JSON.stringify(recentEmojis),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('getRecentEmojisData', () => {
|
||||
const currentUserId = 'currentUserId';
|
||||
const baseState = {
|
||||
entities: {
|
||||
emojis: {
|
||||
customEmoji: {},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
EnableCustomEmojis: 'true',
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should return an empty array when there are no recent emojis in storage', () => {
|
||||
expect(Selectors.getRecentEmojisData(baseState)).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return the names of recent system emojis', () => {
|
||||
const recentEmojis = [
|
||||
{name: 'rage', usageCount: 1},
|
||||
{name: 'nauseated_face', usageCount: 2},
|
||||
{name: 'innocent', usageCount: 3},
|
||||
{name: '+1', usageCount: 4},
|
||||
{name: 'sob', usageCount: 5},
|
||||
{name: 'grinning', usageCount: 6},
|
||||
{name: 'mm', usageCount: 7},
|
||||
];
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: makeRecentEmojisPreferences(recentEmojis),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual(recentEmojis);
|
||||
});
|
||||
|
||||
test('should return the names of recent custom emojis', () => {
|
||||
const recentEmojis = [
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'flag-au', usageCount: 1},
|
||||
{name: 'kappa', usageCount: 1},
|
||||
{name: 'gitlab', usageCount: 1},
|
||||
{name: 'thanks', usageCount: 1},
|
||||
];
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
emojis: {
|
||||
customEmojis: {
|
||||
kappa: TestHelper.getCustomEmojiMock({name: 'kappa'}),
|
||||
gitlab: TestHelper.getCustomEmojiMock({name: 'gitlab'}),
|
||||
thanks: TestHelper.getCustomEmojiMock({name: 'thanks'}),
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: makeRecentEmojisPreferences(recentEmojis),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual(recentEmojis);
|
||||
});
|
||||
|
||||
test('should return the names of missing emojis so that they can be loaded later', () => {
|
||||
const recentEmojis = [
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'flag-au', usageCount: 1},
|
||||
{name: 'kappa', usageCount: 1},
|
||||
{name: 'gitlab', usageCount: 1},
|
||||
{name: 'thanks', usageCount: 1},
|
||||
];
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: makeRecentEmojisPreferences(recentEmojis),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual(recentEmojis);
|
||||
});
|
||||
|
||||
describe('should return skin toned emojis in the user\'s current skin tone', () => {
|
||||
const recentEmojis = [
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'astronaut_dark_skin_tone', usageCount: 2},
|
||||
{name: 'male-teacher', usageCount: 3},
|
||||
{name: 'nose_light_skin_tone', usageCount: 4},
|
||||
{name: 'red_haired_woman_medium_light_skin_tone', usageCount: 5},
|
||||
{name: 'point_up_medium_dark_skin_tone', usageCount: 6},
|
||||
];
|
||||
|
||||
test('with no skin tone set', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: makeRecentEmojisPreferences(recentEmojis),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'astronaut', usageCount: 2},
|
||||
{name: 'male-teacher', usageCount: 3},
|
||||
{name: 'nose', usageCount: 4},
|
||||
{name: 'red_haired_woman', usageCount: 5},
|
||||
{name: 'point_up', usageCount: 6},
|
||||
]);
|
||||
});
|
||||
|
||||
test('with default skin tone set', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
...makeRecentEmojisPreferences(recentEmojis),
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: 'default'},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'astronaut', usageCount: 2},
|
||||
{name: 'male-teacher', usageCount: 3},
|
||||
{name: 'nose', usageCount: 4},
|
||||
{name: 'red_haired_woman', usageCount: 5},
|
||||
{name: 'point_up', usageCount: 6},
|
||||
]);
|
||||
});
|
||||
|
||||
test('with light skin tone set', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
...makeRecentEmojisPreferences(recentEmojis),
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: '1F3FB'},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'astronaut_light_skin_tone', usageCount: 2},
|
||||
{name: 'male-teacher_light_skin_tone', usageCount: 3},
|
||||
{name: 'nose_light_skin_tone', usageCount: 4},
|
||||
{name: 'red_haired_woman_light_skin_tone', usageCount: 5},
|
||||
{name: 'point_up_light_skin_tone', usageCount: 6},
|
||||
]);
|
||||
});
|
||||
|
||||
test('with medium light skin tone set', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
...makeRecentEmojisPreferences(recentEmojis),
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: '1F3FC'},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'astronaut_medium_light_skin_tone', usageCount: 2},
|
||||
{name: 'male-teacher_medium_light_skin_tone', usageCount: 3},
|
||||
{name: 'nose_medium_light_skin_tone', usageCount: 4},
|
||||
{name: 'red_haired_woman_medium_light_skin_tone', usageCount: 5},
|
||||
{name: 'point_up_medium_light_skin_tone', usageCount: 6},
|
||||
]);
|
||||
});
|
||||
|
||||
test('with medium skin tone set', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
...makeRecentEmojisPreferences(recentEmojis),
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: '1F3FD'},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'astronaut_medium_skin_tone', usageCount: 2},
|
||||
{name: 'male-teacher_medium_skin_tone', usageCount: 3},
|
||||
{name: 'nose_medium_skin_tone', usageCount: 4},
|
||||
{name: 'red_haired_woman_medium_skin_tone', usageCount: 5},
|
||||
{name: 'point_up_medium_skin_tone', usageCount: 6},
|
||||
]);
|
||||
});
|
||||
|
||||
test('with medium dark skin tone set', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
...makeRecentEmojisPreferences(recentEmojis),
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: '1F3FE'},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'astronaut_medium_dark_skin_tone', usageCount: 2},
|
||||
{name: 'male-teacher_medium_dark_skin_tone', usageCount: 3},
|
||||
{name: 'nose_medium_dark_skin_tone', usageCount: 4},
|
||||
{name: 'red_haired_woman_medium_dark_skin_tone', usageCount: 5},
|
||||
{name: 'point_up_medium_dark_skin_tone', usageCount: 6},
|
||||
]);
|
||||
});
|
||||
|
||||
test('with dark skin tone set', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
...makeRecentEmojisPreferences(recentEmojis),
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: '1F3FF'},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'astronaut_dark_skin_tone', usageCount: 2},
|
||||
{name: 'male-teacher_dark_skin_tone', usageCount: 3},
|
||||
{name: 'nose_dark_skin_tone', usageCount: 4},
|
||||
{name: 'red_haired_woman_dark_skin_tone', usageCount: 5},
|
||||
{name: 'point_up_dark_skin_tone', usageCount: 6},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not change skin tone of emojis with multiple skin tones', () => {
|
||||
const recentEmojis = [
|
||||
{name: 'strawberry', usageCount: 1},
|
||||
{name: 'man_and_woman_holding_hands_medium_light_skin_tone_medium_dark_skin_tone', usageCount: 1},
|
||||
];
|
||||
|
||||
let state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: makeRecentEmojisPreferences(recentEmojis),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual(recentEmojis);
|
||||
|
||||
state = mergeObjects(state, {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: '1F3FB'},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual(recentEmojis);
|
||||
});
|
||||
|
||||
test('should de-duplicate results', () => {
|
||||
const recentEmojis = [
|
||||
{name: 'banana', usageCount: 1},
|
||||
{name: 'banana', usageCount: 1},
|
||||
{name: 'apple', usageCount: 1},
|
||||
{name: 'banana', usageCount: 1},
|
||||
];
|
||||
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: makeRecentEmojisPreferences(recentEmojis),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'apple', usageCount: 1},
|
||||
{name: 'banana', usageCount: 3},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should de-duplicate results with different skin tones', () => {
|
||||
const recentEmojis = [
|
||||
{name: 'ear', usageCount: 1},
|
||||
{name: 'ear_light_skin_tone', usageCount: 1},
|
||||
{name: 'ear_medium_light_skin_tone', usageCount: 1},
|
||||
{name: 'nose_dark_skin_tone', usageCount: 1},
|
||||
{name: 'nose_medium_dark_skin_tone', usageCount: 1},
|
||||
{name: 'nose_light_skin_tone', usageCount: 1},
|
||||
];
|
||||
|
||||
let state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: makeRecentEmojisPreferences(recentEmojis),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'ear', usageCount: 3},
|
||||
{name: 'nose', usageCount: 3},
|
||||
]);
|
||||
|
||||
state = mergeObjects(state, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: '1F3FE'},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toEqual([
|
||||
{name: 'ear_medium_dark_skin_tone', usageCount: 3},
|
||||
{name: 'nose_medium_dark_skin_tone', usageCount: 3},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should only recalculate if relevant preferences change', () => {
|
||||
const recentEmojis = [
|
||||
{name: 'apple', usageCount: 1},
|
||||
{name: 'banana', usageCount: 1},
|
||||
];
|
||||
|
||||
let state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: makeRecentEmojisPreferences(recentEmojis),
|
||||
},
|
||||
},
|
||||
});
|
||||
const previousResult = Selectors.getRecentEmojisData(state);
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toBe(previousResult);
|
||||
|
||||
state = mergeObjects(state, {
|
||||
preferences: {
|
||||
emojis: {
|
||||
customEmoji: {
|
||||
someNewEmoji: TestHelper.getCustomEmojiMock({name: 'someNewCustomEmoji'}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toBe(previousResult);
|
||||
|
||||
state = mergeObjects(state, {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
some_preference: {value: 'some value'},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).toBe(previousResult);
|
||||
|
||||
state = mergeObjects(state, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE)]: {value: '1F3FE'},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.getRecentEmojisData(state)).not.toBe(previousResult);
|
||||
});
|
||||
});
|
||||
119
webapp/channels/src/selectors/emojis.ts
Обычный файл
119
webapp/channels/src/selectors/emojis.ts
Обычный файл
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {Preferences} from 'utils/constants';
|
||||
import EmojiMap from 'utils/emoji_map';
|
||||
import {EmojiIndicesByAlias, Emojis} from 'utils/emoji';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {RecentEmojiData} from '@mattermost/types/emojis';
|
||||
import {convertEmojiSkinTone} from 'utils/emoji_utils';
|
||||
|
||||
export const getEmojiMap = createSelector(
|
||||
'getEmojiMap',
|
||||
getCustomEmojisByName,
|
||||
(customEmojisByName) => {
|
||||
return new EmojiMap(customEmojisByName);
|
||||
},
|
||||
);
|
||||
|
||||
export const getShortcutReactToLastPostEmittedFrom = (state: GlobalState) =>
|
||||
state.views.emoji.shortcutReactToLastPostEmittedFrom;
|
||||
|
||||
export const getRecentEmojisData = createSelector(
|
||||
'getRecentEmojisData',
|
||||
(state: GlobalState) => {
|
||||
return get(
|
||||
state,
|
||||
Preferences.RECENT_EMOJIS,
|
||||
getCurrentUserId(state),
|
||||
'[]',
|
||||
);
|
||||
},
|
||||
getUserSkinTone,
|
||||
(recentEmojis: string, userSkinTone: string) => {
|
||||
if (!recentEmojis) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsedEmojiData: RecentEmojiData[] = JSON.parse(recentEmojis);
|
||||
return normalizeRecentEmojisData(parsedEmojiData, userSkinTone);
|
||||
},
|
||||
);
|
||||
|
||||
export function normalizeRecentEmojisData(data: RecentEmojiData[], userSkinTone: string) {
|
||||
const usageCounts = new Map<string, number>();
|
||||
|
||||
for (const recentEmoji of data) {
|
||||
const emojiIndex = EmojiIndicesByAlias.get(recentEmoji.name) ?? -1;
|
||||
const systemEmoji = Emojis[emojiIndex];
|
||||
|
||||
let normalizedName;
|
||||
if (systemEmoji) {
|
||||
// This is a system emoji, so we may need to change its skin tone
|
||||
normalizedName = convertEmojiSkinTone(systemEmoji, userSkinTone).short_name;
|
||||
} else {
|
||||
// This is a custom emoji, so its name will never change
|
||||
normalizedName = recentEmoji.name;
|
||||
}
|
||||
|
||||
// Dedupe and sum up the usage counts of any duplicated entries
|
||||
const currentCount = usageCounts.get(normalizedName) ?? 0;
|
||||
usageCounts.set(normalizedName, currentCount + recentEmoji.usageCount);
|
||||
}
|
||||
|
||||
const normalizedData = [];
|
||||
for (const [name, usageCount] of usageCounts.entries()) {
|
||||
normalizedData.push({name, usageCount});
|
||||
}
|
||||
|
||||
// Sort emojis by count in the ascending order, matching addRecentEmoji
|
||||
normalizedData.sort((emojiA: RecentEmojiData, emojiB: RecentEmojiData) => emojiA.usageCount - emojiB.usageCount);
|
||||
|
||||
return normalizedData;
|
||||
}
|
||||
|
||||
export const getRecentEmojisNames = createSelector(
|
||||
'getRecentEmojisNames',
|
||||
getRecentEmojisData,
|
||||
(recentEmojisData: RecentEmojiData[]) => {
|
||||
return recentEmojisData.map((emoji) => emoji.name);
|
||||
},
|
||||
);
|
||||
|
||||
export function getUserSkinTone(state: GlobalState): string {
|
||||
return get(state, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE, 'default');
|
||||
}
|
||||
|
||||
export function isCustomEmojiEnabled(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
return config && config.EnableCustomEmoji === 'true';
|
||||
}
|
||||
|
||||
export const getOneClickReactionEmojis = createSelector(
|
||||
'getOneClickReactionEmojis',
|
||||
getEmojiMap,
|
||||
getRecentEmojisNames,
|
||||
(emojiMap, recentEmojis: string[]) => {
|
||||
if (recentEmojis.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (recentEmojis).
|
||||
map((recentEmoji) => emojiMap.get(recentEmoji)).
|
||||
filter(isDefined).
|
||||
slice(-3).
|
||||
reverse();
|
||||
},
|
||||
);
|
||||
|
||||
function isDefined<T>(t: T | undefined): t is T {
|
||||
return Boolean(t);
|
||||
}
|
||||
62
webapp/channels/src/selectors/general.ts
Обычный файл
62
webapp/channels/src/selectors/general.ts
Обычный файл
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTimezoneForUserProfile} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
basename: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function areTimezonesEnabledAndSupported(state: GlobalState) {
|
||||
if (UserAgent.isInternetExplorer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const config = getConfig(state);
|
||||
return config.ExperimentalTimezone === 'true';
|
||||
}
|
||||
|
||||
export function getBasePath(state: GlobalState) {
|
||||
const config = getConfig(state) || {};
|
||||
|
||||
if (config.SiteURL) {
|
||||
return new URL(config.SiteURL).pathname;
|
||||
}
|
||||
|
||||
return window.basename || '/';
|
||||
}
|
||||
|
||||
export const getCurrentUserTimezone = createSelector(
|
||||
'getCurrentUserTimezone',
|
||||
getCurrentUser,
|
||||
areTimezonesEnabledAndSupported,
|
||||
(user, enabledTimezone) => {
|
||||
let timezone;
|
||||
if (enabledTimezone) {
|
||||
const userTimezone = getTimezoneForUserProfile(user);
|
||||
timezone = userTimezone.useAutomaticTimezone ? userTimezone.automaticTimezone : userTimezone.manualTimezone;
|
||||
}
|
||||
|
||||
return timezone;
|
||||
},
|
||||
);
|
||||
|
||||
export function getConnectionId(state: GlobalState) {
|
||||
return state.websocket.connectionId;
|
||||
}
|
||||
|
||||
export function isDevModeEnabled(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
const EnableDeveloper = config && config.EnableDeveloper ? config.EnableDeveloper === 'true' : false;
|
||||
return EnableDeveloper;
|
||||
}
|
||||
179
webapp/channels/src/selectors/i18n.test.js
Обычный файл
179
webapp/channels/src/selectors/i18n.test.js
Обычный файл
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {getCurrentLocale, getTranslations} from 'selectors/i18n';
|
||||
|
||||
describe('selectors/i18n', () => {
|
||||
describe('getCurrentLocale', () => {
|
||||
test('not logged in', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
DefaultClientLocale: 'fr',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: '',
|
||||
profiles: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getCurrentLocale(state)).toEqual('fr');
|
||||
});
|
||||
|
||||
test('logged in', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
DefaultClientLocale: 'fr',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'abcd',
|
||||
profiles: {
|
||||
abcd: {
|
||||
locale: 'de',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getCurrentLocale(state)).toEqual('de');
|
||||
});
|
||||
|
||||
test('returns default locale when invalid user locale specified', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
DefaultClientLocale: 'en',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'abcd',
|
||||
profiles: {
|
||||
abcd: {
|
||||
locale: 'not_valid',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getCurrentLocale(state)).toEqual(General.DEFAULT_LOCALE);
|
||||
});
|
||||
|
||||
describe('locale from query parameter', () => {
|
||||
// Helper function to mock window.location.search with locale query parameter
|
||||
const setWindowLocaleQueryParameter = (locale) => {
|
||||
window.location.search = `?locale=${locale}`;
|
||||
};
|
||||
|
||||
// Helper function to reset window.location.search
|
||||
const resetWindowLocationSearch = () => {
|
||||
window.location.search = '';
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
resetWindowLocationSearch();
|
||||
});
|
||||
|
||||
test('returns locale from query parameter if provided and not logged in', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
DefaultClientLocale: 'fr',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: '',
|
||||
profiles: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setWindowLocaleQueryParameter('ko');
|
||||
|
||||
expect(getCurrentLocale(state)).toEqual('ko');
|
||||
});
|
||||
|
||||
test('returns DefaultClientLocale if locale from query parameter is not valid', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
DefaultClientLocale: 'fr',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: '',
|
||||
profiles: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setWindowLocaleQueryParameter('invalid_locale');
|
||||
|
||||
expect(getCurrentLocale(state)).toEqual('fr');
|
||||
});
|
||||
|
||||
test('returns user locale when logged in and locale is provided in query parameter', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
DefaultClientLocale: 'fr',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'abcd',
|
||||
profiles: {
|
||||
abcd: {
|
||||
locale: 'de',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setWindowLocaleQueryParameter('ko');
|
||||
|
||||
expect(getCurrentLocale(state)).toEqual('de');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTranslations', () => {
|
||||
const state = {
|
||||
views: {
|
||||
i18n: {
|
||||
translations: {
|
||||
en: {
|
||||
'test.hello_world': 'Hello, World!',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('returns loaded translations', () => {
|
||||
expect(getTranslations(state, 'en')).toBe(state.views.i18n.translations.en);
|
||||
});
|
||||
|
||||
test('returns null for unloaded translations', () => {
|
||||
expect(getTranslations(state, 'fr')).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('returns English translations for unsupported locale', () => {
|
||||
// This test will have to be changed if we add support for Gaelic
|
||||
expect(getTranslations(state, 'gd')).toBe(state.views.i18n.translations.en);
|
||||
});
|
||||
});
|
||||
});
|
||||
39
webapp/channels/src/selectors/i18n.ts
Обычный файл
39
webapp/channels/src/selectors/i18n.ts
Обычный файл
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserLocale} from 'mattermost-redux/selectors/entities/i18n';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import * as I18n from 'i18n/i18n';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {Translations} from 'types/store/i18n';
|
||||
|
||||
// This is a placeholder for if we ever implement browser-locale detection
|
||||
export function getCurrentLocale(state: GlobalState): string {
|
||||
// If locale is provided in query parameter and the user is not logged in, we try get locale from param
|
||||
const localeFromParam: string | null = (new URLSearchParams(window.location?.search)).get('locale');
|
||||
const defaultLocale: string | undefined =
|
||||
localeFromParam && I18n.isLanguageAvailable(localeFromParam) ? localeFromParam : getConfig(state).DefaultClientLocale;
|
||||
|
||||
const currentLocale: string = getCurrentUserLocale(state, defaultLocale);
|
||||
if (I18n.isLanguageAvailable(currentLocale)) {
|
||||
return currentLocale;
|
||||
}
|
||||
return General.DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
export function getTranslations(state: GlobalState, locale: string): Translations {
|
||||
const localeInfo = I18n.getLanguageInfo(locale);
|
||||
|
||||
let translations;
|
||||
if (localeInfo) {
|
||||
translations = state.views.i18n.translations[locale];
|
||||
} else {
|
||||
// Default to English if an unsupported locale is specified
|
||||
translations = state.views.i18n.translations.en;
|
||||
}
|
||||
|
||||
return translations;
|
||||
}
|
||||
16
webapp/channels/src/selectors/insights.ts
Обычный файл
16
webapp/channels/src/selectors/insights.ts
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
export function showInsightsPulsatingDot(state: GlobalState): boolean {
|
||||
if (getIsMobileView(state)) {
|
||||
return false;
|
||||
}
|
||||
const insightsTutorialState = get(state, Preferences.CATEGORY_INSIGHTS, Preferences.NAME_INSIGHTS_TUTORIAL_STATE, false);
|
||||
const modalAlreadyViewed = insightsTutorialState && JSON.parse(insightsTutorialState)[Preferences.INSIGHTS_VIEWED];
|
||||
return !modalAlreadyViewed;
|
||||
}
|
||||
118
webapp/channels/src/selectors/lhs.test.ts
Обычный файл
118
webapp/channels/src/selectors/lhs.test.ts
Обычный файл
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as PreferencesSelectors from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import * as Lhs from './lhs';
|
||||
|
||||
jest.mock('selectors/drafts', () => ({
|
||||
makeGetDraftsCount: jest.fn().mockImplementation(() => jest.fn()),
|
||||
}));
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/preferences', () => ({
|
||||
insightsAreEnabled: jest.fn(),
|
||||
isCollapsedThreadsEnabled: jest.fn(),
|
||||
localDraftsAreEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
describe('Selectors.Lhs', () => {
|
||||
let state: unknown;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {};
|
||||
});
|
||||
|
||||
describe('should return the open state of the sidebar menu', () => {
|
||||
[true, false].forEach((expected) => {
|
||||
it(`when open is ${expected}`, () => {
|
||||
state = {
|
||||
views: {
|
||||
lhs: {
|
||||
isOpen: expected,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Lhs.getIsLhsOpen(state as GlobalState)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVisibleLhsStaticPages', () => {
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
views: {
|
||||
lhs: {
|
||||
isOpen: false,
|
||||
currentStaticPageId: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('handles nothing enabled', () => {
|
||||
jest.spyOn(PreferencesSelectors, 'insightsAreEnabled').mockImplementationOnce(() => false);
|
||||
jest.spyOn(PreferencesSelectors, 'isCollapsedThreadsEnabled').mockImplementationOnce(() => false);
|
||||
jest.spyOn(PreferencesSelectors, 'localDraftsAreEnabled').mockImplementationOnce(() => false);
|
||||
jest.spyOn(Lhs, 'getDraftsCount').mockImplementationOnce(() => 0);
|
||||
const items = Lhs.getVisibleStaticPages(state as GlobalState);
|
||||
expect(items).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles insights', () => {
|
||||
jest.spyOn(PreferencesSelectors, 'insightsAreEnabled').mockImplementation(() => true);
|
||||
jest.spyOn(PreferencesSelectors, 'isCollapsedThreadsEnabled').mockImplementation(() => false);
|
||||
jest.spyOn(PreferencesSelectors, 'localDraftsAreEnabled').mockImplementation(() => false);
|
||||
jest.spyOn(Lhs, 'getDraftsCount').mockImplementationOnce(() => 0);
|
||||
const items = Lhs.getVisibleStaticPages(state as GlobalState);
|
||||
expect(items).toEqual([
|
||||
{
|
||||
id: 'activity-and-insights',
|
||||
isVisible: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles threads - default off', () => {
|
||||
jest.spyOn(PreferencesSelectors, 'insightsAreEnabled').mockImplementation(() => false);
|
||||
jest.spyOn(PreferencesSelectors, 'isCollapsedThreadsEnabled').mockImplementation(() => true);
|
||||
jest.spyOn(PreferencesSelectors, 'localDraftsAreEnabled').mockImplementation(() => false);
|
||||
jest.spyOn(Lhs, 'getDraftsCount').mockImplementationOnce(() => 0);
|
||||
const items = Lhs.getVisibleStaticPages(state as GlobalState);
|
||||
expect(items).toEqual([
|
||||
{
|
||||
id: 'threads',
|
||||
isVisible: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not return drafts when empty', () => {
|
||||
jest.spyOn(PreferencesSelectors, 'insightsAreEnabled').mockImplementation(() => false);
|
||||
jest.spyOn(PreferencesSelectors, 'isCollapsedThreadsEnabled').mockImplementation(() => false);
|
||||
jest.spyOn(PreferencesSelectors, 'localDraftsAreEnabled').mockImplementation(() => true);
|
||||
jest.spyOn(Lhs, 'getDraftsCount').mockImplementationOnce(() => 0);
|
||||
const items = Lhs.getVisibleStaticPages(state as GlobalState);
|
||||
expect(items).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return drafts when there are available', () => {
|
||||
jest.spyOn(PreferencesSelectors, 'insightsAreEnabled').mockImplementation(() => false);
|
||||
jest.spyOn(PreferencesSelectors, 'isCollapsedThreadsEnabled').mockImplementation(() => false);
|
||||
jest.spyOn(PreferencesSelectors, 'localDraftsAreEnabled').mockImplementation(() => true);
|
||||
jest.spyOn(Lhs, 'getDraftsCount').mockImplementationOnce(() => 1);
|
||||
const items = Lhs.getVisibleStaticPages(state as GlobalState);
|
||||
expect(items).toEqual([
|
||||
{
|
||||
id: 'drafts',
|
||||
isVisible: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
56
webapp/channels/src/selectors/lhs.ts
Обычный файл
56
webapp/channels/src/selectors/lhs.ts
Обычный файл
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {StaticPage} from 'types/store/lhs';
|
||||
import {makeGetDraftsCount} from 'selectors/drafts';
|
||||
import {
|
||||
insightsAreEnabled,
|
||||
isCollapsedThreadsEnabled,
|
||||
localDraftsAreEnabled,
|
||||
} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
export function getIsLhsOpen(state: GlobalState): boolean {
|
||||
return state.views.lhs.isOpen;
|
||||
}
|
||||
|
||||
export function getCurrentStaticPageId(state: GlobalState): string {
|
||||
return state.views.lhs.currentStaticPageId;
|
||||
}
|
||||
|
||||
export const getDraftsCount = makeGetDraftsCount();
|
||||
|
||||
export const getVisibleStaticPages = createSelector(
|
||||
'getVisibleSidebarStaticPages',
|
||||
insightsAreEnabled,
|
||||
isCollapsedThreadsEnabled,
|
||||
localDraftsAreEnabled,
|
||||
getDraftsCount,
|
||||
(insightsEnabled, collapsedThreadsEnabled, localDraftsEnabled, draftsCount) => {
|
||||
const staticPages: StaticPage[] = [];
|
||||
|
||||
if (insightsEnabled) {
|
||||
staticPages.push({
|
||||
id: 'activity-and-insights',
|
||||
isVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (collapsedThreadsEnabled) {
|
||||
staticPages.push({
|
||||
id: 'threads',
|
||||
isVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (localDraftsEnabled) {
|
||||
staticPages.push({
|
||||
id: 'drafts',
|
||||
isVisible: draftsCount > 0,
|
||||
});
|
||||
}
|
||||
|
||||
return staticPages.filter((item) => item.isVisible);
|
||||
},
|
||||
);
|
||||
61
webapp/channels/src/selectors/local_storage.ts
Обычный файл
61
webapp/channels/src/selectors/local_storage.ts
Обычный файл
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import localStorageStore from 'stores/local_storage_store';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
// getLastViewedChannelName combines data from the Redux store and localStorage to return the
|
||||
// previously selected channel name, returning the default channel if none exists.
|
||||
//
|
||||
// See LocalStorageStore for context.
|
||||
export const getLastViewedChannelName = (state: GlobalState) => {
|
||||
const userId = getCurrentUserId(state);
|
||||
const teamId = getCurrentTeamId(state);
|
||||
|
||||
return localStorageStore.getPreviousChannelName(userId, teamId);
|
||||
};
|
||||
|
||||
export const getPenultimateViewedChannelName = (state: GlobalState) => {
|
||||
const userId = getCurrentUserId(state);
|
||||
const teamId = getCurrentTeamId(state);
|
||||
|
||||
return localStorageStore.getPenultimateChannelName(userId, teamId);
|
||||
};
|
||||
|
||||
// getLastViewedChannelNameByTeamName combines data from the Redux store and localStorage to return
|
||||
// the url to the previously selected channel, returning the path to the default channel if none
|
||||
// exists.
|
||||
//
|
||||
// See LocalStorageStore for context.
|
||||
export const getLastViewedChannelNameByTeamName = (state: GlobalState, teamName: string) => {
|
||||
const userId = getCurrentUserId(state);
|
||||
const team = getTeamByName(state, teamName);
|
||||
const teamId = team && team.id;
|
||||
|
||||
return localStorageStore.getPreviousChannelName(userId, teamId || '');
|
||||
};
|
||||
|
||||
export const getLastViewedTypeByTeamName = (state: GlobalState, teamName: string) => {
|
||||
const userId = getCurrentUserId(state);
|
||||
const team = getTeamByName(state, teamName);
|
||||
const teamId = team && team.id;
|
||||
|
||||
return localStorageStore.getPreviousViewedType(userId, teamId || '');
|
||||
};
|
||||
|
||||
export const getPreviousTeamId = (state: GlobalState) => {
|
||||
const userId = getCurrentUserId(state);
|
||||
|
||||
return localStorageStore.getPreviousTeamId(userId);
|
||||
};
|
||||
|
||||
export const getPreviousTeamLastViewedType = (state: GlobalState) => {
|
||||
const previousTeamID = getPreviousTeamId(state);
|
||||
const userId = getCurrentUserId(state);
|
||||
|
||||
return localStorageStore.getPreviousViewedType(userId, previousTeamID || '', state);
|
||||
};
|
||||
175
webapp/channels/src/selectors/onboarding.test.ts
Обычный файл
175
webapp/channels/src/selectors/onboarding.test.ts
Обычный файл
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
import TestHelper from 'packages/mattermost-redux/test/test_helper';
|
||||
|
||||
import {OnboardingTaskCategory, OnboardingTaskList} from 'components/onboarding_tasks';
|
||||
|
||||
import {RecommendedNextStepsLegacy, Preferences} from 'utils/constants';
|
||||
|
||||
import {getShowTaskListBool} from 'selectors/onboarding';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
describe('selectors/onboarding', () => {
|
||||
describe('getShowTaskListBool', () => {
|
||||
test('first time user logs in aka firstTimeOnboarding', () => {
|
||||
const user = TestHelper.fakeUserWithId();
|
||||
|
||||
const profiles = {
|
||||
[user.id]: user,
|
||||
};
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
users: {
|
||||
currentUserId: user.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
const [showTaskList, firstTimeOnboarding] = getShowTaskListBool(state);
|
||||
expect(showTaskList).toBeTruthy();
|
||||
expect(firstTimeOnboarding).toBeTruthy();
|
||||
});
|
||||
|
||||
test('previous user skipped legacy next steps so not show the tasklist', () => {
|
||||
const prefSkip = {category: Preferences.RECOMMENDED_NEXT_STEPS, name: RecommendedNextStepsLegacy.SKIP, value: 'true'};
|
||||
const prefHide = {category: Preferences.RECOMMENDED_NEXT_STEPS, name: RecommendedNextStepsLegacy.HIDE, value: 'false'};
|
||||
|
||||
const user = TestHelper.fakeUserWithId();
|
||||
|
||||
const profiles = {
|
||||
[user.id]: user,
|
||||
};
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.RECOMMENDED_NEXT_STEPS, RecommendedNextStepsLegacy.HIDE)]: prefHide,
|
||||
[getPreferenceKey(Preferences.RECOMMENDED_NEXT_STEPS, RecommendedNextStepsLegacy.SKIP)]: prefSkip,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: user.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
const [showTaskList, firstTimeOnboarding] = getShowTaskListBool(state);
|
||||
expect(showTaskList).toBeFalsy();
|
||||
expect(firstTimeOnboarding).toBeFalsy();
|
||||
});
|
||||
|
||||
test('previous user hided legacy next steps so not show the tasklist', () => {
|
||||
const prefSkip = {category: Preferences.RECOMMENDED_NEXT_STEPS, name: RecommendedNextStepsLegacy.SKIP, value: 'false'};
|
||||
const prefHide = {category: Preferences.RECOMMENDED_NEXT_STEPS, name: RecommendedNextStepsLegacy.HIDE, value: 'true'};
|
||||
|
||||
const user = TestHelper.fakeUserWithId();
|
||||
|
||||
const profiles = {
|
||||
[user.id]: user,
|
||||
};
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.RECOMMENDED_NEXT_STEPS, RecommendedNextStepsLegacy.HIDE)]: prefHide,
|
||||
[getPreferenceKey(Preferences.RECOMMENDED_NEXT_STEPS, RecommendedNextStepsLegacy.SKIP)]: prefSkip,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: user.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
const [showTaskList, firstTimeOnboarding] = getShowTaskListBool(state);
|
||||
expect(showTaskList).toBeFalsy();
|
||||
expect(firstTimeOnboarding).toBeFalsy();
|
||||
});
|
||||
|
||||
test('user has preferences set to true for showing the tasklist', () => {
|
||||
const prefShow = {category: OnboardingTaskCategory, name: OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW, value: 'true'};
|
||||
const prefOpen = {category: OnboardingTaskCategory, name: OnboardingTaskList.ONBOARDING_TASK_LIST_OPEN, value: 'true'};
|
||||
|
||||
const user = TestHelper.fakeUserWithId();
|
||||
|
||||
const profiles = {
|
||||
[user.id]: user,
|
||||
};
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW)]: prefShow,
|
||||
[getPreferenceKey(OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_OPEN)]: prefOpen,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: user.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
const [showTaskList, firstTimeOnboarding] = getShowTaskListBool(state);
|
||||
expect(showTaskList).toBeTruthy();
|
||||
expect(firstTimeOnboarding).toBeFalsy();
|
||||
});
|
||||
|
||||
test('user has preferences set to false for showing the tasklist', () => {
|
||||
const prefSkip = {category: Preferences.RECOMMENDED_NEXT_STEPS, name: RecommendedNextStepsLegacy.SKIP, value: 'true'};
|
||||
const prefHide = {category: Preferences.RECOMMENDED_NEXT_STEPS, name: RecommendedNextStepsLegacy.HIDE, value: 'false'};
|
||||
const prefShow = {category: OnboardingTaskCategory, name: OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW, value: 'false'};
|
||||
const prefOpen = {category: OnboardingTaskCategory, name: OnboardingTaskList.ONBOARDING_TASK_LIST_OPEN, value: 'false'};
|
||||
|
||||
const user = TestHelper.fakeUserWithId();
|
||||
|
||||
const profiles = {
|
||||
[user.id]: user,
|
||||
};
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW)]: prefShow,
|
||||
[getPreferenceKey(OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_OPEN)]: prefOpen,
|
||||
[getPreferenceKey(Preferences.RECOMMENDED_NEXT_STEPS, RecommendedNextStepsLegacy.SKIP)]: prefSkip,
|
||||
[getPreferenceKey(Preferences.RECOMMENDED_NEXT_STEPS, RecommendedNextStepsLegacy.HIDE)]: prefHide,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: user.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
const [showTaskList, firstTimeOnboarding] = getShowTaskListBool(state);
|
||||
expect(showTaskList).toBeFalsy();
|
||||
expect(firstTimeOnboarding).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
174
webapp/channels/src/selectors/onboarding.ts
Обычный файл
174
webapp/channels/src/selectors/onboarding.ts
Обычный файл
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {isMobile} from 'utils/utils';
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {makeGetCategory, getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {OnboardingTaskCategory, OnboardingTaskList} from 'components/onboarding_tasks';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {RecommendedNextStepsLegacy, Preferences} from 'utils/constants';
|
||||
|
||||
const getCategory = makeGetCategory();
|
||||
export const getABTestPreferences = (() => {
|
||||
return (state: GlobalState) => getCategory(state, Preferences.AB_TEST_PREFERENCE_VALUE);
|
||||
})();
|
||||
|
||||
const getFirstChannelNamePref = createSelector(
|
||||
'getFirstChannelNamePref',
|
||||
getABTestPreferences,
|
||||
(preferences) => {
|
||||
return preferences.find((pref) => pref.name === RecommendedNextStepsLegacy.CREATE_FIRST_CHANNEL);
|
||||
},
|
||||
);
|
||||
|
||||
export function getFirstChannelNameViews(state: GlobalState) {
|
||||
return state.views.channelSidebar.firstChannelName;
|
||||
}
|
||||
|
||||
export function getFirstChannelName(state: GlobalState) {
|
||||
return getFirstChannelNameViews(state) || getFirstChannelNamePref(state)?.value || '';
|
||||
}
|
||||
|
||||
export function getShowLaunchingWorkspace(state: GlobalState) {
|
||||
return state.views.modals.showLaunchingWorkspace;
|
||||
}
|
||||
|
||||
// Legacy nextSteps section used to determine when to hide the onboarding to end users who have already completed/unfinished it
|
||||
export type StepType = {
|
||||
id: string;
|
||||
|
||||
// An array of all roles a user must have in order to see the step e.g. admins are both system_admin and system_user
|
||||
// so you would require ['system_admin','system_user'] to match.
|
||||
// to show step for all roles, leave the roles array blank.
|
||||
// for a step that must be shown only to the first admin, add the first_admin role to that step
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export const Steps: StepType[] = [
|
||||
{
|
||||
id: RecommendedNextStepsLegacy.COMPLETE_PROFILE,
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: RecommendedNextStepsLegacy.TEAM_SETUP,
|
||||
roles: ['first_admin'],
|
||||
},
|
||||
{
|
||||
id: RecommendedNextStepsLegacy.NOTIFICATION_SETUP,
|
||||
roles: ['system_user'],
|
||||
},
|
||||
{
|
||||
id: RecommendedNextStepsLegacy.PREFERENCES_SETUP,
|
||||
roles: ['system_user'],
|
||||
},
|
||||
{
|
||||
id: RecommendedNextStepsLegacy.INVITE_MEMBERS,
|
||||
roles: ['system_admin', 'system_user'],
|
||||
},
|
||||
{
|
||||
id: RecommendedNextStepsLegacy.DOWNLOAD_APPS,
|
||||
roles: [],
|
||||
},
|
||||
];
|
||||
|
||||
// Filter the steps shown by checking if our user has any of the required roles for that step
|
||||
export function isStepForUser(step: StepType, roles: string): boolean {
|
||||
const userRoles = roles?.split(' ');
|
||||
return (
|
||||
userRoles?.some((role) => step.roles.includes(role)) ||
|
||||
step.roles.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
const getSteps = createSelector(
|
||||
'getSteps',
|
||||
(state: GlobalState) => getCurrentUser(state),
|
||||
(state: GlobalState) => isFirstAdmin(state),
|
||||
(currentUser, firstAdmin) => {
|
||||
const roles = firstAdmin ? `first_admin ${currentUser?.roles}` : currentUser?.roles;
|
||||
return Steps.filter((step) => isStepForUser(step, roles));
|
||||
},
|
||||
);
|
||||
|
||||
// Loop through all Steps. For each step, check that
|
||||
export const legacyNextStepsNotFinished = createSelector(
|
||||
'legacyNextStepsNotFinished',
|
||||
(state: GlobalState) => getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS),
|
||||
(state: GlobalState) => getCurrentUser(state),
|
||||
(state: GlobalState) => isFirstAdmin(state),
|
||||
(state: GlobalState) => getSteps(state),
|
||||
(stepPreferences, currentUser, firstAdmin, mySteps) => {
|
||||
const roles = firstAdmin ? `first_admin ${currentUser?.roles}` : currentUser?.roles;
|
||||
const checkPref = (step: StepType) => stepPreferences.some((pref) => (pref.name === step.id && pref.value === 'true') || !isStepForUser(step, roles));
|
||||
return !mySteps.every(checkPref);
|
||||
},
|
||||
);
|
||||
|
||||
// Loop through all Steps. For each step, check that
|
||||
export const hasLegacyNextStepsPreferences = createSelector(
|
||||
'hasLegacyNextStepsPreferences',
|
||||
(state: GlobalState) => getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS),
|
||||
(state: GlobalState) => getSteps(state),
|
||||
(stepPreferences, mySteps) => {
|
||||
const checkPref = (step: StepType) => stepPreferences.some((pref) => (pref.name === step.id));
|
||||
return mySteps.some(checkPref);
|
||||
},
|
||||
);
|
||||
|
||||
export const getShowTaskListBool = createSelector(
|
||||
'getShowTaskListBool',
|
||||
(state: GlobalState) => state,
|
||||
(state: GlobalState) => getCategory(state, OnboardingTaskCategory),
|
||||
(state: GlobalState) => getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS),
|
||||
(state, onboardingPreferences, legacyStepsPreferences) => {
|
||||
const isMobileView = isMobile();
|
||||
|
||||
// conditions to validate scenario where users (initially first_admins) had already set any of the onboarding task list preferences values.
|
||||
// We check wether the preference value exists meaning the onboarding tasks list already started no matter what the state of the process is
|
||||
const hasUserStartedOnboardingTaskListProcess = onboardingPreferences?.some((pref) =>
|
||||
pref.name === OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW || pref.name === OnboardingTaskList.ONBOARDING_TASK_LIST_OPEN);
|
||||
|
||||
const taskListStatus = getBool(state, OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW);
|
||||
|
||||
if (hasUserStartedOnboardingTaskListProcess) {
|
||||
return [(taskListStatus && !isMobileView), false];
|
||||
}
|
||||
|
||||
// validate is a new user that must do the first time onboarding by checking that:
|
||||
// 1. has not preferences related to the new onboarding task list.
|
||||
// 2. has no legacy skip preference
|
||||
// 3. has no legacy steps preferences
|
||||
// 4. has completed legacy next steps (hide value for recommended_next_steps category set to false)
|
||||
|
||||
// This condition verifies existing users hasn't finished nor skipped legacy next steps or there are still steps not completed
|
||||
const hasSkipLegacyStepsPreference = legacyStepsPreferences.some((pref) => (pref.name === RecommendedNextStepsLegacy.SKIP));
|
||||
const hideLegacyStepsSetToFalse = legacyStepsPreferences.some((pref) => (pref.name === RecommendedNextStepsLegacy.HIDE && pref.value === 'false'));
|
||||
const hasAnyOfTheLegacyStepsPreferences = hasLegacyNextStepsPreferences(state);
|
||||
const areFirstUserPrefs = !hasSkipLegacyStepsPreference && hideLegacyStepsSetToFalse && !hasAnyOfTheLegacyStepsPreferences;
|
||||
|
||||
const completelyNewUserForOnboarding = !hasUserStartedOnboardingTaskListProcess && areFirstUserPrefs;
|
||||
|
||||
if (completelyNewUserForOnboarding) {
|
||||
return [(!isMobileView), true];
|
||||
}
|
||||
|
||||
// If none of the previous conditions matched, then it is an existing user with legacy prefs.
|
||||
// To determine if we show the new onboarding task list we need to validate:
|
||||
// has not skipped nor completed the legacy steps
|
||||
const hasSkippedLegacySteps = legacyStepsPreferences.some((pref) => (pref.name === RecommendedNextStepsLegacy.SKIP && pref.value === 'true'));
|
||||
const hasCompletedLegacySteps = legacyStepsPreferences.some((pref) => (pref.name === RecommendedNextStepsLegacy.HIDE && pref.value === 'true'));
|
||||
|
||||
const existingUserHasntFinishedNorSkippedLegacyNextSteps = !hasSkippedLegacySteps && !hasCompletedLegacySteps;
|
||||
|
||||
const showTaskList = existingUserHasntFinishedNorSkippedLegacyNextSteps && !isMobileView;
|
||||
const firstTimeOnboarding = existingUserHasntFinishedNorSkippedLegacyNextSteps;
|
||||
|
||||
return [showTaskList, firstTimeOnboarding];
|
||||
},
|
||||
);
|
||||
183
webapp/channels/src/selectors/plugins.test.js
Обычный файл
183
webapp/channels/src/selectors/plugins.test.js
Обычный файл
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getChannelHeaderMenuPluginComponents} from 'selectors/plugins';
|
||||
|
||||
describe('selectors/plugins', () => {
|
||||
describe('getChannelHeaderMenuPluginComponents', () => {
|
||||
test('no channel header components found', () => {
|
||||
const expectedComponents = [];
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
components: {
|
||||
ChannelHeader: expectedComponents,
|
||||
},
|
||||
},
|
||||
};
|
||||
const components = getChannelHeaderMenuPluginComponents(state);
|
||||
expect(components).toEqual(expectedComponents);
|
||||
});
|
||||
|
||||
test('one channel header component found as shouldRender returns true', () => {
|
||||
const expectedComponents = [
|
||||
{
|
||||
shouldRender: () => true,
|
||||
},
|
||||
];
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
components: {
|
||||
ChannelHeader: expectedComponents,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const components = getChannelHeaderMenuPluginComponents(state);
|
||||
expect(components).toEqual(expectedComponents);
|
||||
});
|
||||
|
||||
test('one channel header component found as shouldRender is not defined', () => {
|
||||
const expectedComponents = [
|
||||
{
|
||||
id: 'testId',
|
||||
},
|
||||
];
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
components: {
|
||||
ChannelHeader: expectedComponents,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const components = getChannelHeaderMenuPluginComponents(state);
|
||||
expect(components).toEqual(expectedComponents);
|
||||
});
|
||||
|
||||
test('no channel header components found as shouldRender returns false', () => {
|
||||
const expectedComponents = [];
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
components: {
|
||||
ChannelHeader: [{
|
||||
shouldRender: () => false,
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const components = getChannelHeaderMenuPluginComponents(state);
|
||||
expect(components).toEqual(expectedComponents);
|
||||
});
|
||||
|
||||
test('memoization', () => {
|
||||
let shouldRenderResult = false;
|
||||
|
||||
let state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
components: {
|
||||
ChannelHeader: [{
|
||||
shouldRender: () => shouldRenderResult,
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const firstResult = getChannelHeaderMenuPluginComponents(state);
|
||||
|
||||
expect(firstResult).toEqual([]);
|
||||
|
||||
// No changes to state
|
||||
const secondResult = getChannelHeaderMenuPluginComponents(state);
|
||||
|
||||
expect(secondResult).toBe(firstResult);
|
||||
|
||||
// Something unrelated changed in state
|
||||
state = {...state};
|
||||
|
||||
const thirdResult = getChannelHeaderMenuPluginComponents(state);
|
||||
|
||||
expect(thirdResult).toBe(firstResult);
|
||||
|
||||
// shouldRender changed because something else in state changed
|
||||
state = {...state};
|
||||
shouldRenderResult = true;
|
||||
|
||||
const fourthResult = getChannelHeaderMenuPluginComponents(state);
|
||||
|
||||
expect(fourthResult).not.toBe(firstResult);
|
||||
expect(fourthResult).toEqual([
|
||||
state.plugins.components.ChannelHeader[0],
|
||||
]);
|
||||
|
||||
// A new plugin was added
|
||||
state = {
|
||||
...state,
|
||||
plugins: {
|
||||
...state.plugins,
|
||||
components: {
|
||||
...state.plugins.components,
|
||||
ChannelHeader: [
|
||||
...state.plugins.components.ChannelHeader,
|
||||
{
|
||||
id: 'anotherPlugin',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const fifthResult = getChannelHeaderMenuPluginComponents(state);
|
||||
|
||||
expect(fifthResult).not.toBe(fourthResult);
|
||||
expect(fifthResult).toEqual([
|
||||
state.plugins.components.ChannelHeader[0],
|
||||
state.plugins.components.ChannelHeader[1],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
116
webapp/channels/src/selectors/plugins.ts
Обычный файл
116
webapp/channels/src/selectors/plugins.ts
Обычный файл
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {appBarEnabled, getAppBarAppBindings} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {createShallowSelector} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import {autoShowLinkedBoardFFEnabled, get, getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import {OnboardingTaskCategory, OnboardingTaskList} from 'components/onboarding_tasks';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {AppBinding} from '@mattermost/types/apps';
|
||||
|
||||
import {FileDropdownPluginComponent, PluginComponent} from '../types/store/plugins';
|
||||
|
||||
export const getFilesDropdownPluginMenuItems = createSelector(
|
||||
'getFilesDropdownPluginMenuItems',
|
||||
(state: GlobalState) => state.plugins.components.FilesDropdown,
|
||||
(components) => {
|
||||
return (components || []) as unknown as FileDropdownPluginComponent[];
|
||||
},
|
||||
);
|
||||
|
||||
export const getUserGuideDropdownPluginMenuItems = createSelector(
|
||||
'getUserGuideDropdownPluginMenuItems',
|
||||
(state: GlobalState) => state.plugins.components.UserGuideDropdown,
|
||||
(components) => {
|
||||
return components;
|
||||
},
|
||||
);
|
||||
|
||||
export const getChannelHeaderPluginComponents = createSelector(
|
||||
'getChannelHeaderPluginComponents',
|
||||
(state: GlobalState) => appBarEnabled(state),
|
||||
(state: GlobalState) => state.plugins.components.ChannelHeaderButton,
|
||||
(state: GlobalState) => state.plugins.components.AppBar,
|
||||
(enabled, channelHeaderComponents = [], appBarComponents = []) => {
|
||||
if (!enabled || !appBarComponents.length) {
|
||||
return channelHeaderComponents as unknown as PluginComponent[];
|
||||
}
|
||||
|
||||
// Remove channel header icons for plugins that have also registered an app bar component
|
||||
const appBarPluginIds = appBarComponents.map((appBarComponent) => appBarComponent.pluginId);
|
||||
return channelHeaderComponents.filter((channelHeaderComponent) => !appBarPluginIds.includes(channelHeaderComponent.pluginId));
|
||||
},
|
||||
);
|
||||
|
||||
const getChannelHeaderMenuPluginComponentsShouldRender = createSelector(
|
||||
'getChannelHeaderMenuPluginComponentsShouldRender',
|
||||
(state: GlobalState) => state,
|
||||
(state: GlobalState) => state.plugins.components.ChannelHeader,
|
||||
(state, channelHeaderMenuComponents = []) => {
|
||||
return channelHeaderMenuComponents.map((component) => {
|
||||
if (typeof component.shouldRender === 'function') {
|
||||
return component.shouldRender(state);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const getChannelHeaderMenuPluginComponents = createShallowSelector(
|
||||
'getChannelHeaderMenuPluginComponents',
|
||||
getChannelHeaderMenuPluginComponentsShouldRender,
|
||||
(state: GlobalState) => state.plugins.components.ChannelHeader,
|
||||
(componentShouldRender = [], channelHeaderMenuComponents = []) => {
|
||||
return channelHeaderMenuComponents.filter((component, idx) => componentShouldRender[idx]);
|
||||
},
|
||||
);
|
||||
|
||||
export const getChannelIntroPluginButtons = createSelector(
|
||||
'getChannelIntroPluginButtons',
|
||||
(state: GlobalState) => state.plugins.components.ChannelIntroButton,
|
||||
(components = []) => {
|
||||
return components;
|
||||
},
|
||||
);
|
||||
|
||||
export const getAppBarPluginComponents = createSelector(
|
||||
'getAppBarPluginComponents',
|
||||
(state: GlobalState) => state.plugins.components.AppBar,
|
||||
(components = []) => {
|
||||
return components;
|
||||
},
|
||||
);
|
||||
|
||||
export const shouldShowAppBar = createSelector(
|
||||
'shouldShowAppBar',
|
||||
appBarEnabled,
|
||||
getAppBarAppBindings,
|
||||
getAppBarPluginComponents,
|
||||
getChannelHeaderPluginComponents,
|
||||
(enabled: boolean, bindings: AppBinding[], appBarComponents: PluginComponent[], channelHeaderComponents) => {
|
||||
return enabled && Boolean(bindings.length || appBarComponents.length || channelHeaderComponents.length);
|
||||
},
|
||||
);
|
||||
|
||||
export function showNewChannelWithBoardPulsatingDot(state: GlobalState): boolean {
|
||||
const pulsatingDotState = get(state, Preferences.APP_BAR, Preferences.NEW_CHANNEL_WITH_BOARD_TOUR_SHOWED, '');
|
||||
const showPulsatingDot = pulsatingDotState !== '' && JSON.parse(pulsatingDotState)[Preferences.NEW_CHANNEL_WITH_BOARD_TOUR_SHOWED] === false;
|
||||
return showPulsatingDot;
|
||||
}
|
||||
|
||||
export const shouldShowAutoLinkedBoard = createSelector(
|
||||
'shouldShowAutoLinkedBoard',
|
||||
(state: GlobalState) => getBool(state, OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_LINKED_BOARD_AUTO_SHOWN),
|
||||
(state: GlobalState) => autoShowLinkedBoardFFEnabled(state),
|
||||
(showAutoLinkedBoardPref: boolean, showAutoLinkedBoardFFEnabled: boolean) => {
|
||||
return !showAutoLinkedBoardPref && showAutoLinkedBoardFFEnabled;
|
||||
},
|
||||
);
|
||||
54
webapp/channels/src/selectors/posts.ts
Обычный файл
54
webapp/channels/src/selectors/posts.ts
Обычный файл
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getGlobalItem} from 'selectors/storage';
|
||||
import {arePreviewsCollapsed} from 'selectors/preferences';
|
||||
import {StoragePrefixes} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function getIsPostBeingEdited(state: GlobalState, postId: string) {
|
||||
return state.views.posts.editingPost.postId === postId && state.views.posts.editingPost.show;
|
||||
}
|
||||
export function getIsPostBeingEditedInRHS(state: GlobalState, postId: string) {
|
||||
const editingPost = getEditingPost(state);
|
||||
|
||||
return editingPost.isRHS && editingPost.postId === postId && state.views.posts.editingPost.show;
|
||||
}
|
||||
|
||||
export function getPostEditHistory(state: GlobalState): Post[] {
|
||||
return state.entities.posts.postEditHistory;
|
||||
}
|
||||
|
||||
export const getEditingPost = createSelector(
|
||||
'getEditingPost',
|
||||
(state: GlobalState) => state.views.posts.editingPost,
|
||||
(state: GlobalState) => getPost(state, state.views.posts.editingPost.postId),
|
||||
(editingPost, post) => {
|
||||
return {
|
||||
...editingPost,
|
||||
post,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export function isEmbedVisible(state: GlobalState, postId: string) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const previewCollapsed = arePreviewsCollapsed(state);
|
||||
|
||||
return getGlobalItem(state, StoragePrefixes.EMBED_VISIBLE + currentUserId + '_' + postId, !previewCollapsed);
|
||||
}
|
||||
|
||||
export function isInlineImageVisible(state: GlobalState, postId: string, imageKey: string) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const imageCollapsed = arePreviewsCollapsed(state);
|
||||
|
||||
return getGlobalItem(state, StoragePrefixes.INLINE_IMAGE_VISIBLE + currentUserId + '_' + postId + '_' + imageKey, !imageCollapsed);
|
||||
}
|
||||
16
webapp/channels/src/selectors/preferences.ts
Обычный файл
16
webapp/channels/src/selectors/preferences.ts
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getBool as getBoolPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {Preferences} from 'utils/constants';
|
||||
|
||||
export const arePreviewsCollapsed = (state: GlobalState) => {
|
||||
return getBoolPreference(
|
||||
state,
|
||||
Preferences.CATEGORY_DISPLAY_SETTINGS,
|
||||
Preferences.COLLAPSE_DISPLAY,
|
||||
Preferences.COLLAPSE_DISPLAY_DEFAULT !== 'false',
|
||||
);
|
||||
};
|
||||
19
webapp/channels/src/selectors/products.ts
Обычный файл
19
webapp/channels/src/selectors/products.ts
Обычный файл
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ProductIdentifier} from '@mattermost/types/products';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import type {ProductComponent} from '../types/store/plugins';
|
||||
|
||||
import {getCurrentProduct} from 'utils/products';
|
||||
|
||||
export function selectCurrentProduct(state: GlobalState, pathname: string): ProductComponent | null {
|
||||
return getCurrentProduct(selectProducts(state), pathname);
|
||||
}
|
||||
|
||||
export function selectCurrentProductId(state: GlobalState, pathname: string): ProductIdentifier {
|
||||
return selectCurrentProduct(state, pathname)?.id ?? null;
|
||||
}
|
||||
|
||||
export const selectProducts = (state: GlobalState) => state.plugins.components.Product;
|
||||
67
webapp/channels/src/selectors/rhs.test.ts
Обычный файл
67
webapp/channels/src/selectors/rhs.test.ts
Обычный файл
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as Selectors from 'selectors/rhs';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
describe('Selectors.Rhs', () => {
|
||||
describe('should return the last time a post was selected', () => {
|
||||
[0, 1000000, 2000000].forEach((expected) => {
|
||||
it(`when open is ${expected}`, () => {
|
||||
const state = {views: {rhs: {
|
||||
selectedPostFocussedAt: expected,
|
||||
}}} as GlobalState;
|
||||
|
||||
expect(Selectors.getSelectedPostFocussedAt(state)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return the open state of the sidebar', () => {
|
||||
[true, false].forEach((expected) => {
|
||||
it(`when open is ${expected}`, () => {
|
||||
const state = {views: {rhs: {
|
||||
isSidebarOpen: expected,
|
||||
}}} as GlobalState;
|
||||
|
||||
expect(Selectors.getIsRhsOpen(state)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return the open state of the sidebar menu', () => {
|
||||
[true, false].forEach((expected) => {
|
||||
it(`when open is ${expected}`, () => {
|
||||
const state = {views: {rhs: {
|
||||
isMenuOpen: expected,
|
||||
}}} as GlobalState;
|
||||
|
||||
expect(Selectors.getIsRhsMenuOpen(state)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return the highlighted reply\'s id', () => {
|
||||
test.each(['42', ''])('when id is %s', (expected) => {
|
||||
const state = {views: {rhs: {
|
||||
highlightedPostId: expected,
|
||||
}}} as GlobalState;
|
||||
|
||||
expect(Selectors.getHighlightedPostId(state)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return the previousRhsState', () => {
|
||||
test.each([
|
||||
[[], null],
|
||||
[['channel-info'], 'channel-info'],
|
||||
[['channel-info', 'pinned'], 'pinned'],
|
||||
])('%p gives %p', (previousArray, previous) => {
|
||||
const state = {
|
||||
views: {rhs: {
|
||||
previousRhsStates: previousArray,
|
||||
}}} as GlobalState;
|
||||
expect(Selectors.getPreviousRhsState(state)).toEqual(previous);
|
||||
});
|
||||
});
|
||||
});
|
||||
187
webapp/channels/src/selectors/rhs.ts
Обычный файл
187
webapp/channels/src/selectors/rhs.ts
Обычный файл
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {Post, PostType} from '@mattermost/types/posts';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {makeGetGlobalItem, makeGetGlobalItemWithDefault} from 'selectors/storage';
|
||||
import {PostTypes, StoragePrefixes} from 'utils/constants';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {RhsState, FakePost, SearchType} from 'types/store/rhs';
|
||||
import {PostDraft} from 'types/store/draft';
|
||||
|
||||
export function getSelectedPostId(state: GlobalState): Post['id'] {
|
||||
return state.views.rhs.selectedPostId;
|
||||
}
|
||||
|
||||
export function getSelectedPostFocussedAt(state: GlobalState): number {
|
||||
return state.views.rhs.selectedPostFocussedAt;
|
||||
}
|
||||
|
||||
export function getSelectedPostCardId(state: GlobalState): Post['id'] {
|
||||
return state.views.rhs.selectedPostCardId;
|
||||
}
|
||||
|
||||
export function getHighlightedPostId(state: GlobalState): Post['id'] {
|
||||
return state.views.rhs.highlightedPostId;
|
||||
}
|
||||
|
||||
export function getFilesSearchExtFilter(state: GlobalState): string[] {
|
||||
return state.views.rhs.filesSearchExtFilter;
|
||||
}
|
||||
|
||||
export function getSelectedPostCard(state: GlobalState) {
|
||||
return state.entities.posts.posts[getSelectedPostCardId(state)];
|
||||
}
|
||||
|
||||
export function getSelectedChannelId(state: GlobalState) {
|
||||
return state.views.rhs.selectedChannelId;
|
||||
}
|
||||
|
||||
export const getSelectedChannel = (() => {
|
||||
const getChannel = makeGetChannel();
|
||||
|
||||
return (state: GlobalState) => {
|
||||
const channelId = getSelectedChannelId(state);
|
||||
|
||||
return getChannel(state, {id: channelId});
|
||||
};
|
||||
})();
|
||||
|
||||
export function getPluggableId(state: GlobalState) {
|
||||
return state.views.rhs.pluggableId;
|
||||
}
|
||||
|
||||
export function getActiveRhsComponent(state: GlobalState) {
|
||||
const pluggableId = getPluggableId(state);
|
||||
const components = state.plugins.components.RightHandSidebarComponent;
|
||||
return components.find((c) => c.id === pluggableId);
|
||||
}
|
||||
|
||||
function getRealSelectedPost(state: GlobalState) {
|
||||
return state.entities.posts.posts[getSelectedPostId(state)];
|
||||
}
|
||||
|
||||
export const getSelectedPost = createSelector(
|
||||
'getSelectedPost',
|
||||
getSelectedPostId,
|
||||
getRealSelectedPost,
|
||||
getSelectedChannelId,
|
||||
getCurrentUserId,
|
||||
(selectedPostId: Post['id'], selectedPost: Post, selectedPostChannelId: Channel['id'], currentUserId): Post|FakePost => {
|
||||
if (selectedPost) {
|
||||
return selectedPost;
|
||||
}
|
||||
|
||||
// If there is no root post found, assume it has been deleted by data retention policy, and create a fake one.
|
||||
return {
|
||||
id: selectedPostId,
|
||||
exists: false,
|
||||
type: PostTypes.FAKE_PARENT_DELETED as PostType,
|
||||
message: localizeMessage('rhs_thread.rootPostDeletedMessage.body', 'Part of this thread has been deleted due to a data retention policy. You can no longer reply to this thread.'),
|
||||
channel_id: selectedPostChannelId,
|
||||
user_id: currentUserId,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export function getRhsState(state: GlobalState): RhsState {
|
||||
return state.views.rhs.rhsState;
|
||||
}
|
||||
|
||||
export function getPreviousRhsState(state: GlobalState): RhsState {
|
||||
if (state.views.rhs.previousRhsStates === null || state.views.rhs.previousRhsStates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return state.views.rhs.previousRhsStates[state.views.rhs.previousRhsStates.length - 1];
|
||||
}
|
||||
|
||||
export function getSearchTerms(state: GlobalState): string {
|
||||
return state.views.rhs.searchTerms;
|
||||
}
|
||||
|
||||
export function getSearchType(state: GlobalState): SearchType {
|
||||
return state.views.rhs.searchType;
|
||||
}
|
||||
|
||||
export function getSearchResultsTerms(state: GlobalState): string {
|
||||
return state.views.rhs.searchResultsTerms;
|
||||
}
|
||||
|
||||
export function getIsSearchingTerm(state: GlobalState): boolean {
|
||||
return state.entities.search.isSearchingTerm;
|
||||
}
|
||||
|
||||
export function getIsSearchingFlaggedPost(state: GlobalState): boolean {
|
||||
return state.views.rhs.isSearchingFlaggedPost;
|
||||
}
|
||||
|
||||
export function getIsSearchingPinnedPost(state: GlobalState): boolean {
|
||||
return state.views.rhs.isSearchingPinnedPost;
|
||||
}
|
||||
|
||||
export function getIsSearchGettingMore(state: GlobalState): boolean {
|
||||
return state.entities.search.isSearchGettingMore;
|
||||
}
|
||||
|
||||
export function makeGetChannelDraft() {
|
||||
const defaultDraft = Object.freeze({message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''});
|
||||
const getDraft = makeGetGlobalItemWithDefault(defaultDraft);
|
||||
|
||||
return (state: GlobalState, channelId: string): PostDraft => {
|
||||
const draft = getDraft(state, StoragePrefixes.DRAFT + channelId);
|
||||
if (
|
||||
typeof draft.message !== 'undefined' &&
|
||||
typeof draft.uploadsInProgress !== 'undefined' &&
|
||||
typeof draft.fileInfos !== 'undefined'
|
||||
) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
return defaultDraft;
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostDraft(state: GlobalState, prefixId: string, suffixId: string): PostDraft {
|
||||
const defaultDraft = {message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''};
|
||||
|
||||
if (prefixId === StoragePrefixes.COMMENT_DRAFT) {
|
||||
defaultDraft.rootId = suffixId;
|
||||
}
|
||||
const draft = makeGetGlobalItem(prefixId + suffixId, defaultDraft)(state);
|
||||
|
||||
if (
|
||||
typeof draft.message !== 'undefined' &&
|
||||
typeof draft.uploadsInProgress !== 'undefined' &&
|
||||
typeof draft.fileInfos !== 'undefined'
|
||||
) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
return defaultDraft;
|
||||
}
|
||||
|
||||
export function getIsRhsSuppressed(state: GlobalState): boolean {
|
||||
return state.views.rhsSuppressed;
|
||||
}
|
||||
|
||||
export function getIsRhsOpen(state: GlobalState): boolean {
|
||||
return state.views.rhs.isSidebarOpen && !state.views.rhsSuppressed;
|
||||
}
|
||||
|
||||
export function getIsRhsMenuOpen(state: GlobalState): boolean {
|
||||
return state.views.rhs.isMenuOpen;
|
||||
}
|
||||
|
||||
export function getIsRhsExpanded(state: GlobalState): boolean {
|
||||
return state.views.rhs.isSidebarExpanded;
|
||||
}
|
||||
|
||||
export function getIsEditingMembers(state: GlobalState): boolean {
|
||||
return state.views.rhs.editChannelMembers === true;
|
||||
}
|
||||
47
webapp/channels/src/selectors/storage.test.ts
Обычный файл
47
webapp/channels/src/selectors/storage.test.ts
Обычный файл
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getPrefix} from 'utils/storage_utils';
|
||||
import * as Selectors from 'selectors/storage';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
describe('Selectors.Storage', () => {
|
||||
const testState = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'user_id',
|
||||
profiles: {
|
||||
user_id: {
|
||||
id: 'user_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
storage: {
|
||||
'global-item': {value: 'global-item-value', timestamp: new Date()},
|
||||
user_id_item: {value: 'item-value', timestamp: new Date()},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
it('getPrefix', () => {
|
||||
expect(getPrefix({} as GlobalState)).toEqual('unknown_');
|
||||
expect(getPrefix({entities: {}} as GlobalState)).toEqual('unknown_');
|
||||
expect(getPrefix({entities: {users: {currentUserId: 'not-exists'}}} as GlobalState)).toEqual('unknown_');
|
||||
expect(getPrefix({entities: {users: {currentUserId: 'not-exists', profiles: {}}}} as GlobalState)).toEqual('unknown_');
|
||||
expect(getPrefix({entities: {users: {currentUserId: 'exists', profiles: {exists: {id: 'user_id'}}}}} as unknown as GlobalState)).toEqual('user_id_');
|
||||
});
|
||||
|
||||
it('makeGetGlobalItem', () => {
|
||||
expect(Selectors.makeGetGlobalItem('not-existing-global-item', undefined)(testState)).toEqual(undefined);
|
||||
expect(Selectors.makeGetGlobalItem('not-existing-global-item', 'default')(testState)).toEqual('default');
|
||||
expect(Selectors.makeGetGlobalItem('global-item', undefined)(testState)).toEqual('global-item-value');
|
||||
});
|
||||
|
||||
it('makeGetItem', () => {
|
||||
expect(Selectors.makeGetItem('not-existing-item', undefined)(testState)).toEqual(undefined);
|
||||
expect(Selectors.makeGetItem('not-existing-item', 'default')(testState)).toEqual('default');
|
||||
expect(Selectors.makeGetItem('item', undefined)(testState)).toEqual('item-value');
|
||||
});
|
||||
});
|
||||
38
webapp/channels/src/selectors/storage.ts
Обычный файл
38
webapp/channels/src/selectors/storage.ts
Обычный файл
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getPrefix} from 'utils/storage_utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export const getGlobalItem = <T = any>(state: GlobalState, name: string, defaultValue: T) => {
|
||||
const storage = state && state.storage && state.storage.storage;
|
||||
|
||||
return getItemFromStorage(storage, name, defaultValue);
|
||||
};
|
||||
|
||||
export const getItem = <T = any>(state: GlobalState, name: string, defaultValue: T) => {
|
||||
return getGlobalItem(state, getPrefix(state) + name, defaultValue);
|
||||
};
|
||||
|
||||
export const makeGetItem = <T = any>(name: string, defaultValue: T) => {
|
||||
return (state: GlobalState) => {
|
||||
return getItem(state, name, defaultValue);
|
||||
};
|
||||
};
|
||||
|
||||
export const makeGetGlobalItem = <T = any>(name: string, defaultValue: T) => {
|
||||
return (state: GlobalState) => {
|
||||
return getGlobalItem(state, name, defaultValue);
|
||||
};
|
||||
};
|
||||
|
||||
export const getItemFromStorage = <T = any>(storage: Record<string, any>, name: string, defaultValue: T) => {
|
||||
return storage[name]?.value ?? defaultValue;
|
||||
};
|
||||
|
||||
export const makeGetGlobalItemWithDefault = <T = any>(defaultValue: T) => {
|
||||
return (state: GlobalState, name: string) => {
|
||||
return getGlobalItem(state, name, defaultValue);
|
||||
};
|
||||
};
|
||||
8
webapp/channels/src/selectors/views/add_channel_dropdown.ts
Обычный файл
8
webapp/channels/src/selectors/views/add_channel_dropdown.ts
Обычный файл
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export function isAddChannelDropdownOpen(state: GlobalState) {
|
||||
return state.views.addChannelDropdown.isOpen;
|
||||
}
|
||||
20
webapp/channels/src/selectors/views/admin.ts
Обычный файл
20
webapp/channels/src/selectors/views/admin.ts
Обычный файл
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function getNavigationBlocked(state: GlobalState) {
|
||||
return state.views.admin.navigationBlock.blocked;
|
||||
}
|
||||
|
||||
export function showNavigationPrompt(state: GlobalState) {
|
||||
return state.views.admin.navigationBlock.showNavigationPrompt;
|
||||
}
|
||||
|
||||
export function getOnNavigationConfirmed(state: GlobalState) {
|
||||
return state.views.admin.navigationBlock.onNavigationConfirmed;
|
||||
}
|
||||
|
||||
export function getNeedsLoggedInLimitReachedCheck(state: GlobalState): boolean {
|
||||
return state.views.admin.needsLoggedInLimitReachedCheck;
|
||||
}
|
||||
8
webapp/channels/src/selectors/views/announcement_bar.ts
Обычный файл
8
webapp/channels/src/selectors/views/announcement_bar.ts
Обычный файл
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function getAnnouncementBarCount(state: GlobalState) {
|
||||
return state.views.announcementBar.announcementBarState.announcementBarCount;
|
||||
}
|
||||
26
webapp/channels/src/selectors/views/browser.ts
Обычный файл
26
webapp/channels/src/selectors/views/browser.ts
Обычный файл
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {WindowSizes} from 'utils/constants';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export function getIsDesktopView(state: GlobalState): boolean {
|
||||
const windowSize = state.views.browser.windowSize;
|
||||
return windowSize === WindowSizes.DESKTOP_VIEW;
|
||||
}
|
||||
|
||||
export function getIsSmallDesktopView(state: GlobalState): boolean {
|
||||
const windowSize = state.views.browser.windowSize;
|
||||
return windowSize === WindowSizes.SMALL_DESKTOP_VIEW;
|
||||
}
|
||||
|
||||
export function getIsTabletView(state: GlobalState): boolean {
|
||||
const windowSize = state.views.browser.windowSize;
|
||||
return windowSize === WindowSizes.TABLET_VIEW;
|
||||
}
|
||||
|
||||
export function getIsMobileView(state: GlobalState): boolean {
|
||||
const windowSize = state.views.browser.windowSize;
|
||||
return windowSize === WindowSizes.MOBILE_VIEW;
|
||||
}
|
||||
7
webapp/channels/src/selectors/views/channel.ts
Обычный файл
7
webapp/channels/src/selectors/views/channel.ts
Обычный файл
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export const getLastPostsApiTimeForChannel = (state: GlobalState, channelId: string) => state.views.channel.lastGetPosts[channelId];
|
||||
export const getToastStatus = (state: GlobalState) => state.views.channel.toastStatus;
|
||||
747
webapp/channels/src/selectors/views/channel_sidebar.test.js
Обычный файл
747
webapp/channels/src/selectors/views/channel_sidebar.test.js
Обычный файл
@@ -0,0 +1,747 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import mergeObjects from 'mattermost-redux/test/merge_objects';
|
||||
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import * as Selectors from './channel_sidebar';
|
||||
|
||||
describe('isUnreadFilterEnabled', () => {
|
||||
const preferenceKey = getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION);
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[preferenceKey]: {value: 'false'},
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
channelSidebar: {
|
||||
unreadFilterEnabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should return false when filter is disabled', () => {
|
||||
const state = baseState;
|
||||
|
||||
expect(Selectors.isUnreadFilterEnabled(state)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true when filter is enabled and unreads aren\'t separate', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
views: {
|
||||
channelSidebar: {
|
||||
unreadFilterEnabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.isUnreadFilterEnabled(state)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when unreads are separate', () => {
|
||||
const state = mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[preferenceKey]: {value: 'true'},
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
channelSidebar: {
|
||||
unreadFilterEnabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Selectors.isUnreadFilterEnabled(state)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUnreadChannels', () => {
|
||||
const currentChannel = TestHelper.getChannelMock({id: 'currentChannel', delete_at: 0, last_post_at: 0});
|
||||
const readChannel = {id: 'readChannel', delete_at: 0, last_post_at: 300};
|
||||
const unreadChannel1 = {id: 'unreadChannel1', delete_at: 0, last_post_at: 100};
|
||||
const unreadChannel2 = {id: 'unreadChannel2', delete_at: 0, last_post_at: 200};
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
currentChannel,
|
||||
readChannel,
|
||||
unreadChannel1,
|
||||
unreadChannel2,
|
||||
},
|
||||
channelsInTeam: {
|
||||
team1: ['unreadChannel1', 'unreadChannel2', 'readChannel'],
|
||||
},
|
||||
currentChannelId: 'currentChannel',
|
||||
messageCounts: {
|
||||
currentChannel: {total: 0},
|
||||
readChannel: {total: 10},
|
||||
unreadChannel1: {total: 10},
|
||||
unreadChannel2: {total: 10},
|
||||
},
|
||||
myMembers: {
|
||||
currentChannel: {notify_props: {}, mention_count: 0, msg_count: 0},
|
||||
readChannel: {notify_props: {}, mention_count: 0, msg_count: 10},
|
||||
unreadChannel1: {notify_props: {}, mention_count: 0, msg_count: 8},
|
||||
unreadChannel2: {notify_props: {}, mention_count: 0, msg_count: 8},
|
||||
},
|
||||
},
|
||||
posts: {
|
||||
postsInChannel: {},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team1',
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
channel: {
|
||||
lastUnreadChannel: {
|
||||
id: 'currentChannel',
|
||||
hadMentions: false,
|
||||
},
|
||||
},
|
||||
channelSidebar: {
|
||||
unreadFilterEnabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should return channels sorted by recency', () => {
|
||||
expect(Selectors.getUnreadChannels(baseState)).toEqual([unreadChannel2, unreadChannel1, currentChannel]);
|
||||
});
|
||||
|
||||
test('should return channels with mentions before those without', () => {
|
||||
let state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channels: {
|
||||
...baseState.entities.channels,
|
||||
myMembers: {
|
||||
...baseState.entities.channels.myMembers,
|
||||
unreadChannel1: {notify_props: {}, mention_count: 2, msg_count: 8},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
...baseState.entities.general,
|
||||
},
|
||||
preferences: {
|
||||
...baseState.entities.preferences,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([unreadChannel1, unreadChannel2, currentChannel]);
|
||||
|
||||
state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channels: {
|
||||
...baseState.entities.channels,
|
||||
myMembers: {
|
||||
...baseState.entities.channels.myMembers,
|
||||
unreadChannel1: {notify_props: {}, mention_count: 2, msg_count: 8},
|
||||
unreadChannel2: {notify_props: {}, mention_count: 1, msg_count: 8},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
...baseState.entities.general,
|
||||
},
|
||||
preferences: {
|
||||
...baseState.entities.preferences,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([unreadChannel2, unreadChannel1, currentChannel]);
|
||||
});
|
||||
|
||||
test('with the unread filter enabled, should always return the current channel, even if it is not unread', () => {
|
||||
const state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channels: {
|
||||
...baseState.entities.channels,
|
||||
currentChannelId: 'readChannel',
|
||||
},
|
||||
general: {
|
||||
...baseState.entities.general,
|
||||
},
|
||||
preferences: {
|
||||
...baseState.entities.preferences,
|
||||
},
|
||||
},
|
||||
views: {
|
||||
...baseState.views,
|
||||
channel: {
|
||||
...baseState.views.channel,
|
||||
lastUnreadChannel: {
|
||||
id: 'readChannel',
|
||||
hasMentions: true,
|
||||
},
|
||||
},
|
||||
channelSidebar: {
|
||||
...baseState.views.channelSidebar,
|
||||
unreadFilterEnabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([readChannel, unreadChannel2, unreadChannel1]);
|
||||
});
|
||||
|
||||
test('with the unreads category enabled, should only return the current channel if it is lastUnreadChannel', () => {
|
||||
let state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channels: {
|
||||
...baseState.entities.channels,
|
||||
currentChannelId: 'readChannel',
|
||||
},
|
||||
general: {
|
||||
...baseState.entities.general,
|
||||
},
|
||||
preferences: {
|
||||
...baseState.entities.preferences,
|
||||
},
|
||||
},
|
||||
views: {
|
||||
...baseState.views,
|
||||
channel: {
|
||||
...baseState.views.channel,
|
||||
lastUnreadChannel: null,
|
||||
},
|
||||
channelSidebar: {
|
||||
...baseState.views.channelSidebar,
|
||||
unreadFilterEnabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([unreadChannel2, unreadChannel1]);
|
||||
|
||||
state = {
|
||||
...state,
|
||||
views: {
|
||||
...state.views,
|
||||
channel: {
|
||||
...state.views.channels,
|
||||
lastUnreadChannel: {
|
||||
id: 'readChannel',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([readChannel, unreadChannel2, unreadChannel1]);
|
||||
});
|
||||
|
||||
test('should look at lastUnreadChannel to determine if the current channel had mentions before it was read', () => {
|
||||
let state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channels: {
|
||||
...baseState.entities.channels,
|
||||
currentChannelId: 'readChannel',
|
||||
myMembers: {
|
||||
...baseState.entities.channels.myMembers,
|
||||
unreadChannel1: {notify_props: {}, mention_count: 2, msg_count: 8},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
...baseState.entities.general,
|
||||
},
|
||||
preferences: {
|
||||
...baseState.entities.preferences,
|
||||
},
|
||||
},
|
||||
views: {
|
||||
...baseState.views,
|
||||
channel: {
|
||||
...baseState.views.channel,
|
||||
lastUnreadChannel: {
|
||||
id: 'readChannel',
|
||||
hadMentions: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// readChannel previously had no mentions, so it should be sorted with the non-mentions
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([unreadChannel1, readChannel, unreadChannel2]);
|
||||
|
||||
state = {
|
||||
...state,
|
||||
views: {
|
||||
...state.views,
|
||||
channel: {
|
||||
...state.views.channel,
|
||||
lastUnreadChannel: {
|
||||
id: 'readChannel',
|
||||
hadMentions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// readChannel previously had a mention, so it should be sorted with the mentions
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([readChannel, unreadChannel1, unreadChannel2]);
|
||||
});
|
||||
|
||||
test('should sort muted channels last', () => {
|
||||
let state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channels: {
|
||||
...baseState.entities.channels,
|
||||
myMembers: {
|
||||
...baseState.entities.channels.myMembers,
|
||||
unreadChannel2: {notify_props: {mark_unread: 'all'}, msg_count: 10, mention_count: 2},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
...baseState.entities.general,
|
||||
},
|
||||
preferences: {
|
||||
...baseState.entities.preferences,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// No channels are muted
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([unreadChannel2, unreadChannel1, currentChannel]);
|
||||
|
||||
state = {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
channels: {
|
||||
...state.entities.channels,
|
||||
myMembers: {
|
||||
...state.entities.channels.myMembers,
|
||||
unreadChannel2: {notify_props: {mark_unread: 'mention'}, msg_count: 10, mention_count: 2},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
...baseState.entities.general,
|
||||
},
|
||||
preferences: {
|
||||
...baseState.entities.preferences,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// unreadChannel2 is muted and has a mention
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([unreadChannel1, currentChannel, unreadChannel2]);
|
||||
});
|
||||
|
||||
test('should not show archived channels unless they are the current channel', () => {
|
||||
const archivedChannel = {id: 'archivedChannel', delete_at: 1, last_post_at: 400};
|
||||
|
||||
let state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channels: {
|
||||
...baseState.entities.channels,
|
||||
channels: {
|
||||
...baseState.entities.channels.channels,
|
||||
archivedChannel,
|
||||
},
|
||||
channelsInTeam: {
|
||||
...baseState.entities.channels.channelsInTeam,
|
||||
team1: [
|
||||
...baseState.entities.channels.channelsInTeam.team1,
|
||||
'archivedChannel',
|
||||
],
|
||||
},
|
||||
messageCounts: {
|
||||
...baseState.entities.channels.messageCounts,
|
||||
archivedChannel: {total: 10},
|
||||
},
|
||||
myMembers: {
|
||||
...baseState.entities.channels.myMembers,
|
||||
archivedChannel: {notify_props: {}, mention_count: 0, msg_count: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([unreadChannel2, unreadChannel1, currentChannel]);
|
||||
|
||||
state = {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
channels: {
|
||||
...state.entities.channels,
|
||||
currentChannelId: 'archivedChannel',
|
||||
},
|
||||
},
|
||||
views: {
|
||||
...state.views,
|
||||
channel: {
|
||||
...state.views.channel,
|
||||
lastUnreadChannel: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getUnreadChannels(state)).toEqual([archivedChannel, unreadChannel2, unreadChannel1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDisplayedChannels', () => {
|
||||
const currentChannel = TestHelper.getChannelMock({id: 'currentChannel', delete_at: 0, last_post_at: 0});
|
||||
const readChannel = {id: 'readChannel', delete_at: 0, last_post_at: 300};
|
||||
const unreadChannel1 = {id: 'unreadChannel1', delete_at: 0, last_post_at: 100};
|
||||
const unreadChannel2 = {id: 'unreadChannel2', delete_at: 0, last_post_at: 200};
|
||||
|
||||
const category1 = {id: 'category1', team_id: 'team1', channel_ids: [currentChannel.id, unreadChannel1.id]};
|
||||
const category2 = {id: 'category2', team_id: 'team1', channel_ids: [readChannel.id, unreadChannel2.id]};
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
currentChannel,
|
||||
readChannel,
|
||||
unreadChannel1,
|
||||
unreadChannel2,
|
||||
},
|
||||
channelsInTeam: {
|
||||
team1: ['unreadChannel1', 'unreadChannel2', 'readChannel'],
|
||||
},
|
||||
currentChannelId: 'currentChannel',
|
||||
messageCounts: {
|
||||
currentChannel: {total: 0},
|
||||
readChannel: {total: 10},
|
||||
unreadChannel1: {total: 10},
|
||||
unreadChannel2: {total: 10},
|
||||
},
|
||||
myMembers: {
|
||||
currentChannel: {notify_props: {}, mention_count: 0, msg_count: 0},
|
||||
readChannel: {notify_props: {}, mention_count: 0, msg_count: 10},
|
||||
unreadChannel1: {notify_props: {}, mention_count: 0, msg_count: 8},
|
||||
unreadChannel2: {notify_props: {}, mention_count: 0, msg_count: 8},
|
||||
},
|
||||
},
|
||||
channelCategories: {
|
||||
byId: {
|
||||
category1,
|
||||
category2,
|
||||
},
|
||||
orderByTeam: {
|
||||
team1: [category1.id, category2.id],
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
ExperimentalGroupUnreadChannels: 'true',
|
||||
},
|
||||
},
|
||||
posts: {
|
||||
postsInChannel: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION)]: {value: 'false'},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team1',
|
||||
},
|
||||
users: {
|
||||
profiles: {},
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
storage: {},
|
||||
},
|
||||
views: {
|
||||
channel: {
|
||||
lastUnreadChannel: null,
|
||||
},
|
||||
channelSidebar: {
|
||||
unreadFilterEnabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should return channels in the order that they appear in each category', () => {
|
||||
const state = baseState;
|
||||
|
||||
expect(Selectors.getDisplayedChannels(state)).toEqual([
|
||||
currentChannel,
|
||||
unreadChannel1,
|
||||
readChannel,
|
||||
unreadChannel2,
|
||||
]);
|
||||
});
|
||||
|
||||
test('with unread filter enabled, should not return read channels', () => {
|
||||
const state = {
|
||||
...baseState,
|
||||
views: {
|
||||
...baseState.views,
|
||||
channelSidebar: {
|
||||
unreadFilterEnabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getDisplayedChannels(state)).toEqual([
|
||||
unreadChannel2,
|
||||
unreadChannel1,
|
||||
currentChannel,
|
||||
]);
|
||||
});
|
||||
|
||||
test('with unreads section enabled, should have unread channels first', () => {
|
||||
const state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
preferences: {
|
||||
...baseState.preferences,
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION)]: {value: 'true'},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getDisplayedChannels(state)).toEqual([
|
||||
unreadChannel2,
|
||||
unreadChannel1,
|
||||
currentChannel,
|
||||
readChannel,
|
||||
]);
|
||||
});
|
||||
|
||||
describe('memoization', () => {
|
||||
test('should return the same result when called with the same state', () => {
|
||||
expect(Selectors.getDisplayedChannels(baseState)).toBe(Selectors.getDisplayedChannels(baseState));
|
||||
});
|
||||
|
||||
test('should return the same result when called with identical state', () => {
|
||||
const modifiedState = {...baseState};
|
||||
|
||||
expect(Selectors.getDisplayedChannels(baseState)).toBe(Selectors.getDisplayedChannels(modifiedState));
|
||||
});
|
||||
|
||||
test('should return the same result when called with unrelated state changing', () => {
|
||||
const modifiedState = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
users: {
|
||||
...baseState.entities.users,
|
||||
profiles: {
|
||||
someUser: {id: 'someUser'},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getDisplayedChannels(baseState)).toBe(Selectors.getDisplayedChannels(modifiedState));
|
||||
});
|
||||
|
||||
test('should return a new result when the unreads section is enabled', () => {
|
||||
const modifiedState = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
preferences: {
|
||||
...baseState.preferences,
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION)]: {value: 'true'},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Selectors.getDisplayedChannels(baseState)).not.toBe(Selectors.getDisplayedChannels(modifiedState));
|
||||
expect(Selectors.getDisplayedChannels(modifiedState)).toBe(Selectors.getDisplayedChannels(modifiedState));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeGetFilteredChannelIdsForCategory', () => {
|
||||
const currentChannel = TestHelper.getChannelMock({id: 'currentChannel', delete_at: 0, last_post_at: 0});
|
||||
const readChannel = {id: 'readChannel', delete_at: 0, last_post_at: 300};
|
||||
const unreadChannel1 = {id: 'unreadChannel1', delete_at: 0, last_post_at: 100};
|
||||
const unreadChannel2 = {id: 'unreadChannel2', delete_at: 0, last_post_at: 200};
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
currentChannel,
|
||||
readChannel,
|
||||
unreadChannel1,
|
||||
unreadChannel2,
|
||||
},
|
||||
channelsInTeam: {
|
||||
team1: ['unreadChannel1', 'unreadChannel2', 'readChannel'],
|
||||
},
|
||||
currentChannelId: 'currentChannel',
|
||||
messageCounts: {
|
||||
currentChannel: {total: 0},
|
||||
readChannel: {total: 10},
|
||||
unreadChannel1: {total: 10},
|
||||
unreadChannel2: {total: 10},
|
||||
},
|
||||
myMembers: {
|
||||
currentChannel: {notify_props: {}, mention_count: 0, msg_count: 0},
|
||||
readChannel: {notify_props: {}, mention_count: 0, msg_count: 10},
|
||||
unreadChannel1: {notify_props: {}, mention_count: 0, msg_count: 8},
|
||||
unreadChannel2: {notify_props: {}, mention_count: 0, msg_count: 8},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
ExperimentalGroupUnreadChannels: 'true',
|
||||
},
|
||||
},
|
||||
posts: {
|
||||
postsInChannel: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION)]: {value: 'false'},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team1',
|
||||
},
|
||||
users: {
|
||||
profiles: {},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
channel: {
|
||||
lastUnreadChannel: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should only include channels in the given category', () => {
|
||||
const category1 = {id: 'category1', team_id: 'team1', channel_ids: [currentChannel.id, unreadChannel1.id]};
|
||||
const category2 = {id: 'category2', team_id: 'team1', channel_ids: [readChannel.id, unreadChannel2.id]};
|
||||
|
||||
const state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channelCategories: {
|
||||
...baseState.entities.channelCategories,
|
||||
byId: {
|
||||
category1,
|
||||
category2,
|
||||
},
|
||||
orderByTeam: {
|
||||
team1: [category1, category2],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getFilteredChannelIdsForCategory = Selectors.makeGetFilteredChannelIdsForCategory();
|
||||
|
||||
expect(getFilteredChannelIdsForCategory(state, category1)).toEqual([currentChannel.id, unreadChannel1.id]);
|
||||
expect(getFilteredChannelIdsForCategory(state, category2)).toEqual([readChannel.id, unreadChannel2.id]);
|
||||
});
|
||||
|
||||
test('with the unreads category enabled, should not include unread channels', () => {
|
||||
const category1 = {id: 'category1', team_id: 'team1', channel_ids: [currentChannel.id, readChannel.id, unreadChannel1.id, unreadChannel2.id]};
|
||||
|
||||
const state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channelCategories: {
|
||||
...baseState.entities.channelCategories,
|
||||
byId: {
|
||||
category1,
|
||||
},
|
||||
orderByTeam: {
|
||||
team1: [category1],
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION)]: {value: 'true'},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getFilteredChannelIdsForCategory = Selectors.makeGetFilteredChannelIdsForCategory();
|
||||
|
||||
expect(getFilteredChannelIdsForCategory(state, category1)).toEqual([currentChannel.id, readChannel.id]);
|
||||
});
|
||||
|
||||
test('with the unreads category enabled, should not include the current channel if it was previously unread', () => {
|
||||
const category1 = {id: 'category1', team_id: 'team1', channel_ids: [currentChannel.id, readChannel.id, unreadChannel1.id, unreadChannel2.id]};
|
||||
|
||||
const state = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
channelCategories: {
|
||||
...baseState.entities.channelCategories,
|
||||
byId: {
|
||||
category1,
|
||||
},
|
||||
orderByTeam: {
|
||||
team1: [category1],
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION)]: {value: 'true'},
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
...baseState.views,
|
||||
channel: {
|
||||
...baseState.views.channel,
|
||||
lastUnreadChannel: {
|
||||
id: currentChannel.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getFilteredChannelIdsForCategory = Selectors.makeGetFilteredChannelIdsForCategory();
|
||||
|
||||
expect(getFilteredChannelIdsForCategory(state, category1)).toEqual([readChannel.id]);
|
||||
});
|
||||
});
|
||||
218
webapp/channels/src/selectors/views/channel_sidebar.ts
Обычный файл
218
webapp/channels/src/selectors/views/channel_sidebar.ts
Обычный файл
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {
|
||||
getAllChannels,
|
||||
getCurrentChannelId,
|
||||
getMyChannelMemberships,
|
||||
getUnreadChannelIds,
|
||||
sortUnreadChannels,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {
|
||||
makeGetCategoriesForTeam,
|
||||
makeGetChannelsByCategory,
|
||||
makeGetChannelIdsForCategory,
|
||||
} from 'mattermost-redux/selectors/entities/channel_categories';
|
||||
import {shouldShowUnreadsCategory, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {Channel} from '@mattermost/types/channels';
|
||||
import {CategorySorting, ChannelCategory} from '@mattermost/types/channel_categories';
|
||||
import {RelationOneToOne} from '@mattermost/types/utilities';
|
||||
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import {DraggingState, GlobalState} from 'types/store';
|
||||
|
||||
export function isUnreadFilterEnabled(state: GlobalState): boolean {
|
||||
return state.views.channelSidebar.unreadFilterEnabled && !shouldShowUnreadsCategory(state);
|
||||
}
|
||||
|
||||
export const getCategoriesForCurrentTeam: (state: GlobalState) => ChannelCategory[] = (() => {
|
||||
const getCategoriesForTeam = makeGetCategoriesForTeam();
|
||||
|
||||
return memoizeResult((state: GlobalState) => {
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
return getCategoriesForTeam(state, currentTeamId);
|
||||
});
|
||||
})();
|
||||
|
||||
export const getAutoSortedCategoryIds: (state: GlobalState) => Set<string> = (() => createSelector(
|
||||
'getAutoSortedCategoryIds',
|
||||
(state: GlobalState) => getCategoriesForCurrentTeam(state),
|
||||
(categories) => {
|
||||
return new Set(categories.filter((category) =>
|
||||
category.sorting === CategorySorting.Alphabetical ||
|
||||
category.sorting === CategorySorting.Recency).map((category) => category.id));
|
||||
},
|
||||
))();
|
||||
|
||||
export const getChannelsByCategoryForCurrentTeam: (state: GlobalState) => RelationOneToOne<ChannelCategory, Channel[]> = (() => {
|
||||
const getChannelsByCategory = makeGetChannelsByCategory();
|
||||
|
||||
return memoizeResult((state: GlobalState) => {
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
return getChannelsByCategory(state, currentTeamId);
|
||||
});
|
||||
})();
|
||||
|
||||
const getUnreadChannelIdsSet = createSelector(
|
||||
'getUnreadChannelIdsSet',
|
||||
(state: GlobalState) => getUnreadChannelIds(state, state.views.channel.lastUnreadChannel),
|
||||
(unreadChannelIds) => {
|
||||
return new Set(unreadChannelIds);
|
||||
},
|
||||
);
|
||||
|
||||
// getChannelsInCategoryOrder returns an array of channels on the current team that are currently visible in the sidebar.
|
||||
// Channels are returned in the same order as in the sidebar. Channels in the Unreads category are not included.
|
||||
export const getChannelsInCategoryOrder = (() => {
|
||||
return createSelector(
|
||||
'getChannelsInCategoryOrder',
|
||||
getCategoriesForCurrentTeam,
|
||||
getChannelsByCategoryForCurrentTeam,
|
||||
getCurrentChannelId,
|
||||
getUnreadChannelIdsSet,
|
||||
shouldShowUnreadsCategory,
|
||||
(categories, channelsByCategory, currentChannelId, unreadChannelIds, showUnreadsCategory) => {
|
||||
return categories.map((category) => {
|
||||
const channels = channelsByCategory[category.id];
|
||||
|
||||
return channels.filter((channel: Channel) => {
|
||||
const isUnread = unreadChannelIds.has(channel.id);
|
||||
|
||||
if (showUnreadsCategory) {
|
||||
// Filter out channels that have been moved to the Unreads category
|
||||
if (isUnread) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (category.collapsed) {
|
||||
// Filter out channels that would be hidden by a collapsed category
|
||||
if (!isUnread && currentChannelId !== channel.id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}).flat();
|
||||
},
|
||||
);
|
||||
})();
|
||||
|
||||
// getUnreadChannels returns an array of all unread channels on the current team for display with the unread filter
|
||||
// enabled. Channels are sorted by recency with channels containing a mention grouped first.
|
||||
export const getUnreadChannels = (() => {
|
||||
const getUnsortedUnreadChannels = createSelector(
|
||||
'getUnreadChannels',
|
||||
getAllChannels,
|
||||
getUnreadChannelIdsSet,
|
||||
getCurrentChannelId,
|
||||
isUnreadFilterEnabled,
|
||||
(allChannels, unreadChannelIds, currentChannelId, unreadFilterEnabled) => {
|
||||
const unreadChannels: Channel[] = [];
|
||||
|
||||
for (const channelId of unreadChannelIds) {
|
||||
const channel = allChannels[channelId];
|
||||
|
||||
if (channel) {
|
||||
// Only include an archived channel if it's the current channel
|
||||
if (channel.delete_at > 0 && channel.id !== currentChannelId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unreadChannels.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// This selector is used for both the unread filter and the unreads category which treat the current
|
||||
// channel differently
|
||||
if (unreadFilterEnabled) {
|
||||
// The current channel is already in unreadChannels if it was previously unread but we need to add it
|
||||
// if it wasn't previously unread
|
||||
if (currentChannelId && unreadChannels.findIndex((channel) => channel.id === currentChannelId) === -1) {
|
||||
if (allChannels[currentChannelId]) {
|
||||
unreadChannels.push(allChannels[currentChannelId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unreadChannels;
|
||||
},
|
||||
);
|
||||
|
||||
const sortChannels = createSelector(
|
||||
'sortChannels',
|
||||
(_: GlobalState, channels: Channel[]) => channels,
|
||||
getMyChannelMemberships,
|
||||
(state: GlobalState) => state.views.channel.lastUnreadChannel,
|
||||
isCollapsedThreadsEnabled,
|
||||
(channels, myMembers, lastUnreadChannel, crtEnabled) => {
|
||||
return sortUnreadChannels(channels, myMembers, lastUnreadChannel, crtEnabled);
|
||||
},
|
||||
);
|
||||
|
||||
return (state: GlobalState) => {
|
||||
const channels = getUnsortedUnreadChannels(state);
|
||||
return sortChannels(state, channels);
|
||||
};
|
||||
})();
|
||||
|
||||
// WARNING: below functions are used in getDisplayedChannels only. Do not use it elsewhere.
|
||||
function concatChannels(channelsA: Channel[], channelsB: Channel[]) {
|
||||
return [...channelsA, ...channelsB];
|
||||
}
|
||||
const memoizedConcatChannels = memoizeResult(concatChannels);
|
||||
|
||||
// Returns an array of channels in the order that they currently appear in the sidebar. Channels are filtered out if they
|
||||
// are hidden such as by a collapsed category or the unread filter.
|
||||
export const getDisplayedChannels = createSelector(
|
||||
'getDisplayedChannels',
|
||||
isUnreadFilterEnabled,
|
||||
getUnreadChannels,
|
||||
shouldShowUnreadsCategory,
|
||||
getChannelsInCategoryOrder,
|
||||
(unreadFilterEnabled, unreadChannels, showUnreadsCategory, channelsInCategoryOrder) => {
|
||||
if (unreadFilterEnabled) {
|
||||
return unreadChannels;
|
||||
}
|
||||
|
||||
if (showUnreadsCategory) {
|
||||
return memoizedConcatChannels(unreadChannels, channelsInCategoryOrder);
|
||||
}
|
||||
|
||||
return channelsInCategoryOrder;
|
||||
},
|
||||
);
|
||||
|
||||
// Returns a selector that, given a category, returns the ids of channels visible in that category. The returned channels do not
|
||||
// include unread channels when the Unreads category is enabled.
|
||||
export function makeGetFilteredChannelIdsForCategory(): (state: GlobalState, category: ChannelCategory) => string[] {
|
||||
const getChannelIdsForCategory = makeGetChannelIdsForCategory();
|
||||
|
||||
return createSelector(
|
||||
'makeGetFilteredChannelIdsForCategory',
|
||||
getChannelIdsForCategory,
|
||||
getUnreadChannelIdsSet,
|
||||
shouldShowUnreadsCategory,
|
||||
(channelIds, unreadChannelIdsSet, showUnreadsCategory) => {
|
||||
if (!showUnreadsCategory) {
|
||||
return channelIds;
|
||||
}
|
||||
|
||||
const filtered = channelIds.filter((id) => !unreadChannelIdsSet.has(id));
|
||||
|
||||
return filtered.length === channelIds.length ? channelIds : filtered;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getDraggingState(state: GlobalState): DraggingState {
|
||||
return state.views.channelSidebar.draggingState;
|
||||
}
|
||||
|
||||
export function isChannelSelected(state: GlobalState, channelId: string): boolean {
|
||||
return state.views.channelSidebar.multiSelectedChannelIds.indexOf(channelId) !== -1;
|
||||
}
|
||||
107
webapp/channels/src/selectors/views/custom_status.test.ts
Обычный файл
107
webapp/channels/src/selectors/views/custom_status.test.ts
Обычный файл
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as UserSelectors from 'mattermost-redux/selectors/entities/users';
|
||||
import * as GeneralSelectors from 'mattermost-redux/selectors/entities/general';
|
||||
import * as PreferenceSelectors from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import configureStore from 'store';
|
||||
import {makeGetCustomStatus, getRecentCustomStatuses, isCustomStatusEnabled, showStatusDropdownPulsatingDot, showPostHeaderUpdateStatusButton} from 'selectors/views/custom_status';
|
||||
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/users');
|
||||
jest.mock('mattermost-redux/selectors/entities/general');
|
||||
jest.mock('mattermost-redux/selectors/entities/preferences');
|
||||
|
||||
const customStatus = {
|
||||
emoji: 'speech_balloon',
|
||||
text: 'speaking',
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
};
|
||||
|
||||
describe('getCustomStatus', () => {
|
||||
const user = TestHelper.getUserMock();
|
||||
const getCustomStatus = makeGetCustomStatus();
|
||||
|
||||
it('should return undefined when current user has no custom status set', async () => {
|
||||
const store = await configureStore();
|
||||
(UserSelectors.getCurrentUser as jest.Mock).mockReturnValue(user);
|
||||
expect(getCustomStatus(store.getState())).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when user with given id has no custom status set', async () => {
|
||||
const store = await configureStore();
|
||||
(UserSelectors.getUser as jest.Mock).mockReturnValue(user);
|
||||
expect(getCustomStatus(store.getState(), user.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return customStatus object when there is custom status set', async () => {
|
||||
const store = await configureStore();
|
||||
const newUser = {...user};
|
||||
newUser.props.customStatus = JSON.stringify(customStatus);
|
||||
(UserSelectors.getCurrentUser as jest.Mock).mockReturnValue(newUser);
|
||||
expect(getCustomStatus(store.getState())).toStrictEqual(customStatus);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecentCustomStatuses', () => {
|
||||
const preference = {
|
||||
myPreference: {
|
||||
value: JSON.stringify([]),
|
||||
},
|
||||
};
|
||||
|
||||
it('should return empty arr if there are no recent custom statuses', async () => {
|
||||
const store = await configureStore();
|
||||
(PreferenceSelectors.get as jest.Mock).mockReturnValue(preference.myPreference.value);
|
||||
expect(getRecentCustomStatuses(store.getState())).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should return arr of custom statuses if there are recent custom statuses', async () => {
|
||||
const store = await configureStore();
|
||||
preference.myPreference.value = JSON.stringify([customStatus]);
|
||||
(PreferenceSelectors.get as jest.Mock).mockReturnValue(preference.myPreference.value);
|
||||
expect(getRecentCustomStatuses(store.getState())).toStrictEqual([customStatus]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomStatusEnabled', () => {
|
||||
const config = {
|
||||
EnableCustomUserStatuses: 'true',
|
||||
};
|
||||
|
||||
it('should return false if EnableCustomUserStatuses is false in the config', async () => {
|
||||
const store = await configureStore();
|
||||
expect(isCustomStatusEnabled(store.getState())).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true if EnableCustomUserStatuses is true in the config', async () => {
|
||||
const store = await configureStore();
|
||||
(GeneralSelectors.getConfig as jest.Mock).mockReturnValue(config);
|
||||
expect(isCustomStatusEnabled(store.getState())).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('showStatusDropdownPulsatingDot and showPostHeaderUpdateStatusButton', () => {
|
||||
const preference = {
|
||||
myPreference: {
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
it('should return true if user has not opened the custom status modal before', async () => {
|
||||
const store = await configureStore();
|
||||
(PreferenceSelectors.get as jest.Mock).mockReturnValue(preference.myPreference.value);
|
||||
expect(showStatusDropdownPulsatingDot(store.getState())).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false if user has opened the custom status modal before', async () => {
|
||||
const store = await configureStore();
|
||||
preference.myPreference.value = JSON.stringify({[Preferences.CUSTOM_STATUS_MODAL_VIEWED]: true});
|
||||
(PreferenceSelectors.get as jest.Mock).mockReturnValue(preference.myPreference.value);
|
||||
expect(showPostHeaderUpdateStatusButton(store.getState())).toBeFalsy();
|
||||
});
|
||||
});
|
||||
70
webapp/channels/src/selectors/views/custom_status.ts
Обычный файл
70
webapp/channels/src/selectors/views/custom_status.ts
Обычный файл
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCurrentUser, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {CustomStatusDuration, UserCustomStatus} from '@mattermost/types/users';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {getCurrentUserTimezone} from 'selectors/general';
|
||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
|
||||
export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => UserCustomStatus {
|
||||
return createSelector(
|
||||
'makeGetCustomStatus',
|
||||
(state: GlobalState, userID?: string) => (userID ? getUser(state, userID) : getCurrentUser(state)),
|
||||
(user) => {
|
||||
const userProps = user?.props || {};
|
||||
return userProps.customStatus ? JSON.parse(userProps.customStatus) : undefined;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function isCustomStatusExpired(state: GlobalState, customStatus?: UserCustomStatus) {
|
||||
if (!customStatus) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (customStatus.duration === CustomStatusDuration.DONT_CLEAR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expiryTime = moment(customStatus.expires_at);
|
||||
const timezone = getCurrentUserTimezone(state);
|
||||
const currentTime = getCurrentMomentForTimezone(timezone);
|
||||
return currentTime.isSameOrAfter(expiryTime);
|
||||
}
|
||||
|
||||
export const getRecentCustomStatuses = createSelector(
|
||||
'getRecentCustomStatuses',
|
||||
(state: GlobalState) => get(state, Preferences.CATEGORY_CUSTOM_STATUS, Preferences.NAME_RECENT_CUSTOM_STATUSES),
|
||||
(value) => {
|
||||
return value ? JSON.parse(value) : [];
|
||||
},
|
||||
);
|
||||
|
||||
export function isCustomStatusEnabled(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
return config && config.EnableCustomUserStatuses === 'true';
|
||||
}
|
||||
|
||||
function showCustomStatusPulsatingDotAndPostHeader(state: GlobalState) {
|
||||
const customStatusTutorialState = get(state, Preferences.CATEGORY_CUSTOM_STATUS, Preferences.NAME_CUSTOM_STATUS_TUTORIAL_STATE);
|
||||
const modalAlreadyViewed = customStatusTutorialState && JSON.parse(customStatusTutorialState)[Preferences.CUSTOM_STATUS_MODAL_VIEWED];
|
||||
return !modalAlreadyViewed;
|
||||
}
|
||||
|
||||
export function showStatusDropdownPulsatingDot(state: GlobalState) {
|
||||
return showCustomStatusPulsatingDotAndPostHeader(state);
|
||||
}
|
||||
|
||||
export function showPostHeaderUpdateStatusButton(state: GlobalState) {
|
||||
return showCustomStatusPulsatingDotAndPostHeader(state);
|
||||
}
|
||||
157
webapp/channels/src/selectors/views/marketplace.test.ts
Обычный файл
157
webapp/channels/src/selectors/views/marketplace.test.ts
Обычный файл
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AuthorType, MarketplaceApp, MarketplacePlugin, ReleaseStage} from '@mattermost/types/marketplace';
|
||||
|
||||
import {
|
||||
getPlugins,
|
||||
getListing,
|
||||
getInstalledListing,
|
||||
getApp,
|
||||
getPlugin,
|
||||
getFilter,
|
||||
getInstalling,
|
||||
getError,
|
||||
} from 'selectors/views/marketplace';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
describe('marketplace', () => {
|
||||
const samplePlugin: MarketplacePlugin = {
|
||||
homepage_url: 'https://github.com/mattermost/mattermost-plugin-nps',
|
||||
download_url: 'https://github.com/mattermost/mattermost-plugin-nps/releases/download/v1.0.3/com.mattermost.nps-1.0.3.tar.gz',
|
||||
author_type: AuthorType.Mattermost,
|
||||
release_stage: ReleaseStage.Production,
|
||||
enterprise: false,
|
||||
manifest: {
|
||||
id: 'com.mattermost.nps',
|
||||
name: 'User Satisfaction Surveys',
|
||||
description: 'This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost',
|
||||
version: '1.0.3',
|
||||
min_server_version: '5.14.0',
|
||||
},
|
||||
installed_version: '',
|
||||
};
|
||||
|
||||
const sampleInstalledPlugin: MarketplacePlugin = {
|
||||
homepage_url: 'https://github.com/mattermost/mattermost-test',
|
||||
download_url: 'https://github.com/mattermost/mattermost-test/releases/download/v1.0.3/com.mattermost.nps-1.0.3.tar.gz',
|
||||
author_type: AuthorType.Mattermost,
|
||||
release_stage: ReleaseStage.Production,
|
||||
enterprise: false,
|
||||
manifest: {
|
||||
id: 'com.mattermost.test',
|
||||
name: 'Test',
|
||||
description: 'This plugin is to test',
|
||||
version: '1.0.3',
|
||||
min_server_version: '5.14.0',
|
||||
},
|
||||
installed_version: '1.0.3',
|
||||
};
|
||||
|
||||
const sampleApp: MarketplaceApp = {
|
||||
installed: false,
|
||||
author_type: AuthorType.Mattermost,
|
||||
release_stage: ReleaseStage.Production,
|
||||
enterprise: false,
|
||||
manifest: {
|
||||
app_id: 'some.id',
|
||||
display_name: 'Some App',
|
||||
},
|
||||
};
|
||||
|
||||
const sampleInstalledApp: MarketplaceApp = {
|
||||
installed: true,
|
||||
author_type: AuthorType.Mattermost,
|
||||
release_stage: ReleaseStage.Production,
|
||||
enterprise: false,
|
||||
manifest: {
|
||||
app_id: 'some.other.id',
|
||||
display_name: 'Some other App',
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
views: {
|
||||
marketplace: {
|
||||
plugins: [samplePlugin, sampleInstalledPlugin],
|
||||
apps: [sampleApp, sampleInstalledApp],
|
||||
installing: {'com.mattermost.nps': true},
|
||||
errors: {'com.mattermost.test': 'An error occurred'},
|
||||
filter: 'existing',
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
it('getListing should return all plugins and apps', () => {
|
||||
expect(getListing(state)).toEqual([samplePlugin, sampleInstalledPlugin, sampleApp, sampleInstalledApp]);
|
||||
});
|
||||
|
||||
it('getInstalledListing should return only installed plugins and apps', () => {
|
||||
expect(getInstalledListing(state)).toEqual([sampleInstalledPlugin, sampleInstalledApp]);
|
||||
});
|
||||
|
||||
it('getPlugins should return all plugins', () => {
|
||||
expect(getPlugins(state)).toEqual([samplePlugin, sampleInstalledPlugin]);
|
||||
});
|
||||
|
||||
describe('getPlugin', () => {
|
||||
it('should return samplePlugin', () => {
|
||||
expect(getPlugin(state, 'com.mattermost.nps')).toEqual(samplePlugin);
|
||||
});
|
||||
|
||||
it('should return sampleInstalledPlugin', () => {
|
||||
expect(getPlugin(state, 'com.mattermost.test')).toEqual(sampleInstalledPlugin);
|
||||
});
|
||||
|
||||
it('should return undefined for unknown plugin', () => {
|
||||
expect(getPlugin(state, 'unknown')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApp', () => {
|
||||
it('should return sampleApp', () => {
|
||||
expect(getApp(state, 'some.id')).toEqual(sampleApp);
|
||||
});
|
||||
|
||||
it('should return sampleInstalledApp', () => {
|
||||
expect(getApp(state, 'some.other.id')).toEqual(sampleInstalledApp);
|
||||
});
|
||||
|
||||
it('should return undefined for unknown app', () => {
|
||||
expect(getApp(state, 'unknown')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('getFilter should return the active filter', () => {
|
||||
expect(getFilter(state)).toEqual('existing');
|
||||
});
|
||||
|
||||
describe('getInstalling', () => {
|
||||
it('should return true for samplePlugin', () => {
|
||||
expect(getInstalling(state, 'com.mattermost.nps')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for sampleInstalledPlugin', () => {
|
||||
expect(getInstalling(state, 'com.mattermost.test')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for unknown plugin', () => {
|
||||
expect(getInstalling(state, 'unknown')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getError', () => {
|
||||
it('should return undefined for samplePlugin', () => {
|
||||
expect(getError(state, 'com.mattermost.nps')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return error value for sampleInstalledPlugin', () => {
|
||||
expect(getError(state, 'com.mattermost.test')).toBe('An error occurred');
|
||||
});
|
||||
|
||||
it('should return undefined for unknown plugin', () => {
|
||||
expect(getError(state, 'unknown')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
50
webapp/channels/src/selectors/views/marketplace.ts
Обычный файл
50
webapp/channels/src/selectors/views/marketplace.ts
Обычный файл
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {isPlugin} from 'mattermost-redux/utils/marketplace';
|
||||
import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export const getPlugins = (state: GlobalState): MarketplacePlugin[] => state.views.marketplace.plugins;
|
||||
|
||||
export const getApps = (state: GlobalState): MarketplaceApp[] => state.views.marketplace.apps;
|
||||
|
||||
export const getListing = createSelector(
|
||||
'getListing',
|
||||
getPlugins,
|
||||
getApps,
|
||||
(plugins, apps) => {
|
||||
if (plugins) {
|
||||
return (plugins as Array<MarketplacePlugin | MarketplaceApp>).concat(apps);
|
||||
}
|
||||
|
||||
return apps;
|
||||
},
|
||||
);
|
||||
|
||||
export const getInstalledListing = createSelector(
|
||||
'getInstalledListing',
|
||||
getListing,
|
||||
(listing) => listing.filter((i) => {
|
||||
if (isPlugin(i)) {
|
||||
return i.installed_version !== '';
|
||||
}
|
||||
|
||||
return i.installed;
|
||||
}),
|
||||
);
|
||||
|
||||
export const getPlugin = (state: GlobalState, id: string): MarketplacePlugin | undefined =>
|
||||
getPlugins(state).find(((p) => p.manifest && p.manifest.id === id));
|
||||
|
||||
export const getApp = (state: GlobalState, id: string): MarketplaceApp | undefined =>
|
||||
getApps(state).find(((p) => p.manifest && p.manifest.app_id === id));
|
||||
|
||||
export const getFilter = (state: GlobalState): string => state.views.marketplace.filter;
|
||||
|
||||
export const getInstalling = (state: GlobalState, id: string): boolean => Boolean(state.views.marketplace.installing[id]);
|
||||
|
||||
export const getError = (state: GlobalState, id: string): string => state.views.marketplace.errors[id];
|
||||
26
webapp/channels/src/selectors/views/modals.test.js
Обычный файл
26
webapp/channels/src/selectors/views/modals.test.js
Обычный файл
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
|
||||
describe('modals selector', () => {
|
||||
const state = {
|
||||
views: {
|
||||
modals: {
|
||||
modalState: {
|
||||
someModalId: {
|
||||
open: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should return the isOpen value from the state for the given modalId', () => {
|
||||
expect(isModalOpen(state, 'someModalId')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false when the given ModalId is not in state', () => {
|
||||
expect(isModalOpen(state, 'unknownModalId')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
23
webapp/channels/src/selectors/views/modals.ts
Обычный файл
23
webapp/channels/src/selectors/views/modals.ts
Обычный файл
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function isModalOpen(state: GlobalState, modalId: string) {
|
||||
return Boolean(state.views.modals.modalState[modalId] && state.views.modals.modalState[modalId].open);
|
||||
}
|
||||
export function isAnyModalOpen(state: GlobalState) {
|
||||
return Boolean(state.views.modals.modalState && findOpenModal(state));
|
||||
}
|
||||
|
||||
function findOpenModal(state: GlobalState) {
|
||||
let isOpen = false;
|
||||
const modalStateObject = state.views.modals.modalState;
|
||||
for (const modal in modalStateObject) {
|
||||
if (modal && modalStateObject[modal].open) {
|
||||
isOpen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isOpen;
|
||||
}
|
||||
16
webapp/channels/src/selectors/views/onboarding_tasks.ts
Обычный файл
16
webapp/channels/src/selectors/views/onboarding_tasks.ts
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export function isShowOnboardingTaskCompletion(state: GlobalState) {
|
||||
return state.views.onboardingTasks.isShowOnboardingTaskCompletion;
|
||||
}
|
||||
|
||||
export function isShowOnboardingCompleteProfileTour(state: GlobalState) {
|
||||
return state.views.onboardingTasks.isShowOnboardingCompleteProfileTour;
|
||||
}
|
||||
|
||||
export function isShowOnboardingVisitConsoleTour(state: GlobalState) {
|
||||
return state.views.onboardingTasks.isShowOnboardingVisitConsoleTour;
|
||||
}
|
||||
8
webapp/channels/src/selectors/views/product_menu.ts
Обычный файл
8
webapp/channels/src/selectors/views/product_menu.ts
Обычный файл
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function isSwitcherOpen(state: GlobalState): boolean {
|
||||
return state.views.productMenu.switcherOpen;
|
||||
}
|
||||
19
webapp/channels/src/selectors/views/status_dropdown.test.ts
Обычный файл
19
webapp/channels/src/selectors/views/status_dropdown.test.ts
Обычный файл
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {isStatusDropdownOpen} from 'selectors/views/status_dropdown';
|
||||
import {setStatusDropdown} from 'actions/views/status_dropdown';
|
||||
import configureStore from 'store';
|
||||
|
||||
describe('status_dropdown selector', () => {
|
||||
it('should return the isOpen value from the state', async () => {
|
||||
const store = await configureStore();
|
||||
expect(isStatusDropdownOpen(store.getState())).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true if statusDropdown in explicitly opened', async () => {
|
||||
const store = await configureStore();
|
||||
await store.dispatch(setStatusDropdown(true));
|
||||
expect(isStatusDropdownOpen(store.getState())).toBeTruthy();
|
||||
});
|
||||
});
|
||||
8
webapp/channels/src/selectors/views/status_dropdown.ts
Обычный файл
8
webapp/channels/src/selectors/views/status_dropdown.ts
Обычный файл
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export function isStatusDropdownOpen(state: GlobalState) {
|
||||
return state.views.statusDropdown.isOpen;
|
||||
}
|
||||
8
webapp/channels/src/selectors/views/system.ts
Обычный файл
8
webapp/channels/src/selectors/views/system.ts
Обычный файл
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function connectionErrorCount(state: GlobalState) {
|
||||
return state.views.system.websocketConnectionErrorCount;
|
||||
}
|
||||
16
webapp/channels/src/selectors/views/textbox.ts
Обычный файл
16
webapp/channels/src/selectors/views/textbox.ts
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function showPreviewOnCreateComment(state: GlobalState) {
|
||||
return state.views.textbox.shouldShowPreviewOnCreateComment;
|
||||
}
|
||||
|
||||
export function showPreviewOnCreatePost(state: GlobalState) {
|
||||
return state.views.textbox.shouldShowPreviewOnCreatePost;
|
||||
}
|
||||
|
||||
export function showPreviewOnEditChannelHeaderModal(state: GlobalState) {
|
||||
return state.views.textbox.shouldShowPreviewOnEditChannelHeaderModal;
|
||||
}
|
||||
62
webapp/channels/src/selectors/views/threads.test.ts
Обычный файл
62
webapp/channels/src/selectors/views/threads.test.ts
Обычный файл
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import * as selectors from './threads';
|
||||
|
||||
describe('selectors/views/threads', () => {
|
||||
const makeState = (selectedThreadId: string|null, selectedPostId: string, isSidebarOpen = true) => ({
|
||||
entities: {
|
||||
teams: {
|
||||
currentTeamId: 'current_team_id',
|
||||
},
|
||||
threads: {
|
||||
selected_thread_id: {
|
||||
id: 'selected_thread_id',
|
||||
},
|
||||
selected_post_id: {
|
||||
id: 'selected_post_id',
|
||||
},
|
||||
post_id: {
|
||||
id: 'post_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
threads: {
|
||||
selectedThreadIdInTeam: {
|
||||
current_team_id: selectedThreadId,
|
||||
},
|
||||
},
|
||||
rhs: {
|
||||
selectedPostId,
|
||||
isSidebarOpen,
|
||||
},
|
||||
rhsSuppressed: false,
|
||||
},
|
||||
}) as unknown as GlobalState;
|
||||
|
||||
describe('isThreadOpen', () => {
|
||||
test('should return true when a specific thread is open', () => {
|
||||
const state = makeState('selected_thread_id', '');
|
||||
expect(selectors.isThreadOpen(state, 'selected_thread_id')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when another thread is open', () => {
|
||||
const state = makeState(null, 'selected_post_id');
|
||||
expect(selectors.isThreadOpen(state, 'selected_thread_id')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when no threads are open', () => {
|
||||
const state = makeState(null, '');
|
||||
expect(selectors.isThreadOpen(state, 'post_id')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for either thread with both threads are open', () => {
|
||||
const state = makeState('selected_thread_id', 'selected_post_id');
|
||||
expect(selectors.isThreadOpen(state, 'selected_thread_id')).toBe(true);
|
||||
expect(selectors.isThreadOpen(state, 'selected_post_id')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
175
webapp/channels/src/selectors/views/threads.ts
Обычный файл
175
webapp/channels/src/selectors/views/threads.ts
Обычный файл
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {makeGetPostsForIds} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getThreads} from 'mattermost-redux/selectors/entities/threads';
|
||||
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
|
||||
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
import {UserThread} from '@mattermost/types/threads';
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {DATE_LINE, makeCombineUserActivityPosts, START_OF_NEW_MESSAGES, CREATE_COMMENT} from 'mattermost-redux/utils/post_list';
|
||||
import {createIdsSelector} from 'mattermost-redux/utils/helpers';
|
||||
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {ViewsState} from 'types/store/views';
|
||||
import {getIsRhsOpen, getSelectedPostId} from 'selectors/rhs';
|
||||
import {isFromWebhook} from 'utils/post_utils';
|
||||
|
||||
interface PostFilterOptions {
|
||||
postIds: Array<Post['id']>;
|
||||
showDate: boolean;
|
||||
lastViewedAt?: number;
|
||||
}
|
||||
|
||||
export function getSelectedThreadIdInTeam(state: GlobalState): ViewsState['threads']['selectedThreadIdInTeam'] {
|
||||
return state.views.threads.selectedThreadIdInTeam;
|
||||
}
|
||||
|
||||
export const getSelectedThreadIdInCurrentTeam: (state: GlobalState) => ViewsState['threads']['selectedThreadIdInTeam'][Team['id']] = createSelector(
|
||||
'getSelectedThreadIdInCurrentTeam',
|
||||
getCurrentTeamId,
|
||||
getSelectedThreadIdInTeam,
|
||||
(
|
||||
currentTeamId,
|
||||
selectedThreadIdInTeam,
|
||||
) => {
|
||||
return selectedThreadIdInTeam?.[currentTeamId] ?? null;
|
||||
},
|
||||
);
|
||||
|
||||
export const getSelectedThreadInCurrentTeam: (state: GlobalState) => UserThread | null = createSelector(
|
||||
'getSelectedThreadInCurrentTeam',
|
||||
getCurrentTeamId,
|
||||
getSelectedThreadIdInTeam,
|
||||
getThreads,
|
||||
(
|
||||
currentTeamId,
|
||||
selectedThreadIdInTeam,
|
||||
threads,
|
||||
) => {
|
||||
const threadId = selectedThreadIdInTeam?.[currentTeamId];
|
||||
return threadId ? threads[threadId] : null;
|
||||
},
|
||||
);
|
||||
|
||||
export function makeGetThreadLastViewedAt(): (state: GlobalState, threadId: Post['id']) => number {
|
||||
return createSelector(
|
||||
'makeGetThreadLastViewedAt',
|
||||
(state: GlobalState, threadId: Post['id']) => state.views.threads.lastViewedAt[threadId],
|
||||
getThreads,
|
||||
(_state, threadId) => threadId,
|
||||
(lastViewedAt, threads, threadId) => {
|
||||
if (typeof lastViewedAt === 'number') {
|
||||
return lastViewedAt;
|
||||
}
|
||||
|
||||
return threads[threadId]?.last_viewed_at;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const isThreadOpen = (state: GlobalState, threadId: UserThread['id']): boolean => {
|
||||
return (
|
||||
threadId === getSelectedThreadIdInCurrentTeam(state) ||
|
||||
(getIsRhsOpen(state) && threadId === getSelectedPostId(state))
|
||||
);
|
||||
};
|
||||
|
||||
export const isThreadManuallyUnread = (state: GlobalState, threadId: UserThread['id']): boolean => {
|
||||
return state.views.threads.manuallyUnread[threadId] || false;
|
||||
};
|
||||
|
||||
// Returns a selector that, given the state and an object containing an array of postIds and an optional
|
||||
// timestamp of when the channel was last read, returns a memoized array of postIds interspersed with
|
||||
// day indicators, an optional new message indicator and create comment.
|
||||
export function makeFilterRepliesAndAddSeparators() {
|
||||
const getPostsForIds = makeGetPostsForIds();
|
||||
|
||||
return createIdsSelector(
|
||||
'makeFilterPostsAndAddSeparators',
|
||||
(state: GlobalState, {postIds}: PostFilterOptions) => getPostsForIds(state, postIds),
|
||||
(_state: GlobalState, {lastViewedAt}: PostFilterOptions) => lastViewedAt,
|
||||
(_state: GlobalState, {showDate}: PostFilterOptions) => showDate,
|
||||
getCurrentUser,
|
||||
isTimezoneEnabled,
|
||||
(posts, lastViewedAt, showDate, currentUser, timeZoneEnabled) => {
|
||||
if (posts.length === 0 || !currentUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const out: string[] = [];
|
||||
let lastDate;
|
||||
let addedNewMessagesIndicator = false;
|
||||
|
||||
// Iterating through the posts from oldest to newest
|
||||
for (let i = posts.length - 1; i >= 0; i--) {
|
||||
const post = posts[i];
|
||||
|
||||
if (!post) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (showDate) {
|
||||
// Push on a date header if the last post was on a different day than the current one
|
||||
const postDate = new Date(post.create_at);
|
||||
if (timeZoneEnabled) {
|
||||
const currentOffset = postDate.getTimezoneOffset() * 60 * 1000;
|
||||
const timezone = getUserCurrentTimezone(currentUser.timezone);
|
||||
if (timezone) {
|
||||
const zone = moment.tz.zone(timezone);
|
||||
if (zone) {
|
||||
const timezoneOffset = zone.utcOffset(post.create_at) * 60 * 1000;
|
||||
postDate.setTime(post.create_at + (currentOffset - timezoneOffset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((!lastDate || lastDate.toDateString() !== postDate.toDateString())) {
|
||||
out.push(DATE_LINE + postDate.getTime());
|
||||
lastDate = postDate;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof lastViewedAt === 'number' &&
|
||||
post.create_at >= lastViewedAt &&
|
||||
(i < posts.length - 1) &&
|
||||
(post.user_id !== currentUser.id || isFromWebhook(post)) &&
|
||||
!addedNewMessagesIndicator
|
||||
) {
|
||||
out.push(START_OF_NEW_MESSAGES);
|
||||
addedNewMessagesIndicator = true;
|
||||
}
|
||||
|
||||
out.push(post.id);
|
||||
}
|
||||
|
||||
out.push(CREATE_COMMENT);
|
||||
|
||||
// Flip it back to newest to oldest
|
||||
return out.reverse();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function makePrepareReplyIdsForThreadViewer() {
|
||||
const filterRepliesAndAddSeparators = makeFilterRepliesAndAddSeparators();
|
||||
const combineUserActivityPosts = makeCombineUserActivityPosts();
|
||||
return (state: GlobalState, options: PostFilterOptions) => {
|
||||
const postIds = filterRepliesAndAddSeparators(state, options);
|
||||
return combineUserActivityPosts(state, postIds);
|
||||
};
|
||||
}
|
||||
|
||||
export function getThreadToastStatus(state: GlobalState) {
|
||||
return state.views.threads.toastStatus;
|
||||
}
|
||||
6
webapp/channels/src/selectors/views/websocket.ts
Обычный файл
6
webapp/channels/src/selectors/views/websocket.ts
Обычный файл
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export const getSocketStatus = (state: GlobalState) => state.websocket;
|
||||
18
webapp/channels/src/selectors/work_template.ts
Обычный файл
18
webapp/channels/src/selectors/work_template.ts
Обычный файл
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export const areWorkTemplatesEnabled = createSelector(
|
||||
'areWorktemplatesEnabled',
|
||||
(state: GlobalState) => getFeatureFlagValue(state, 'WorkTemplate') === 'true',
|
||||
(state: GlobalState) => getFeatureFlagValue(state, 'BoardsProduct') === 'true',
|
||||
(state: GlobalState) => Boolean(state.plugins.plugins?.playbooks),
|
||||
(workTemplateFF, boardProductEnabled, pluginPlaybooksInstalled) => {
|
||||
return workTemplateFF && boardProductEnabled && pluginPlaybooksInstalled;
|
||||
},
|
||||
);
|
||||
|
||||
Ссылка в новой задаче
Block a user