From 5540a31891e4dd0989223b983095a794eb3ccf40 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Thu, 22 Nov 2018 02:38:47 +0800 Subject: [PATCH 1/6] add space between suggestion and hint (#2067) --- .../command_provider.test.js.snap | 21 +++++++++++++ components/suggestion/command_provider.jsx | 4 +-- .../suggestion/command_provider.test.js | 30 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 components/suggestion/__snapshots__/command_provider.test.js.snap create mode 100644 components/suggestion/command_provider.test.js diff --git a/components/suggestion/__snapshots__/command_provider.test.js.snap b/components/suggestion/__snapshots__/command_provider.test.js.snap new file mode 100644 index 000000000000..4df6bdfe8f00 --- /dev/null +++ b/components/suggestion/__snapshots__/command_provider.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CommandSuggestion should match snapshot 1`] = ` +
+
+ /invite @[username] ~[channel] +
+
+ Invite a user to a channel +
+
+`; diff --git a/components/suggestion/command_provider.jsx b/components/suggestion/command_provider.jsx index d05fc01b4fd5..6e36f79b17da 100644 --- a/components/suggestion/command_provider.jsx +++ b/components/suggestion/command_provider.jsx @@ -12,7 +12,7 @@ import * as UserAgent from 'utils/user_agent.jsx'; import Suggestion from './suggestion.jsx'; -class CommandSuggestion extends Suggestion { +export class CommandSuggestion extends Suggestion { render() { const {item, isSelection} = this.props; @@ -28,7 +28,7 @@ class CommandSuggestion extends Suggestion { {...Suggestion.baseProps} >
- {item.suggestion + item.hint} + {item.suggestion + ' ' + item.hint}
{item.description} diff --git a/components/suggestion/command_provider.test.js b/components/suggestion/command_provider.test.js new file mode 100644 index 000000000000..d983299f0f76 --- /dev/null +++ b/components/suggestion/command_provider.test.js @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {CommandSuggestion} from './command_provider'; + +describe('CommandSuggestion', () => { + const baseProps = { + item: { + suggestion: '/invite', + hint: '@[username] ~[channel]', + description: 'Invite a user to a channel', + }, + isSelection: true, + term: '/', + matchedPretext: '', + }; + + test('should match snapshot', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('.command__title').first().text()).toEqual('/invite @[username] ~[channel]'); + expect(wrapper.find('.command__desc').first().text()).toEqual('Invite a user to a channel'); + }); +}); From 1d5d3eace93b8b7e116676e35e52ec2466f2b0a8 Mon Sep 17 00:00:00 2001 From: Sudheer Date: Thu, 22 Nov 2018 01:24:03 +0530 Subject: [PATCH 2/6] MM-4712 Add progress bar to uploads (#2010) * MM-4712 Add progressbar to uploads * Fix % spacing * * Add a new component for file_progress * * Change extentions key to type key --- .../create_comment.test.jsx.snap | 6 ++ components/create_comment/create_comment.jsx | 10 +- .../create_comment/create_comment.test.jsx | 13 +++ .../__snapshots__/create_post.test.jsx.snap | 4 + components/create_post/create_post.jsx | 10 +- components/create_post/create_post.test.jsx | 8 ++ .../__snapshots__/file_preview.test.jsx.snap | 58 ++++------ .../file_progress_preview.test.jsx.snap | 76 +++++++++++++ .../{ => file_preview}/file_preview.jsx | 34 ++---- .../{ => file_preview}/file_preview.test.jsx | 10 +- .../file_preview/file_progress_preview.jsx | 100 ++++++++++++++++++ .../file_progress_preview.test.jsx | 30 ++++++ components/file_upload/file_upload.jsx | 13 +++ components/file_upload/file_upload.test.jsx | 2 + i18n/en.json | 1 + sass/components/_files.scss | 17 +++ utils/constants.jsx | 1 + utils/file_utils.jsx | 23 ++++ utils/file_utils.test.jsx | 36 ++++++- 19 files changed, 386 insertions(+), 66 deletions(-) rename components/{ => file_preview}/__snapshots__/file_preview.test.jsx.snap (91%) create mode 100644 components/file_preview/__snapshots__/file_progress_preview.test.jsx.snap rename components/{ => file_preview}/file_preview.jsx (82%) rename components/{ => file_preview}/file_preview.test.jsx (87%) create mode 100644 components/file_preview/file_progress_preview.jsx create mode 100644 components/file_preview/file_progress_preview.test.jsx diff --git a/components/create_comment/__snapshots__/create_comment.test.jsx.snap b/components/create_comment/__snapshots__/create_comment.test.jsx.snap index a9149eb5e2c9..ae2d44ca37eb 100644 --- a/components/create_comment/__snapshots__/create_comment.test.jsx.snap +++ b/components/create_comment/__snapshots__/create_comment.test.jsx.snap @@ -134,6 +134,7 @@ exports[`components/CreateComment should match snapshot, comment with message 1` onFileUpload={[Function]} onFileUploadChange={[Function]} onUploadError={[Function]} + onUploadProgress={[Function]} onUploadStart={[Function]} postType="comment" rootId="" @@ -261,6 +262,7 @@ exports[`components/CreateComment should match snapshot, emoji picker disabled 1 onFileUpload={[Function]} onFileUploadChange={[Function]} onUploadError={[Function]} + onUploadProgress={[Function]} onUploadStart={[Function]} postType="comment" rootId="" @@ -291,6 +293,7 @@ exports[`components/CreateComment should match snapshot, emoji picker disabled 1 } onRemove={[Function]} uploadsInProgress={Array []} + uploadsProgressPercent={Object {}} />
@@ -375,6 +378,7 @@ exports[`components/CreateComment should match snapshot, empty comment 1`] = ` onFileUpload={[Function]} onFileUploadChange={[Function]} onUploadError={[Function]} + onUploadProgress={[Function]} onUploadStart={[Function]} postType="comment" rootId="" @@ -502,6 +506,7 @@ exports[`components/CreateComment should match snapshot, non-empty message and u onFileUpload={[Function]} onFileUploadChange={[Function]} onUploadError={[Function]} + onUploadProgress={[Function]} onUploadStart={[Function]} postType="comment" rootId="" @@ -569,6 +574,7 @@ exports[`components/CreateComment should match snapshot, non-empty message and u Object {}, ] } + uploadsProgressPercent={Object {}} /> diff --git a/components/create_comment/create_comment.jsx b/components/create_comment/create_comment.jsx index a2b1d2fe64f3..162f8bfb9b2d 100644 --- a/components/create_comment/create_comment.jsx +++ b/components/create_comment/create_comment.jsx @@ -17,7 +17,7 @@ import {containsAtChannel, postMessageOnKeyPress, shouldFocusMainTextbox} from ' import ConfirmModal from 'components/confirm_modal.jsx'; import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx'; -import FilePreview from 'components/file_preview.jsx'; +import FilePreview from 'components/file_preview/file_preview.jsx'; import FileUpload from 'components/file_upload'; import MsgTyping from 'components/msg_typing'; import PostDeletedModal from 'components/post_deleted_modal.jsx'; @@ -193,6 +193,7 @@ export default class CreateComment extends React.PureComponent { fileInfos: [], }, channelMembersCount: 0, + uploadsProgressPercent: {}, }; this.lastBlurAt = 0; @@ -536,6 +537,11 @@ export default class CreateComment extends React.PureComponent { this.focusTextbox(); } + handleUploadProgress = ({clientId, name, percent, type}) => { + const uploadsProgressPercent = {...this.state.uploadsProgressPercent, [clientId]: {percent, name, type}}; + this.setState({uploadsProgressPercent}); + } + handleFileUploadComplete = (fileInfos, clientIds, channelId, rootId) => { const draft = this.draftsForPost[rootId]; const uploadsInProgress = [...draft.uploadsInProgress]; @@ -735,6 +741,7 @@ export default class CreateComment extends React.PureComponent { fileInfos={draft.fileInfos} onRemove={this.removePreview} uploadsInProgress={draft.uploadsInProgress} + uploadsProgressPercent={this.state.uploadsProgressPercent} ref='preview' /> ); @@ -775,6 +782,7 @@ export default class CreateComment extends React.PureComponent { onUploadStart={this.handleUploadStart} onFileUpload={this.handleFileUploadComplete} onUploadError={this.handleUploadError} + onUploadProgress={this.handleUploadProgress} rootId={this.props.rootId} postType='comment' /> diff --git a/components/create_comment/create_comment.test.jsx b/components/create_comment/create_comment.test.jsx index 8c4a835a1c48..99b371ed34a5 100644 --- a/components/create_comment/create_comment.test.jsx +++ b/components/create_comment/create_comment.test.jsx @@ -7,6 +7,8 @@ import {shallow} from 'enzyme'; import Constants from 'utils/constants.jsx'; import CreateComment from 'components/create_comment/create_comment.jsx'; +import FileUpload from 'components/file_upload'; +import FilePreview from 'components/file_preview/file_preview.jsx'; jest.mock('stores/post_store.jsx', () => ({ clearCommentDraftUploads: jest.fn(), @@ -315,6 +317,17 @@ describe('components/CreateComment', () => { expect(wrapper.state().draft.fileInfos).toEqual(expectedNewFileInfos); }); + it('check for uploadsProgressPercent state on handleUploadProgress callback', () => { + const wrapper = shallow( + + ); + + wrapper.find(FileUpload).prop('onUploadProgress')({clientId: 'clientId', name: 'name', percent: 10, type: 'type'}); + expect(wrapper.find(FilePreview).prop('uploadsProgressPercent')).toEqual({clientId: {percent: 10, name: 'name', type: 'type'}}); + + expect(wrapper.state('uploadsProgressPercent')).toEqual({clientId: {percent: 10, name: 'name', type: 'type'}}); + }); + test('calls showPostDeletedModal when createPostErrorId === api.post.create_post.root_id.app_error', () => { const onUpdateCommentDraft = jest.fn(); const draft = { diff --git a/components/create_post/__snapshots__/create_post.test.jsx.snap b/components/create_post/__snapshots__/create_post.test.jsx.snap index 937a2f423ded..ed6ba5398edf 100644 --- a/components/create_post/__snapshots__/create_post.test.jsx.snap +++ b/components/create_post/__snapshots__/create_post.test.jsx.snap @@ -42,6 +42,7 @@ exports[`components/create_post Show tutorial 1`] = ` onFileUpload={[Function]} onFileUploadChange={[Function]} onUploadError={[Function]} + onUploadProgress={[Function]} onUploadStart={[Function]} postType="post" /> @@ -206,6 +207,7 @@ exports[`components/create_post should match snapshot for center textbox 1`] = ` onFileUpload={[Function]} onFileUploadChange={[Function]} onUploadError={[Function]} + onUploadProgress={[Function]} onUploadStart={[Function]} postType="post" /> @@ -439,6 +441,7 @@ exports[`components/create_post should match snapshot when file upload disabled onFileUpload={[Function]} onFileUploadChange={[Function]} onUploadError={[Function]} + onUploadProgress={[Function]} onUploadStart={[Function]} postType="post" /> @@ -572,6 +575,7 @@ exports[`components/create_post should match snapshot, init 1`] = ` onFileUpload={[Function]} onFileUploadChange={[Function]} onUploadError={[Function]} + onUploadProgress={[Function]} onUploadStart={[Function]} postType="post" /> diff --git a/components/create_post/create_post.jsx b/components/create_post/create_post.jsx index 11eef3740f80..932025199163 100644 --- a/components/create_post/create_post.jsx +++ b/components/create_post/create_post.jsx @@ -17,7 +17,7 @@ import * as Utils from 'utils/utils.jsx'; import ConfirmModal from 'components/confirm_modal.jsx'; import EditChannelHeaderModal from 'components/edit_channel_header_modal'; import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx'; -import FilePreview from 'components/file_preview.jsx'; +import FilePreview from 'components/file_preview/file_preview.jsx'; import FileUpload from 'components/file_upload'; import MsgTyping from 'components/msg_typing'; import PostDeletedModal from 'components/post_deleted_modal.jsx'; @@ -240,6 +240,7 @@ export default class CreatePost extends React.Component { showEmojiPicker: false, showConfirmModal: false, channelMembersCount: 0, + uploadsProgressPercent: {}, }; this.lastBlurAt = 0; @@ -597,6 +598,11 @@ export default class CreatePost extends React.Component { this.focusTextbox(); } + handleUploadProgress = ({clientId, name, percent, type}) => { + const uploadsProgressPercent = {...this.state.uploadsProgressPercent, [clientId]: {percent, name, type}}; + this.setState({uploadsProgressPercent}); + } + handleFileUploadComplete = (fileInfos, clientIds, channelId) => { const draft = {...this.draftsForChannel[channelId]}; @@ -967,6 +973,7 @@ export default class CreatePost extends React.Component { fileInfos={draft.fileInfos} onRemove={this.removePreview} uploadsInProgress={draft.uploadsInProgress} + uploadsProgressPercent={this.state.uploadsProgressPercent} /> ); } @@ -1007,6 +1014,7 @@ export default class CreatePost extends React.Component { onUploadStart={this.handleUploadStart} onFileUpload={this.handleFileUploadComplete} onUploadError={this.handleUploadError} + onUploadProgress={this.handleUploadProgress} postType='post' /> ); diff --git a/components/create_post/create_post.test.jsx b/components/create_post/create_post.test.jsx index 6c350df35ce7..a3a5fa956353 100644 --- a/components/create_post/create_post.test.jsx +++ b/components/create_post/create_post.test.jsx @@ -12,6 +12,7 @@ import Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants.jsx' import * as Utils from 'utils/utils.jsx'; import CreatePost from 'components/create_post/create_post.jsx'; +import FileUpload from 'components/file_upload'; jest.mock('actions/global_actions.jsx', () => ({ emitLocalUserTypingEvent: jest.fn(), @@ -556,6 +557,13 @@ describe('components/create_post', () => { expect(setDraft).toHaveBeenCalledWith(StoragePrefixes.DRAFT + currentChannelProp.id, draftProp); }); + it('check for uploadsProgressPercent state on handleUploadProgress callback', () => { + const wrapper = shallow(createPost({})); + wrapper.find(FileUpload).prop('onUploadProgress')({clientId: 'clientId', name: 'name', percent: 10, type: 'type'}); + + expect(wrapper.state('uploadsProgressPercent')).toEqual({clientId: {percent: 10, name: 'name', type: 'type'}}); + }); + it('Remove preview from fileInfos', () => { const setDraft = jest.fn(); const fileInfos = { diff --git a/components/__snapshots__/file_preview.test.jsx.snap b/components/file_preview/__snapshots__/file_preview.test.jsx.snap similarity index 91% rename from components/__snapshots__/file_preview.test.jsx.snap rename to components/file_preview/__snapshots__/file_preview.test.jsx.snap index 627430c02422..90aa732a1d37 100644 --- a/components/__snapshots__/file_preview.test.jsx.snap +++ b/components/file_preview/__snapshots__/file_preview.test.jsx.snap @@ -70,25 +70,18 @@ exports[`component/FilePreview should match snapshot 1`] = ` -
- - - - -
+ /> `; @@ -162,25 +155,18 @@ exports[`component/FilePreview should match snapshot when props are changed 1`] -
- - - - -
+ /> `; diff --git a/components/file_preview/__snapshots__/file_progress_preview.test.jsx.snap b/components/file_preview/__snapshots__/file_progress_preview.test.jsx.snap new file mode 100644 index 000000000000..f284c73f2ffe --- /dev/null +++ b/components/file_preview/__snapshots__/file_progress_preview.test.jsx.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component/file_preview/file_progress_preview should match snapshot 1`] = ` +
+
+
+
+
+
+
+ + + + + (50%) + + + +
+
+
+ + + +
+
+
+`; diff --git a/components/file_preview.jsx b/components/file_preview/file_preview.jsx similarity index 82% rename from components/file_preview.jsx rename to components/file_preview/file_preview.jsx index f406f9ee94cb..3852b2c658f2 100644 --- a/components/file_preview.jsx +++ b/components/file_preview/file_preview.jsx @@ -9,26 +9,22 @@ import FilenameOverlay from 'components/file_attachment/filename_overlay.jsx'; import Constants, {FileTypes} from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; -import loadingGif from 'images/load.gif'; +import FileProgressPreview from './file_progress_preview.jsx'; export default class FilePreview extends React.PureComponent { static propTypes = { onRemove: PropTypes.func.isRequired, fileInfos: PropTypes.arrayOf(PropTypes.object).isRequired, uploadsInProgress: PropTypes.array, + uploadsProgressPercent: PropTypes.object, }; static defaultProps = { fileInfos: [], uploadsInProgress: [], + uploadsProgressPercent: {}, }; - componentDidUpdate() { - if (this.props.uploadsInProgress.length > 0) { - this.refs[this.props.uploadsInProgress[0]].scrollIntoView(); - } - } - handleRemove = (id) => { this.props.onRemove(id); } @@ -115,26 +111,12 @@ export default class FilePreview extends React.PureComponent { this.props.uploadsInProgress.forEach((clientId) => { previews.push( -
- - - - -
+ clientId={clientId} + fileInfo={this.props.uploadsProgressPercent[clientId]} + handleRemove={this.handleRemove} + /> ); }); diff --git a/components/file_preview.test.jsx b/components/file_preview/file_preview.test.jsx similarity index 87% rename from components/file_preview.test.jsx rename to components/file_preview/file_preview.test.jsx index 51c402c070cd..abed274506ea 100644 --- a/components/file_preview.test.jsx +++ b/components/file_preview/file_preview.test.jsx @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import FilePreview from 'components/file_preview.jsx'; +import FilePreview from './file_preview.jsx'; describe('component/FilePreview', () => { const onRemove = jest.fn(); @@ -18,11 +18,19 @@ describe('component/FilePreview', () => { }, ]; const uploadsInProgress = ['clientID_1']; + const uploadsProgressPercent = { + clientID_1: { + name: 'file', + percent: 50, + extension: 'image/png', + }, + }; const baseProps = { fileInfos, uploadsInProgress, onRemove, + uploadsProgressPercent, }; test('should match snapshot', () => { diff --git a/components/file_preview/file_progress_preview.jsx b/components/file_preview/file_progress_preview.jsx new file mode 100644 index 000000000000..278d9a47d3be --- /dev/null +++ b/components/file_preview/file_progress_preview.jsx @@ -0,0 +1,100 @@ +// 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 {ProgressBar} from 'react-bootstrap'; + +import FilenameOverlay from 'components/file_attachment/filename_overlay.jsx'; +import {getFileTypeFromMime} from 'utils/file_utils'; +import * as Utils from 'utils/utils.jsx'; + +export default class FileProgressPreview extends React.PureComponent { + static propTypes = { + handleRemove: PropTypes.func.isRequired, + clientId: PropTypes.string.isRequired, + fileInfo: PropTypes.object, + }; + + handleRemove = () => { + this.props.handleRemove(this.props.clientId); + } + + render() { + let percent = 0; + let fileNameComponent; + let previewImage; + const {fileInfo, clientId} = this.props; + if (fileInfo) { + percent = fileInfo.percent; + const percentTxt = ` (${percent.toFixed(0)}%)`; + const fileType = getFileTypeFromMime(fileInfo.type); + previewImage =
; + + fileNameComponent = ( + + + + {percent === 100 ? ( + + ) : ( + + + {percentTxt} + + )} + + + + ); + } + + return ( +
+
+ {previewImage} +
+
+
+
+ {fileNameComponent} +
+
+
+ + + +
+
+
+ ); + } +} diff --git a/components/file_preview/file_progress_preview.test.jsx b/components/file_preview/file_progress_preview.test.jsx new file mode 100644 index 000000000000..e460134b3892 --- /dev/null +++ b/components/file_preview/file_progress_preview.test.jsx @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import FileProgressPreview from './file_progress_preview.jsx'; + +describe('component/file_preview/file_progress_preview', () => { + const handleRemove = jest.fn(); + + const fileInfo = { + name: 'file', + percent: 50, + type: 'image/png', + }; + + const baseProps = { + clientId: 'clientId', + fileInfo, + handleRemove, + }; + + test('should match snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/components/file_upload/file_upload.jsx b/components/file_upload/file_upload.jsx index 6caccd39aa92..ea0d72ade206 100644 --- a/components/file_upload/file_upload.jsx +++ b/components/file_upload/file_upload.jsx @@ -131,6 +131,10 @@ export default class FileUpload extends PureComponent { pluginFileUploadMethods: PropTypes.arrayOf(PropTypes.object), pluginFilesWillUploadHooks: PropTypes.arrayOf(PropTypes.object), + /** + * Function called when superAgent fires progress event. + */ + onUploadProgress: PropTypes.func.isRequired, actions: PropTypes.shape({ /** @@ -264,6 +268,15 @@ export default class FileUpload extends PureComponent { clientId, ); + request.on('progress', (progressEvent) => { + this.props.onUploadProgress({ + clientId, + name: sortedFiles[i].name, + percent: progressEvent.percent, + type: sortedFiles[i].type, + }); + }); + request.end((err, res) => { const {error, data} = this.props.actions.handleFileUploadEnd( sortedFiles[i], diff --git a/components/file_upload/file_upload.test.jsx b/components/file_upload/file_upload.test.jsx index cc778e0d9c4e..f026c34fa621 100644 --- a/components/file_upload/file_upload.test.jsx +++ b/components/file_upload/file_upload.test.jsx @@ -38,6 +38,7 @@ describe('components/FileUpload', () => { beforeEach(() => { uploadFile = jest.fn(() => ({ end: emptyFunction, + on: emptyFunction, })); baseProps = { @@ -51,6 +52,7 @@ describe('components/FileUpload', () => { onFileUploadChange: jest.fn(), onUploadError: jest.fn(), onUploadStart: jest.fn(), + onUploadProgress: jest.fn(), postType: 'post', maxFileSize: MaxFileSize, canUploadFiles: true, diff --git a/i18n/en.json b/i18n/en.json index a3377696470b..42ebe1b064c0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1655,6 +1655,7 @@ "create_post.tutorialTip1": "Type here to write a message and press **Enter** to post it.", "create_post.tutorialTip2": "Click the **Attachment** button to upload an image or a file.", "create_post.write": "Write a message...", + "create_post.fileProcessing": "Processing...", "create_team.agreement": "By proceeding to create your account and use {siteName}, you agree to our [Terms of Service]({TermsOfServiceLink}) and [Privacy Policy]({PrivacyPolicyLink}). If you do not agree, you cannot use {siteName}.", "create_team.display_name.charLength": "Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description later.", "create_team.display_name.nameHelp": "Name your team in any language. Your team name shows in menus and headings.", diff --git a/sass/components/_files.scss b/sass/components/_files.scss index 8c59998b71aa..73b50cbc95a4 100644 --- a/sass/components/_files.scss +++ b/sass/components/_files.scss @@ -333,6 +333,7 @@ .post-image__details { @include clearfix; + position: relative; align-items: center; border-left: 1px solid $light-gray; color: alpha-color($black, .8); @@ -402,6 +403,22 @@ } } } + .post-image__progressBar { + margin-bottom: 0px; + height: 7px; + position: absolute; + bottom: 0px; + width: 100%; + margin-left: -15px; + border-radius: 0px; + } + + .progress-bar { + background-color: rgb(40, 90, 185); + } + .post-image__uploadingTxt { + opacity: 0.6; + } } .file-details__container { diff --git a/utils/constants.jsx b/utils/constants.jsx index 62b01b836f96..eb8b17216e1b 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -709,6 +709,7 @@ export const Constants = { word: 'word', patch: 'patch', other: 'generic', + image: 'image', }, MAX_UPLOAD_FILES: 5, MAX_FILENAME_LENGTH: 35, diff --git a/utils/file_utils.jsx b/utils/file_utils.jsx index 08dd148667cc..b1ff6253d540 100644 --- a/utils/file_utils.jsx +++ b/utils/file_utils.jsx @@ -35,3 +35,26 @@ export function trimFilename(filename) { return trimmedFilename; } + +export function getFileTypeFromMime(mimetype) { + const mimeTypeSplitBySlash = mimetype.split('/'); + const mimeTypePrefix = mimeTypeSplitBySlash[0]; + const mimeTypeSuffix = mimeTypeSplitBySlash[1]; + if (mimeTypePrefix === 'video') { + return 'video'; + } else if (mimeTypePrefix === 'audio') { + return 'audio'; + } else if (mimeTypePrefix === 'image') { + return 'image'; + } else if (mimeTypeSuffix === 'pdf') { + return 'pdf'; + } else if (mimeTypeSuffix.includes('vnd.ms-excel') || mimeTypeSuffix.includes('spreadsheetml') || mimeTypeSuffix.includes('vnd.sun.xml.calc') || mimeTypeSuffix.includes('opendocument.spreadsheet')) { + return 'spreadsheet'; + } else if (mimeTypeSuffix.includes('vnd.ms-powerpoint') || mimeTypeSuffix.includes('presentationml') || mimeTypeSuffix.includes('vnd.sun.xml.impress') || mimeTypeSuffix.includes('opendocument.presentation')) { + return 'presentation'; + } else if ((mimeTypeSuffix === 'msword') || mimeTypeSuffix.includes('vnd.ms-word') || mimeTypeSuffix.includes('officedocument.wordprocessingml') || mimeTypeSuffix.includes('application/x-mswrite')) { + return 'word'; + } + + return 'other'; +} diff --git a/utils/file_utils.test.jsx b/utils/file_utils.test.jsx index a864f3563610..c8176208d2b3 100644 --- a/utils/file_utils.test.jsx +++ b/utils/file_utils.test.jsx @@ -3,7 +3,7 @@ import assert from 'assert'; -import {trimFilename, canUploadFiles} from 'utils/file_utils.jsx'; +import {trimFilename, canUploadFiles, getFileTypeFromMime} from 'utils/file_utils.jsx'; import * as UserAgent from 'utils/user_agent'; describe('FileUtils.trimFilename', () => { @@ -66,4 +66,38 @@ describe('FileUtils.canUploadFiles', () => { assert.equal(canUploadFiles(config), false); }); }); + + describe('get filetypes based on mime interpreted from browsers', () => { + it('mime type for videos', () => { + assert.equal(getFileTypeFromMime('video/mp4'), 'video'); + }); + + it('mime type for audio', () => { + assert.equal(getFileTypeFromMime('audio/mp3'), 'audio'); + }); + + it('mime type for image', () => { + assert.equal(getFileTypeFromMime('image/JPEG'), 'image'); + }); + + it('mime type for pdf', () => { + assert.equal(getFileTypeFromMime('application/pdf'), 'pdf'); + }); + + it('mime type for spreadsheet', () => { + assert.equal(getFileTypeFromMime('application/vnd.ms-excel'), 'spreadsheet'); + }); + + it('mime type for presentation', () => { + assert.equal(getFileTypeFromMime('application/vnd.ms-powerpoint'), 'presentation'); + }); + + it('mime type for word', () => { + assert.equal(getFileTypeFromMime('application/vnd.ms-word'), 'word'); + }); + + it('mime type for unknown file format', () => { + assert.equal(getFileTypeFromMime('application/unknownFormat'), 'other'); + }); + }); }); From 4fa4eb3b9787e73c2bdac36adb0879a35c5e1810 Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Thu, 22 Nov 2018 13:14:45 +0100 Subject: [PATCH 3/6] update redux deps (#2069) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d7b8c4cc2fd0..97154f56521b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11581,8 +11581,8 @@ "dev": true }, "mattermost-redux": { - "version": "github:mattermost/mattermost-redux#638fb54604e795a4776e3ae6b0d06aed84a9bdb9", - "from": "github:mattermost/mattermost-redux#638fb54604e795a4776e3ae6b0d06aed84a9bdb9", + "version": "github:mattermost/mattermost-redux#138c99e9c76bcec053d7d6983ec8bddbb33dca16", + "from": "github:mattermost/mattermost-redux#138c99e9c76bcec053d7d6983ec8bddbb33dca16", "requires": { "deep-equal": "1.0.1", "eslint-plugin-header": "1.2.0", diff --git a/package.json b/package.json index 3fcf23c8c5af..acc4acfbb217 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "localforage": "1.7.2", "localforage-observable": "1.4.0", "marked": "github:mattermost/marked#ed33baecd7d7fa97d479ba22dde9d226b083d67d", - "mattermost-redux": "github:mattermost/mattermost-redux#638fb54604e795a4776e3ae6b0d06aed84a9bdb9", + "mattermost-redux": "github:mattermost/mattermost-redux#138c99e9c76bcec053d7d6983ec8bddbb33dca16", "moment-timezone": "0.5.21", "pdfjs-dist": "2.0.489", "perfect-scrollbar": "0.8.1", From e729b7b82aee0bece072912529a6eaf0ead2dbe9 Mon Sep 17 00:00:00 2001 From: Jason Blais <13119842+jasonblais@users.noreply.github.com> Date: Thu, 22 Nov 2018 09:11:47 -0500 Subject: [PATCH 4/6] Take data retention out of beta (#2070) * Update admin_sidebar.jsx * Update data_retention_settings.jsx * Update en.json --- components/admin_console/admin_sidebar/admin_sidebar.jsx | 2 +- components/admin_console/data_retention_settings.jsx | 2 +- i18n/en.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/admin_console/admin_sidebar/admin_sidebar.jsx b/components/admin_console/admin_sidebar/admin_sidebar.jsx index 9c41301a43fe..0ef58cf31f83 100644 --- a/components/admin_console/admin_sidebar/admin_sidebar.jsx +++ b/components/admin_console/admin_sidebar/admin_sidebar.jsx @@ -288,7 +288,7 @@ export default class AdminSidebar extends React.Component { title={ } /> diff --git a/components/admin_console/data_retention_settings.jsx b/components/admin_console/data_retention_settings.jsx index b7f73c957dc4..943faa16a3d0 100644 --- a/components/admin_console/data_retention_settings.jsx +++ b/components/admin_console/data_retention_settings.jsx @@ -162,7 +162,7 @@ export default class DataRetentionSettings extends AdminSettings { return ( ); } diff --git a/i18n/en.json b/i18n/en.json index 42ebe1b064c0..9ceded5deb8f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -335,7 +335,7 @@ "admin.data_retention.messageRetentionDays.example": "E.g.: \"60\"", "admin.data_retention.note.description": "Caution: Once a message or a file is deleted, the action is irreversible. Please be careful when setting up a custom data retention policy. See {documentationLink} to learn more.", "admin.data_retention.note.description.documentationLinkText": "documentation", - "admin.data_retention.title": "Data Retention Policy (Beta)", + "admin.data_retention.title": "Data Retention Policy", "admin.database.title": "Database Settings", "admin.developer.title": "Developer Settings", "admin.elasticsearch.bulkIndexingTitle": "Bulk Indexing:", @@ -1145,7 +1145,7 @@ "admin.sidebar.customBrand": "Custom Branding", "admin.sidebar.customIntegrations": "Custom Integrations", "admin.sidebar.customization": "Customization", - "admin.sidebar.data_retention": "Data Retention Policy (Beta)", + "admin.sidebar.data_retention": "Data Retention Policy", "admin.sidebar.database": "Database", "admin.sidebar.developer": "Developer", "admin.sidebar.elasticsearch": "Elasticsearch", From 8b14021148da9d10dfecfd87a95311308a0dd3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 22 Nov 2018 15:31:38 +0100 Subject: [PATCH 5/6] MM-12839: Remove archived channels posts from search results when needed (#2066) --- actions/websocket_actions.jsx | 2 +- components/rhs_thread/rhs_thread.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/websocket_actions.jsx b/actions/websocket_actions.jsx index 154776263391..77d14d6a047d 100644 --- a/actions/websocket_actions.jsx +++ b/actions/websocket_actions.jsx @@ -706,7 +706,7 @@ function handleChannelDeletedEvent(msg) { browserHistory.push(teamUrl + '/channels/' + Constants.DEFAULT_CHANNEL); } - dispatch({type: ChannelTypes.RECEIVED_CHANNEL_DELETED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, deleteAt: msg.data.delete_at}}); + dispatch({type: ChannelTypes.RECEIVED_CHANNEL_DELETED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, deleteAt: msg.data.delete_at, viewArchivedChannels}}); } function handlePreferenceChangedEvent(msg) { diff --git a/components/rhs_thread/rhs_thread.jsx b/components/rhs_thread/rhs_thread.jsx index 15091c50b112..40002cfe51b9 100644 --- a/components/rhs_thread/rhs_thread.jsx +++ b/components/rhs_thread/rhs_thread.jsx @@ -241,7 +241,7 @@ export default class RhsThread extends React.Component { const {selected, currentUser} = this.props; let createAt = selected.create_at; - if (!createAt) { + if (!createAt && this.props.posts.length > 0) { createAt = this.props.posts[this.props.posts.length - 1].create_at; } const rootPostDay = Utils.getDateForUnixTicks(createAt); @@ -362,7 +362,7 @@ export default class RhsThread extends React.Component { previewEnabled={this.props.previewEnabled} isBusy={this.state.isBusy} /> - {isFakeDeletedPost && } + {isFakeDeletedPost && rootPostDay && }
Date: Fri, 23 Nov 2018 02:58:57 +0900 Subject: [PATCH 6/6] Refactor Modals, ChannelHeader and Navbar component (#1666) * Refactor duplicated RenameChannelModal component Remove duplicated RenameChannelModal code from Navbar and ChannelHeader, Use ModalController instead * Refactor duplicated EditChannelPurposeModal comp - Remove duplicated EditChannelPurposeModal code from ChannelHeader and Navbar - Use ModalController instead of to use it directly - Change the modal to PureComponent * Refactor duplicated ChannelMembersModal component - Remove ChannelMembersModal code from ChannelHeader and Navbar - Now the modal is managed by the ModalController instead of directly by the component. - Add docs for props of ChannelMemebersModal * Refactor ChannelNotificationsModal comp - Change the ChannelNotificationsModal component to PureComponent - Add docs for props of ChannelNotificationsModal - Remove duplicated ChannelNotificationModal code from ChannelHeader and Navbar * Move QuickSwitchModal to ModalController * Refactor ModalController component - Change to PureComponent - Add docs for props * Change Navbar component to PureComponent * Migrate NavBar code to be use mattermost-redux * Refactor actions in NavBar * Extract SetChannelHeaderOption component from NavBar * Extract ViewChannelInfoOption component from NavBar * Extract NotificationPreferencesOption component from NavBar * Extract ChannelMembersOption component from NavBar * Extract ViewPinnedPostsOption component from NavBar * Extract AddMembersOption component from NavBar * Extract SetChannelPurposeOption component from NavBar * Extract RenameChannelOption component from NavBar * Extract ConvertChannelOption component from NavBar * Extract DeleteChannelOption component from NavBar * Extract LeaveChannelOption component from NavBar * Extract ToggleFavoriteChannel component from NavBar * Fix typo in ModalIdentifiers * Extract WebrtcOption component from NavBar * Building clean dropdown code, start from AddMembers * ... ViewChannelInfo * ... ViewPinnedPosts * ... NotificationPreferences * ... ToggleFavoriteChannel * ... SetChannelHeader * ... SetChannelPurpose * ... ViewMembers * ... ManageMembers * ... ConvertChannel * ... RenameChannel * ... DeleteChannel * ... LeaveChannel * ... WebRTC * Add draft of ChannelHeaderDropdown component * Connect MobileChannelHeaderComponent to redux store * Change the default rendering of dropdown * Clean Navbar component with the new dropdown component * Remove more * Extract ShowSearchButton component from Navbar * Remove undefined prop * Remove unnecessary wrapper * Comment on collapseButtons * Refactor Navbar even more - Extract collapse buttons - Remove code that is unused or never called - Remove jQuery * Fix character of close button * Make channel prop to be required * Remove unused codes * Fix ToggleFavorite menu * [MM-11162] Add mute icon to mobile view (#1744) * add mute icon to mobile view * use let or const as necessary and do some clean up Modified. * Cherry-picked MM-11577: Back to previous channel on archive (#1775) * Fix the composition of navbar TODO: - Notification preference would not be updated when change * Fix NotificationPreference modal * Extract ToggleMuteChannel comp from ChannelHeader * Cleanup ChannelHeader with new ChannelHeaderDropdown * Update snapshot of NotificationPreferences menu * Refactor ChannelHeader component * Fix tests * MM-12150 - Updating x icon and plugins (#1841) * Fix tests * Rename Navbar to ChannelHeaderMobile - Rename `navbar` to `channel_header_mobile` - Rename `navbar_info_button` to `channel_info_button` * MM-12503: Show Mute/Unmute menu item for GMs too. (#1847) * Remove comment from view * Change ext of modified components to .js instead of .jsx * MM-12494 Removing some flux store usage. (#1882) * Removing some flux store usage. * Feedback fixes Cherry-picked. * Fix some filename extensions * Migrate ChannelActions joinChannel and leaveChannel to redux (#1898) * Migrate ChannelActions favorite and unfavorite channel to redux * Fix unit test after rebase * Migrate ChannelActions joinChannel and leaveChannel to redux Cherry-picked * Fix a updated snaphost * Update ModalController snapshot * Cherry-picked: MM-12497 Remove flux store usages from user settings (#1915) * Remove Flux store usages from account settings modal * Fix test * Remove global action for opening account settings modal * Fix notifications not updating correctly * Refactor executeCommand to remove import cycle * Drop WebRTC from ChannelHeader * Remove deprecated lifecycle method from ChannelHeader * Replace hardcoded isMobile to utils * Fix import annotations * Migrate current channel state selectors to use mattermost-redux * Remove unused declarations * Change ToggleMuteChannel component to PureComponent class style to avoid arrow function in render * Remove unnecessary arrow function in render * Rollback mapStateToProps to plain function * Change UnmuteChannelButton component to PureComponent class style to avoid arrow function in render * Change file ext for UnmuteChannelButton to .js * Move back QuickSwitchModal to ChannelHeader (Desktop only), and use redux action * Change ShowSearchButton component to PureComponent class style to avoid arrow function in render * Rollback mapStateToProps for ViewPinnedPosts to plain function * Cherry-picked: Remove flux usages from email invite modal (#1954) * Fix Channel Header * Fix ShowSearchButton in ChannelHeaderMobile * Fix ChannelMembersModal on PopoverListMembers * Migrate test files to component path * fix eslint style error on test mock * Update per feedback * Fix ChannelMembersModal test, IDK when it have been break exactly * Fix errors on dropdown * Fix view_and_manage_members to show different name by permission * Fix to show RenameChannel on defaultChannel * Refactor ChannelMembersModal to use redux action * Fix ChannelNotificationModal to handle hide anim correctly * Fix ViewAndManageMembers to be view mode only in default channel --- .../channel_header.test.jsx.snap | 737 +----- components/channel_header/channel_header.js | 581 +++++ components/channel_header/channel_header.jsx | 1143 ---------- .../channel_header/channel_header.test.jsx | 3 + ....snap => header_icon_wrapper.test.js.snap} | 0 ...con_wrapper.jsx => header_icon_wrapper.js} | 2 +- ...r.test.jsx => header_icon_wrapper.test.js} | 2 +- components/channel_header/index.js | 95 +- .../channel_header_dropdown.js | 100 + components/channel_header_dropdown/index.js | 61 + .../__snapshots__/add_members.test.js.snap | 38 + .../convert_channel.test.js.snap | 34 + .../__snapshots__/group.test.js.snap | 10 + .../notification_preferences.test.js.snap | 27 + .../__snapshots__/rename_channel.test.js.snap | 38 + .../set_channel_header.test.js.snap | 39 + .../set_channel_purpose.test.js.snap | 38 + .../view_and_manage_members.test.js.snap | 82 + .../view_channel_info.test.js.snap | 26 + .../menu_items/add_members.js | 75 + .../menu_items/add_members.test.js | 58 + .../__snapshots__/close_channel.test.js.snap | 19 + .../menu_items/close_channel/close_channel.js | 40 + .../close_channel/close_channel.test.js | 43 + .../menu_items/close_channel/index.js | 17 + .../menu_items/convert_channel.js | 67 + .../menu_items/convert_channel.test.js | 62 + .../__snapshots__/delete_channel.test.js.snap | 39 + .../delete_channel/delete_channel.js | 73 + .../delete_channel/delete_channel.test.js | 74 + .../menu_items/delete_channel/index.js | 15 + .../menu_items/group.js | 21 + .../menu_items/group.test.js | 14 + .../menu_items/index.js | 19 + .../__snapshots__/leave_channel.test.js.snap | 19 + .../menu_items/leave_channel/index.js | 14 + .../menu_items/leave_channel/leave_channel.js | 83 + .../leave_channel/leave_channel.test.js | 79 + .../menu_items/notification_preferences.js | 61 + .../notification_preferences.test.js | 44 + .../menu_items/rename_channel.js | 66 + .../menu_items/rename_channel.test.js | 65 + .../menu_items/set_channel_header.js | 68 + .../menu_items/set_channel_header.test.js | 63 + .../menu_items/set_channel_purpose.js | 75 + .../menu_items/set_channel_purpose.test.js | 76 + .../toggle_favorite_channel.test.js.snap | 37 + .../toggle_favorite_channel/index.js | 17 + .../toggle_favorite_channel.js | 78 + .../toggle_favorite_channel.test.js | 70 + .../toggle_mute_channel.test.js.snap | 19 + .../menu_items/toggle_mute_channel/index.js | 16 + .../toggle_mute_channel.js | 85 + .../toggle_mute_channel.test.js | 80 + .../menu_items/view_and_manage_members.js | 76 + .../view_and_manage_members.test.js | 48 + .../menu_items/view_channel_info.js | 47 + .../menu_items/view_channel_info.test.js | 35 + .../view_pinned_posts.test.js.snap | 19 + .../menu_items/view_pinned_posts/index.js | 24 + .../view_pinned_posts/view_pinned_posts.js | 66 + .../view_pinned_posts.test.js | 49 + .../mobile_channel_header_dropdown.js | 141 ++ .../channel_header_mobile.test.js.snap | 281 +++ .../channel_header_mobile.js | 111 + .../channel_header_mobile.test.js | 101 + .../channel_info_button.test.js.snap} | 6 +- .../channel_info_button.js} | 84 +- .../channel_info_button.test.js} | 10 +- .../channel_info_button}/index.js | 4 +- .../collapse_lhs_button.js | 41 + .../collapse_lhs_button/index.js | 18 + .../collapse_rhs_button.js | 32 + .../collapse_rhs_button/index.js | 17 + components/channel_header_mobile/index.js | 43 + .../show_search_button/index.js | 17 + .../show_search_button/show_search_button.js | 35 + .../unmute_channel_button.test.js.snap | 13 + .../unmute_channel_button/index.js | 16 + .../unmute_channel_button.js | 47 + .../unmute_channel_button.test.js | 43 + .../center_channel/center_channel.jsx | 4 +- .../channel_members_modal.jsx | 49 +- .../channel_members_modal.test.jsx | 98 +- components/channel_members_modal/index.js | 19 +- .../channel_notifications_modal.jsx | 48 +- .../channel_notifications_modal.test.jsx | 6 +- .../channel_notifications_modal/index.js | 28 +- components/channel_view/index.js | 3 +- .../edit_channel_purpose_modal.test.jsx.snap | 7 - .../edit_channel_purpose_modal.jsx | 24 +- .../edit_channel_purpose_modal.test.jsx | 16 + .../modal_controller/modal_controller.jsx | 18 +- .../navbar/__snapshots__/navbar.test.jsx.snap | 1974 ----------------- components/navbar/index.js | 56 - components/navbar/navbar.jsx | 909 -------- components/navbar/navbar.test.jsx | 202 -- .../popover_list_members.jsx | 2 +- .../quick_switch_modal/quick_switch_modal.jsx | 24 +- .../quick_switch_modal.test.jsx | 2 - .../rename_channel_modal.test.jsx.snap | 1 + .../rename_channel_modal.jsx | 14 +- utils/constants.jsx | 4 + 103 files changed, 4457 insertions(+), 5282 deletions(-) create mode 100644 components/channel_header/channel_header.js delete mode 100644 components/channel_header/channel_header.jsx rename components/channel_header/components/__snapshots__/{header_icon_wrapper.test.jsx.snap => header_icon_wrapper.test.js.snap} (100%) rename components/channel_header/components/{header_icon_wrapper.jsx => header_icon_wrapper.js} (98%) rename components/channel_header/components/{header_icon_wrapper.test.jsx => header_icon_wrapper.test.js} (99%) create mode 100644 components/channel_header_dropdown/channel_header_dropdown.js create mode 100644 components/channel_header_dropdown/index.js create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/add_members.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/convert_channel.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/group.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/notification_preferences.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/rename_channel.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/set_channel_header.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/set_channel_purpose.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/view_and_manage_members.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/__snapshots__/view_channel_info.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/add_members.js create mode 100644 components/channel_header_dropdown/menu_items/add_members.test.js create mode 100644 components/channel_header_dropdown/menu_items/close_channel/__snapshots__/close_channel.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/close_channel/close_channel.js create mode 100644 components/channel_header_dropdown/menu_items/close_channel/close_channel.test.js create mode 100644 components/channel_header_dropdown/menu_items/close_channel/index.js create mode 100644 components/channel_header_dropdown/menu_items/convert_channel.js create mode 100644 components/channel_header_dropdown/menu_items/convert_channel.test.js create mode 100644 components/channel_header_dropdown/menu_items/delete_channel/__snapshots__/delete_channel.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/delete_channel/delete_channel.js create mode 100644 components/channel_header_dropdown/menu_items/delete_channel/delete_channel.test.js create mode 100644 components/channel_header_dropdown/menu_items/delete_channel/index.js create mode 100644 components/channel_header_dropdown/menu_items/group.js create mode 100644 components/channel_header_dropdown/menu_items/group.test.js create mode 100644 components/channel_header_dropdown/menu_items/index.js create mode 100644 components/channel_header_dropdown/menu_items/leave_channel/__snapshots__/leave_channel.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/leave_channel/index.js create mode 100644 components/channel_header_dropdown/menu_items/leave_channel/leave_channel.js create mode 100644 components/channel_header_dropdown/menu_items/leave_channel/leave_channel.test.js create mode 100644 components/channel_header_dropdown/menu_items/notification_preferences.js create mode 100644 components/channel_header_dropdown/menu_items/notification_preferences.test.js create mode 100644 components/channel_header_dropdown/menu_items/rename_channel.js create mode 100644 components/channel_header_dropdown/menu_items/rename_channel.test.js create mode 100644 components/channel_header_dropdown/menu_items/set_channel_header.js create mode 100644 components/channel_header_dropdown/menu_items/set_channel_header.test.js create mode 100644 components/channel_header_dropdown/menu_items/set_channel_purpose.js create mode 100644 components/channel_header_dropdown/menu_items/set_channel_purpose.test.js create mode 100644 components/channel_header_dropdown/menu_items/toggle_favorite_channel/__snapshots__/toggle_favorite_channel.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/toggle_favorite_channel/index.js create mode 100644 components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.js create mode 100644 components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.test.js create mode 100644 components/channel_header_dropdown/menu_items/toggle_mute_channel/__snapshots__/toggle_mute_channel.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/toggle_mute_channel/index.js create mode 100644 components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.js create mode 100644 components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.test.js create mode 100644 components/channel_header_dropdown/menu_items/view_and_manage_members.js create mode 100644 components/channel_header_dropdown/menu_items/view_and_manage_members.test.js create mode 100644 components/channel_header_dropdown/menu_items/view_channel_info.js create mode 100644 components/channel_header_dropdown/menu_items/view_channel_info.test.js create mode 100644 components/channel_header_dropdown/menu_items/view_pinned_posts/__snapshots__/view_pinned_posts.test.js.snap create mode 100644 components/channel_header_dropdown/menu_items/view_pinned_posts/index.js create mode 100644 components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.js create mode 100644 components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.test.js create mode 100644 components/channel_header_dropdown/mobile_channel_header_dropdown.js create mode 100644 components/channel_header_mobile/__snapshots__/channel_header_mobile.test.js.snap create mode 100644 components/channel_header_mobile/channel_header_mobile.js create mode 100644 components/channel_header_mobile/channel_header_mobile.test.js rename components/{navbar/navbar_info_button/__snapshots__/navbar_info_button.test.jsx.snap => channel_header_mobile/channel_info_button/__snapshots__/channel_info_button.test.js.snap} (96%) rename components/{navbar/navbar_info_button/navbar_info_button.jsx => channel_header_mobile/channel_info_button/channel_info_button.js} (60%) rename components/{navbar/navbar_info_button/navbar_info_button.test.jsx => channel_header_mobile/channel_info_button/channel_info_button.test.js} (83%) rename components/{navbar/navbar_info_button => channel_header_mobile/channel_info_button}/index.js (75%) create mode 100644 components/channel_header_mobile/collapse_lhs_button/collapse_lhs_button.js create mode 100644 components/channel_header_mobile/collapse_lhs_button/index.js create mode 100644 components/channel_header_mobile/collapse_rhs_button/collapse_rhs_button.js create mode 100644 components/channel_header_mobile/collapse_rhs_button/index.js create mode 100644 components/channel_header_mobile/index.js create mode 100644 components/channel_header_mobile/show_search_button/index.js create mode 100644 components/channel_header_mobile/show_search_button/show_search_button.js create mode 100644 components/channel_header_mobile/unmute_channel_button/__snapshots__/unmute_channel_button.test.js.snap create mode 100644 components/channel_header_mobile/unmute_channel_button/index.js create mode 100644 components/channel_header_mobile/unmute_channel_button/unmute_channel_button.js create mode 100644 components/channel_header_mobile/unmute_channel_button/unmute_channel_button.test.js delete mode 100644 components/navbar/__snapshots__/navbar.test.jsx.snap delete mode 100644 components/navbar/index.js delete mode 100644 components/navbar/navbar.jsx delete mode 100644 components/navbar/navbar.test.jsx diff --git a/components/channel_header/__snapshots__/channel_header.test.jsx.snap b/components/channel_header/__snapshots__/channel_header.test.jsx.snap index 83e29e34f32c..f93050c76d94 100644 --- a/components/channel_header/__snapshots__/channel_header.test.jsx.snap +++ b/components/channel_header/__snapshots__/channel_header.test.jsx.snap @@ -41,141 +41,7 @@ exports[`components/ChannelHeader should render archived view 1`] = ` title="Dropdown Icon" /> - +
- -
`; @@ -338,176 +169,8 @@ exports[`components/ChannelHeader should render correct menu when muted 1`] = ` title="Dropdown Icon" /> - + - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - -
- - `; @@ -677,140 +304,7 @@ exports[`components/ChannelHeader should render properly when populated 1`] = ` title="Dropdown Icon" /> - +
- - `; @@ -969,148 +430,7 @@ exports[`components/ChannelHeader should render properly when populated with cha title="Dropdown Icon" /> - + - - `; diff --git a/components/channel_header/channel_header.js b/components/channel_header/channel_header.js new file mode 100644 index 000000000000..4ab795df30a0 --- /dev/null +++ b/components/channel_header/channel_header.js @@ -0,0 +1,581 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {OverlayTrigger, Popover, Tooltip} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; +import {Permissions} from 'mattermost-redux/constants'; +import {memoizeResult} from 'mattermost-redux/utils/helpers'; + +import 'bootstrap'; + +import Markdown from 'components/markdown'; +import PopoverListMembers from 'components/popover_list_members'; +import SearchBar from 'components/search_bar'; +import StatusIcon from 'components/status_icon'; +import FlagIcon from 'components/svg/flag_icon'; +import MentionsIcon from 'components/svg/mentions_icon'; +import PinIcon from 'components/svg/pin_icon'; +import SearchIcon from 'components/svg/search_icon'; +import ArchiveIcon from 'components/svg/archive_icon'; +import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; +import QuickSwitchModal from 'components/quick_switch_modal'; +import {ChannelHeaderDropdown} from 'components/channel_header_dropdown'; + +import { + Constants, + ModalIdentifiers, + NotificationLevels, + RHSStates, +} from 'utils/constants'; +import * as Utils from 'utils/utils'; + +import ChannelHeaderPlug from 'plugins/channel_header_plug'; + +import HeaderIconWrapper from './components/header_icon_wrapper'; + +const headerMarkdownOptions = {singleline: true, mentionHighlight: false, atMentions: true}; +const popoverMarkdownOptions = {singleline: false, mentionHighlight: false, atMentions: true}; + +const SEARCH_BAR_MINIMUM_WINDOW_SIZE = 1140; + +export default class ChannelHeader extends React.PureComponent { + static propTypes = { + teamId: PropTypes.string.isRequired, + currentUser: PropTypes.object.isRequired, + channel: PropTypes.object, + channelMember: PropTypes.object, + dmUser: PropTypes.object, + isFavorite: PropTypes.bool, + isReadOnly: PropTypes.bool, + isMuted: PropTypes.bool, + rhsState: PropTypes.oneOf( + Object.values(RHSStates), + ), + actions: PropTypes.shape({ + favoriteChannel: PropTypes.func.isRequired, + unfavoriteChannel: PropTypes.func.isRequired, + showFlaggedPosts: PropTypes.func.isRequired, + showPinnedPosts: PropTypes.func.isRequired, + showMentions: PropTypes.func.isRequired, + closeRightHandSide: PropTypes.func.isRequired, + updateRhsState: PropTypes.func.isRequired, + getCustomEmojisInText: PropTypes.func.isRequired, + updateChannelNotifyProps: PropTypes.func.isRequired, + goToLastViewedChannel: PropTypes.func.isRequired, + openModal: PropTypes.func.isRequired, + closeModal: PropTypes.func.isRequired, + }).isRequired, + }; + + static defaultProps = { + dmUser: {}, + }; + + constructor(props) { + super(props); + + const showSearchBar = Utils.windowWidth() > SEARCH_BAR_MINIMUM_WINDOW_SIZE; + this.state = { + showSearchBar, + showQuickSwitch: false, + }; + + this.getHeaderMarkdownOptions = memoizeResult((channelNamesMap) => ( + {...headerMarkdownOptions, channelNamesMap} + )); + this.getPopoverMarkdownOptions = memoizeResult((channelNamesMap) => ( + {...popoverMarkdownOptions, channelNamesMap} + )); + } + + componentDidMount() { + this.props.actions.getCustomEmojisInText(this.props.channel ? this.props.channel.header : ''); + document.addEventListener('keydown', this.handleShortcut); + document.addEventListener('keydown', this.handleQuickSwitchKeyPress); + window.addEventListener('resize', this.handleResize); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleShortcut); + document.removeEventListener('keydown', this.handleQuickSwitchKeyPress); + window.removeEventListener('resize', this.handleResize); + } + + componentDidUpdate(prevProps) { + const header = this.props.channel ? this.props.channel.header : ''; + const prevHeader = prevProps.channel ? prevProps.channel.header : ''; + if (header !== prevHeader) { + this.props.actions.getCustomEmojisInText(header); + } + } + + handleResize = () => { + const windowWidth = Utils.windowWidth(); + + this.setState({showSearchBar: windowWidth > SEARCH_BAR_MINIMUM_WINDOW_SIZE}); + }; + + handleClose = () => { + this.props.actions.goToLastViewedChannel(); + }; + + toggleFavorite = () => { + if (this.props.isFavorite) { + this.props.actions.unfavoriteChannel(this.props.channel.id); + } else { + this.props.actions.favoriteChannel(this.props.channel.id); + } + }; + + unmute = () => { + const {actions, channel, channelMember, currentUser} = this.props; + + if (!channelMember || !currentUser || !channel) { + return; + } + + const options = {mark_unread: NotificationLevels.ALL}; + actions.updateChannelNotifyProps(currentUser.id, channel.id, options); + }; + + mute = () => { + const {actions, channel, channelMember, currentUser} = this.props; + + if (!channelMember || !currentUser || !channel) { + return; + } + + const options = {mark_unread: NotificationLevels.MENTION}; + actions.updateChannelNotifyProps(currentUser.id, channel.id, options); + }; + + searchMentions = (e) => { + e.preventDefault(); + if (this.props.rhsState === RHSStates.MENTION) { + this.props.actions.closeRightHandSide(); + } else { + this.props.actions.showMentions(); + } + }; + + getPinnedPosts = (e) => { + e.preventDefault(); + if (this.props.rhsState === RHSStates.PIN) { + this.props.actions.closeRightHandSide(); + } else { + this.props.actions.showPinnedPosts(); + } + }; + + getFlagged = (e) => { + e.preventDefault(); + if (this.props.rhsState === RHSStates.FLAG) { + this.props.actions.closeRightHandSide(); + } else { + this.props.actions.showFlaggedPosts(); + } + }; + + searchButtonClick = (e) => { + e.preventDefault(); + this.props.actions.updateRhsState(RHSStates.SEARCH); + }; + + handleShortcut = (e) => { + if (Utils.cmdOrCtrlPressed(e) && e.shiftKey) { + if (Utils.isKeyPressed(e, Constants.KeyCodes.M)) { + e.preventDefault(); + this.searchMentions(e); + } + } + }; + + handleOnMouseOver = () => { + if (this.refs.headerOverlay) { + this.refs.headerOverlay.show(); + } + }; + + handleOnMouseOut = () => { + if (this.refs.headerOverlay) { + this.refs.headerOverlay.hide(); + } + }; + + handleQuickSwitchKeyPress = (e) => { + if (Utils.cmdOrCtrlPressed(e) && !e.shiftKey && Utils.isKeyPressed(e, Constants.KeyCodes.K)) { + if (!e.altKey) { + e.preventDefault(); + this.toggleQuickSwitchModal(); + } + } + } + + toggleQuickSwitchModal = () => { + const {showQuickSwitch} = this.state; + if (showQuickSwitch) { + this.props.actions.openModal({ + modalId: ModalIdentifiers.QUICK_SWITCH, + dialogType: QuickSwitchModal, + }); + } else { + this.props.actions.closeModal(ModalIdentifiers.QUICK_SWITCH); + } + + this.setState({showQuickSwitch: !showQuickSwitch}); + } + + render() { + const { + teamId, + currentUser, + channel, + channelMember, + isMuted: channelMuted, + isReadOnly, + isFavorite, + dmUser, + rhsState, + } = this.props; + + const channelIsArchived = channel.delete_at !== 0; + if (Utils.isEmptyObject(channel) || + Utils.isEmptyObject(channelMember) || + Utils.isEmptyObject(currentUser)) { + // Use an empty div to make sure the header's height stays constant + return ( +
+ ); + } + + const channelNamesMap = channel.props && channel.props.channel_mentions; + + const popoverContent = ( + + + + ); + + let channelTitle = channel.display_name; + let archivedIcon = null; + if (channelIsArchived) { + archivedIcon = (); + } + const isDirect = (channel.type === Constants.DM_CHANNEL); + const isGroup = (channel.type === Constants.GM_CHANNEL); + const isPrivate = (channel.type === Constants.PRIVATE_CHANNEL); + + if (isDirect) { + const teammateId = dmUser.id; + if (currentUser.id === teammateId) { + channelTitle = ( + + ); + } else { + channelTitle = Utils.getDisplayNameByUserId(teammateId) + ' '; + } + } + + let popoverListMembers; + if (!isDirect) { + popoverListMembers = ( + + ); + } + + let dmHeaderIconStatus; + let dmHeaderTextStatus; + if (channel.type === Constants.DM_CHANNEL && !dmUser.delete_at) { + dmHeaderIconStatus = ( + + ); + + dmHeaderTextStatus = ( + + + + ); + } + + let headerTextContainer; + if (channel.header) { + headerTextContainer = ( + +
+ {dmHeaderIconStatus} + {dmHeaderTextStatus} + + + +
+
+ ); + } else { + let editMessage; + if (!isReadOnly && !channelIsArchived) { + if (isDirect || isGroup) { + editMessage = ( + + ); + } else { + editMessage = ( + + + + ); + } + } + headerTextContainer = ( +
+ {dmHeaderIconStatus} + {dmHeaderTextStatus} + {editMessage} +
+ ); + } + + let toggleFavoriteTooltip; + let toggleFavorite = null; + if (!channelIsArchived) { + if (isFavorite) { + toggleFavoriteTooltip = ( + + + + ); + } else { + toggleFavoriteTooltip = ( + + + + ); + } + + toggleFavorite = ( + + + + ); + } + + const channelMutedTooltip = ( + + + + ); + + let muteTrigger; + if (channelMuted) { + muteTrigger = ( + + + + ); + } + + let pinnedIconClass = 'channel-header__icon'; + if (rhsState === RHSStates.PIN) { + pinnedIconClass += ' active'; + } + + return ( +
+
+
+
+
+ {toggleFavorite} +

+ + +

+ {muteTrigger} +
+ {headerTextContainer} +
+
+
+ {popoverListMembers} +
+ +
+
+ ); + } +} diff --git a/components/channel_header/channel_header.jsx b/components/channel_header/channel_header.jsx deleted file mode 100644 index e03dd46eed9d..000000000000 --- a/components/channel_header/channel_header.jsx +++ /dev/null @@ -1,1143 +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 {OverlayTrigger, Popover, Tooltip} from 'react-bootstrap'; -import {FormattedMessage} from 'react-intl'; -import {Permissions} from 'mattermost-redux/constants'; -import {memoizeResult} from 'mattermost-redux/utils/helpers'; - -import 'bootstrap'; - -import {isChannelMuted} from 'mattermost-redux/utils/channel_utils'; - -import * as GlobalActions from 'actions/global_actions.jsx'; - -import Markdown from 'components/markdown'; -import {Constants, NotificationLevels, RHSStates, ModalIdentifiers} from 'utils/constants.jsx'; -import * as Utils from 'utils/utils.jsx'; -import ChannelInfoModal from 'components/channel_info_modal'; -import ChannelInviteModal from 'components/channel_invite_modal'; -import ChannelMembersModal from 'components/channel_members_modal'; -import ChannelNotificationsModal from 'components/channel_notifications_modal'; -import ConvertChannelModal from 'components/convert_channel_modal'; -import DeleteChannelModal from 'components/delete_channel_modal'; -import EditChannelHeaderModal from 'components/edit_channel_header_modal'; -import EditChannelPurposeModal from 'components/edit_channel_purpose_modal'; -import MoreDirectChannels from 'components/more_direct_channels'; -import PopoverListMembers from 'components/popover_list_members'; -import RenameChannelModal from 'components/rename_channel_modal'; -import SearchBar from 'components/search_bar'; -import StatusIcon from 'components/status_icon.jsx'; -import FlagIcon from 'components/svg/flag_icon'; -import MentionsIcon from 'components/svg/mentions_icon'; -import PinIcon from 'components/svg/pin_icon'; -import SearchIcon from 'components/svg/search_icon'; -import ArchiveIcon from 'components/svg/archive_icon'; -import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; -import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; -import TeamPermissionGate from 'components/permissions_gates/team_permission_gate'; - -import ChannelHeaderPlug from 'plugins/channel_header_plug'; - -import HeaderIconWrapper from './components/header_icon_wrapper'; - -const headerMarkdownOptions = {singleline: true, mentionHighlight: false, atMentions: true}; -const popoverMarkdownOptions = {singleline: false, mentionHighlight: false, atMentions: true}; - -const SEARCH_BAR_MINIMUM_WINDOW_SIZE = 1140; - -export default class ChannelHeader extends React.Component { - static propTypes = { - actions: PropTypes.shape({ - leaveChannel: PropTypes.func.isRequired, - favoriteChannel: PropTypes.func.isRequired, - unfavoriteChannel: PropTypes.func.isRequired, - showFlaggedPosts: PropTypes.func.isRequired, - showPinnedPosts: PropTypes.func.isRequired, - showMentions: PropTypes.func.isRequired, - closeRightHandSide: PropTypes.func.isRequired, - updateRhsState: PropTypes.func.isRequired, - openModal: PropTypes.func.isRequired, - getCustomEmojisInText: PropTypes.func.isRequired, - updateChannelNotifyProps: PropTypes.func.isRequired, - goToLastViewedChannel: PropTypes.func.isRequired, - }).isRequired, - channel: PropTypes.object.isRequired, - channelMember: PropTypes.object.isRequired, - isFavorite: PropTypes.bool, - isDefault: PropTypes.bool, - currentUser: PropTypes.object.isRequired, - dmUser: PropTypes.object, - isReadOnly: PropTypes.bool, - rhsState: PropTypes.oneOf( - Object.values(RHSStates) - ), - penultimateViewedChannelName: PropTypes.string.isRequired, - }; - - static defaultProps = { - dmUser: {}, - }; - - constructor(props) { - super(props); - - const showSearchBar = Utils.windowWidth() > SEARCH_BAR_MINIMUM_WINDOW_SIZE; - this.state = { - showSearchBar, - showEditChannelHeaderModal: false, - showEditChannelPurposeModal: false, - showMembersModal: false, - showRenameChannelModal: false, - showChannelNotificationsModal: false, - }; - - this.getHeaderMarkdownOptions = memoizeResult((channelNamesMap) => ( - {...headerMarkdownOptions, channelNamesMap} - )); - this.getPopoverMarkdownOptions = memoizeResult((channelNamesMap) => ( - {...popoverMarkdownOptions, channelNamesMap} - )); - } - - componentDidMount() { - this.props.actions.getCustomEmojisInText(this.props.channel.header); - document.addEventListener('keydown', this.handleShortcut); - window.addEventListener('resize', this.handleResize); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleShortcut); - window.removeEventListener('resize', this.handleResize); - } - - UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase - if (this.props.channel.id !== nextProps.channel.id) { - this.props.actions.getCustomEmojisInText(nextProps.channel.header); - } - } - - handleResize = () => { - const windowWidth = Utils.windowWidth(); - - this.setState({showSearchBar: windowWidth > SEARCH_BAR_MINIMUM_WINDOW_SIZE}); - }; - - handleLeave = () => { - if (this.props.channel.type === Constants.PRIVATE_CHANNEL) { - GlobalActions.showLeavePrivateChannelModal(this.props.channel); - } else { - this.props.actions.leaveChannel(this.props.channel.id); - } - }; - - handleClose = () => { - this.props.actions.goToLastViewedChannel(); - }; - - toggleFavorite = () => { - if (this.props.isFavorite) { - this.props.actions.unfavoriteChannel(this.props.channel.id); - } else { - this.props.actions.favoriteChannel(this.props.channel.id); - } - }; - - unmute = () => { - const {actions, channel, channelMember, currentUser} = this.props; - - if (!channelMember || !currentUser || !channel) { - return; - } - - const options = {mark_unread: NotificationLevels.ALL}; - actions.updateChannelNotifyProps(currentUser.id, channel.id, options); - }; - - mute = () => { - const {actions, channel, channelMember, currentUser} = this.props; - - if (!channelMember || !currentUser || !channel) { - return; - } - - const options = {mark_unread: NotificationLevels.MENTION}; - actions.updateChannelNotifyProps(currentUser.id, channel.id, options); - }; - - searchMentions = (e) => { - e.preventDefault(); - if (this.props.rhsState === RHSStates.MENTION) { - this.props.actions.closeRightHandSide(); - } else { - this.props.actions.showMentions(); - } - }; - - getPinnedPosts = (e) => { - e.preventDefault(); - if (this.props.rhsState === RHSStates.PIN) { - this.props.actions.closeRightHandSide(); - } else { - this.props.actions.showPinnedPosts(); - } - }; - - getFlagged = (e) => { - e.preventDefault(); - if (this.props.rhsState === RHSStates.FLAG) { - this.props.actions.closeRightHandSide(); - } else { - this.props.actions.showFlaggedPosts(); - } - }; - - searchButtonClick = (e) => { - e.preventDefault(); - this.props.actions.updateRhsState(RHSStates.SEARCH); - }; - - handleShortcut = (e) => { - if (Utils.cmdOrCtrlPressed(e) && e.shiftKey) { - if (Utils.isKeyPressed(e, Constants.KeyCodes.M)) { - e.preventDefault(); - this.searchMentions(e); - } - } - }; - - showRenameChannelModal = (e) => { - e.preventDefault(); - - this.setState({ - showRenameChannelModal: true, - }); - }; - - hideRenameChannelModal = () => { - this.setState({ - showRenameChannelModal: false, - }); - }; - - showChannelNotificationsModal = (e) => { - e.preventDefault(); - - this.setState({ - showChannelNotificationsModal: true, - }); - }; - - hideChannelNotificationsModal = () => { - this.setState({ - showChannelNotificationsModal: false, - }); - }; - - handleOnMouseOver = () => { - if (this.refs.headerOverlay) { - this.refs.headerOverlay.show(); - } - }; - - handleOnMouseOut = () => { - if (this.refs.headerOverlay) { - this.refs.headerOverlay.hide(); - } - }; - - showMembersModal = () => { - this.setState({showMembersModal: true}); - }; - - hideMembersModal = () => { - this.setState({showMembersModal: false}); - }; - - showEditChannelPurposeModal = () => { - this.setState({showEditChannelPurposeModal: true}); - }; - - hideEditChannelPurposeModal = () => { - this.setState({showEditChannelPurposeModal: false}); - }; - - hideEditChannelHeaderModal = () => { - this.setState({showEditChannelHeaderModal: false}); - }; - - showEditChannelHeaderModal = () => { - this.setState({showEditChannelHeaderModal: true}); - }; - - showInviteModal = () => { - const {channel, currentUser, actions} = this.props; - const inviteModalData = { - modalId: ModalIdentifiers.CHANNEL_INVITE, - dialogType: ChannelInviteModal, - dialogProps: {channel, currentUser}, - }; - - actions.openModal(inviteModalData); - }; - - renderMute = () => { - const channelMuted = isChannelMuted(this.props.channelMember); - - if (channelMuted) { - return ( -
  • - -
  • - ); - } - - return ( -
  • - -
  • - ); - }; - - render() { - const channelIsArchived = this.props.channel.delete_at !== 0; - if (Utils.isEmptyObject(this.props.channel) || - Utils.isEmptyObject(this.props.channelMember) || - Utils.isEmptyObject(this.props.currentUser)) { - // Use an empty div to make sure the header's height stays constant - return ( -
    - ); - } - - const channel = this.props.channel; - const channelNamesMap = this.props.channel.props && this.props.channel.props.channel_mentions; - - const popoverContent = ( - - - - ); - - let channelTitle = channel.display_name; - let archivedIcon = null; - if (channelIsArchived) { - archivedIcon = (); - } - const isDirect = (this.props.channel.type === Constants.DM_CHANNEL); - const isGroup = (this.props.channel.type === Constants.GM_CHANNEL); - const isPrivate = (this.props.channel.type === Constants.PRIVATE_CHANNEL); - - const channelMuted = isChannelMuted(this.props.channelMember); - - const teamId = this.props.channel.team_id; - - if (isDirect) { - const teammateId = Utils.getUserIdFromChannelName(channel); - if (this.props.currentUser.id === teammateId) { - channelTitle = ( - - ); - } else { - channelTitle = Utils.getDisplayNameByUserId(teammateId) + ' '; - } - } - - let popoverListMembers; - if (!isDirect) { - popoverListMembers = ( - - ); - } - - const dropdownContents = []; - if (isDirect) { - dropdownContents.push( -
  • - - - -
  • - ); - } else if (isGroup) { - dropdownContents.push( -
  • - - - -
  • - ); - - dropdownContents.push( -
  • - -
  • - ); - - dropdownContents.push(this.renderMute()); - - dropdownContents.push( -
  • - - - -
  • - ); - } else { - dropdownContents.push( -
  • - - - -
  • - ); - - if (this.props.isDefault || channelIsArchived) { - dropdownContents.push( -
  • - -
  • - ); - } - - if (!channelIsArchived) { - dropdownContents.push( -
  • - -
  • - ); - } - - dropdownContents.push(this.renderMute()); - - if (!this.props.isDefault) { - dropdownContents.push( -
  • - ); - - if (!channelIsArchived) { - dropdownContents.push( - -
  • - - - -
  • - - ); - dropdownContents.push( - -
  • - -
  • -
    - ); - } - - dropdownContents.push( - -
  • - -
  • -
    - ); - } - - if (!this.props.isReadOnly && !channelIsArchived) { - dropdownContents.push( - -
  • - -
  • - - - -
  • - -
  • - -
  • - -
  • - -
  • -
    - ); - } - - if (!this.props.isDefault && channel.type === Constants.OPEN_CHANNEL && !channelIsArchived) { - dropdownContents.push( - -
  • - - - -
  • -
    - ); - } - - if (!this.props.isDefault && !channelIsArchived) { - dropdownContents.push( - -
  • - - - -
  • -
    - ); - - dropdownContents.push( -
  • - ); - } - - if (!this.props.isDefault) { - dropdownContents.push( -
  • - -
  • - ); - } - - if (channelIsArchived) { - dropdownContents.push( -
  • - -
  • - ); - } - } - - let dmHeaderIconStatus; - let dmHeaderTextStatus; - if (channel.type === Constants.DM_CHANNEL && !this.props.dmUser.delete_at) { - dmHeaderIconStatus = ( - - ); - - dmHeaderTextStatus = ( - - - - ); - } - - let headerTextContainer; - if (channel.header) { - headerTextContainer = ( - -
    - {dmHeaderIconStatus} - {dmHeaderTextStatus} - - - -
    -
    - ); - } else { - let editMessage; - if (!this.props.isReadOnly && !channelIsArchived) { - if (isDirect || isGroup) { - editMessage = ( - - ); - } else { - editMessage = ( - - - - ); - } - } - headerTextContainer = ( -
    - {dmHeaderIconStatus} - {dmHeaderTextStatus} - {editMessage} -
    - ); - } - - let editHeaderModal; - if (this.state.showEditChannelHeaderModal) { - editHeaderModal = ( - - ); - } - - let toggleFavoriteTooltip; - let toggleFavorite = null; - if (!channelIsArchived) { - if (this.props.isFavorite) { - toggleFavoriteTooltip = ( - - - - ); - } else { - toggleFavoriteTooltip = ( - - - - ); - } - - toggleFavorite = ( - - - - ); - } - - const channelMutedTooltip = ( - - - - ); - - let muteTrigger; - if (channelMuted) { - muteTrigger = ( - - - - ); - } - - let channelMembersModal; - if (this.state.showMembersModal) { - channelMembersModal = ( - - ); - } - - let editPurposeModal; - if (this.state.showEditChannelPurposeModal) { - editPurposeModal = ( - - ); - } - - let pinnedIconClass = 'channel-header__icon'; - if (this.props.rhsState === RHSStates.PIN) { - pinnedIconClass += ' active'; - } - - return ( -
    -
    -
    -
    -
    - {toggleFavorite} -

    - - -

    - {muteTrigger} -
    - {headerTextContainer} -
    -
    -
    - {popoverListMembers} -
    - -
    - {editHeaderModal} - {editPurposeModal} - {channelMembersModal} - - -
    - ); - } -} diff --git a/components/channel_header/channel_header.test.jsx b/components/channel_header/channel_header.test.jsx index e1cb4421fea5..41d5f8b9aa0a 100644 --- a/components/channel_header/channel_header.test.jsx +++ b/components/channel_header/channel_header.test.jsx @@ -19,10 +19,13 @@ describe('components/ChannelHeader', () => { closeRightHandSide: jest.fn(), updateRhsState: jest.fn(), openModal: jest.fn(), + closeModal: jest.fn(), getCustomEmojisInText: jest.fn(), updateChannelNotifyProps: jest.fn(), goToLastViewedChannel: jest.fn(), }, + teamUrl: 'team_url', + teamId: 'team_id', channel: {}, channelMember: {}, currentUser: {}, diff --git a/components/channel_header/components/__snapshots__/header_icon_wrapper.test.jsx.snap b/components/channel_header/components/__snapshots__/header_icon_wrapper.test.js.snap similarity index 100% rename from components/channel_header/components/__snapshots__/header_icon_wrapper.test.jsx.snap rename to components/channel_header/components/__snapshots__/header_icon_wrapper.test.js.snap diff --git a/components/channel_header/components/header_icon_wrapper.jsx b/components/channel_header/components/header_icon_wrapper.js similarity index 98% rename from components/channel_header/components/header_icon_wrapper.jsx rename to components/channel_header/components/header_icon_wrapper.js index 2d77dd1b3cdf..2088247bebd6 100644 --- a/components/channel_header/components/header_icon_wrapper.jsx +++ b/components/channel_header/components/header_icon_wrapper.js @@ -6,7 +6,7 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import {OverlayTrigger, Tooltip} from 'react-bootstrap'; -import {Constants} from 'utils/constants.jsx'; +import {Constants} from 'utils/constants'; import {t} from 'utils/i18n'; export default function HeaderIconWrapper({ diff --git a/components/channel_header/components/header_icon_wrapper.test.jsx b/components/channel_header/components/header_icon_wrapper.test.js similarity index 99% rename from components/channel_header/components/header_icon_wrapper.test.jsx rename to components/channel_header/components/header_icon_wrapper.test.js index d73bb8c3ec0e..db130a1ac8b3 100644 --- a/components/channel_header/components/header_icon_wrapper.test.jsx +++ b/components/channel_header/components/header_icon_wrapper.test.js @@ -9,7 +9,7 @@ import MentionsIcon from 'components/svg/mentions_icon'; import PinIcon from 'components/svg/pin_icon'; import SearchIcon from 'components/svg/search_icon'; -import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper.jsx'; +import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper'; describe('components/channel_header/components/HeaderIconWrapper', () => { function emptyFunction() {} //eslint-disable-line no-empty-function diff --git a/components/channel_header/index.js b/components/channel_header/index.js index 3e3a7e8006a3..f7a9988e2b38 100644 --- a/components/channel_header/index.js +++ b/components/channel_header/index.js @@ -1,24 +1,32 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {favoriteChannel, leaveChannel, unfavoriteChannel, updateChannelNotifyProps} from 'mattermost-redux/actions/channels'; +import {connect} from 'react-redux'; +import {withRouter} from 'react-router-dom'; +import { + favoriteChannel, + unfavoriteChannel, + updateChannelNotifyProps, +} from 'mattermost-redux/actions/channels'; import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis'; import {General} from 'mattermost-redux/constants'; -import {getChannel, getMyChannelMember, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels'; -import {getMyTeamMember} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentUser, getUser} from 'mattermost-redux/selectors/entities/users'; -import {getUserIdFromChannelName, isDefault, isFavoriteChannel} from 'mattermost-redux/utils/channel_utils'; -import {getLicense} from 'mattermost-redux/selectors/entities/general'; - -import {withRouter} from 'react-router-dom'; +import { + getCurrentChannel, + getMyCurrentChannelMembership, + isCurrentChannelFavorite, + isCurrentChannelMuted, + isCurrentChannelReadOnly, +} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import { + getCurrentUser, + getUser, +} from 'mattermost-redux/selectors/entities/users'; +import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils'; import {goToLastViewedChannel} from 'actions/views/channel'; - -import {getPenultimateViewedChannelName} from 'selectors/local_storage'; -import {Constants} from 'utils/constants.jsx'; - +import {openModal, closeModal} from 'actions/views/modals'; import { showFlaggedPosts, showPinnedPosts, @@ -26,14 +34,12 @@ import { closeRightHandSide, updateRhsState, } from 'actions/views/rhs'; -import {openModal} from 'actions/views/modals'; import {getRhsState} from 'selectors/rhs'; -import ChannelHeader from './channel_header.jsx'; +import ChannelHeader from './channel_header'; -function mapStateToProps(state, ownProps) { - const channel = getChannel(state, ownProps.channelId) || {}; - const prefs = state.entities.preferences.myPreferences; +const mapStateToProps = (state) => { + const channel = getCurrentChannel(state) || {}; const user = getCurrentUser(state); let dmUser; @@ -42,45 +48,34 @@ function mapStateToProps(state, ownProps) { dmUser = getUser(state, dmUserId); } - const license = getLicense(state); - - let penultimateViewedChannelName = getPenultimateViewedChannelName(state); - if (!penultimateViewedChannelName) { - penultimateViewedChannelName = Constants.DEFAULT_CHANNEL; - } - return { + teamId: getCurrentTeamId(state), channel, - channelMember: getMyChannelMember(state, ownProps.channelId), - teamMember: getMyTeamMember(state, channel.team_id), - isFavorite: isFavoriteChannel(prefs, ownProps.channelId), - isDefault: isDefault(channel), + channelMember: getMyCurrentChannelMembership(state), currentUser: user, dmUser, rhsState: getRhsState(state), - isLicensed: license.IsLicensed === 'true', + isFavorite: isCurrentChannelFavorite(state), isReadOnly: isCurrentChannelReadOnly(state), - penultimateViewedChannelName, + isMuted: isCurrentChannelMuted(state), }; -} +}; -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - leaveChannel, - favoriteChannel, - unfavoriteChannel, - showFlaggedPosts, - showPinnedPosts, - showMentions, - closeRightHandSide, - updateRhsState, - openModal, - getCustomEmojisInText, - updateChannelNotifyProps, - goToLastViewedChannel, - }, dispatch), - }; -} +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + favoriteChannel, + unfavoriteChannel, + showFlaggedPosts, + showPinnedPosts, + showMentions, + closeRightHandSide, + updateRhsState, + getCustomEmojisInText, + updateChannelNotifyProps, + goToLastViewedChannel, + openModal, + closeModal, + }, dispatch), +}); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChannelHeader)); diff --git a/components/channel_header_dropdown/channel_header_dropdown.js b/components/channel_header_dropdown/channel_header_dropdown.js new file mode 100644 index 000000000000..9a81dabe2e57 --- /dev/null +++ b/components/channel_header_dropdown/channel_header_dropdown.js @@ -0,0 +1,100 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +import * as MenuItem from './menu_items'; + +export default class ChannelHeaderDropdown extends React.PureComponent { + static propTypes = { + user: PropTypes.object.isRequired, + channel: PropTypes.object.isRequired, + isDefault: PropTypes.bool.isRequired, + isReadonly: PropTypes.bool.isRequired, + isMuted: PropTypes.bool.isRequired, + isArchived: PropTypes.bool.isRequired, + } + + render() { + const { + user, + channel, + isDefault, + isMuted, + isReadonly, + isArchived, + } = this.props; + + return ( + + ); + } +} diff --git a/components/channel_header_dropdown/index.js b/components/channel_header_dropdown/index.js new file mode 100644 index 000000000000..71aaacd4938b --- /dev/null +++ b/components/channel_header_dropdown/index.js @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; +import {createSelector} from 'reselect'; +import { + getCurrentUser, + getUserStatuses, +} from 'mattermost-redux/selectors/entities/users'; +import { + getCurrentChannel, + isCurrentChannelDefault, + isCurrentChannelFavorite, + isCurrentChannelMuted, + isCurrentChannelArchived, + isCurrentChannelReadOnly, +} from 'mattermost-redux/selectors/entities/channels'; + +import {Constants} from 'utils/constants'; +import * as Utils from 'utils/utils'; + +import Desktop from './channel_header_dropdown'; +import Mobile from './mobile_channel_header_dropdown'; + +const getTeammateId = createSelector( + getCurrentChannel, + (channel) => { + if (channel.type !== Constants.DM_CHANNEL) { + return null; + } + + return Utils.getUserIdFromChannelName(channel); + }, +); + +const getTeammateStatus = createSelector( + getUserStatuses, + getTeammateId, + (userStatuses, teammateId) => { + if (!teammateId) { + return null; + } + + return userStatuses[teammateId]; + } +); + +const mapStateToProps = (state) => ({ + user: getCurrentUser(state), + channel: getCurrentChannel(state), + isDefault: isCurrentChannelDefault(state), + isFavorite: isCurrentChannelFavorite(state), + isMuted: isCurrentChannelMuted(state), + isReadonly: isCurrentChannelReadOnly(state), + isArchived: isCurrentChannelArchived(state), + teammateId: getTeammateId(state), + teammateStatus: getTeammateStatus(state), +}); + +export const ChannelHeaderDropdown = connect(mapStateToProps)(Desktop); +export const MobileChannelHeaderDropdown = connect(mapStateToProps)(Mobile); diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/add_members.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/add_members.test.js.snap new file mode 100644 index 000000000000..9a302e6edace --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/add_members.test.js.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.AddMembers should match snapshot 1`] = ` + +
  • + + + +
  • +
    +`; diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/convert_channel.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/convert_channel.test.js.snap new file mode 100644 index 000000000000..0f03bf96cf7a --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/convert_channel.test.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.ConvertChannel should match snapshot 1`] = ` + +
  • + + + +
  • +
    +`; diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/group.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/group.test.js.snap new file mode 100644 index 000000000000..53ca218fe14c --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/group.test.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.Group should match snapshot 1`] = ` + + children +
  • + +`; diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/notification_preferences.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/notification_preferences.test.js.snap new file mode 100644 index 000000000000..78f9c7a6d730 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/notification_preferences.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.NotificationPreferences should match snapshot 1`] = ` +
  • + + + +
  • +`; diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/rename_channel.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/rename_channel.test.js.snap new file mode 100644 index 000000000000..4c741b85814c --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/rename_channel.test.js.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.RenameChannel should match snapshot 1`] = ` + +
  • + + + +
  • +
    +`; diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/set_channel_header.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/set_channel_header.test.js.snap new file mode 100644 index 000000000000..2b16514514dc --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/set_channel_header.test.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.SetChannelHeader should match snapshot 1`] = ` + +
  • + + + +
  • +
    +`; diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/set_channel_purpose.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/set_channel_purpose.test.js.snap new file mode 100644 index 000000000000..f30995d27fa6 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/set_channel_purpose.test.js.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.SetChannelPurpose should match snapshot 1`] = ` + +
  • + + + +
  • +
    +`; diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/view_and_manage_members.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/view_and_manage_members.test.js.snap new file mode 100644 index 000000000000..e7983ad03c2d --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/view_and_manage_members.test.js.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.ViewAndManageMembers should match snapshot 1`] = ` +
  • + + + + + + + + +
  • +`; + +exports[`components/ChannelHeaderDropdown/MenuItem.ViewAndManageMembers should match snapshot when the channel is town square 1`] = ` +
  • + + + + + +
  • +`; diff --git a/components/channel_header_dropdown/menu_items/__snapshots__/view_channel_info.test.js.snap b/components/channel_header_dropdown/menu_items/__snapshots__/view_channel_info.test.js.snap new file mode 100644 index 000000000000..098ebcf95a80 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/__snapshots__/view_channel_info.test.js.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.ViewChannelInfo should match snapshot 1`] = ` +
  • + + + +
  • +`; diff --git a/components/channel_header_dropdown/menu_items/add_members.js b/components/channel_header_dropdown/menu_items/add_members.js new file mode 100644 index 000000000000..01c3480901e4 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/add_members.js @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {Permissions} from 'mattermost-redux/constants'; + +import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import ChannelInviteModal from 'components/channel_invite_modal'; +import {Constants, ModalIdentifiers} from 'utils/constants'; + +const AddMembers = ({channel, isDefault, isArchived}) => { + if (isDefault) { + return null; + } + + if (isArchived) { + return null; + } + + if (channel.type === Constants.DM_CHANNEL) { + return null; + } + + if (channel.type === Constants.GM_CHANNEL) { + return null; + } + + const isPrivate = channel.type === Constants.PRIVATE_CHANNEL; + const permission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS : Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS; + + return ( + +
  • + + + +
  • +
    + ); +}; + +AddMembers.propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the channel is default channel + */ + isDefault: PropTypes.bool.isRequired, + + /** + * Boolean whether the channel is archived + */ + isArchived: PropTypes.bool.isRequired, +}; + +export default AddMembers; diff --git a/components/channel_header_dropdown/menu_items/add_members.test.js b/components/channel_header_dropdown/menu_items/add_members.test.js new file mode 100644 index 000000000000..daaef40ac9d0 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/add_members.test.js @@ -0,0 +1,58 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {Constants} from 'utils/constants'; + +import AddMembers from './add_members'; + +describe('components/ChannelHeaderDropdown/MenuItem.AddMembers', () => { + const baseProps = { + channel: { + id: 'channel_id', + team_id: 'team_id', + type: Constants.OPEN_CHANNEL, + }, + isDefault: false, + isArchived: false, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel is default channel', () => { + const props = { + ...baseProps, + isDefault: true, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is archived', () => { + const props = { + ...baseProps, + isArchived: true, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel type is DM or GM', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.DM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.GM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/close_channel/__snapshots__/close_channel.test.js.snap b/components/channel_header_dropdown/menu_items/close_channel/__snapshots__/close_channel.test.js.snap new file mode 100644 index 000000000000..c07e37efade1 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/close_channel/__snapshots__/close_channel.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.CloseChannel should match snapshot 1`] = ` +
  • + +
  • +`; diff --git a/components/channel_header_dropdown/menu_items/close_channel/close_channel.js b/components/channel_header_dropdown/menu_items/close_channel/close_channel.js new file mode 100644 index 000000000000..35dd9706803d --- /dev/null +++ b/components/channel_header_dropdown/menu_items/close_channel/close_channel.js @@ -0,0 +1,40 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; + +export default class CloseChannel extends React.PureComponent { + static propTypes = { + isArchived: PropTypes.bool.isRequired, + actions: PropTypes.shape({ + goToLastViewedChannel: PropTypes.func.isRequired, + }).isRequired, + } + + handleClose = () => { + this.props.actions.goToLastViewedChannel(); + } + + render() { + if (!this.props.isArchived) { + return null; + } + + return ( +
  • + +
  • + ); + } +} diff --git a/components/channel_header_dropdown/menu_items/close_channel/close_channel.test.js b/components/channel_header_dropdown/menu_items/close_channel/close_channel.test.js new file mode 100644 index 000000000000..f23a7f005526 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/close_channel/close_channel.test.js @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import CloseChannel from './close_channel'; + +describe('components/ChannelHeaderDropdown/MenuItem.CloseChannel', () => { + const baseProps = { + isArchived: true, + actions: { + goToLastViewedChannel: jest.fn(), + }, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('shoud be hidden if the channel is not archived', () => { + const props = { + ...baseProps, + isArchived: false, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should runs goToLastViewedChannel function on click', () => { + const props = { + ...baseProps, + actions: { + ...baseProps.actions, + goToLastViewedChannel: jest.fn(), + }, + }; + const wrapper = shallow(); + wrapper.find('button').simulate('click'); + expect(props.actions.goToLastViewedChannel).toHaveBeenCalled(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/close_channel/index.js b/components/channel_header_dropdown/menu_items/close_channel/index.js new file mode 100644 index 000000000000..3884cbe4cf29 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/close_channel/index.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {goToLastViewedChannel} from 'actions/views/channel'; + +import CloseChannel from './close_channel'; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + goToLastViewedChannel, + }, dispatch), +}); + +export default connect(null, mapDispatchToProps)(CloseChannel); diff --git a/components/channel_header_dropdown/menu_items/convert_channel.js b/components/channel_header_dropdown/menu_items/convert_channel.js new file mode 100644 index 000000000000..bdf38764d090 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/convert_channel.js @@ -0,0 +1,67 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {Permissions} from 'mattermost-redux/constants'; + +import {Constants, ModalIdentifiers} from 'utils/constants'; + +import TeamPermissionGate from 'components/permissions_gates/team_permission_gate'; +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import ConvertChannelModal from 'components/convert_channel_modal'; + +const ConvertChannel = ({channel, isDefault, isArchived}) => { + if (isDefault || isArchived) { + return null; + } + + if (channel.type !== Constants.OPEN_CHANNEL) { + return null; + } + + return ( + +
  • + + + +
  • +
    + ); +}; + +ConvertChannel.propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the channel is readonly + */ + isDefault: PropTypes.bool.isRequired, + + /** + * Boolean whether the channel is readonly + */ + isArchived: PropTypes.bool.isRequired, +}; + +export default ConvertChannel; diff --git a/components/channel_header_dropdown/menu_items/convert_channel.test.js b/components/channel_header_dropdown/menu_items/convert_channel.test.js new file mode 100644 index 000000000000..195c89a29a57 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/convert_channel.test.js @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {Constants} from 'utils/constants'; + +import ConvertChannel from './convert_channel'; + +describe('components/ChannelHeaderDropdown/MenuItem.ConvertChannel', () => { + const baseProps = { + channel: { + id: 'channel_id', + team_id: 'team_id', + display_name: 'Test Channel', + type: Constants.OPEN_CHANNEL, + }, + isDefault: false, + isArchived: false, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel is default channel', () => { + const props = { + ...baseProps, + isDefault: true, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is archived', () => { + const props = { + ...baseProps, + isArchived: true, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is not public channel', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.PRIVATE_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.DM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.GM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/delete_channel/__snapshots__/delete_channel.test.js.snap b/components/channel_header_dropdown/menu_items/delete_channel/__snapshots__/delete_channel.test.js.snap new file mode 100644 index 000000000000..d1598dbe7194 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/delete_channel/__snapshots__/delete_channel.test.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.DeleteChannel should match snapshot 1`] = ` + +
  • + + + +
  • +
    +`; diff --git a/components/channel_header_dropdown/menu_items/delete_channel/delete_channel.js b/components/channel_header_dropdown/menu_items/delete_channel/delete_channel.js new file mode 100644 index 000000000000..c212ab71788d --- /dev/null +++ b/components/channel_header_dropdown/menu_items/delete_channel/delete_channel.js @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {Permissions} from 'mattermost-redux/constants'; + +import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import DeleteChannelModal from 'components/delete_channel_modal'; +import {Constants, ModalIdentifiers} from 'utils/constants'; + +const DeleteChannel = ({channel, isDefault, isArchived, penultimateViewedChannelName}) => { + if (isDefault || isArchived) { + return null; + } + + if (channel.type === Constants.DM_CHANNEL) { + return null; + } + + if (channel.type === Constants.GM_CHANNEL) { + return null; + } + + const isPrivate = channel.type === Constants.PRIVATE_CHANNEL; + const permission = isPrivate ? Permissions.DELETE_PRIVATE_CHANNEL : Permissions.DELETE_PUBLIC_CHANNEL; + + return ( + +
  • + + + +
  • +
    + ); +}; + +DeleteChannel.propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the channel is readonly + */ + isDefault: PropTypes.bool.isRequired, + + /** + * Boolean whether the channel is readonly + */ + isArchived: PropTypes.bool.isRequired, + + penultimateViewedChannelName: PropTypes.string.isRequired, +}; + +export default DeleteChannel; diff --git a/components/channel_header_dropdown/menu_items/delete_channel/delete_channel.test.js b/components/channel_header_dropdown/menu_items/delete_channel/delete_channel.test.js new file mode 100644 index 000000000000..b2fdf8240006 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/delete_channel/delete_channel.test.js @@ -0,0 +1,74 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; +import {Permissions} from 'mattermost-redux/constants'; + +import {Constants} from 'utils/constants'; + +import DeleteChannel from './delete_channel'; + +describe('components/ChannelHeaderDropdown/MenuItem.DeleteChannel', () => { + const baseProps = { + channel: { + id: 'channel_id', + team_id: 'team_id', + type: Constants.OPEN_CHANNEL, + }, + isDefault: false, + isArchived: false, + penultimateViewedChannelName: '', + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel is default', () => { + const props = { + ...baseProps, + isDefault: true, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is archived', () => { + const props = { + ...baseProps, + isArchived: true, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is DM or GM', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.DM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.GM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + }); + + it('should requires right permission level by channel type', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.OPEN_CHANNEL; + expect(makeWrapper().prop('permissions')[0]).toBe(Permissions.DELETE_PUBLIC_CHANNEL); + + props.channel.type = Constants.PRIVATE_CHANNEL; + expect(makeWrapper().prop('permissions')[0]).toBe(Permissions.DELETE_PRIVATE_CHANNEL); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/delete_channel/index.js b/components/channel_header_dropdown/menu_items/delete_channel/index.js new file mode 100644 index 000000000000..504c7fe4d85a --- /dev/null +++ b/components/channel_header_dropdown/menu_items/delete_channel/index.js @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; + +import {getPenultimateViewedChannelName} from 'selectors/local_storage'; +import {Constants} from 'utils/constants'; + +import DeleteChannel from './delete_channel'; + +const mapStateToProps = (state) => ({ + penultimateViewedChannelName: getPenultimateViewedChannelName(state) || Constants.DEFAULT_CHANNEL, +}); + +export default connect(mapStateToProps)(DeleteChannel); diff --git a/components/channel_header_dropdown/menu_items/group.js b/components/channel_header_dropdown/menu_items/group.js new file mode 100644 index 000000000000..5c5fe6f71905 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/group.js @@ -0,0 +1,21 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +const DropdownMenuItemGroup = ({children, showDivider = true}) => ( + + {children} + {children.length && showDivider && ( +
  • + )} + +); + +DropdownMenuItemGroup.propTypes = { + children: PropTypes.any, + showDivider: PropTypes.bool, +}; + +export default DropdownMenuItemGroup; diff --git a/components/channel_header_dropdown/menu_items/group.test.js b/components/channel_header_dropdown/menu_items/group.test.js new file mode 100644 index 000000000000..5e02e6904898 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/group.test.js @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import Group from './group'; + +describe('components/ChannelHeaderDropdown/MenuItem.Group', () => { + it('should match snapshot', () => { + const wrapper = shallow({'children'}); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/index.js b/components/channel_header_dropdown/menu_items/index.js new file mode 100644 index 000000000000..47b509343b63 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/index.js @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export {default as Group} from './group'; + +export {default as ViewChannelInfo} from './view_channel_info'; +export {default as ViewPinnedPosts} from './view_pinned_posts'; +export {default as NotificationPreferences} from './notification_preferences'; +export {default as ToggleFavoriteChannel} from './toggle_favorite_channel'; +export {default as ToggleMuteChannel} from './toggle_mute_channel'; +export {default as SetChannelHeader} from './set_channel_header'; +export {default as SetChannelPurpose} from './set_channel_purpose'; +export {default as AddMembers} from './add_members'; +export {default as ViewAndManageMembers} from './view_and_manage_members'; +export {default as ConvertChannel} from './convert_channel'; +export {default as RenameChannel} from './rename_channel'; +export {default as DeleteChannel} from './delete_channel'; +export {default as LeaveChannel} from './leave_channel'; +export {default as CloseChannel} from './close_channel'; diff --git a/components/channel_header_dropdown/menu_items/leave_channel/__snapshots__/leave_channel.test.js.snap b/components/channel_header_dropdown/menu_items/leave_channel/__snapshots__/leave_channel.test.js.snap new file mode 100644 index 000000000000..0f66d420ccaf --- /dev/null +++ b/components/channel_header_dropdown/menu_items/leave_channel/__snapshots__/leave_channel.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should match snapshot 1`] = ` +
  • + +
  • +`; diff --git a/components/channel_header_dropdown/menu_items/leave_channel/index.js b/components/channel_header_dropdown/menu_items/leave_channel/index.js new file mode 100644 index 000000000000..8224a59a023b --- /dev/null +++ b/components/channel_header_dropdown/menu_items/leave_channel/index.js @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; +import {leaveChannel} from 'mattermost-redux/actions/channels'; + +import LeaveChannel from './leave_channel'; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({leaveChannel}, dispatch), +}); + +export default connect(null, mapDispatchToProps)(LeaveChannel); diff --git a/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.js b/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.js new file mode 100644 index 000000000000..222534e8163f --- /dev/null +++ b/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.js @@ -0,0 +1,83 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +import {showLeavePrivateChannelModal} from 'actions/global_actions'; +import {Constants} from 'utils/constants'; + +export default class LeaveChannel extends React.PureComponent { + static propTypes = { + + /** + * Object with info about user + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the channel is default + */ + isDefault: PropTypes.bool.isRequired, + + /** + * Object with action creators + */ + actions: PropTypes.shape({ + + /** + * Action creator to leave channel + */ + leaveChannel: PropTypes.func.isRequired, + }).isRequired, + }; + + handleLeave = (e) => { + e.preventDefault(); + + const { + channel, + actions: { + leaveChannel, + }, + } = this.props; + + if (channel.type === Constants.PRIVATE_CHANNEL) { + showLeavePrivateChannelModal(channel); + } else { + leaveChannel(channel.id); + } + } + + render() { + const {channel, isDefault} = this.props; + + if (isDefault) { + return null; + } + + if (channel.type === Constants.DM_CHANNEL) { + return null; + } + + if (channel.type === Constants.GM_CHANNEL) { + return null; + } + + return ( +
  • + +
  • + ); + } +} diff --git a/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.test.js b/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.test.js new file mode 100644 index 000000000000..0078be672354 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.test.js @@ -0,0 +1,79 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {Constants} from 'utils/constants'; + +import LeaveChannel from './leave_channel'; + +jest.mock('actions/global_actions', () => ({ + showLeavePrivateChannelModal: jest.fn(), +})); + +describe('components/ChannelHeaderDropdown/MenuItem.LeaveChannel', () => { + const baseProps = { + channel: { + id: 'channel_id', + type: Constants.OPEN_CHANNEL, + }, + isDefault: false, + actions: { + leaveChannel: jest.fn(), + }, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel is default channel', () => { + const props = { + ...baseProps, + isDefault: true, + }; + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel type is DM or GM', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.DM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.GM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + }); + + it('should runs leaveChannel function on click only if the channel is not private', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + actions: {...baseProps.actions}, + }; + const {showLeavePrivateChannelModal} = require('actions/global_actions'); //eslint-disable-line global-require + const wrapper = shallow(); + + wrapper.find('button').simulate('click', { + preventDefault: jest.fn(), + }); + expect(props.actions.leaveChannel).toHaveBeenCalledWith(props.channel.id); + expect(showLeavePrivateChannelModal).not.toHaveBeenCalled(); + + props.channel.type = Constants.PRIVATE_CHANNEL; + props.actions.leaveChannel = jest.fn(); + wrapper.find('button').simulate('click', { + preventDefault: jest.fn(), + }); + expect(props.actions.leaveChannel).not.toHaveBeenCalled(); + expect(showLeavePrivateChannelModal).toHaveBeenCalledWith(props.channel); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/notification_preferences.js b/components/channel_header_dropdown/menu_items/notification_preferences.js new file mode 100644 index 000000000000..f6e150f622ce --- /dev/null +++ b/components/channel_header_dropdown/menu_items/notification_preferences.js @@ -0,0 +1,61 @@ + +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +import {Constants, ModalIdentifiers} from 'utils/constants'; + +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import ChannelNotificationsModal from 'components/channel_notifications_modal'; + +const NotificationPreferences = ({user, channel, isArchived}) => { + if (channel.type === Constants.DM_CHANNEL) { + return null; + } + + if (isArchived) { + return null; + } + + return ( +
  • + + + +
  • + ); +}; + +NotificationPreferences.propTypes = { + + /** + * Object with info about user + */ + user: PropTypes.object.isRequired, + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the current channel is archived + */ + isArchived: PropTypes.bool.isRequired, +}; + +export default NotificationPreferences; diff --git a/components/channel_header_dropdown/menu_items/notification_preferences.test.js b/components/channel_header_dropdown/menu_items/notification_preferences.test.js new file mode 100644 index 000000000000..e00c2b8e9b49 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/notification_preferences.test.js @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {Constants} from 'utils/constants'; + +import NotificationPreferences from './notification_preferences'; + +describe('components/ChannelHeaderDropdown/MenuItem.NotificationPreferences', () => { + const baseProps = { + user: {}, + channel: { + type: Constants.OPEN_CHANNEL, + }, + isArchived: false, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel type is DM', () => { + const props = { + ...baseProps, + channel: { + type: Constants.DM_CHANNEL, + }, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is archived', () => { + const props = { + ...baseProps, + isArchived: true, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/rename_channel.js b/components/channel_header_dropdown/menu_items/rename_channel.js new file mode 100644 index 000000000000..b858340fe5db --- /dev/null +++ b/components/channel_header_dropdown/menu_items/rename_channel.js @@ -0,0 +1,66 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {Permissions} from 'mattermost-redux/constants'; + +import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import RenameChannelModal from 'components/rename_channel_modal'; +import {Constants, ModalIdentifiers} from 'utils/constants'; + +const RenameChannel = ({channel, isArchived}) => { + if (isArchived) { + return null; + } + + if (channel.type === Constants.DM_CHANNEL) { + return null; + } + + if (channel.type === Constants.GM_CHANNEL) { + return null; + } + + const isPrivate = channel.type === Constants.PRIVATE_CHANNEL; + const permission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES; + + return ( + +
  • + + + +
  • +
    + ); +}; + +RenameChannel.propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the current channel is archived + */ + isArchived: PropTypes.bool.isRequired, +}; + +export default RenameChannel; diff --git a/components/channel_header_dropdown/menu_items/rename_channel.test.js b/components/channel_header_dropdown/menu_items/rename_channel.test.js new file mode 100644 index 000000000000..5f30bf6142c2 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/rename_channel.test.js @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; +import {Permissions} from 'mattermost-redux/constants'; + +import {Constants} from 'utils/constants'; + +import RenameChannel from './rename_channel'; + +describe('components/ChannelHeaderDropdown/MenuItem.RenameChannel', () => { + const baseProps = { + channel: { + id: 'channel_id', + team_id: 'team_id', + type: Constants.OPEN_CHANNEL, + }, + isArchived: false, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel is archived', () => { + const props = { + ...baseProps, + isArchived: true, + }; + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is DM or GM', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.DM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.GM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + }); + + it('should requires right permission level for channel type', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.OPEN_CHANNEL; + expect(makeWrapper().prop('permissions')[0]).toBe(Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES); + + props.channel.type = Constants.PRIVATE_CHANNEL; + expect(makeWrapper().prop('permissions')[0]).toBe(Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/set_channel_header.js b/components/channel_header_dropdown/menu_items/set_channel_header.js new file mode 100644 index 000000000000..44f435381f51 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/set_channel_header.js @@ -0,0 +1,68 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {Permissions} from 'mattermost-redux/constants'; + +import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import EditChannelHeaderModal from 'components/edit_channel_header_modal'; +import {Constants, ModalIdentifiers} from 'utils/constants'; + +const SetChannelHeader = ({channel, isArchived, isReadonly}) => { + if (isArchived) { + return null; + } + + if (isReadonly) { + return null; + } + + const isPrivate = channel.type === Constants.PRIVATE_CHANNEL; + const permission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES; + + return ( + +
  • + + + +
  • +
    + ); +}; + +SetChannelHeader.propTypes = { + + /** + * Object with info about user + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the channel is archived + */ + isArchived: PropTypes.bool.isRequired, + + /** + * Boolean whether the channel is readonly + */ + isReadonly: PropTypes.bool.isRequired, +}; + +export default SetChannelHeader; diff --git a/components/channel_header_dropdown/menu_items/set_channel_header.test.js b/components/channel_header_dropdown/menu_items/set_channel_header.test.js new file mode 100644 index 000000000000..a43b5d01d918 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/set_channel_header.test.js @@ -0,0 +1,63 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; +import {Permissions} from 'mattermost-redux/constants'; + +import {Constants} from 'utils/constants'; + +import SetChannelHeader from './set_channel_header'; + +describe('components/ChannelHeaderDropdown/MenuItem.SetChannelHeader', () => { + const baseProps = { + channel: { + id: 'channel_id', + team_id: 'team_id', + type: Constants.OPEN_CHANNEL, + }, + isReadonly: false, + isArchived: false, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel is archived', () => { + const props = { + ...baseProps, + isReadonly: true, + }; + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is readonly', () => { + const props = { + ...baseProps, + isReadonly: true, + }; + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should requires right permission level for channel type to manage header', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + // Public, DM, GM (is this correct?) + props.channel.type = Constants.OPEN_CHANNEL; + expect(makeWrapper().prop('permissions')[0]).toBe(Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES); + + props.channel.type = Constants.PRIVATE_CHANNEL; + expect(makeWrapper().prop('permissions')[0]).toBe(Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/set_channel_purpose.js b/components/channel_header_dropdown/menu_items/set_channel_purpose.js new file mode 100644 index 000000000000..4e4c23c13401 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/set_channel_purpose.js @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {Permissions} from 'mattermost-redux/constants'; + +import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import EditChannelPurposeModal from 'components/edit_channel_purpose_modal'; +import {Constants, ModalIdentifiers} from 'utils/constants'; + +const SetChannelPurpose = ({channel, isArchived, isReadonly}) => { + if (isArchived) { + return null; + } + + if (isReadonly) { + return null; + } + + if (channel.type === Constants.DM_CHANNEL) { + return null; + } + + if (channel.type === Constants.GM_CHANNEL) { + return null; + } + + const isPrivate = channel.type === Constants.PRIVATE_CHANNEL; + const permission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES; + + return ( + +
  • + + + +
  • +
    + ); +}; + +SetChannelPurpose.propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the channel is readonly + */ + isArchived: PropTypes.bool.isRequired, + + /** + * Boolean whether the channel is readonly + */ + isReadonly: PropTypes.bool.isRequired, +}; + +export default SetChannelPurpose; diff --git a/components/channel_header_dropdown/menu_items/set_channel_purpose.test.js b/components/channel_header_dropdown/menu_items/set_channel_purpose.test.js new file mode 100644 index 000000000000..2da907ac2cda --- /dev/null +++ b/components/channel_header_dropdown/menu_items/set_channel_purpose.test.js @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; +import {Permissions} from 'mattermost-redux/constants'; + +import {Constants} from 'utils/constants'; + +import SetChannelPurpose from './set_channel_purpose'; + +describe('components/ChannelHeaderDropdown/MenuItem.SetChannelPurpose', () => { + const baseProps = { + channel: { + id: 'channel_id', + team_id: 'team_id', + type: Constants.OPEN_CHANNEL, + }, + isArchived: false, + isReadonly: false, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel is archived', () => { + const props = { + ...baseProps, + isArchived: true, + }; + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel is readonly', () => { + const props = { + ...baseProps, + isReadonly: true, + }; + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + + it('should be hidden if the channel type is DM or GM', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.DM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.GM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + }); + + it('should requires right permission level for channel type to manage purpose', () => { + const props = { + ...baseProps, + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.OPEN_CHANNEL; + expect(makeWrapper().prop('permissions')[0]).toBe(Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES); + + props.channel.type = Constants.PRIVATE_CHANNEL; + expect(makeWrapper().prop('permissions')[0]).toBe(Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/toggle_favorite_channel/__snapshots__/toggle_favorite_channel.test.js.snap b/components/channel_header_dropdown/menu_items/toggle_favorite_channel/__snapshots__/toggle_favorite_channel.test.js.snap new file mode 100644 index 000000000000..2668afca7ffc --- /dev/null +++ b/components/channel_header_dropdown/menu_items/toggle_favorite_channel/__snapshots__/toggle_favorite_channel.test.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel should match snapshot for favorite channel 1`] = ` +
  • + +
  • +`; + +exports[`components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel should match snapshot for not favorite channel 1`] = ` +
  • + +
  • +`; diff --git a/components/channel_header_dropdown/menu_items/toggle_favorite_channel/index.js b/components/channel_header_dropdown/menu_items/toggle_favorite_channel/index.js new file mode 100644 index 000000000000..74288fc7789a --- /dev/null +++ b/components/channel_header_dropdown/menu_items/toggle_favorite_channel/index.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; +import {favoriteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels'; + +import ToggleFavoriteChannel from './toggle_favorite_channel'; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + favoriteChannel, + unfavoriteChannel, + }, dispatch), +}); + +export default connect(null, mapDispatchToProps)(ToggleFavoriteChannel); diff --git a/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.js b/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.js new file mode 100644 index 000000000000..1130c499c36f --- /dev/null +++ b/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.js @@ -0,0 +1,78 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +export default class ToggleFavoriteChannel extends React.PureComponent { + static propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Bool whether current channel is favorite + */ + isFavorite: PropTypes.bool.isRequired, + + /** + * Object with action creators + */ + actions: PropTypes.shape({ + + /** + * Action creator to add current channel to favorites + */ + favoriteChannel: PropTypes.func.isRequired, + + /** + * Action creator to remove current channel from favorites + */ + unfavoriteChannel: PropTypes.func.isRequired, + }).isRequired, + }; + + toggleFavoriteChannel = (channelId) => { + const { + isFavorite, + actions: { + favoriteChannel, + unfavoriteChannel, + }, + } = this.props; + + return isFavorite ? unfavoriteChannel(channelId) : favoriteChannel(channelId); + } + + handleClick = (e) => { + e.preventDefault(); + this.toggleFavoriteChannel(this.props.channel.id); + } + + render() { + return ( +
  • + +
  • + ); + } +} diff --git a/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.test.js b/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.test.js new file mode 100644 index 000000000000..6374dff5359c --- /dev/null +++ b/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.test.js @@ -0,0 +1,70 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import ToggleFavoriteChannel from './toggle_favorite_channel'; + +describe('components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel', () => { + const baseProps = { + channel: { + id: 'channel_id', + }, + actions: { + favoriteChannel: jest.fn(), + unfavoriteChannel: jest.fn(), + }, + }; + + const propsForFavorite = { + ...baseProps, + isFavorite: true, + actions: { + favoriteChannel: jest.fn(), + unfavoriteChannel: jest.fn(), + }, + }; + + const propsForNotFavorite = { + ...baseProps, + isFavorite: false, + actions: { + favoriteChannel: jest.fn(), + unfavoriteChannel: jest.fn(), + }, + }; + + it('should match snapshot for favorite channel', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should runs unfavoriteChannel function for favorite channel', () => { + const wrapper = shallow(); + + wrapper.find('button').simulate('click', { + preventDefault: jest.fn(), + }); + + expect(propsForFavorite.actions.unfavoriteChannel).toHaveBeenCalledWith(propsForFavorite.channel.id); + expect(propsForFavorite.actions.favoriteChannel).not.toHaveBeenCalled(); + }); + + it('should match snapshot for not favorite channel', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should runs favoriteChannel function for not favorite channel', () => { + const wrapper = shallow(); + + wrapper.find('button').simulate('click', { + preventDefault: jest.fn(), + }); + + expect(propsForNotFavorite.actions.favoriteChannel).toHaveBeenCalledWith(propsForFavorite.channel.id); + expect(propsForNotFavorite.actions.unfavoriteChannel).not.toHaveBeenCalled(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/toggle_mute_channel/__snapshots__/toggle_mute_channel.test.js.snap b/components/channel_header_dropdown/menu_items/toggle_mute_channel/__snapshots__/toggle_mute_channel.test.js.snap new file mode 100644 index 000000000000..50b01aaf1d46 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/toggle_mute_channel/__snapshots__/toggle_mute_channel.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.ToggleMuteChannel should match snapshot 1`] = ` +
  • + +
  • +`; diff --git a/components/channel_header_dropdown/menu_items/toggle_mute_channel/index.js b/components/channel_header_dropdown/menu_items/toggle_mute_channel/index.js new file mode 100644 index 000000000000..5535489ef10d --- /dev/null +++ b/components/channel_header_dropdown/menu_items/toggle_mute_channel/index.js @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; +import {updateChannelNotifyProps} from 'mattermost-redux/actions/channels'; + +import ToggleMuteChannel from './toggle_mute_channel'; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + updateChannelNotifyProps, + }, dispatch), +}); + +export default connect(null, mapDispatchToProps)(ToggleMuteChannel); diff --git a/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.js b/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.js new file mode 100644 index 000000000000..993c8dcbceb0 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.js @@ -0,0 +1,85 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +import {Constants, NotificationLevels} from 'utils/constants'; + +export default class ToggleMuteChannel extends React.PureComponent { + static propTypes = { + + /** + * Object with info about the current user + */ + user: PropTypes.object.isRequired, + + /** + * Object with info about the current channel + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the current channel is muted + */ + isMuted: PropTypes.bool.isRequired, + + /** + * Object with action creators + */ + actions: PropTypes.shape({ + updateChannelNotifyProps: PropTypes.func.isRequired, + }).isRequired, + }; + + handleClick = () => { + const { + user, + channel, + isMuted, + actions: { + updateChannelNotifyProps, + }, + } = this.props; + + updateChannelNotifyProps(user.id, channel.id, { + mark_unread: isMuted ? NotificationLevels.ALL : NotificationLevels.MENTION, + }); + } + + render() { + if (this.props.channel.type === Constants.DM_CHANNEL) { + return null; + } + + let message; + if (this.props.isMuted) { + message = ( + + ); + } else { + message = ( + + ); + } + + return ( +
  • + +
  • + ); + } +} diff --git a/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.test.js b/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.test.js new file mode 100644 index 000000000000..36f0c68f1eda --- /dev/null +++ b/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.test.js @@ -0,0 +1,80 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {Constants, NotificationLevels} from 'utils/constants'; + +import ToggleMuteChannel from './toggle_mute_channel'; + +describe('components/ChannelHeaderDropdown/MenuItem.ToggleMuteChannel', () => { + const baseProps = { + user: { + id: 'user_id', + }, + channel: { + id: 'channel_id', + type: Constants.OPEN_CHANNEL, + }, + isMuted: false, + actions: { + updateChannelNotifyProps: jest.fn(), + }, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should unmute channel on click the channel was muted', () => { + const props = { + ...baseProps, + isMuted: true, + actions: { + updateChannelNotifyProps: jest.fn(), + }, + }; + const wrapper = shallow(); + + wrapper.find('button').simulate('click'); + + expect(props.actions.updateChannelNotifyProps).toBeCalledWith( + props.user.id, + props.channel.id, + {mark_unread: NotificationLevels.ALL} + ); + }); + + it('should mute channel on click the channel was unmuted', () => { + const props = { + ...baseProps, + isMuted: false, + actions: { + updateChannelNotifyProps: jest.fn(), + }, + }; + const wrapper = shallow(); + + wrapper.find('button').simulate('click'); + + expect(props.actions.updateChannelNotifyProps).toBeCalledWith( + props.user.id, + props.channel.id, + {mark_unread: NotificationLevels.MENTION} + ); + }); + + it('should be hidden if the channel type is DM', () => { + const props = { + ...baseProps, + channel: { + ...baseProps.channel, + type: Constants.DM_CHANNEL, + }, + }; + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/view_and_manage_members.js b/components/channel_header_dropdown/menu_items/view_and_manage_members.js new file mode 100644 index 000000000000..cd08f1bd1fd0 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/view_and_manage_members.js @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {Permissions} from 'mattermost-redux/constants'; + +import {Constants, ModalIdentifiers} from 'utils/constants'; + +import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import ChannelMembersModal from 'components/channel_members_modal'; + +const ViewAndManageMembers = ({channel, isDefault}) => { + if (channel.type === Constants.DM_CHANNEL) { + return null; + } + + if (channel.type === Constants.GM_CHANNEL) { + return null; + } + + const isPrivate = channel.type === Constants.PRIVATE_CHANNEL; + const permission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS : Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS; + + return ( +
  • + + {!isDefault && + + + + } + + + + +
  • + ); +}; + +ViewAndManageMembers.propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Boolean whether the current channel is default + */ + isDefault: PropTypes.bool.isRequired, +}; + +export default ViewAndManageMembers; diff --git a/components/channel_header_dropdown/menu_items/view_and_manage_members.test.js b/components/channel_header_dropdown/menu_items/view_and_manage_members.test.js new file mode 100644 index 000000000000..016c2d43c7b4 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/view_and_manage_members.test.js @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {Constants} from 'utils/constants'; + +import ViewAndManageMembers from './view_and_manage_members'; + +describe('components/ChannelHeaderDropdown/MenuItem.ViewAndManageMembers', () => { + const baseProps = { + channel: { + type: Constants.OPEN_CHANNEL, + }, + isDefault: false, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should match snapshot when the channel is town square', () => { + const props = { + ...baseProps, + isDefault: true, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel type is DM or GM', () => { + const props = { + ...baseProps, + channel: { + ...baseProps.channel, + }, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.DM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.GM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/view_channel_info.js b/components/channel_header_dropdown/menu_items/view_channel_info.js new file mode 100644 index 000000000000..3c73b4564b95 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/view_channel_info.js @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +import {Constants, ModalIdentifiers} from 'utils/constants'; + +import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; +import ChannelInfoModal from 'components/channel_info_modal'; + +const ViewChannelInfo = ({channel}) => { + if (channel.type === Constants.DM_CHANNEL) { + return null; + } + + if (channel.type === Constants.GM_CHANNEL) { + return null; + } + + return ( +
  • + + + +
  • + ); +}; + +ViewChannelInfo.propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, +}; + +export default ViewChannelInfo; diff --git a/components/channel_header_dropdown/menu_items/view_channel_info.test.js b/components/channel_header_dropdown/menu_items/view_channel_info.test.js new file mode 100644 index 000000000000..a847f0719536 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/view_channel_info.test.js @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {Constants} from 'utils/constants'; + +import ViewChannelInfo from './view_channel_info'; + +describe('components/ChannelHeaderDropdown/MenuItem.ViewChannelInfo', () => { + const baseProps = { + channel: { + type: Constants.OPEN_CHANNEL, + }, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should be hidden if the channel type is DM or GM', () => { + const props = { + channel: {...baseProps.channel}, + }; + const makeWrapper = () => shallow(); + + props.channel.type = Constants.DM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + + props.channel.type = Constants.GM_CHANNEL; + expect(makeWrapper().isEmptyRender()).toBeTruthy(); + }); +}); diff --git a/components/channel_header_dropdown/menu_items/view_pinned_posts/__snapshots__/view_pinned_posts.test.js.snap b/components/channel_header_dropdown/menu_items/view_pinned_posts/__snapshots__/view_pinned_posts.test.js.snap new file mode 100644 index 000000000000..19e579074573 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/view_pinned_posts/__snapshots__/view_pinned_posts.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderDropdown/MenuItem.ViewPinnedPosts should match snapshot 1`] = ` +
  • + +
  • +`; diff --git a/components/channel_header_dropdown/menu_items/view_pinned_posts/index.js b/components/channel_header_dropdown/menu_items/view_pinned_posts/index.js new file mode 100644 index 000000000000..1ea4bc35b7fb --- /dev/null +++ b/components/channel_header_dropdown/menu_items/view_pinned_posts/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {closeRightHandSide, showPinnedPosts} from 'actions/views/rhs'; +import {getRhsState} from 'selectors/rhs'; +import {RHSStates} from 'utils/constants'; + +import ViewPinnedPosts from './view_pinned_posts'; + +const mapStateToProps = (state) => ({ + hasPinnedPosts: getRhsState(state) === RHSStates.PIN, +}); + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + closeRightHandSide, + showPinnedPosts, + }, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ViewPinnedPosts); diff --git a/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.js b/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.js new file mode 100644 index 000000000000..a23d58a4aa2a --- /dev/null +++ b/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.js @@ -0,0 +1,66 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +export default class ViewPinnedPosts extends React.PureComponent { + static propTypes = { + + /** + * Object with info about channel + */ + channel: PropTypes.object.isRequired, + + /** + * Bool whether the channel has pinned any posts + * from redux store + */ + hasPinnedPosts: PropTypes.bool.isRequired, + + /** + * Object with action creators + */ + actions: PropTypes.shape({ + closeRightHandSide: PropTypes.func.isRequired, + showPinnedPosts: PropTypes.func.isRequired, + }).isRequired, + } + + handleClick = (e) => { + e.preventDefault(); + + const { + channel, + hasPinnedPosts, + actions: { + closeRightHandSide, + showPinnedPosts, + }, + } = this.props; + + if (hasPinnedPosts) { + closeRightHandSide(); + } else { + showPinnedPosts(channel.id); + } + } + + render() { + return ( +
  • + +
  • + ); + } +} diff --git a/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.test.js b/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.test.js new file mode 100644 index 000000000000..1041d39cff60 --- /dev/null +++ b/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.test.js @@ -0,0 +1,49 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import ViewPinnedPosts from './view_pinned_posts'; + +describe('components/ChannelHeaderDropdown/MenuItem.ViewPinnedPosts', () => { + const baseProps = { + channel: { + id: 'channel_id', + }, + hasPinnedPosts: true, + actions: { + closeRightHandSide: jest.fn(), + showPinnedPosts: jest.fn(), + }, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should runs closeRightHandSide function if has any pinned posts', () => { + const wrapper = shallow(); + + wrapper.find('button').simulate('click', { + preventDefault: jest.fn(), + }); + + expect(baseProps.actions.closeRightHandSide).toHaveBeenCalled(); + }); + + it('should runs showPinnedPosts function if has not pinned posts', () => { + const props = { + ...baseProps, + hasPinnedPosts: false, + }; + const wrapper = shallow(); + + wrapper.find('button').simulate('click', { + preventDefault: jest.fn(), + }); + + expect(baseProps.actions.showPinnedPosts).toHaveBeenCalled(); + }); +}); diff --git a/components/channel_header_dropdown/mobile_channel_header_dropdown.js b/components/channel_header_dropdown/mobile_channel_header_dropdown.js new file mode 100644 index 000000000000..112d1290ea54 --- /dev/null +++ b/components/channel_header_dropdown/mobile_channel_header_dropdown.js @@ -0,0 +1,141 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +import StatusIcon from 'components/status_icon'; +import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug'; +import {Constants} from 'utils/constants'; +import * as Utils from 'utils/utils'; + +import * as MenuItem from './menu_items'; + +export default class MobileChannelHeaderDropdown extends React.PureComponent { + static propTypes = { + user: PropTypes.object.isRequired, + channel: PropTypes.object.isRequired, + isDefault: PropTypes.bool.isRequired, + isFavorite: PropTypes.bool.isRequired, + isReadonly: PropTypes.bool.isRequired, + isArchived: PropTypes.bool.isRequired, + teammateId: PropTypes.string, + teammateStatus: PropTypes.string, + } + + getChannelTitle = () => { + const {user, channel, teammateId} = this.props; + + if (channel.type === Constants.DM_CHANNEL) { + const displayname = Utils.getDisplayNameByUserId(teammateId); + if (user.id === teammateId) { + return ( + + ); + } + return displayname; + } + return channel.display_name; + } + + render() { + const { + user, + channel, + teammateStatus, + isDefault, + isFavorite, + isReadonly, + isArchived, + } = this.props; + + return ( +
    + + + + {this.getChannelTitle()} + + + + +
      + + + + + + + + + + + + + + + +
      + {'×'} +
      +
    +
    + ); + } +} diff --git a/components/channel_header_mobile/__snapshots__/channel_header_mobile.test.js.snap b/components/channel_header_mobile/__snapshots__/channel_header_mobile.test.js.snap new file mode 100644 index 000000000000..6effc330334b --- /dev/null +++ b/components/channel_header_mobile/__snapshots__/channel_header_mobile.test.js.snap @@ -0,0 +1,281 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot 1`] = ` + +`; + +exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, for default channel 1`] = ` + +`; + +exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, for private channel 1`] = ` + +`; + +exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, if WebRTC is not enabled 1`] = ` + +`; + +exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, if enabled WebRTC and DM channel 1`] = ` + +`; + +exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, if not licensed 1`] = ` + +`; diff --git a/components/channel_header_mobile/channel_header_mobile.js b/components/channel_header_mobile/channel_header_mobile.js new file mode 100644 index 000000000000..9a42c9639868 --- /dev/null +++ b/components/channel_header_mobile/channel_header_mobile.js @@ -0,0 +1,111 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +import {MobileChannelHeaderDropdown} from 'components/channel_header_dropdown'; +import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug'; + +import * as Utils from 'utils/utils'; + +import CollapseLhsButton from './collapse_lhs_button'; +import CollapseRhsButton from './collapse_rhs_button'; +import ChannelInfoButton from './channel_info_button'; +import ShowSearchButton from './show_search_button'; +import UnmuteChannelButton from './unmute_channel_button'; + +export default class ChannelHeaderMobile extends React.PureComponent { + static propTypes = { + + /** + * + */ + user: PropTypes.object.isRequired, + + /** + * Object with info about current channel + */ + channel: PropTypes.object, + + /** + * Bool whether the current channel is read only + */ + isReadOnly: PropTypes.bool, + + /** + * Bool whether the current channel is muted + */ + isMuted: PropTypes.bool, + + /** + * Object with action creators + */ + actions: PropTypes.shape({ + closeLhs: PropTypes.func.isRequired, + closeRhs: PropTypes.func.isRequired, + closeRhsMenu: PropTypes.func.isRequired, + }).isRequired, + }; + + componentDidMount() { + document.querySelector('.inner-wrap').addEventListener('click', this.hideSidebars); + } + + componentWillUnmount() { + document.querySelector('.inner-wrap').removeEventListener('click', this.hideSidebars); + } + + hideSidebars = (e) => { + if (Utils.isMobile()) { + this.props.actions.closeRhs(); + + if (e.target.className !== 'navbar-toggle' && e.target.className !== 'icon-bar') { + this.props.actions.closeLhs(); + this.props.actions.closeRhsMenu(); + } + } + } + + render() { + const {user, channel, isMuted} = this.props; + + return ( + + ); + } +} diff --git a/components/channel_header_mobile/channel_header_mobile.test.js b/components/channel_header_mobile/channel_header_mobile.test.js new file mode 100644 index 000000000000..7c233928b3d4 --- /dev/null +++ b/components/channel_header_mobile/channel_header_mobile.test.js @@ -0,0 +1,101 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import ChannelHeaderMobile from './channel_header_mobile'; + +describe('components/ChannelHeaderMobile/ChannelHeaderMobile', () => { + global.document.querySelector = jest.fn().mockReturnValue({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }); + + const baseProps = { + user: { + id: 'user_id', + }, + channel: {type: 'O', id: 'channel_id', display_name: 'display_name', team_id: 'team_id'}, + member: {id: 'member_id'}, + teamDisplayName: 'team_display_name', + isPinnedPosts: true, + actions: { + closeLhs: jest.fn(), + closeRhs: jest.fn(), + closeRhsMenu: jest.fn(), + leaveChannel: jest.fn(), + markFavorite: jest.fn(), + showPinnedPosts: jest.fn(), + toggleLhs: jest.fn(), + toggleRhsMenu: jest.fn(), + unmarkFavorite: jest.fn(), + updateChannelNotifyProps: jest.fn(), + updateRhsState: jest.fn(), + }, + isLicensed: true, + isFavoriteChannel: false, + }; + + test('should match snapshot', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, for default channel', () => { + const props = { + ...baseProps, + channel: {type: 'O', id: '123', name: 'town-square', display_name: 'Town Square', team_id: 'team_id'}, + }; + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, if not licensed', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, if enabled WebRTC and DM channel', () => { + const props = { + ...baseProps, + channel: {type: 'D', id: 'channel_id', name: 'user_id_1__user_id_2', display_name: 'display_name', team_id: 'team_id'}, + enableWebrtc: true, + }; + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, if WebRTC is not enabled', () => { + const props = { + ...baseProps, + enableWebrtc: false, + }; + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, for private channel', () => { + const props = { + ...baseProps, + channel: {type: 'P', id: 'channel_id', display_name: 'display_name', team_id: 'team_id'}, + }; + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/components/navbar/navbar_info_button/__snapshots__/navbar_info_button.test.jsx.snap b/components/channel_header_mobile/channel_info_button/__snapshots__/channel_info_button.test.js.snap similarity index 96% rename from components/navbar/navbar_info_button/__snapshots__/navbar_info_button.test.jsx.snap rename to components/channel_header_mobile/channel_info_button/__snapshots__/channel_info_button.test.js.snap index a2fc444fa41b..614dcd783234 100644 --- a/components/navbar/navbar_info_button/__snapshots__/navbar_info_button.test.jsx.snap +++ b/components/channel_header_mobile/channel_info_button/__snapshots__/channel_info_button.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/navbar/NavbarInfoButton should match snapshot, with channel header 1`] = ` +exports[`components/ChannelHeaderMobile/ChannelInfoButton should match snapshot, with channel header 1`] = ` `; -exports[`components/navbar/NavbarInfoButton should match snapshot, without channel header 1`] = ` +exports[`components/ChannelHeaderMobile/ChannelInfoButton should match snapshot, without channel header 1`] = ` + if (channel.header) { + popoverContent = ( + + ); + } else { + let addOne; + if (!isReadOnly) { + const link = ( + + + ); - } else { - let addOne; - if (!this.props.isReadOnly) { - const link = ( - - - - ); - addOne = ( - -
    - -
    - ); - } - - popoverContent = ( -
    + addOne = ( + +
    - {addOne} -
    +
    ); } + + popoverContent = ( +
    + + {addOne} +
    + ); } const popover = ( diff --git a/components/navbar/navbar_info_button/navbar_info_button.test.jsx b/components/channel_header_mobile/channel_info_button/channel_info_button.test.js similarity index 83% rename from components/navbar/navbar_info_button/navbar_info_button.test.jsx rename to components/channel_header_mobile/channel_info_button/channel_info_button.test.js index 71215a6a83fa..d64f930538d1 100644 --- a/components/navbar/navbar_info_button/navbar_info_button.test.jsx +++ b/components/channel_header_mobile/channel_info_button/channel_info_button.test.js @@ -4,14 +4,16 @@ import React from 'react'; import {mountWithIntl} from 'tests/helpers/intl-test-helper.jsx'; -import NavbarInfoButton from 'components/navbar/navbar_info_button/navbar_info_button.jsx'; -describe('components/navbar/NavbarInfoButton', () => { +import ChannelInfoButton from './channel_info_button'; + +describe('components/ChannelHeaderMobile/ChannelInfoButton', () => { const baseProps = { channel: { id: 'channel_id', header: 'channel header', }, + isReadOnly: false, actions: { openModal: jest.fn(), }, @@ -19,7 +21,7 @@ describe('components/navbar/NavbarInfoButton', () => { test('should match snapshot, with channel header', () => { const wrapper = mountWithIntl( - + ); expect(wrapper).toMatchSnapshot(); @@ -33,7 +35,7 @@ describe('components/navbar/NavbarInfoButton', () => { test('should match snapshot, without channel header', () => { const props = {...baseProps, channel: {id: 'channel_id'}}; const wrapper = mountWithIntl( - + ); expect(wrapper).toMatchSnapshot(); diff --git a/components/navbar/navbar_info_button/index.js b/components/channel_header_mobile/channel_info_button/index.js similarity index 75% rename from components/navbar/navbar_info_button/index.js rename to components/channel_header_mobile/channel_info_button/index.js index e6abd0dbf76f..911ed3f13e7b 100644 --- a/components/navbar/navbar_info_button/index.js +++ b/components/channel_header_mobile/channel_info_button/index.js @@ -6,7 +6,7 @@ import {bindActionCreators} from 'redux'; import {openModal} from 'actions/views/modals'; -import NavbarInfoButton from './navbar_info_button.jsx'; +import ChannelInfoButton from './channel_info_button'; function mapDispatchToProps(dispatch) { return { @@ -16,4 +16,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(null, mapDispatchToProps)(NavbarInfoButton); +export default connect(null, mapDispatchToProps)(ChannelInfoButton); diff --git a/components/channel_header_mobile/collapse_lhs_button/collapse_lhs_button.js b/components/channel_header_mobile/collapse_lhs_button/collapse_lhs_button.js new file mode 100644 index 000000000000..25f4dedcf937 --- /dev/null +++ b/components/channel_header_mobile/collapse_lhs_button/collapse_lhs_button.js @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +import NotifyCounts from 'components/notify_counts'; +import MenuIcon from 'components/svg/menu_icon'; + +const CollapseLhsButton = ({ + actions: { + toggleLhs, + }, +}) => ( + +); + +CollapseLhsButton.propTypes = { + actions: PropTypes.shape({ + toggleLhs: PropTypes.func.isRequired, + }).isRequired, +}; + +export default CollapseLhsButton; diff --git a/components/channel_header_mobile/collapse_lhs_button/index.js b/components/channel_header_mobile/collapse_lhs_button/index.js new file mode 100644 index 000000000000..f50a2c4aadb5 --- /dev/null +++ b/components/channel_header_mobile/collapse_lhs_button/index.js @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {toggle as toggleLhs} from 'actions/views/lhs'; + +import CollapseLhsButton from './collapse_lhs_button'; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + toggleLhs, + }, dispatch), +}); + +export default connect(null, mapDispatchToProps)(CollapseLhsButton); + diff --git a/components/channel_header_mobile/collapse_rhs_button/collapse_rhs_button.js b/components/channel_header_mobile/collapse_rhs_button/collapse_rhs_button.js new file mode 100644 index 000000000000..1778fc7151a1 --- /dev/null +++ b/components/channel_header_mobile/collapse_rhs_button/collapse_rhs_button.js @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +import MenuIcon from 'components/svg/menu_icon'; + +const CollapseRhsButton = ({ + actions: { + toggleRhsMenu, + }, +}) => ( + +); + +CollapseRhsButton.propTypes = { + actions: PropTypes.shape({ + toggleRhsMenu: PropTypes.func.isRequired, + }).isRequired, +}; + +export default CollapseRhsButton; diff --git a/components/channel_header_mobile/collapse_rhs_button/index.js b/components/channel_header_mobile/collapse_rhs_button/index.js new file mode 100644 index 000000000000..a3438d02c19c --- /dev/null +++ b/components/channel_header_mobile/collapse_rhs_button/index.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {toggleMenu as toggleRhsMenu} from 'actions/views/rhs'; + +import CollapseRhsButton from './collapse_rhs_button'; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + toggleRhsMenu, + }, dispatch), +}); + +export default connect(null, mapDispatchToProps)(CollapseRhsButton); diff --git a/components/channel_header_mobile/index.js b/components/channel_header_mobile/index.js new file mode 100644 index 000000000000..c3ec56f7e195 --- /dev/null +++ b/components/channel_header_mobile/index.js @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {createSelector} from 'reselect'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import { + getCurrentChannel, + getMyCurrentChannelMembership, + isCurrentChannelReadOnly, +} from 'mattermost-redux/selectors/entities/channels'; +import {isChannelMuted} from 'mattermost-redux/utils/channel_utils'; + +import { + closeRightHandSide as closeRhs, + closeMenu as closeRhsMenu, +} from 'actions/views/rhs'; +import {close as closeLhs} from 'actions/views/lhs'; + +import ChannelHeaderMobile from './channel_header_mobile'; + +const isCurrentChannelMuted = createSelector( + getMyCurrentChannelMembership, + (membership) => isChannelMuted(membership), +); + +const mapStateToProps = (state) => ({ + user: getCurrentUser(state), + channel: getCurrentChannel(state), + isMuted: isCurrentChannelMuted(state), + isReadOnly: isCurrentChannelReadOnly(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + closeLhs, + closeRhs, + closeRhsMenu, + }, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ChannelHeaderMobile); diff --git a/components/channel_header_mobile/show_search_button/index.js b/components/channel_header_mobile/show_search_button/index.js new file mode 100644 index 000000000000..91144b80a179 --- /dev/null +++ b/components/channel_header_mobile/show_search_button/index.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {updateRhsState} from 'actions/views/rhs'; + +import ShowSearchButton from './show_search_button'; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + updateRhsState, + }, dispatch), +}); + +export default connect(null, mapDispatchToProps)(ShowSearchButton); diff --git a/components/channel_header_mobile/show_search_button/show_search_button.js b/components/channel_header_mobile/show_search_button/show_search_button.js new file mode 100644 index 000000000000..dbb350ea6ae8 --- /dev/null +++ b/components/channel_header_mobile/show_search_button/show_search_button.js @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +import SearchIcon from 'components/svg/search_icon'; +import {RHSStates} from 'utils/constants'; + +export default class ShowSearchButton extends React.PureComponent { + static propTypes = { + actions: PropTypes.shape({ + updateRhsState: PropTypes.func.isRequired, + }).isRequired, + } + + handleClick = () => { + this.props.actions.updateRhsState(RHSStates.SEARCH); + } + + render() { + return ( + + ); + } +} diff --git a/components/channel_header_mobile/unmute_channel_button/__snapshots__/unmute_channel_button.test.js.snap b/components/channel_header_mobile/unmute_channel_button/__snapshots__/unmute_channel_button.test.js.snap new file mode 100644 index 000000000000..aaa11abd1da1 --- /dev/null +++ b/components/channel_header_mobile/unmute_channel_button/__snapshots__/unmute_channel_button.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/ChannelHeaderMobile/UnmuteChannelButton should match snapshot 1`] = ` + +`; diff --git a/components/channel_header_mobile/unmute_channel_button/index.js b/components/channel_header_mobile/unmute_channel_button/index.js new file mode 100644 index 000000000000..8dabf0bf1030 --- /dev/null +++ b/components/channel_header_mobile/unmute_channel_button/index.js @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; +import {updateChannelNotifyProps} from 'mattermost-redux/actions/channels'; + +import UnmuteChannelButton from './unmute_channel_button'; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + updateChannelNotifyProps, + }, dispatch), +}); + +export default connect(null, mapDispatchToProps)(UnmuteChannelButton); diff --git a/components/channel_header_mobile/unmute_channel_button/unmute_channel_button.js b/components/channel_header_mobile/unmute_channel_button/unmute_channel_button.js new file mode 100644 index 000000000000..65f5fdabc606 --- /dev/null +++ b/components/channel_header_mobile/unmute_channel_button/unmute_channel_button.js @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +import {NotificationLevels} from 'utils/constants'; + +export default class UnmuteChannelButton extends React.PureComponent { + static propTypes = { + user: PropTypes.shape({ + id: PropTypes.string.isRequired, + }).isRequired, + + channel: PropTypes.shape({ + id: PropTypes.string.isRequired, + }).isRequired, + + actions: PropTypes.shape({ + updateChannelNotifyProps: PropTypes.func.isRequired, + }).isRequired, + }; + + handleClick = () => { + const { + user, + channel, + actions: { + updateChannelNotifyProps, + }, + } = this.props; + + updateChannelNotifyProps(user.id, channel.id, {mark_unread: NotificationLevels.ALL}); + } + + render() { + return ( + + ); + } +} diff --git a/components/channel_header_mobile/unmute_channel_button/unmute_channel_button.test.js b/components/channel_header_mobile/unmute_channel_button/unmute_channel_button.test.js new file mode 100644 index 000000000000..178493e9ce62 --- /dev/null +++ b/components/channel_header_mobile/unmute_channel_button/unmute_channel_button.test.js @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {NotificationLevels} from 'utils/constants'; + +import UnmuteChannelButton from './unmute_channel_button'; + +describe('components/ChannelHeaderMobile/UnmuteChannelButton', () => { + const baseProps = { + user: { + id: 'user_id', + }, + channel: { + id: 'channel_id', + }, + actions: { + updateChannelNotifyProps: jest.fn(), + }, + }; + + it('should match snapshot', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should runs updateChannelNotifyProps on click', () => { + const props = baseProps; + props.actions.updateChannelNotifyProps = jest.fn(); + + const wrapper = shallow(); + wrapper.simulate('click'); + + expect(props.actions.updateChannelNotifyProps).toBeCalledWith( + props.user.id, + props.channel.id, + {mark_unread: NotificationLevels.ALL}, + ); + }); +}); diff --git a/components/channel_layout/center_channel/center_channel.jsx b/components/channel_layout/center_channel/center_channel.jsx index 2d76fab6f4e1..889392a769ed 100644 --- a/components/channel_layout/center_channel/center_channel.jsx +++ b/components/channel_layout/center_channel/center_channel.jsx @@ -7,7 +7,7 @@ import {Route, Switch, Redirect} from 'react-router-dom'; import classNames from 'classnames'; import PermalinkView from 'components/permalink_view'; -import Navbar from 'components/navbar'; +import ChannelHeaderMobile from 'components/channel_header_mobile'; import ChannelIdentifierRouter from 'components/channel_layout/channel_identifier_router'; export default class CenterChannel extends React.PureComponent { @@ -47,7 +47,7 @@ export default class CenterChannel extends React.PureComponent { >
    diff --git a/components/channel_members_modal/channel_members_modal.jsx b/components/channel_members_modal/channel_members_modal.jsx index cac5f74a0205..58eda4c17c78 100644 --- a/components/channel_members_modal/channel_members_modal.jsx +++ b/components/channel_members_modal/channel_members_modal.jsx @@ -7,16 +7,30 @@ import {Modal} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; import MemberListChannel from 'components/member_list_channel'; +import ChannelInviteModal from 'components/channel_invite_modal'; +import {ModalIdentifiers} from 'utils/constants'; export default class ChannelMembersModal extends React.PureComponent { static propTypes = { + + /** + * Bool whether user has permission to manage current channel + */ canManageChannelMembers: PropTypes.bool.isRequired, - channel: PropTypes.shape({ - display_name: PropTypes.string.isRequired, - delete_at: PropTypes.number.isRequired, + + /** + * Object with info about current channel + */ + channel: PropTypes.object.isRequired, + + /** + * Function that is called when modal is hidden + */ + onHide: PropTypes.func.isRequired, + + actions: PropTypes.shape({ + openModal: PropTypes.func.isRequired, }).isRequired, - onModalDismissed: PropTypes.func.isRequired, - showInviteModal: PropTypes.func.isRequired, } constructor(props) { @@ -27,15 +41,26 @@ export default class ChannelMembersModal extends React.PureComponent { }; } - onHide = () => { + handleHide = () => { this.setState({ show: false, }); } - onClickManageChannelsButton = () => { - this.props.showInviteModal(); - this.onHide(); + handleExit = () => { + this.props.onHide(); + } + + onAddNewMembersButton = () => { + const {channel, actions} = this.props; + + actions.openModal({ + modalId: ModalIdentifiers.CHANNEL_INVITE, + dialogType: ChannelInviteModal, + dialogProps: {channel}, + }); + + this.handleExit(); } render() { @@ -44,9 +69,9 @@ export default class ChannelMembersModal extends React.PureComponent {
    @@ -61,7 +86,7 @@ export default class ChannelMembersModal extends React.PureComponent { id='showInviteModal' className='btn btn-md btn-primary' href='#' - onClick={this.onClickManageChannelsButton} + onClick={this.onAddNewMembersButton} > { const baseProps = { - channel: {id: 'channel_id', display_name: 'channel_display_name', delete_at: 0}, + channel: { + id: 'channel_id', + display_name: 'channel_display_name', + delete_at: 0, + }, canManageChannelMembers: true, - onModalDismissed: () => { }, //eslint-disable-line no-empty-function - showInviteModal: () => { }, //eslint-disable-line no-empty-function + onHide: jest.fn(), + actions: { + openModal: jest.fn(), + }, }; test('should match snapshot', () => { @@ -29,21 +38,27 @@ describe('components/ChannelMembersModal', () => { ); wrapper.setState({show: true}); - wrapper.instance().onHide(); + wrapper.instance().handleHide(); expect(wrapper.state('show')).toEqual(false); }); - test('should have called props.showInviteModal and match state when onClickManageChannelsButton is called', () => { - const showInviteModal = jest.fn(); - const props = {...baseProps, showInviteModal}; + test('should have called props.actions.openModal and props.onHide when onAddNewMembersButton is called', () => { + const onHide = jest.fn(); + const openModal = jest.fn(); + const props = { + ...baseProps, + onHide, + actions: { + openModal, + }, + }; const wrapper = shallow( ); - wrapper.setState({show: true}); - wrapper.instance().onClickManageChannelsButton(); - expect(showInviteModal).toHaveBeenCalledTimes(1); - expect(wrapper.state('show')).toEqual(false); + wrapper.instance().onAddNewMembersButton(); + expect(openModal).toHaveBeenCalledTimes(1); + expect(onHide).toHaveBeenCalledTimes(1); }); test('should have state when Modal.onHide', () => { @@ -56,17 +71,6 @@ describe('components/ChannelMembersModal', () => { expect(wrapper.state('show')).toEqual(false); }); - test('should have called props.onModalDismissed when Modal.onExited', () => { - const onModalDismissed = jest.fn(); - const props = {...baseProps, onModalDismissed}; - const wrapper = shallow( - - ); - - wrapper.find(Modal).first().props().onExited(); - expect(onModalDismissed).toHaveBeenCalledTimes(1); - }); - test('should match snapshot with archived channel', () => { const props = {...baseProps, channel: {...baseProps.channel, delete_at: 1234}}; @@ -76,21 +80,6 @@ describe('components/ChannelMembersModal', () => { expect(wrapper).toMatchSnapshot(); }); -}); - -describe('components/ChannelMembersModal', () => { - const baseProps = { - channel: { - display_name: 'testchannel', - header: '', - name: 'testchannel', - purpose: '', - delete_at: 0, - }, - canManageChannelMembers: true, - onModalDismissed: () => {}, // eslint-disable-line no-empty-function - showInviteModal: () => {}, // eslint-disable-line no-empty-function - }; test('renders the channel display name', () => { const wrapper = shallow( @@ -99,17 +88,6 @@ describe('components/ChannelMembersModal', () => { expect(wrapper.find('.name').text()).toBe(baseProps.channel.display_name); }); - test('should call the onHide callback when the modal is hidden', () => { - const onModalDismissed = jest.fn(); - const newProps = {...baseProps, onModalDismissed}; - const wrapper = shallow( - - ); - expect(onModalDismissed).not.toHaveBeenCalled(); - wrapper.find(Modal).first().props().onExited(); - expect(onModalDismissed).toHaveBeenCalled(); - }); - test('should show the invite modal link if the user can manage channel members', () => { const newProps = {...baseProps, canManageChannelMembers: true}; const wrapper = shallow( @@ -126,14 +104,24 @@ describe('components/ChannelMembersModal', () => { expect(wrapper.find('#showInviteModal').length).toBe(0); }); - test('should call showInviteModal when the invite modal link is clicked', () => { - const showInviteModal = jest.fn(); - const newProps = {...baseProps, canManageChannelMembers: false, showInviteModal}; + test('should call openModal with ChannelInviteModal when the add members link is clicked', () => { + const openModal = jest.fn(); + const newProps = { + ...baseProps, + canManageChannelMembers: false, + actions: { + openModal, + }, + }; const wrapper = shallow( ); - expect(showInviteModal).not.toHaveBeenCalled(); - wrapper.instance().onClickManageChannelsButton(); - expect(showInviteModal).toHaveBeenCalled(); + expect(openModal).not.toHaveBeenCalled(); + wrapper.instance().onAddNewMembersButton(); + expect(openModal).toHaveBeenCalledWith({ + modalId: ModalIdentifiers.CHANNEL_INVITE, + dialogType: ChannelInviteModal, + dialogProps: {channel: newProps.channel}, + }); }); }); diff --git a/components/channel_members_modal/index.js b/components/channel_members_modal/index.js index 754cad8490fc..e98f30ebad34 100644 --- a/components/channel_members_modal/index.js +++ b/components/channel_members_modal/index.js @@ -1,15 +1,20 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels'; -import ChannelMembersModal from './channel_members_modal.jsx'; +import {openModal} from 'actions/views/modals'; -function mapStateToProps(state) { - return { - canManageChannelMembers: canManageChannelMembers(state), - }; -} +import ChannelMembersModal from './channel_members_modal'; -export default connect(mapStateToProps)(ChannelMembersModal); +const mapStateToProps = (state) => ({ + canManageChannelMembers: canManageChannelMembers(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({openModal}, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ChannelMembersModal); diff --git a/components/channel_notifications_modal/channel_notifications_modal.jsx b/components/channel_notifications_modal/channel_notifications_modal.jsx index 9ed621379d9c..139c810e3a37 100644 --- a/components/channel_notifications_modal/channel_notifications_modal.jsx +++ b/components/channel_notifications_modal/channel_notifications_modal.jsx @@ -14,15 +14,42 @@ import * as Utils from 'utils/utils.jsx'; import NotificationSection from 'components/channel_notifications_modal/components/notification_section.jsx'; -export default class ChannelNotificationsModal extends React.Component { +export default class ChannelNotificationsModal extends React.PureComponent { static propTypes = { - show: PropTypes.bool.isRequired, + + /** + * Function that is called when modal is hidden + */ onHide: PropTypes.func.isRequired, + + /** + * Object with info about current channel + */ channel: PropTypes.object.isRequired, + + /** + * Object with info about current channel membership + */ channelMember: PropTypes.object.isRequired, + + /** + * Object with info about current user + */ currentUser: PropTypes.object.isRequired, + + /** + * Boolean whether server sends push notifications + */ sendPushNotifications: PropTypes.bool.isRequired, + + /* + * Object with redux action creators + */ actions: PropTypes.shape({ + + /* + * Action creator to update channel notify props + */ updateChannelNotifyProps: PropTypes.func.isRequired, }), }; @@ -31,6 +58,7 @@ export default class ChannelNotificationsModal extends React.Component { super(props); this.state = { + show: true, activeSection: NotificationSections.NONE, serverError: null, ...this.getStateFromNotifyProps(props.channelMember.notify_props), @@ -55,9 +83,14 @@ export default class ChannelNotificationsModal extends React.Component { }; } - handleOnHide = () => { - this.updateSection(NotificationSections.NONE); + handleHide = () => { + this.setState({ + show: false, + }); + } + handleExit = () => { + this.updateSection(NotificationSections.NONE); this.props.onHide(); } @@ -152,7 +185,6 @@ export default class ChannelNotificationsModal extends React.Component { channelMember, currentUser, sendPushNotifications, - show, } = this.props; let serverErrorTag = null; @@ -162,10 +194,10 @@ export default class ChannelNotificationsModal extends React.Component { return ( diff --git a/components/channel_notifications_modal/channel_notifications_modal.test.jsx b/components/channel_notifications_modal/channel_notifications_modal.test.jsx index 23b7c1e508af..64e7f6addd17 100644 --- a/components/channel_notifications_modal/channel_notifications_modal.test.jsx +++ b/components/channel_notifications_modal/channel_notifications_modal.test.jsx @@ -60,19 +60,19 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () ); wrapper.setState({activeSection: NotificationSections.DESKTOP, desktopNotifyLevel: NotificationLevels.NONE}); - wrapper.instance().handleOnHide(); + wrapper.instance().handleExit(); expect(onHide).toHaveBeenCalledTimes(1); expect(wrapper.state('activeSection')).toEqual(NotificationSections.NONE); expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.ALL); wrapper.setState({activeSection: NotificationSections.MARK_UNREAD, markUnreadNotifyLevel: NotificationLevels.NONE}); - wrapper.instance().handleOnHide(); + wrapper.instance().handleExit(); expect(onHide).toHaveBeenCalledTimes(2); expect(wrapper.state('activeSection')).toEqual(NotificationSections.NONE); expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.ALL); wrapper.setState({activeSection: NotificationSections.PUSH, pushNotifyLevel: NotificationLevels.NONE}); - wrapper.instance().handleOnHide(); + wrapper.instance().handleExit(); expect(onHide).toHaveBeenCalledTimes(3); expect(wrapper.state('activeSection')).toEqual(NotificationSections.NONE); expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.DEFAULT); diff --git a/components/channel_notifications_modal/index.js b/components/channel_notifications_modal/index.js index 5ed7ee618109..8970f7a0abbb 100644 --- a/components/channel_notifications_modal/index.js +++ b/components/channel_notifications_modal/index.js @@ -1,29 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; - +import {connect} from 'react-redux'; import {updateChannelNotifyProps} from 'mattermost-redux/actions/channels'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; import ChannelNotificationsModal from './channel_notifications_modal.jsx'; -function mapStateToProps(state) { - const config = getConfig(state); - const sendPushNotifications = config.SendPushNotifications === 'true'; - - return { - sendPushNotifications, - }; -} +const mapStateToProps = (state) => ({ + channelMember: getMyCurrentChannelMembership(state), + sendPushNotifications: getConfig(state).SendPushNotifications === 'true', +}); -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - updateChannelNotifyProps, - }, dispatch), - }; -} +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ + updateChannelNotifyProps, + }, dispatch), +}); export default connect(mapStateToProps, mapDispatchToProps)(ChannelNotificationsModal); diff --git a/components/channel_view/index.js b/components/channel_view/index.js index ed98b4b1bff2..4b5155c07089 100644 --- a/components/channel_view/index.js +++ b/components/channel_view/index.js @@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {createSelector} from 'reselect'; import {getInt} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {withRouter} from 'react-router-dom'; @@ -27,7 +28,7 @@ const getDeactivatedChannel = createSelector( ); function mapStateToProps(state) { - const channel = state.entities.channels.channels[state.entities.channels.currentChannelId]; + const channel = getCurrentChannel(state); const config = getConfig(state); const enableTutorial = config.EnableTutorial === 'true'; diff --git a/components/edit_channel_purpose_modal/__snapshots__/edit_channel_purpose_modal.test.jsx.snap b/components/edit_channel_purpose_modal/__snapshots__/edit_channel_purpose_modal.test.jsx.snap index 8393b42d5926..38776671e09b 100644 --- a/components/edit_channel_purpose_modal/__snapshots__/edit_channel_purpose_modal.test.jsx.snap +++ b/components/edit_channel_purpose_modal/__snapshots__/edit_channel_purpose_modal.test.jsx.snap @@ -6,7 +6,6 @@ exports[`comoponents/EditChannelPurposeModal clear error on next 1`] = ` autoFocus={true} backdrop={true} bsClass="modal" - className="modal-edit-channel-purpose" dialogComponentClass={[Function]} enforceFocus={true} keyboard={true} @@ -104,7 +103,6 @@ exports[`comoponents/EditChannelPurposeModal match with modal error 1`] = ` autoFocus={true} backdrop={true} bsClass="modal" - className="modal-edit-channel-purpose" dialogComponentClass={[Function]} enforceFocus={true} keyboard={true} @@ -212,7 +210,6 @@ exports[`comoponents/EditChannelPurposeModal match with modal error with fake id autoFocus={true} backdrop={true} bsClass="modal" - className="modal-edit-channel-purpose" dialogComponentClass={[Function]} enforceFocus={true} keyboard={true} @@ -320,7 +317,6 @@ exports[`comoponents/EditChannelPurposeModal should match for private channel 1` autoFocus={true} backdrop={true} bsClass="modal" - className="modal-edit-channel-purpose" dialogComponentClass={[Function]} enforceFocus={true} keyboard={true} @@ -418,7 +414,6 @@ exports[`comoponents/EditChannelPurposeModal should match on init 1`] = ` autoFocus={true} backdrop={true} bsClass="modal" - className="modal-edit-channel-purpose" dialogComponentClass={[Function]} enforceFocus={true} keyboard={true} @@ -516,7 +511,6 @@ exports[`comoponents/EditChannelPurposeModal should match submitted 1`] = ` autoFocus={true} backdrop={true} bsClass="modal" - className="modal-edit-channel-purpose" dialogComponentClass={[Function]} enforceFocus={true} keyboard={true} @@ -614,7 +608,6 @@ exports[`comoponents/EditChannelPurposeModal should match with display name 1`] autoFocus={true} backdrop={true} bsClass="modal" - className="modal-edit-channel-purpose" dialogComponentClass={[Function]} enforceFocus={true} keyboard={true} diff --git a/components/edit_channel_purpose_modal/edit_channel_purpose_modal.jsx b/components/edit_channel_purpose_modal/edit_channel_purpose_modal.jsx index 6c37a5d3a443..dbe13996a8b3 100644 --- a/components/edit_channel_purpose_modal/edit_channel_purpose_modal.jsx +++ b/components/edit_channel_purpose_modal/edit_channel_purpose_modal.jsx @@ -10,9 +10,14 @@ import {RequestStatus} from 'mattermost-redux/constants'; import Constants from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; -export default class EditChannelPurposeModal extends React.Component { +export default class EditChannelPurposeModal extends React.PureComponent { static propTypes = { + /* + * callback to call when modal will hide + */ + onHide: PropTypes.func.isRequired, + /* * Channel info object */ @@ -33,11 +38,6 @@ export default class EditChannelPurposeModal extends React.Component { */ requestStatus: PropTypes.string.isRequired, - /* - * Callback to call on modal hide - */ - onModalDismissed: PropTypes.func.isRequired, - /* * Object with redux action creators */ @@ -66,7 +66,7 @@ export default class EditChannelPurposeModal extends React.Component { const {requestStatus} = this.props; if (requestStatus !== nextRequestStatus && nextRequestStatus === RequestStatus.SUCCESS) { - this.handleHide(); + this.onHide(); } if (requestStatus !== nextRequestStatus && nextRequestStatus === RequestStatus.FAILURE) { @@ -97,7 +97,7 @@ export default class EditChannelPurposeModal extends React.Component { Utils.placeCaretAtEnd(this.purpose); } - handleHide = () => { + onHide = () => { this.setState({show: false}); } @@ -180,12 +180,10 @@ export default class EditChannelPurposeModal extends React.Component { return ( @@ -211,7 +209,7 @@ export default class EditChannelPurposeModal extends React.Component { -
    -
    - - - - - - - -
      -
    • - - - -
    • -
    • - -
    • -
    • - -
    • -
      - × -
      -
    -
    -
    - - - -
    -
    - - - - -
    -`; - -exports[`components/navbar/Navbar should match snapshot, for default channel 1`] = ` -
    - - - - -
    -`; - -exports[`components/navbar/Navbar should match snapshot, for private channel 1`] = ` -
    - - - - -
    -`; - -exports[`components/navbar/Navbar should match snapshot, if not licensed 1`] = ` -
    - - - - -
    -`; - -exports[`components/navbar/Navbar should match snapshot, invalid state 1`] = `""`; - -exports[`components/navbar/Navbar should match snapshot, renderEditChannelHeaderOption 1`] = `null`; - -exports[`components/navbar/Navbar should match snapshot, valid state 1`] = ` -
    - - - - -
    -`; diff --git a/components/navbar/index.js b/components/navbar/index.js deleted file mode 100644 index 6227576e5b23..000000000000 --- a/components/navbar/index.js +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; - -import {updateChannelNotifyProps, favoriteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels'; -import {isCurrentChannelReadOnly, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {isFavoriteChannel} from 'mattermost-redux/utils/channel_utils'; - -import {leaveChannel} from 'actions/views/channel'; -import { - closeRightHandSide as closeRhs, - updateRhsState, - showPinnedPosts, - toggleMenu as toggleRhsMenu, - closeMenu as closeRhsMenu, -} from 'actions/views/rhs'; -import {toggle as toggleLhs, close as closeLhs} from 'actions/views/lhs'; -import {getRhsState} from 'selectors/rhs'; -import {RHSStates} from 'utils/constants.jsx'; - -import Navbar from './navbar.jsx'; - -function mapStateToProps(state) { - const prefs = state.entities.preferences.myPreferences; - const currentChannelId = getCurrentChannelId(state); - - const rhsState = getRhsState(state); - - return { - isPinnedPosts: rhsState === RHSStates.PIN, - isReadOnly: isCurrentChannelReadOnly(state), - isFavoriteChannel: isFavoriteChannel(prefs, currentChannelId), - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - closeLhs, - closeRhs, - closeRhsMenu, - leaveChannel, - markFavorite: favoriteChannel, - showPinnedPosts, - toggleLhs, - toggleRhsMenu, - unmarkFavorite: unfavoriteChannel, - updateChannelNotifyProps, - updateRhsState, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(Navbar); diff --git a/components/navbar/navbar.jsx b/components/navbar/navbar.jsx deleted file mode 100644 index 8615a4f4cc5c..000000000000 --- a/components/navbar/navbar.jsx +++ /dev/null @@ -1,909 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import $ from 'jquery'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import {Link} from 'react-router-dom'; - -import {Permissions} from 'mattermost-redux/constants'; -import {isChannelMuted} from 'mattermost-redux/utils/channel_utils'; - -import EditChannelHeaderModal from 'components/edit_channel_header_modal'; -import EditChannelPurposeModal from 'components/edit_channel_purpose_modal'; -import * as GlobalActions from 'actions/global_actions.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; -import ModalStore from 'stores/modal_store.jsx'; -import PreferenceStore from 'stores/preference_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; -import UserStore from 'stores/user_store.jsx'; -import { - ActionTypes, - Constants, - ModalIdentifiers, - NotificationLevels, - RHSStates, -} from 'utils/constants.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import ConvertChannelModal from 'components/convert_channel_modal'; -import ChannelInfoModal from 'components/channel_info_modal'; -import ChannelInviteModal from 'components/channel_invite_modal'; -import ChannelMembersModal from 'components/channel_members_modal'; -import ChannelNotificationsModal from 'components/channel_notifications_modal'; -import DeleteChannelModal from 'components/delete_channel_modal'; -import MoreDirectChannels from 'components/more_direct_channels'; -import NotifyCounts from 'components/notify_counts'; -import QuickSwitchModal from 'components/quick_switch_modal'; -import RenameChannelModal from 'components/rename_channel_modal'; -import StatusIcon from 'components/status_icon.jsx'; -import MenuIcon from 'components/svg/menu_icon'; -import SearchIcon from 'components/svg/search_icon'; -import ToggleModalButton from 'components/toggle_modal_button.jsx'; -import ToggleModalButtonRedux from 'components/toggle_modal_button_redux'; -import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; -import TeamPermissionGate from 'components/permissions_gates/team_permission_gate'; - -import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug'; - -import NavbarInfoButton from './navbar_info_button'; - -export default class Navbar extends React.Component { - static propTypes = { - teamDisplayName: PropTypes.string, - isPinnedPosts: PropTypes.bool, - isReadOnly: PropTypes.bool, - isFavoriteChannel: PropTypes.bool.isRequired, - actions: PropTypes.shape({ - closeLhs: PropTypes.func.isRequired, - closeRhs: PropTypes.func.isRequired, - closeRhsMenu: PropTypes.func.isRequired, - leaveChannel: PropTypes.func.isRequired, - markFavorite: PropTypes.func.isRequired, - showPinnedPosts: PropTypes.func, - toggleLhs: PropTypes.func.isRequired, - toggleRhsMenu: PropTypes.func.isRequired, - unmarkFavorite: PropTypes.func.isRequired, - updateChannelNotifyProps: PropTypes.func.isRequired, - updateRhsState: PropTypes.func.isRequired, - }).isRequired, - }; - - static defaultProps = { - teamDisplayName: '', - }; - - constructor(props) { - super(props); - - this.state = { - ...this.getStateFromStores(), - showEditChannelPurposeModal: false, - showMembersModal: false, - showRenameChannelModal: false, - showQuickSwitchModal: false, - showChannelNotificationsModal: false, - quickSwitchMode: 'channel', - }; - } - - componentDidMount() { - ChannelStore.addChangeListener(this.onChange); - ChannelStore.addStatsChangeListener(this.onChange); - UserStore.addStatusesChangeListener(this.onChange); - UserStore.addChangeListener(this.onChange); - PreferenceStore.addChangeListener(this.onChange); - ModalStore.addModalListener(ActionTypes.TOGGLE_QUICK_SWITCH_MODAL, this.toggleQuickSwitchModal); - ModalStore.addModalListener(ActionTypes.TOGGLE_CHANNEL_PURPOSE_UPDATE_MODAL, this.showChannelPurposeModal); - ModalStore.addModalListener(ActionTypes.TOGGLE_CHANNEL_NAME_UPDATE_MODAL, this.showRenameChannelModal); - document.addEventListener('keydown', this.handleQuickSwitchKeyPress); - $('.inner-wrap').on('click', this.hideSidebars); - } - - componentWillUnmount() { - ChannelStore.removeChangeListener(this.onChange); - ChannelStore.removeStatsChangeListener(this.onChange); - UserStore.removeStatusesChangeListener(this.onChange); - UserStore.removeChangeListener(this.onChange); - PreferenceStore.removeChangeListener(this.onChange); - ModalStore.removeModalListener(ActionTypes.TOGGLE_QUICK_SWITCH_MODAL, this.toggleQuickSwitchModal); - ModalStore.removeModalListener(ActionTypes.TOGGLE_CHANNEL_PURPOSE_UPDATE_MODAL, this.showChannelPurposeModal); - ModalStore.removeModalListener(ActionTypes.TOGGLE_CHANNEL_NAME_UPDATE_MODAL, this.showRenameChannelModal); - document.removeEventListener('keydown', this.handleQuickSwitchKeyPress); - $('.inner-wrap').off('click', this.hideSidebars); - } - - getStateFromStores = () => { - const channel = ChannelStore.getCurrent(); - - let contactId = null; - if (channel && channel.type === 'D') { - contactId = Utils.getUserIdFromChannelName(channel); - } - - return { - channel, - member: ChannelStore.getCurrentMember(), - users: [], - userCount: ChannelStore.getCurrentStats().member_count, - currentUser: UserStore.getCurrentUser(), - contactId, - }; - } - - isStateValid = () => { - return this.state.channel && this.state.member && this.state.users && this.state.currentUser; - } - - handleLeave = () => { - const {actions} = this.props; - if (this.state.channel.type === Constants.PRIVATE_CHANNEL) { - GlobalActions.showLeavePrivateChannelModal(this.state.channel); - } else { - actions.leaveChannel(this.state.channel.id); - } - } - - hideSidebars = (e) => { - var windowWidth = $(window).outerWidth(); - if (windowWidth <= 768) { - this.props.actions.closeRhs(); - - if (e.target.className !== 'navbar-toggle' && e.target.className !== 'icon-bar') { - this.props.actions.closeLhs(); - this.props.actions.closeRhs(); - this.props.actions.closeRhsMenu(); - } - } - } - - toggleLeftSidebar = () => { - this.props.actions.toggleLhs(); - } - - toggleRightSidebar = () => { - this.props.actions.toggleRhsMenu(); - } - - showSearch = () => { - this.props.actions.updateRhsState(RHSStates.SEARCH); - } - - onChange = () => { - this.setState(this.getStateFromStores()); - } - - showChannelNotificationsModal = (e) => { - e.preventDefault(); - - this.setState({ - showChannelNotificationsModal: true, - }); - } - - hideChannelNotificationsModal = () => { - this.setState({ - showChannelNotificationsModal: false, - }); - } - - showChannelPurposeModal = () => { - this.setState({ - showEditChannelPurposeModal: true, - }); - } - - hideChannelPurposeModal = () => { - this.setState({ - showEditChannelPurposeModal: false, - }); - } - - showRenameChannelModal = () => { - this.setState({ - showRenameChannelModal: true, - }); - } - - hideRenameChannelModal = () => { - this.setState({ - showRenameChannelModal: false, - }); - } - - showMembersModal = (e) => { - e.preventDefault(); - - this.setState({showMembersModal: true}); - } - - hideMembersModal = () => { - this.setState({showMembersModal: false}); - } - - handleQuickSwitchKeyPress = (e) => { - if (Utils.cmdOrCtrlPressed(e) && !e.shiftKey && Utils.isKeyPressed(e, Constants.KeyCodes.K)) { - if (!e.altKey) { - e.preventDefault(); - this.toggleQuickSwitchModal('channel'); - } - } - } - - toggleQuickSwitchModal = (mode = 'channel') => { - if (this.state.showQuickSwitchModal) { - this.setState({showQuickSwitchModal: false, quickSwitchMode: 'channel'}); - } else { - this.setState({showQuickSwitchModal: true, quickSwitchMode: mode}); - } - } - - hideQuickSwitchModal = () => { - this.setState({ - showQuickSwitchModal: false, - quickSwitchMode: 'channel', - }); - } - - getPinnedPosts = (e) => { - e.preventDefault(); - if (this.props.isPinnedPosts) { - GlobalActions.emitCloseRightHandSide(); - } else { - this.props.actions.showPinnedPosts(this.state.channel.id); - } - } - - toggleFavorite = (e) => { - const {markFavorite, unmarkFavorite} = this.props.actions; - e.preventDefault(); - - if (this.props.isFavoriteChannel) { - unmarkFavorite(this.state.channel.id); - } else { - markFavorite(this.state.channel.id); - } - }; - - renderEditChannelHeaderOption = (channel) => { - if (!channel || !channel.id) { - return null; - } - - return ( -
  • - - - -
  • - ); - } - - handleUnmuteChannel = () => { - const {channel, currentUser} = this.state; - - if (!currentUser || !channel) { - return; - } - - const props = {mark_unread: NotificationLevels.ALL}; - this.props.actions.updateChannelNotifyProps(currentUser.id, channel.id, props); - }; - - createDropdown = (teamId, channel, channelTitle, isDirect, isGroup) => { - if (channel) { - let viewInfoOption; - let viewPinnedPostsOption; - let addMembersOption; - let manageMembersOption; - let setChannelHeaderOption; - let setChannelPurposeOption; - let notificationPreferenceOption; - let renameChannelOption; - let convertChannelOption; - let deleteChannelOption; - let leaveChannelOption; - - if (isDirect) { - setChannelHeaderOption = this.renderEditChannelHeaderOption(channel); - } else if (isGroup) { - setChannelHeaderOption = this.renderEditChannelHeaderOption(channel); - - notificationPreferenceOption = ( -
  • - -
  • - ); - - addMembersOption = ( -
  • - - - -
  • - ); - } else { - const isPrivate = channel.type === Constants.PRIVATE_CHANNEL; - viewInfoOption = ( -
  • - - - -
  • - ); - - viewPinnedPostsOption = ( -
  • - -
  • - ); - - if (ChannelStore.isDefault(channel)) { - manageMembersOption = ( -
  • - -
  • - ); - } else { - addMembersOption = ( - -
  • - - - -
  • -
    - ); - - manageMembersOption = ( -
  • - -
  • - ); - } - - notificationPreferenceOption = ( -
  • - -
  • - ); - - if (!this.props.isReadOnly) { - setChannelHeaderOption = ( - - {this.renderEditChannelHeaderOption(channel)} - - ); - - setChannelPurposeOption = ( - -
  • - -
  • -
    - ); - - if (!ChannelStore.isDefault(channel) && channel.type === Constants.OPEN_CHANNEL) { - convertChannelOption = ( - -
  • - - - -
  • -
    - ); - } - - renameChannelOption = ( - -
  • - -
  • -
    - ); - } - - if (!ChannelStore.isDefault(channel)) { - deleteChannelOption = ( - -
  • - - - -
  • -
    - ); - - leaveChannelOption = ( -
  • - -
  • - ); - } - } - - const toggleFavoriteOption = ( -
  • - -
  • - ); - - const channelMuted = isChannelMuted(this.state.member); - - return ( -
    -
    - - {channelTitle} - - -
      - {viewInfoOption} - {viewPinnedPostsOption} - {notificationPreferenceOption} - {addMembersOption} - {manageMembersOption} - {setChannelHeaderOption} - {setChannelPurposeOption} - {renameChannelOption} - {convertChannelOption} - {deleteChannelOption} - {leaveChannelOption} - {toggleFavoriteOption} -
    • - -
    • -
      - {'×'} -
      -
    -
    - {channelMuted && - - } -
    - ); - } - - return ( -
    - - {channelTitle} - -
    - ); - } - - createLhsButton = (currentId) => { - let lhsButton; - - if (currentId == null) { - lhsButton = ( - - ); - } else { - lhsButton = ( - - ); - } - - return lhsButton; - } - - createRhsButton = (currentId) => { - let rhsButton; - if (currentId != null) { - rhsButton = ( - - ); - } - - return rhsButton; - } - - getTeammateStatus = () => { - const channel = this.state.channel; - - // get status for direct message channels - if (channel.type === 'D') { - const currentUserId = this.state.currentUser.id; - const teammate = this.state.users.find((user) => user.id !== currentUserId); - if (teammate) { - return UserStore.getStatus(teammate.id); - } - } - return null; - } - - showChannelInviteModalButton = () => { - if (this.refs.channelInviteModalButton) { - this.refs.channelInviteModalButton.show(); - } - } - - render() { - if (!this.isStateValid()) { - return null; - } - - var currentId = this.state.currentUser.id; - var channel = this.state.channel; - var channelTitle = this.props.teamDisplayName; - var isDirect = false; - let isGroup = false; - const teamId = channel && channel.team_id; - - var editChannelPurposeModal = null; - let renameChannelModal = null; - let channelMembersModal = null; - let channelNotificationsModal = null; - let quickSwitchModal = null; - - if (channel) { - channelTitle = channel.display_name; - - if (channel.type === Constants.DM_CHANNEL) { - isDirect = true; - const teammateId = Utils.getUserIdFromChannelName(channel); - if (currentId === teammateId) { - channelTitle = ( - - ); - } else { - channelTitle = Utils.getDisplayNameByUserId(teammateId); - } - } else if (channel.type === Constants.GM_CHANNEL) { - isGroup = true; - } - - if (this.state.showEditChannelPurposeModal) { - editChannelPurposeModal = ( - - ); - } - - renameChannelModal = ( - - ); - - if (this.state.showMembersModal) { - channelMembersModal = ( - - ); - } - - channelNotificationsModal = ( - - ); - - quickSwitchModal = ( - - ); - } - - const searchButton = ( - - ); - - const channelMenuDropdown = this.createDropdown(teamId, channel, channelTitle, isDirect, isGroup); - - return ( -
    - - {editChannelPurposeModal} - {renameChannelModal} - {channelMembersModal} - {channelNotificationsModal} - {quickSwitchModal} -
    - ); - } -} diff --git a/components/navbar/navbar.test.jsx b/components/navbar/navbar.test.jsx deleted file mode 100644 index bb21874a337b..000000000000 --- a/components/navbar/navbar.test.jsx +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {shallow} from 'enzyme'; - -import Navbar from 'components/navbar/navbar.jsx'; - -jest.mock('utils/browser_history', () => { - const original = require.requireActual('utils/browser_history'); - return { - ...original, - browserHistory: { - push: jest.fn(), - }, - }; -}); - -describe('components/navbar/Navbar', () => { - const baseProps = { - teamDisplayName: 'team_display_name', - isPinnedPosts: true, - actions: { - closeLhs: jest.fn(), - closeRhs: jest.fn(), - closeRhsMenu: jest.fn(), - leaveChannel: jest.fn(), - markFavorite: jest.fn(), - showPinnedPosts: jest.fn(), - toggleLhs: jest.fn(), - toggleRhsMenu: jest.fn(), - unmarkFavorite: jest.fn(), - updateChannelNotifyProps: jest.fn(), - updateRhsState: jest.fn(), - }, - isLicensed: true, - isFavoriteChannel: false, - }; - - const validState = { - channel: {type: 'O', id: 'channel_id', display_name: 'display_name', team_id: 'team_id'}, - member: {id: 'member_id'}, - users: [{id: 'user_id_1'}], - currentUser: {id: 'current_user_id'}, - }; - - test('should match snapshot, invalid state', () => { - const wrapper = shallow( - - ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot, valid state', () => { - const wrapper = shallow( - - ); - - wrapper.setState(validState); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot, for default channel', () => { - const wrapper = shallow( - - ); - - const channel = {type: 'O', id: '123', name: 'town-square', display_name: 'Town Square', team_id: 'team_id'}; - wrapper.setState({...validState, channel}); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot, if not licensed', () => { - const wrapper = shallow( - - ); - - wrapper.setState(validState); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot, for DM channel', () => { - const wrapper = shallow( - - ); - - const newValidState = {...validState, channel: {type: 'D', id: 'channel_id', name: 'user_id_1__user_id_2', display_name: 'display_name', team_id: 'team_id'}}; - wrapper.setState(newValidState); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot, for private channel', () => { - const wrapper = shallow( - - ); - - const newValidState = {...validState, channel: {type: 'P', id: 'channel_id', display_name: 'display_name', team_id: 'team_id'}}; - wrapper.setState(newValidState); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot, renderEditChannelHeaderOption', () => { - const wrapper = shallow( - - ); - - const editChannelHeaderOption = wrapper.instance().renderEditChannelHeaderOption(wrapper.state('channel')); - - expect(editChannelHeaderOption).toMatchSnapshot(); - }); - - test('should match state when corresponding function is called', () => { - const wrapper = shallow( - - ); - - wrapper.setState({showEditChannelPurposeModal: false}); - wrapper.instance().showChannelPurposeModal(); - expect(wrapper.state('showEditChannelPurposeModal')).toEqual(true); - wrapper.instance().hideChannelPurposeModal(); - expect(wrapper.state('showEditChannelPurposeModal')).toEqual(false); - - wrapper.setState({showRenameChannelModal: false}); - wrapper.instance().showRenameChannelModal(); - expect(wrapper.state('showRenameChannelModal')).toEqual(true); - wrapper.instance().hideRenameChannelModal(); - expect(wrapper.state('showRenameChannelModal')).toEqual(false); - - wrapper.setState({showMembersModal: false}); - wrapper.instance().showMembersModal({preventDefault: jest.fn()}); - expect(wrapper.state('showMembersModal')).toEqual(true); - wrapper.instance().hideMembersModal(); - expect(wrapper.state('showMembersModal')).toEqual(false); - - wrapper.setState({showQuickSwitchModal: false, quickSwitchMode: 'channel'}); - wrapper.instance().toggleQuickSwitchModal('other_mode'); - expect(wrapper.state('showQuickSwitchModal')).toEqual(true); - expect(wrapper.state('quickSwitchMode')).toEqual('other_mode'); - wrapper.instance().toggleQuickSwitchModal(); - expect(wrapper.state('showQuickSwitchModal')).toEqual(false); - expect(wrapper.state('quickSwitchMode')).toEqual('channel'); - - wrapper.setState({showQuickSwitchModal: true, quickSwitchMode: 'other_mode'}); - wrapper.instance().hideQuickSwitchModal(); - expect(wrapper.state('showQuickSwitchModal')).toEqual(false); - expect(wrapper.state('quickSwitchMode')).toEqual('channel'); - }); - - test('should toggle favorite channel', () => { - const wrapper = shallow( - - ); - - const event = { - preventDefault: jest.fn(), - }; - - wrapper.setState(validState); - wrapper.instance().toggleFavorite(event); - expect(wrapper.instance().props.actions.markFavorite).toBeCalled(); - - wrapper.setProps({isFavoriteChannel: true}); - wrapper.instance().toggleFavorite(event); - expect(wrapper.instance().props.actions.unmarkFavorite).toBeCalled(); - }); - - test('should leave public channel', () => { - const props = { - ...baseProps, - actions: { - ...baseProps.actions, - leaveChannel: jest.fn().mockImplementation(() => { - const data = true; - - return Promise.resolve({data}); - }), - }, - }; - - const channel = { - id: 'channel-1', - name: 'test-channel-1', - display_name: 'Test Channel 1', - type: 'O', - team_id: 'team-1', - }; - - const wrapper = shallow( - - ); - - wrapper.setState({channel}); - wrapper.instance().handleLeave(); - expect(wrapper.instance().props.actions.leaveChannel).toHaveBeenCalledTimes(1); - }); -}); diff --git a/components/popover_list_members/popover_list_members.jsx b/components/popover_list_members/popover_list_members.jsx index d8b420676f71..af576e0ea772 100644 --- a/components/popover_list_members/popover_list_members.jsx +++ b/components/popover_list_members/popover_list_members.jsx @@ -179,7 +179,7 @@ export default class PopoverListMembers extends React.Component { if (this.state.showChannelMembersModal) { channelMembersModal = ( diff --git a/components/quick_switch_modal/quick_switch_modal.jsx b/components/quick_switch_modal/quick_switch_modal.jsx index 56a9933e3099..0ace94302836 100644 --- a/components/quick_switch_modal/quick_switch_modal.jsx +++ b/components/quick_switch_modal/quick_switch_modal.jsx @@ -22,16 +22,6 @@ const TEAM_MODE = 'team'; export default class QuickSwitchModal extends React.PureComponent { static propTypes = { - /** - * The mode to start in when showing the modal, either 'channel' or 'team' - */ - initialMode: PropTypes.string.isRequired, - - /** - * Set to show the modal - */ - show: PropTypes.bool.isRequired, - /** * The function called to hide the modal */ @@ -47,10 +37,6 @@ export default class QuickSwitchModal extends React.PureComponent { }).isRequired, } - static defaultProps = { - initialMode: CHANNEL_MODE, - } - constructor(props) { super(props); @@ -61,16 +47,10 @@ export default class QuickSwitchModal extends React.PureComponent { this.state = { text: '', - mode: props.initialMode, + mode: CHANNEL_MODE, }; } - UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase - if (!this.props.show && nextProps.show) { - this.setState({mode: nextProps.initialMode, text: ''}); - } - } - focusTextbox = () => { if (this.switchBox == null) { return; @@ -270,7 +250,7 @@ export default class QuickSwitchModal extends React.PureComponent { { const baseProps = { - initialMode: 'channel', - show: true, onHide: jest.fn(), showTeamSwitcher: false, actions: { diff --git a/components/rename_channel_modal/__snapshots__/rename_channel_modal.test.jsx.snap b/components/rename_channel_modal/__snapshots__/rename_channel_modal.test.jsx.snap index b9cb02968949..8e410369f5aa 100644 --- a/components/rename_channel_modal/__snapshots__/rename_channel_modal.test.jsx.snap +++ b/components/rename_channel_modal/__snapshots__/rename_channel_modal.test.jsx.snap @@ -22,6 +22,7 @@ exports[`components/RenameChannelModal should match snapshot 1`] = ` } } onEntering={[Function]} + onExited={[MockFunction]} onHide={[Function]} renderBackdrop={[Function]} restoreFocus={true} diff --git a/components/rename_channel_modal/rename_channel_modal.jsx b/components/rename_channel_modal/rename_channel_modal.jsx index 0343b4d4425a..f5b192592532 100644 --- a/components/rename_channel_modal/rename_channel_modal.jsx +++ b/components/rename_channel_modal/rename_channel_modal.jsx @@ -51,11 +51,6 @@ export class RenameChannelModal extends React.PureComponent { */ intl: intlShape.isRequired, - /** - * Determines whether this modal should be shown or not - */ - show: PropTypes.bool.isRequired, - /** * Function that is called when modal is hidden */ @@ -98,6 +93,7 @@ export class RenameChannelModal extends React.PureComponent { nameError: '', displayNameError: '', invalid: false, + show: true, }; } @@ -127,13 +123,12 @@ export class RenameChannelModal extends React.PureComponent { e.preventDefault(); } - this.props.onHide(); - this.setState({ serverError: '', nameError: '', displayNameError: '', invalid: false, + show: false, }); } @@ -272,9 +267,10 @@ export class RenameChannelModal extends React.PureComponent { return ( diff --git a/utils/constants.jsx b/utils/constants.jsx index eb8b17216e1b..af618f562724 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -277,13 +277,17 @@ export const ModalIdentifiers = { DELETE_CHANNEL: 'delete_channel', CHANNEL_NOTIFICATIONS: 'channel_notifications', CHANNEL_INVITE: 'channel_invite', + CHANNEL_MEMBERS: 'channel_members', CREATE_DM_CHANNEL: 'create_dm_channel', EDIT_CHANNEL_HEADER: 'edit_channel_header', + EDIT_CHANNEL_PURPOSE: 'edit_channel_purpose', DELETE_POST: 'delete_post', CONVERT_CHANNEL: 'convert_channel', RESET_STATUS: 'reset_status', LEAVE_TEAM: 'leave_team', + RENAME_CHANNEL: 'rename_channel', USER_SETTINGS: 'user_settings', + QUICK_SWITCH: 'quick_switch', REMOVED_FROM_CHANNEL: 'removed_from_channel', EMAIL_INVITE: 'email_invite', };