mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-18 18:18:23 -05:00
[MM-66862] Channel Info RHS: add ability to rename and open channel settings (#34708)
* Channel Info RHS: add rename-from-info and settings access Add channel name editable area with pencil hover and wire to a lightweight Rename Channel modal; add Channel Settings item to RHS menu with permission checks; ensure navigation after rename uses relative path to avoid 404. * linter changes * add padding so field labels don't get cut off * fixes for keyboard accessibility and tooltips * don't show channel settings for DMs and GMs * chore(i18n): run extract to reorder new keys and fix CI Re-extracted webapp i18n to place newly added keys (editable tooltips and rename modal) in canonical order expected by translation tooling. * use generic_btn.cancel/save for rename modal buttons * chore(i18n): remove unused rename_channel.cancel/save keys * updated tests to account for new elements in the info rhs * add cypress test for new rhs info function * fix linting issues * fixed tests * linter fixes * tweak position of edit button * style tweaks, remove subtitle from info rhs head (redundant now), update archived state * added 'unarchive' button to archived notice, updated translations * fixed tests that I broke in channel info header * add url name to channel info view * update to 'channel handle' instead of url name' * change order of channel handle * add copy button * Update about_area_channel.test.tsx * fixed test and brought back channel subtitle in header for consistency * fixed header test * make channel info rhs scrollable * fix merge issue * Fix lint --------- Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
This commit is contained in:
parent
4049300129
commit
95ba2db4f0
20 changed files with 647 additions and 99 deletions
|
|
@ -227,6 +227,39 @@ describe('Channel Info RHS', () => {
|
|||
cy.uiGetRHS().findByText('header for the tests').should('be.visible');
|
||||
});
|
||||
});
|
||||
it('should be able to rename channel from About area', () => {
|
||||
// # Create a dedicated channel for renaming to avoid affecting other tests
|
||||
cy.apiCreateChannel(testTeam.id, 'channel-to-rename', 'Channel To Rename', 'O').then(({channel}) => {
|
||||
cy.apiAddUserToChannel(channel.id, admin.id);
|
||||
|
||||
// # Go to the channel
|
||||
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
||||
|
||||
// # Open Channel Info RHS
|
||||
cy.get('#channel-info-btn').click();
|
||||
|
||||
// # Click edit on channel name (first Edit in About)
|
||||
cy.uiGetRHS().findAllByLabelText('Edit').first().click({force: true});
|
||||
|
||||
// * Rename Channel modal appears
|
||||
cy.findByRole('heading', {name: /rename channel/i}).should('be.visible');
|
||||
|
||||
// # Fill display name and URL
|
||||
cy.findByPlaceholderText(/enter display name/i).clear().type('Renamed Channel');
|
||||
cy.get('.url-input-button').click();
|
||||
cy.get('.url-input-container input').clear().type('renamed-channel');
|
||||
cy.get('.url-input-container button.url-input-button').click();
|
||||
|
||||
// # Save
|
||||
cy.findByRole('button', {name: /save/i}).click();
|
||||
|
||||
// * URL updated
|
||||
cy.location('pathname').should('include', `/${testTeam.name}/channels/renamed-channel`);
|
||||
|
||||
// * Header shows new name
|
||||
cy.get('#channelHeaderTitle').should('contain', 'Renamed Channel');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('bottom menu', () => {
|
||||
it('should be able to manage notifications', () => {
|
||||
|
|
@ -237,11 +270,29 @@ describe('Channel Info RHS', () => {
|
|||
cy.get('#channel-info-btn').click();
|
||||
|
||||
// # Click on "Notification Preferences"
|
||||
cy.uiGetRHS().findByText('Notification Preferences').should('be.visible').click();
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Notification Preferences').scrollIntoView().should('be.visible').click();
|
||||
|
||||
// * Ensures the modal is there
|
||||
cy.get('.ChannelNotificationModal').should('be.visible');
|
||||
});
|
||||
it('should open Channel Settings from RHS menu', () => {
|
||||
// # Go to test channel
|
||||
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
|
||||
|
||||
// # Close RHS if it's open, then click on the channel info button
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('#rhsCloseButton').length > 0) {
|
||||
cy.get('#rhsCloseButton').click();
|
||||
}
|
||||
cy.get('#channel-info-btn').should('be.visible').click();
|
||||
});
|
||||
|
||||
// * Channel Settings item is visible in RHS menu
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Channel Settings').scrollIntoView().should('be.visible').click();
|
||||
|
||||
// * Channel Settings modal opens
|
||||
cy.get('.ChannelSettingsModal').should('be.visible');
|
||||
});
|
||||
it('should be able to view files and come back', () => {
|
||||
// # Go to test channel
|
||||
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
|
||||
|
|
@ -250,7 +301,7 @@ describe('Channel Info RHS', () => {
|
|||
cy.get('#channel-info-btn').click();
|
||||
|
||||
// # Click on "Files"
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Files').should('be.visible').click();
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Files').scrollIntoView().should('be.visible').click();
|
||||
|
||||
// * Ensure we see the files RHS
|
||||
cy.uiGetRHS().findByText('No files yet').should('be.visible');
|
||||
|
|
@ -277,10 +328,10 @@ describe('Channel Info RHS', () => {
|
|||
cy.get('#channel-info-btn').click();
|
||||
|
||||
// # Click on "Pinned Messages"
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Pinned messages').should('be.visible').click();
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Pinned messages').scrollIntoView().should('be.visible').click();
|
||||
|
||||
// * Ensure we see the Pinned Post RHS
|
||||
cy.uiGetRHS().findByText('Hello channel info rhs spec').should('be.visible');
|
||||
cy.uiGetRHS().findByText('Hello channel info rhs spec').first().should('be.visible');
|
||||
|
||||
// # Click the Back Icon
|
||||
cy.uiGetRHS().get('[aria-label="Back Icon"]').click();
|
||||
|
|
@ -296,7 +347,7 @@ describe('Channel Info RHS', () => {
|
|||
cy.get('#channel-info-btn').click();
|
||||
|
||||
// # Click on "Members"
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Members').should('be.visible').click();
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Members').scrollIntoView().should('be.visible').click();
|
||||
|
||||
// * Ensure we see the members
|
||||
cy.uiGetRHS().contains('sysadmin').should('be.visible');
|
||||
|
|
@ -399,7 +450,7 @@ describe('Channel Info RHS', () => {
|
|||
cy.get('#channel-info-btn').click();
|
||||
|
||||
// # Click on "Notification Preferences"
|
||||
cy.uiGetRHS().findByText('Notification Preferences').should('be.visible').click();
|
||||
cy.uiGetRHS().findByTestId('channel_info_rhs-menu').findByText('Notification Preferences').scrollIntoView().should('be.visible').click();
|
||||
|
||||
// * Ensures the modal is there
|
||||
cy.get('.ChannelNotificationModal').should('be.visible');
|
||||
|
|
@ -489,6 +540,6 @@ describe('Channel Info RHS', () => {
|
|||
function ensureRHSIsOpenOnChannelInfo(testChannel) {
|
||||
cy.get('#rhsContainer').then((rhsContainer) => {
|
||||
cy.wrap(rhsContainer).findByText('Info').should('be.visible');
|
||||
cy.wrap(rhsContainer).findByText(testChannel.display_name).should('be.visible');
|
||||
cy.wrap(rhsContainer).find('.sidebar--right__title__subtitle').should('contain', testChannel.display_name);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ interface Props {
|
|||
gmUsers?: UserProfile[];
|
||||
canEditChannelProperties: boolean;
|
||||
actions: {
|
||||
editChannelName: () => void;
|
||||
editChannelPurpose: () => void;
|
||||
editChannelHeader: () => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import React from 'react';
|
|||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
|
|
@ -112,12 +112,15 @@ describe('channel_info_rhs/about_area_channel', () => {
|
|||
const defaultProps = {
|
||||
channel: {
|
||||
id: 'test-c-id',
|
||||
name: 'my-channel',
|
||||
header: 'my channel header',
|
||||
purpose: 'my channel purpose',
|
||||
display_name: 'My Channel',
|
||||
} as Channel,
|
||||
channelURL: 'https://my-url.mm',
|
||||
canEditChannelProperties: true,
|
||||
actions: {
|
||||
editChannelName: jest.fn(),
|
||||
editChannelPurpose: jest.fn(),
|
||||
editChannelHeader: jest.fn(),
|
||||
},
|
||||
|
|
@ -144,4 +147,25 @@ describe('channel_info_rhs/about_area_channel', () => {
|
|||
|
||||
expect(screen.getByText('my channel header')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should trigger editChannelName when clicking channel display name', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
actions: {
|
||||
...defaultProps.actions,
|
||||
editChannelName: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<AboutAreaChannel
|
||||
{...props}
|
||||
/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
const editButtons = screen.getAllByLabelText('Edit');
|
||||
fireEvent.click(editButtons[0]);
|
||||
expect(props.actions.editChannelName).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,17 +7,36 @@ import styled from 'styled-components';
|
|||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import CopyButton from 'components/copy_button';
|
||||
import Markdown from 'components/markdown';
|
||||
|
||||
import EditableArea from './components/editable_area';
|
||||
import LineLimiter from './components/linelimiter';
|
||||
|
||||
const ChannelId = styled.div`
|
||||
const ChannelName = styled.div`
|
||||
margin-bottom: 12px;
|
||||
font-size: 20px;
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
`;
|
||||
|
||||
const ChannelId = styled.div`
|
||||
padding: 4px 0;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.02em;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.post-code__clipboard {
|
||||
opacity: 0;
|
||||
}
|
||||
&:hover .post-code__clipboard {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChannelPurpose = styled.div`
|
||||
|
|
@ -29,23 +48,36 @@ const ChannelPurpose = styled.div`
|
|||
|
||||
const ChannelDescriptionHeading = styled.div`
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.24px;
|
||||
text-transform: uppercase;
|
||||
padding: 6px 0px;
|
||||
padding: 4px 0px;
|
||||
`;
|
||||
|
||||
const ChannelHeader = styled.div`
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
const SmallCopyButton = styled(CopyButton)`
|
||||
i {
|
||||
font-size: 14px;
|
||||
margin-left: 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.88);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
channel: Channel;
|
||||
canEditChannelProperties: boolean;
|
||||
actions: {
|
||||
editChannelName: () => void;
|
||||
editChannelPurpose: () => void;
|
||||
editChannelHeader: () => void;
|
||||
};
|
||||
|
|
@ -56,6 +88,16 @@ const AboutAreaChannel = ({channel, canEditChannelProperties, actions}: Props) =
|
|||
|
||||
return (
|
||||
<>
|
||||
<ChannelName>
|
||||
<EditableArea
|
||||
editable={canEditChannelProperties}
|
||||
content={<div>{channel.display_name}</div>}
|
||||
onEdit={actions.editChannelName}
|
||||
editTooltip={formatMessage({id: 'channel_info_rhs.about_area.edit_channel_name', defaultMessage: 'Rename channel'})}
|
||||
emptyLabel={formatMessage({id: 'channel_info_rhs.about_area.edit_channel_name', defaultMessage: 'Rename channel'})}
|
||||
/>
|
||||
</ChannelName>
|
||||
|
||||
{(channel.purpose || canEditChannelProperties) && (
|
||||
<ChannelPurpose>
|
||||
<ChannelDescriptionHeading>
|
||||
|
|
@ -74,6 +116,7 @@ const AboutAreaChannel = ({channel, canEditChannelProperties, actions}: Props) =
|
|||
</LineLimiter>
|
||||
)}
|
||||
onEdit={actions.editChannelPurpose}
|
||||
editTooltip={formatMessage({id: 'channel_info_rhs.about_area.edit_channel_purpose', defaultMessage: 'Edit channel purpose'})}
|
||||
emptyLabel={formatMessage({id: 'channel_info_rhs.about_area.add_channel_purpose', defaultMessage: 'Add a channel purpose'})}
|
||||
/>
|
||||
</ChannelPurpose>
|
||||
|
|
@ -97,14 +140,27 @@ const AboutAreaChannel = ({channel, canEditChannelProperties, actions}: Props) =
|
|||
)}
|
||||
editable={canEditChannelProperties}
|
||||
onEdit={actions.editChannelHeader}
|
||||
editTooltip={formatMessage({id: 'channel_info_rhs.about_area.edit_channel_header', defaultMessage: 'Edit channel header'})}
|
||||
emptyLabel={formatMessage({id: 'channel_info_rhs.about_area.add_channel_header', defaultMessage: 'Add a channel header'})}
|
||||
/>
|
||||
</ChannelHeader>
|
||||
)}
|
||||
|
||||
<ChannelId>
|
||||
{formatMessage({id: 'channel_info_rhs.about_area_id', defaultMessage: 'ID:'})} {channel.id}
|
||||
{formatMessage({id: 'channel_info_rhs.about_area_handle', defaultMessage: 'Channel handle:'})} {channel.name}
|
||||
<SmallCopyButton
|
||||
content={channel.name}
|
||||
isForText={true}
|
||||
/>
|
||||
</ChannelId>
|
||||
<ChannelId>
|
||||
{formatMessage({id: 'channel_info_rhs.about_area_id', defaultMessage: 'ID:'})} {channel.id}
|
||||
<SmallCopyButton
|
||||
content={channel.id}
|
||||
isForText={true}
|
||||
/>
|
||||
</ChannelId>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ const AboutAreaDM = ({channel, dmUser, actions}: Props) => {
|
|||
)}
|
||||
editable={true}
|
||||
onEdit={actions.editChannelHeader}
|
||||
editTooltip={formatMessage({id: 'channel_info_rhs.about_area.edit_channel_header', defaultMessage: 'Edit channel header'})}
|
||||
emptyLabel={formatMessage({id: 'channel_info_rhs.about_area.add_channel_header', defaultMessage: 'Add a channel header'})}
|
||||
/>
|
||||
</ChannelHeader>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ const AboutAreaGM = ({channel, gmUsers, actions}: Props) => {
|
|||
)}
|
||||
editable={true}
|
||||
onEdit={actions.editChannelHeader}
|
||||
editTooltip={formatMessage({id: 'channel_info_rhs.about_area.edit_channel_header', defaultMessage: 'Edit channel header'})}
|
||||
emptyLabel={formatMessage({id: 'channel_info_rhs.about_area.add_channel_header', defaultMessage: 'Add a channel header'})}
|
||||
/>
|
||||
</ChannelHeader>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {Team} from '@mattermost/types/teams';
|
|||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {act, renderWithContext} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import ChannelInfoRHS from './channel_info_rhs';
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ describe('channel_info_rhs', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
props = {...OriginalProps};
|
||||
mockAboutArea.mockClear();
|
||||
});
|
||||
|
||||
describe('about area', () => {
|
||||
|
|
@ -88,4 +90,27 @@ describe('channel_info_rhs', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('editChannelName opens Rename Channel modal', () => {
|
||||
props.currentTeam = {name: 'team-1'} as Team;
|
||||
renderWithContext(
|
||||
<ChannelInfoRHS
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Invoke the handler passed into the mocked AboutArea
|
||||
const lastArgs = mockAboutArea.mock.calls[mockAboutArea.mock.calls.length - 1][0];
|
||||
lastArgs.actions.editChannelName();
|
||||
|
||||
expect(props.actions.openModal).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modalId: ModalIdentifiers.RENAME_CHANNEL,
|
||||
dialogProps: expect.objectContaining({
|
||||
channel: props.channel,
|
||||
teamName: 'team-1',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,18 +2,24 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {Channel, ChannelStats} from '@mattermost/types/channels';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import ChannelInviteModal from 'components/channel_invite_modal';
|
||||
import ChannelNotificationsModal from 'components/channel_notifications_modal';
|
||||
import Scrollbars from 'components/common/scrollbars';
|
||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
|
||||
import MoreDirectChannels from 'components/more_direct_channels';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
import RenameChannelModal from 'components/rename_channel_modal';
|
||||
import UnarchiveChannelModal from 'components/unarchive_channel_modal';
|
||||
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
|
|
@ -25,12 +31,44 @@ import Header from './header';
|
|||
import Menu from './menu';
|
||||
import TopButtons from './top_buttons';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 88%;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.04);
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const ArchivedNoticeContainer = styled.div`
|
||||
margin: 24px 24px 0 24px;
|
||||
`;
|
||||
|
||||
const ArchivedNotice = styled.div`
|
||||
.sectionNoticeIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.sectionNoticeTitle {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.88);
|
||||
display: inline;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sectionNoticeTitle .sectionNoticeButton {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline;
|
||||
margin: 0 0 2px 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface DMUser {
|
||||
user: UserProfile;
|
||||
display_name: string;
|
||||
|
|
@ -129,12 +167,24 @@ const ChannelInfoRhs = ({
|
|||
dialogProps: {channel},
|
||||
});
|
||||
|
||||
const editChannelName = () => actions.openModal({
|
||||
modalId: ModalIdentifiers.RENAME_CHANNEL,
|
||||
dialogType: RenameChannelModal,
|
||||
dialogProps: {channel, teamName: currentTeam.name},
|
||||
});
|
||||
|
||||
const openNotificationSettings = () => actions.openModal({
|
||||
modalId: ModalIdentifiers.CHANNEL_NOTIFICATIONS,
|
||||
dialogType: ChannelNotificationsModal,
|
||||
dialogProps: {channel, currentUser, focusOriginElement: 'channelInfoRHSNotificationSettings'},
|
||||
});
|
||||
|
||||
const openUnarchiveChannel = () => actions.openModal({
|
||||
modalId: ModalIdentifiers.UNARCHIVE_CHANNEL,
|
||||
dialogType: UnarchiveChannelModal,
|
||||
dialogProps: {channel},
|
||||
});
|
||||
|
||||
const gmUsers = channelMembers.filter((user) => {
|
||||
return user.id !== currentUser.id;
|
||||
});
|
||||
|
|
@ -148,45 +198,83 @@ const ChannelInfoRhs = ({
|
|||
>
|
||||
<Header
|
||||
channel={channel}
|
||||
isArchived={isArchived}
|
||||
isMobile={isMobile}
|
||||
onClose={actions.closeRightHandSide}
|
||||
/>
|
||||
<Scrollbars
|
||||
color='--center-channel-color-rgb'
|
||||
>
|
||||
<TopButtons
|
||||
channelType={channel.type}
|
||||
channelURL={channelURL}
|
||||
isFavorite={isFavorite}
|
||||
isMuted={isMuted}
|
||||
isInvitingPeople={isInvitingPeople}
|
||||
canAddPeople={canManageMembers}
|
||||
actions={{toggleFavorite, toggleMute, addPeople}}
|
||||
/>
|
||||
<AboutArea
|
||||
channel={channel}
|
||||
dmUser={dmUser}
|
||||
gmUsers={gmUsers}
|
||||
canEditChannelProperties={canEditChannelProperties}
|
||||
actions={{
|
||||
editChannelHeader,
|
||||
editChannelPurpose,
|
||||
}}
|
||||
/>
|
||||
<Divider/>
|
||||
<Menu
|
||||
channel={channel}
|
||||
channelStats={channelStats}
|
||||
isArchived={isArchived}
|
||||
actions={{
|
||||
openNotificationSettings,
|
||||
showChannelFiles: actions.showChannelFiles,
|
||||
showPinnedPosts: actions.showPinnedPosts,
|
||||
showChannelMembers: actions.showChannelMembers,
|
||||
getChannelStats: actions.getChannelStats,
|
||||
}}
|
||||
/>
|
||||
<Container>
|
||||
{isArchived && (
|
||||
<ArchivedNoticeContainer className='sectionNoticeContainer warning'>
|
||||
<ArchivedNotice className='sectionNoticeContent'>
|
||||
<i className='icon icon-archive-outline sectionNoticeIcon'/>
|
||||
<div className='sectionNoticeBody'>
|
||||
<h4 className='sectionNoticeTitle'>
|
||||
<FormattedMessage
|
||||
id='channel_info_rhs.archived.title'
|
||||
defaultMessage='This channel is archived.'
|
||||
/>
|
||||
{channel.name !== Constants.DEFAULT_CHANNEL && (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='sectionNoticeButton btn btn-link'
|
||||
onClick={() => {
|
||||
openUnarchiveChannel();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_info_rhs.archived.unarchive'
|
||||
defaultMessage='Unarchive'
|
||||
/>
|
||||
</button>
|
||||
</ChannelPermissionGate>
|
||||
)}
|
||||
</h4>
|
||||
</div>
|
||||
</ArchivedNotice>
|
||||
</ArchivedNoticeContainer>
|
||||
)}
|
||||
<TopButtons
|
||||
channelType={channel.type}
|
||||
channelURL={channelURL}
|
||||
isFavorite={isFavorite}
|
||||
isMuted={isMuted}
|
||||
isInvitingPeople={isInvitingPeople}
|
||||
isArchived={isArchived}
|
||||
canAddPeople={!isArchived && canManageMembers}
|
||||
actions={{toggleFavorite, toggleMute, addPeople}}
|
||||
/>
|
||||
<AboutArea
|
||||
channel={channel}
|
||||
dmUser={dmUser}
|
||||
gmUsers={gmUsers}
|
||||
canEditChannelProperties={canEditChannelProperties}
|
||||
actions={{
|
||||
editChannelName,
|
||||
editChannelHeader,
|
||||
editChannelPurpose,
|
||||
}}
|
||||
/>
|
||||
<Divider/>
|
||||
<Menu
|
||||
channel={channel}
|
||||
channelStats={channelStats}
|
||||
isArchived={isArchived}
|
||||
actions={{
|
||||
openNotificationSettings,
|
||||
showChannelFiles: actions.showChannelFiles,
|
||||
showPinnedPosts: actions.showPinnedPosts,
|
||||
showChannelMembers: actions.showChannelMembers,
|
||||
getChannelStats: actions.getChannelStats,
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,13 +5,17 @@ import React from 'react';
|
|||
import {useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
const EditButton = styled.button`
|
||||
border: 0;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border-radius: 4px;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
background: none;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
|
|
@ -47,9 +51,10 @@ interface EditableAreaProps {
|
|||
emptyLabel: string;
|
||||
onEdit: () => void;
|
||||
className?: string;
|
||||
editTooltip?: string;
|
||||
}
|
||||
|
||||
const EditableAreaBase = ({editable, content, emptyLabel, onEdit, className}: EditableAreaProps) => {
|
||||
const EditableAreaBase = ({editable, content, emptyLabel, onEdit, className, editTooltip}: EditableAreaProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const allowEditArea = editable && content;
|
||||
|
|
@ -70,12 +75,14 @@ const EditableAreaBase = ({editable, content, emptyLabel, onEdit, className}: Ed
|
|||
</div>
|
||||
<div className='EditableArea__edit'>
|
||||
{allowEditArea ? (
|
||||
<EditButton
|
||||
onClick={onEdit}
|
||||
aria-label={formatMessage({id: 'channel_info_rhs.edit_link', defaultMessage: 'Edit'})}
|
||||
>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</EditButton>
|
||||
<WithTooltip title={editTooltip || formatMessage({id: 'channel_info_rhs.edit_link', defaultMessage: 'Edit'})}>
|
||||
<EditButton
|
||||
onClick={onEdit}
|
||||
aria-label={formatMessage({id: 'channel_info_rhs.edit_link', defaultMessage: 'Edit'})}
|
||||
>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</EditButton>
|
||||
</WithTooltip>
|
||||
) : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -90,14 +97,13 @@ const EditableArea = styled(EditableAreaBase)`
|
|||
margin-bottom:0;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
&>.EditableArea__edit {
|
||||
visibility: visible;
|
||||
}
|
||||
&:hover > .EditableArea__edit,
|
||||
&:focus-within > .EditableArea__edit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&>.EditableArea__edit {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
width: 24px;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -10,16 +10,16 @@ import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
|
|||
import Header from './header';
|
||||
|
||||
describe('channel_info_rhs/header', () => {
|
||||
test('should the current channel name', () => {
|
||||
test('renders the header title', () => {
|
||||
renderWithContext(
|
||||
<Header
|
||||
channel={{display_name: 'my channel title'} as Channel}
|
||||
isMobile={false}
|
||||
isArchived={false}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Info')).toBeInTheDocument();
|
||||
expect(screen.getByText('my channel title')).toBeInTheDocument();
|
||||
});
|
||||
test('should call onClose when clicking on the close icon', async () => {
|
||||
|
|
@ -29,7 +29,6 @@ describe('channel_info_rhs/header', () => {
|
|||
<Header
|
||||
channel={{display_name: 'my channel title'} as Channel}
|
||||
isMobile={false}
|
||||
isArchived={false}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -45,7 +44,6 @@ describe('channel_info_rhs/header', () => {
|
|||
<Header
|
||||
channel={{display_name: 'my channel title'} as Channel}
|
||||
isMobile={true}
|
||||
isArchived={false}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -54,28 +52,4 @@ describe('channel_info_rhs/header', () => {
|
|||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
test('should have archived icon when channel is archived', () => {
|
||||
const {container} = renderWithContext(
|
||||
<Header
|
||||
channel={{display_name: 'my channel title'} as Channel}
|
||||
isMobile={false}
|
||||
isArchived={true}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('i.icon-archive-outline')).toBeInTheDocument();
|
||||
});
|
||||
test('should not have archived icon when channel is archived', () => {
|
||||
const {container} = renderWithContext(
|
||||
<Header
|
||||
channel={{display_name: 'my channel title'} as Channel}
|
||||
isMobile={false}
|
||||
isArchived={false}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('i.icon-archive-outline')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,20 +11,15 @@ import WithTooltip from 'components/with_tooltip';
|
|||
|
||||
interface Props {
|
||||
channel: Channel;
|
||||
isArchived: boolean;
|
||||
isMobile: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const Icon = styled.i`
|
||||
font-size:12px;
|
||||
`;
|
||||
|
||||
const HeaderTitle = styled.span`
|
||||
line-height: 2.4rem;
|
||||
`;
|
||||
|
||||
const Header = ({channel, isArchived, isMobile, onClose}: Props) => {
|
||||
const Header = ({channel, isMobile, onClose}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
|
|
@ -50,12 +45,10 @@ const Header = ({channel, isArchived, isMobile, onClose}: Props) => {
|
|||
defaultMessage='Info'
|
||||
/>
|
||||
</HeaderTitle>
|
||||
|
||||
{channel.display_name &&
|
||||
<span
|
||||
className='style--none sidebar--right__title__subtitle'
|
||||
>
|
||||
{isArchived && (<Icon className='icon icon-archive-outline'/>)}
|
||||
{channel.display_name}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,30 @@ import React from 'react';
|
|||
|
||||
import type {Channel, ChannelStats} from '@mattermost/types/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {canAccessChannelSettings} from 'selectors/views/channel_settings';
|
||||
|
||||
import {
|
||||
act,
|
||||
renderWithContext,
|
||||
screen,
|
||||
userEvent,
|
||||
fireEvent,
|
||||
} from 'tests/react_testing_utils';
|
||||
import Constants from 'utils/constants';
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
jest.mock('selectors/views/channel_settings', () => ({
|
||||
canAccessChannelSettings: jest.fn(),
|
||||
}));
|
||||
jest.mock('actions/views/modals', () => ({
|
||||
openModal: jest.fn(() => ({type: 'OPEN_MODAL'})),
|
||||
}));
|
||||
|
||||
import Menu from './menu';
|
||||
|
||||
const mockedCanAccessChannelSettings = canAccessChannelSettings as unknown as jest.Mock;
|
||||
const mockedOpenModal = openModal as unknown as jest.Mock;
|
||||
|
||||
describe('channel_info_rhs/menu', () => {
|
||||
const defaultProps = {
|
||||
channel: {type: Constants.OPEN_CHANNEL} as Channel,
|
||||
|
|
@ -30,6 +44,8 @@ describe('channel_info_rhs/menu', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockedOpenModal.mockClear();
|
||||
mockedCanAccessChannelSettings.mockReset();
|
||||
defaultProps.actions = {
|
||||
openNotificationSettings: jest.fn(),
|
||||
showChannelFiles: jest.fn(),
|
||||
|
|
@ -182,4 +198,90 @@ describe('channel_info_rhs/menu', () => {
|
|||
const membersItem = screen.queryByText('Members');
|
||||
expect(membersItem).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should display Channel Settings and open modal on click (non-DM/GM, not archived, permitted)', async () => {
|
||||
mockedCanAccessChannelSettings.mockReturnValue(true);
|
||||
const props = {...defaultProps};
|
||||
|
||||
renderWithContext(
|
||||
<Menu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
props.actions.getChannelStats();
|
||||
});
|
||||
|
||||
const settingsItem = screen.getByText('Channel Settings');
|
||||
expect(settingsItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(settingsItem);
|
||||
expect(mockedOpenModal).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modalId: ModalIdentifiers.CHANNEL_SETTINGS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should NOT display Channel Settings in DM', async () => {
|
||||
mockedCanAccessChannelSettings.mockReturnValue(true);
|
||||
const props = {
|
||||
...defaultProps,
|
||||
channel: {type: Constants.DM_CHANNEL} as Channel,
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<Menu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
await act(async () => props.actions.getChannelStats());
|
||||
expect(screen.queryByText('Channel Settings')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT display Channel Settings in GM', async () => {
|
||||
mockedCanAccessChannelSettings.mockReturnValue(true);
|
||||
const props = {
|
||||
...defaultProps,
|
||||
channel: {type: Constants.GM_CHANNEL} as Channel,
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<Menu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
await act(async () => props.actions.getChannelStats());
|
||||
expect(screen.queryByText('Channel Settings')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT display Channel Settings when archived', async () => {
|
||||
mockedCanAccessChannelSettings.mockReturnValue(true);
|
||||
const props = {
|
||||
...defaultProps,
|
||||
isArchived: true,
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<Menu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
await act(async () => props.actions.getChannelStats());
|
||||
expect(screen.queryByText('Channel Settings')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT display Channel Settings without permission', async () => {
|
||||
mockedCanAccessChannelSettings.mockReturnValue(false);
|
||||
const props = {...defaultProps};
|
||||
|
||||
renderWithContext(
|
||||
<Menu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
await act(async () => props.actions.getChannelStats());
|
||||
expect(screen.queryByText('Channel Settings')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,13 +3,20 @@
|
|||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {Channel, ChannelStats} from '@mattermost/types/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {canAccessChannelSettings} from 'selectors/views/channel_settings';
|
||||
|
||||
import ChannelSettingsModal from 'components/channel_settings_modal/channel_settings_modal';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
const MenuContainer = styled.nav`
|
||||
display: flex;
|
||||
|
|
@ -116,6 +123,7 @@ interface MenuProps {
|
|||
|
||||
export default function Menu(props: MenuProps) {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
channel,
|
||||
channelStats,
|
||||
|
|
@ -128,7 +136,9 @@ export default function Menu(props: MenuProps) {
|
|||
|
||||
const showNotificationPreferences = channel.type !== Constants.DM_CHANNEL && !isArchived;
|
||||
const showMembers = channel.type !== Constants.DM_CHANNEL;
|
||||
const showChannelSettings = channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived;
|
||||
const fileCount = channelStats?.files_count >= 0 ? channelStats?.files_count : 0;
|
||||
const canAccessSettings = useSelector((state: GlobalState) => canAccessChannelSettings(state, channel.id));
|
||||
|
||||
useEffect(() => {
|
||||
actions.getChannelStats(channel.id, true).then(() => {
|
||||
|
|
@ -139,6 +149,20 @@ export default function Menu(props: MenuProps) {
|
|||
};
|
||||
}, [channel.id]);
|
||||
|
||||
const openChannelSettings = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.CHANNEL_SETTINGS,
|
||||
dialogType: ChannelSettingsModal,
|
||||
dialogProps: {
|
||||
channelId: channel.id,
|
||||
focusOriginElement: 'channelInfoRHSChannelSettings',
|
||||
isOpen: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuContainer
|
||||
className={className}
|
||||
|
|
@ -148,6 +172,17 @@ export default function Menu(props: MenuProps) {
|
|||
defaultMessage: 'Channel Info Actions',
|
||||
})}
|
||||
>
|
||||
{showChannelSettings && canAccessSettings && (
|
||||
<MenuItem
|
||||
id='channelInfoRHSChannelSettings'
|
||||
icon={<i className='icon icon-cog-outline'/>}
|
||||
text={formatMessage({
|
||||
id: 'channel_header.channel_settings',
|
||||
defaultMessage: 'Channel Settings',
|
||||
})}
|
||||
onClick={openChannelSettings}
|
||||
/>
|
||||
)}
|
||||
{showNotificationPreferences && (
|
||||
<MenuItem
|
||||
id='channelInfoRHSNotificationSettings'
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export interface Props {
|
|||
isFavorite: boolean;
|
||||
isMuted: boolean;
|
||||
isInvitingPeople: boolean;
|
||||
isArchived?: boolean;
|
||||
|
||||
canAddPeople: boolean;
|
||||
|
||||
|
|
@ -99,6 +100,7 @@ export default function TopButtons({
|
|||
isFavorite,
|
||||
isMuted,
|
||||
isInvitingPeople,
|
||||
isArchived = false,
|
||||
canAddPeople: propsCanAddPeople,
|
||||
actions,
|
||||
}: Props) {
|
||||
|
|
@ -109,7 +111,7 @@ export default function TopButtons({
|
|||
successCopyTimeout: 1000,
|
||||
});
|
||||
|
||||
const canAddPeople = ([Constants.OPEN_CHANNEL, Constants.PRIVATE_CHANNEL].includes(channelType) && propsCanAddPeople) || channelType === Constants.GM_CHANNEL;
|
||||
const canAddPeople = !isArchived && (([Constants.OPEN_CHANNEL, Constants.PRIVATE_CHANNEL].includes(channelType) && propsCanAddPeople) || channelType === Constants.GM_CHANNEL);
|
||||
|
||||
const canCopyLink = [Constants.OPEN_CHANNEL, Constants.PRIVATE_CHANNEL].includes(channelType);
|
||||
|
||||
|
|
|
|||
25
webapp/channels/src/components/rename_channel_modal/index.ts
Normal file
25
webapp/channels/src/components/rename_channel_modal/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// 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 type {Dispatch} from 'redux';
|
||||
|
||||
import {patchChannel} from 'mattermost-redux/actions/channels';
|
||||
|
||||
import RenameChannelModal from './rename_channel_modal';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
patchChannel,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RenameChannelModal);
|
||||
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import ChannelNameFormField from 'components/channel_name_form_field/channel_name_form_field';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
type Actions = {
|
||||
patchChannel: (channelId: string, patch: Partial<Channel>) => Promise<ActionResult>;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
teamName: string;
|
||||
onExited: () => void;
|
||||
actions: Actions;
|
||||
intl: IntlShape;
|
||||
}
|
||||
|
||||
type State = {
|
||||
show: boolean;
|
||||
displayName: string;
|
||||
channelUrl: string;
|
||||
isSaving: boolean;
|
||||
urlError: string;
|
||||
}
|
||||
|
||||
export class RenameChannelModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
show: true,
|
||||
displayName: props.channel.display_name,
|
||||
channelUrl: props.channel.name,
|
||||
isSaving: false,
|
||||
urlError: '',
|
||||
};
|
||||
}
|
||||
|
||||
onHide = () => {
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
handleSave = async () => {
|
||||
const {channel, actions: {patchChannel}} = this.props;
|
||||
const {displayName, channelUrl} = this.state;
|
||||
|
||||
if (!channel || !displayName?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate min/max on display name
|
||||
const trimmedDisplayName = displayName.trim();
|
||||
if (trimmedDisplayName.length < Constants.MIN_CHANNELNAME_LENGTH ||
|
||||
trimmedDisplayName.length > Constants.MAX_CHANNELNAME_LENGTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({isSaving: true});
|
||||
const {data, error} = await patchChannel(channel.id, {
|
||||
display_name: trimmedDisplayName,
|
||||
name: channelUrl.trim(),
|
||||
});
|
||||
this.setState({isSaving: false});
|
||||
|
||||
if (data && !error) {
|
||||
this.onHide();
|
||||
|
||||
// Use the actual channel name from the response, as the server may have sanitized it
|
||||
const updatedChannelName = data.name || channelUrl.trim();
|
||||
const path = `/${this.props.teamName}/channels/${updatedChannelName}`;
|
||||
getHistory().push(path);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal'
|
||||
show={this.state.show}
|
||||
onHide={this.onHide}
|
||||
onExited={this.props.onExited}
|
||||
role='dialog'
|
||||
aria-labelledby='renameChannelModalLabel'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title
|
||||
componentClass='h1'
|
||||
id='renameChannelModalLabel'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='rename_channel.title'
|
||||
defaultMessage='Rename Channel'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<ChannelNameFormField
|
||||
value={this.state.displayName}
|
||||
name='rename-channel'
|
||||
placeholder={formatMessage({
|
||||
id: 'rename_channel.displayNameHolder',
|
||||
defaultMessage: 'Enter display name',
|
||||
})}
|
||||
onDisplayNameChange={(name) => this.setState({displayName: name})}
|
||||
onURLChange={(url) => this.setState({channelUrl: url})}
|
||||
currentUrl={this.state.channelUrl}
|
||||
readOnly={false}
|
||||
isEditingExistingChannel={true}
|
||||
onErrorStateChange={(isError, errorMsg) => this.setState({urlError: isError ? (errorMsg || '') : ''})}
|
||||
urlError={this.state.urlError}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-tertiary cancel-button'
|
||||
onClick={this.onHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='generic_btn.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
disabled={this.state.isSaving}
|
||||
onClick={this.handleSave}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='generic_btn.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(RenameChannelModal);
|
||||
|
||||
|
|
@ -3709,6 +3709,7 @@
|
|||
"channel_header.unmute": "Unmute Channel",
|
||||
"channel_header.unmuteConversation": "Unmute",
|
||||
"channel_header.userHelpGuide": "Help",
|
||||
"channel_info_rhs.about_area_handle": "Channel handle:",
|
||||
"channel_info_rhs.about_area_id": "ID:",
|
||||
"channel_info_rhs.about_area.add_channel_header": "Add a channel header",
|
||||
"channel_info_rhs.about_area.add_channel_purpose": "Add a channel purpose",
|
||||
|
|
@ -3718,6 +3719,11 @@
|
|||
"channel_info_rhs.about_area.channel_purpose.heading": "Channel Purpose",
|
||||
"channel_info_rhs.about_area.channel_purpose.line_limiter.less": "less",
|
||||
"channel_info_rhs.about_area.channel_purpose.line_limiter.more": "more",
|
||||
"channel_info_rhs.about_area.edit_channel_header": "Edit channel header",
|
||||
"channel_info_rhs.about_area.edit_channel_name": "Rename channel",
|
||||
"channel_info_rhs.about_area.edit_channel_purpose": "Edit channel purpose",
|
||||
"channel_info_rhs.archived.title": "This channel is archived.",
|
||||
"channel_info_rhs.archived.unarchive": "Unarchive",
|
||||
"channel_info_rhs.edit_link": "Edit",
|
||||
"channel_info_rhs.header.title": "Info",
|
||||
"channel_info_rhs.menu.files": "Files",
|
||||
|
|
@ -5698,6 +5704,8 @@
|
|||
"removed_channel.someone": "Someone",
|
||||
"rename_category_modal.rename": "Rename",
|
||||
"rename_category_modal.renameCategory": "Rename Category",
|
||||
"rename_channel.displayNameHolder": "Enter display name",
|
||||
"rename_channel.title": "Rename Channel",
|
||||
"restricted_indicator.tooltip.mesage": "During your trial you are able to use this feature.",
|
||||
"restricted_indicator.tooltip.message.blocked": "This is a paid feature, available with a free {trialLength}-day trial",
|
||||
"restricted_indicator.tooltip.title": "{minimumPlanRequiredForFeature} feature",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
.modal-body {
|
||||
overflow: auto;
|
||||
max-height: calc(90vh - 80px);
|
||||
padding: 2px 32px;
|
||||
padding: 4px 32px;
|
||||
|
||||
&.overflow--visible {
|
||||
overflow: visible;
|
||||
|
|
|
|||
|
|
@ -943,6 +943,7 @@
|
|||
|
||||
.channel-header-archived-icon {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 5px;
|
||||
fill: var(--center-channel-color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@
|
|||
font-family: Metropolis, sans-serif;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
|
||||
@include mixins.clearfix;
|
||||
h2 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue