[MM-48670] Fix persistence of placeholder text (#22820)

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Konstantinos Pittas
2023-04-18 10:56:41 +03:00
committed by GitHub
parent b200a07881
commit 0c375e1ebd
29 changed files with 128 additions and 45 deletions

View File

@@ -18,7 +18,7 @@ import {
makeOnSubmit,
makeOnEditLatestPost,
} from 'actions/views/create_comment';
import {removeDraft} from 'actions/views/drafts';
import {removeDraft, setGlobalDraftSource} from 'actions/views/drafts';
import {setGlobalItem, actionOnGlobalItemsWithPrefix} from 'actions/storage';
import * as PostActions from 'actions/post_actions';
import {executeCommand} from 'actions/command';
@@ -205,12 +205,13 @@ describe('rhs view actions', () => {
const testStore = mockStore(initialState);
testStore.dispatch(setGlobalItem(`${StoragePrefixes.COMMENT_DRAFT}${rootId}`, {
const expectedKey = `${StoragePrefixes.COMMENT_DRAFT}${rootId}`;
testStore.dispatch(setGlobalItem(expectedKey, {
...draft,
createAt: 42,
updateAt: 42,
remote: false,
}));
testStore.dispatch(setGlobalDraftSource(expectedKey, false));
expect(store.getActions()).toEqual(testStore.getActions());
jest.useRealTimers();

View File

@@ -13,7 +13,7 @@ import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import {Client4} from 'mattermost-redux/client';
import {removeDraft, updateDraft} from './drafts';
import {removeDraft, setGlobalDraftSource, updateDraft} from './drafts';
jest.mock('mattermost-redux/client', () => {
const original = jest.requireActual('mattermost-redux/client');
@@ -146,12 +146,13 @@ describe('draft actions', () => {
const testStore = mockStore(initialState);
testStore.dispatch(setGlobalItem(StoragePrefixes.DRAFT + channelId, {
const expectedKey = StoragePrefixes.DRAFT + channelId;
testStore.dispatch(setGlobalItem(expectedKey, {
...draft,
createAt: 42,
updateAt: 42,
remote: false,
}));
testStore.dispatch(setGlobalDraftSource(expectedKey, false));
expect(store.getActions()).toEqual(testStore.getActions());
jest.useRealTimers();

View File

@@ -15,7 +15,7 @@ import {PostDraft} from 'types/store/draft';
import {getGlobalItem} from 'selectors/storage';
import {makeGetDrafts} from 'selectors/drafts';
import {StoragePrefixes} from 'utils/constants';
import {ActionTypes, StoragePrefixes} from 'utils/constants';
import type {Draft as ServerDraft} from '@mattermost/types/drafts';
import type {UserProfile} from '@mattermost/types/users';
@@ -101,11 +101,10 @@ export function updateDraft(key: string, value: PostDraft|null, rootId = '', sav
...value,
createAt: data.createAt || timestamp,
updateAt: timestamp,
remote: false,
};
}
dispatch(setGlobalItem(key, updatedValue));
dispatch(setGlobalDraft(key, updatedValue, false));
if (syncedDraftsAreAllowedAndEnabled(state) && save && updatedValue) {
const connectionId = getConnectionId(state);
@@ -153,6 +152,24 @@ export function setDraftsTourTipPreference(initializationState: Record<string, b
};
}
export function setGlobalDraft(key: string, value: PostDraft|null, isRemote: boolean) {
return (dispatch: DispatchFunc) => {
dispatch(setGlobalItem(key, value));
dispatch(setGlobalDraftSource(key, isRemote));
return {data: true};
};
}
export function setGlobalDraftSource(key: string, isRemote: boolean) {
return {
type: ActionTypes.SET_DRAFT_SOURCE,
data: {
key,
isRemote,
},
};
}
export function transformServerDraft(draft: ServerDraft): Draft {
let key: Draft['key'] = `${StoragePrefixes.DRAFT}${draft.channel_id}`;

View File

@@ -68,7 +68,7 @@ import {
} from 'mattermost-redux/actions/users';
import {removeNotVisibleUsers} from 'mattermost-redux/actions/websocket';
import {setGlobalItem} from 'actions/storage';
import {transformServerDraft} from 'actions/views/drafts';
import {setGlobalDraft, transformServerDraft} from 'actions/views/drafts';
import {Client4} from 'mattermost-redux/client';
import {getCurrentUser, getCurrentUserId, getUser, getIsManualStatusForUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
@@ -89,7 +89,6 @@ import {getStandardAnalytics} from 'mattermost-redux/actions/admin';
import {fetchAppBindings, fetchRHSAppsBindings} from 'mattermost-redux/actions/apps';
import {getConnectionId} from 'selectors/general';
import {getSelectedChannelId, getSelectedPost} from 'selectors/rhs';
import {isThreadOpen, isThreadManuallyUnread} from 'selectors/views/threads';
@@ -1700,20 +1699,12 @@ function handlePostAcknowledgementRemoved(msg) {
}
function handleUpsertDraftEvent(msg) {
return async (doDispatch, doGetState) => {
const state = doGetState();
const connectionId = getConnectionId(state);
return async (doDispatch) => {
const draft = JSON.parse(msg.data.draft);
const {key, value} = transformServerDraft(draft);
value.show = true;
value.remote = false;
if (msg.broadcast.omit_connection_id !== connectionId) {
value.remote = true;
}
doDispatch(setGlobalItem(key, value));
doDispatch(setGlobalDraft(key, value, true));
};
}
@@ -1722,7 +1713,11 @@ function handleDeleteDraftEvent(msg) {
const draft = JSON.parse(msg.data.draft);
const {key} = transformServerDraft(draft);
doDispatch(setGlobalItem(key, {message: '', fileInfos: [], uploadsInProgress: [], remote: true}));
doDispatch(setGlobalItem(key, {
message: '',
fileInfos: [],
uploadsInProgress: [],
}));
};
}

View File

@@ -40,6 +40,7 @@ describe('components/AdvancedCreateComment', () => {
uploadsInProgress: [{}],
fileInfos: [{}, {}, {}],
},
isRemoteDraft: false,
enableAddButton: true,
ctrlSend: false,
latestPostId,
@@ -84,9 +85,10 @@ describe('components/AdvancedCreateComment', () => {
test('should match snapshot, empty comment', () => {
const draft = emptyDraft;
const isRemoteDraft = false;
const enableAddButton = false;
const ctrlSend = true;
const props = {...baseProps, draft, enableAddButton, ctrlSend};
const props = {...baseProps, draft, isRemoteDraft, enableAddButton, ctrlSend};
const wrapper = shallow(
<AdvancedCreateComment {...props}/>,
@@ -104,8 +106,9 @@ describe('components/AdvancedCreateComment', () => {
uploadsInProgress: [],
fileInfos: [],
};
const isRemoteDraft = false;
const ctrlSend = true;
const props = {...baseProps, ctrlSend, draft, clearCommentDraftUploads, onResetHistoryIndex, getChannelMemberCountsByGroup};
const props = {...baseProps, ctrlSend, draft, isRemoteDraft, clearCommentDraftUploads, onResetHistoryIndex, getChannelMemberCountsByGroup};
const wrapper = shallow(
<AdvancedCreateComment {...props}/>,

View File

@@ -74,6 +74,9 @@ type Props = {
// The current draft of the comment
draft: PostDraft;
// Data used for knowing if the draft came from a WS event
isRemoteDraft: boolean;
// Determines if the submit button should be rendered
enableAddButton?: boolean;
@@ -233,8 +236,14 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
const rootChanged = props.rootId !== state.rootId;
const messageInHistoryChanged = props.messageInHistory !== state.messageInHistory;
if (rootChanged || messageInHistoryChanged || props.draft.remote) {
updatedState = {...updatedState, draft: {...props.draft, uploadsInProgress: rootChanged ? [] : props.draft.uploadsInProgress}};
if (rootChanged || messageInHistoryChanged || (props.isRemoteDraft && props.draft.message !== state.draft?.message)) {
updatedState = {
...updatedState,
draft: {
...props.draft,
uploadsInProgress: rootChanged ? [] : props.draft.uploadsInProgress,
},
};
}
return updatedState;
@@ -252,6 +261,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
serverError: null,
showFormat: false,
isFormattingBarHidden: props.isFormattingBarHidden,
caretPosition: props.draft.caretPosition,
};
this.textboxRef = React.createRef();
@@ -343,7 +353,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
const updatedDraft = {
...this.state.draft,
show: !isDraftEmpty(this.state.draft),
remote: false,
} as PostDraft;
this.props.onUpdateCommentDraft(updatedDraft, true);
@@ -356,7 +365,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
draft: {
...prev.draft,
show: !isDraftEmpty(prev.draft),
remote: false,
} as PostDraft,
};
}

View File

@@ -64,6 +64,7 @@ function makeMapStateToProps() {
const err = state.requests.posts.createPost.error || {};
const draft = getPostDraft(state, StoragePrefixes.COMMENT_DRAFT, ownProps.rootId);
const isRemoteDraft = state.views.drafts.remotes[`${StoragePrefixes.COMMENT_DRAFT}${ownProps.rootId}`] || false;
const channelMembersCount = getAllChannelStats(state)[ownProps.channelId] ? getAllChannelStats(state)[ownProps.channelId].member_count : 1;
const messageInHistory = getMessageInHistoryItem(state);
@@ -91,6 +92,7 @@ function makeMapStateToProps() {
return {
currentTeamId,
draft,
isRemoteDraft,
messageInHistory,
channelMembersCount,
currentUserId,
@@ -121,11 +123,11 @@ function makeMapStateToProps() {
}
function makeOnUpdateCommentDraft(rootId: string, channelId: string) {
return (draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId, remote: false} : draft, save);
return (draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId} : draft, save);
}
function makeUpdateCommentDraftWithRootId(channelId: string) {
return (rootId: string, draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId, remote: false} : draft, save);
return (rootId: string, draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId} : draft, save);
}
type Actions = {

View File

@@ -115,6 +115,7 @@ function advancedCreatePost({
fullWidthTextBox={fullWidthTextBox}
currentChannelMembersCount={currentChannelMembersCount}
draft={draft}
isRemoteDraft={false}
recentPostIdInChannel={recentPostIdInChannel}
latestReplyablePostId={latestReplyablePostId}
locale={locale}

View File

@@ -124,6 +124,9 @@ type Props = {
// Data used for populating message state from previous draft
draft: PostDraft;
// Data used for knowing if the draft came from a WS event
isRemoteDraft: boolean;
// Data used dispatching handleViewAction ex: edit post
latestReplyablePostId?: string;
locale: string;
@@ -279,7 +282,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
};
if (
props.currentChannel.id !== state.currentChannel.id ||
(props.draft.remote && props.draft.message !== state.message)
(props.isRemoteDraft && props.draft.message !== state.message)
) {
updatedState = {
...updatedState,
@@ -294,8 +297,8 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
message: this.props.draft.message,
caretPosition: this.props.draft.message.length,
message: props.draft.message,
caretPosition: props.draft.message.length,
submitting: false,
showEmojiPicker: false,
uploadsProgressPercent: {},
@@ -387,7 +390,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
this.draftsForChannel[channelId] = {
...draft,
show: !isDraftEmpty(draft),
remote: false,
} as PostDraft;
}
}

View File

@@ -77,6 +77,7 @@ function makeMapStateToProps() {
const currentChannel = getCurrentChannel(state) || {};
const currentChannelTeammateUsername = getUser(state, currentChannel.teammate_id || '')?.username;
const draft = getChannelDraft(state, currentChannel.id);
const isRemoteDraft = state.views.drafts.remotes[`${StoragePrefixes.DRAFT}${currentChannel.id}`] || false;
const latestReplyablePostId = getLatestReplyablePostId(state);
const currentChannelMembersCount = getCurrentChannelStats(state) ? getCurrentChannelStats(state).member_count : 1;
const enableEmojiPicker = config.EnableEmojiPicker === 'true';
@@ -117,6 +118,7 @@ function makeMapStateToProps() {
showSendTutorialTip,
messageInHistoryItem: getMessageInHistoryItem(state),
draft,
isRemoteDraft,
latestReplyablePostId,
locale: getCurrentLocale(state),
currentUsersLatestPost: getCurrentUsersLatestPost(state, ''),
@@ -181,12 +183,7 @@ function setDraft(key: string, value: PostDraft, draftChannelId: string, save =
const channelId = draftChannelId || getCurrentChannelId(getState());
let updatedValue = null;
if (value) {
updatedValue = {...value};
updatedValue = {
...value,
channelId,
remote: false,
};
updatedValue = {...value, channelId};
}
if (updatedValue) {
return dispatch(updateDraft(key, updatedValue, '', save));

View File

@@ -39,6 +39,7 @@ exports[`components/drafts/drafts_row should match snapshot for channel draft 1`
"type": "channel",
}
}
isRemote={false}
status={Object {}}
user={Object {}}
/>
@@ -84,6 +85,7 @@ exports[`components/drafts/drafts_row should match snapshot for thread draft 1`]
"type": "thread",
}
}
isRemote={false}
status={Object {}}
user={Object {}}
/>

View File

@@ -34,6 +34,7 @@ exports[`components/drafts/drafts should match snapshot 1`] = `
>
<Memo(Drafts)
displayName="display_name"
draftRemotes={Object {}}
drafts={Array []}
localDraftsAreEnabled={true}
status={Object {}}
@@ -76,6 +77,7 @@ exports[`components/drafts/drafts should match snapshot for local drafts disable
>
<Memo(Drafts)
displayName="display_name"
draftRemotes={Object {}}
drafts={Array []}
localDraftsAreEnabled={false}
status={Object {}}

View File

@@ -42,6 +42,7 @@ exports[`components/drafts/drafts_row should match snapshot for channel draft 1`
displayName=""
draftId=""
id={Object {}}
isRemote={false}
status={Object {}}
type="channel"
user={Object {}}
@@ -88,6 +89,7 @@ exports[`components/drafts/drafts_row should match snapshot for undefined channe
displayName=""
draftId=""
id={Object {}}
isRemote={false}
status={Object {}}
type="channel"
user={Object {}}

View File

@@ -26,6 +26,7 @@ describe('components/drafts/drafts_row', () => {
type: 'channel' as 'channel' | 'thread',
user: {} as UserProfile,
value: {} as PostDraft,
isRemote: false,
};
it('should match snapshot for channel draft', () => {

View File

@@ -29,6 +29,7 @@ type Props = {
type: 'channel' | 'thread';
user: UserProfile;
value: PostDraft;
isRemote: boolean;
}
function ChannelDraft({
@@ -40,6 +41,7 @@ function ChannelDraft({
type,
user,
value,
isRemote,
}: Props) {
const dispatch = useDispatch();
const history = useHistory();
@@ -101,7 +103,7 @@ function ChannelDraft({
/>
)}
timestamp={value.updateAt}
remote={value.remote || false}
remote={isRemote || false}
/>
<PanelBody
channelId={channel.id}

View File

@@ -21,6 +21,7 @@ describe('components/drafts/drafts_row', () => {
user: {} as UserProfile,
status: {} as UserStatus['status'],
displayName: 'test',
isRemote: false,
};
it('should match snapshot for channel draft', () => {

View File

@@ -14,9 +14,10 @@ type Props = {
status: UserStatus['status'];
displayName: string;
draft: Draft;
isRemote: boolean;
}
function DraftRow({draft, user, status, displayName}: Props) {
function DraftRow({draft, user, status, displayName, isRemote}: Props) {
switch (draft.type) {
case 'channel':
return (
@@ -26,6 +27,7 @@ function DraftRow({draft, user, status, displayName}: Props) {
user={user}
status={status}
displayName={displayName}
isRemote={isRemote}
/>
);
case 'thread':
@@ -37,6 +39,7 @@ function DraftRow({draft, user, status, displayName}: Props) {
user={user}
status={status}
displayName={displayName}
isRemote={isRemote}
/>
);
default:

View File

@@ -20,6 +20,7 @@ describe('components/drafts/drafts', () => {
displayName: 'display_name',
status: {} as UserStatus['status'],
localDraftsAreEnabled: true,
draftRemotes: {},
};
it('should match snapshot', () => {

View File

@@ -27,11 +27,13 @@ type Props = {
displayName: string;
status: UserStatus['status'];
localDraftsAreEnabled: boolean;
draftRemotes: Record<string, boolean>;
}
function Drafts({
displayName,
drafts,
draftRemotes,
status,
user,
localDraftsAreEnabled,
@@ -75,6 +77,7 @@ function Drafts({
key={d.key}
displayName={displayName}
draft={d}
isRemote={draftRemotes[d.key]}
user={user}
status={status}
/>

View File

@@ -22,6 +22,7 @@ function makeMapStateToProps() {
return {
displayName: displayUsername(user, getTeammateNameDisplaySetting(state)),
drafts: getDrafts(state),
draftRemotes: state.views.drafts.remotes,
status,
user,
localDraftsAreEnabled: localDraftsAreEnabled(state),

View File

@@ -42,6 +42,7 @@ exports[`components/drafts/drafts_row should match snapshot for channel draft 1`
displayName=""
draftId=""
id={Object {}}
isRemote={false}
rootId=""
status={Object {}}
thread={
@@ -98,6 +99,7 @@ exports[`components/drafts/drafts_row should match snapshot for undefined thread
displayName=""
draftId=""
id={Object {}}
isRemote={false}
rootId=""
status={Object {}}
thread={null}

View File

@@ -31,6 +31,7 @@ describe('components/drafts/drafts_row', () => {
type: 'thread' as 'channel' | 'thread',
user: {} as UserProfile,
value: {} as PostDraft,
isRemote: false,
};
it('should match snapshot for channel draft', () => {

View File

@@ -33,6 +33,7 @@ type Props = {
type: 'channel' | 'thread';
user: UserProfile;
value: PostDraft;
isRemote: boolean;
}
function ThreadDraft({
@@ -45,6 +46,7 @@ function ThreadDraft({
type,
user,
value,
isRemote,
}: Props) {
const dispatch = useDispatch();
@@ -107,7 +109,7 @@ function ThreadDraft({
/>
)}
timestamp={value.updateAt}
remote={value.remote || false}
remote={isRemote || false}
/>
<PanelBody
channelId={channel.id}

View File

@@ -144,7 +144,6 @@ function migrateDrafts(state: any) {
createAt: timestamp.getTime(),
updateAt: timestamp.getTime(),
show: true,
remote: false,
},
};

View File

@@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {GenericAction} from 'mattermost-redux/types/actions';
import {ActionTypes} from 'utils/constants';
function remotes(state: Record<string, boolean> = {}, action: GenericAction) {
switch (action.type) {
case ActionTypes.SET_DRAFT_SOURCE:
return {
...state,
[action.data.key]: action.data.isRemote,
};
default:
return state;
}
}
export default combineReducers({
// object that stores global draft keys indicating whether the draft came from a WebSocket event.
remotes,
});

View File

@@ -28,6 +28,7 @@ import addChannelDropdown from './add_channel_dropdown';
import addChannelCtaDropdown from './add_channel_cta_dropdown';
import threads from './threads';
import onboardingTasks from './onboarding_tasks';
import drafts from './drafts';
export default combineReducers({
admin,
@@ -55,4 +56,5 @@ export default combineReducers({
onboardingTasks,
threads,
productMenu,
drafts,
});

View File

@@ -20,7 +20,6 @@ export type PostDraft = {
createAt: number;
updateAt: number;
show?: boolean;
remote?: boolean;
metadata?: {
priority?: {
priority: PostPriority|'';

View File

@@ -62,6 +62,12 @@ export type ViewsState = {
toastStatus: boolean;
};
drafts: {
remotes: {
[storageKey: string]: boolean;
};
};
rhs: RhsViewState;
rhsSuppressed: boolean;

View File

@@ -337,6 +337,8 @@ export const ActionTypes = keyMirror({
RECEIVED_PLUGIN_INSIGHT: null,
SET_EDIT_CHANNEL_MEMBERS: null,
NEEDS_LOGGED_IN_LIMIT_REACHED_CHECK: null,
SET_DRAFT_SOURCE: null,
});
export const PostRequestTypes = keyMirror({