diff --git a/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.test.tsx b/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.test.tsx
new file mode 100644
index 0000000000..021c9dee81
--- /dev/null
+++ b/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.test.tsx
@@ -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(
+ ,
+ 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');
+ });
+});
diff --git a/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.tsx b/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.tsx
index 5cdf20bc3c..2ec0e48c43 100644
--- a/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.tsx
+++ b/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.tsx
@@ -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 (
@@ -146,7 +150,7 @@ function ScheduledPostActions({scheduledPost, channel, onReschedule, onDelete, o
/>
{
- showEditOption &&
+ (isAdmin || showEditOption) &&
{
- showRescheduleOption &&
+ (isAdmin || showRescheduleOption) &&
makeGetScheduledPostsByTeam(), []);
const scheduledPosts = useSelector((state: GlobalState) => getScheduledPostsByTeam(state, currentTeamId, true));
const isScheduledPostEnabled = useSelector(isScheduledPostsEnabled);
diff --git a/webapp/channels/src/components/drafts/drafts_illustration.tsx b/webapp/channels/src/components/drafts/drafts_illustration.tsx
index 7b97514c5e..b48f249bb8 100644
--- a/webapp/channels/src/components/drafts/drafts_illustration.tsx
+++ b/webapp/channels/src/components/drafts/drafts_illustration.tsx
@@ -6,7 +6,7 @@ import React from 'react';
export default (
);
+ const tooltipText = (
+
+ );
+
if (type === 'thread') {
title = (
+
+ {title}
+
+
+ );
}
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 8f222070ec..4595e7a5b6 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -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",