MM-61830 - hide actions to no permissions channels (#29319)
* MM-61830 - hide actions to no permissions channels * add translations * add unit tests * expand functionality to archived channels, update tests * simplify logic to verify membership
Этот коммит содержится в:
@@ -0,0 +1,235 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel, ChannelType} from '@mattermost/types/channels';
|
||||
import type {ScheduledPost} from '@mattermost/types/schedule_post';
|
||||
|
||||
import * as commonSelectors from 'mattermost-redux/selectors/entities/common';
|
||||
import * as usersSelectors from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
import ScheduledPostActions from './scheduled_post_actions';
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'user_id',
|
||||
profiles: {
|
||||
user_id: {
|
||||
roles: 'custom_role',
|
||||
timezone: {
|
||||
useAutomaticTimezone: true,
|
||||
automaticTimezone: '',
|
||||
manualTimezone: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
license: {},
|
||||
},
|
||||
channels: {
|
||||
currentChannelId: 'channel_id',
|
||||
channels: {
|
||||
channel_id: {
|
||||
id: 'channel_id',
|
||||
type: 'O' as ChannelType,
|
||||
display_name: 'Test Channel',
|
||||
delete_at: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
scheduledPost: {
|
||||
id: 'scheduled_post_id',
|
||||
channel_id: 'channel_id',
|
||||
scheduled_at: Date.now(),
|
||||
error_code: null,
|
||||
create_at: Date.now(),
|
||||
update_at: Date.now(),
|
||||
user_id: 'user_id',
|
||||
root_id: '',
|
||||
message: 'Test message',
|
||||
props: {},
|
||||
metadata: {},
|
||||
} as unknown as ScheduledPost,
|
||||
channel: {
|
||||
id: 'channel_id',
|
||||
type: 'O' as ChannelType,
|
||||
display_name: 'Test Channel',
|
||||
delete_at: 0,
|
||||
} as Channel,
|
||||
onReschedule: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
onSend: jest.fn(),
|
||||
onEdit: jest.fn(),
|
||||
onCopyText: jest.fn(),
|
||||
};
|
||||
|
||||
describe('ScheduledPostActions Component', () => {
|
||||
let isCurrentUserSystemAdminMock: jest.SpyInstance;
|
||||
let getMyChannelMembershipsnMock: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
isCurrentUserSystemAdminMock = jest.spyOn(usersSelectors, 'isCurrentUserSystemAdmin');
|
||||
getMyChannelMembershipsnMock = jest.spyOn(commonSelectors, 'getMyChannelMemberships');
|
||||
|
||||
// Set default return values
|
||||
isCurrentUserSystemAdminMock.mockReturnValue(false);
|
||||
getMyChannelMembershipsnMock.mockReturnValue({
|
||||
channel_id: {
|
||||
channel_id: 'channel_id',
|
||||
user_id: 'user_id',
|
||||
roles: 'channel_user',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function renderComponent(props = defaultProps, state = initialState) {
|
||||
return renderWithContext(
|
||||
<ScheduledPostActions
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
/>,
|
||||
state,
|
||||
);
|
||||
}
|
||||
it('should render all action buttons when user is an ADMIN', () => {
|
||||
isCurrentUserSystemAdminMock.mockReturnValue(true);
|
||||
|
||||
renderComponent();
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(5);
|
||||
|
||||
const buttonIds = buttons.map((button) => button.id);
|
||||
expect(buttonIds).toContain('draft_icon-trash-can-outline_delete');
|
||||
expect(buttonIds).toContain('draft_icon-pencil-outline_edit');
|
||||
expect(buttonIds).toContain('draft_icon-content-copy_copy_text');
|
||||
expect(buttonIds).toContain('draft_icon-clock-send-outline_reschedule');
|
||||
expect(buttonIds).toContain('draft_icon-send-outline_sendNow');
|
||||
});
|
||||
|
||||
it('should render appropriate action buttons when user is NOT an admin but IS member of the channel', () => {
|
||||
isCurrentUserSystemAdminMock.mockReturnValue(false);
|
||||
|
||||
renderComponent();
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(5);
|
||||
|
||||
const buttonIds = buttons.map((button) => button.id);
|
||||
expect(buttonIds).toContain('draft_icon-trash-can-outline_delete');
|
||||
expect(buttonIds).toContain('draft_icon-pencil-outline_edit');
|
||||
expect(buttonIds).toContain('draft_icon-content-copy_copy_text');
|
||||
expect(buttonIds).toContain('draft_icon-clock-send-outline_reschedule');
|
||||
expect(buttonIds).toContain('draft_icon-send-outline_sendNow');
|
||||
});
|
||||
|
||||
it('should only render delete and copy text button when regular user is NOT member of the channel', () => {
|
||||
isCurrentUserSystemAdminMock.mockReturnValue(false);
|
||||
|
||||
// Regular User is not a member of the channel
|
||||
getMyChannelMembershipsnMock.mockReturnValue({});
|
||||
|
||||
renderComponent();
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(2);
|
||||
|
||||
const buttonIds = buttons.map((button) => button.id);
|
||||
expect(buttonIds).toContain('draft_icon-trash-can-outline_delete');
|
||||
expect(buttonIds).toContain('draft_icon-content-copy_copy_text');
|
||||
|
||||
// validate action buttons are not present
|
||||
expect(buttonIds).not.toContain('draft_icon-send-outline_sendNow');
|
||||
expect(buttonIds).not.toContain('draft_icon-pencil-outline_edit');
|
||||
expect(buttonIds).not.toContain('draft_icon-clock-send-outline_reschedule');
|
||||
});
|
||||
|
||||
it('should render all action buttons when user is not member of the channel but is an admin', () => {
|
||||
isCurrentUserSystemAdminMock.mockReturnValue(true);
|
||||
getMyChannelMembershipsnMock.mockReturnValue({});
|
||||
|
||||
renderComponent();
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(5);
|
||||
|
||||
const buttonIds = buttons.map((button) => button.id);
|
||||
expect(buttonIds).toContain('draft_icon-trash-can-outline_delete');
|
||||
expect(buttonIds).toContain('draft_icon-pencil-outline_edit');
|
||||
expect(buttonIds).toContain('draft_icon-content-copy_copy_text');
|
||||
expect(buttonIds).toContain('draft_icon-clock-send-outline_reschedule');
|
||||
expect(buttonIds).toContain('draft_icon-send-outline_sendNow');
|
||||
});
|
||||
|
||||
it('should only render delete and copy text buttons when the channel is archived and is regular user', () => {
|
||||
const archivedChannelProps = {
|
||||
...defaultProps,
|
||||
channel: {
|
||||
...defaultProps.channel,
|
||||
delete_at: 1,
|
||||
} as Channel,
|
||||
};
|
||||
|
||||
renderComponent(archivedChannelProps);
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(2);
|
||||
|
||||
const buttonIds = buttons.map((button) => button.id);
|
||||
expect(buttonIds).toContain('draft_icon-trash-can-outline_delete');
|
||||
expect(buttonIds).toContain('draft_icon-content-copy_copy_text');
|
||||
|
||||
// Validate that other action buttons are not present
|
||||
expect(buttonIds).not.toContain('draft_icon-send-outline_sendNow');
|
||||
expect(buttonIds).not.toContain('draft_icon-pencil-outline_edit');
|
||||
expect(buttonIds).not.toContain('draft_icon-clock-send-outline_reschedule');
|
||||
});
|
||||
|
||||
it('should render all action buttons when the channel is archived and the user is admin', () => {
|
||||
const archivedChannelProps = {
|
||||
...defaultProps,
|
||||
channel: {
|
||||
...defaultProps.channel,
|
||||
delete_at: 1,
|
||||
} as Channel,
|
||||
};
|
||||
|
||||
isCurrentUserSystemAdminMock.mockReturnValue(true);
|
||||
|
||||
renderComponent(archivedChannelProps);
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(5);
|
||||
|
||||
const buttonIds = buttons.map((button) => button.id);
|
||||
expect(buttonIds).toContain('draft_icon-trash-can-outline_delete');
|
||||
expect(buttonIds).toContain('draft_icon-content-copy_copy_text');
|
||||
expect(buttonIds).toContain('draft_icon-send-outline_sendNow');
|
||||
expect(buttonIds).toContain('draft_icon-pencil-outline_edit');
|
||||
expect(buttonIds).toContain('draft_icon-clock-send-outline_reschedule');
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,9 @@ import type {ScheduledPost} from '@mattermost/types/schedule_post';
|
||||
|
||||
import {fetchMissingChannels} from 'mattermost-redux/actions/channels';
|
||||
import {isDeactivatedDirectChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
@@ -75,6 +77,8 @@ type Props = {
|
||||
function ScheduledPostActions({scheduledPost, channel, onReschedule, onDelete, onSend, onEdit, onCopyText}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const userTimezone = useSelector(getCurrentTimezone);
|
||||
const myChannelsMemberships = useSelector((state: GlobalState) => getMyChannelMemberships(state));
|
||||
const isAdmin = useSelector((state: GlobalState) => isCurrentUserSystemAdmin(state));
|
||||
|
||||
useEffect(() => {
|
||||
// this ensures the DM is loaded in redux store and is available
|
||||
@@ -127,13 +131,13 @@ function ScheduledPostActions({scheduledPost, channel, onReschedule, onDelete, o
|
||||
}));
|
||||
}, [channel, dispatch, onSend, scheduledPost.id]);
|
||||
|
||||
const showEditOption = !scheduledPost.error_code;
|
||||
|
||||
const userChannelMember = Boolean(channel && myChannelsMemberships[channel.id]);
|
||||
const isChannelArchived = Boolean(channel?.delete_at);
|
||||
const isDeactivatedDM = useSelector((state: GlobalState) => isDeactivatedDirectChannel(state, scheduledPost.channel_id));
|
||||
const showSendNowOption = (!scheduledPost.error_code || scheduledPost.error_code === 'unknown' || scheduledPost.error_code === 'unable_to_send') && channel && !isChannelArchived && !isDeactivatedDM;
|
||||
|
||||
const showRescheduleOption = !scheduledPost.error_code || scheduledPost.error_code === 'unknown' || scheduledPost.error_code === 'unable_to_send';
|
||||
const showEditOption = !scheduledPost.error_code && userChannelMember && !isChannelArchived;
|
||||
const isDeactivatedDM = useSelector((state: GlobalState) => isDeactivatedDirectChannel(state, scheduledPost.channel_id));
|
||||
const showSendNowOption = (!scheduledPost.error_code || scheduledPost.error_code === 'unknown' || scheduledPost.error_code === 'unable_to_send') && channel && !isChannelArchived && !isDeactivatedDM && userChannelMember;
|
||||
const showRescheduleOption = (!scheduledPost.error_code || scheduledPost.error_code === 'unknown' || scheduledPost.error_code === 'unable_to_send') && userChannelMember && !isChannelArchived;
|
||||
|
||||
return (
|
||||
<div className='ScheduledPostActions'>
|
||||
@@ -146,7 +150,7 @@ function ScheduledPostActions({scheduledPost, channel, onReschedule, onDelete, o
|
||||
/>
|
||||
|
||||
{
|
||||
showEditOption &&
|
||||
(isAdmin || showEditOption) &&
|
||||
<Action
|
||||
icon='icon-pencil-outline'
|
||||
id='edit'
|
||||
@@ -166,7 +170,7 @@ function ScheduledPostActions({scheduledPost, channel, onReschedule, onDelete, o
|
||||
/>
|
||||
|
||||
{
|
||||
showRescheduleOption &&
|
||||
(isAdmin || showRescheduleOption) &&
|
||||
<Action
|
||||
icon='icon-clock-send-outline'
|
||||
id='reschedule'
|
||||
@@ -177,7 +181,7 @@ function ScheduledPostActions({scheduledPost, channel, onReschedule, onDelete, o
|
||||
}
|
||||
|
||||
{
|
||||
showSendNowOption &&
|
||||
(isAdmin || showSendNowOption) &&
|
||||
<Action
|
||||
icon='icon-send-outline'
|
||||
id='sendNow'
|
||||
|
||||
@@ -59,7 +59,7 @@ function Drafts({
|
||||
const isScheduledPostsTab = useRouteMatch('/:team/' + SCHEDULED_POST_URL_SUFFIX);
|
||||
|
||||
const currentTeamId = useSelector(getCurrentTeamId);
|
||||
const getScheduledPostsByTeam = makeGetScheduledPostsByTeam();
|
||||
const getScheduledPostsByTeam = useMemo(() => makeGetScheduledPostsByTeam(), []);
|
||||
const scheduledPosts = useSelector((state: GlobalState) => getScheduledPostsByTeam(state, currentTeamId, true));
|
||||
const isScheduledPostEnabled = useSelector(isScheduledPostsEnabled);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
export default (
|
||||
<svg
|
||||
width='100%'
|
||||
height='auto'
|
||||
height='100%'
|
||||
viewBox='0 224 724 290'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
type Props = {
|
||||
type: 'channel' | 'thread';
|
||||
}
|
||||
@@ -17,6 +19,13 @@ export default function PlaceholderScheduledPostsTitle({type}: Props) {
|
||||
/>
|
||||
);
|
||||
|
||||
const tooltipText = (
|
||||
<FormattedMessage
|
||||
id='scheduled_posts.row_title_thread.placeholder_tooltip'
|
||||
defaultMessage={'The channel either doesn’t exist or you do not have access to it.'}
|
||||
/>
|
||||
);
|
||||
|
||||
if (type === 'thread') {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
@@ -39,5 +48,15 @@ export default function PlaceholderScheduledPostsTitle({type}: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
return title;
|
||||
return (
|
||||
<WithTooltip
|
||||
id='scheduled_posts__placeholder'
|
||||
placement={'top'}
|
||||
title={tooltipText}
|
||||
>
|
||||
<div>
|
||||
{title}
|
||||
</div>
|
||||
</WithTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4907,6 +4907,7 @@
|
||||
"scheduled_post.panel.header.time": "Send {isTodayOrTomorrow, select, true {} other {on}} {scheduledDateTime}",
|
||||
"scheduled_posts.row_title_channel.placeholder": "In: {icon} No Destination",
|
||||
"scheduled_posts.row_title_thread.placeholder": "Thread to: {icon} No Destination",
|
||||
"scheduled_posts.row_title_thread.placeholder_tooltip": "The channel either doesn’t exist or you do not have access to it.",
|
||||
"search_bar.channels": "Channels",
|
||||
"search_bar.clear": "Clear",
|
||||
"search_bar.file_types": "File types",
|
||||
|
||||
Ссылка в новой задаче
Block a user