Unverified Commit 97beac07 authored by Mattermost Build's avatar Mattermost Build Committed by GitHub
Browse files

MM-27343 Adjust sidebar channel drop IDs to account for archived channels (#6015) (#6041)

* MM-27343 Adjust sidebar channel drop IDs to account for archived channels

* Add missing type

* Remove test timeout

(cherry picked from commit 44336e9d

)
Co-authored-by: default avatarHarrison Healey <harrisonmhealey@gmail.com>
parent 90c88f4d
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {CategorySorting} from 'mattermost-redux/types/channel_categories';
import {insertWithoutDuplicates} from 'mattermost-redux/utils/array_utils';
import configureStore from 'store';
import {isCategoryCollapsedFromStorage} from 'selectors/views/channel_sidebar';
......@@ -9,18 +12,20 @@ import {getPrefix} from 'utils/storage_utils';
import * as Actions from './channel_sidebar';
describe('setCategoryCollapsed', () => {
test('should save category expanded and category collapsed', async () => {
test('should save category expanded and category collapsed', () => {
const category1 = 'category1';
const initialState = {
users: {
currentUserId: 'user1',
profiles: {
user1: {},
entities: {
users: {
currentUserId: 'user1',
profiles: {
user1: {},
},
},
},
};
const store = await configureStore(initialState);
const store = configureStore(initialState);
store.dispatch(Actions.setCategoryCollapsed(category1, true));
......@@ -31,3 +36,157 @@ describe('setCategoryCollapsed', () => {
expect(isCategoryCollapsedFromStorage(getPrefix(store.getState()), store.getState().storage.storage, category1)).toBe(false);
});
});
describe('adjustTargetIndexForMove', () => {
const channelIds = ['one', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'seven'];
const initialState = {
entities: {
channelCategories: {
byId: {
category1: {
id: 'category1',
channel_ids: channelIds,
sorting: CategorySorting.Manual,
},
},
},
channels: {
channels: {
new: {id: 'new', delete_at: 0},
one: {id: 'one', delete_at: 0},
twoDeleted: {id: 'twoDeleted', delete_at: 1},
three: {id: 'three', delete_at: 0},
four: {id: 'four', delete_at: 0},
fiveDeleted: {id: 'fiveDeleted', delete_at: 1},
six: {id: 'six', delete_at: 0},
seven: {id: 'seven', delete_at: 0},
},
},
},
};
describe('should place newly added channels correctly in the category', () => {
const testCases = [
{
inChannelIds: ['new', 'one', 'three', 'four', 'six', 'seven'],
expectedChannelIds: ['new', 'one', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'seven'],
},
{
inChannelIds: ['one', 'new', 'three', 'four', 'six', 'seven'],
expectedChannelIds: ['one', 'new', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'seven'],
},
{
inChannelIds: ['one', 'three', 'new', 'four', 'six', 'seven'],
expectedChannelIds: ['one', 'twoDeleted', 'three', 'new', 'four', 'fiveDeleted', 'six', 'seven'],
},
{
inChannelIds: ['one', 'three', 'four', 'new', 'six', 'seven'],
expectedChannelIds: ['one', 'twoDeleted', 'three', 'four', 'new', 'fiveDeleted', 'six', 'seven'],
},
{
inChannelIds: ['one', 'three', 'four', 'six', 'new', 'seven'],
expectedChannelIds: ['one', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'new', 'seven'],
},
{
inChannelIds: ['one', 'three', 'four', 'six', 'seven', 'new'],
expectedChannelIds: ['one', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'seven', 'new'],
},
];
for (let i = 0; i < testCases.length; i++) {
const testCase = testCases[i];
test('at ' + i, async () => {
const store = configureStore(initialState);
const targetIndex = testCase.inChannelIds.indexOf('new');
const newIndex = Actions.adjustTargetIndexForMove(store.getState(), 'category1', 'new', targetIndex);
const actualChannelIds = insertWithoutDuplicates(channelIds, 'new', newIndex);
expect(actualChannelIds).toEqual(testCase.expectedChannelIds);
});
}
});
describe('should be able to move channels forwards', () => {
const testCases = [
{
inChannelIds: ['one', 'three', 'four', 'six', 'seven'],
expectedChannelIds: ['one', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'seven'],
},
{
inChannelIds: ['three', 'one', 'four', 'six', 'seven'],
expectedChannelIds: ['twoDeleted', 'three', 'one', 'four', 'fiveDeleted', 'six', 'seven'],
},
{
inChannelIds: ['three', 'four', 'one', 'six', 'seven'],
expectedChannelIds: ['twoDeleted', 'three', 'four', 'one', 'fiveDeleted', 'six', 'seven'],
},
{
inChannelIds: ['three', 'four', 'six', 'one', 'seven'],
expectedChannelIds: ['twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'one', 'seven'],
},
{
inChannelIds: ['three', 'four', 'six', 'seven', 'one'],
expectedChannelIds: ['twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'seven', 'one'],
},
];
for (let i = 0; i < testCases.length; i++) {
const testCase = testCases[i];
test('at ' + i, async () => {
const store = configureStore(initialState);
const targetIndex = testCase.inChannelIds.indexOf('one');
const newIndex = Actions.adjustTargetIndexForMove(store.getState(), 'category1', 'one', targetIndex);
const actualChannelIds = insertWithoutDuplicates(channelIds, 'one', newIndex);
expect(actualChannelIds).toEqual(testCase.expectedChannelIds);
});
}
});
describe('should be able to move channels backwards', () => {
const testCases = [
{
inChannelIds: ['one', 'three', 'four', 'six', 'seven'],
expectedChannelIds: ['one', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six', 'seven'],
},
{
inChannelIds: ['one', 'three', 'four', 'seven', 'six'],
expectedChannelIds: ['one', 'twoDeleted', 'three', 'four', 'seven', 'fiveDeleted', 'six'],
},
{
inChannelIds: ['one', 'three', 'seven', 'four', 'six'],
expectedChannelIds: ['one', 'twoDeleted', 'three', 'seven', 'four', 'fiveDeleted', 'six'],
},
{
inChannelIds: ['one', 'seven', 'three', 'four', 'six'],
expectedChannelIds: ['one', 'seven', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six'],
},
{
inChannelIds: ['seven', 'one', 'three', 'four', 'six'],
expectedChannelIds: ['seven', 'one', 'twoDeleted', 'three', 'four', 'fiveDeleted', 'six'],
},
];
for (let i = 0; i < testCases.length; i++) {
const testCase = testCases[i];
test('at ' + i, async () => {
const store = configureStore(initialState);
const targetIndex = testCase.inChannelIds.indexOf('seven');
const newIndex = Actions.adjustTargetIndexForMove(store.getState(), 'category1', 'seven', targetIndex);
const actualChannelIds = insertWithoutDuplicates(channelIds, 'seven', newIndex);
expect(actualChannelIds).toEqual(testCase.expectedChannelIds);
});
}
});
});
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {createCategory as createCategoryRedux} from 'mattermost-redux/actions/channel_categories';
import {DispatchFunc} from 'mattermost-redux/types/actions';
import {createCategory as createCategoryRedux, moveChannelToCategory} from 'mattermost-redux/actions/channel_categories';
import {getCategory, makeGetChannelsForCategory} from 'mattermost-redux/selectors/entities/channel_categories';
import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
import {GlobalState} from 'mattermost-redux/types/store';
import {insertWithoutDuplicates} from 'mattermost-redux/utils/array_utils';
import {setItem} from 'actions/storage';
import {DraggingState} from 'types/store';
......@@ -52,3 +55,48 @@ export function createCategory(teamId: string, displayName: string, channelIds?:
});
};
}
// moveChannelInSidebar moves a channel to a given category in the sidebar, but it accounts for when the target index
// may have changed due to archived channels not being shown in the sidebar.
export function moveChannelInSidebar(categoryId: string, channelId: string, targetIndex: number) {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
const newIndex = adjustTargetIndexForMove(getState(), categoryId, channelId, targetIndex);
return dispatch(moveChannelToCategory(categoryId, channelId, newIndex));
};
}
export function adjustTargetIndexForMove(state: GlobalState, categoryId: string, channelId: string, targetIndex: number) {
if (targetIndex === 0) {
// The channel is being placed first, so there's nothing above that could affect the index
return 0;
}
const category = getCategory(state, categoryId);
const filteredChannels = makeGetChannelsForCategory()(state, category);
if (category.channel_ids.length === filteredChannels.length) {
// There are no archived channels in the category, so the targetIndex from react-beautiful-dnd will be correct
return targetIndex;
}
const filteredChannelIds = filteredChannels.map((channel) => channel.id);
const updatedChannelIds = insertWithoutDuplicates(filteredChannelIds, channelId, targetIndex);
// After "moving" the channel in the sidebar, find what channel comes above it
const previousChannelId = updatedChannelIds[updatedChannelIds.indexOf(channelId) - 1];
// We want the channel to still be below that channel, so place the new index below it
let newIndex = category.channel_ids.indexOf(previousChannelId) + 1;
// If the channel is moving downwards, then the target index will need to be reduced by one to account for
// the channel being removed. For example, if we're moving channelA from [channelA, channelB, channelC] to
// [channelB, channelA, channelC], newIndex would currently be 2 (which comes after channelB), but we need
// it to be 1 (which comes after channelB once channelA is removed).
const sourceIndex = category.channel_ids.indexOf(channelId);
if (sourceIndex !== -1 && sourceIndex < newIndex) {
newIndex -= 1;
}
return newIndex;
}
......@@ -4,14 +4,19 @@
import {connect} from 'react-redux';
import {bindActionCreators, Dispatch} from 'redux';
import {moveChannelToCategory, moveCategory} from 'mattermost-redux/actions/channel_categories';
import {moveCategory} from 'mattermost-redux/actions/channel_categories';
import {getSortedUnreadChannelIds, getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {makeGetCategoriesForTeam} from 'mattermost-redux/selectors/entities/channel_categories';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {GenericAction} from 'mattermost-redux/types/actions';
import {switchToChannelById} from 'actions/views/channel';
import {setDraggingState, stopDragging, expandCategory} from 'actions/views/channel_sidebar';
import {
expandCategory,
moveChannelInSidebar,
setDraggingState,
stopDragging,
} from 'actions/views/channel_sidebar';
import {close} from 'actions/views/lhs';
import {isUnreadFilterEnabled, makeGetCurrentlyDisplayedChannelsForTeam, getDraggingState, makeGetCollapsedStateForAllCategoriesByTeam} from 'selectors/views/channel_sidebar';
import {GlobalState} from 'types/store';
......@@ -46,7 +51,7 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
actions: bindActionCreators({
close,
switchToChannelById,
moveChannelToCategory,
moveChannelInSidebar,
moveCategory,
setDraggingState,
stopDragging,
......
......@@ -94,7 +94,7 @@ describe('components/sidebar/sidebar_category_list', () => {
actions: {
switchToChannelById: jest.fn(),
close: jest.fn(),
moveChannelToCategory: jest.fn(),
moveChannelInSidebar: jest.fn(),
moveCategory: jest.fn(),
removeFromCategory: jest.fn(),
setDraggingState: jest.fn(),
......@@ -302,6 +302,6 @@ describe('components/sidebar/sidebar_category_list', () => {
};
wrapper.instance().onDragEnd(channelResult);
expect(baseProps.actions.moveChannelToCategory).toHaveBeenCalledWith(channelResult.destination!.droppableId, channelResult.draggableId, channelResult.destination!.index);
expect(baseProps.actions.moveChannelInSidebar).toHaveBeenCalledWith(channelResult.destination!.droppableId, channelResult.draggableId, channelResult.destination!.index);
});
});
......@@ -63,7 +63,7 @@ type Props = {
onDragEnd: (result: DropResult) => void;
actions: {
moveChannelToCategory: (categoryId: string, channelId: string, newIndex: number) => void;
moveChannelInSidebar: (categoryId: string, channelId: string, newIndex: number) => void;
moveCategory: (teamId: string, categoryId: string, newIndex: number) => void;
switchToChannelById: (channelId: string) => void;
close: () => void;
......@@ -397,7 +397,7 @@ export default class SidebarCategoryList extends React.PureComponent<Props, Stat
if (result.reason === 'DROP' && result.destination) {
if (result.type === 'SIDEBAR_CHANNEL') {
this.props.actions.moveChannelToCategory(result.destination.droppableId, result.draggableId, result.destination.index);
this.props.actions.moveChannelInSidebar(result.destination.droppableId, result.draggableId, result.destination.index);
trackEvent('ui', 'ui_sidebar_dragdrop_dropped_channel');
} else if (result.type === 'SIDEBAR_CATEGORY') {
this.props.actions.moveCategory(this.props.currentTeam.id, result.draggableId, result.destination.index);
......
......@@ -212,5 +212,5 @@ export default function configureStore(initialState) {
detectNetwork: detect,
};
return configureServiceStore({}, appReducer, offlineOptions, getAppReducer, {enableBuffer: false});
return configureServiceStore(initialState, appReducer, offlineOptions, getAppReducer, {enableBuffer: false});
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment