MM-61266 | MM-60948 - verified available actions on scheduled posts (#29106)

* verified available actions on scheduled posts

* Review fixes

* Added check for archived channel and deactivated DM
Этот коммит содержится в:
Harshil Sharma
2024-11-08 10:34:38 +05:30
коммит произвёл GitHub
родитель bd8774bdce
Коммит faa6853a28
9 изменённых файлов: 109 добавлений и 63 удалений

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

@@ -58,6 +58,7 @@ import type {OnSubmitOptions, SubmitPostReturnType} from './views/create_comment
export type CreatePostOptions = { export type CreatePostOptions = {
keepDraft?: boolean; keepDraft?: boolean;
ignorePostError?: boolean;
} }
export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & GroupChannel}): ActionFuncAsync<boolean, GlobalState> { export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & GroupChannel}): ActionFuncAsync<boolean, GlobalState> {

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

@@ -126,7 +126,7 @@ const useSubmit = (
return; return;
} }
if (postError) { if (postError && !createPostOptions?.ignorePostError) {
setErrorClass('animation--highlight'); setErrorClass('animation--highlight');
setTimeout(() => { setTimeout(() => {
setErrorClass(null); setErrorClass(null);

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

@@ -7,7 +7,11 @@ import {withRouter} from 'react-router-dom';
import type {Channel} from '@mattermost/types/channels'; import type {Channel} from '@mattermost/types/channels';
import {getCurrentChannel, getDirectTeammate, getMyChannelMembership} from 'mattermost-redux/selectors/entities/channels'; import {
getCurrentChannel,
getMyChannelMembership,
isDeactivatedDirectChannel,
} from 'mattermost-redux/selectors/entities/channels';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getRoles} from 'mattermost-redux/selectors/entities/roles_helpers'; import {getRoles} from 'mattermost-redux/selectors/entities/roles_helpers';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
@@ -21,12 +25,6 @@ import type {GlobalState} from 'types/store';
import ChannelView from './channel_view'; import ChannelView from './channel_view';
function isDeactivatedChannel(state: GlobalState, channelId: string) {
const teammate = getDirectTeammate(state, channelId);
return Boolean(teammate && teammate.delete_at);
}
function isMissingChannelRoles(state: GlobalState, channel?: Channel) { function isMissingChannelRoles(state: GlobalState, channel?: Channel) {
const channelRoles = channel ? getMyChannelMembership(state, channel.id)?.roles || '' : ''; const channelRoles = channel ? getMyChannelMembership(state, channel.id)?.roles || '' : '';
return !channelRoles.split(' ').some((v) => Boolean(getRoles(state)[v])); return !channelRoles.split(' ').some((v) => Boolean(getRoles(state)[v]));
@@ -45,7 +43,7 @@ function mapStateToProps(state: GlobalState) {
return { return {
channelId: channel ? channel.id : '', channelId: channel ? channel.id : '',
deactivatedChannel: channel ? isDeactivatedChannel(state, channel.id) : false, deactivatedChannel: channel ? isDeactivatedDirectChannel(state, channel.id) : false,
enableOnboardingFlow, enableOnboardingFlow,
channelIsArchived: channel ? channel.delete_at !== 0 : false, channelIsArchived: channel ? channel.delete_at !== 0 : false,
viewArchivedChannels, viewArchivedChannels,

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

@@ -8,7 +8,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
import {GenericModal} from '@mattermost/components'; import {GenericModal} from '@mattermost/components';
type Props = { type Props = {
channelDisplayName: string; channelDisplayName?: string;
onConfirm: () => Promise<{error?: string}>; onConfirm: () => Promise<{error?: string}>;
onExited: () => void; onExited: () => void;
} }
@@ -54,14 +54,25 @@ export default function DeleteScheduledPostModal({
autoCloseOnConfirmButton={false} autoCloseOnConfirmButton={false}
errorText={errorMessage} errorText={errorMessage}
> >
<FormattedMessage {
id={'scheduled_post.delete_modal.body'} channelDisplayName &&
defaultMessage={'Are you sure you want to delete this scheduled post to <strong>{displayName}</strong>?'} <FormattedMessage
values={{ id={'scheduled_post.delete_modal.body'}
strong: (chunk: string) => <strong>{chunk}</strong>, defaultMessage={'Are you sure you want to delete this scheduled post to <strong>{displayName}</strong>?'}
displayName: channelDisplayName, values={{
}} strong: (chunk: string) => <strong>{chunk}</strong>,
/> displayName: channelDisplayName,
}}
/>
}
{
!channelDisplayName &&
<FormattedMessage
id={'scheduled_post.delete_modal.body_no_channel'}
defaultMessage={'Are you sure you want to delete this scheduled post?'}
/>
}
</GenericModal> </GenericModal>
); );
} }

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

@@ -2,12 +2,15 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import moment from 'moment'; import moment from 'moment';
import React, {memo, useCallback} from 'react'; import React, {memo, useCallback, useEffect} from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import type {Channel} from '@mattermost/types/channels';
import type {ScheduledPost} from '@mattermost/types/schedule_post'; import type {ScheduledPost} from '@mattermost/types/schedule_post';
import {fetchMissingChannels} from 'mattermost-redux/actions/channels';
import {isDeactivatedDirectChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
import {openModal} from 'actions/views/modals'; import {openModal} from 'actions/views/modals';
@@ -19,9 +22,10 @@ import DeleteScheduledPostModal
from 'components/drafts/draft_actions/schedule_post_actions/delete_scheduled_post_modal'; from 'components/drafts/draft_actions/schedule_post_actions/delete_scheduled_post_modal';
import SendDraftModal from 'components/drafts/draft_actions/send_draft_modal'; import SendDraftModal from 'components/drafts/draft_actions/send_draft_modal';
import {ModalIdentifiers} from 'utils/constants'; import Constants, {ModalIdentifiers} from 'utils/constants';
import './style.scss'; import './style.scss';
import type {GlobalState} from 'types/store';
const deleteTooltipText = ( const deleteTooltipText = (
<FormattedMessage <FormattedMessage
@@ -53,17 +57,28 @@ const sendNowTooltipText = (
type Props = { type Props = {
scheduledPost: ScheduledPost; scheduledPost: ScheduledPost;
channelDisplayName: string; channel?: Channel;
onReschedule: (timestamp: number) => Promise<{error?: string}>; onReschedule: (timestamp: number) => Promise<{error?: string}>;
onDelete: (scheduledPostId: string) => Promise<{error?: string}>; onDelete: (scheduledPostId: string) => Promise<{error?: string}>;
onSend: (scheduledPostId: string) => void; onSend: (scheduledPostId: string) => void;
onEdit: () => void; onEdit: () => void;
} }
function ScheduledPostActions({scheduledPost, onReschedule, onDelete, channelDisplayName, onSend, onEdit}: Props) { function ScheduledPostActions({scheduledPost, channel, onReschedule, onDelete, onSend, onEdit}: Props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const userTimezone = useSelector(getCurrentTimezone); const userTimezone = useSelector(getCurrentTimezone);
useEffect(() => {
// this ensures the DM is loaded in redux store and is available
// later when we check if the DM is with a deactivated user.
if (channel?.type === Constants.DM_CHANNEL) {
// fetchMissingChannels uses DataLoader which de-duplicates all requested data,
// so even if we have multiple scheduled posts in a DM,
// the data loader ensured we fetch that DM only once.
dispatch(fetchMissingChannels([channel.id]));
}
}, [channel, dispatch]);
const handleReschedulePost = useCallback(() => { const handleReschedulePost = useCallback(() => {
const initialTime = moment.tz(scheduledPost.scheduled_at, userTimezone); const initialTime = moment.tz(scheduledPost.scheduled_at, userTimezone);
@@ -83,22 +98,34 @@ function ScheduledPostActions({scheduledPost, onReschedule, onDelete, channelDis
modalId: ModalIdentifiers.DELETE_DRAFT, modalId: ModalIdentifiers.DELETE_DRAFT,
dialogType: DeleteScheduledPostModal, dialogType: DeleteScheduledPostModal,
dialogProps: { dialogProps: {
channelDisplayName, channelDisplayName: channel?.display_name,
onConfirm: () => onDelete(scheduledPost.id), onConfirm: () => onDelete(scheduledPost.id),
}, },
})); }));
}, [channelDisplayName, dispatch, onDelete, scheduledPost.id]); }, [channel, dispatch, onDelete, scheduledPost.id]);
const handleSend = useCallback(() => { const handleSend = useCallback(() => {
if (!channel) {
return;
}
dispatch(openModal({ dispatch(openModal({
modalId: ModalIdentifiers.SEND_DRAFT, modalId: ModalIdentifiers.SEND_DRAFT,
dialogType: SendDraftModal, dialogType: SendDraftModal,
dialogProps: { dialogProps: {
displayName: channelDisplayName, displayName: channel.display_name,
onConfirm: () => onSend(scheduledPost.id), onConfirm: () => onSend(scheduledPost.id),
}, },
})); }));
}, [channelDisplayName, dispatch, onSend, scheduledPost.id]); }, [channel, dispatch, onSend, scheduledPost.id]);
const showEditOption = !scheduledPost.error_code;
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';
return ( return (
<div className='ScheduledPostActions'> <div className='ScheduledPostActions'>
@@ -111,36 +138,38 @@ function ScheduledPostActions({scheduledPost, onReschedule, onDelete, channelDis
/> />
{ {
!scheduledPost.error_code && ( showEditOption &&
<React.Fragment> <Action
<Action icon='icon-pencil-outline'
icon='icon-pencil-outline' id='edit'
id='edit' name='edit'
name='edit' tooltipText={editTooltipText}
tooltipText={editTooltipText} onClick={onEdit}
onClick={onEdit}
/> />
<Action
icon='icon-clock-send-outline'
id='reschedule'
name='reschedule'
tooltipText={rescheduleTooltipText}
onClick={handleReschedulePost}
/>
<Action
icon='icon-send-outline'
id='sendNow'
name='sendNow'
tooltipText={sendNowTooltipText}
onClick={handleSend}
/>
</React.Fragment>
)
} }
{
showRescheduleOption &&
<Action
icon='icon-clock-send-outline'
id='reschedule'
name='reschedule'
tooltipText={rescheduleTooltipText}
onClick={handleReschedulePost}
/>
}
{
showSendNowOption &&
<Action
icon='icon-send-outline'
id='sendNow'
name='sendNow'
tooltipText={sendNowTooltipText}
onClick={handleSend}
/>
}
</div> </div>
); );
} }

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

@@ -15,7 +15,7 @@ import type {UserProfile, UserStatus} from '@mattermost/types/users';
import {getPost as getPostAction} from 'mattermost-redux/actions/posts'; import {getPost as getPostAction} from 'mattermost-redux/actions/posts';
import {deleteScheduledPost, updateScheduledPost} from 'mattermost-redux/actions/scheduled_posts'; import {deleteScheduledPost, updateScheduledPost} from 'mattermost-redux/actions/scheduled_posts';
import {Permissions} from 'mattermost-redux/constants'; import {Permissions} from 'mattermost-redux/constants';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; import {isDeactivatedDirectChannel, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
@@ -107,13 +107,17 @@ function DraftRow({
const connectionId = useSelector(getConnectionId); const connectionId = useSelector(getConnectionId);
const isChannelArchived = Boolean(channel?.delete_at);
const isDeactivatedDM = useSelector((state: GlobalState) => isDeactivatedDirectChannel(state, channelId));
let postError = ''; let postError = '';
if (isScheduledPost) { if (isScheduledPost) {
// This is applicable only for scheduled post. // This is applicable only for scheduled post.
if (item.error_code) { if (item.error_code) {
postError = getErrorStringFromCode(intl, item.error_code); postError = getErrorStringFromCode(intl, item.error_code);
postError = getErrorStringFromCode(intl, item.error_code); } else if (isChannelArchived || isDeactivatedDM) {
postError = getErrorStringFromCode(intl, 'channel_archived');
} }
} else if (rootPostDeleted) { } else if (rootPostDeleted) {
postError = intl.formatMessage({id: 'drafts.error.post_not_found', defaultMessage: 'Thread not found'}); postError = intl.formatMessage({id: 'drafts.error.post_not_found', defaultMessage: 'Thread not found'});
@@ -279,19 +283,15 @@ function DraftRow({
isScheduledPostBeingSent.current = true; isScheduledPostBeingSent.current = true;
const postDraft = scheduledPostToPostDraft(item as ScheduledPost); const postDraft = scheduledPostToPostDraft(item as ScheduledPost);
handleOnSend(postDraft, undefined, {keepDraft: true}); handleOnSend(postDraft, undefined, {keepDraft: true, ignorePostError: true});
return Promise.resolve({}); return Promise.resolve({});
}, [handleOnSend, item, handleCancelEdit]); }, [handleOnSend, item, handleCancelEdit]);
const scheduledPostActions = useMemo(() => { const scheduledPostActions = useMemo(() => {
if (!channel) {
return null;
}
return ( return (
<ScheduledPostActions <ScheduledPostActions
scheduledPost={item as ScheduledPost} scheduledPost={item as ScheduledPost}
channelDisplayName={channel.display_name} channel={channel}
onReschedule={handleSchedulePostOnReschedule} onReschedule={handleSchedulePostOnReschedule}
onDelete={handleSchedulePostOnDelete} onDelete={handleSchedulePostOnDelete}
onSend={handleScheduledPostOnSend} onSend={handleScheduledPostOnSend}

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

@@ -50,5 +50,6 @@ const errorCodeToErrorMessage = defineMessages<ScheduledPostErrorCode>({
}); });
export function getErrorStringFromCode(intl: IntlShape, errorCode: ScheduledPostErrorCode = 'unknown') { export function getErrorStringFromCode(intl: IntlShape, errorCode: ScheduledPostErrorCode = 'unknown') {
return intl.formatMessage(errorCodeToErrorMessage[errorCode]).toUpperCase(); const textDefinition = errorCodeToErrorMessage[errorCode] ?? errorCodeToErrorMessage.unknown;
return intl.formatMessage(textDefinition).toUpperCase();
} }

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

@@ -4868,6 +4868,7 @@
"scheduled_post.channel_indicator.single": "Message scheduled for {dateTime}.", "scheduled_post.channel_indicator.single": "Message scheduled for {dateTime}.",
"scheduled_post.channel_indicator.with_other_user_late_time": "You have {count, plural, =1 {one} other {#}} <a>scheduled {count, plural, =1 {message} other {messages}}</a>.", "scheduled_post.channel_indicator.with_other_user_late_time": "You have {count, plural, =1 {one} other {#}} <a>scheduled {count, plural, =1 {message} other {messages}}</a>.",
"scheduled_post.delete_modal.body": "Are you sure you want to delete this scheduled post to <strong>{displayName}</strong>?", "scheduled_post.delete_modal.body": "Are you sure you want to delete this scheduled post to <strong>{displayName}</strong>?",
"scheduled_post.delete_modal.body_no_channel": "Are you sure you want to delete this scheduled post?",
"scheduled_post.delete_modal.title": "Delete scheduled post", "scheduled_post.delete_modal.title": "Delete scheduled post",
"scheduled_post.error_code.channel_archived": "Channel Archived", "scheduled_post.error_code.channel_archived": "Channel Archived",
"scheduled_post.error_code.channel_removed": "Channel Removed", "scheduled_post.error_code.channel_removed": "Channel Removed",

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

@@ -1341,7 +1341,7 @@ export function searchChannelsInPolicy(state: GlobalState, policyId: string, ter
export function getDirectTeammate(state: GlobalState, channelId: string): UserProfile | undefined { export function getDirectTeammate(state: GlobalState, channelId: string): UserProfile | undefined {
const channel = getChannel(state, channelId); const channel = getChannel(state, channelId);
if (!channel) { if (!channel || channel.type !== 'D') {
return undefined; return undefined;
} }
@@ -1444,3 +1444,8 @@ export const getRecentProfilesFromDMs: (state: GlobalState) => UserProfile[] = c
return [...sortedUserProfiles]; return [...sortedUserProfiles];
}, },
); );
export const isDeactivatedDirectChannel = (state: GlobalState, channelId: string) => {
const teammate = getDirectTeammate(state, channelId);
return Boolean(teammate && teammate.delete_at);
};