[MM-51680] : Revert New Browse Channels UI modal changes (https://github.com/mattermost/mattermost-webapp/pull/11262) (#22613)

* Revert https://github.com/mattermost/mattermost-webapp/pull/11262

* fix missing translation

* fix indentation issue

* fix imports in Update accessibility_modals_dialogs_spec.js

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Ashish Dhama
2023-03-28 06:17:42 +05:30
committed by GitHub
parent d0babfd254
commit dc4e7bf2ec
24 changed files with 631 additions and 1173 deletions

View File

@@ -97,7 +97,7 @@ describe('Leave an archived channel', () => {
// # More channels modal opens
cy.get('#moreChannelsModal').should('be.visible').within(() => {
// # Click on dropdown
cy.findByText('Channel Type: Public').should('be.visible').click();
cy.findByText('Show: Public Channels').should('be.visible').click();
// # Click archived channels
cy.findByText('Archived Channels').click();
@@ -143,9 +143,9 @@ describe('Leave an archived channel', () => {
cy.get('#showMoreChannels').click();
// # More channels modal opens
cy.get('#moreChannelsModal').should('be.visible').within(() => {
cy.get('.more-modal').should('be.visible').within(() => {
// # Public channel list opens by default
cy.findByText('Channel Type: Public').should('be.visible').click();
cy.findByText('Show: Public Channels').should('be.visible').click();
// # Click on archived channels
cy.findByText('Archived Channels').click();
@@ -196,9 +196,9 @@ describe('Leave an archived channel', () => {
cy.get('#showMoreChannels').click();
// # More channels modal opens
cy.get('#moreChannelsModal').should('be.visible').within(() => {
cy.get('.more-modal').should('be.visible').within(() => {
// # Public channels are shown by default
cy.findByText('Channel Type: Public').should('be.visible').click();
cy.findByText('Show: Public Channels').should('be.visible').click();
// # Go to archived channels
cy.findByText('Archived Channels').click();
@@ -250,9 +250,9 @@ describe('Leave an archived channel', () => {
cy.get('#showMoreChannels').click();
// # More channels modal opens
cy.get('#moreChannelsModal').should('be.visible').within(() => {
cy.get('.more-modal').should('be.visible').within(() => {
// # Show public channels is visible by default
cy.findByText('Channel Type: Public').should('be.visible').click();
cy.findByText('Show: Public Channels').should('be.visible').click();
// # Go to archived channels
cy.findByText('Archived Channels').click();
@@ -286,7 +286,7 @@ describe('Leave an archived channel', () => {
// # More channels modal opens and lands on public channels
cy.get('#moreChannelsModal').should('be.visible').within(() => {
cy.findByText('Channel Type: Public').should('be.visible').click();
cy.findByText('Show: Public Channels').should('be.visible').click();
// # Go to archived channels
cy.findByText('Archived Channels').click();

View File

@@ -65,7 +65,7 @@ describe('Channels', () => {
cy.get('#moreChannelsModal').should('be.visible').within(() => {
// * Dropdown should be visible, defaulting to "Public Channels"
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').wait(TIMEOUTS.HALF_SEC);
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').wait(TIMEOUTS.HALF_SEC);
cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC);
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => {
@@ -80,8 +80,8 @@ describe('Channels', () => {
});
});
// # Verify that the modal is not closed
cy.get('#moreChannelsModal').should('exist');
// # Verify that the modal is closed and it's redirected to the selected channel
cy.get('#moreChannelsModal').should('not.exist');
cy.url().should('include', `/${testTeam.name}/channels/${testChannel.name}`);
// # Login as channel admin and go directly to the channel
@@ -113,7 +113,7 @@ describe('Channels', () => {
cy.findByText('Archived Channels').should('be.visible').click();
// * Channel test should be visible as an archived channel in the list
cy.wrap(el).should('contain', 'Channel Type: Archived');
cy.wrap(el).should('contain', 'Show: Archived Channels');
});
cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC);
@@ -196,7 +196,7 @@ describe('Channels', () => {
// * Dropdown should be visible, defaulting to "Public Channels"
cy.get('#channelsMoreDropdown').should('be.visible').within((el) => {
cy.wrap(el).should('contain', 'Channel Type: Public');
cy.wrap(el).should('contain', 'Show: Public Channels');
});
// * Users should be able to type and search
@@ -207,12 +207,12 @@ describe('Channels', () => {
cy.get('#moreChannelsModal').should('be.visible').within(() => {
// * Users should be able to switch to "Archived Channels" list
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').click().within((el) => {
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').click().within((el) => {
// # Click on archived channels item
cy.findByText('Archived Channels').should('be.visible').click();
// * Modal should show the archived channels list
cy.wrap(el).should('contain', 'Channel Type: Archived');
cy.wrap(el).should('contain', 'Show: Archived Channels');
}).wait(TIMEOUTS.HALF_SEC);
cy.get('#searchChannelsTextbox').clear();
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2);
@@ -250,7 +250,7 @@ function verifyMoreChannelsModal(isEnabled) {
// * Verify that the more channels modal is open and with or without option to view archived channels
cy.get('#moreChannelsModal').should('be.visible').within(() => {
if (isEnabled) {
cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Channel Type: Public');
cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Show: Public Channels');
} else {
cy.get('#channelsMoreDropdown').should('not.exist');
}

View File

@@ -11,7 +11,8 @@
// Group: @channels @channel
function verifyNoChannelToJoinMessage(isVisible) {
cy.findByText('No public channels').should(isVisible ? 'be.visible' : 'not.exist');
cy.findByText('No more channels to join').should(isVisible ? 'be.visible' : 'not.exist');
cy.findByText('Click \'Create New Channel\' to make a new one').should(isVisible ? 'be.visible' : 'not.exist');
}
describe('more public channels', () => {
@@ -52,10 +53,7 @@ describe('more public channels', () => {
cy.uiBrowseOrCreateChannel('Browse Channels').click();
// * Assert that the moreChannelsModel is visible
cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => {
// # Click hide joined checkbox
cy.findByText('Hide Joined').should('be.visible').click();
cy.findByRole('dialog', {name: 'More Channels'}).should('be.visible').within(() => {
// * Assert that the moreChannelsList is visible and the number of channels is 31
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 31);
@@ -88,9 +86,9 @@ describe('more public channels', () => {
cy.uiBrowseOrCreateChannel('Browse Channels').click();
// * Assert the moreChannelsModel is visible
cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => {
// # Click hide joined checkbox
cy.findByText('Hide Joined').should('be.visible').click();
cy.findByRole('dialog', {name: 'More Channels'}).should('be.visible').within(() => {
// * Assert the moreChannelsList does have one child
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1);
// * Assert that the "No more channels to join" message is visible
verifyNoChannelToJoinMessage(true);

View File

@@ -68,13 +68,13 @@ describe('Channel sidebar', () => {
cy.get('.AddChannelDropdown .MenuItem:contains(Browse Channels) button').should('be.visible').click();
// * Verify that the more channels modal is visible
cy.get('#moreChannelsModal').should('be.visible');
cy.get('.more-modal').should('be.visible');
// Click the Off-Topic channel
cy.findByText('Off-Topic').should('be.visible').click();
cy.get('.more-modal button:contains(Off-Topic)').should('be.visible').click();
// Verify that new channel is in the sidebar and is active
cy.get('#moreChannelsModal').should('exist');
cy.get('.more-modal').should('not.exist');
cy.url().should('include', `/${teamName}/channels/off-topic`);
cy.get('#channelHeaderTitle').should('contain', 'Off-Topic');
cy.get('.SidebarChannel.active:contains(Off-Topic)').should('be.visible');

View File

@@ -104,26 +104,30 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
cy.uiBrowseOrCreateChannel('Browse Channels').click();
// * Verify the accessibility support in More Channels Dialog
cy.findByRole('dialog', {name: 'Browse Channels'}).within(() => {
cy.findByRole('heading', {name: 'Browse Channels'});
cy.findByRole('dialog', {name: 'More Channels'}).within(() => {
cy.findByRole('heading', {name: 'More Channels'});
// * Verify the accessibility support in search input
cy.findByPlaceholderText('Search channels');
cy.get('#moreChannelsList').should('be.visible').then((el) => {
cy.waitUntil(() => cy.get('#moreChannelsList').then((el) => {
return el[0].children.length === 2;
});
}));
// # Hide already joined channels
cy.findByText('Hide Joined').click();
// # Focus on the Create Channel button and TAB three time
cy.get('#createNewChannelButton').focus().tab().tab().tab();
// # Focus on the Create Channel button and TAB twice
cy.get('#createNewChannel').focus().tab().tab();
// * Verify channel name is highlighted and reader reads the channel name and channel description
cy.get('#moreChannelsList').within(() => {
cy.get('#moreChannelsList').children().eq(0).within(() => {
const selectedChannel = getChannelAriaLabel(channel);
cy.findByLabelText(selectedChannel).should('be.visible').should('be.focused');
cy.findByLabelText(selectedChannel).should('be.focused');
// * Press Tab and verify if focus changes to Join button
cy.focused().tab();
cy.findByText('Join').parent().should('be.focused');
// * Verify previous button should no longer be focused
cy.findByLabelText(selectedChannel).should('not.be.focused');
});
// * Press Tab again and verify if focus changes to next row

View File

@@ -234,7 +234,7 @@ context('ldap', () => {
// * Search private channel name and make sure it isn't there in public channel directory
cy.get('#searchChannelsTextbox').type(testChannel.display_name);
cy.get('#moreChannelsList').should('include.text', 'No results for');
cy.get('#moreChannelsList').should('include.text', 'No more channels to join');
});
it('MM-T2629 - Private to public - More....', () => {
@@ -473,7 +473,7 @@ context('ldap', () => {
// * Search private channel name and make sure it isn't there in public channel directory
cy.get('#searchChannelsTextbox').type(publicChannel.display_name);
cy.get('#moreChannelsList').should('include.text', 'No results for');
cy.get('#moreChannelsList').should('include.text', 'No more channels to join');
});
});

View File

@@ -167,7 +167,7 @@ describe('Actions.Channel', () => {
}],
}];
await testStore.dispatch(searchMoreChannels('', false, true));
await testStore.dispatch(searchMoreChannels('', false));
expect(testStore.getActions()).toEqual(expectedActions);
});

View File

@@ -109,7 +109,7 @@ export function loadChannelsForCurrentUser(): ActionFunc {
};
}
export function searchMoreChannels(term: string, showArchivedChannels: boolean, hideJoinedChannels: boolean): ActionFunc<Channel[], ServerError> {
export function searchMoreChannels(term: string, showArchivedChannels: boolean): ActionFunc<Channel[], ServerError> {
return async (dispatch, getState) => {
const state = getState();
const teamId = getCurrentTeamId(state);
@@ -121,7 +121,9 @@ export function searchMoreChannels(term: string, showArchivedChannels: boolean,
const {data, error} = await dispatch(ChannelActions.searchChannels(teamId, term, showArchivedChannels));
if (data) {
const myMembers = getMyChannelMemberships(state);
const channels = hideJoinedChannels ? (data as Channel[]).filter((channel) => !myMembers[channel.id]) : data;
// When searching public channels, only get channels user is not a member of
const channels = showArchivedChannels ? data : (data as Channel[]).filter((c) => !myMembers[c.id]);
return {data: channels};
}

View File

@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/SearchableChannelList should match init snapshot 1`] = `
<div
className="filtered-user-list"
>
<div
className="filter-row filter-row--full"
>
<div
className="col-sm-12"
>
<QuickInput
className="form-control filter-textbox"
id="searchChannelsTextbox"
inputComponent={
Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
}
}
onInput={[Function]}
placeholder={
Object {
"defaultMessage": "Search channels",
"id": "filtered_channels_list.search",
}
}
/>
</div>
</div>
<div
className="more-modal__list"
role="application"
>
<div
id="moreChannelsList"
>
<LoadingScreen />
</div>
</div>
<div
className="filter-controls"
/>
</div>
`;

View File

@@ -1,39 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {SVGProps} from 'react';
const SvgComponent = (props: SVGProps<SVGSVGElement>) => (
<svg
width={140}
height={141}
fill='none'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path
opacity={0.4}
d='M37.593 38.008c4.754-4.815 10.754-7.295 17.989-7.428 7.101.133 13.065 2.601 17.892 7.428 4.815 4.827 7.295 10.791 7.428 17.892-.133 7.235-2.601 13.223-7.428 17.99-4.827 4.754-10.791 7.27-17.892 7.512-7.235-.254-13.223-2.758-17.99-7.513-4.754-4.766-7.258-10.766-7.512-18 .254-7.102 2.758-13.066 7.513-17.881Z'
fill='#fff'
/>
<path
d='M78.887 51.382c-2.151-6.992-6.225-12.225-12.226-15.69-6.001-3.465-12.57-4.376-19.701-2.743-3.9.995-7.297 2.717-10.22 5.162 3.269-3.567 7.415-6.037 12.428-7.416 7.13-1.633 13.732-.703 19.787 2.793s10.161 8.748 12.313 15.74c1.323 5.037 1.257 9.862-.21 14.47-1.454 4.614-4.066 8.49-7.84 11.611 2.833-3.087 4.783-6.713 5.844-10.894 1.05-4.187.991-8.522-.175-13.033Z'
fill='#000'
fillOpacity={0.4}
/>
<path
d='M86.76 53.929c-.508-7.506-3.553-14.097-9.125-19.774-6.346-6.05-13.67-9.08-21.974-9.08-8.303 0-15.616 3.03-21.961 9.08-6.08 6.315-9.126 13.591-9.126 21.855 0 8.262 3.046 15.551 9.126 21.854 5.826 5.556 12.485 8.551 19.967 8.984 7.481.445 14.383-1.611 20.728-6.146l4.75 4.727 6.08-6.05-4.75-4.727c4.69-6.302 6.78-13.218 6.285-20.723Zm-13.126 19.87c-4.823 4.726-10.781 7.228-17.876 7.468-7.228-.252-13.21-2.742-17.973-7.469-4.75-4.727-7.252-10.692-7.506-17.885.254-7.06 2.756-12.99 7.506-17.789 4.75-4.787 10.745-7.252 17.973-7.385 7.095.133 13.053 2.586 17.876 7.385 4.81 4.8 7.288 10.73 7.421 17.79-.133 7.192-2.599 13.157-7.421 17.884Z'
fill='#BABEC9'
/>
<path
d='M106.202 114.187c-1.567.449-2.728.291-3.482-.472L78.06 86.651c-.753-.762-1.064-1.743-.945-2.954.12-1.211.874-2.567 2.262-4.093 1.507-1.393 2.847-2.192 4.044-2.385 1.196-.194 2.165.157 2.92 1.053l26.921 24.957c.753.763.873 1.901.37 3.427-.502 1.525-1.447 3.051-2.823 4.577-1.496 1.526-3.039 2.506-4.607 2.954Z'
fill='#FFBC1F'
/>
<path
d='m108.007 98.343-10.08 10.164-12.154-13.34 8.914-9.106 13.32 12.282Z'
fill='#7A5600'
/>
</svg>
);
export default SvgComponent;

View File

@@ -133,7 +133,6 @@
}
.GenericModal__header {
width: 85%;
padding: 0;
border-top-left-radius: 12px;
border-top-right-radius: 12px;

View File

@@ -1,15 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/MoreChannels should match snapshot and state 1`] = `
<GenericModal
<Modal
animation={true}
aria-labelledby="moreChannelsModalLabel"
aria-modal={true}
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={false}
compassDesign={true}
enforceFocus={false}
headerButton={
<Memo(Connect(TeamPermissionGate))
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal more-modal more-modal--action"
dialogComponentClass={[Function]}
enforceFocus={true}
id="moreChannelsModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="dialog"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
id="moreChannelsModalHeader"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="moreChannelsModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="More Channels"
id="more_channels.title"
/>
</ModalTitle>
<Connect(TeamPermissionGate)
permissions={
Array [
"create_public_channel",
@@ -18,30 +56,74 @@ exports[`components/MoreChannels should match snapshot and state 1`] = `
teamId="team_id"
>
<button
aria-label="Create New Channel"
className="btn outlineButton"
id="createNewChannelButton"
className="btn btn-primary channel-create-btn"
id="createNewChannel"
onClick={[Function]}
type="button"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Create New Channel"
<MemoizedFormattedMessage
defaultMessage="Create Channel"
id="more_channels.create"
/>
</button>
</Memo(Connect(TeamPermissionGate))>
}
id="moreChannelsModal"
keyboardEscape={true}
modalHeaderText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Browse Channels"
id="more_channels.title"
</Connect(TeamPermissionGate)>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<SearchableChannelList
canShowArchivedChannels={true}
channels={
Array [
Object {
"create_at": 0,
"creator_id": "id",
"delete_at": 0,
"display_name": "name",
"group_constrained": false,
"header": "header",
"id": "channel_id",
"last_post_at": 0,
"last_root_post_at": 0,
"name": "DN",
"purpose": "purpose",
"scheme_id": "id",
"team_id": "team_id",
"type": "O",
"update_at": 0,
},
]
}
channelsPerPage={50}
handleJoin={[Function]}
isSearch={false}
loading={false}
nextPage={[Function]}
noResultsText={
<Memo(Connect(TeamPermissionGate))
permissions={
Array [
"create_public_channel",
"create_private_channel",
]
}
teamId="team_id"
>
<p
className="secondary-message"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Click 'Create New Channel' to make a new one"
id="more_channels.createClick"
/>
</p>
</Memo(Connect(TeamPermissionGate))>
}
search={[Function]}
shouldShowArchivedChannels={false}
toggleArchivedChannels={[Function]}
/>
}
onExited={[Function]}
show={true}
>
<LoadingScreen />
</GenericModal>
</ModalBody>
</Modal>
`;

View File

@@ -1,82 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/SearchableChannelList should match init snapshot 1`] = `
<div
className="filtered-user-list"
>
<div
className="filter-row filter-row--full"
>
<span
aria-hidden="true"
id="searchIcon"
>
<MagnifyIcon
size={18}
/>
</span>
<QuickInput
aria-label="Search Channels"
className="form-control filter-textbox"
clearable={true}
id="searchChannelsTextbox"
inputComponent={
Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
}
}
onClear={[Function]}
onInput={[Function]}
placeholder={
Object {
"defaultMessage": "Search channels",
"id": "filtered_channels_list.search",
}
}
value=""
/>
</div>
<div
className="more-modal__dropdown"
>
<span
id="channelCountLabel"
>
0 Results
</span>
<div
id="modalPreferenceContainer"
>
<div
id="hideJoinedPreferenceCheckbox"
onClick={[Function]}
>
<button
aria-label="Hide joined channels checkbox, not checked"
className="get-app__checkbox"
/>
<MemoizedFormattedMessage
defaultMessage="Hide Joined"
id="more_channels.hide_joined"
/>
</div>
</div>
</div>
<div
className="more-modal__list"
role="application"
tabIndex={-1}
>
<div
id="moreChannelsList"
tabIndex={-1}
>
<LoadingScreen />
</div>
</div>
<div
className="filter-controls"
/>
</div>
`;

View File

@@ -12,29 +12,24 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {Action, ActionResult} from 'mattermost-redux/types/actions';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getChannels, getArchivedChannels, joinChannel, getChannelStats} from 'mattermost-redux/actions/channels';
import {getChannelsInCurrentTeam, getMyChannelMemberships, getAllChannelStats} from 'mattermost-redux/selectors/entities/channels';
import {Constants, StoragePrefixes} from 'utils/constants';
import {getChannels, getArchivedChannels, joinChannel} from 'mattermost-redux/actions/channels';
import {getOtherChannels, getChannelsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {searchMoreChannels} from 'actions/channel_actions';
import {openModal, closeModal} from 'actions/views/modals';
import {setGlobalItem} from 'actions/storage';
import {closeRightHandSide} from 'actions/views/rhs';
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
import {GlobalState} from 'types/store';
import {ModalData} from 'types/actions';
import {makeGetGlobalItem} from 'selectors/storage';
import {GlobalState} from 'types/store';
import MoreChannels from './more_channels';
const getChannelsWithoutArchived = createSelector(
'getChannelsWithoutArchived',
getChannelsInCurrentTeam,
(channels: Channel[]) => channels && channels.filter((c) => c.delete_at === 0 && c.type !== Constants.PRIVATE_CHANNEL),
const getNotArchivedOtherChannels = createSelector(
'getNotArchivedOtherChannels',
getOtherChannels,
(channels: Channel[]) => channels && channels.filter((c) => c.delete_at === 0),
);
const getArchivedOtherChannels = createSelector(
@@ -45,19 +40,15 @@ const getArchivedOtherChannels = createSelector(
function mapStateToProps(state: GlobalState) {
const team = getCurrentTeam(state) || {};
const getGlobalItem = makeGetGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, 'false');
return {
channels: getChannelsWithoutArchived(state) || [],
channels: getNotArchivedOtherChannels(state) || [],
archivedChannels: getArchivedOtherChannels(state) || [],
currentUserId: getCurrentUserId(state),
teamId: team.id,
teamName: team.name,
channelsRequestStarted: state.requests.channels.getChannels.status === RequestStatus.STARTED,
canShowArchivedChannels: (getConfig(state).ExperimentalViewArchivedChannels === 'true'),
myChannelMemberships: getMyChannelMemberships(state) || {},
allChannelStats: getAllChannelStats(state) || {},
shouldHideJoinedChannels: getGlobalItem(state) === 'true',
rhsState: getRhsState(state),
rhsOpen: getIsRhsOpen(state),
};
@@ -70,8 +61,6 @@ type Actions = {
searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean) => Promise<ActionResult>;
openModal: <P>(modalData: ModalData<P>) => void;
closeModal: (modalId: string) => void;
getChannelStats: (channelId: string) => void;
setGlobalItem: (name: string, value: string) => void;
closeRightHandSide: () => void;
}
@@ -84,8 +73,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
searchMoreChannels,
openModal,
closeModal,
getChannelStats,
setGlobalItem,
closeRightHandSide,
}, dispatch),
};

View File

@@ -1,295 +0,0 @@
@charset 'UTF-8';
#moreChannelsModal {
.modal-content {
min-height: 600px;
max-height: calc(50vh - 240px);
}
.modal-dialog {
margin-top: calc(45vh - 240px) !important;
}
.filter-row--full {
position: relative;
margin: 0 32px;
.input-clear {
top: 16px;
right: 16px;
}
#searchIcon {
position: absolute;
top: 14px;
left: 16px;
color: rgba(var(--center-channel-color-rgb), 0.64);
pointer-events: none;
}
#searchChannelsTextbox {
height: 48px;
padding-left: 40px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
box-shadow: none;
font-size: 16px;
&::placeholder {
color: var(--center-channel-color);
}
&:focus {
border: 2px solid var(--button-bg);
}
}
}
.more-modal__dropdown {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 32px;
border-bottom: solid 1px rgba(var(--center-channel-color-rgb), 0.16);
margin: 0;
span {
color: rgba(var(--center-channel-color-rgb), 0.64);
font-size: 12px;
line-height: 16px;
}
.MenuItem__primary-text {
width: 100%;
color: var(--center-channel-color);
font-size: 14px;
font-weight: 400;
line-height: 20px;
svg {
margin-left: auto;
}
}
.Menu__content {
border-color: rgba(var(--center-channel-color-rgb), 0.16);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
#channelCountLabel {
color: var(--center-channel-color);
font-size: 12px;
font-weight: 400;
}
#modalPreferenceContainer {
display: flex;
align-items: center;
justify-content: center;
.get-app__checkbox {
display: flex;
width: 16px;
height: 16px;
align-items: center;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
}
#hideJoinedPreferenceCheckbox {
display: flex;
align-items: center;
cursor: pointer;
}
#channelsMoreDropdown {
margin: 0 8px;
}
#menuWrapper {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 6px 4px 8px;
}
.MenuWrapper:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
}
.MenuWrapper--open {
background-color: rgba(var(--button-bg-rgb), 0.12);
border-radius: 4px;
&:hover {
background-color: rgba(var(--button-bg-rgb), 0.12);
}
}
}
}
.modal-body {
padding: 15px 0 0;
.filtered-user-list {
height: 500px;
}
.more-modal__row {
padding: 0 32px;
border-bottom: none;
.more-modal__details {
padding-left: 0;
color: rgba(var(--center-channel-color-rgb), 0.56);
svg {
flex-shrink: 0;
}
.more-modal__name {
align-items: center;
margin-top: 0;
span {
color: var(--center-channel-color);
font-weight: 500;
}
}
#channelPurposeContainer {
display: flex;
align-items: center;
justify-content: flex-start;
.dot {
width: 3px;
height: 3px;
flex-shrink: 0;
background-color: rgba(var(--center-channel-color-rgb), 0.56);
border-radius: 50%;
}
.more-modal__description {
margin-left: 4px;
font-weight: 400;
}
#membershipIndicatorContainer {
display: flex;
align-items: center;
span,
svg {
color: var(--online-indicator);
}
}
span {
margin: 0 4px;
font-size: 12px;
font-weight: 600;
line-height: 12px;
opacity: 1;
}
}
}
.more-modal__actions {
button {
display: none;
min-width: 54px;
height: 32px;
font-size: 12px;
font-weight: 600;
}
}
}
.more-modal__row:hover,
.more-modal__row:focus {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
cursor: pointer;
.more-modal__actions {
.primaryButton,
.outlineButton {
display: inline-block;
}
}
}
.form-group {
padding: 0 32px;
margin-bottom: 0;
}
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: none;
}
}
.modal-header {
.GenericModal__header {
display: flex;
width: 95%;
align-items: center;
justify-content: space-between;
padding-right: 4px;
}
.close {
top: 22px;
}
}
.outlineButton {
border: 1px solid var(--button-bg);
background: none;
border-radius: 4px;
color: var(--button-bg);
font-size: 12px;
font-weight: 600;
line-height: 16px;
}
.outlineButton:hover {
background-color: rgba(var(--button-bg-rgb), 0.08);
}
.filter-controls {
padding: 0;
button {
min-width: 72px;
margin: 8px 32px;
}
}
}
#moreChannelsList {
.primary-message {
margin-top: 8px;
color: var(--center-channel-color);
line-height: 28px;
}
.secondary-message {
margin-bottom: 30px;
}
.primaryButton {
background-color: var(--button-bg);
border-radius: 4px;
color: var(--button-color);
font-size: 14px;
font-weight: 600;
}
#createNewChannelButton {
padding: 10px 20px;
}
}

View File

@@ -7,7 +7,7 @@ import {shallow} from 'enzyme';
import {ActionResult} from 'mattermost-redux/types/actions';
import MoreChannels, {Props} from 'components/more_channels/more_channels';
import SearchableChannelList from 'components/more_channels/searchable_channel_list.jsx';
import SearchableChannelList from 'components/searchable_channel_list.jsx';
import {getHistory} from 'utils/browser_history';
import {TestHelper} from 'utils/test_helper';
@@ -59,16 +59,7 @@ describe('components/MoreChannels', () => {
};
const baseProps: Props = {
channels: [
TestHelper.getChannelMock({
id: 'channel-1',
name: 'channel-1',
}),
TestHelper.getChannelMock({
id: 'channel-2',
name: 'channel-2',
}),
],
channels: [TestHelper.getChannelMock({})],
archivedChannels: [TestHelper.getChannelMock({
id: 'channel_id_2',
team_id: 'channel_team_2',
@@ -82,14 +73,6 @@ describe('components/MoreChannels', () => {
teamName: 'team_name',
channelsRequestStarted: false,
canShowArchivedChannels: true,
myChannelMemberships: {
'channel-2': TestHelper.getChannelMembershipMock({
channel_id: 'channel-2',
user_id: 'user-1',
}),
},
allChannelStats: {},
shouldHideJoinedChannels: false,
actions: {
getChannels: jest.fn(),
getArchivedChannels: jest.fn(),
@@ -97,8 +80,6 @@ describe('components/MoreChannels', () => {
searchMoreChannels: jest.fn(channelActions.searchMoreChannels),
openModal: jest.fn(),
closeModal: jest.fn(),
getChannelStats: jest.fn(),
setGlobalItem: jest.fn(),
closeRightHandSide: jest.fn(),
},
};
@@ -110,6 +91,7 @@ describe('components/MoreChannels', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.state('searchedChannels')).toEqual([]);
expect(wrapper.state('show')).toEqual(true);
expect(wrapper.state('shouldShowArchivedChannels')).toEqual(false);
expect(wrapper.state('search')).toEqual(false);
expect(wrapper.state('serverError')).toBeNull();
@@ -120,6 +102,16 @@ describe('components/MoreChannels', () => {
expect(wrapper.instance().props.actions.getChannels).toHaveBeenCalledWith(wrapper.instance().props.teamId, 0, 100);
});
test('should match state on handleHide', () => {
const wrapper = shallow<MoreChannels>(
<MoreChannels {...baseProps}/>,
);
wrapper.setState({show: true});
wrapper.instance().handleHide();
expect(wrapper.state('show')).toEqual(false);
});
test('should call closeModal on handleExit', () => {
const wrapper = shallow<MoreChannels>(
<MoreChannels {...baseProps}/>,
@@ -160,7 +152,7 @@ describe('components/MoreChannels', () => {
<MoreChannels {...baseProps}/>,
);
wrapper.setState({loading: false, search: true, searching: true});
wrapper.setState({search: true, searching: true});
const searchList = wrapper.find(SearchableChannelList);
expect(searchList.props().loading).toEqual(true);
});
@@ -219,6 +211,7 @@ describe('components/MoreChannels', () => {
process.nextTick(() => {
expect(getHistory().push).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledTimes(1);
expect(wrapper.state('show')).toEqual(false);
done();
});
});
@@ -256,7 +249,7 @@ describe('components/MoreChannels', () => {
jest.runOnlyPendingTimers();
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1);
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('fail', false, false);
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('fail', false);
process.nextTick(() => {
expect(wrapper.state('search')).toEqual(true);
expect(wrapper.state('searching')).toEqual(false);
@@ -283,7 +276,7 @@ describe('components/MoreChannels', () => {
jest.runOnlyPendingTimers();
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1);
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', false, false);
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', false);
process.nextTick(() => {
expect(wrapper.state('search')).toEqual(true);
expect(wrapper.state('searching')).toEqual(false);
@@ -310,7 +303,7 @@ describe('components/MoreChannels', () => {
jest.runOnlyPendingTimers();
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledTimes(1);
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', true, false);
expect(wrapper.instance().props.actions.searchMoreChannels).toHaveBeenCalledWith('channel', true);
process.nextTick(() => {
expect(wrapper.state('search')).toEqual(true);
expect(wrapper.state('searching')).toEqual(false);
@@ -318,16 +311,4 @@ describe('components/MoreChannels', () => {
done();
});
});
test('should hide joined channels from channels props when shouldHideJoinedChannels prop is true', () => {
const props = {
...baseProps,
shouldHideJoinedChannels: true,
};
const wrapper = shallow<MoreChannels>(
<MoreChannels {...props}/>,
);
expect(wrapper.instance().activeChannels).not.toContain(baseProps.channels[1]);
});
});

View File

@@ -2,32 +2,23 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import {ActionResult} from 'mattermost-redux/types/actions';
import {Channel, ChannelMembership, ChannelStats} from '@mattermost/types/channels';
import {Channel} from '@mattermost/types/channels';
import Permissions from 'mattermost-redux/constants/permissions';
import {RelationOneToOne} from '@mattermost/types/utilities';
import NewChannelModal from 'components/new_channel_modal/new_channel_modal';
import SearchableChannelList from 'components/searchable_channel_list.jsx';
import TeamPermissionGate from 'components/permissions_gates/team_permission_gate';
import GenericModal from 'components/generic_modal';
import LoadingScreen from 'components/loading_screen';
import {ModalData} from 'types/actions';
import {RhsState} from 'types/store/rhs';
import {getHistory} from 'utils/browser_history';
import {ModalIdentifiers, StoragePrefixes, RHSStates} from 'utils/constants';
import {ModalIdentifiers, RHSStates} from 'utils/constants';
import {getRelativeChannelURL} from 'utils/url';
import {localizeMessage} from 'utils/utils';
import SearchableChannelList from './searchable_channel_list';
import './more_channels.scss';
const CHANNELS_CHUNK_SIZE = 50;
const CHANNELS_PER_PAGE = 50;
@@ -37,15 +28,9 @@ type Actions = {
getChannels: (teamId: string, page: number, perPage: number) => void;
getArchivedChannels: (teamId: string, page: number, channelsPerPage: number) => void;
joinChannel: (currentUserId: string, teamId: string, channelId: string) => Promise<ActionResult>;
searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean, shouldHideJoinedChannels: boolean) => Promise<ActionResult>;
searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean) => Promise<ActionResult>;
openModal: <P>(modalData: ModalData<P>) => void;
closeModal: (modalId: string) => void;
getChannelStats: (channelId: string) => void;
/*
* Function to set a key-value pair in the local storage
*/
setGlobalItem: (name: string, value: string) => void;
closeRightHandSide: () => void;
}
@@ -58,27 +43,23 @@ export type Props = {
channelsRequestStarted?: boolean;
canShowArchivedChannels?: boolean;
morePublicChannelsModalType?: string;
myChannelMemberships: RelationOneToOne<Channel, ChannelMembership>;
allChannelStats: RelationOneToOne<Channel, ChannelStats>;
shouldHideJoinedChannels: boolean;
rhsState?: RhsState;
rhsOpen?: boolean;
actions: Actions;
}
type State = {
show: boolean;
shouldShowArchivedChannels: boolean;
search: boolean;
searchedChannels: Channel[];
serverError: React.ReactNode | string;
searching: boolean;
searchTerm: string;
loading: boolean;
}
export default class MoreChannels extends React.PureComponent<Props, State> {
public searchTimeoutId: number;
activeChannels: Channel[] = [];
constructor(props: Props) {
super(props);
@@ -86,27 +67,25 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
this.searchTimeoutId = 0;
this.state = {
show: true,
shouldShowArchivedChannels: this.props.morePublicChannelsModalType === 'private',
search: false,
searchedChannels: [],
serverError: null,
searching: false,
searchTerm: '',
loading: true,
};
}
async componentDidMount() {
await this.props.actions.getChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2);
componentDidMount() {
this.props.actions.getChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2);
if (this.props.canShowArchivedChannels) {
await this.props.actions.getArchivedChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2);
this.props.actions.getArchivedChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2);
}
await this.props.channels.forEach((channel) => this.props.actions.getChannelStats(channel.id));
this.loadComplete();
}
loadComplete = () => {
this.setState({loading: false});
handleHide = () => {
this.setState({show: false});
}
handleNewChannel = () => {
@@ -145,17 +124,14 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
handleJoin = async (channel: Channel, done: () => void) => {
const {actions, currentUserId, teamId, teamName} = this.props;
let result;
const result = await actions.joinChannel(currentUserId, teamId, channel.id);
if (!this.isMemberOfChannel(channel.id)) {
result = await actions.joinChannel(currentUserId, teamId, channel.id);
}
if (result?.error) {
if (result.error) {
this.setState({serverError: result.error.message});
} else {
getHistory().push(getRelativeChannelURL(teamName, channel.name));
this.closeEditRHS();
this.handleHide();
}
if (done) {
@@ -177,7 +153,7 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
const searchTimeoutId = window.setTimeout(
async () => {
try {
const {data} = await this.props.actions.searchMoreChannels(term, this.state.shouldShowArchivedChannels, this.props.shouldHideJoinedChannels);
const {data} = await this.props.actions.searchMoreChannels(term, this.state.shouldShowArchivedChannels);
if (searchTimeoutId !== this.searchTimeoutId) {
return;
}
@@ -207,47 +183,29 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
this.setState({shouldShowArchivedChannels});
}
isMemberOfChannel(channelId: string) {
return this.props.myChannelMemberships[channelId];
}
handleShowJoinedChannelsPreference = (shouldHideJoinedChannels: boolean) => {
// search again when switching channels to update search results
this.search(this.state.searchTerm);
this.props.actions.setGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, shouldHideJoinedChannels.toString());
}
otherChannelsWithoutJoined = this.props.channels.filter((channel) => !this.isMemberOfChannel(channel.id));
archivedChannelsWithoutJoined = this.props.archivedChannels.filter((channel) => !this.isMemberOfChannel(channel.id));
render() {
const {
channels,
archivedChannels,
teamId,
channelsRequestStarted,
shouldHideJoinedChannels,
} = this.props;
const {
search,
searchedChannels,
serverError: serverErrorState,
show,
searching,
shouldShowArchivedChannels,
} = this.state;
const otherChannelsWithoutJoined = channels.filter((channel) => !this.isMemberOfChannel(channel.id));
const archivedChannelsWithoutJoined = archivedChannels.filter((channel) => !this.isMemberOfChannel(channel.id));
let activeChannels;
if (shouldShowArchivedChannels && shouldHideJoinedChannels) {
this.activeChannels = search ? searchedChannels : archivedChannelsWithoutJoined;
} else if (shouldShowArchivedChannels && !shouldHideJoinedChannels) {
this.activeChannels = search ? searchedChannels : archivedChannels;
} else if (!shouldShowArchivedChannels && shouldHideJoinedChannels) {
this.activeChannels = search ? searchedChannels : otherChannelsWithoutJoined;
if (shouldShowArchivedChannels) {
activeChannels = search ? searchedChannels : archivedChannels;
} else {
this.activeChannels = search ? searchedChannels : channels;
activeChannels = search ? searchedChannels : channels;
}
let serverError;
@@ -256,87 +214,87 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
<div className='form-group has-error'><label className='control-label'>{serverErrorState}</label></div>;
}
const createNewChannelButton = (className: string, icon?: JSX.Element) => {
const buttonClassName = classNames('btn', className);
return (
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.CREATE_PUBLIC_CHANNEL]}
const createNewChannelButton = (
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.CREATE_PUBLIC_CHANNEL]}
>
<button
id='createNewChannel'
type='button'
className='btn btn-primary channel-create-btn'
onClick={this.handleNewChannel}
>
<button
type='button'
id='createNewChannelButton'
className={buttonClassName}
onClick={this.handleNewChannel}
aria-label={localizeMessage('more_channels.create', 'Create New Channel')}
>
{icon}
<FormattedMessage
id='more_channels.create'
defaultMessage='Create New Channel'
/>
</button>
</TeamPermissionGate>
);
};
const noResultsText = (
<>
<p className='secondary-message'>
<FormattedMessage
id='more_channels.searchError'
defaultMessage='Try searching different keywords, checking for typos or adjusting the filters.'
id='more_channels.create'
defaultMessage='Create Channel'
/>
</p>
{createNewChannelButton('primaryButton', <i className='icon-plus'/>)}
</>
</button>
</TeamPermissionGate>
);
const body = this.state.loading ? <LoadingScreen/> : (
const createChannelHelpText = (
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.CREATE_PUBLIC_CHANNEL, Permissions.CREATE_PRIVATE_CHANNEL]}
>
<p className='secondary-message'>
<FormattedMessage
id='more_channels.createClick'
defaultMessage="Click 'Create New Channel' to make a new one"
/>
</p>
</TeamPermissionGate>
);
const body = (
<React.Fragment>
<SearchableChannelList
channels={this.activeChannels}
channels={activeChannels}
channelsPerPage={CHANNELS_PER_PAGE}
nextPage={this.nextPage}
isSearch={search}
search={this.search}
handleJoin={this.handleJoin}
noResultsText={noResultsText}
noResultsText={createChannelHelpText}
loading={search ? searching : channelsRequestStarted}
toggleArchivedChannels={this.toggleArchivedChannels}
shouldShowArchivedChannels={this.state.shouldShowArchivedChannels}
canShowArchivedChannels={this.props.canShowArchivedChannels}
myChannelMemberships={this.props.myChannelMemberships}
allChannelStats={this.props.allChannelStats}
closeModal={this.props.actions.closeModal}
hideJoinedChannelsPreference={this.handleShowJoinedChannelsPreference}
rememberHideJoinedChannelsChecked={shouldHideJoinedChannels}
/>
{serverError}
</React.Fragment>
);
const title = (
<FormattedMessage
id='more_channels.title'
defaultMessage='Browse Channels'
/>
);
return (
<GenericModal
<Modal
dialogClassName='a11y__modal more-modal more-modal--action'
show={show}
onHide={this.handleHide}
onExited={this.handleExit}
compassDesign={true}
role='dialog'
id='moreChannelsModal'
aria-labelledby='moreChannelsModalLabel'
modalHeaderText={title}
headerButton={createNewChannelButton('outlineButton')}
autoCloseOnConfirmButton={false}
aria-modal={true}
enforceFocus={false}
>
{body}
</GenericModal>
<Modal.Header
id='moreChannelsModalHeader'
closeButton={true}
>
<Modal.Title
componentClass='h1'
id='moreChannelsModalLabel'
>
<FormattedMessage
id='more_channels.title'
defaultMessage='More Channels'
/>
</Modal.Title>
{createNewChannelButton}
</Modal.Header>
<Modal.Body>
{body}
</Modal.Body>
</Modal>
);
}
}

View File

@@ -1,483 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {AccountOutlineIcon, ArchiveOutlineIcon, CheckIcon, ChevronDownIcon, GlobeIcon, LockOutlineIcon, MagnifyIcon} from '@mattermost/compass-icons/components';
import classNames from 'classnames';
import {isPrivateChannel} from 'mattermost-redux/utils/channel_utils';
import LoadingScreen from 'components/loading_screen';
import LoadingWrapper from 'components/widgets/loading/loading_wrapper';
import QuickInput from 'components/quick_input';
import LocalizedInput from 'components/localized_input/localized_input';
import CheckboxCheckedIcon from 'components/widgets/icons/checkbox_checked_icon';
import MagnifyingGlassSVG from 'components/common/svg_images_components/magnifying_glass_svg';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import Menu from 'components/widgets/menu/menu';
import {t} from 'utils/i18n';
import * as UserAgent from 'utils/user_agent';
import Constants, {ModalIdentifiers} from 'utils/constants';
import {isKeyPressed, localizeMessage, localizeAndFormatMessage} from 'utils/utils';
import {isArchivedChannel} from 'utils/channel_utils';
const NEXT_BUTTON_TIMEOUT_MILLISECONDS = 500;
export default class SearchableChannelList extends React.PureComponent {
static getDerivedStateFromProps(props, state) {
return {isSearch: props.isSearch, page: props.isSearch && !state.isSearch ? 0 : state.page};
}
constructor(props) {
super(props);
this.nextTimeoutId = 0;
this.state = {
joiningChannel: '',
page: 0,
nextDisabled: false,
channelSearchValue: '',
};
this.filter = React.createRef();
this.channelListScroll = React.createRef();
}
componentDidMount() {
// only focus the search box on desktop so that we don't cause the keyboard to open on mobile
if (!UserAgent.isMobile() && this.filter.current) {
this.filter.current.focus();
}
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
}
onKeyDown = (e) => {
const target = e.target;
const isEnterKeyPressed = isKeyPressed(e, Constants.KeyCodes.ENTER);
if (isEnterKeyPressed && (e.shiftKey || e.ctrlKey || e.altKey)) {
return;
}
if (isEnterKeyPressed && target.classList.contains('more-modal__row')) {
target.click();
}
}
handleJoin = (channel, e) => {
e.stopPropagation();
this.setState({joiningChannel: channel.id});
this.props.handleJoin(
channel,
() => {
this.setState({joiningChannel: ''});
},
);
if (this.isMemberOfChannel(channel.id)) {
this.props.closeModal(ModalIdentifiers.MORE_CHANNELS);
}
}
isMemberOfChannel(channelId) {
return this.props.myChannelMemberships[channelId];
}
createChannelRow = (channel) => {
const ariaLabel = `${channel.display_name}, ${channel.purpose}`.toLowerCase();
let channelTypeIcon;
let memberCount = 0;
if (this.props.allChannelStats[channel.id]) {
memberCount = this.props.allChannelStats[channel.id].member_count;
}
if (isArchivedChannel(channel)) {
channelTypeIcon = <ArchiveOutlineIcon size={18}/>;
} else if (isPrivateChannel(channel)) {
channelTypeIcon = <LockOutlineIcon size={18}/>;
} else {
channelTypeIcon = <GlobeIcon size={18}/>;
}
const membershipIndicator = this.isMemberOfChannel(channel.id) ? (
<div
id='membershipIndicatorContainer'
aria-label={localizeMessage('more_channels.membership_indicator', 'Membership Indicator: Joined')}
>
<CheckIcon size={14}/>
<FormattedMessage
id={'more_channels.joined'}
defaultMessage={'Joined'}
/>
<span className='dot'/>
</div>
) : null;
const channelPurposeContainerAriaLabel = localizeAndFormatMessage(
t('more_channels.channel_purpose'),
'Channel Information: Membership Indicator: Joined, Member count {memberCount} , Purpose: {channelPurpose}',
{memberCount, channelPurpose: channel.purpose || ''},
);
const channelPurposeContainer = (
<div
id='channelPurposeContainer'
aria-label={channelPurposeContainerAriaLabel}
>
{membershipIndicator}
<AccountOutlineIcon size={14}/>
<span>{memberCount}</span>
{channel.purpose.length > 0 && <span className='dot'/>}
<span className='more-modal__description'>{channel.purpose}</span>
</div>
);
const joinViewChannelButtonClass = classNames('btn', {
outlineButton: this.isMemberOfChannel(channel.id),
primaryButton: !this.isMemberOfChannel(channel.id),
});
const joinViewChannelButton = (
<button
id='joinViewChannelButton'
onClick={(e) => this.handleJoin(channel, e)}
className={joinViewChannelButtonClass}
disabled={this.state.joiningChannel}
tabIndex={-1}
aria-label={this.isMemberOfChannel(channel.id) ? localizeMessage('more_channels.view', 'View') : localizeMessage('joinChannel.JoinButton', 'Join')}
>
<LoadingWrapper
loading={this.state.joiningChannel === channel.id}
text={localizeMessage('joinChannel.joiningButton', 'Joining...')}
>
<FormattedMessage
id={this.isMemberOfChannel(channel.id) ? 'more_channels.view' : 'joinChannel.JoinButton'}
defaultMessage={this.isMemberOfChannel(channel.id) ? 'View' : 'Join'}
/>
</LoadingWrapper>
</button>
);
return (
<div
className='more-modal__row'
key={channel.id}
id={`ChannelRow-${channel.name}`}
aria-label={ariaLabel}
onClick={(e) => this.handleJoin(channel, e)}
tabIndex={0}
>
<div className='more-modal__details'>
<div className='style--none more-modal__name'>
{channelTypeIcon}
<span id='channelName'>{channel.display_name}</span>
</div>
{channelPurposeContainer}
</div>
<div className='more-modal__actions'>
{joinViewChannelButton}
</div>
</div>
);
}
nextPage = (e) => {
e.preventDefault();
this.setState({page: this.state.page + 1, nextDisabled: true});
this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT_MILLISECONDS);
this.props.nextPage(this.state.page + 1);
this.channelListScroll.current?.scrollTo({top: 0});
}
previousPage = (e) => {
e.preventDefault();
this.setState({page: this.state.page - 1});
this.channelListScroll.current?.scrollTo({top: 0});
}
doSearch = () => {
this.props.search(this.state.channelSearchValue);
if (this.state.channelSearchValue === '') {
this.setState({page: 0});
}
}
handleChange = (e) => {
if (e.target) {
this.setState({channelSearchValue: e.target.value}, () => this.doSearch());
}
}
handleClear = () => {
this.setState({channelSearchValue: ''}, () => this.doSearch());
}
toggleArchivedChannelsOn = () => {
this.props.toggleArchivedChannels(true);
}
toggleArchivedChannelsOff = () => {
this.props.toggleArchivedChannels(false);
}
handleChecked = () => {
// If it was checked, and now we're unchecking it, clear the preference
if (this.props.rememberHideJoinedChannelsChecked) {
this.props.hideJoinedChannelsPreference(false);
} else {
this.props.hideJoinedChannelsPreference(true);
}
}
render() {
const channels = this.props.channels;
let listContent;
let nextButton;
let previousButton;
let emptyStateMessage = (
<FormattedMessage
id={this.props.shouldShowArchivedChannels ? t('more_channels.noArchived') : t('more_channels.noPublic')}
tagName='strong'
defaultMessage={this.props.shouldShowArchivedChannels ? 'No archived channels' : 'No public channels'}
/>
);
if (this.state.channelSearchValue.length > 0) {
emptyStateMessage = (
<FormattedMessage
id='more_channels.noMore'
tagName='strong'
defaultMessage='No results for {text}'
values={{
text: this.state.channelSearchValue,
}}
/>
);
}
if (this.props.loading && channels.length === 0) {
listContent = <LoadingScreen/>;
} else if (channels.length === 0) {
listContent = (
<div
className='no-channel-message'
aria-label={this.state.channelSearchValue.length > 0 ?
localizeAndFormatMessage(t('more_channels.noMore'), 'No results for {text}', {text: this.state.channelSearchValue}) :
localizeMessage('widgets.channels_input.empty', 'No channels found')
}
>
<MagnifyingGlassSVG/>
<h3 className='primary-message'>
{emptyStateMessage}
</h3>
{this.props.noResultsText}
</div>
);
} else {
const pageStart = this.state.page * this.props.channelsPerPage;
const pageEnd = pageStart + this.props.channelsPerPage;
const channelsToDisplay = this.props.channels.slice(pageStart, pageEnd);
listContent = channelsToDisplay.map(this.createChannelRow);
if (channelsToDisplay.length >= this.props.channelsPerPage && pageEnd < this.props.channels.length) {
nextButton = (
<button
className='btn filter-control filter-control__next outlineButton'
onClick={this.nextPage}
disabled={this.state.nextDisabled}
aria-label={localizeMessage('more_channels.next', 'Next')}
>
<FormattedMessage
id='more_channels.next'
defaultMessage='Next'
/>
</button>
);
}
if (this.state.page > 0) {
previousButton = (
<button
className='btn filter-control filter-control__prev outlineButton'
onClick={this.previousPage}
aria-label={localizeMessage('more_channels.prev', 'Previous')}
>
<FormattedMessage
id='more_channels.prev'
defaultMessage='Previous'
/>
</button>
);
}
}
const input = (
<div className='filter-row filter-row--full'>
<span
id='searchIcon'
aria-hidden='true'
>
<MagnifyIcon size={18}/>
</span>
<QuickInput
id='searchChannelsTextbox'
ref={this.filter}
className='form-control filter-textbox'
placeholder={{id: t('filtered_channels_list.search'), defaultMessage: 'Search channels'}}
inputComponent={LocalizedInput}
onInput={this.handleChange}
clearable={true}
onClear={this.handleClear}
value={this.state.channelSearchValue}
aria-label={localizeMessage('filtered_channels_list.search', 'Search Channels')}
/>
</div>
);
let channelDropdown;
let checkIcon;
if (this.props.canShowArchivedChannels) {
checkIcon = (
<CheckIcon
size={18}
color={'var(--button-bg)'}
/>
);
channelDropdown = (
<MenuWrapper id='channelsMoreDropdown'>
<a id='menuWrapper'>
<span>{this.props.shouldShowArchivedChannels ? localizeMessage('more_channels.show_archived_channels', 'Channel Type: Archived') : localizeMessage('more_channels.show_public_channels', 'Channel Type: Public')}</span>
<ChevronDownIcon
color={'rgba(var(--center-channel-color-rgb), 0.64)'}
size={16}
/>
</a>
<Menu
openLeft={false}
ariaLabel={localizeMessage('more_channels.title', 'Browse channels')}
>
<div id='modalPreferenceContainer'>
<Menu.ItemAction
id='channelsMoreDropdownPublic'
onClick={this.toggleArchivedChannelsOff}
icon={<GlobeIcon size={16}/>}
text={localizeMessage('suggestion.search.public', 'Public Channels')}
rightDecorator={this.props.shouldShowArchivedChannels ? null : checkIcon}
ariaLabel={localizeMessage('suggestion.search.public', 'Public Channels')}
/>
</div>
<Menu.ItemAction
id='channelsMoreDropdownArchived'
onClick={this.toggleArchivedChannelsOn}
icon={<ArchiveOutlineIcon size={16}/>}
text={localizeMessage('suggestion.archive', 'Archived Channels')}
rightDecorator={this.props.shouldShowArchivedChannels ? checkIcon : null}
ariaLabel={localizeMessage('suggestion.archive', 'Archived Channels')}
/>
</Menu>
</MenuWrapper>
);
}
const hideJoinedButtonClass = classNames('get-app__checkbox', {checked: this.props.rememberHideJoinedChannelsChecked});
const hideJoinedPreferenceCheckbox = (
<div
id={'hideJoinedPreferenceCheckbox'}
onClick={this.handleChecked}
>
<button
className={hideJoinedButtonClass}
aria-label={this.props.rememberHideJoinedChannelsChecked ? localizeMessage('more_channels.hide_joined_checked', 'Hide joined channels checkbox, checked') : localizeMessage('more_channels.hide_joined_not_checked', 'Hide joined channels checkbox, not checked')
}
>
{this.props.rememberHideJoinedChannelsChecked ? <CheckboxCheckedIcon/> : null}
</button>
<FormattedMessage
id='more_channels.hide_joined'
defaultMessage='Hide Joined'
/>
</div>
);
let channelCountLabel;
if (channels.length === 0) {
channelCountLabel = localizeMessage('more_channels.count_zero', '0 Results');
} else if (channels.length === 1) {
channelCountLabel = localizeMessage('more_channels.count_one', '1 Result');
} else if (channels.length > 1) {
channelCountLabel = localizeAndFormatMessage(t('more_channels.count'), '0 Results', {count: channels.length});
} else {
channelCountLabel = localizeMessage('more_channels.count_zero', '0 Results');
}
const dropDownContainer = (
<div className='more-modal__dropdown'>
<span id='channelCountLabel'>{channelCountLabel}</span>
<div id='modalPreferenceContainer'>
{channelDropdown}
{hideJoinedPreferenceCheckbox}
</div>
</div>
);
return (
<div className='filtered-user-list'>
{input}
{dropDownContainer}
<div
role='application'
className='more-modal__list'
tabIndex={-1}
>
<div
id='moreChannelsList'
tabIndex={-1}
ref={this.channelListScroll}
>
{listContent}
</div>
</div>
<div className='filter-controls'>
{previousButton}
{nextButton}
</div>
</div>
);
}
}
SearchableChannelList.defaultProps = {
channels: [],
isSearch: false,
};
SearchableChannelList.propTypes = {
channels: PropTypes.arrayOf(PropTypes.object),
channelsPerPage: PropTypes.number,
nextPage: PropTypes.func.isRequired,
isSearch: PropTypes.bool,
search: PropTypes.func.isRequired,
handleJoin: PropTypes.func.isRequired,
noResultsText: PropTypes.object,
loading: PropTypes.bool,
toggleArchivedChannels: PropTypes.func.isRequired,
shouldShowArchivedChannels: PropTypes.bool.isRequired,
canShowArchivedChannels: PropTypes.bool.isRequired,
myChannelMemberships: PropTypes.object.isRequired,
allChannelStats: PropTypes.object.isRequired,
closeModal: PropTypes.func.isRequired,
hideJoinedChannelsPreference: PropTypes.func.isRequired,
rememberHideJoinedChannelsChecked: PropTypes.bool.isRequired,
};
/* eslint-enable react/no-string-refs */

View File

@@ -0,0 +1,319 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components';
import LoadingScreen from 'components/loading_screen';
import LoadingWrapper from 'components/widgets/loading/loading_wrapper';
import QuickInput from 'components/quick_input';
import * as UserAgent from 'utils/user_agent';
import {localizeMessage} from 'utils/utils';
import LocalizedInput from 'components/localized_input/localized_input';
import SharedChannelIndicator from 'components/shared_channel_indicator';
import {t} from 'utils/i18n';
import MenuWrapper from './widgets/menu/menu_wrapper';
import Menu from './widgets/menu/menu';
const NEXT_BUTTON_TIMEOUT_MILLISECONDS = 500;
export default class SearchableChannelList extends React.PureComponent {
static getDerivedStateFromProps(props, state) {
return {isSearch: props.isSearch, page: props.isSearch && !state.isSearch ? 0 : state.page};
}
constructor(props) {
super(props);
this.nextTimeoutId = 0;
this.state = {
joiningChannel: '',
page: 0,
nextDisabled: false,
};
this.filter = React.createRef();
this.channelListScroll = React.createRef();
}
componentDidMount() {
// only focus the search box on desktop so that we don't cause the keyboard to open on mobile
if (!UserAgent.isMobile() && this.filter.current) {
this.filter.current.focus();
}
}
handleJoin(channel) {
this.setState({joiningChannel: channel.id});
this.props.handleJoin(
channel,
() => {
this.setState({joiningChannel: ''});
},
);
}
createChannelRow = (channel) => {
const ariaLabel = `${channel.display_name}, ${channel.purpose}`.toLowerCase();
let archiveIcon;
let sharedIcon;
const {shouldShowArchivedChannels} = this.props;
if (shouldShowArchivedChannels) {
archiveIcon = (
<ArchiveOutlineIcon
size={20}
color={'currentColor'}
/>
);
}
if (channel.shared) {
sharedIcon = (
<SharedChannelIndicator
className='shared-channel-icon'
channelType={channel.type}
withTooltip={true}
/>
);
}
return (
<div
className='more-modal__row'
key={channel.id}
id={`ChannelRow-${channel.name}`}
>
<div className='more-modal__details'>
<button
onClick={this.handleJoin.bind(this, channel)}
aria-label={ariaLabel}
className='style--none more-modal__name'
>
{archiveIcon}
{channel.display_name}
{sharedIcon}
</button>
<p className='more-modal__description'>{channel.purpose}</p>
</div>
<div className='more-modal__actions'>
<button
onClick={this.handleJoin.bind(this, channel)}
className='btn btn-primary'
disabled={this.state.joiningChannel}
>
<LoadingWrapper
loading={this.state.joiningChannel === channel.id}
text={localizeMessage('more_channels.joining', 'Joining...')}
>
<FormattedMessage
id={shouldShowArchivedChannels ? t('more_channels.view') : t('more_channels.join')}
defaultMessage={shouldShowArchivedChannels ? 'View' : 'Join'}
/>
</LoadingWrapper>
</button>
</div>
</div>
);
}
nextPage = (e) => {
e.preventDefault();
this.setState({page: this.state.page + 1, nextDisabled: true});
this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT_MILLISECONDS);
this.props.nextPage(this.state.page + 1);
this.channelListScroll.current?.scrollTo({top: 0});
}
previousPage = (e) => {
e.preventDefault();
this.setState({page: this.state.page - 1});
this.channelListScroll.current?.scrollTo({top: 0});
}
doSearch = () => {
const term = this.filter.current.value;
this.props.search(term);
if (term === '') {
this.setState({page: 0});
}
}
toggleArchivedChannelsOn = () => {
this.props.toggleArchivedChannels(true);
}
toggleArchivedChannelsOff = () => {
this.props.toggleArchivedChannels(false);
}
render() {
const channels = this.props.channels;
let listContent;
let nextButton;
let previousButton;
if (this.props.loading && channels.length === 0) {
listContent = <LoadingScreen/>;
} else if (channels.length === 0) {
listContent = (
<div className='no-channel-message'>
<h3 className='primary-message'>
<FormattedMessage
id='more_channels.noMore'
tagName='strong'
defaultMessage='No more channels to join'
/>
</h3>
{this.props.noResultsText}
</div>
);
} else {
const pageStart = this.state.page * this.props.channelsPerPage;
const pageEnd = pageStart + this.props.channelsPerPage;
const channelsToDisplay = this.props.channels.slice(pageStart, pageEnd);
listContent = channelsToDisplay.map(this.createChannelRow);
if (channelsToDisplay.length >= this.props.channelsPerPage && pageEnd < this.props.channels.length) {
nextButton = (
<button
className='btn btn-link filter-control filter-control__next'
onClick={this.nextPage}
disabled={this.state.nextDisabled}
>
<FormattedMessage
id='more_channels.next'
defaultMessage='Next'
/>
</button>
);
}
if (this.state.page > 0) {
previousButton = (
<button
className='btn btn-link filter-control filter-control__prev'
onClick={this.previousPage}
>
<FormattedMessage
id='more_channels.prev'
defaultMessage='Previous'
/>
</button>
);
}
}
let input = (
<div className='filter-row filter-row--full'>
<div className='col-sm-12'>
<QuickInput
id='searchChannelsTextbox'
ref={this.filter}
className='form-control filter-textbox'
placeholder={{id: t('filtered_channels_list.search'), defaultMessage: 'Search channels'}}
inputComponent={LocalizedInput}
onInput={this.doSearch}
/>
</div>
</div>
);
if (this.props.createChannelButton) {
input = (
<div className='channel_search'>
<div className='search_input'>
<QuickInput
id='searchChannelsTextbox'
ref={this.filter}
className='form-control filter-textbox'
placeholder={{id: t('filtered_channels_list.search'), defaultMessage: 'Search channels'}}
inputComponent={LocalizedInput}
onInput={this.doSearch}
/>
</div>
<div className='create_button'>
{this.props.createChannelButton}
</div>
</div>
);
}
let channelDropdown;
if (this.props.canShowArchivedChannels) {
channelDropdown = (
<div className='more-modal__dropdown'>
<MenuWrapper id='channelsMoreDropdown'>
<a>
<span>{this.props.shouldShowArchivedChannels ? localizeMessage('more_channels.show_archived_channels', 'Show: Archived Channels') : localizeMessage('more_channels.show_public_channels', 'Show: Public Channels')}</span>
<span className='caret'/>
</a>
<Menu
openLeft={false}
ariaLabel={localizeMessage('team_members_dropdown.menuAriaLabel', 'Change the role of a team member')}
>
<Menu.ItemAction
id='channelsMoreDropdownPublic'
onClick={this.toggleArchivedChannelsOff}
text={localizeMessage('suggestion.search.public', 'Public Channels')}
/>
<Menu.ItemAction
id='channelsMoreDropdownArchived'
onClick={this.toggleArchivedChannelsOn}
text={localizeMessage('suggestion.archive', 'Archived Channels')}
/>
</Menu>
</MenuWrapper>
</div>
);
}
return (
<div className='filtered-user-list'>
{input}
{channelDropdown}
<div
role='application'
className='more-modal__list'
>
<div
id='moreChannelsList'
ref={this.channelListScroll}
>
{listContent}
</div>
</div>
<div className='filter-controls'>
{previousButton}
{nextButton}
</div>
</div>
);
}
}
SearchableChannelList.defaultProps = {
channels: [],
isSearch: false,
};
SearchableChannelList.propTypes = {
channels: PropTypes.arrayOf(PropTypes.object),
channelsPerPage: PropTypes.number,
nextPage: PropTypes.func.isRequired,
isSearch: PropTypes.bool,
search: PropTypes.func.isRequired,
handleJoin: PropTypes.func.isRequired,
noResultsText: PropTypes.object,
loading: PropTypes.bool,
createChannelButton: PropTypes.element,
toggleArchivedChannels: PropTypes.func.isRequired,
shouldShowArchivedChannels: PropTypes.bool.isRequired,
canShowArchivedChannels: PropTypes.bool.isRequired,
};

View File

@@ -4,25 +4,20 @@
import React from 'react';
import {shallow} from 'enzyme';
import SearchableChannelList from './searchable_channel_list.jsx';
import SearchableChannelList from 'components/searchable_channel_list.jsx';
describe('components/SearchableChannelList', () => {
const baseProps = {
channels: [],
isSearch: false,
channelsPerPage: 10,
nextPage: jest.fn(),
search: jest.fn(),
handleJoin: jest.fn(),
nextPage: () => {}, // eslint-disable-line no-empty-function
search: () => {}, // eslint-disable-line no-empty-function
handleJoin: () => {}, // eslint-disable-line no-empty-function
loading: true,
rememberHideJoinedChannelsChecked: false,
toggleArchivedChannels: jest.fn(),
toggleArchivedChannels: () => {}, // eslint-disable-line no-empty-function
shouldShowArchivedChannels: false,
canShowArchivedChannels: false,
myChannelMemberships: {},
allChannelStats: {},
closeModal: jest.fn(),
hideJoinedChannelsPreference: jest.fn(),
};
test('should match init snapshot', () => {

View File

@@ -4155,22 +4155,13 @@
"modal.manual_status.title_dnd": "Your Status is Set to \"Do Not Disturb\"",
"modal.manual_status.title_offline": "Your Status is Set to \"Offline\"",
"modal.manual_status.title_ooo": "Your Status is Set to \"Out of Office\"",
"more_channels.channel_purpose": "Channel Information: Membership Indicator: Joined, Member count {memberCount} , Purpose: {channelPurpose}",
"more_channels.count": "{count} Results",
"more_channels.count_one": "1 Result",
"more_channels.count_zero": "0 Results",
"more_channels.create": "Create New Channel",
"more_channels.hide_joined": "Hide Joined",
"more_channels.hide_joined_checked": "Hide joined channels checkbox, checked",
"more_channels.hide_joined_not_checked": "Hide joined channels checkbox, not checked",
"more_channels.joined": "Joined",
"more_channels.membership_indicator": "Membership Indicator: Joined",
"more_channels.createClick": "Click 'Create New Channel' to make a new one",
"more_channels.join": "Join",
"more_channels.joining": "Joining...",
"more_channels.next": "Next",
"more_channels.noArchived": "No archived channels",
"more_channels.noMore": "No results for \"{text}\"",
"more_channels.noPublic": "No public channels",
"more_channels.prev": "Previous",
"more_channels.searchError": "Try searching different keywords, checking for typos or adjusting the filters.",
"more_channels.show_archived_channels": "Channel Type: Archived",
"more_channels.show_public_channels": "Channel Type: Public",
"more_channels.title": "Browse Channels",

View File

@@ -71,9 +71,7 @@
.primary-message {
margin: 0;
color: var(--center-channel-color);
font-size: inherit;
line-height: 28px;
}
}

View File

@@ -896,7 +896,6 @@ export const StoragePrefixes = {
CHANNEL_CATEGORY_COLLAPSED: 'channelCategoryCollapsed_',
INLINE_IMAGE_VISIBLE: 'isInlineImageVisible_',
DELINQUENCY: 'delinquency_',
HIDE_JOINED_CHANNELS: 'hideJoinedChannels',
};
export const LandingPreferenceTypes = {

View File

@@ -38,7 +38,6 @@ export type Props = {
compassDesign?: boolean;
backdrop?: boolean;
backdropClassName?: string;
headerButton?: React.ReactNode;
tabIndex?: number;
children: React.ReactNode;
keyboardEscape?: boolean;
@@ -166,7 +165,6 @@ export class GenericModal extends React.PureComponent<Props, State> {
<h1 id='genericModalLabel'>
{this.props.modalHeaderText}
</h1>
{this.props.headerButton}
</div>
);