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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
bd8774bdce
Коммит
faa6853a28
@@ -58,6 +58,7 @@ import type {OnSubmitOptions, SubmitPostReturnType} from './views/create_comment
|
||||
|
||||
export type CreatePostOptions = {
|
||||
keepDraft?: boolean;
|
||||
ignorePostError?: boolean;
|
||||
}
|
||||
|
||||
export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & GroupChannel}): ActionFuncAsync<boolean, GlobalState> {
|
||||
|
||||
@@ -126,7 +126,7 @@ const useSubmit = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (postError) {
|
||||
if (postError && !createPostOptions?.ignorePostError) {
|
||||
setErrorClass('animation--highlight');
|
||||
setTimeout(() => {
|
||||
setErrorClass(null);
|
||||
|
||||
@@ -7,7 +7,11 @@ import {withRouter} from 'react-router-dom';
|
||||
|
||||
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 {getRoles} from 'mattermost-redux/selectors/entities/roles_helpers';
|
||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||
@@ -21,12 +25,6 @@ import type {GlobalState} from 'types/store';
|
||||
|
||||
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) {
|
||||
const channelRoles = channel ? getMyChannelMembership(state, channel.id)?.roles || '' : '';
|
||||
return !channelRoles.split(' ').some((v) => Boolean(getRoles(state)[v]));
|
||||
@@ -45,7 +43,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
|
||||
return {
|
||||
channelId: channel ? channel.id : '',
|
||||
deactivatedChannel: channel ? isDeactivatedChannel(state, channel.id) : false,
|
||||
deactivatedChannel: channel ? isDeactivatedDirectChannel(state, channel.id) : false,
|
||||
enableOnboardingFlow,
|
||||
channelIsArchived: channel ? channel.delete_at !== 0 : false,
|
||||
viewArchivedChannels,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
|
||||
type Props = {
|
||||
channelDisplayName: string;
|
||||
channelDisplayName?: string;
|
||||
onConfirm: () => Promise<{error?: string}>;
|
||||
onExited: () => void;
|
||||
}
|
||||
@@ -54,14 +54,25 @@ export default function DeleteScheduledPostModal({
|
||||
autoCloseOnConfirmButton={false}
|
||||
errorText={errorMessage}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'scheduled_post.delete_modal.body'}
|
||||
defaultMessage={'Are you sure you want to delete this scheduled post to <strong>{displayName}</strong>?'}
|
||||
values={{
|
||||
strong: (chunk: string) => <strong>{chunk}</strong>,
|
||||
displayName: channelDisplayName,
|
||||
}}
|
||||
/>
|
||||
{
|
||||
channelDisplayName &&
|
||||
<FormattedMessage
|
||||
id={'scheduled_post.delete_modal.body'}
|
||||
defaultMessage={'Are you sure you want to delete this scheduled post to <strong>{displayName}</strong>?'}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment';
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import React, {memo, useCallback, useEffect} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
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 {openModal} from 'actions/views/modals';
|
||||
@@ -19,9 +22,10 @@ import DeleteScheduledPostModal
|
||||
from 'components/drafts/draft_actions/schedule_post_actions/delete_scheduled_post_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 type {GlobalState} from 'types/store';
|
||||
|
||||
const deleteTooltipText = (
|
||||
<FormattedMessage
|
||||
@@ -53,17 +57,28 @@ const sendNowTooltipText = (
|
||||
|
||||
type Props = {
|
||||
scheduledPost: ScheduledPost;
|
||||
channelDisplayName: string;
|
||||
channel?: Channel;
|
||||
onReschedule: (timestamp: number) => Promise<{error?: string}>;
|
||||
onDelete: (scheduledPostId: string) => Promise<{error?: string}>;
|
||||
onSend: (scheduledPostId: string) => 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 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 initialTime = moment.tz(scheduledPost.scheduled_at, userTimezone);
|
||||
|
||||
@@ -83,22 +98,34 @@ function ScheduledPostActions({scheduledPost, onReschedule, onDelete, channelDis
|
||||
modalId: ModalIdentifiers.DELETE_DRAFT,
|
||||
dialogType: DeleteScheduledPostModal,
|
||||
dialogProps: {
|
||||
channelDisplayName,
|
||||
channelDisplayName: channel?.display_name,
|
||||
onConfirm: () => onDelete(scheduledPost.id),
|
||||
},
|
||||
}));
|
||||
}, [channelDisplayName, dispatch, onDelete, scheduledPost.id]);
|
||||
}, [channel, dispatch, onDelete, scheduledPost.id]);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.SEND_DRAFT,
|
||||
dialogType: SendDraftModal,
|
||||
dialogProps: {
|
||||
displayName: channelDisplayName,
|
||||
displayName: channel.display_name,
|
||||
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 (
|
||||
<div className='ScheduledPostActions'>
|
||||
@@ -111,36 +138,38 @@ function ScheduledPostActions({scheduledPost, onReschedule, onDelete, channelDis
|
||||
/>
|
||||
|
||||
{
|
||||
!scheduledPost.error_code && (
|
||||
<React.Fragment>
|
||||
<Action
|
||||
icon='icon-pencil-outline'
|
||||
id='edit'
|
||||
name='edit'
|
||||
tooltipText={editTooltipText}
|
||||
onClick={onEdit}
|
||||
showEditOption &&
|
||||
<Action
|
||||
icon='icon-pencil-outline'
|
||||
id='edit'
|
||||
name='edit'
|
||||
tooltipText={editTooltipText}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {UserProfile, UserStatus} from '@mattermost/types/users';
|
||||
import {getPost as getPostAction} from 'mattermost-redux/actions/posts';
|
||||
import {deleteScheduledPost, updateScheduledPost} from 'mattermost-redux/actions/scheduled_posts';
|
||||
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 {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
@@ -107,13 +107,17 @@ function DraftRow({
|
||||
|
||||
const connectionId = useSelector(getConnectionId);
|
||||
|
||||
const isChannelArchived = Boolean(channel?.delete_at);
|
||||
const isDeactivatedDM = useSelector((state: GlobalState) => isDeactivatedDirectChannel(state, channelId));
|
||||
|
||||
let postError = '';
|
||||
|
||||
if (isScheduledPost) {
|
||||
// This is applicable only for scheduled post.
|
||||
if (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) {
|
||||
postError = intl.formatMessage({id: 'drafts.error.post_not_found', defaultMessage: 'Thread not found'});
|
||||
@@ -279,19 +283,15 @@ function DraftRow({
|
||||
|
||||
isScheduledPostBeingSent.current = true;
|
||||
const postDraft = scheduledPostToPostDraft(item as ScheduledPost);
|
||||
handleOnSend(postDraft, undefined, {keepDraft: true});
|
||||
handleOnSend(postDraft, undefined, {keepDraft: true, ignorePostError: true});
|
||||
return Promise.resolve({});
|
||||
}, [handleOnSend, item, handleCancelEdit]);
|
||||
|
||||
const scheduledPostActions = useMemo(() => {
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScheduledPostActions
|
||||
scheduledPost={item as ScheduledPost}
|
||||
channelDisplayName={channel.display_name}
|
||||
channel={channel}
|
||||
onReschedule={handleSchedulePostOnReschedule}
|
||||
onDelete={handleSchedulePostOnDelete}
|
||||
onSend={handleScheduledPostOnSend}
|
||||
|
||||
@@ -50,5 +50,6 @@ const errorCodeToErrorMessage = defineMessages<ScheduledPostErrorCode>({
|
||||
});
|
||||
|
||||
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.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_no_channel": "Are you sure you want to delete this scheduled post?",
|
||||
"scheduled_post.delete_modal.title": "Delete scheduled post",
|
||||
"scheduled_post.error_code.channel_archived": "Channel Archived",
|
||||
"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 {
|
||||
const channel = getChannel(state, channelId);
|
||||
if (!channel) {
|
||||
if (!channel || channel.type !== 'D') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1444,3 +1444,8 @@ export const getRecentProfilesFromDMs: (state: GlobalState) => UserProfile[] = c
|
||||
return [...sortedUserProfiles];
|
||||
},
|
||||
);
|
||||
|
||||
export const isDeactivatedDirectChannel = (state: GlobalState, channelId: string) => {
|
||||
const teammate = getDirectTeammate(state, channelId);
|
||||
return Boolean(teammate && teammate.delete_at);
|
||||
};
|
||||
|
||||
Ссылка в новой задаче
Block a user