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 = {
|
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);
|
||||||
|
};
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user