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
Этот коммит содержится в:
Pablo Vélez
2024-11-21 09:50:10 +01:00
коммит произвёл GitHub
родитель 746eb70e01
Коммит 6656a54484
6 изменённых файлов: 270 добавлений и 11 удалений

Просмотреть файл

@@ -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 doesnt 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 doesnt exist or you do not have access to it.",
"search_bar.channels": "Channels",
"search_bar.clear": "Clear",
"search_bar.file_types": "File types",