mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-38745 feat: Add dont clear option for DND (#26334)
* feat: Add dont clear option for DND * Merge conflicts fixed * ci: lint, types, i18n fix * refactor: Remove unused code --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
9e5d486a5b
commit
1dc5c006f2
@ -11,6 +11,7 @@
|
||||
|
||||
describe('DND Status - Setting Your Own DND Status', () => {
|
||||
const dndTimes = [
|
||||
'dndTime-dont_clear_menuitem',
|
||||
'dndTime-thirty_minutes_menuitem',
|
||||
'dndTime-one_hour_menuitem',
|
||||
'dndTime-two_hours_menuitem',
|
||||
@ -29,7 +30,7 @@ describe('DND Status - Setting Your Own DND Status', () => {
|
||||
|
||||
it('MM-8497_1 Set status DND with predefined end times', () => {
|
||||
// # Loop through all predefined end times and verify them
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// # Open status dropdown menu and hover over Do Not Disturb option
|
||||
openDndStatusSubMenu();
|
||||
|
||||
|
@ -102,6 +102,7 @@ exports[`components/StatusDropdown should match snapshot in default state 1`] =
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -122,7 +123,13 @@ exports[`components/StatusDropdown should match snapshot in default state 1`] =
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -168,7 +175,7 @@ exports[`components/StatusDropdown should match snapshot in default state 1`] =
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -423,6 +430,7 @@ exports[`components/StatusDropdown should match snapshot with custom status and
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -443,7 +451,13 @@ exports[`components/StatusDropdown should match snapshot with custom status and
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -489,7 +503,7 @@ exports[`components/StatusDropdown should match snapshot with custom status and
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -696,6 +710,7 @@ exports[`components/StatusDropdown should match snapshot with custom status enab
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -716,7 +731,13 @@ exports[`components/StatusDropdown should match snapshot with custom status enab
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -762,7 +783,7 @@ exports[`components/StatusDropdown should match snapshot with custom status enab
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -969,6 +990,7 @@ exports[`components/StatusDropdown should match snapshot with custom status expi
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -989,7 +1011,13 @@ exports[`components/StatusDropdown should match snapshot with custom status expi
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -1035,7 +1063,7 @@ exports[`components/StatusDropdown should match snapshot with custom status expi
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -1243,6 +1271,7 @@ exports[`components/StatusDropdown should match snapshot with custom status puls
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -1263,7 +1292,13 @@ exports[`components/StatusDropdown should match snapshot with custom status puls
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -1309,7 +1344,7 @@ exports[`components/StatusDropdown should match snapshot with custom status puls
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -1492,6 +1527,7 @@ exports[`components/StatusDropdown should match snapshot with profile picture UR
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -1512,7 +1548,13 @@ exports[`components/StatusDropdown should match snapshot with profile picture UR
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -1558,7 +1600,7 @@ exports[`components/StatusDropdown should match snapshot with profile picture UR
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -1733,6 +1775,7 @@ exports[`components/StatusDropdown should match snapshot with status dropdown op
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -1753,7 +1796,13 @@ exports[`components/StatusDropdown should match snapshot with status dropdown op
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -1799,7 +1848,7 @@ exports[`components/StatusDropdown should match snapshot with status dropdown op
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -2006,6 +2055,7 @@ exports[`components/StatusDropdown should not show clear status button when cust
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -2026,7 +2076,13 @@ exports[`components/StatusDropdown should not show clear status button when cust
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -2072,7 +2128,7 @@ exports[`components/StatusDropdown should not show clear status button when cust
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -2327,6 +2383,7 @@ exports[`components/StatusDropdown should show clear status button when custom s
|
||||
text="Away"
|
||||
/>
|
||||
<SubMenuItem
|
||||
action={[Function]}
|
||||
ariaLabel="Do not disturb. Disables all notifications"
|
||||
direction="left"
|
||||
extraText="Disables all notifications"
|
||||
@ -2347,7 +2404,13 @@ exports[`components/StatusDropdown should show clear status button when custom s
|
||||
"direction": "right",
|
||||
"id": "dndSubMenu-header",
|
||||
"isHeader": true,
|
||||
"text": "Disable notifications until:",
|
||||
"text": "Clear after:",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-dont_clear",
|
||||
"text": "Don't clear",
|
||||
},
|
||||
Object {
|
||||
"action": [Function],
|
||||
@ -2393,7 +2456,7 @@ exports[`components/StatusDropdown should show clear status button when custom s
|
||||
"action": [Function],
|
||||
"direction": "right",
|
||||
"id": "dndTime-custom",
|
||||
"text": "Custom",
|
||||
"text": "Choose date and time",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import {Client4} from 'mattermost-redux/client';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {get, getBool, getInt} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentUser, getDndEndTimeForUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {setStatusDropdown} from 'actions/views/status_dropdown';
|
||||
@ -53,6 +53,7 @@ function makeMapStateToProps() {
|
||||
showCustomStatusPulsatingDot: showStatusDropdownPulsatingDot(state),
|
||||
showCompleteYourProfileTour,
|
||||
timezone: getCurrentTimezone(state),
|
||||
dndEndTime: getDndEndTimeForUserId(state, userId),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {injectIntl, FormattedDate, FormattedMessage, FormattedTime, defineMessage, defineMessages} from 'react-intl';
|
||||
@ -35,7 +36,7 @@ import Avatar from 'components/widgets/users/avatar/avatar';
|
||||
import type {TAvatarSizeToken} from 'components/widgets/users/avatar/avatar';
|
||||
|
||||
import {Constants, ModalIdentifiers, UserStatuses} from 'utils/constants';
|
||||
import {getCurrentDateTimeForTimezone, getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
import {getBrowserTimezone, getCurrentDateTimeForTimezone, getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
|
||||
import type {ModalData} from 'types/actions';
|
||||
import type {Menu as MenuType} from 'types/store/plugins';
|
||||
@ -64,6 +65,7 @@ type Props = {
|
||||
showCompleteYourProfileTour: boolean;
|
||||
showCustomStatusPulsatingDot: boolean;
|
||||
timezone?: string;
|
||||
dndEndTime?: number;
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -100,10 +102,6 @@ export const statusDropdownMessages: Record<string, Record<string, MessageDescri
|
||||
id: 'status_dropdown.set_dnd',
|
||||
defaultMessage: 'Do not disturb',
|
||||
},
|
||||
extra: {
|
||||
id: 'status_dropdown.set_dnd.extra',
|
||||
defaultMessage: 'Disables all notifications',
|
||||
},
|
||||
}),
|
||||
offline: defineMessages({
|
||||
name: {
|
||||
@ -115,11 +113,12 @@ export const statusDropdownMessages: Record<string, Record<string, MessageDescri
|
||||
|
||||
export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
dndTimes = [
|
||||
{id: 'dont_clear', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.dont_clear', defaultMessage: 'Don\'t clear'})},
|
||||
{id: 'thirty_minutes', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.thirty_minutes', defaultMessage: '30 mins'})},
|
||||
{id: 'one_hour', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.one_hour', defaultMessage: '1 hour'})},
|
||||
{id: 'two_hours', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.two_hours', defaultMessage: '2 hours'})},
|
||||
{id: 'tomorrow', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.tomorrow', defaultMessage: 'Tomorrow'})},
|
||||
{id: 'custom', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.custom', defaultMessage: 'Custom'})},
|
||||
{id: 'custom', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.custom', defaultMessage: 'Choose date and time'})},
|
||||
];
|
||||
static defaultProps = {
|
||||
userId: '',
|
||||
@ -176,18 +175,21 @@ export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
let endTime = currentDate;
|
||||
switch (index) {
|
||||
case 0:
|
||||
endTime = moment(0);
|
||||
break;
|
||||
case 1:
|
||||
// add 30 minutes in current time
|
||||
endTime = currentDate.add(30, 'minutes');
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
// add 1 hour in current time
|
||||
endTime = currentDate.add(1, 'hour');
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
// add 2 hours in current time
|
||||
endTime = currentDate.add(2, 'hours');
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
// set to next day 9 in the morning
|
||||
endTime = currentDate.add(1, 'day').set({hour: 9, minute: 0});
|
||||
break;
|
||||
@ -381,10 +383,55 @@ export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
renderDndExtraText = (dndEndTime?: number, timezone?: string) => {
|
||||
if (!(dndEndTime && dndEndTime > 0)) {
|
||||
return this.props.intl.formatMessage({id: 'status_dropdown.set_dnd.extra', defaultMessage: 'Disables all notifications'});
|
||||
}
|
||||
|
||||
const tz = timezone || getBrowserTimezone();
|
||||
const currentTime = moment().tz(tz);
|
||||
const endTime = moment.unix(dndEndTime).tz(tz);
|
||||
|
||||
let formattedEndTime;
|
||||
|
||||
const diffDays = endTime.clone().startOf('day').diff(currentTime.clone().startOf('day'), 'days');
|
||||
|
||||
switch (diffDays) {
|
||||
case 0:
|
||||
formattedEndTime = (
|
||||
<FormattedMessage
|
||||
id='custom_status.expiry.until'
|
||||
defaultMessage='Until {time}'
|
||||
values={{time: endTime.format('h:mm A')}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
formattedEndTime = (
|
||||
<FormattedMessage
|
||||
id='custom_status.expiry.until_tomorrow'
|
||||
defaultMessage='Until Tomorrow {time}'
|
||||
values={{time: endTime.format('h:mm A')}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
formattedEndTime = (
|
||||
<FormattedMessage
|
||||
id='custom_status.expiry.until'
|
||||
defaultMessage='Until {time}'
|
||||
values={{time: endTime.format('lll')}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return formattedEndTime;
|
||||
};
|
||||
|
||||
render = (): JSX.Element => {
|
||||
const {intl} = this.props;
|
||||
const needsConfirm = this.isUserOutOfOffice() && this.props.autoResetPref === '';
|
||||
const {status, customStatus, isCustomStatusExpired, currentUser} = this.props;
|
||||
const {status, customStatus, isCustomStatusExpired, currentUser, timezone, dndEndTime} = this.props;
|
||||
const isStatusSet = customStatus && !isCustomStatusExpired && (customStatus.text?.length > 0 || customStatus.emoji?.length > 0);
|
||||
|
||||
const setOnline = needsConfirm ? () => this.showStatusChangeConfirmation('online') : this.setOnline;
|
||||
@ -404,13 +451,13 @@ export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
{
|
||||
id: 'dndSubMenu-header',
|
||||
direction: 'right',
|
||||
text: this.props.intl.formatMessage({id: 'status_dropdown.dnd_sub_menu_header', defaultMessage: 'Disable notifications until:'}),
|
||||
text: this.props.intl.formatMessage({id: 'status_dropdown.dnd_sub_menu_header', defaultMessage: 'Clear after:'}),
|
||||
isHeader: true,
|
||||
},
|
||||
] as MenuType[])?.concat(
|
||||
this.dndTimes.map<MenuType>(({id, label}, index) => {
|
||||
let text: MenuType['text'] = this.props.intl.formatMessage(label);
|
||||
if (index === 3) {
|
||||
if (index === 4) {
|
||||
const tomorrow = getCurrentMomentForTimezone(this.props.timezone).add(1, 'day').set({hour: 9, minute: 0}).toDate();
|
||||
text = (
|
||||
<>
|
||||
@ -437,7 +484,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
direction: 'right',
|
||||
text,
|
||||
action:
|
||||
index === 4 ? () => setCustomTimedDnd() : () => setDnd(index),
|
||||
index === 5 ? () => setCustomTimedDnd() : () => setDnd(index),
|
||||
};
|
||||
}),
|
||||
);
|
||||
@ -483,6 +530,8 @@ export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
const dndExtraText = this.renderDndExtraText(dndEndTime, timezone);
|
||||
|
||||
return (
|
||||
<MenuWrapper
|
||||
onToggle={this.onToggle}
|
||||
@ -575,9 +624,9 @@ export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
<Menu.ItemSubMenu
|
||||
subMenu={dndSubMenuItems}
|
||||
ariaLabel={`${this.props.intl.formatMessage(statusDropdownMessages.dnd.name)}. ${this.props.intl.formatMessage(statusDropdownMessages.dnd.extra)}`}
|
||||
ariaLabel={`${this.props.intl.formatMessage(statusDropdownMessages.dnd.name)}. ${dndExtraText}`}
|
||||
text={this.props.intl.formatMessage(statusDropdownMessages.dnd.name)}
|
||||
extraText={this.props.intl.formatMessage(statusDropdownMessages.dnd.extra)}
|
||||
extraText={dndExtraText}
|
||||
icon={(
|
||||
<StatusIcon
|
||||
status={'dnd'}
|
||||
@ -588,6 +637,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
direction={'left'}
|
||||
openUp={this.state.openUp}
|
||||
id={'status-menu-dnd'}
|
||||
action={() => setDnd(0)}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
onClick={setOffline}
|
||||
|
@ -21,7 +21,6 @@ exports[`components/widgets/menu/menu_items/submenu_item empty subMenu should ma
|
||||
<div
|
||||
className=""
|
||||
id="1"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
@ -89,7 +88,6 @@ exports[`components/widgets/menu/menu_items/submenu_item present subMenu should
|
||||
<div
|
||||
className=""
|
||||
id="1"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
@ -142,7 +140,6 @@ exports[`components/widgets/menu/menu_items/submenu_item present subMenu should
|
||||
<div
|
||||
className=""
|
||||
id="A"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
@ -199,7 +196,6 @@ exports[`components/widgets/menu/menu_items/submenu_item present subMenu should
|
||||
<div
|
||||
className=""
|
||||
id="B"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
|
@ -4,6 +4,7 @@
|
||||
import {mount} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import {render, screen, userEvent} from 'tests/react_testing_utils';
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import SubMenuItem from './submenu_item';
|
||||
@ -27,6 +28,7 @@ describe('components/widgets/menu/menu_items/submenu_item', () => {
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('present subMenu should match snapshot with submenu', () => {
|
||||
const wrapper = mount(
|
||||
<SubMenuItem
|
||||
@ -52,11 +54,13 @@ describe('components/widgets/menu/menu_items/submenu_item', () => {
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('test subMenu click triggers action', async () => {
|
||||
const action1 = jest.fn().mockReturnValueOnce('default');
|
||||
const action2 = jest.fn().mockReturnValueOnce('default');
|
||||
const action3 = jest.fn().mockReturnValueOnce('default');
|
||||
const wrapper = mount(
|
||||
|
||||
test('test subMenu click triggers action', () => {
|
||||
const action1 = jest.fn();
|
||||
const action2 = jest.fn();
|
||||
const action3 = jest.fn();
|
||||
|
||||
render(
|
||||
<SubMenuItem
|
||||
key={'_pluginmenuitem'}
|
||||
id={'Z'}
|
||||
@ -79,16 +83,20 @@ describe('components/widgets/menu/menu_items/submenu_item', () => {
|
||||
root={true}
|
||||
/>,
|
||||
);
|
||||
wrapper.setState({show: true});
|
||||
wrapper.find('#Z').at(1).simulate('click');
|
||||
await expect(action1).toHaveBeenCalledTimes(1);
|
||||
wrapper.setState({show: true});
|
||||
wrapper.find('#A').at(1).simulate('click');
|
||||
await expect(action2).toHaveBeenCalledTimes(1);
|
||||
wrapper.setState({show: true});
|
||||
wrapper.find('#B').at(1).simulate('click');
|
||||
await expect(action3).toHaveBeenCalledTimes(1);
|
||||
|
||||
userEvent.click(screen.getByText('test'));
|
||||
expect(action1).toHaveBeenCalledTimes(1);
|
||||
|
||||
userEvent.click(screen.getByText('Test A'));
|
||||
expect(action2).toHaveBeenCalledTimes(1);
|
||||
|
||||
userEvent.click(screen.getByText('Test B'));
|
||||
expect(action3).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Confirm that the parent's action wasn't called again when clicking on a child item
|
||||
expect(action1).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should show/hide submenu based on keyboard commands', () => {
|
||||
const wrapper = mount<SubMenuItem>(
|
||||
<SubMenuItem
|
||||
|
@ -55,7 +55,7 @@ export type Props = {
|
||||
direction?: 'left' | 'right';
|
||||
openUp?: boolean;
|
||||
styleSelectableItem?: boolean;
|
||||
extraText?: string;
|
||||
extraText?: string| JSX.Element;
|
||||
rightDecorator?: React.ReactNode;
|
||||
isHeader?: boolean;
|
||||
tabIndex?: number;
|
||||
@ -95,7 +95,7 @@ export default class SubMenuItem extends React.PureComponent<Props, State> {
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
private onClick = (event: React.SyntheticEvent<HTMLElement>) => {
|
||||
private onClick = (event: React.SyntheticEvent<HTMLElement>| React.BaseSyntheticEvent<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
const {id, postId, subMenu, action, root, isHeader} = this.props;
|
||||
const isMobile = isMobileViewHack();
|
||||
@ -112,8 +112,13 @@ export default class SubMenuItem extends React.PureComponent<Props, State> {
|
||||
} else if (action) { // leaf node in the tree handles action only
|
||||
action(postId);
|
||||
}
|
||||
} else if (event.currentTarget.id === id && action) {
|
||||
action(postId);
|
||||
} else {
|
||||
const shouldCallAction =
|
||||
(event.type === 'keydown' && event.currentTarget.id === id) ||
|
||||
event.target.parentElement.id === id;
|
||||
if (shouldCallAction && action) {
|
||||
action(postId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -233,7 +238,6 @@ export default class SubMenuItem extends React.PureComponent<Props, State> {
|
||||
aria-label={ariaLabel}
|
||||
onMouseEnter={this.show}
|
||||
onMouseLeave={this.hide}
|
||||
onClick={this.onClick}
|
||||
tabIndex={tabIndex ?? 0}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
>
|
||||
|
@ -1,10 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow, mount} from 'enzyme';
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import {render, screen, userEvent} from 'tests/react_testing_utils';
|
||||
|
||||
import SubMenuModal from './submenu_modal';
|
||||
|
||||
jest.mock('../../is_mobile_view_hack', () => ({
|
||||
@ -67,24 +69,19 @@ describe('components/submenu_modal', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
};
|
||||
const wrapper = mount(
|
||||
|
||||
render(
|
||||
<SubMenuModal {...props}/>,
|
||||
);
|
||||
|
||||
wrapper.setState({show: true});
|
||||
await wrapper.find('#A').at(1).simulate('click');
|
||||
userEvent.click(screen.getByText('Text A'));
|
||||
expect(action1).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.state('show')).toEqual(false);
|
||||
|
||||
wrapper.setState({show: true});
|
||||
await wrapper.find('#B').at(1).simulate('click');
|
||||
userEvent.click(screen.getByText('Text B'));
|
||||
expect(action2).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.state('show')).toEqual(false);
|
||||
|
||||
wrapper.setState({show: true});
|
||||
await wrapper.find('#C').at(1).simulate('click');
|
||||
userEvent.click(screen.getByText('Text C'));
|
||||
expect(action3).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.state('show')).toEqual(false);
|
||||
});
|
||||
|
||||
test('should have called props.onExited when Modal.onExited is called', () => {
|
||||
|
@ -3433,7 +3433,8 @@
|
||||
"custom_status.expiry_dropdown.this_week": "This week",
|
||||
"custom_status.expiry_dropdown.today": "Today",
|
||||
"custom_status.expiry.time_picker.title": "Time",
|
||||
"custom_status.expiry.until": "Until",
|
||||
"custom_status.expiry.until": "Until {time}",
|
||||
"custom_status.expiry.until_tomorrow": "Until Tomorrow {time}",
|
||||
"custom_status.modal_cancel": "Clear Status",
|
||||
"custom_status.modal_confirm": "Set Status",
|
||||
"custom_status.set_status": "Set a status",
|
||||
@ -5181,8 +5182,9 @@
|
||||
"start_trial.modal.loading": "Loading...",
|
||||
"start_trial.tutorialTip.desc": "Explore our most requested premium features. Determine user access with Guest Accounts, automate compliance reports, and send secure ID-only mobile push notifications.",
|
||||
"start_trial.tutorialTip.title": "Try our premium features for free",
|
||||
"status_dropdown.dnd_sub_menu_header": "Disable notifications until:",
|
||||
"status_dropdown.dnd_sub_menu_item.custom": "Custom",
|
||||
"status_dropdown.dnd_sub_menu_header": "Clear after:",
|
||||
"status_dropdown.dnd_sub_menu_item.custom": "Choose date and time",
|
||||
"status_dropdown.dnd_sub_menu_item.dont_clear": "Don't clear",
|
||||
"status_dropdown.dnd_sub_menu_item.one_hour": "1 hour",
|
||||
"status_dropdown.dnd_sub_menu_item.thirty_minutes": "30 mins",
|
||||
"status_dropdown.dnd_sub_menu_item.tomorrow": "Tomorrow",
|
||||
|
@ -1012,6 +1012,7 @@ describe('Reducers.users', () => {
|
||||
|
||||
let state: UsersState = {
|
||||
currentUserId: '',
|
||||
dndEndTimes: {},
|
||||
mySessions: [],
|
||||
myAudits: [],
|
||||
myUserAccessTokens: {},
|
||||
@ -1060,6 +1061,7 @@ describe('Reducers.users', () => {
|
||||
|
||||
expect(nextState).toEqual({
|
||||
currentUserId: '',
|
||||
dndEndTimes: {},
|
||||
mySessions: [],
|
||||
myAudits: [],
|
||||
myUserAccessTokens: {},
|
||||
|
@ -496,6 +496,35 @@ function addToState<T>(state: Record<string, T>, key: string, value: T): Record<
|
||||
};
|
||||
}
|
||||
|
||||
function dndEndTimes(state: RelationOneToOne<UserProfile, number> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_STATUS: {
|
||||
const userId = action.data.user_id;
|
||||
const endTime = action.data.dnd_end_time;
|
||||
|
||||
return addToState(state, userId, endTime);
|
||||
}
|
||||
case UserTypes.RECEIVED_STATUSES: {
|
||||
const userStatuses: UserStatus[] = action.data;
|
||||
|
||||
return userStatuses.reduce((nextState, userStatus) => addToState(nextState, userStatus.user_id, userStatus.dnd_end_time || 0), state);
|
||||
}
|
||||
case UserTypes.PROFILE_NO_LONGER_VISIBLE: {
|
||||
if (state[action.data.user_id]) {
|
||||
const newState = {...state};
|
||||
delete newState[action.data.user_id];
|
||||
return newState;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function statuses(state: RelationOneToOne<UserProfile, string> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_STATUS: {
|
||||
@ -705,6 +734,9 @@ export default combineReducers({
|
||||
// object where every key is a group id and has a Set with the users id that are members of the group
|
||||
profilesNotInGroup,
|
||||
|
||||
// object where every key is the user id and has a value with the dnd end time of each user
|
||||
dndEndTimes,
|
||||
|
||||
// object where every key is the user id and has a value with the current status of each user
|
||||
statuses,
|
||||
|
||||
|
@ -435,6 +435,10 @@ export function getStatusForUserId(state: GlobalState, userId: UserProfile['id']
|
||||
return getUserStatuses(state)[userId];
|
||||
}
|
||||
|
||||
export function getDndEndTimeForUserId(state: GlobalState, userId: UserProfile['id']): number {
|
||||
return state.entities.users.dndEndTimes[userId];
|
||||
}
|
||||
|
||||
export function getTotalUsersStats(state: GlobalState) {
|
||||
return state.entities.users.stats;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ const state: GlobalState = {
|
||||
filteredStats: {},
|
||||
myUserAccessTokens: {},
|
||||
lastActivity: {},
|
||||
dndEndTimes: {},
|
||||
},
|
||||
limits: {
|
||||
usersLimits: {
|
||||
|
@ -23,6 +23,7 @@ const emptyOtherUsersState: Omit<GlobalState['entities']['users'], 'profiles' |
|
||||
filteredStats: {},
|
||||
myUserAccessTokens: {},
|
||||
lastActivity: {},
|
||||
dndEndTimes: {},
|
||||
};
|
||||
|
||||
export const adminUsersState: () => GlobalState['entities']['users'] = () => ({
|
||||
|
@ -82,6 +82,7 @@ export type UsersState = {
|
||||
filteredStats: Partial<UsersStats>;
|
||||
myUserAccessTokens: Record<string, UserAccessToken>;
|
||||
lastActivity: RelationOneToOne<UserProfile, number>;
|
||||
dndEndTimes: RelationOneToOne<UserProfile, number>;
|
||||
};
|
||||
|
||||
export type UserTimezone = {
|
||||
|
Loading…
Reference in New Issue
Block a user