* removed server side

* Updated store layer

* unused import

* Updated autogenerated code template

* Updated tests

* lint fix

* unused translations

* webapp side

* Updated i18n

* lint fix:

* type fix

* Updated snapshots

* Removed insights from API specs

* updated e2e

* Updated e2e tests

* Updated e2e tests

* Removed insights tests

* Removed Insights as possible channel to load in sidebar from test

* Removed more insights tests

* More e2e fixed

* More cleanup

* Lint

* More cleanup in client4 and boards api

* More cleanup

* Fixes

* lint fix

---------

Co-authored-by: maria.nunez <maria.nunez@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Harshil Sharma
2023-07-25 12:34:38 +05:30
коммит произвёл GitHub
родитель e37459cd00
Коммит 26617fcbdc
192 изменённых файлов: 47 добавлений и 19885 удалений

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

@@ -133,8 +133,6 @@ export function switchToChannel(channel: Channel & {userId?: string}) {
getHistory().push(`${teamUrl}/channels/${gmChannel.name}`);
} else if (channel.type === Constants.THREADS) {
getHistory().push(`${teamUrl}/${channel.name}`);
} else if (channel.type === Constants.INSIGHTS) {
getHistory().push(`${teamUrl}/${channel.name}`);
} else {
getHistory().push(`${teamUrl}/channels/${channel.name}`);
}

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

@@ -1,921 +0,0 @@
@mixin channel-display-name {
overflow: hidden;
margin-left: 7px;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin channel-icon {
width: 14px;
height: 14px;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-size: 14px;
}
.ActivityAndInsights {
background-color: rgba(var(--center-channel-color-rgb), 0.04);
.insights-body {
padding: 24px;
overflow-x: hidden;
overflow-y: auto;
@media screen and (min-width: 1680px) {
display: flex;
flex-direction: column;
.card-row {
width: 1280px;
margin: auto;
}
}
.card-row {
&::after {
display: block;
height: 0;
clear: both;
content: " ";
}
}
@media screen and (max-width: 768px) {
padding: 16px;
}
.top-channels-card {
.Card__body {
padding-right: 12px;
@media screen and (max-width: 1019px) {
padding-left: 12px;
}
}
}
.top-reactions-card {
&.small {
@media screen and (max-width: 1279px) and (min-width: 769px) {
width: calc(100% - 16px);
}
}
.Card__header {
padding-bottom: 16px;
}
}
.top-playbooks-card,
.top-boards-card,
.least-active-channels-card,
.top-threads-card {
&.small {
@media screen and (max-width: 1279px) and (min-width: 1020px) {
width: calc((100%/2) - 16px);
}
@media screen and (max-width: 1019px) and (min-width: 769px) {
width: calc(100% - 16px);
}
}
.Card__header {
padding-bottom: 16px;
}
.Card__body {
padding: 0 12px 12px;
}
}
.top-boards-card {
.Card__body {
padding: 0 12px 18px;
}
}
.top-playbooks-card,
.least-active-channels-card {
&.medium {
@media screen and (max-width: 1279px) and (min-width: 769px) {
width: calc(100% - 16px);
}
}
}
.top-playbooks-card,
.top-dms-card {
.Card__body {
padding: 0 12px 12px;
}
}
.top-channel-container {
display: flex;
@media screen and (max-width: 1019px) {
flex-direction: column;
}
.top-channel-line-chart {
flex: 0.67;
margin-right: 16px;
@media screen and (max-width: 1019px) {
flex: 1;
padding: 12px;
margin-right: 0;
}
.col-sm-12 {
padding: 0;
.total-count {
position: relative;
.title {
display: none;
}
.content {
position: relative;
}
.chartjs-render-monitor {
width: 100% !important;
min-height: 200px !important;
max-height: 200px;
&:hover {
cursor: pointer;
}
}
}
}
}
.top-channel-list {
flex: 0.33;
margin-left: 16px;
@media screen and (max-width: 1019px) {
flex: 1;
margin-left: 0;
}
.top-channel-loading-row {
display: flex;
align-items: center;
padding: 12px 12px 12px 0;
}
.channel-row {
display: flex;
align-items: center;
padding: 10px 12px;
&:nth-of-type(1) {
span.horizontal-bar {
background-color: var(--button-bg);
}
}
&:nth-of-type(2) {
span.horizontal-bar {
background-color: var(--online-indicator);
}
}
&:nth-of-type(3) {
span.horizontal-bar {
background-color: var(--away-indicator);
}
}
&:nth-of-type(4) {
span.horizontal-bar {
background-color: var(--dnd-indicator);
}
}
&:nth-of-type(5) {
span.horizontal-bar {
background-color: var(--new-message-separator);
}
}
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
text-decoration: none;
}
&:focus {
text-decoration: none;
}
.channel-display-name {
display: flex;
overflow: hidden;
flex: 1 0;
i.icon {
@include channel-icon;
}
.display-name {
@include channel-display-name;
}
}
.channel-message-count {
display: flex;
flex: 1 0;
align-items: center;
.message-count {
display: inline-flex;
flex: 0.2 0;
justify-content: flex-end;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
text-align: right;
}
span.horizontal-bar {
height: 8px;
margin-left: 16px;
border-radius: 6px;
}
}
}
}
}
.top-reaction-container {
display: flex;
align-items: center;
justify-content: center;
margin: 0 18px;
.bar-chart-entry {
display: flex;
flex: 0.2;
flex-direction: column;
align-items: center;
margin-top: auto;
&:nth-of-type(1) {
.bar-chart-data {
background-color: var(--button-bg);
}
}
&:nth-of-type(2) {
.bar-chart-data {
background-color: var(--online-indicator);
}
}
&:nth-of-type(3) {
.bar-chart-data {
background-color: var(--dnd-indicator);
}
}
&:nth-of-type(4) {
.bar-chart-data {
background-color: var(--new-message-separator);
}
}
&:nth-of-type(5) {
.bar-chart-data {
background-color: var(--away-indicator);
}
}
.reaction-count {
margin-bottom: 6px;
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 16px;
}
.bar-chart-data {
width: 8px;
margin-bottom: 6px;
border-radius: 6px;
}
}
}
.top-thread-container {
.top-thread-loading-container {
padding: 0 12px;
&:nth-of-type(2) {
margin-top: 20px;
}
&:nth-of-type(3) {
margin-top: 20px;
}
.top-thread-loading-row {
display: flex;
align-items: center;
padding-bottom: 8px;
}
}
.thread-list {
.thread-item {
padding: 8px 12px;
&:nth-of-type(3) {
.preview {
margin-bottom: 0;
}
}
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
}
}
}
}
.top-board-container {
margin-bottom: -3px;
.top-board-loading-container {
display: flex;
align-items: center;
padding: 0 12px;
margin-bottom: 20px;
&:nth-of-type(4) {
margin-bottom: 4px;
}
.loading-lines {
display: flex;
flex: 1;
flex-direction: column;
margin-left: 10px;
}
}
.board-list {
.board-item {
display: flex;
align-items: center;
padding: 10px 12px;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
&:focus {
text-decoration: none;
}
span.board-icon {
display: flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
float: left;
font-size: 32px;
}
.display-info {
display: flex;
overflow: hidden;
flex-direction: column;
margin-left: 12px;
.display-name {
overflow: hidden;
margin-right: 10px;
color: var(--center-channel-color);
float: left;
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.update-counts {
color: rgba(var(--center-channel-color-rgb), 0.56);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.Avatars.Avatars___xs {
margin-left: auto;
float: right;
}
}
}
}
.least-active-channels-container {
.least-active-channels-loading-container {
display: flex;
width: 100%;
align-items: center;
padding: 0 12px;
margin-top: 24px;
&:nth-of-type(1) {
margin-top: 10px;
}
&:nth-of-type(4) {
margin-bottom: 10px;
}
}
.channel-list {
.channel-row {
display: flex;
align-items: center;
padding: 10px 12px;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
&:focus {
text-decoration: none;
}
.channel-info {
display: flex;
overflow: hidden;
flex: 0.4;
flex-direction: column;
.channel-display-name {
display: flex;
overflow: hidden;
i.icon {
@include channel-icon;
}
.display-name {
@include channel-display-name;
}
}
.last-activity {
color: rgba(var(--center-channel-color-rgb), 0.56);
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.Avatars {
margin-left: auto;
}
}
}
.empty-state {
margin: 38px 0;
}
}
.top-playbooks-container {
.top-playbooks-loading-container {
padding: 0 12px;
&:nth-of-type(1) {
margin-top: 12px;
}
&:nth-of-type(2) {
margin-top: 24px;
margin-bottom: 24px;
}
&:nth-of-type(3) {
margin-bottom: 12px;
> div {
&:nth-of-type(2) {
margin-bottom: 0;
}
}
}
}
.playbooks-list {
.playbook-item {
display: flex;
align-items: center;
padding: 10px 12px;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
&:focus {
text-decoration: none;
}
&:nth-of-type(2) {
.horizontal-bar {
opacity: 0.7;
}
}
&:nth-of-type(3) {
.horizontal-bar {
opacity: 0.5;
}
}
.display-info {
display: flex;
overflow: hidden;
flex: 0.6 0;
flex-direction: column;
&.run-info {
flex: 0.4 0;
}
.display-name {
overflow: hidden;
margin-right: 10px;
color: var(--center-channel-color);
float: left;
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.last-run-time {
color: rgba(var(--center-channel-color-rgb), 0.56);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.horizontal-bar {
height: 8px;
margin-top: 4px;
margin-bottom: 4px;
background-color: var(--button-bg);
border-radius: 6px;
}
}
}
.empty-state {
margin: 38px 0;
}
}
.top-dms-container {
display: flex;
align-items: center;
@media screen and (max-width: 1279px) {
flex-direction: row;
flex-wrap: wrap;
}
.dms-loading-container {
display: flex;
flex: 0.2;
flex-direction: column;
align-items: left;
margin-top: auto;
.title-line {
width: 124px;
margin-top: 16px;
margin-bottom: 8px;
}
}
.top-dms-item {
display: flex;
width: 16.66%;
flex-direction: column;
padding: 12px;
margin-top: auto;
font-family: 'Open Sans';
&:nth-of-type(1) {
margin-left: 0;
}
&:nth-of-type(6) {
margin-right: 0;
}
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
@media screen and (max-width: 1279px) {
width: 100%;
flex-direction: row;
align-items: center;
margin: 0;
&:nth-of-type(1) {
margin-top: 0;
}
}
@media screen and (max-width: 1279px) and (min-width: 880px) {
overflow: hidden;
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
@media screen and (max-width: 768px) and (min-width: 576px) {
overflow: hidden;
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
.Avatar-xl {
width: 72px;
min-width: 72px;
height: 72px;
}
.dm-info {
display: flex;
flex-direction: column;
margin-top: 12px;
@media screen and (max-width: 1279px) {
overflow: hidden;
width: 90%;
margin-top: 0;
margin-left: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.dm-name {
overflow: hidden;
color: var(--center-channel-color);
font-size: 14px;
font-weight: 600;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.dm-role {
overflow: hidden;
min-height: 16px;
margin-top: 4px;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-size: 12px;
font-weight: 400;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.channel-message-count {
display: flex;
align-items: center;
margin-top: 4px;
.message-count {
color: rgba(var(--center-channel-color-rgb), 0.72);
font-size: 11px;
font-weight: 400;
line-height: 16px;
}
.horizontal-bar {
min-height: 8px;
margin-left: 8px;
background-color: var(--online-indicator);
border-radius: 6px;
}
.say-hello {
margin-left: 6px;
color: var(--button-bg);
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 10px;
&:hover {
text-decoration: none;
}
}
}
}
}
.new-members-item {
width: 16.66%;
@media screen and (max-width: 1279px) {
width: auto;
width: 100%;
flex-direction: row;
align-items: center;
margin: 0;
&:nth-of-type(1) {
margin-top: 0;
}
}
@media screen and (max-width: 1279px) and (min-width: 1020px) {
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
@media screen and (max-width: 1279px) and (min-width: 880px) {
overflow: hidden;
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
@media screen and (max-width: 768px) and (min-width: 576px) {
overflow: hidden;
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
&.new-members-info {
display: flex;
flex-direction: column;
align-self: flex-start;
padding: 12px;
@media screen and (max-width: 1279px) {
flex-direction: row;
}
span.total-count {
color: var(--center-channel-color);
font-family: 'Metropolis';
font-size: 52px;
font-style: normal;
font-weight: 400;
line-height: 48px;
@media screen and (max-width: 1279px) {
margin-top: 8px;
margin-left: 12px;
}
}
.members-info {
display: flex;
flex-direction: column;
@media screen and (max-width: 1279px) {
margin-top: 0;
margin-left: 24px;
}
.time-range-info {
margin-top: 2px;
color: rgba(var(--center-channel-color-rgb), 0.64);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
button.see-all-button {
display: flex;
align-items: center;
align-self: flex-start;
padding: 0;
border: none;
margin-top: 5px;
background: none;
color: var(--button-bg);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 10px;
.icon {
width: 14px;
height: 14px;
font-size: 14px;
}
}
}
}
}
}
.empty-state {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
margin: 65px 0;
.empty-state-emoticon {
display: flex;
padding: 10px;
margin-bottom: 8px;
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 50px;
i.icon {
width: 20px;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-size: 20px;
}
}
.empty-state-text {
max-width: 120px;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
text-align: center;
}
}
}
}

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

@@ -1,27 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import classNames from 'classnames';
import './activity_and_insights.scss';
import Insights from './insights/insights';
const ActivityAndInsights = () => {
/**
* Here we can do a check to see if both insights and activity are enabled. If that condition is true we can render the tabbed header.
* Otherwise we can render either insights or activity based on which flag is enabled.
*/
return (
<div
id='app-content'
className={classNames('ActivityAndInsights app__content')}
>
<Insights/>
</div>
);
};
export default memo(ActivityAndInsights);

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

@@ -1,69 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
#SidebarContainer .SidebarInsights {
.SidebarChannel {
height: 100%;
}
.SidebarLink {
width: 100%;
.icon {
padding: 3px;
margin-right: 10px;
margin-left: 1px;
i {
vertical-align: middle;
}
}
&:hover {
padding-right: 16px;
.badge {
position: initial;
visibility: visible;
}
}
}
}
// legacy
.sidebar--left .SidebarInsights {
padding-bottom: 0;
margin-top: 13px;
.icon {
width: auto;
padding-left: 1px;
margin-top: 1px;
}
.SidebarChannelLinkLabel_wrapper {
display: flex;
flex: 1;
}
.badge {
margin: 0 12px 0 0;
}
}
.tippy-box[data-placement^=right] {
&.tour-tip__box {
&.insights-tip {
.tour-tip__body {
img.insights-img {
margin-top: 2.4rem;
}
}
.tippy-arrow::before {
border-width: 0;
transform-origin: 6px 6px;
}
}
}
}

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

@@ -1,78 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {Link, useRouteMatch, useLocation, matchPath} from 'react-router-dom';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import classNames from 'classnames';
import {ChartLineIcon} from '@mattermost/compass-icons/components';
import {insightsAreEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
import {t} from 'utils/i18n';
import {RHSStates} from 'utils/constants';
import {trackEvent} from 'actions/telemetry_actions';
import {closeRightHandSide} from 'actions/views/rhs';
import InsightsTourTip from './insights_tour_tip/insights_tour_tip';
import './activity_and_insights_link.scss';
const ActivityAndInsightsLink = () => {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const insightsEnabled = useSelector(insightsAreEnabled);
const rhsOpen = useSelector(getIsRhsOpen);
const rhsState = useSelector(getRhsState);
const {url} = useRouteMatch();
const {pathname} = useLocation();
const inInsights = matchPath(pathname, {path: '/:team/activity-and-insights'}) != null;
const openInsights = useCallback((e) => {
e.stopPropagation();
trackEvent('insights', 'sidebar_open_insights');
if (rhsOpen && rhsState === RHSStates.EDIT_HISTORY) {
dispatch(closeRightHandSide());
}
}, [rhsOpen, rhsState]);
if (!insightsEnabled) {
return null;
}
return (
<ul className='SidebarInsights NavGroupContent nav nav-pills__container'>
<li
id={'sidebar-insights-button'}
className={classNames('SidebarChannel', {
active: inInsights,
})}
tabIndex={-1}
>
<Link
onClick={openInsights}
to={`${url}/activity-and-insights`}
id='sidebarItem_insights'
draggable='false'
className={'SidebarLink sidebar-item'}
tabIndex={0}
>
<span className='icon'>
<ChartLineIcon size={14}/>
</span>
<div className='SidebarChannelLinkLabel_wrapper'>
<span className='SidebarChannelLinkLabel sidebar-item__name'>
{formatMessage({id: t('activityAndInsights.sidebarLink'), defaultMessage: 'Insights'})}
</span>
</div>
</Link>
<InsightsTourTip/>
</li>
</ul>
);
};
export default ActivityAndInsightsLink;

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

@@ -1,142 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {useHistory} from 'react-router-dom';
import {setInsightsInitialisationState} from 'mattermost-redux/actions/preferences';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {GlobalState as EntitiesGlobalState} from '@mattermost/types/store';
import {Preferences} from 'mattermost-redux/constants';
import {showInsightsPulsatingDot} from 'selectors/insights';
import {GlobalState} from 'types/store';
import insightsPreview from 'images/Insights-Preview-Image.jpg';
import {TourTip, useMeasurePunchouts} from '@mattermost/components';
import {OnboardingTaskCategory, OnboardingTaskList} from 'components/onboarding_tasks';
const title = (
<FormattedMessage
id='activityAndInsights.tutorialTip.title'
defaultMessage='Introducing: Insights'
/>
);
const screen = (
<>
<FormattedMessage
id='activityAndInsights.tutorialTip.description'
defaultMessage='Check out the new Insights feature added to your workspace. See what content is most active, and learn how you and your teammates are using your workspace.'
/>
<img
src={insightsPreview}
className='insights-img'
/>
</>
);
const prevBtn = (
<FormattedMessage
id='activityAndInsights.tutorial_tip.notNow'
defaultMessage='Not now'
/>
);
const nextBtn = (
<FormattedMessage
id='activityAndInsights.tutorial_tip.viewInsights'
defaultMessage='View insights'
/>
);
const InsightsTourTip = () => {
const dispatch = useDispatch();
const history = useHistory();
const showTip = useSelector(showInsightsPulsatingDot);
const showTaskList = useSelector((state: EntitiesGlobalState) => getBool(state, OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW));
const teamUrl = useSelector((state: GlobalState) => getCurrentRelativeTeamUrl(state));
const nextUrl = `${teamUrl}/activity-and-insights`;
const [tipOpened, setTipOpened] = useState(showTip);
const handleDismiss = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
dispatch(setInsightsInitialisationState({[Preferences.INSIGHTS_VIEWED]: true}));
setTipOpened(false);
}, []);
const handleNext = useCallback(() => {
dispatch(setInsightsInitialisationState({[Preferences.INSIGHTS_VIEWED]: true}));
setTipOpened(false);
history.push(nextUrl);
}, []);
const handleOpen = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
if (tipOpened) {
dispatch(setInsightsInitialisationState({[Preferences.INSIGHTS_VIEWED]: true}));
setTipOpened(false);
} else {
setTipOpened(true);
}
}, []);
const isOnboardingOngoing = useCallback(() => {
if (showTaskList) {
return true;
}
return false;
}, [showTaskList]);
const overlayPunchOut = useMeasurePunchouts(['sidebar-insights-button'], []);
useEffect(() => {
// If the user has ongoing onboarding steps we want to just remove the insights intro modal in order to not overburden with tips
if (showTip && isOnboardingOngoing()) {
dispatch(setInsightsInitialisationState({[Preferences.INSIGHTS_VIEWED]: true}));
}
}, [showTip, isOnboardingOngoing]);
return (
<>
{
(showTip && !isOnboardingOngoing()) &&
<TourTip
show={tipOpened}
screen={screen}
title={title}
overlayPunchOut={overlayPunchOut}
placement='right-start'
pulsatingDotPlacement='right'
step={1}
singleTip={true}
showOptOut={false}
interactivePunchOut={true}
handleDismiss={handleDismiss}
handleNext={handleNext}
handleOpen={handleOpen}
handlePrevious={handleDismiss}
nextBtn={nextBtn}
prevBtn={prevBtn}
offset={[-30, 5]}
className={'insights-tip'}
/>
}
</>
);
};
export default memo(InsightsTourTip);

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

@@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights should match snapshot 1`] = `
<ContextProvider
value={
Object {
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
},
}
}
>
<Memo(Insights) />
</ContextProvider>
`;

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

@@ -1,79 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights/insights_title should match snapshot with data 1`] = `
<Card
className="insights-card expanded top-channels-card small"
>
<CardHeader
onClick={[MockFunction]}
>
<div
className="title-and-subtitle"
>
<div
className="text-top"
>
<h2>
Top channels
</h2>
</div>
<div
className="text-bottom"
>
Top channels subtitle
</div>
</div>
<button
aria-haspopup="dialog"
aria-label="Open modal for Top channels"
className="icon"
onClick={[Function]}
>
<i
className="icon icon-chevron-right"
/>
</button>
</CardHeader>
<div
className="Card__body expanded"
>
test data
</div>
</Card>
`;
exports[`components/activity_and_insights/insights/insights_title should match snapshot with no data 1`] = `
<Card
className="insights-card expanded small"
>
<CardHeader
onClick={[MockFunction]}
>
<div
className="title-and-subtitle"
>
<div
className="text-top"
>
<h2 />
</div>
<div
className="text-bottom"
/>
</div>
<button
aria-haspopup="dialog"
aria-label="Open modal for "
className="icon"
onClick={[Function]}
>
<i
className="icon icon-chevron-right"
/>
</button>
</CardHeader>
<div
className="Card__body expanded"
/>
</Card>
`;

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

@@ -1,129 +0,0 @@
.Card.insights-card {
border: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
margin: 8px;
border-radius: 8px;
box-shadow: none;
.Card__header {
position: relative;
display: flex;
align-items: center;
padding: 24px;
&:hover {
cursor: pointer;
}
.title-and-subtitle {
display: flex;
flex-direction: column;
.text-top {
display: block;
h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
}
.text-bottom {
display: block;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
}
button.icon {
position: absolute;
top: 20px;
right: 20px;
padding: 8px;
border: none;
background: none;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.56);
}
i.icon-chevron-right {
width: 16px;
height: 16px;
font-size: 16px;
}
}
.Card__hr {
display: none;
}
}
.Card__body {
padding: 0 24px 24px;
&.expanding {
transition: none;
}
&.expanded {
overflow: visible;
margin: 0;
}
}
&:hover {
border-color: rgba(var(--center-channel-color-rgb), 0.16);
box-shadow: 0 6px 14px rgb(0 0 0 / 12%);
}
&.large {
width: auto;
clear: both;
.Card__header {
padding-bottom: 16px;
}
@media screen and (min-width: 1680px) {
width: 1264px;
align-self: center;
}
@media screen and (max-width: 1019px) {
width: calc(100% - 16px);
}
}
&.medium {
width: calc((100%/2) - 16px);
float: left;
}
&.small {
width: calc((100%/3) - 16px);
min-height: 310px;
float: left;
}
@media (max-width: 768px) {
margin: 8px 0;
&:nth-of-type(4) {
margin: 8px 0 0;
}
&.large,
&.medium,
&.small {
width: 100%;
min-height: auto;
}
}
}

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

@@ -1,44 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {CardSizes} from '@mattermost/types/insights';
import InsightsCard from './card';
describe('components/activity_and_insights/insights/insights_title', () => {
const props = {
class: '',
children: <></>,
title: '',
subTitle: '',
size: CardSizes.small,
onClick: jest.fn(),
};
test('should match snapshot with no data', () => {
const wrapper = shallow(
<InsightsCard
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with data', () => {
const wrapper = shallow(
<InsightsCard
{...props}
class='top-channels-card'
title='Top channels'
subTitle='Top channels subtitle'
>
{'test data'}
</InsightsCard>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -1,77 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {useIntl} from 'react-intl';
import classNames from 'classnames';
import {CardSize, CardSizes} from '@mattermost/types/insights';
import Card from 'components/card/card';
import CardHeader from 'components/card/card_header';
import './card.scss';
type Props = {
class: string;
children: React.ReactNode;
title: string;
subTitle?: string;
size: CardSize;
onClick: () => void;
}
const InsightsCard = (props: Props) => {
const {formatMessage} = useIntl();
return (
<Card
className={classNames('insights-card expanded', props.class, {
large: props.size === CardSizes.large,
medium: props.size === CardSizes.medium,
small: props.size === CardSizes.small,
})}
>
<CardHeader
onClick={props.onClick}
>
<div className='title-and-subtitle'>
<div className='text-top'>
<h2>
{props.title}
</h2>
</div>
<div className='text-bottom'>
{props.subTitle}
</div>
</div>
<button
className='icon'
onClick={(e) => {
e.stopPropagation();
props.onClick();
}}
aria-label={formatMessage(
{
id: 'insights.card.ariaLabel',
defaultMessage: 'Open modal for {title}',
},
{
title: props.title,
},
)}
aria-haspopup='dialog'
>
<i className='icon icon-chevron-right'/>
</button>
</CardHeader>
<div
className='Card__body expanded'
>
{props.children}
</div>
</Card>
);
};
export default memo(InsightsCard);

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

@@ -1,58 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useSelector} from 'react-redux';
import {getCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {isCloudLicense} from 'utils/license_utils';
import {CloudProducts, InsightsScopes} from 'utils/constants';
import {setGlobalItem} from 'actions/storage';
import {useGlobalState} from 'stores/hooks';
/**
* Returns some checks for free trial users or starter licenses
*/
export function useLicenseChecks(): {isStarterFree: boolean; isFreeTrial: boolean; isEnterpriseReady: boolean} {
const subscription = useSelector(getCloudSubscription);
const license = useSelector(getLicense);
const subscriptionProduct = useSelector(getSubscriptionProduct);
const config = useSelector(getConfig);
const isCloud = isCloudLicense(license);
const isCloudStarterFree = isCloud && subscriptionProduct?.sku === CloudProducts.STARTER;
const isCloudFreeTrial = isCloud && subscription?.is_free_trial === 'true';
const isEnterpriseReady = config.BuildEnterpriseReady === 'true';
const isSelfHostedStarter = isEnterpriseReady && (license.IsLicensed === 'false');
const isSelfHostedFreeTrial = license.IsTrial === 'true';
const isStarterFree = isCloudStarterFree || isSelfHostedStarter;
const isFreeTrial = isCloudFreeTrial || isSelfHostedFreeTrial;
return {
isStarterFree,
isFreeTrial,
isEnterpriseReady,
};
}
export function useGetFilterType(): [string, (value: string) => ReturnType<typeof setGlobalItem>] {
const {isStarterFree, isEnterpriseReady} = useLicenseChecks();
const [filterType, setFilterType] = useGlobalState(InsightsScopes.TEAM, 'insightsScope');
let returnedFilterType = filterType;
// Force MY Insights for free trial users
if (isStarterFree || !isEnterpriseReady) {
returnedFilterType = InsightsScopes.MY;
}
return [
returnedFilterType,
setFilterType,
];
}

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

@@ -1,23 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import * as redux from 'react-redux';
import mockStore from 'tests/test_store';
import Insights from './insights';
describe('components/activity_and_insights/insights', () => {
const store = mockStore({});
test('should match snapshot', () => {
const wrapper = shallow(
<redux.Provider store={store}>
<Insights/>
</redux.Provider>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -1,165 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useEffect, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {trackEvent} from 'actions/telemetry_actions';
import {suppressRHS, unsuppressRHS} from 'actions/views/rhs';
import LocalStorageStore from 'stores/local_storage_store';
import {useGlobalState} from 'stores/hooks';
import {InsightsScopes, PreviousViewedTypes, suitePluginIds} from 'utils/constants';
import {useProducts} from 'utils/products';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {selectLhsItem} from 'actions/views/lhs';
import {LhsItemType, LhsPage} from 'types/store/lhs';
import {CardSizes, InsightsWidgetTypes, TimeFrame, TimeFrames} from '@mattermost/types/insights';
import InsightsHeader from './insights_header/insights_header';
import TopChannels from './top_channels/top_channels';
import TopReactions from './top_reactions/top_reactions';
import TopThreads from './top_threads/top_threads';
import TopBoards from './top_boards/top_boards';
import LeastActiveChannels from './least_active_channels/least_active_channels';
import TopPlaybooks from './top_playbooks/top_playbooks';
import TopDMsAndNewMembers from './top_dms_and_new_members/top_dms_and_new_members';
import {useGetFilterType} from './hooks';
import './../activity_and_insights.scss';
type SelectOption = {
value: string;
label: string;
}
const Insights = () => {
const dispatch = useDispatch();
const products = useProducts();
const focalboardEnabled = false;
let playbooksEnabled = false;
if (products) {
products.forEach((product) => {
if (product.pluginId === suitePluginIds.playbooks) {
playbooksEnabled = true;
}
});
}
const currentUserId = useSelector(getCurrentUserId);
const currentTeamId = useSelector(getCurrentTeamId);
const [filterType, setFilterType] = useGetFilterType();
const [timeFrame, setTimeFrame] = useGlobalState(TimeFrames.INSIGHTS_7_DAYS as string, 'insightsTimeFrame');
const setFilterTypeTeam = useCallback(() => {
trackEvent('insights', 'change_scope_to_team_insights');
setFilterType(InsightsScopes.TEAM);
}, []);
const setFilterTypeMy = useCallback(() => {
trackEvent('insights', 'change_scope_to_my_insights');
setFilterType(InsightsScopes.MY);
}, []);
const setTimeFrameValue = useCallback((value: SelectOption) => {
setTimeFrame(value.value);
}, []);
useEffect(() => {
dispatch(selectLhsItem(LhsItemType.Page, LhsPage.Insights));
dispatch(suppressRHS);
const penultimateType = LocalStorageStore.getPreviousViewedType(currentUserId, currentTeamId);
if (penultimateType !== PreviousViewedTypes.INSIGHTS) {
LocalStorageStore.setPenultimateViewedType(currentUserId, currentTeamId, penultimateType);
LocalStorageStore.setPreviousViewedType(currentUserId, currentTeamId, PreviousViewedTypes.INSIGHTS);
}
return () => {
dispatch(unsuppressRHS);
};
}, []);
return (
<>
<InsightsHeader
filterType={filterType}
setFilterTypeTeam={setFilterTypeTeam}
setFilterTypeMy={setFilterTypeMy}
timeFrame={timeFrame}
setTimeFrame={setTimeFrameValue}
/>
<div className='insights-body'>
<TopChannels
size={CardSizes.large}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_CHANNELS}
class={'top-channels-card'}
timeFrame={timeFrame as TimeFrame}
/>
<div className='card-row'>
<TopThreads
size={focalboardEnabled ? CardSizes.small : CardSizes.medium}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_THREADS}
class={'top-threads-card'}
timeFrame={timeFrame as TimeFrame}
/>
{
focalboardEnabled &&
<TopBoards
size={CardSizes.small}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_BOARDS}
class={'top-boards-card'}
timeFrame={timeFrame as TimeFrame}
/>
}
<TopReactions
size={focalboardEnabled ? CardSizes.small : CardSizes.medium}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_REACTIONS}
class={'top-reactions-card'}
timeFrame={timeFrame as TimeFrame}
/>
</div>
<TopDMsAndNewMembers
size={CardSizes.large}
filterType={filterType}
widgetType={filterType === InsightsScopes.MY ? InsightsWidgetTypes.TOP_DMS : InsightsWidgetTypes.NEW_TEAM_MEMBERS}
class={'top-dms-card'}
timeFrame={timeFrame as TimeFrame}
/>
<div className='card-row'>
<LeastActiveChannels
size={CardSizes.medium}
filterType={filterType}
widgetType={InsightsWidgetTypes.LEAST_ACTIVE_CHANNELS}
class={'least-active-channels-card'}
timeFrame={timeFrame as TimeFrame}
/>
{
playbooksEnabled &&
<TopPlaybooks
size={CardSizes.medium}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_PLAYBOOKS}
class={'top-playbooks-card'}
timeFrame={timeFrame as TimeFrame}
/>
}
</div>
</div>
</>
);
};
export default memo(Insights);

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

@@ -1,81 +0,0 @@
.Insights___header {
&.Header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 12px;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
background-color: var(--center-channel-bg);
grid-area: header;
.insights-title {
display: flex;
align-items: center;
padding: 2px 6px 2px 8px;
border: none;
margin: 0;
background-color: transparent;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 1);
font-family: 'Metropolis', sans-serif;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
button.insights-title {
i.icon {
width: 16px;
height: 16px;
&::before {
margin: 0;
}
}
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
cursor: pointer;
}
}
.MenuWrapper--open {
button.insights-title {
background-color: rgba(var(--button-bg-rgb), 0.08);
}
}
.MenuItem {
i.icon {
width: 16px;
height: 16px;
color: rgba(var(--center-channel-color-rgb), 0.56);
&::before {
margin: 0;
}
}
.RestrictedIndicator__icon-tooltip-container {
align-self: center;
padding: 7px 10px;
}
}
.react-select {
.react-select__control {
height: 32px;
min-height: 32px;
}
.react-select__value-container {
padding-left: 12px;
}
.react-select__indicators {
padding-right: 10px;
color: rgba(var(--center-channel-color-rgb), 0.64);
}
}
}
}

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

@@ -1,48 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import classNames from 'classnames';
import InsightsTitle from '../insights_title/insights_title';
import TimeFrameDropdown from '../time_frame_dropdown/time_frame_dropdown';
import './insights_header.scss';
type SelectOption = {
value: string;
label: string;
}
type Props = {
filterType: string;
setFilterTypeTeam: () => void;
setFilterTypeMy: () => void;
timeFrame: string;
setTimeFrame: (value: SelectOption) => void;
}
const InsightsHeader = (props: Props) => {
return (
<header
className={classNames('Header Insights___header')}
>
<div className='left'>
<InsightsTitle
filterType={props.filterType}
setFilterTypeTeam={props.setFilterTypeTeam}
setFilterTypeMy={props.setFilterTypeMy}
/>
</div>
<div className='right'>
<TimeFrameDropdown
timeFrame={props.timeFrame}
setTimeFrame={props.setTimeFrame}
/>
</div>
</header>
);
};
export default memo(InsightsHeader);

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

@@ -1,139 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights/insights_modal should match snapshot with My insights 1`] = `
<Modal
animation={true}
aria-labelledby="insightsModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal insights-modal"
dialogComponentClass={[Function]}
enforceFocus={true}
id="insightsModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<div
className="title-section"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="insightsModalTitle"
>
My top reactions
</ModalTitle>
<div
className="subtitle"
>
Reactions I've used the most
</div>
</div>
<Memo(TimeFrameDropdown)
setTimeFrame={[Function]}
timeFrame="today"
/>
</ModalHeader>
<ModalBody
bsClass="modal-body"
className="overflow--visible"
componentClass="div"
>
<Memo(TopReactionsTable)
filterType="MY"
timeFrame="today"
/>
</ModalBody>
</Modal>
`;
exports[`components/activity_and_insights/insights/insights_modal should match snapshot with team 1`] = `
<Modal
animation={true}
aria-labelledby="insightsModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal insights-modal"
dialogComponentClass={[Function]}
enforceFocus={true}
id="insightsModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<div
className="title-section"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="insightsModalTitle"
>
Top reactions
</ModalTitle>
<div
className="subtitle"
>
The team's most-used reactions
</div>
</div>
<Memo(TimeFrameDropdown)
setTimeFrame={[Function]}
timeFrame="7_day"
/>
</ModalHeader>
<ModalBody
bsClass="modal-body"
className="overflow--visible"
componentClass="div"
>
<Memo(TopReactionsTable)
filterType="TEAM"
timeFrame="7_day"
/>
</ModalBody>
</Modal>
`;

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

@@ -1,428 +0,0 @@
.insights-modal {
&.modal-dialog {
margin-top: calc(50vh - 340px);
}
&.join-channel-modal {
max-width: 600px;
}
.modal-content {
.modal-header {
align-items: flex-start;
padding: 22px 32px 26px;
.title-section {
display: flex;
flex: 1;
flex-direction: column;
.subtitle {
margin-top: 6px;
margin-bottom: 0;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.react-select {
margin-right: 40px;
}
.close {
top: 16px;
}
}
.modal-body {
padding: 4px 32px 28px;
.InsightsTable {
padding: 0;
background: var(--center-channel-bg);
&.TopPlaybooksTable,
&.TopBoardsTable,
&.TopThreadsTable {
.DataGrid_row {
&:hover {
cursor: pointer;
}
}
}
&.TopDMsTable {
.DataGrid_header {
.DataGrid_cell {
&:nth-of-type(3),
&:nth-of-type(4) {
text-align: right;
}
}
}
}
&.LeastActiveChannelsTable {
.DataGrid_header {
.DataGrid_cell {
&:nth-of-type(4) {
text-align: right;
}
}
}
}
.DataGrid_header {
padding: 12px 0;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
background-color: rgba(var(--center-channel-color-rgb), 0.04);
border-radius: 4px 4px 0 0;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
&:last-child {
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
}
.DataGrid_cell {
padding: 0 16px;
}
}
.DataGrid_rows {
background: transparent;
.DataGrid_loading {
padding: 234px 0;
}
.DataGrid_row {
display: flex;
flex-direction: row;
padding: 0;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
border-left: none;
background-color: var(--center-channel-bg);
color: var(--center-channel-color);
&:hover {
border-left: none;
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: var(--center-channel-color);
}
&:nth-of-type(2) {
span.horizontal-bar {
opacity: 0.9;
}
}
&:nth-of-type(3) {
span.horizontal-bar {
opacity: 0.8;
}
}
&:nth-of-type(4) {
span.horizontal-bar {
opacity: 0.7;
}
}
&:nth-of-type(5) {
span.horizontal-bar {
opacity: 0.6;
}
}
&:nth-of-type(6) {
span.horizontal-bar {
opacity: 0.5;
}
}
&:nth-of-type(7) {
span.horizontal-bar {
opacity: 0.5;
}
}
&:nth-of-type(8) {
span.horizontal-bar {
opacity: 0.4;
}
}
&:nth-of-type(9) {
span.horizontal-bar {
opacity: 0.3;
}
}
&:nth-of-type(10) {
span.horizontal-bar {
opacity: 0.3;
}
}
.DataGrid_cell {
display: flex;
align-items: center;
padding: 0 16px;
margin: 0;
white-space: normal;
&.message-count {
justify-content: end;
}
&.rankCell {
flex: 0.2 0;
}
.emoticon {
margin: 12px 12px 12px 0;
}
span.replies,
span.cell-text {
display: inline-block;
padding: 14px 0;
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.channel-display-name {
display: flex;
overflow: hidden;
color: var(--center-channel-color);
&:hover {
text-decoration: none;
}
.icon {
color: rgba(var(--center-channel-color-rgb), 0.56);
}
span.cell-text {
overflow: hidden;
margin-left: 4px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.times-used-container {
display: flex;
width: 100%;
align-items: center;
span.cell-text {
flex: 0.1 0;
text-align: right;
}
span.horizontal-bar {
height: 8px;
margin-left: 16px;
background: var(--button-bg);
border-radius: 6px;
&.top-dms {
background: var(--online-indicator);
opacity: 1;
}
}
}
.thread-item {
width: 100%;
padding: 12px 0;
.preview {
height: auto;
}
}
.board-item {
display: flex;
align-items: center;
span.board-icon {
display: flex;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
font-size: 24px;
}
span.board-title {
overflow: hidden;
max-width: 409px;
margin-left: 12px;
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.replies,
span.board-updates,
.Avatars___xs {
margin-left: auto;
}
&.last-activity {
justify-content: flex-end;
margin-right: 8px;
.timestamp {
overflow: hidden;
color: rgba(var(--center-channel-color-rgb), 0.72);
text-overflow: ellipsis;
time {
white-space: nowrap;
}
}
}
.role {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.actions {
overflow: visible;
}
.user-info {
display: flex;
overflow: hidden;
align-items: center;
&:hover {
text-decoration: none;
}
.display-name {
overflow: hidden;
margin-left: 12px;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.button-footer {
padding-top: 24px;
text-align: right;
button {
height: 40px;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
&.join-channel-cancel {
padding: 13px 20px;
margin-right: 8px;
background: rgba(var(--button-bg-rgb), 0.08);
color: rgb(var(--button-bg-rgb));
line-height: 14px;
}
&.save-button {
padding: 0 20px;
}
}
}
.pagination-buttons {
display: flex;
align-items: center;
justify-content: end;
.pagination {
margin-right: 4px;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.pagination-button {
padding: 8px;
border: none;
background: none;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.56);
}
&:disabled {
&:hover {
background-color: transparent;
color: rgba(var(--center-channel-color-rgb), 0.56);
}
}
&:nth-of-type(1) {
margin-right: 4px;
}
i {
width: 16px;
height: 16px;
font-size: 16px;
}
}
}
}
}
}
@media (min-width: 768px) {
.insights-modal {
width: 800px;
}
}

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

@@ -1,44 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import InsightsModal from './insights_modal';
describe('components/activity_and_insights/insights/insights_modal', () => {
const props = {
filterType: 'TEAM',
onExited: jest.fn(),
widgetType: InsightsWidgetTypes.TOP_REACTIONS,
title: 'Top reactions',
subtitle: 'The team\'s most-used reactions',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
setShowModal: jest.fn(),
};
test('should match snapshot with team', () => {
const wrapper = shallow(
<InsightsModal
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with My insights', () => {
const wrapper = shallow(
<InsightsModal
{...props}
filterType={'MY'}
title={'My top reactions'}
subtitle={'Reactions I\'ve used the most'}
timeFrame={TimeFrames.INSIGHTS_1_DAY}
/>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -1,155 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useCallback} from 'react';
import {Modal} from 'react-bootstrap';
import {InsightsWidgetTypes, TimeFrame} from '@mattermost/types/insights';
import TimeFrameDropdown from '../time_frame_dropdown/time_frame_dropdown';
import TopReactionsTable from '../top_reactions/top_reactions_table/top_reactions_table';
import TopChannelsTable from '../top_channels/top_channels_table/top_channels_table';
import TopThreadsTable from '../top_threads/top_threads_table/top_threads_table';
import TopBoardsTable from '../top_boards/top_boards_table/top_boards_table';
import LeastActiveChannelsTable from '../least_active_channels/least_active_channels_table/least_active_channels_table';
import TopPlaybooksTable from '../top_playbooks/top_playbooks_table/top_playbooks_table';
import TopDMsTable from '../top_dms_and_new_members/top_dms_table/top_dms_table';
import NewMembersTable from '../top_dms_and_new_members/new_members_table/new_members_table';
import './../../activity_and_insights.scss';
import './insights_modal.scss';
type Props = {
onExited: () => void;
widgetType: InsightsWidgetTypes;
title: string;
subtitle: string;
filterType: string;
timeFrame: TimeFrame;
setShowModal?: (show: boolean) => void;
}
const InsightsModal = (props: Props) => {
const [show, setShow] = useState(true);
const [timeFrame, setTimeFrame] = useState(props.timeFrame);
const [offset, setOffset] = useState(0);
const setTimeFrameValue = useCallback((value) => {
setTimeFrame(value.value);
setOffset(0);
}, []);
const doHide = useCallback(() => {
props.setShowModal?.(false);
setShow(false);
}, []);
const modalContent = useCallback(() => {
switch (props.widgetType) {
case InsightsWidgetTypes.TOP_CHANNELS:
return (
<TopChannelsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.TOP_REACTIONS:
return (
<TopReactionsTable
filterType={props.filterType}
timeFrame={timeFrame}
/>
);
case InsightsWidgetTypes.TOP_THREADS:
return (
<TopThreadsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.TOP_BOARDS:
return (
<TopBoardsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.LEAST_ACTIVE_CHANNELS:
return (
<LeastActiveChannelsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.TOP_PLAYBOOKS:
return (
<TopPlaybooksTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.TOP_DMS:
return (
<TopDMsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.NEW_TEAM_MEMBERS:
return (
<NewMembersTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
offset={offset}
setOffset={setOffset}
/>
);
default:
return null;
}
}, [props.widgetType, timeFrame, offset]);
return (
<Modal
dialogClassName='a11y__modal insights-modal'
show={show}
onHide={doHide}
onExited={props.onExited}
aria-labelledby='insightsModalLabel'
id='insightsModal'
>
<Modal.Header closeButton={true}>
<div className='title-section'>
<Modal.Title
componentClass='h1'
id='insightsModalTitle'
>
{props.title}
</Modal.Title>
<div className='subtitle'>
{props.subtitle}
</div>
</div>
<TimeFrameDropdown
timeFrame={timeFrame}
setTimeFrame={setTimeFrameValue}
/>
</Modal.Header>
<Modal.Body
className='overflow--visible'
>
{modalContent()}
</Modal.Body>
</Modal>
);
};
export default memo(InsightsModal);

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

@@ -1,59 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithFullContext, screen} from 'tests/react_testing_utils';
import InsightsTitle from './insights_title';
describe('components/activity_and_insights/insights/insights_title', () => {
const props = {
filterType: 'TEAM',
setFilterTypeMy: jest.fn(),
setFilterTypeTeam: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {},
},
},
},
};
test('should match snapshot with My insights', () => {
renderWithFullContext(
<InsightsTitle
{...props}
filterType={'MY'}
/>,
initialState,
);
expect(screen.getByText('My insights')).toBeInTheDocument();
});
test('should match snapshot with Team insights', () => {
renderWithFullContext(
<InsightsTitle
{...props}
/>,
initialState,
);
expect(screen.getByText('Team insights')).toBeInTheDocument();
});
});

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

@@ -1,241 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import Menu from 'components/widgets/menu/menu';
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
import * as Utils from 'utils/utils';
import {InsightsScopes, LicenseSkus, MattermostFeatures} from 'utils/constants';
import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
import {useLicenseChecks} from '../hooks';
import ExternalLink from 'components/external_link';
type Props = {
filterType: string;
setFilterTypeTeam: () => void;
setFilterTypeMy: () => void;
}
const InsightsTitle = (props: Props) => {
const {formatMessage} = useIntl();
const {isStarterFree, isFreeTrial, isEnterpriseReady} = useLicenseChecks();
const title = useCallback(() => {
if (props.filterType === InsightsScopes.TEAM) {
return (
formatMessage({
id: 'insights.teamHeading',
defaultMessage: 'Team insights',
})
);
}
return (
formatMessage({
id: 'insights.myHeading',
defaultMessage: 'My insights',
})
);
}, [props.filterType]);
const openInsightsDoc = useCallback(() => {
trackEvent('insights', 'open_insights_doc');
}, []);
const openTeamInsightsDoc = useCallback(() => {
trackEvent('insights', 'open_team_insights_doc');
}, []);
const openContactSales = useCallback(() => {
trackEvent('insights', 'open_contact_sales_from_insights');
}, []);
if (!isEnterpriseReady) {
return (
<div className='insights-title'>
{title()}
</div>
);
}
return (
<MenuWrapper id='insightsFilterDropdown'>
<button className='insights-title'>
{title()}
<span className='icon'>
<i className='icon icon-chevron-down'/>
</span>
</button>
<Menu
ariaLabel={Utils.localizeMessage('insights.filter.ariaLabel', 'Insights Filter Menu')}
>
<Menu.ItemAction
id='insightsDropdownMy'
buttonClass='insights-filter-btn'
onClick={props.setFilterTypeMy}
icon={
<span className='icon'>
<i className='icon icon-account-outline'/>
</span>
}
text={Utils.localizeMessage('insights.filter.myInsights', 'My insights')}
/>
<Menu.ItemAction
id='insightsDropdownTeam'
buttonClass={'insights-filter-btn'}
onClick={props.setFilterTypeTeam}
icon={
<span className='icon'>
<i className='icon icon-account-multiple-outline'/>
</span>
}
text={Utils.localizeMessage('insights.filter.teamInsights', 'Team insights')}
disabled={isStarterFree}
sibling={(isStarterFree || isFreeTrial) && (
<RestrictedIndicator
blocked={isStarterFree}
feature={MattermostFeatures.TEAM_INSIGHTS}
minimumPlanRequiredForFeature={LicenseSkus.Professional}
tooltipMessage={formatMessage({
id: 'insights.accessModal.cloudFreeTrial',
defaultMessage: 'During your trial you are able to view Team Insights.',
})}
titleAdminPreTrial={formatMessage({
id: 'insights.accessModal.titleAdminPreTrial',
defaultMessage: 'Try team insights with a free trial',
})}
messageAdminPreTrial={formatMessage({
id: 'insights.accessModal.messageAdminPreTrial',
defaultMessage: 'Use {teamInsights} with one of our paid plans. Get the full experience of Enterprise when you start a free, {trialLength} day trial.',
},
{
trialLength: FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS,
teamInsights: (
<ExternalLink
location='insights_title'
onClick={openTeamInsightsDoc}
href={'https://docs.mattermost.com/welcome/insights.html#team-insights'}
>
<FormattedMessage
id='insights.accessModal.teamDocsLink'
defaultMessage='Team Insights'
/>
</ExternalLink>
),
},
)}
titleAdminPostTrial={formatMessage({
id: 'insights.accessModal.titleAdminPostTrial',
defaultMessage: 'Upgrade to access team insights',
})}
messageAdminPostTrial={
formatMessage(
{
id: 'insights.accessModal.messageAdminPostTrial',
defaultMessage: 'To access your complete {insightsDoc} dashboard, including {teamInsights}, please upgrade your plan to Professional or Enterprise. For questions on upgrading your plan, please {contactSales} for support.',
},
{
insightsDoc: (
<ExternalLink
onClick={openInsightsDoc}
location='insights_title'
href={'https://docs.mattermost.com/welcome/insights.html'}
>
<FormattedMessage
id='insights.accessModal.docsLink'
defaultMessage='Insights'
/>
</ExternalLink>
),
teamInsights: (
<ExternalLink
onClick={openTeamInsightsDoc}
location='insights_title'
href={'https://docs.mattermost.com/welcome/insights.html#team-insights'}
>
<FormattedMessage
id='insights.accessModal.teamDocsLink'
defaultMessage='Team Insights'
/>
</ExternalLink>
),
contactSales: (
<ExternalLink
onClick={openContactSales}
href={'https://mattermost.com/contact-sales/'}
location='insights_title'
>
<FormattedMessage
id='insights.accessModal.contactSales'
defaultMessage='contact our Sales team'
/>
</ExternalLink>
),
},
)}
titleEndUser={formatMessage({
id: 'insights.accessModal.titleEndUser',
defaultMessage: 'Team insights are available in paid plans',
})}
messageEndUser={
formatMessage(
{
id: 'insights.accessModal.messageEndUser',
defaultMessage: 'To access your complete {insightsDoc} dashboard, including {teamInsights}, please notify your Admin to upgrade your plan to Professional or Enterprise. For questions on upgrading your plan, please {contactSales} for support.',
},
{
insightsDoc: (
<ExternalLink
onClick={openInsightsDoc}
href={'https://docs.mattermost.com/welcome/insights.html'}
location='insights_title'
>
<FormattedMessage
id='insights.accessModal.docsLink'
defaultMessage='Insights'
/>
</ExternalLink>
),
teamInsights: (
<ExternalLink
onClick={openTeamInsightsDoc}
href={'https://docs.mattermost.com/welcome/insights.html#team-insights'}
location='insights_title'
>
<FormattedMessage
id='insights.accessModal.teamDocsLink'
defaultMessage='Team Insights'
/>
</ExternalLink>
),
contactSales: (
<ExternalLink
onClick={openContactSales}
href={'https://mattermost.com/contact-sales/'}
location='insights_title'
>
<FormattedMessage
id='insights.accessModal.contactSales'
defaultMessage='contact our Sales team'
/>
</ExternalLink>
),
},
)
}
/>
)}
/>
</Menu>
</MenuWrapper>
);
};
export default memo(InsightsTitle);

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

@@ -1,140 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {act} from 'tests/react_testing_utils';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {PostType} from '@mattermost/types/posts';
import JoinChannelModal from './join_channel_modal';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/join_channel_modal', () => {
const props = {
thread: {
channel_id: 'channel1',
channel_display_name: 'nostrum',
channel_name: 'channel1',
participants: [
'user1',
],
user_information: {
id: 'user1',
last_picture_update: 0,
first_name: 'Kathryn',
last_name: 'Mills',
},
post: {
id: 'post1',
create_at: 1653488972484,
update_at: 1653489070820,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'user1',
channel_id: 'channel1',
root_id: '',
original_id: '',
message: 'ducimus sed aut sunt corrupti necessitatibus quasi.\nreiciendis ipsa consequuntur fugiat a eaque.',
type: '' as PostType,
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 18,
last_reply_at: 0,
participants: null,
metadata: {
embeds: [],
emojis: [],
files: [],
images: {},
reactions: [],
},
},
},
onExited: jest.fn(),
currentTeamId: 'team_id1',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('should match snapshot', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<JoinChannelModal
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.text().includes('You\'ll need to join the nostrum channel to see this thread. Do you want to join nostrum now?')).toBe(true);
});
});

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

@@ -1,111 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
import {joinChannel} from 'mattermost-redux/actions/channels';
import {TopThread} from '@mattermost/types/insights';
import {ActionResult} from 'mattermost-redux/types/actions';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {selectPost} from 'actions/views/rhs';
import {localizeMessage} from 'utils/utils';
import SaveButton from 'components/save_button';
import './../../activity_and_insights.scss';
type Props = {
onExited: () => void;
thread: TopThread;
currentTeamId: string;
}
const JoinChannelModal = (props: Props) => {
const dispatch = useDispatch();
const [show, setShow] = useState(true);
const [saving, setSaving] = useState(false);
const currentUserId = useSelector(getCurrentUserId);
const doHide = useCallback(() => {
setShow(false);
}, []);
const openRHS = useCallback(async () => {
setSaving(true);
const {error} = await dispatch(joinChannel(currentUserId, props.currentTeamId, props.thread.channel_id, props.thread.channel_name)) as ActionResult;
if (!error) {
await dispatch(selectPost(props.thread.post));
}
doHide();
}, []);
return (
<Modal
dialogClassName='a11y__modal insights-modal join-channel-modal'
show={show}
onHide={doHide}
onExited={props.onExited}
aria-labelledby='insightsModalLabel'
id='insightsModal'
>
<Modal.Header closeButton={true}>
<div className='title-section'>
<Modal.Title
componentClass='h1'
id='insightsModalTitle'
>
<FormattedMessage
id='joinChannel.title'
defaultMessage='Join channel?'
/>
</Modal.Title>
</div>
</Modal.Header>
<Modal.Body
className='overflow--visible'
>
<FormattedMessage
id='joinChannel.desciption'
defaultMessage={'You\'ll need to join the {channel} channel to see this thread. Do you want to join {channel} now?'}
values={{
channel: <strong>{props.thread.channel_display_name}</strong>,
}}
/>
<div className='button-footer'>
<button
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
props.onExited();
}}
className='btn join-channel-cancel'
>
{localizeMessage('joinChannel.cancelButton', 'Cancel')}
</button>
<SaveButton
id='saveItems'
saving={saving}
onClick={(e) => {
e.preventDefault();
openRHS();
}}
defaultMessage={localizeMessage('joinChannel.JoinButton', 'Join')}
savingMessage={localizeMessage('joinChannel.joiningButton', 'Joining...')}
/>
</div>
</Modal.Body>
</Modal>
);
};
export default memo(JoinChannelModal);

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

@@ -1,35 +0,0 @@
.channel-action {
margin-left: auto;
button.action-wrapper {
display: flex;
width: 32px;
height: 32px;
flex-direction: column;
align-items: center;
padding: 6px 8px;
border: none;
margin-left: auto;
background: transparent;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
text-decoration: none;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.72);
fill: rgba(var(--center-channel-color-rgb), 0.72);
}
&:active {
background-color: rgba(var(--button-bg-rgb), 0.08);
color: var(--button-bg);
}
i.icon {
font-size: 18px;
line-height: 18px;
}
}
}

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

@@ -1,111 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useIntl} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions';
import {openModal} from 'actions/views/modals';
import {General} from 'mattermost-redux/constants';
import {leaveChannel} from 'mattermost-redux/actions/channels';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {getMyChannelMembership} from 'mattermost-redux/selectors/entities/channels';
import {LeastActiveChannel} from '@mattermost/types/insights';
import {GlobalState} from '@mattermost/types/store';
import Constants, {ModalIdentifiers} from 'utils/constants';
import {copyToClipboard} from 'utils/utils';
import {getSiteURL} from 'utils/url';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import Menu from 'components/widgets/menu/menu';
import LeaveChannelModal from 'components/leave_channel_modal';
import './channel_actions_menu.scss';
type Props = {
channel: LeastActiveChannel;
actionCallback?: () => Promise<void>;
}
const ChannelActionsMenu = ({channel, actionCallback}: Props) => {
const dispatch = useDispatch();
const {formatMessage} = useIntl();
const currentTeamUrl = useSelector(getCurrentRelativeTeamUrl);
const isChannelMember = useSelector((state: GlobalState) => getMyChannelMembership(state, channel.id));
const isDefault = channel.name === General.DEFAULT_CHANNEL;
const handleLeave = useCallback(async (e: Event) => {
e.preventDefault();
trackEvent('insights', 'leave_channel_action');
if (channel.type === Constants.PRIVATE_CHANNEL) {
dispatch(openModal({
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
dialogType: LeaveChannelModal,
dialogProps: {
channel,
callback: actionCallback,
},
}));
} else {
await dispatch(leaveChannel(channel.id));
actionCallback?.();
}
}, [channel]);
const copyLink = useCallback(() => {
trackEvent('insights', 'copy_channel_link_action');
copyToClipboard(`${getSiteURL()}${currentTeamUrl}/channels/${channel.name}`);
}, [currentTeamUrl, channel]);
return (
<div className='channel-action'>
<MenuWrapper
isDisabled={false}
stopPropagationOnToggle={true}
id={`customWrapper-${channel.id}`}
>
<button
className='icon action-wrapper'
aria-label={formatMessage({id: 'insights.leastActiveChannels.menuButtonAriaLabel', defaultMessage: 'Open manage channel menu'})}
>
<i className='icon icon-dots-vertical'/>
</button>
<Menu
openLeft={true}
openUp={false}
className={'group-actions-menu'}
ariaLabel={formatMessage({id: 'insights.leastActiveChannels.menuAriaLabel', defaultMessage: 'Manage channel menu'})}
>
<Menu.Group>
<Menu.ItemAction
onClick={handleLeave}
icon={<i className='icon-logout-variant'/>}
text={formatMessage({id: 'insights.leastActiveChannels.leaveChannel', defaultMessage: 'Leave channel'})}
disabled={false}
isDangerous={true}
show={Boolean(isChannelMember) && !isDefault}
/>
</Menu.Group>
<Menu.Group>
<Menu.ItemAction
onClick={copyLink}
icon={<i className='icon-link-variant'/>}
text={formatMessage({id: 'insights.leastActiveChannels.copyLink', defaultMessage: 'Copy link'})}
disabled={false}
/>
</Menu.Group>
</Menu>
</MenuWrapper>
</div>
);
};
export default memo(ChannelActionsMenu);

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

@@ -1,190 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import LeastActiveChannels from './least_active_channels';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyLeastActiveChannels: () => ({type: 'adsf', data: {}}),
getLeastActiveChannelsForTeam: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
id: 'ztbmh49z7pgtbbuximwxrogxzr',
type: 'O',
display_name: 'ut',
name: 'ratione-1',
last_activity_at: 1660175452131,
participants: [],
},
{
id: 'fbgmxnxxmfy1z8m855d9m88ipe',
type: 'O',
display_name: 'veritatis',
name: 'minima-3',
last_activity_at: 1660175525869,
participants: [],
},
{
id: 'uziynciroprq3g6ohhnednoeuw',
type: 'O',
display_name: 'autem',
name: 'aut-8',
last_activity_at: 1660175775169,
participants: [],
},
{
id: 'uziynciroprq4g6ohhnednoeuw',
type: 'O',
display_name: 'dolor',
name: 'aut-9',
last_activity_at: 0,
participants: [],
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_boards', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
size: CardSizes.small,
widgetType: InsightsWidgetTypes.LEAST_ACTIVE_CHANNELS,
class: 'least-active-channels-card',
timeFrameLabel: 'Last 7 days',
showModal: false,
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
ztbmh49z7pgtbbuximwxrogxzr: {
id: 'ztbmh49z7pgtbbuximwxrogxzr',
team_id: 'team_id1',
type: 'O',
display_name: 'ut',
name: 'ratione-1',
last_activity_at: 1660175452131,
},
fbgmxnxxmfy1z8m855d9m88ipe: {
id: 'fbgmxnxxmfy1z8m855d9m88ipe',
type: 'O',
display_name: 'veritatis',
name: 'minima-3',
last_activity_at: 1660175525869,
team_id: 'team_id1',
},
uziynciroprq3g6ohhnednoeuw: {
id: 'uziynciroprq3g6ohhnednoeuw',
type: 'O',
display_name: 'autem',
name: 'aut-8',
last_activity_at: 1660175775169,
team_id: 'team_id1',
},
uziynciroprq4g6ohhnednoeuw: {
id: 'uziynciroprq4g6ohhnednoeuw',
type: 'O',
display_name: 'dolor',
name: 'aut-9',
last_activity_at: 0,
participants: [],
},
},
myMembers: {},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if 4 channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<LeastActiveChannels
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('a.channel-row').length).toEqual(4);
});
test('check if 0 channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<LeastActiveChannels
{...props}
filterType='MY'
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.empty-state').length).toEqual(1);
});
});

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

@@ -1,127 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useCallback, useEffect, useMemo} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {LeastActiveChannel} from '@mattermost/types/insights';
import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components';
import {getMyLeastActiveChannels, getLeastActiveChannelsForTeam} from 'mattermost-redux/actions/insights';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {InsightsScopes} from 'utils/constants';
import widgetHoc, {WidgetHocProps} from '../widget_hoc/widget_hoc';
import WidgetEmptyState from '../widget_empty_state/widget_empty_state';
import LeastActiveChannelsItem from './least_active_channels_item/least_active_channels_item';
import './../../activity_and_insights.scss';
interface Props {
showModal?: boolean;
}
const LeastActiveChannels = (props: WidgetHocProps & Props) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const [leastActiveChannels, setLeastActiveChannels] = useState([] as LeastActiveChannel[]);
const currentTeamId = useSelector(getCurrentTeamId);
const getInactiveChannels = useCallback(async () => {
if (props.showModal === false) {
setLoading(true);
if (props.filterType === InsightsScopes.TEAM) {
const data: any = await dispatch(getLeastActiveChannelsForTeam(currentTeamId, 0, 3, props.timeFrame));
if (data.data?.items) {
setLeastActiveChannels(data.data.items);
}
} else {
const data: any = await dispatch(getMyLeastActiveChannels(currentTeamId, 0, 3, props.timeFrame));
if (data.data?.items) {
setLeastActiveChannels(data.data.items);
}
}
setLoading(false);
}
}, [props.timeFrame, currentTeamId, props.filterType, props.showModal]);
useEffect(() => {
getInactiveChannels();
}, [getInactiveChannels]);
const skeletonLoader = useMemo(() => {
const entries = [];
for (let i = 0; i < 4; i++) {
entries.push(
<div
className='least-active-channels-loading-container'
key={i}
>
<CircleSkeletonLoader size={16}/>
<RectangleSkeletonLoader
width='30%'
height={12}
margin='0 0 0 6px'
flex='1'
/>
<RectangleSkeletonLoader
width='30%'
height={12}
margin='0 0 0 30px'
flex='1'
/>
<RectangleSkeletonLoader
width='20%'
height={12}
margin='0 0 0 50px'
flex='1'
/>
</div>,
);
}
return entries;
}, []);
const getListItems = useCallback(() => {
return (
leastActiveChannels.map((channel, i) => {
return (
<LeastActiveChannelsItem
channel={channel}
key={i}
actionCallback={getInactiveChannels}
/>
);
})
);
}, [leastActiveChannels]);
return (
<div className='least-active-channels-container'>
{
loading &&
skeletonLoader
}
{
(leastActiveChannels && !loading) &&
<div className='channel-list'>
{getListItems()}
</div>
}
{
(leastActiveChannels.length === 0 && !loading) &&
<WidgetEmptyState
icon={'globe'}
/>
}
</div>
);
};
export default memo(widgetHoc(LeastActiveChannels));

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

@@ -1,108 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {trackEvent} from 'actions/telemetry_actions';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {LeastActiveChannel} from '@mattermost/types/insights';
import Constants from 'utils/constants';
import Avatars from 'components/widgets/users/avatars';
import Timestamp from 'components/timestamp';
import ChannelActionsMenu from '../channel_actions_menu/channel_actions_menu';
import './../../../activity_and_insights.scss';
type Props = {
channel: LeastActiveChannel;
actionCallback: () => Promise<void>;
}
const LeastActiveChannelsItem = ({channel, actionCallback}: Props) => {
const currentTeamUrl = useSelector(getCurrentRelativeTeamUrl);
const trackClickEvent = useCallback(() => {
trackEvent('insights', 'open_channel_from_least_active_channels_widget');
}, []);
const iconToDisplay = useCallback(() => {
let iconToDisplay = <i className='icon icon-globe'/>;
if (channel.type === Constants.PRIVATE_CHANNEL) {
iconToDisplay = <i className='icon icon-lock-outline'/>;
}
return iconToDisplay;
}, [channel]);
let timeMessage = (
<FormattedMessage
id='insights.leastActiveChannels.lastActivity'
defaultMessage='Last activity: {time}'
values={{
time:
(
<Timestamp
value={channel.last_activity_at}
units={[
'now',
'minute',
'hour',
'day',
'week',
'month',
]}
useTime={false}
/>
),
}}
/>
);
if (channel.last_activity_at === 0) {
timeMessage = (
<FormattedMessage
id='insights.leastActiveChannels.lastActivityNone'
defaultMessage='No activity'
/>
);
}
return (
<Link
className='channel-row'
onClick={trackClickEvent}
to={`${currentTeamUrl}/channels/${channel.name}`}
>
<div className='channel-info'>
<div className='channel-display-name'>
<span className='icon'>
{iconToDisplay()}
</span>
<span className='display-name'>{channel.display_name}</span>
</div>
<span className='last-activity'>
{timeMessage}
</span>
</div>
<Avatars
userIds={channel.participants}
size='xs'
disableProfileOverlay={true}
/>
<ChannelActionsMenu
channel={channel}
actionCallback={actionCallback}
/>
</Link>
);
};
export default memo(LeastActiveChannelsItem);

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

@@ -1,9 +0,0 @@
.LeastActiveChannelsTable {
.DataGrid_header {
.DataGrid_cell {
&:nth-of-type(3) {
text-align: right;
}
}
}
}

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

@@ -1,170 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {TimeFrames} from '@mattermost/types/insights';
import {act} from 'tests/react_testing_utils';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import LeastActiveChannelsTable from './least_active_channels_table';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyLeastActiveChannels: () => ({type: 'adsf', data: {}}),
getLeastActiveChannelsForTeam: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
id: 'ztbmh49z7pgtbbuximwxrogxzr',
type: 'O',
display_name: 'ut',
name: 'ratione-1',
last_activity_at: 1660175452131,
participants: [],
},
{
id: 'fbgmxnxxmfy1z8m855d9m88ipe',
type: 'O',
display_name: 'veritatis',
name: 'minima-3',
last_activity_at: 1660175525869,
participants: [],
},
{
id: 'uziynciroprq3g6ohhnednoeuw',
type: 'O',
display_name: 'autem',
name: 'aut-8',
last_activity_at: 1660175775169,
participants: [],
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_boards', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
closeModal: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
ztbmh49z7pgtbbuximwxrogxzr: {
id: 'ztbmh49z7pgtbbuximwxrogxzr',
team_id: 'team_id1',
type: 'O',
display_name: 'ut',
name: 'ratione-1',
last_activity_at: 1660175452131,
},
fbgmxnxxmfy1z8m855d9m88ipe: {
id: 'fbgmxnxxmfy1z8m855d9m88ipe',
type: 'O',
display_name: 'veritatis',
name: 'minima-3',
last_activity_at: 1660175525869,
team_id: 'team_id1',
},
uziynciroprq3g6ohhnednoeuw: {
id: 'uziynciroprq3g6ohhnednoeuw',
type: 'O',
display_name: 'autem',
name: 'aut-8',
last_activity_at: 1660175775169,
team_id: 'team_id1',
},
},
myMembers: {},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if 3 channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<LeastActiveChannelsTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(3);
});
test('check if 0 channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<LeastActiveChannelsTable
{...props}
filterType='MY'
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(0);
});
});

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

@@ -1,218 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {useHistory} from 'react-router-dom';
import classNames from 'classnames';
import {trackEvent} from 'actions/telemetry_actions';
import {getLeastActiveChannelsForTeam, getMyLeastActiveChannels} from 'mattermost-redux/actions/insights';
import {LeastActiveChannel, TimeFrame} from '@mattermost/types/insights';
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import Constants, {InsightsScopes} from 'utils/constants';
import Avatars from 'components/widgets/users/avatars';
import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid';
import Timestamp from 'components/timestamp';
import ChannelActionsMenu from '../channel_actions_menu/channel_actions_menu';
import './../../../activity_and_insights.scss';
import './least_active_channels_table.scss';
type Props = {
filterType: string;
timeFrame: TimeFrame;
closeModal: () => void;
}
const LeastActiveChannelsTable = (props: Props) => {
const dispatch = useDispatch();
const history = useHistory();
const [loading, setLoading] = useState(true);
const [leastActiveChannels, setLeastActiveChannels] = useState([] as LeastActiveChannel[]);
const currentTeamUrl = useSelector(getCurrentRelativeTeamUrl);
const currentTeamId = useSelector(getCurrentTeamId);
const getInactiveChannels = useCallback(async () => {
setLoading(true);
if (props.filterType === InsightsScopes.TEAM) {
const data: any = await dispatch(getLeastActiveChannelsForTeam(currentTeamId, 0, 10, props.timeFrame));
if (data.data?.items) {
setLeastActiveChannels(data.data.items);
}
} else {
const data: any = await dispatch(getMyLeastActiveChannels(currentTeamId, 0, 10, props.timeFrame));
if (data.data?.items) {
setLeastActiveChannels(data.data.items);
}
}
setLoading(false);
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getInactiveChannels();
}, [getInactiveChannels]);
const goToChannel = useCallback((channel: LeastActiveChannel) => {
props.closeModal();
trackEvent('insights', 'open_channel_from_least_active_channels_modal');
history.push(`${currentTeamUrl}/channels/${channel.name}`);
}, [props.closeModal]);
const getColumns = useMemo((): Column[] => {
const columns: Column[] = [
{
name: (
<FormattedMessage
id='insights.topReactions.rank'
defaultMessage='Rank'
/>
),
field: 'rank',
className: 'rankCell',
width: 0.07,
},
{
name: (
<FormattedMessage
id='insights.leastActiveChannels.channel'
defaultMessage='Channel'
/>
),
field: 'channel',
width: 0.48,
},
{
name: (
<FormattedMessage
id='insights.leastActiveChannels.lastActivityCell'
defaultMessage='Last activity'
/>
),
className: 'last-activity',
field: 'lastActivity',
width: 0.2,
},
{
name: (
<FormattedMessage
id='insights.leastActiveChannels.members'
defaultMessage='Members'
/>
),
className: 'participants',
field: 'participants',
width: 0.15,
},
{
name: (''),
field: 'actions',
width: 0.1,
className: 'actions',
},
];
return columns;
}, []);
const getRows = useMemo((): Row[] => {
return leastActiveChannels.map((channel, i) => {
let iconToDisplay = <i className='icon icon-globe'/>;
if (channel.type === Constants.PRIVATE_CHANNEL) {
iconToDisplay = <i className='icon icon-lock-outline'/>;
}
return (
{
cells: {
rank: (
<span className='cell-text'>
{i + 1}
</span>
),
channel: (
<div className='channel-display-name'>
<span className='icon'>
{iconToDisplay}
</span>
<span className='cell-text'>
{channel.display_name}
</span>
</div>
),
lastActivity: (
<span className='timestamp'>
{
channel.last_activity_at === 0 ?
<FormattedMessage
id='insights.leastActiveChannels.lastActivityNone'
defaultMessage='No activity'
/> :
<Timestamp
value={channel.last_activity_at}
units={[
'now',
'minute',
'hour',
'day',
'week',
'month',
]}
useTime={false}
/>
}
</span>
),
participants: (
<>
{channel.participants && channel.participants.length > 0 ? (
<Avatars
userIds={channel.participants}
size='xs'
disableProfileOverlay={true}
/>
) : null}
</>
),
actions: (
<ChannelActionsMenu
channel={channel}
actionCallback={getInactiveChannels}
/>
),
},
onClick: () => goToChannel(channel),
}
);
});
}, [leastActiveChannels]);
return (
<DataGrid
columns={getColumns}
rows={getRows}
loading={loading}
page={0}
nextPage={() => {}}
previousPage={() => {}}
startCount={1}
endCount={10}
total={0}
className={classNames('InsightsTable', 'LeastActiveChannelsTable')}
/>
);
};
export default memo(LeastActiveChannelsTable);

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

@@ -1,49 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {BrowserRouter} from 'react-router-dom';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import ModalPagination from './modal_pagination';
describe('components/activity_and_insights/insights/modal_pagination', () => {
const props = {
hasNext: false,
offset: 0,
setOffset: jest.fn(),
};
test('check if 1 - 10 renders', async () => {
const store = await mockStore({});
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<ModalPagination
{...props}
/>
</BrowserRouter>
</Provider>,
);
expect(wrapper.text().includes('1 - 10')).toBe(true);
});
test('check if 20 - 30 renders', async () => {
const store = await mockStore({});
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<ModalPagination
{...props}
offset={2}
/>
</BrowserRouter>
</Provider>,
);
expect(wrapper.text().includes('20 - 30')).toBe(true);
});
});

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

@@ -1,57 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
type Props = {
offset: number;
hasNext: boolean;
setOffset: (offset: number) => void;
}
const ModalPagination = ({offset, hasNext, setOffset}: Props) => {
const incrementOffset = useCallback(() => {
setOffset(offset + 1);
}, [offset]);
const decrementOffset = useCallback(() => {
setOffset(offset - 1);
}, [offset]);
const pagination = useCallback(() => {
let min = offset + 1;
const max = min * 10;
if (offset !== 0) {
min = offset * 10;
}
return (
<span className='pagination'>
{`${min} - ${max}`}
</span>
);
}, [offset]);
return (
<div className='pagination-buttons'>
{pagination()}
<button
onClick={decrementOffset}
disabled={offset === 0}
className='icon pagination-button'
>
<i className='icon icon-chevron-left'/>
</button>
<button
onClick={incrementOffset}
disabled={!hasNext}
className='icon pagination-button'
>
<i className='icon icon-chevron-right'/>
</button>
</div>
);
};
export default memo(ModalPagination);

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

@@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights/insights_title should match snapshot with data 1`] = `
<StateManager
aria-labelledby="changeInsightsTemporal"
className="react-select react-select-top"
classNamePrefix="react-select"
clearable={false}
components={
Object {
"DropdownIndicator": [Function],
}
}
defaultInputValue=""
defaultMenuIsOpen={false}
defaultValue={null}
id="insightsTemporal"
isSearchable={false}
menuPortalTarget={<body />}
onChange={[Function]}
options={
Array [
Object {
"label": "Today",
"value": "today",
},
Object {
"label": "Last 7 days",
"value": "7_day",
},
Object {
"label": "Last 28 days",
"value": "28_day",
},
]
}
styles={
Object {
"control": [Function],
"indicatorSeparator": [Function],
"menuPortal": [Function],
"option": [Function],
}
}
value={
Object {
"label": "Last 7 days",
"value": "7_day",
}
}
/>
`;

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

@@ -1,24 +0,0 @@
.ActivityAndInsights,
.insights-modal {
.react-select {
.react-select__control {
height: 32px;
min-height: 32px;
border-color: rgba(var(--center-channel-color-rgb), 0.2);
background: var(--center-channel-bg);
}
.react-select__value-container {
padding-left: 12px;
}
.react-select__indicators {
padding-right: 10px;
color: rgba(var(--center-channel-color-rgb), 0.64);
}
.react-select__input {
color: var(--center-channel-color);
}
}
}

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

@@ -1,26 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {TimeFrames} from '@mattermost/types/insights';
import TimeFrameDropdown from './time_frame_dropdown';
describe('components/activity_and_insights/insights/insights_title', () => {
const props = {
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
setTimeFrame: jest.fn(),
};
test('should match snapshot with data', () => {
const wrapper = shallow(
<TimeFrameDropdown
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -1,120 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import ReactSelect, {ValueType} from 'react-select';
import ChevronDownIcon from '@mattermost/compass-icons/components/chevron-down';
import {trackEvent} from 'actions/telemetry_actions';
import {TimeFrames} from '@mattermost/types/insights';
import {localizeMessage} from 'utils/utils';
import './time_frame_dropdown.scss';
type SelectOption = {
value: string;
label: string;
}
type Props = {
timeFrame: string;
setTimeFrame: (value: SelectOption) => void;
}
const TimeFrameDropdown = (props: Props) => {
const reactStyles = {
control: (provided: React.CSSProperties) => ({
...provided,
width: '140px',
cursor: 'pointer',
fontSize: '12px',
lineHeight: '16px',
}),
indicatorSeparator: (provided: React.CSSProperties) => ({
...provided,
display: 'none',
}),
option: (provided: React.CSSProperties) => ({
...provided,
cursor: 'pointer',
}),
menuPortal: (provided: React.CSSProperties) => ({
...provided,
zIndex: 1100,
}),
};
const onTimeFrameChange = (selectedOption: ValueType<SelectOption>) => {
if (selectedOption && 'value' in selectedOption) {
trackEvent('insights', `time_frame_selected_${selectedOption.value}`);
props.setTimeFrame(selectedOption);
}
};
const CustomDropwdown = () => {
return (
<span className='icon'>
<ChevronDownIcon size={12}/>
</span>
);
};
const getCurrentTimeFrame = useCallback((): SelectOption => {
const timeFrame = props.timeFrame;
if (timeFrame === TimeFrames.INSIGHTS_1_DAY) {
return {
value: TimeFrames.INSIGHTS_1_DAY,
label: localizeMessage('insights.timeFrame.today', 'Today'),
};
}
if (timeFrame === TimeFrames.INSIGHTS_28_DAYS) {
return {
value: TimeFrames.INSIGHTS_28_DAYS,
label: localizeMessage('insights.timeFrame.longRange', 'Last 28 days'),
};
}
return {
value: TimeFrames.INSIGHTS_7_DAYS,
label: localizeMessage('insights.timeFrame.mediumRange', 'Last 7 days'),
};
}, [props.timeFrame]);
return (
<ReactSelect
className='react-select react-select-top'
classNamePrefix='react-select'
id='insightsTemporal'
menuPortalTarget={document.body}
styles={reactStyles}
options={[
{
value: TimeFrames.INSIGHTS_1_DAY,
label: localizeMessage('insights.timeFrame.today', 'Today'),
},
{
value: TimeFrames.INSIGHTS_7_DAYS,
label: localizeMessage('insights.timeFrame.mediumRange', 'Last 7 days'),
},
{
value: TimeFrames.INSIGHTS_28_DAYS,
label: localizeMessage('insights.timeFrame.longRange', 'Last 28 days'),
},
]}
clearable={false}
onChange={onTimeFrameChange}
value={getCurrentTimeFrame()}
aria-labelledby='changeInsightsTemporal'
components={{
DropdownIndicator: CustomDropwdown,
}}
isSearchable={false}
/>
);
};
export default memo(TimeFrameDropdown);

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

@@ -1,171 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopBoards from './top_boards';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn().mockReturnValue(() => {}),
}));
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/top_boards', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
size: CardSizes.small,
widgetType: InsightsWidgetTypes.TOP_BOARDS,
class: 'top-baords-card',
timeFrameLabel: 'Last 7 days',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
plugins: {
insightsHandlers: {
focalboard: async () => {
return {
items: [
{
boardID: 'b8i4hjy9z6igjjbs68fudzr6z8h',
icon: '📅',
title: 'Test calendar ',
activityCount: 32,
activeUsers: ['9qobtrxa93dhfg1fqmhcq5wj4o'],
createdBy: '9qobtrxa93dhfg1fqmhcq5wj4o',
},
{
boardID: 'bf3mmu7hjgprpmp1ekiozyggrjh',
icon: '📅',
title: 'Content Calendar ',
activityCount: 24,
activeUsers: ['9qobtrxa93dhfg1fqmhcq5wj4o', '9x4to68xqiyfzb8dxwfpbqopie'],
createdBy: '9qobtrxa93dhfg1fqmhcq5wj4o',
},
{
boardID: 'bf3mmu7hjgprpmp1ekiozyggrjh',
icon: '📅',
title: 'Content Calendar ',
activityCount: 24,
// MM-49023
activeUsers: '9qobtrxa93dhfg1fqmhcq5wj4o,9x4to68xqiyfzb8dxwfpbqopie',
createdBy: '9qobtrxa93dhfg1fqmhcq5wj4o',
},
],
};
},
},
},
};
test('check if 3 team top boards render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopBoards
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
// Link causes the class to render 3 times for each item
expect(wrapper.find('.board-item').length).toEqual(9);
});
test('check if 0 top boards render', async () => {
const state = {
...initialState,
plugins: {
insightsHandlers: {
focalboard: async () => {
return {
items: [],
};
},
},
},
};
const store = await mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopBoards
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.empty-state').length).toEqual(1);
});
});

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

@@ -1,131 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useCallback, useEffect, useMemo} from 'react';
import {useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {TopBoard} from '@mattermost/types/insights';
import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {trackEvent} from 'actions/telemetry_actions';
import {GlobalState} from 'types/store';
import Avatars from 'components/widgets/users/avatars';
import widgetHoc, {WidgetHocProps} from '../widget_hoc/widget_hoc';
import WidgetEmptyState from '../widget_empty_state/widget_empty_state';
import './../../activity_and_insights.scss';
const TopBoards = (props: WidgetHocProps) => {
const [loading, setLoading] = useState(true);
const [topBoards, setTopBoards] = useState([] as TopBoard[]);
const currentTeamId = useSelector(getCurrentTeamId);
const boardsHandler = useSelector((state: GlobalState) => state.plugins.insightsHandlers.focalboard || state.plugins.insightsHandlers.boards);
const getTopBoards = useCallback(async () => {
setLoading(true);
const data: any = await boardsHandler(props.timeFrame, 0, 4, currentTeamId, props.filterType);
if (data && data.items) {
setTopBoards(data.items);
}
setLoading(false);
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopBoards();
}, [getTopBoards]);
const skeletonLoader = useMemo(() => {
const entries = [];
for (let i = 0; i < 4; i++) {
entries.push(
<div
className='top-board-loading-container'
key={i}
>
<CircleSkeletonLoader size={32}/>
<div className='loading-lines'>
<RectangleSkeletonLoader
height={12}
flex='none'
/>
<RectangleSkeletonLoader
height={8}
flex='none'
margin='6px 0 0 0'
/>
</div>
</div>,
);
}
return entries;
}, []);
const trackClickEvent = useCallback(() => {
trackEvent('insights', 'open_board_from_top_boards_widget');
}, []);
return (
<div className='top-board-container'>
{
loading &&
skeletonLoader
}
{
(topBoards && !loading) &&
<div className='board-list'>
{
topBoards.map((board, i) => {
return (
<Link
className='board-item'
onClick={trackClickEvent}
key={i}
to={`/boards/team/${currentTeamId}/${board.boardID}`}
>
<span className='board-icon'>{board.icon}</span>
<div className='display-info'>
<span className='display-name'>{board.title}</span>
<span className='update-counts'>
<FormattedMessage
id='insights.topBoards.updates'
defaultMessage='{updateCount} updates'
values={{
updateCount: board.activityCount,
}}
/>
</span>
</div>
<Avatars
// MM-49023: community bugfix to maintain backwards compatibility
userIds={typeof board.activeUsers === 'string' ? board.activeUsers.split(',') : board.activeUsers}
size='xs'
disableProfileOverlay={true}
/>
</Link>
);
})
}
</div>
}
{
(topBoards.length === 0 && !loading) &&
<WidgetEmptyState
icon={'product-boards'}
/>
}
</div>
);
};
export default memo(widgetHoc(TopBoards));

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

@@ -1,161 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopBoardsTable from './top_boards_table';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/top_boards_table', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
closeModal: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
plugins: {
insightsHandlers: {
focalboard: async () => {
return {
items: [
{
boardID: 'b8i4hjy9z6igjjbs68fudzr6z8h',
icon: '📅',
title: 'Test calendar ',
activityCount: 32,
activeUsers: ['9qobtrxa93dhfg1fqmhcq5wj4o'],
createdBy: '9qobtrxa93dhfg1fqmhcq5wj4o',
},
{
boardID: 'bf3mmu7hjgprpmp1ekiozyggrjh',
icon: '📅',
title: 'Content Calendar ',
activityCount: 24,
activeUsers: ['9qobtrxa93dhfg1fqmhcq5wj4o', '9x4to68xqiyfzb8dxwfpbqopie'],
createdBy: '9qobtrxa93dhfg1fqmhcq5wj4o',
},
{
boardID: 'bf3mmu7hjgprpmp1ekiozyggrjh',
icon: '📅',
title: 'Content Calendar ',
activityCount: 24,
// MM-49023
activeUsers: '9qobtrxa93dhfg1fqmhcq5wj4o,9x4to68xqiyfzb8dxwfpbqopie',
createdBy: '9qobtrxa93dhfg1fqmhcq5wj4o',
},
],
};
},
},
},
};
test('check if 3 team top boards render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopBoardsTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(3);
});
test('check if 0 top boards render', async () => {
const state = {
...initialState,
plugins: {
insightsHandlers: {
focalboard: async () => {
return {
items: [],
};
},
},
},
};
const store = await mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopBoardsTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(0);
});
});

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

@@ -1,158 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useMemo, useEffect, useCallback} from 'react';
import {useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import {useHistory} from 'react-router-dom';
import classNames from 'classnames';
import {trackEvent} from 'actions/telemetry_actions';
import {GlobalState} from 'types/store';
import {TimeFrame, TopBoard} from '@mattermost/types/insights';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid';
import Avatars from 'components/widgets/users/avatars';
import './../../../activity_and_insights.scss';
type Props = {
filterType: string;
timeFrame: TimeFrame;
closeModal: () => void;
}
const TopBoardsTable = (props: Props) => {
const history = useHistory();
const [loading, setLoading] = useState(true);
const [topBoards, setTopBoards] = useState([] as TopBoard[]);
const currentTeamId = useSelector(getCurrentTeamId);
const boardsHandler = useSelector((state: GlobalState) => state.plugins.insightsHandlers.focalboard || state.plugins.insightsHandlers.boards);
const getTopBoards = useCallback(async () => {
setLoading(true);
const data: any = await boardsHandler(props.timeFrame, 0, 10, currentTeamId, props.filterType);
if (data && data.items) {
setTopBoards(data.items);
}
setLoading(false);
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopBoards();
}, [getTopBoards]);
const goToBoard = useCallback((board: TopBoard) => {
props.closeModal();
trackEvent('insights', 'open_board_from_top_boards_modal');
history.push(`/boards/team/${currentTeamId}/${board.boardID}`);
}, [props.closeModal]);
const getColumns = useMemo((): Column[] => {
const columns: Column[] = [
{
name: (
<FormattedMessage
id='insights.topReactions.rank'
defaultMessage='Rank'
/>
),
field: 'rank',
className: 'rankCell',
width: 0.07,
},
{
name: (
<FormattedMessage
id='insights.topBoardsTable.board'
defaultMessage='Board'
/>
),
field: 'board',
width: 0.7,
},
{
name: (
<FormattedMessage
id='insights.topBoardsTable.updates'
defaultMessage='Updates'
/>
),
field: 'updates',
width: 0.08,
},
{
name: (
<FormattedMessage
id='insights.topBoardsTable.participants'
defaultMessage='Participants'
/>
),
field: 'participants',
width: 0.15,
},
];
return columns;
}, []);
const getRows = useMemo((): Row[] => {
return topBoards.map((board, i) => {
return (
{
cells: {
rank: (
<span className='cell-text'>
{i + 1}
</span>
),
board: (
<div className='board-item'>
<span className='board-icon'>{board.icon}</span>
<span className='board-title'>{board.title}</span>
</div>
),
updates: (
<span className='board-updates'>{board.activityCount}</span>
),
participants: (
<Avatars
// MM-49023: community bugfix to maintain backwards compatibility
userIds={typeof board.activeUsers === 'string' ? board.activeUsers.split(',') : board.activeUsers}
size='xs'
disableProfileOverlay={true}
/>
),
},
onClick: () => {
goToBoard(board);
},
}
);
});
}, [topBoards]);
return (
<DataGrid
columns={getColumns}
rows={getRows}
loading={loading}
page={0}
nextPage={() => {}}
previousPage={() => {}}
startCount={1}
endCount={10}
total={0}
className={classNames('InsightsTable', 'TopBoardsTable')}
/>
);
};
export default memo(TopBoardsTable);

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

@@ -1,123 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopChannels from './top_channels';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyTopChannels: () => ({type: 'adsf', data: {}}),
getTopChannelsForTeam: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
id: '4r98uzxe4b8t5g9ntt9zcdzktw',
type: 'P',
display_name: 'nesciunt',
name: 'sequi-7',
team_id: 'team_id1',
message_count: 1,
},
{
id: '4r98uzxe4b8t5gfdsdfggs',
type: 'P',
display_name: 'test',
name: 'test-7',
team_id: 'team_id1',
message_count: 2,
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_channels', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
size: CardSizes.large,
widgetType: InsightsWidgetTypes.TOP_CHANNELS,
class: 'top-channels-card',
timeFrameLabel: 'Last 7 days',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {},
},
},
preferences: {
myPreferences: {},
},
},
};
test('check if 2 team top channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopChannels
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.channel-message-count').length).toEqual(2);
});
test('check if 0 my top channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopChannels
{...props}
filterType={'MY'}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.empty-state').length).toEqual(1);
});
});

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

@@ -1,209 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import {FormattedMessage} from 'react-intl';
import {TopChannel, TopChannelGraphData} from '@mattermost/types/insights';
import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components';
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
import {getMyTopChannels, getTopChannelsForTeam} from 'mattermost-redux/actions/insights';
import Constants, {InsightsScopes} from 'utils/constants';
import {trackEvent} from 'actions/telemetry_actions';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import widgetHoc, {WidgetHocProps} from '../widget_hoc/widget_hoc';
import WidgetEmptyState from '../widget_empty_state/widget_empty_state';
import TopChannelsLineChart from './top_channels_line_chart/top_channels_line_chart';
import './../../activity_and_insights.scss';
const TopChannels = (props: WidgetHocProps) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const [topChannels, setTopChannels] = useState([] as TopChannel[]);
const [channelLineChartData, setChannelLineChartData] = useState({} as TopChannelGraphData);
const currentTeamId = useSelector(getCurrentTeamId);
const currentTeamUrl = useSelector(getCurrentRelativeTeamUrl);
const timeZone = useSelector(getCurrentTimezone);
const getTopTeamChannels = useCallback(async () => {
if (props.filterType === InsightsScopes.TEAM) {
setLoading(true);
const data: any = await dispatch(getTopChannelsForTeam(currentTeamId, 0, 5, props.timeFrame));
if (data.data?.items) {
setTopChannels(data.data.items);
}
if (data.data?.channel_post_counts_by_duration) {
setChannelLineChartData(data.data.channel_post_counts_by_duration);
}
setLoading(false);
}
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopTeamChannels();
}, [getTopTeamChannels]);
const getMyTeamChannels = useCallback(async () => {
if (props.filterType === InsightsScopes.MY) {
setLoading(true);
const data: any = await dispatch(getMyTopChannels(currentTeamId, 0, 5, props.timeFrame));
if (data.data?.items) {
setTopChannels(data.data.items);
}
if (data.data?.channel_post_counts_by_duration) {
setChannelLineChartData(data.data.channel_post_counts_by_duration);
}
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
useEffect(() => {
getMyTeamChannels();
}, [getMyTeamChannels]);
const skeletonTitle = useMemo(() => {
const skeletonFlexes = ['1', '0.85', '0.9', '0.5', '0.7'];
const titles = [];
for (let i = 0; i < 5; i++) {
titles.push(
<div
className='top-channel-loading-row'
key={i}
>
<CircleSkeletonLoader size={16}/>
<RectangleSkeletonLoader
height={12}
margin='0 0 0 8px'
flex={skeletonFlexes[i]}
/>
</div>,
);
}
return titles;
}, []);
const tooltip = useCallback((messageCount: number) => {
return (
<Tooltip
id='total-messages'
>
<FormattedMessage
id='insights.topChannels.messageCount'
defaultMessage='{messageCount} total messages'
values={{
messageCount,
}}
/>
</Tooltip>
);
}, []);
const trackClickEvent = useCallback(() => {
trackEvent('insights', 'open_channel_from_top_channels_widget');
}, []);
return (
<>
<div className='top-channel-container'>
<div className='top-channel-line-chart'>
{
loading &&
<RectangleSkeletonLoader
height={200}
borderRadius={4}
/>
}
{
(!loading && topChannels.length !== 0) &&
<>
<TopChannelsLineChart
topChannels={topChannels}
timeFrame={props.timeFrame}
channelLineChartData={channelLineChartData}
timeZone={timeZone || 'utc'}
/>
</>
}
</div>
<div className='top-channel-list'>
{
loading &&
skeletonTitle
}
{
(!loading && topChannels.length !== 0) &&
<div className='channel-list'>
{
topChannels.map((channel) => {
const barSize = ((channel.message_count / topChannels[0].message_count) * 0.8);
let iconToDisplay = <i className='icon icon-globe'/>;
if (channel.type === Constants.PRIVATE_CHANNEL) {
iconToDisplay = <i className='icon icon-lock-outline'/>;
}
return (
<Link
className='channel-row'
to={`${currentTeamUrl}/channels/${channel.name}`}
key={channel.id}
onClick={trackClickEvent}
>
<div
className='channel-display-name'
>
<span className='icon'>
{iconToDisplay}
</span>
<span className='display-name'>{channel.display_name}</span>
</div>
<div className='channel-message-count'>
<OverlayTrigger
trigger={['hover']}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='right'
overlay={tooltip(channel.message_count)}
>
<span className='message-count'>{channel.message_count}</span>
</OverlayTrigger>
<span
className='horizontal-bar'
style={{
flex: `${barSize} 0`,
}}
/>
</div>
</Link>
);
})
}
</div>
}
</div>
</div>
{
(topChannels.length === 0 && !loading) &&
<WidgetEmptyState
icon={'globe'}
/>
}
</>
);
};
export default memo(widgetHoc(TopChannels));

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

@@ -1,273 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights/top_channels should match snapshot 1`] = `
<Provider
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
>
<BrowserRouter>
<Router
history={
Object {
"action": "POP",
"block": [Function],
"createHref": [Function],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"length": 1,
"listen": [Function],
"location": Object {
"hash": "",
"pathname": "undefinedundefined",
"search": "",
"state": undefined,
},
"push": [Function],
"replace": [Function],
}
}
>
<Memo(TopChannelsLineChart)
channelLineChartData={
Object {
"2022-05-01": Object {
"4r98uzxe4b8t5g9ntt9zcdzktw": 10,
"4r98uzxe4b8t5gfdsdfggsdfgs": 20,
},
"2022-05-02": Object {
"4r98uzxe4b8t5g9ntt9zcdzktw": 20,
"4r98uzxe4b8t5gfdsdfggsdfgs": 40,
},
"2022-05-03": Object {
"4r98uzxe4b8t5g9ntt9zcdzktw": 15,
"4r98uzxe4b8t5gfdsdfggsdfgs": 25,
},
"2022-05-04": Object {
"4r98uzxe4b8t5g9ntt9zcdzktw": 15,
"4r98uzxe4b8t5gfdsdfggsdfgs": 10,
},
"2022-05-05": Object {
"4r98uzxe4b8t5g9ntt9zcdzktw": 10,
"4r98uzxe4b8t5gfdsdfggsdfgs": 15,
},
"2022-05-06": Object {
"4r98uzxe4b8t5g9ntt9zcdzktw": 20,
"4r98uzxe4b8t5gfdsdfggsdfgs": 50,
},
"2022-05-07": Object {
"4r98uzxe4b8t5g9ntt9zcdzktw": 10,
"4r98uzxe4b8t5gfdsdfggsdfgs": 40,
},
}
}
timeFrame="7_day"
timeZone="America/Toronto"
topChannels={
Array [
Object {
"display_name": "nesciunt",
"id": "4r98uzxe4b8t5g9ntt9zcdzktw",
"message_count": 100,
"name": "sequi-7",
"team_id": "team_id1",
"type": "P",
},
Object {
"display_name": "test",
"id": "4r98uzxe4b8t5gfdsdfggsdfgs",
"message_count": 200,
"name": "test-7",
"team_id": "team_id1",
"type": "P",
},
]
}
>
<LineChart
data={
Object {
"datasets": Array [
Object {
"backgroundColor": "transparent",
"borderColor": "#1c58d9",
"data": Array [
10,
20,
15,
15,
10,
20,
10,
],
"fillColor": "#1c58d9",
"hitRadius": 10,
"hoverBackgroundColor": "#1c58d9",
"label": "nesciunt",
"pointBackgroundColor": "#1c58d9",
"pointBorderColor": "transparent",
"pointRadius": 0,
"tension": 0.4,
},
Object {
"backgroundColor": "transparent",
"borderColor": "#3db887",
"data": Array [
20,
40,
25,
10,
15,
50,
40,
],
"fillColor": "#3db887",
"hitRadius": 10,
"hoverBackgroundColor": "#3db887",
"label": "test",
"pointBackgroundColor": "#3db887",
"pointBorderColor": "transparent",
"pointRadius": 0,
"tension": 0.4,
},
],
"labels": Array [
"May 01",
"May 02",
"May 03",
"May 04",
"May 05",
"May 06",
"May 07",
],
}
}
height={200}
id="totalPostsPerChannel"
options={
Object {
"hover": Object {
"mode": "point",
},
"maintainAspectRatio": false,
"onHover": [Function],
"plugins": Object {
"legend": Object {
"display": false,
},
"tooltip": Object {
"bodyAlign": "left",
"bodyFont": Object {
"family": "Open Sans",
"weight": "bold",
},
"bodySpacing": 10,
"callbacks": Object {
"footer": [Function],
"label": [Function],
"title": [Function],
},
"footerAlign": "center",
"footerColor": "rgba(255, 255, 255, .64)",
"footerFont": Object {
"family": "Open Sans",
"size": 11,
"style": "normal",
},
"footerSpacing": 10,
"multiKeyBackground": "transparent",
},
},
"responsive": true,
"scales": Object {
"xAxis": Object {
"grid": Object {
"drawOnChartArea": false,
},
"ticks": Object {
"callback": [Function],
"color": "rgba(63,67,80,0.72)",
"font": Object {
"family": "Open Sans",
"size": 10,
},
},
},
"yAxis": Object {
"beginAtZero": true,
"grid": Object {
"drawOnChartArea": true,
},
"ticks": Object {
"color": "rgba(63,67,80,0.72)",
"font": Object {
"family": "Open Sans",
"size": 10,
},
"maxTicksLimit": 5,
"precision": 0,
},
},
},
}
}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Top Channels"
id="analytics.system.topChannels"
/>
}
>
<div
className="col-sm-12"
>
<div
className="total-count by-day"
>
<div
className="title"
>
<FormattedMessage
defaultMessage="Top Channels"
id="analytics.system.topChannels"
>
<span>
Top Channels
</span>
</FormattedMessage>
</div>
<div
className="content"
>
<canvas
data-labels={
Array [
"May 01",
"May 02",
"May 03",
"May 04",
"May 05",
"May 06",
"May 07",
]
}
data-testid="totalPostsPerChannel"
height={200}
/>
</div>
</div>
</div>
</LineChart>
</Memo(TopChannelsLineChart)>
</Router>
</BrowserRouter>
</Provider>
`;

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

@@ -1,133 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {BrowserRouter} from 'react-router-dom';
import {TimeFrames, TopChannel} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import TopChannelsLineChart from './top_channels_line_chart';
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyTopChannels: () => ({type: 'adsf', data: {}}),
getTopChannelsForTeam: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
id: '4r98uzxe4b8t5g9ntt9zcdzktw',
type: 'P',
display_name: 'nesciunt',
name: 'sequi-7',
team_id: 'team_id1',
message_count: 1,
},
{
id: '4r98uzxe4b8t5gfdsdfggs',
type: 'P',
display_name: 'test',
name: 'test-7',
team_id: 'team_id1',
message_count: 2,
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_channels', () => {
const props = {
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
topChannels: [
{
id: '4r98uzxe4b8t5g9ntt9zcdzktw',
type: 'P',
display_name: 'nesciunt',
name: 'sequi-7',
team_id: 'team_id1',
message_count: 100,
} as TopChannel,
{
id: '4r98uzxe4b8t5gfdsdfggsdfgs',
type: 'P',
display_name: 'test',
name: 'test-7',
team_id: 'team_id1',
message_count: 200,
} as TopChannel,
],
channelLineChartData: {
'2022-05-01': {
'4r98uzxe4b8t5g9ntt9zcdzktw': 10,
'4r98uzxe4b8t5gfdsdfggsdfgs': 20,
},
'2022-05-02': {
'4r98uzxe4b8t5g9ntt9zcdzktw': 20,
'4r98uzxe4b8t5gfdsdfggsdfgs': 40,
},
'2022-05-03': {
'4r98uzxe4b8t5g9ntt9zcdzktw': 15,
'4r98uzxe4b8t5gfdsdfggsdfgs': 25,
},
'2022-05-04': {
'4r98uzxe4b8t5g9ntt9zcdzktw': 15,
'4r98uzxe4b8t5gfdsdfggsdfgs': 10,
},
'2022-05-05': {
'4r98uzxe4b8t5g9ntt9zcdzktw': 10,
'4r98uzxe4b8t5gfdsdfggsdfgs': 15,
},
'2022-05-06': {
'4r98uzxe4b8t5g9ntt9zcdzktw': 20,
'4r98uzxe4b8t5gfdsdfggsdfgs': 50,
},
'2022-05-07': {
'4r98uzxe4b8t5g9ntt9zcdzktw': 10,
'4r98uzxe4b8t5gfdsdfggsdfgs': 40,
},
},
timeZone: 'America/Toronto',
};
const store = mockStore({
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
},
preferences: {
myPreferences: {},
},
},
});
test('should match snapshot', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopChannelsLineChart
{...props}
/>
</BrowserRouter>
</Provider>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -1,231 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useMemo} from 'react';
import {useSelector} from 'react-redux';
import moment from 'moment-timezone';
import {FormattedMessage, useIntl} from 'react-intl';
import {TimeFrame, TimeFrames, TopChannel, TopChannelGraphData} from '@mattermost/types/insights';
import {GlobalState} from '@mattermost/types/store';
import {getBool, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {changeOpacity} from 'mattermost-redux/utils/theme_utils';
import {Preferences} from 'mattermost-redux/constants';
import LineChart from 'components/analytics/line_chart';
import './../../../activity_and_insights.scss';
type Props = {
topChannels: TopChannel[];
timeFrame: TimeFrame;
channelLineChartData: TopChannelGraphData;
timeZone: string;
}
const TopChannelsLineChart = ({topChannels, timeFrame, channelLineChartData, timeZone}: Props) => {
const theme = useSelector(getTheme);
const intl = useIntl();
const isMilitaryTime = useSelector((state: GlobalState) => getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false));
const getLabels = useMemo(() => {
const labels: any[] = Object.keys(channelLineChartData);
for (let i = 0; i < labels.length; i++) {
const label = labels[i];
if (timeFrame === TimeFrames.INSIGHTS_1_DAY) {
labels[i] = intl.formatTime(Date.parse(label), {
hour12: isMilitaryTime ? undefined : true,
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
timeZone,
});
} else {
labels[i] = intl.formatDate(moment.tz(label, timeZone).toDate(), {
month: 'short',
day: '2-digit',
});
}
}
return labels;
}, [channelLineChartData, timeZone]);
const sortGraphData = useMemo(() => {
const labels = getLabels;
const values = {} as Record<string, number[]>;
const keys = Object.keys(channelLineChartData);
for (let i = 0, len = keys.length; i < len; i++) {
const item = channelLineChartData[keys[i]];
const channelIds = Object.keys(item);
for (let j = 0, channelIdsLength = channelIds.length; j < channelIdsLength; j++) {
const count = item[channelIds[j]];
if (values[channelIds[j]]) {
values[channelIds[j]].push(count);
} else {
values[channelIds[j]] = [count];
}
}
}
return {
labels,
values,
};
}, []);
const getGraphData = useMemo(() => {
const data = sortGraphData;
const dataset = [];
const colours = [theme.buttonBg, theme.onlineIndicator, theme.awayIndicator, theme.dndIndicator, theme.newMessageSeparator];
for (let i = 0; i < 5; i++) {
if (topChannels[i]) {
const colour = colours[i];
dataset.push({
fillColor: colour,
borderColor: colour,
pointBackgroundColor: colour,
pointBorderColor: 'transparent',
backgroundColor: 'transparent',
pointRadius: 0,
hoverBackgroundColor: colour,
label: topChannels[i].display_name,
data: data.values[topChannels[i].id],
hitRadius: 10,
tension: 0.4,
});
}
}
return {
datasets: dataset,
labels: data.labels,
};
}, []);
return (
<LineChart
title={
<FormattedMessage
id='analytics.system.topChannels'
defaultMessage='Top Channels'
/>
}
id='totalPostsPerChannel'
height={200}
options={{
responsive: true,
hover: {
mode: 'point',
},
onHover: (e, activeElements) => {
if (e.native && e.native.target) {
(e.native.target as HTMLElement).style.cursor = activeElements?.length > 0 ? 'pointer' : 'auto';
}
},
maintainAspectRatio: false,
scales: {
xAxis: {
grid: {
drawOnChartArea: false,
},
ticks: {
callback(_, index) {
const labels = getLabels;
const value = labels[index];
// Hide every 4th tick label for 28 day time frame
if (timeFrame === TimeFrames.INSIGHTS_28_DAYS) {
return index % 4 === 0 ? value : '';
}
if (timeFrame === TimeFrames.INSIGHTS_1_DAY) {
if (labels.length > 8 && labels.length <= 12) {
return index % 2 === 0 ? value : '';
}
if (labels.length > 12) {
return index % 4 === 0 ? value : '';
}
}
return value;
},
font: {
family: 'Open Sans',
size: 10,
},
color: changeOpacity(theme.centerChannelColor, 0.72),
},
},
yAxis: {
grid: {
drawOnChartArea: true,
},
beginAtZero: true,
ticks: {
maxTicksLimit: 5,
font: {
family: 'Open Sans',
size: 10,
},
precision: 0,
color: changeOpacity(theme.centerChannelColor, 0.72),
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
callbacks: {
label(context) {
const index = context.datasetIndex;
if (typeof index !== 'undefined' && context.dataset && context.dataset?.label) {
const label = context.dataset?.label || '';
if (label.length > 16) {
return ` ${label.slice(0, 16)}...`;
}
return ` ${label}`;
}
return '';
},
title() {
return '';
},
footer(tooltipItems) {
return `${tooltipItems[0].parsed.y} messages`;
},
},
bodyFont: {
weight: 'bold',
family: 'Open Sans',
},
bodyAlign: 'left',
bodySpacing: 10,
footerFont: {
family: 'Open Sans',
size: 11,
style: 'normal',
},
footerAlign: 'center',
footerSpacing: 10,
footerColor: 'rgba(255, 255, 255, .64)',
multiKeyBackground: 'transparent',
},
},
}}
data={getGraphData}
/>
);
};
export default memo(TopChannelsLineChart);

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

@@ -1,114 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopChannelsTable from './top_channels_table';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyTopChannels: () => ({type: 'adsf', data: {}}),
getTopChannelsForTeam: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
id: '4r98uzxe4b8t5g9ntt9zcdzktw',
type: 'P',
display_name: 'nesciunt',
name: 'sequi-7',
team_id: 'team_id1',
message_count: 1,
},
{
id: '4r98uzxe4b8t5gfdsdfggs',
type: 'P',
display_name: 'test',
name: 'test-7',
team_id: 'team_id1',
message_count: 2,
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_channels', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
closeModal: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
},
},
};
test('check if 2 team top channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopChannelsTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(2);
});
test('check if 0 my top channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopChannelsTable
{...props}
filterType={'MY'}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(0);
});
});

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

@@ -1,172 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import {trackEvent} from 'actions/telemetry_actions';
import {TimeFrame, TopChannel} from '@mattermost/types/insights';
import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid';
import Constants, {InsightsScopes} from 'utils/constants';
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getMyTopChannels, getTopChannelsForTeam} from 'mattermost-redux/actions/insights';
import './../../../activity_and_insights.scss';
type Props = {
filterType: string;
timeFrame: TimeFrame;
closeModal: () => void;
}
const TopChannelsTable = (props: Props) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const [topChannels, setTopChannels] = useState([] as TopChannel[]);
const currentTeamId = useSelector(getCurrentTeamId);
const currentTeamUrl = useSelector(getCurrentRelativeTeamUrl);
const getTopTeamChannels = useCallback(async () => {
if (props.filterType === InsightsScopes.TEAM) {
setLoading(true);
const data: any = await dispatch(getTopChannelsForTeam(currentTeamId, 0, 10, props.timeFrame));
if (data.data && data.data.items) {
setTopChannels(data.data.items);
}
setLoading(false);
}
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopTeamChannels();
}, [getTopTeamChannels]);
const getMyTeamChannels = useCallback(async () => {
if (props.filterType === InsightsScopes.MY) {
setLoading(true);
const data: any = await dispatch(getMyTopChannels(currentTeamId, 0, 10, props.timeFrame));
if (data.data && data.data.items) {
setTopChannels(data.data.items);
}
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
useEffect(() => {
getMyTeamChannels();
}, [getMyTeamChannels]);
const closeModal = useCallback(() => {
trackEvent('insights', 'open_channel_from_top_channels_modal');
props.closeModal();
}, [props.closeModal]);
const getColumns = useMemo((): Column[] => {
const columns: Column[] = [
{
name: (
<FormattedMessage
id='insights.topReactions.rank'
defaultMessage='Rank'
/>
),
field: 'rank',
className: 'rankCell',
width: 0.2,
},
{
name: (
<FormattedMessage
id='insights.topChannels.channel'
defaultMessage='Channel'
/>
),
field: 'channel',
},
{
name: (
<FormattedMessage
id='insights.topChannels.totalMessages'
defaultMessage='Total messages'
/>
),
field: 'message_count',
},
];
return columns;
}, []);
const getRows = useMemo((): Row[] => {
return topChannels.map((channel, i) => {
const barSize = (channel.message_count / topChannels[0].message_count);
let iconToDisplay = <i className='icon icon-globe'/>;
if (channel.type === Constants.PRIVATE_CHANNEL) {
iconToDisplay = <i className='icon icon-lock-outline'/>;
}
return (
{
cells: {
rank: (
<span className='cell-text'>
{i + 1}
</span>
),
channel: (
<Link
className='channel-display-name'
to={`${currentTeamUrl}/channels/${channel.name}`}
onClick={closeModal}
>
<span className='icon'>
{iconToDisplay}
</span>
<span className='cell-text'>
{channel.display_name}
</span>
</Link>
),
message_count: (
<div className='times-used-container'>
<span className='cell-text'>
{channel.message_count}
</span>
<span
className='horizontal-bar'
style={{
flex: `${barSize} 0`,
}}
/>
</div>
),
},
}
);
});
}, [topChannels]);
return (
<DataGrid
columns={getColumns}
rows={getRows}
loading={loading}
page={0}
nextPage={() => {}}
previousPage={() => {}}
startCount={1}
endCount={10}
total={0}
className={'InsightsTable'}
/>
);
};
export default memo(TopChannelsTable);

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

@@ -1,85 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {trackEvent} from 'actions/telemetry_actions';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {UserProfile} from '@mattermost/types/users';
import {NewMember} from '@mattermost/types/insights';
import {GlobalState} from '@mattermost/types/store';
import {Team} from '@mattermost/types/teams';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import Avatar from 'components/widgets/users/avatar';
import RenderEmoji from 'components/emoji/render_emoji';
import {imageURLForUser} from 'utils/utils';
import './../../../activity_and_insights.scss';
type Props = {
newMember: NewMember;
team: Team;
}
const NewMembersItem = ({newMember, team}: Props) => {
const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
const user = useSelector((state: GlobalState) => getUser(state, newMember.id)) as UserProfile;
let member = newMember;
if (user) {
member = {
...member,
username: user.username,
first_name: user.first_name,
last_name: user.last_name,
nickname: user.nickname,
last_picture_update: user.last_picture_update,
};
}
const trackClick = useCallback(() => {
trackEvent('insights', 'open_new_members_from_new_members_widget');
}, []);
return (
<Link
className='top-dms-item new-members-item'
onClick={trackClick}
to={`/${team.name}/messages/@${member.username}`}
>
<Avatar
url={imageURLForUser(member.id, member.last_picture_update || 0)}
size={'xl'}
/>
<div className='dm-info'>
<div className='dm-name'>
{displayUsername(member as UserProfile, teammateNameDisplaySetting)}
</div>
<span className='dm-role'>{member.position}</span>
<div className='channel-message-count'>
<RenderEmoji
emojiName={'wave'}
size={14}
/>
<div className='say-hello'>
<FormattedMessage
id='insights.newMembers.sayHello'
defaultMessage='Say hello'
/>
</div>
</div>
</div>
</Link>
);
};
export default memo(NewMembersItem);

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

@@ -1,152 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import NewMembersTable from './new_members_table';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getNewTeamMembers: () => ({type: 'adsf',
data: {
has_next: false,
total_count: 2,
items: [
{
id: 'q7bgtp9dtfdwxriz1e8p8zon3e',
first_name: 'Aaron',
last_name: 'Medina',
nickname: 'Centidel',
username: 'aaron.medina',
position: 'Systems Administrator I',
create_at: 1659641095563,
},
{
id: 'gqzxf4ibfiddfy5xitc9gtjykc',
first_name: 'Peter',
last_name: 'Jones',
nickname: '',
username: 'peter.jones',
position: 'Account Executive',
create_at: 1659641095563,
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_dms_and_new_members/new_members_table', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
closeModal: jest.fn(),
offset: 0,
setOffset: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if new members render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<NewMembersTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(2);
});
test('check if 0 new members render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<NewMembersTable
{...props}
filterType={'MY'}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(0);
});
});

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

@@ -1,182 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedDate, FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import classNames from 'classnames';
import {trackEvent} from 'actions/telemetry_actions';
import {getNewTeamMembers} from 'mattermost-redux/actions/insights';
import {NewMember, TimeFrame} from '@mattermost/types/insights';
import {UserProfile} from '@mattermost/types/users';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {InsightsScopes} from 'utils/constants';
import {imageURLForUser} from 'utils/utils';
import Avatar from 'components/widgets/users/avatar';
import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid';
import ModalPagination from 'components/activity_and_insights/insights/modal_pagination/modal_pagination';
import './../../../activity_and_insights.scss';
type Props = {
filterType: string;
timeFrame: TimeFrame;
offset: number;
setOffset: (offset: number) => void;
closeModal: () => void;
}
const NewMembersTable = (props: Props) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [newMembers, setNewMembers] = useState([] as NewMember[]);
const [hasNext, setHasNext] = useState(false);
const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
const currentTeam = useSelector(getCurrentTeam);
const getNewTeamMembersList = useCallback(async () => {
if (props.filterType === InsightsScopes.TEAM) {
setLoading(true);
const data: any = await dispatch(getNewTeamMembers(currentTeam.id, props.offset, 10, props.timeFrame));
if (data.data?.items) {
setNewMembers(data.data.items);
}
// Workaround for null response from API
if (data.data?.items === null) {
setNewMembers([]);
}
if ('has_next' in data.data) {
setHasNext(data.data?.has_next);
}
setLoading(false);
}
}, [props.timeFrame, props.filterType, props.offset]);
useEffect(() => {
getNewTeamMembersList();
}, [getNewTeamMembersList]);
const closeModal = useCallback(() => {
trackEvent('insights', 'open_new_members_from_new_members_modal');
props.closeModal();
}, [props.closeModal]);
const getColumns = useMemo((): Column[] => {
const columns: Column[] = [
{
name: (
<FormattedMessage
id='insights.newMembers.member'
defaultMessage='Team member'
/>
),
field: 'teamMember',
width: 0.4,
},
{
name: (
<FormattedMessage
id='insights.newMembers.position'
defaultMessage='Position'
/>
),
field: 'position',
width: 0.4,
},
{
name: (
<FormattedMessage
id='insights.newMembers.joined'
defaultMessage='Date joined'
/>
),
field: 'joined',
width: 0.2,
},
];
return columns;
}, []);
const getRows = useMemo((): Row[] => {
return newMembers.map((member) => {
return (
{
cells: {
teamMember: (
<Link
className='user-info'
to={`/${currentTeam.name}/messages/@${member.username}`}
onClick={closeModal}
>
<Avatar
url={imageURLForUser(member.id)}
size={'sm'}
/>
<span className='display-name'>{displayUsername(member as UserProfile, teammateNameDisplaySetting)}</span>
</Link>
),
position: (
<span className='role'>
{member.position}
</span>
),
joined: (
<span className='cell-text'>
<FormattedDate
value={new Date(member.create_at)}
day='2-digit'
month='short'
year='numeric'
/>
</span>
),
},
}
);
});
}, [newMembers]);
return (
<>
<DataGrid
columns={getColumns}
rows={getRows}
loading={loading}
page={0}
nextPage={() => {}}
previousPage={() => {}}
startCount={1}
endCount={10}
total={0}
className={classNames('InsightsTable', 'NewMembersTable')}
/>
{
(props.offset !== 0 || newMembers.length === 10) &&
<ModalPagination
hasNext={hasNext}
offset={props.offset}
setOffset={props.setOffset}
/>
}
</>
);
};
export default memo(NewMembersTable);

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

@@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights/top_dms_and_new_members/new_members_total should match snapshot with team 1`] = `
<div
className="new-members-item new-members-info"
>
<span
className="total-count"
>
2
</span>
<div
className="members-info"
>
<span
className="time-range-info"
>
Joined the team in the last 7 days
</span>
<button
className="see-all-button"
onClick={[Function]}
>
See all
<i
className="icon icon-chevron-right"
/>
</button>
</div>
</div>
`;

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

@@ -1,26 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {TimeFrames} from '@mattermost/types/insights';
import NewMembersTotal from './new_members_total';
describe('components/activity_and_insights/insights/top_dms_and_new_members/new_members_total', () => {
const props = {
total: 2,
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
openInsightsModal: jest.fn(),
};
test('should match snapshot with team', () => {
const wrapper = shallow(
<NewMembersTotal
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -1,55 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {TimeFrame, TimeFrames} from '@mattermost/types/insights';
import {localizeMessage} from 'utils/utils';
import './../../../activity_and_insights.scss';
type Props = {
total: number;
timeFrame: TimeFrame;
openInsightsModal: () => void;
}
const NewMembersTotal = ({total, timeFrame, openInsightsModal}: Props) => {
const timeFrameInfo = useCallback(() => {
switch (timeFrame) {
case TimeFrames.INSIGHTS_1_DAY:
return localizeMessage('insights.newMembers.today', 'Joined the team today');
case TimeFrames.INSIGHTS_7_DAYS:
return localizeMessage('insights.newMembers.lastSevenDays', 'Joined the team in the last 7 days');
case TimeFrames.INSIGHTS_28_DAYS:
return localizeMessage('insights.newMembers.lastTwentyEightDays', 'Joined the team in the last 28 days');
default:
return localizeMessage('insights.newMembers.lastSevenDays', 'Joined the team in the last 7 days');
}
}, [timeFrame]);
return (
<div className='new-members-item new-members-info'>
<span className='total-count'>{total}</span>
<div className='members-info'>
<span className='time-range-info'>
{timeFrameInfo()}
</span>
<button
className='see-all-button'
onClick={(e) => {
e.stopPropagation();
openInsightsModal();
}}
>
{localizeMessage('insights.newMembers.seeAll', 'See all')}
<i className='icon icon-chevron-right'/>
</button>
</div>
</div>
);
};
export default memo(NewMembersTotal);

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

@@ -1,148 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopDMsAndNewMembers from './top_dms_and_new_members';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyTopDMs: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
post_count: 17,
outgoing_message_count: 12,
second_participant: {
id: 'q7bgtp9dtfdwxriz1e8p8zon3e',
last_picture_update: 0,
first_name: 'Aaron',
last_name: 'Medina',
nickname: 'Centidel',
username: 'aaron.medina',
position: 'Systems Administrator I',
},
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_dms_and_new_members', () => {
const props = {
filterType: 'MY',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
size: CardSizes.large,
widgetType: InsightsWidgetTypes.TOP_DMS,
class: 'top-dms-card',
timeFrameLabel: 'Last 7 days',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if top dms render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopDMsAndNewMembers
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('a.top-dms-item').length).toEqual(1);
});
test('new members should render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopDMsAndNewMembers
{...props}
filterType='TEAM'
widgetType={InsightsWidgetTypes.NEW_TEAM_MEMBERS}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.text().includes('New team members')).toBe(true);
});
});

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

@@ -1,176 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useEffect, useState, useCallback, useMemo} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {NewMember, TopDM} from '@mattermost/types/insights';
import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components';
import {getMyTopDMs, getNewTeamMembers} from 'mattermost-redux/actions/insights';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {openModal} from 'actions/views/modals';
import {trackEvent} from 'actions/telemetry_actions';
import {InsightsScopes, ModalIdentifiers} from 'utils/constants';
import {localizeMessage} from 'utils/utils';
import widgetHoc, {WidgetHocProps} from '../widget_hoc/widget_hoc';
import WidgetEmptyState from '../widget_empty_state/widget_empty_state';
import InsightsModal from '../insights_modal/insights_modal';
import TopDMsItem from './top_dms_item/top_dms_item';
import NewMembersItem from './new_members_item/new_members_item';
import NewMembersTotal from './new_members_total/new_members_total';
import './../../activity_and_insights.scss';
const TopDMsAndNewMembers = (props: WidgetHocProps) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [topDMs, setTopDMs] = useState([] as TopDM[]);
const [newMembers, setNewMembers] = useState([] as NewMember[]);
const [totalNewMembers, setTotalNewMembers] = useState(0);
const currentTeam = useSelector(getCurrentTeam);
const getMyTopTeamDMs = useCallback(async () => {
if (props.filterType === InsightsScopes.MY) {
setLoading(true);
const data: any = await dispatch(getMyTopDMs(currentTeam.id, 0, 6, props.timeFrame));
if (data.data?.items) {
setTopDMs(data.data.items);
}
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
const getNewTeamMembersList = useCallback(async () => {
if (props.filterType === InsightsScopes.TEAM) {
setLoading(true);
const data: any = await dispatch(getNewTeamMembers(currentTeam.id, 0, 5, props.timeFrame));
if (data.data?.items) {
setNewMembers(data.data.items);
}
// Workaround for null response from API
if (data.data?.items === null) {
setNewMembers([]);
}
if (data.data?.total_count) {
setTotalNewMembers(data.data.total_count);
}
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
useEffect(() => {
getNewTeamMembersList();
}, [getNewTeamMembersList]);
useEffect(() => {
getMyTopTeamDMs();
}, [getMyTopTeamDMs]);
const skeletonLoader = useMemo(() => {
const entries = [];
for (let i = 0; i < 5; i++) {
entries.push(
<div
className='dms-loading-container'
key={i}
>
<CircleSkeletonLoader size={72}/>
<div className='title-line'>
<RectangleSkeletonLoader
height={12}
flex='1'
/>
</div>
<div>
<RectangleSkeletonLoader
height={8}
width={92}
margin='0 0 12px 0'
/>
<RectangleSkeletonLoader
height={8}
width={72}
/>
</div>
</div>,
);
}
return entries;
}, []);
const openInsightsModal = useCallback(() => {
trackEvent('insights', `open_modal_${props.widgetType.toLowerCase()}`);
dispatch(openModal({
modalId: ModalIdentifiers.INSIGHTS,
dialogType: InsightsModal,
dialogProps: {
widgetType: props.widgetType,
title: localizeMessage('insights.newTeamMembers.title', 'New team members'),
subtitle: '',
filterType: props.filterType,
timeFrame: props.timeFrame,
},
}));
}, [props.widgetType, props.filterType, props.timeFrame]);
return (
<div className='top-dms-container'>
{
loading &&
skeletonLoader
}
{
(!loading && topDMs && props.filterType === InsightsScopes.MY) &&
topDMs.map((topDM: TopDM, index: number) => {
const barSize = ((topDM.post_count / topDMs[0].post_count) * 0.75);
return (
<TopDMsItem
key={index}
dm={topDM}
barSize={barSize}
team={currentTeam}
/>
);
})
}
{
(!loading && props.filterType === InsightsScopes.TEAM && newMembers.length > 0) && (
<>
<NewMembersTotal
total={totalNewMembers}
timeFrame={props.timeFrame}
openInsightsModal={openInsightsModal}
/>
{newMembers.map((newMember, index) => {
return (
<NewMembersItem
key={index}
newMember={newMember}
team={currentTeam}
/>
);
})}
</>
)
}
{
(((topDMs.length === 0 && props.filterType === InsightsScopes.MY) || (newMembers.length === 0 && props.filterType === InsightsScopes.TEAM)) && !loading) &&
<WidgetEmptyState
icon={'account-multiple-outline'}
/>
}
</div>
);
};
export default memo(widgetHoc(TopDMsAndNewMembers));

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

@@ -1,96 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {trackEvent} from 'actions/telemetry_actions';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {UserProfile} from '@mattermost/types/users';
import {TopDM} from '@mattermost/types/insights';
import {Team} from '@mattermost/types/teams';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import OverlayTrigger from 'components/overlay_trigger';
import Avatar from 'components/widgets/users/avatar';
import Tooltip from 'components/tooltip';
import {imageURLForUser} from 'utils/utils';
import Constants from 'utils/constants';
import './../../../activity_and_insights.scss';
type Props = {
dm: TopDM;
barSize: number;
team: Team;
}
const TopDMsItem = ({dm, barSize, team}: Props) => {
const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
const tooltip = useCallback((messageCount: number) => {
return (
<Tooltip
id='total-messages'
>
<FormattedMessage
id='insights.topChannels.messageCount'
defaultMessage='{messageCount} total messages'
values={{
messageCount,
}}
/>
</Tooltip>
);
}, []);
const trackClick = useCallback(() => {
trackEvent('insights', 'open_dm_from_top_dms_widget');
}, []);
return (
<Link
className='top-dms-item'
to={`/${team.name}/messages/@${dm.second_participant.username}`}
onClick={trackClick}
>
<Avatar
url={imageURLForUser(dm.second_participant.id, dm.second_participant.last_picture_update)}
size={'xl'}
/>
<div className='dm-info'>
<div
className='dm-name'
>
{displayUsername(dm.second_participant as UserProfile, teammateNameDisplaySetting)}
</div>
<span className='dm-role'>{dm.second_participant.position}</span>
<div className='channel-message-count'>
<OverlayTrigger
trigger={['hover']}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='top'
overlay={tooltip(dm.post_count)}
>
<span className='message-count'>{dm.post_count}</span>
</OverlayTrigger>
<span
className='horizontal-bar'
style={{
flex: `${barSize} 0`,
}}
/>
</div>
</div>
</Link>
);
};
export default memo(TopDMsItem);

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

@@ -1,143 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopDMsTable from './top_dms_table';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyTopDMs: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
post_count: 17,
second_participant: {
id: 'q7bgtp9dtfdwxriz1e8p8zon3e',
last_picture_update: 0,
first_name: 'Aaron',
last_name: 'Medina',
nickname: 'Centidel',
username: 'aaron.medina',
position: 'Systems Administrator I',
},
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_dms_and_new_members', () => {
const props = {
filterType: 'MY',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
closeModal: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if top dms render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopDMsTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(1);
});
test('check if 0 top dms render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopDMsTable
{...props}
filterType={'TEAM'}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(0);
});
});

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

@@ -1,195 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import classNames from 'classnames';
import {trackEvent} from 'actions/telemetry_actions';
import {getMyTopDMs} from 'mattermost-redux/actions/insights';
import {TimeFrame, TopDM} from '@mattermost/types/insights';
import {UserProfile} from '@mattermost/types/users';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {InsightsScopes} from 'utils/constants';
import {imageURLForUser} from 'utils/utils';
import Avatar from 'components/widgets/users/avatar';
import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid';
import './../../../activity_and_insights.scss';
type Props = {
filterType: string;
timeFrame: TimeFrame;
closeModal: () => void;
}
const TopDMsTable = (props: Props) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [topDMs, setTopDMs] = useState([] as TopDM[]);
const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
const currentTeam = useSelector(getCurrentTeam);
const getMyTopTeamDMs = useCallback(async () => {
if (props.filterType === InsightsScopes.MY) {
setLoading(true);
const data: any = await dispatch(getMyTopDMs(currentTeam.id, 0, 10, props.timeFrame));
if (data.data?.items) {
setTopDMs(data.data.items);
}
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
useEffect(() => {
getMyTopTeamDMs();
}, [getMyTopTeamDMs]);
const closeModal = useCallback(() => {
trackEvent('insights', 'open_dm_from_top_dms_modal');
props.closeModal();
}, [props.closeModal]);
const getColumns = useMemo((): Column[] => {
const columns: Column[] = [
{
name: (
<FormattedMessage
id='insights.topReactions.rank'
defaultMessage='Rank'
/>
),
field: 'rank',
className: 'rankCell',
width: 0.05,
},
{
name: (
<FormattedMessage
id='insights.topDMs.user'
defaultMessage='User'
/>
),
field: 'user',
width: 0.4,
},
{
name: (
<FormattedMessage
id='insights.topDMs.sentMessages'
defaultMessage='Sent'
/>
),
field: 'sent',
className: 'message-count',
width: 0.15,
},
{
name: (
<FormattedMessage
id='insights.topDMs.receivedMessages'
defaultMessage='Received'
/>
),
field: 'received',
className: 'message-count',
width: 0.15,
},
{
name: (
<FormattedMessage
id='insights.topDMs.totalMessages'
defaultMessage='Total messages'
/>
),
field: 'total',
width: 0.25,
},
];
return columns;
}, []);
const getRows = useMemo((): Row[] => {
return topDMs.map((dm, i) => {
const barSize = (dm.post_count / topDMs[0].post_count);
return (
{
cells: {
rank: (
<span className='cell-text'>
{i + 1}
</span>
),
user: (
<Link
className='user-info'
to={`/${currentTeam.name}/messages/@${dm.second_participant.username}`}
onClick={closeModal}
>
<Avatar
url={imageURLForUser(dm.second_participant.id, dm.second_participant.last_picture_update)}
size={'sm'}
/>
<span className='display-name'>{displayUsername(dm.second_participant as UserProfile, teammateNameDisplaySetting)}</span>
</Link>
),
sent: (
<>
{dm.outgoing_message_count}
</>
),
received: (
<>
{dm.post_count - dm.outgoing_message_count}
</>
),
total: (
<div className='times-used-container'>
<span className='cell-text'>
{dm.post_count}
</span>
<span
className='horizontal-bar top-dms'
style={{
flex: `${barSize} 0`,
}}
/>
</div>
),
},
}
);
});
}, [topDMs]);
return (
<DataGrid
columns={getColumns}
rows={getRows}
loading={loading}
page={0}
nextPage={() => {}}
previousPage={() => {}}
startCount={1}
endCount={10}
total={0}
className={classNames('InsightsTable', 'TopDMsTable')}
/>
);
};
export default memo(TopDMsTable);

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

@@ -1,161 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopPlaybooks from './top_playbooks';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn().mockReturnValue(() => {}),
}));
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/top_playbooks', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
size: CardSizes.small,
widgetType: InsightsWidgetTypes.TOP_PLAYBOOKS,
class: 'top-playbooks-card',
timeFrameLabel: 'Last 7 days',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
plugins: {
insightsHandlers: {
playbooks: async () => {
return {
items: [
{
playbook_id: '6nrnwj5hfty8787mos9h6dtx6y',
num_runs: 3,
title: 'sth',
last_run_at: 1658827931570,
},
{
playbook_id: '3ii9fwq47pdtupqdd6ajo3axuc',
num_runs: 2,
title: 'Test playbook',
last_run_at: 1659986623440,
},
{
playbook_id: '3ii9fwq47pdtupqdd6ajo3axuc',
num_runs: 1,
title: 'Another playbook',
last_run_at: 1659555849231,
},
],
};
},
},
},
};
test('check if 3 team top playbooks render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopPlaybooks
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('a.playbook-item').length).toEqual(3);
});
test('check if 0 top boards render', async () => {
const state = {
...initialState,
plugins: {
insightsHandlers: {
playbooks: async () => {
return {
items: [],
};
},
},
},
};
const store = await mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopPlaybooks
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.empty-state').length).toEqual(1);
});
});

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

@@ -1,159 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useCallback, useEffect, useMemo, ComponentProps} from 'react';
import {useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import {FormattedMessage} from 'react-intl';
import {TopPlaybook} from '@mattermost/types/insights';
import {RectangleSkeletonLoader} from '@mattermost/components';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {trackEvent} from 'actions/telemetry_actions';
import {GlobalState} from 'types/store';
import Timestamp from 'components/timestamp';
import widgetHoc, {WidgetHocProps} from '../widget_hoc/widget_hoc';
import WidgetEmptyState from '../widget_empty_state/widget_empty_state';
import './../../activity_and_insights.scss';
const TIME_SPEC: Partial<ComponentProps<typeof Timestamp>> = {
units: [
'now',
'minute',
['hour', -48],
['day', -30],
'month',
'year',
],
useTime: false,
day: 'numeric',
style: 'long',
};
const TopPlaybooks = (props: WidgetHocProps) => {
const [loading, setLoading] = useState(false);
const [topPlaybooks, setPlaybooks] = useState([] as TopPlaybook[]);
const currentTeamId = useSelector(getCurrentTeamId);
const playbooksHandler = useSelector((state: GlobalState) => state.plugins.insightsHandlers.playbooks);
const getTopPlaybooks = useCallback(async () => {
setLoading(true);
const data: any = await playbooksHandler(props.timeFrame, 0, 3, currentTeamId, props.filterType);
if (data.items) {
setPlaybooks(data.items);
}
setLoading(false);
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopPlaybooks();
}, [getTopPlaybooks]);
const skeletonLoader = useMemo(() => {
const entries = [];
for (let i = 0; i < 3; i++) {
entries.push(
<div
className='top-playbooks-loading-container'
key={i}
>
<RectangleSkeletonLoader
height={12}
margin='0 0 8px 0'
/>
<RectangleSkeletonLoader
height={8}
margin='0 0 8px 0'
width='80%'
/>
</div>,
);
}
return entries;
}, []);
const trackClickEvent = useCallback(() => {
trackEvent('insights', 'open_playbook_from_top_playbooks_widget');
}, []);
return (
<div className='top-playbooks-container'>
{
loading &&
skeletonLoader
}
{
(topPlaybooks && !loading) &&
<div className='playbooks-list'>
{
topPlaybooks.map((playbook, key) => {
const barSize = (playbook.num_runs / topPlaybooks[0].num_runs) * 100;
return (
<Link
className='playbook-item'
onClick={trackClickEvent}
to={`/playbooks/playbooks/${playbook.playbook_id}`}
key={key}
>
<div className='display-info'>
<span className='display-name'>{playbook.title}</span>
<span className='last-run-time'>
<FormattedMessage
id='insights.topPlaybooks.lastRun'
defaultMessage='Last run: {relativeTime}'
values={{
relativeTime: (
<Timestamp
value={playbook.last_run_at}
{...TIME_SPEC}
/>
),
}}
/>
</span>
</div>
<div className='display-info run-info'>
<span
className='horizontal-bar'
style={{
width: `${barSize}%`,
}}
/>
<span className='last-run-time'>
<FormattedMessage
id='insights.topPlaybooks.totalRuns'
defaultMessage={'{total} runs'}
values={{
total: playbook.num_runs,
}}
/>
</span>
</div>
</Link>
);
})
}
</div>
}
{
(topPlaybooks.length === 0 && !loading) &&
<WidgetEmptyState
icon={'product-playbooks'}
/>
}
</div>
);
};
export default memo(widgetHoc(TopPlaybooks));

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

@@ -1,153 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopPlaybooksTable from './top_playbooks_table';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/top_playbooks_table', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
closeModal: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
plugins: {
insightsHandlers: {
playbooks: async () => {
return {
items: [
{
playbook_id: '6nrnwj5hfty8787mos9h6dtx6y',
num_runs: 3,
title: 'sth',
last_run_at: 1658827931570,
},
{
playbook_id: '3ii9fwq47pdtupqdd6ajo3axuc',
num_runs: 2,
title: 'Test playbook',
last_run_at: 1659986623440,
},
{
playbook_id: '3ii9fwq47pdtupqdd6ajo3axuc',
num_runs: 1,
title: 'Another playbook',
last_run_at: 1659555849231,
},
],
};
},
},
},
};
test('check if 3 team top boards render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopPlaybooksTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(3);
});
test('check if 0 top boards render', async () => {
const state = {
...initialState,
plugins: {
insightsHandlers: {
playbooks: async () => {
return {
items: [],
};
},
},
},
};
const store = await mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopPlaybooksTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(0);
});
});

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

@@ -1,185 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useMemo, useEffect, useCallback, ComponentProps} from 'react';
import {useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import {useHistory} from 'react-router-dom';
import classNames from 'classnames';
import {trackEvent} from 'actions/telemetry_actions';
import {GlobalState} from 'types/store';
import {TimeFrame, TopPlaybook} from '@mattermost/types/insights';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid';
import Timestamp from 'components/timestamp';
import './../../../activity_and_insights.scss';
type Props = {
filterType: string;
timeFrame: TimeFrame;
closeModal: () => void;
}
const TIME_SPEC: Partial<ComponentProps<typeof Timestamp>> = {
units: [
'now',
'minute',
['hour', -48],
['day', -30],
'month',
'year',
],
useTime: false,
day: 'numeric',
style: 'long',
};
const TopPlaybooksTable = (props: Props) => {
const history = useHistory();
const [loading, setLoading] = useState(false);
const [topPlaybooks, setTopPlaybooks] = useState([] as TopPlaybook[]);
const currentTeamId = useSelector(getCurrentTeamId);
const playbooksHandler = useSelector((state: GlobalState) => state.plugins.insightsHandlers.playbooks);
const getTopPlaybooks = useCallback(async () => {
setLoading(true);
const data: any = await playbooksHandler(props.timeFrame, 0, 10, currentTeamId, props.filterType);
if (data.items) {
setTopPlaybooks(data.items);
}
setLoading(false);
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopPlaybooks();
}, [getTopPlaybooks]);
const goToPlaybook = useCallback((playbook: TopPlaybook) => {
props.closeModal();
trackEvent('insights', 'open_playbook_from_top_playbooks_modal');
history.push(`/playbooks/playbooks/${playbook.playbook_id}`);
}, [props.closeModal]);
const getColumns = useMemo((): Column[] => {
const columns: Column[] = [
{
name: (
<FormattedMessage
id='insights.topReactions.rank'
defaultMessage='Rank'
/>
),
field: 'rank',
className: 'rankCell',
width: 0.07,
},
{
name: (
<FormattedMessage
id='insights.topPlaybooksTable.playbook'
defaultMessage='Playbook'
/>
),
field: 'playbook',
width: 0.4,
},
{
name: (
<FormattedMessage
id='insights.topPlaybooksTable.updates'
defaultMessage='Last run'
/>
),
field: 'lastRun',
width: 0.23,
},
{
name: (
<FormattedMessage
id='insights.topPlaybooksTable.participants'
defaultMessage='Total runs'
/>
),
field: 'totalRuns',
width: 0.3,
},
];
return columns;
}, []);
const getRows = useMemo((): Row[] => {
return topPlaybooks.map((playbook, i) => {
const barSize = (playbook.num_runs / topPlaybooks[0].num_runs);
return (
{
cells: {
rank: (
<span className='cell-text'>
{i + 1}
</span>
),
playbook: (
<div className='channel-display-name'>
<span className='cell-text'>
{playbook.title}
</span>
</div>
),
lastRun: (
<>
<Timestamp
value={playbook.last_run_at}
{...TIME_SPEC}
/>
</>
),
totalRuns: (
<div className='times-used-container'>
<span className='cell-text'>
{playbook.num_runs}
</span>
<span
className='horizontal-bar'
style={{
flex: `${barSize} 0`,
}}
/>
</div>
),
},
onClick: () => {
goToPlaybook(playbook);
},
}
);
});
}, [topPlaybooks]);
return (
<DataGrid
columns={getColumns}
rows={getRows}
loading={loading}
page={0}
nextPage={() => {}}
previousPage={() => {}}
startCount={1}
endCount={10}
total={0}
className={classNames('InsightsTable', 'TopPlaybooksTable')}
/>
);
};
export default memo(TopPlaybooksTable);

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

@@ -1,141 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopReactions from './top_reactions';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/top_reactions', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
size: CardSizes.small,
widgetType: InsightsWidgetTypes.TOP_REACTIONS,
class: 'top-reactions-card',
timeFrameLabel: 'Last 7 days',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
general: {
config: {},
},
emojis: {
customEmoji: {},
},
users: {
currentUserId: 'current_user_id',
},
insights: {
myTopReactions: {
team_id1: {
today: {},
'7_day': {
grinning: {
emoji_name: 'grinning',
count: 190,
},
tada: {
emoji_name: 'tada',
count: 180,
},
heart: {
emoji_name: 'heart',
count: 110,
},
laughing: {
emoji_name: 'laughing',
count: 80,
},
},
'28_day': {},
},
},
topReactions: {
team_id1: {
today: {},
'7_day': {
grinning: {
emoji_name: 'grinning',
count: 145,
},
tada: {
emoji_name: 'tada',
count: 100,
},
},
'28_day': {},
},
},
},
},
};
test('check if empty', async () => {
const store = await mockStore({
entities: {
teams: {
currentTeamId: 'team_id1',
},
users: {
currentUserId: 'current_user_id',
},
insights: {
topReactions: {},
myTopReactions: {},
},
},
});
const wrapper = mountWithIntl(
<Provider store={store}>
<TopReactions
{...props}
/>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.empty-state').length).toEqual(1);
});
test('check if bar chart renders', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<TopReactions
{...props}
/>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.bar-chart-entry').length).toEqual(2);
});
});

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

@@ -1,113 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useEffect, useState, useCallback, useMemo} from 'react';
import {shallowEqual, useDispatch, useSelector} from 'react-redux';
import {GlobalState} from '@mattermost/types/store';
import {TopReaction} from '@mattermost/types/insights';
import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components';
import {getTopReactionsForTeam, getMyTopReactions} from 'mattermost-redux/actions/insights';
import {getTopReactionsForCurrentTeam, getMyTopReactionsForCurrentTeam} from 'mattermost-redux/selectors/entities/insights';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {loadCustomEmojisIfNeeded} from 'actions/emoji_actions';
import {InsightsScopes} from 'utils/constants';
import widgetHoc, {WidgetHocProps} from '../widget_hoc/widget_hoc';
import WidgetEmptyState from '../widget_empty_state/widget_empty_state';
import TopReactionsBarChart from './top_reactions_bar_chart/top_reactions_bar_chart';
import './../../activity_and_insights.scss';
const TopReactions = (props: WidgetHocProps) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const [topReactions, setTopReactions] = useState([] as TopReaction[]);
const teamTopReactions = useSelector((state: GlobalState) => getTopReactionsForCurrentTeam(state, props.timeFrame, 5), shallowEqual);
const myTopReactions = useSelector((state: GlobalState) => getMyTopReactionsForCurrentTeam(state, props.timeFrame, 5), shallowEqual);
useEffect(() => {
const reactions = props.filterType === InsightsScopes.TEAM ? teamTopReactions : myTopReactions;
setTopReactions(reactions);
dispatch(loadCustomEmojisIfNeeded(reactions.map((reaction) => reaction.emoji_name)));
}, [props.filterType, teamTopReactions, myTopReactions]);
const currentTeamId = useSelector(getCurrentTeamId);
const getTopTeamReactions = useCallback(async () => {
if (props.filterType === InsightsScopes.TEAM) {
setLoading(true);
await dispatch(getTopReactionsForTeam(currentTeamId, 0, 10, props.timeFrame));
setLoading(false);
}
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopTeamReactions();
}, [getTopTeamReactions]);
const getMyTeamReactions = useCallback(async () => {
if (props.filterType === InsightsScopes.MY) {
setLoading(true);
await dispatch(getMyTopReactions(currentTeamId, 0, 10, props.timeFrame));
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
useEffect(() => {
getMyTeamReactions();
}, [getMyTeamReactions]);
const skeletonLoader = useMemo(() => {
const barChartHeights = [140, 178, 140, 120, 140];
const entries = [];
for (let i = 0; i < 5; i++) {
entries.push(
<div
className='bar-chart-entry'
key={i}
>
<RectangleSkeletonLoader
width={8}
height={barChartHeights[i]}
borderRadius={6}
margin='0 0 6px 0'
/>
<CircleSkeletonLoader size={20}/>
</div>,
);
}
return entries;
}, []);
return (
<div className='top-reaction-container'>
{
loading &&
skeletonLoader
}
{
(topReactions && !loading) &&
<TopReactionsBarChart
reactions={topReactions}
/>
}
{
(topReactions.length === 0 && !loading) &&
<WidgetEmptyState
icon={'emoticon-outline'}
/>
}
</div>
);
};
export default memo(widgetHoc(TopReactions));

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

@@ -1,151 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights/top_reactions/top_reactions_bar_chart should match snapshot with team 1`] = `
<Fragment>
<div
className="bar-chart-entry"
key="grinning"
>
<span
className="reaction-count"
>
190
</span>
<div
className="bar-chart-data"
style={
Object {
"height": "156px",
}
}
/>
<SimpleTooltip
content="grinning"
placement="bottom"
>
<span>
<Memo(RenderEmoji)
emojiName="grinning"
size={20}
/>
</span>
</SimpleTooltip>
</div>
<div
className="bar-chart-entry"
key="tada"
>
<span
className="reaction-count"
>
180
</span>
<div
className="bar-chart-data"
style={
Object {
"height": "147.78947368421052px",
}
}
/>
<SimpleTooltip
content="tada"
placement="bottom"
>
<span>
<Memo(RenderEmoji)
emojiName="tada"
size={20}
/>
</span>
</SimpleTooltip>
</div>
<div
className="bar-chart-entry"
key="heart"
>
<span
className="reaction-count"
>
110
</span>
<div
className="bar-chart-data"
style={
Object {
"height": "90.31578947368422px",
}
}
/>
<SimpleTooltip
content="heart"
placement="bottom"
>
<span>
<Memo(RenderEmoji)
emojiName="heart"
size={20}
/>
</span>
</SimpleTooltip>
</div>
<div
className="bar-chart-entry"
key="laughing"
>
<span
className="reaction-count"
>
80
</span>
<div
className="bar-chart-data"
style={
Object {
"height": "65.68421052631578px",
}
}
/>
<SimpleTooltip
content="laughing"
placement="bottom"
>
<span>
<Memo(RenderEmoji)
emojiName="laughing"
size={20}
/>
</span>
</SimpleTooltip>
</div>
<div
className="bar-chart-entry"
key="+1"
>
<span
className="reaction-count"
>
40
</span>
<div
className="bar-chart-data"
style={
Object {
"height": "32.84210526315789px",
}
}
/>
<SimpleTooltip
content="+1"
placement="bottom"
>
<span>
<Memo(RenderEmoji)
emojiName="+1"
size={20}
/>
</span>
</SimpleTooltip>
</div>
</Fragment>
`;

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

@@ -1,44 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import TopReactionsBarChart from './top_reactions_bar_chart';
describe('components/activity_and_insights/insights/top_reactions/top_reactions_bar_chart', () => {
const props = {
reactions: [
{
emoji_name: 'grinning',
count: 190,
},
{
emoji_name: 'tada',
count: 180,
},
{
emoji_name: 'heart',
count: 110,
},
{
emoji_name: 'laughing',
count: 80,
},
{
emoji_name: '+1',
count: 40,
},
],
};
test('should match snapshot with team', () => {
const wrapper = shallow(
<TopReactionsBarChart
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -1,64 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {TopReaction} from '@mattermost/types/insights';
import RenderEmoji from 'components/emoji/render_emoji';
import SimpleTooltip from 'components/widgets/simple_tooltip';
type Props = {
reactions: TopReaction[];
}
const TopReactionsBarChart = (props: Props) => {
const barChartEntries = useCallback(() => {
const reactions = props.reactions.map((reaction) => {
const highestCount = props.reactions[0].count;
const maxHeight = 156;
let barHeight = (reaction.count / highestCount) * maxHeight;
if (highestCount === reaction.count) {
barHeight = maxHeight;
}
return (
<div
className='bar-chart-entry'
key={reaction.emoji_name}
>
<span className='reaction-count'>{reaction.count}</span>
<div
className='bar-chart-data'
style={{
height: `${barHeight}px`,
}}
/>
<SimpleTooltip
content={reaction.emoji_name}
placement='bottom'
>
<span>
<RenderEmoji
emojiName={reaction.emoji_name}
size={20}
/>
</span>
</SimpleTooltip>
</div>
);
});
return reactions;
}, [props.reactions]);
return (
<>
{barChartEntries()}
</>
);
};
export default memo(TopReactionsBarChart);

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

@@ -1,135 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopReactionsTable from './top_reactions_table';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/top_reactions/top_reactions_table', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
};
const initialState = {
entities: {
general: {config: {}},
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
users: {
currentUserId: 'current_user_id',
},
insights: {
myTopReactions: {
team_id1: {
today: {},
'7_day': {
grinning: {
emoji_name: 'grinning',
count: 190,
},
tada: {
emoji_name: 'tada',
count: 180,
},
heart: {
emoji_name: 'heart',
count: 110,
},
laughing: {
emoji_name: 'laughing',
count: 80,
},
},
'28_day': {},
},
},
topReactions: {
team_id1: {
today: {},
'7_day': {
grinning: {
emoji_name: 'grinning',
count: 145,
},
tada: {
emoji_name: 'tada',
count: 100,
},
},
'28_day': {},
},
},
},
},
};
test('should be 2 rows for team reactions', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<TopReactionsTable
{...props}
/>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(2);
});
test('should be 4 rows for my reactions', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<TopReactionsTable
{...props}
filterType={'MY'}
/>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(4);
});
test('should be 0 rows for my reactions today', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<TopReactionsTable
{...props}
filterType={'MY'}
timeFrame={TimeFrames.INSIGHTS_1_DAY}
/>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(0);
});
});

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

@@ -1,162 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {shallowEqual, useDispatch, useSelector} from 'react-redux';
import {TimeFrame} from '@mattermost/types/insights';
import {loadCustomEmojisIfNeeded} from 'actions/emoji_actions';
import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid';
import RenderEmoji from 'components/emoji/render_emoji';
import {InsightsScopes} from 'utils/constants';
import {GlobalState} from '@mattermost/types/store';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getMyTopReactionsForCurrentTeam, getTopReactionsForCurrentTeam} from 'mattermost-redux/selectors/entities/insights';
import {getTopReactionsForTeam, getMyTopReactions} from 'mattermost-redux/actions/insights';
import './../../../activity_and_insights.scss';
type Props = {
filterType: string;
timeFrame: TimeFrame;
}
const TopReactionsTable = (props: Props) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const teamTopReactions = useSelector((state: GlobalState) => getTopReactionsForCurrentTeam(state, props.timeFrame, 10), shallowEqual);
const myTopReactions = useSelector((state: GlobalState) => getMyTopReactionsForCurrentTeam(state, props.timeFrame, 10), shallowEqual);
const currentTeamId = useSelector(getCurrentTeamId);
const getTopTeamReactions = useCallback(async () => {
if (props.filterType === InsightsScopes.TEAM) {
setLoading(true);
await dispatch(getTopReactionsForTeam(currentTeamId, 0, 10, props.timeFrame));
setLoading(false);
}
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopTeamReactions();
}, [getTopTeamReactions]);
const getMyTeamReactions = useCallback(async () => {
if (props.filterType === InsightsScopes.MY) {
setLoading(true);
await dispatch(getMyTopReactions(currentTeamId, 0, 10, props.timeFrame));
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
useEffect(() => {
getMyTeamReactions();
}, [getMyTeamReactions]);
useEffect(() => {
const reactions = props.filterType === InsightsScopes.TEAM ? teamTopReactions : myTopReactions;
dispatch(loadCustomEmojisIfNeeded(reactions.map((reaction) => reaction.emoji_name)));
}, [props.filterType, teamTopReactions, myTopReactions]);
const getColumns = useMemo((): Column[] => {
const columns: Column[] = [
{
name: (
<FormattedMessage
id='insights.topReactions.rank'
defaultMessage='Rank'
/>
),
field: 'rank',
className: 'rankCell',
width: 0.2,
},
{
name: (
<FormattedMessage
id='insights.topReactions.reaction'
defaultMessage='Reaction'
/>
),
field: 'reaction',
},
{
name: (
<FormattedMessage
id='insights.topReactions.timesUsed'
defaultMessage='Times used'
/>
),
field: 'times_used',
},
];
return columns;
}, []);
const getRows = useMemo((): Row[] => {
const topReactions = props.filterType === InsightsScopes.TEAM ? teamTopReactions : myTopReactions;
return topReactions.map((reaction, i) => {
const barSize = (reaction.count / topReactions[0].count);
return (
{
cells: {
rank: (
<span className='cell-text'>
{i + 1}
</span>
),
reaction: (
<>
<RenderEmoji
emojiName={reaction.emoji_name}
size={24}
/>
<span className='cell-text'>
{reaction.emoji_name}
</span>
</>
),
times_used: (
<div className='times-used-container'>
<span className='cell-text'>
{reaction.count}
</span>
<span
className='horizontal-bar'
style={{
flex: `${barSize} 0`,
}}
/>
</div>
),
},
}
);
});
}, [teamTopReactions, myTopReactions]);
return (
<DataGrid
columns={getColumns}
rows={getRows}
loading={loading}
page={0}
nextPage={() => {}}
previousPage={() => {}}
startCount={1}
endCount={10}
total={0}
className={'InsightsTable'}
/>
);
};
export default memo(TopReactionsTable);

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

@@ -1,82 +0,0 @@
.InsightsTable,
.top-thread-container {
.thread-details {
display: flex;
align-items: center;
margin-bottom: 4px;
gap: 8px;
.display-name {
overflow: hidden;
color: var(--center-channel-color);
font-family: 'Open Sans', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.reply-count {
flex: 1;
margin-left: auto;
text-align: end;
i.icon {
width: 12px;
height: 12px;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-size: 12px;
}
span {
color: rgba(var(--center-channel-color-rgb), 0.64);
font-family: 'Open Sans', sans-serif;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 16px;
}
}
}
.preview {
display: -webkit-box;
overflow: hidden;
height: 32px;
max-height: 32px;
margin: 0;
-webkit-box-orient: vertical;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
-webkit-line-clamp: 2;
line-height: 16px;
.compliance-information {
font-style: italic;
}
.markdown__heading {
margin: inherit;
font-size: inherit;
line-height: inherit;
}
.file_card {
width: auto;
height: 32px;
margin: 0;
img.file_card__image {
min-width: 24px;
max-width: 24px;
min-height: 24px;
max-height: 24px;
}
}
}
}

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

@@ -1,174 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopThreads from './top_threads';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyTopThreads: () => ({type: 'adsf', data: {}}),
getTopThreadsForTeam: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
post_id: 'post1',
reply_count: 18,
channel_id: 'channel1',
channel_display_name: 'nostrum',
channel_name: 'channel1',
message: 'ducimus sed aut sunt corrupti necessitatibus quasi.\nreiciendis ipsa consequuntur fugiat a eaque.',
participants: [
'user1',
],
user_id: 'user1',
user_information: {
id: 'user1',
last_picture_update: 0,
first_name: 'Kathryn',
last_name: 'Mills',
},
post: {
id: 'post1',
create_at: 1653488972484,
update_at: 1653489070820,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'user1',
channel_id: 'channel1',
root_id: '',
original_id: '',
message: 'ducimus sed aut sunt corrupti necessitatibus quasi.\nreiciendis ipsa consequuntur fugiat a eaque.',
type: '',
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 18,
last_reply_at: 0,
participants: null,
metadata: {},
},
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_threads', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
size: CardSizes.small,
widgetType: InsightsWidgetTypes.TOP_THREADS,
class: 'top-threads-card',
timeFrameLabel: 'Last 7 days',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if 1 team top threads render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopThreads
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.thread-item').length).toEqual(1);
});
test('check if 0 my top channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopThreads
{...props}
filterType={'MY'}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.empty-state').length).toEqual(1);
});
});

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

@@ -1,126 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useEffect, useState, useCallback, useMemo} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {TopThread} from '@mattermost/types/insights';
import {GlobalState} from '@mattermost/types/store';
import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components';
import {getMyTopThreads, getTopThreadsForTeam} from 'mattermost-redux/actions/insights';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {InsightsScopes} from 'utils/constants';
import widgetHoc, {WidgetHocProps} from '../widget_hoc/widget_hoc';
import WidgetEmptyState from '../widget_empty_state/widget_empty_state';
import TopThreadsItem from './top_threads_item/top_threads_item';
import './../../activity_and_insights.scss';
import './top_threads.scss';
const TopThreads = (props: WidgetHocProps) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const [topThreads, setTopThreads] = useState([] as TopThread[]);
const currentTeamId = useSelector(getCurrentTeamId);
const complianceExportEnabled = useSelector((state: GlobalState) => state.entities.general.config.EnableComplianceExport);
const getTopTeamThreads = useCallback(async () => {
if (props.filterType === InsightsScopes.TEAM) {
setLoading(true);
const data: any = await dispatch(getTopThreadsForTeam(currentTeamId, 0, 3, props.timeFrame));
if (data.data?.items) {
setTopThreads(data.data.items);
}
setLoading(false);
}
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopTeamThreads();
}, [getTopTeamThreads]);
const getMyTeamThreads = useCallback(async () => {
if (props.filterType === InsightsScopes.MY) {
setLoading(true);
const data: any = await dispatch(getMyTopThreads(currentTeamId, 0, 3, props.timeFrame));
if (data.data?.items) {
setTopThreads(data.data.items);
}
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
useEffect(() => {
getMyTeamThreads();
}, [getMyTeamThreads]);
const skeletonLoader = useMemo(() => {
const entries = [];
for (let i = 0; i < 3; i++) {
entries.push(
<div
className='top-thread-loading-container'
key={i}
>
<div className='top-thread-loading-row'>
<CircleSkeletonLoader size={20}/>
<RectangleSkeletonLoader
height={12}
margin='0 0 0 8px'
flex='0.5'
/>
</div>
<div>
<RectangleSkeletonLoader
height={8}
margin='0 0 8px 0'
/>
<RectangleSkeletonLoader height={8}/>
</div>
</div>,
);
}
return entries;
}, []);
return (
<div className='top-thread-container'>
{
loading &&
skeletonLoader
}
{
(topThreads && !loading) &&
<div className='thread-list'>
{
topThreads.map((thread, key) => {
return (
<TopThreadsItem
thread={thread}
key={key}
complianceExportEnabled={complianceExportEnabled}
/>
);
})
}
</div>
}
{
(topThreads.length === 0 && !loading) &&
<WidgetEmptyState
icon={'message-text-outline'}
/>
}
</div>
);
};
export default memo(widgetHoc(TopThreads));

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

@@ -1,237 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {PostType} from '@mattermost/types/posts';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopThreadsItem from './top_threads_item';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/top_threads/top_threads_item', () => {
const props = {
thread: {
channel_id: 'channel1',
channel_display_name: 'nostrum',
channel_name: 'channel1',
participants: [
'user1',
],
user_information: {
id: 'user1',
last_picture_update: 0,
first_name: 'Kathryn',
last_name: 'Mills',
},
post: {
id: 'post1',
create_at: 1653488972484,
update_at: 1653489070820,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'user1',
channel_id: 'channel1',
root_id: '',
original_id: '',
message: 'ducimus sed aut sunt corrupti necessitatibus quasi.\nreiciendis ipsa consequuntur fugiat a eaque.',
type: '' as PostType,
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 18,
last_reply_at: 0,
participants: null,
metadata: {
embeds: [],
emojis: [],
files: [],
images: {},
reactions: [],
},
},
},
complianceExportEnabled: 'false',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {},
},
general: {
config: {},
license: {
Compliance: 'true',
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if thread item renders', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopThreadsItem
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.thread-item').length).toEqual(1);
});
test('check compliance preview does not render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopThreadsItem
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.text().includes('You\'ll need to join the nostrum channel to see this thread.')).toBe(false);
});
test('check if compliance preview renders', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopThreadsItem
{...props}
complianceExportEnabled={'true'}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.text().includes('You\'ll need to join the nostrum channel to see this thread.')).toBe(true);
});
test('check if compliance preview does not render when not licensed for it', async () => {
const store = await mockStore({
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {},
},
general: {
config: {},
license: {
Compliance: 'false',
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
});
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopThreadsItem
{...props}
complianceExportEnabled={'true'}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.text().includes('You\'ll need to join the nostrum channel to see this thread.')).toBe(false);
});
});

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

@@ -1,126 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import Tag from 'components/widgets/tag/tag';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {TopThread} from '@mattermost/types/insights';
import {UserProfile} from '@mattermost/types/users';
import {selectPostAndParentChannel} from 'actions/views/rhs';
import {trackEvent} from 'actions/telemetry_actions';
import {openModal} from 'actions/views/modals';
import Avatar from 'components/widgets/users/avatar';
import Markdown from 'components/markdown';
import Attachment from 'components/threading/global_threads/thread_item/attachments';
import {ModalIdentifiers} from 'utils/constants';
import {imageURLForUser} from 'utils/utils';
import JoinChannelModal from '../../join_channel_modal/join_channel_modal';
import {getMyChannelMembership} from 'mattermost-redux/selectors/entities/channels';
import {GlobalState} from '@mattermost/types/store';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
type Props = {
thread: TopThread;
complianceExportEnabled?: string;
}
const TopThreadsItem = ({thread, complianceExportEnabled}: Props) => {
const dispatch = useDispatch();
const isChannelMember = useSelector((state: GlobalState) => getMyChannelMembership(state, thread.channel_id));
const currentTeamId = useSelector(getCurrentTeamId);
const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
const license = useSelector(getLicense);
const openRHSOrJoinChannel = useCallback(() => {
if (!isChannelMember && (license.Compliance === 'true' && complianceExportEnabled === 'true')) {
dispatch(openModal({
modalId: ModalIdentifiers.INSIGHTS,
dialogType: JoinChannelModal,
dialogProps: {
thread,
currentTeamId,
},
}));
} else {
dispatch(selectPostAndParentChannel(thread.post));
}
}, [thread, isChannelMember, currentTeamId]);
const getPreview = useCallback(() => {
if (!isChannelMember && (license.Compliance === 'true' && complianceExportEnabled === 'true')) {
return (
<span className='compliance-information'>
<FormattedMessage
id='insights.topThreadItem.notChannelMember'
defaultMessage={'You\'ll need to join the {channel} channel to see this thread.'}
values={{
channel: <strong>{thread.channel_display_name}</strong>,
}}
/>
</span>
);
}
if (thread.post.message) {
return (
<Markdown
message={thread.post.message}
options={{
singleline: true,
mentionHighlight: false,
atMentions: false,
}}
imagesMetadata={thread.post?.metadata && thread.post?.metadata?.images}
/>
);
}
return (
<Attachment post={thread.post}/>
);
}, [thread, isChannelMember]);
return (
<div
className='thread-item'
onClick={() => {
trackEvent('insights', 'open_thread_from_top_threads_widget');
openRHSOrJoinChannel();
}}
key={thread.post.id}
>
<div className='thread-details'>
<Avatar
url={imageURLForUser(thread.user_information.id)}
size={'xs'}
/>
<span className='display-name'>{displayUsername(thread.user_information as UserProfile, teammateNameDisplaySetting)}</span>
<Tag text={thread.channel_display_name}/>
<div className='reply-count'>
<i className='icon icon-reply-outline'/>
<span>{thread.post.reply_count}</span>
</div>
</div>
<div
className='preview'
>
{getPreview()}
</div>
</div>
);
};
export default memo(TopThreadsItem);

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

@@ -1,174 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import TopThreadsTable from './top_threads_table';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyTopThreads: () => ({type: 'adsf', data: {}}),
getTopThreadsForTeam: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
post_id: 'post1',
reply_count: 18,
channel_id: 'channel1',
channel_display_name: 'nostrum',
channel_name: 'channel1',
message: 'ducimus sed aut sunt corrupti necessitatibus quasi.\nreiciendis ipsa consequuntur fugiat a eaque.',
participants: [
'user1',
],
user_id: 'user1',
user_information: {
id: 'user1',
last_picture_update: 0,
first_name: 'Kathryn',
last_name: 'Mills',
},
post: {
id: 'post1',
create_at: 1653488972484,
update_at: 1653489070820,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'user1',
channel_id: 'channel1',
root_id: '',
original_id: '',
message: 'ducimus sed aut sunt corrupti necessitatibus quasi.\nreiciendis ipsa consequuntur fugiat a eaque.',
type: '',
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 18,
last_reply_at: 0,
participants: null,
metadata: {},
},
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_threads/top_threads_table', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
closeModal: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
},
general: {
config: {},
license: {
Compliance: 'true',
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if 1 team top thread renders', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopThreadsTable
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(1);
});
test('check if 0 my top threads render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<TopThreadsTable
{...props}
filterType={'MY'}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.DataGrid_row').length).toEqual(0);
});
});

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

@@ -1,272 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import classNames from 'classnames';
import Tag from 'components/widgets/tag/tag';
import {selectPostAndParentChannel} from 'actions/views/rhs';
import {trackEvent} from 'actions/telemetry_actions';
import {openModal} from 'actions/views/modals';
import {getMyTopThreads as fetchMyTopThreads, getTopThreadsForTeam} from 'mattermost-redux/actions/insights';
import {TimeFrame, TopThread} from '@mattermost/types/insights';
import {UserProfile} from '@mattermost/types/users';
import {GlobalState} from '@mattermost/types/store';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {InsightsScopes, ModalIdentifiers} from 'utils/constants';
import {imageURLForUser} from 'utils/utils';
import Avatar from 'components/widgets/users/avatar';
import Avatars from 'components/widgets/users/avatars';
import Markdown from 'components/markdown';
import Attachment from 'components/threading/global_threads/thread_item/attachments';
import DataGrid, {Row, Column} from 'components/admin_console/data_grid/data_grid';
import JoinChannelModal from '../../join_channel_modal/join_channel_modal';
import './../../../activity_and_insights.scss';
import '../top_threads.scss';
type Props = {
filterType: string;
timeFrame: TimeFrame;
closeModal: () => void;
}
const TopThreadsTable = (props: Props) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const [topThreads, setTopThreads] = useState([] as TopThread[]);
const currentTeamId = useSelector(getCurrentTeamId);
const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
const myChannelMemberships = useSelector(getMyChannelMemberships);
const complianceExportEnabled = useSelector((state: GlobalState) => state.entities.general.config.EnableComplianceExport);
const license = useSelector(getLicense);
const getTopTeamThreads = useCallback(async () => {
if (props.filterType === InsightsScopes.TEAM) {
setLoading(true);
const data: any = await dispatch(getTopThreadsForTeam(currentTeamId, 0, 5, props.timeFrame));
if (data.data && data.data.items) {
setTopThreads(data.data.items);
}
setLoading(false);
}
}, [props.timeFrame, currentTeamId, props.filterType]);
useEffect(() => {
getTopTeamThreads();
}, [getTopTeamThreads]);
const getMyTopThreads = useCallback(async () => {
if (props.filterType === InsightsScopes.MY) {
setLoading(true);
const data: any = await dispatch(fetchMyTopThreads(currentTeamId, 0, 5, props.timeFrame));
if (data.data && data.data.items) {
setTopThreads(data.data.items);
}
setLoading(false);
}
}, [props.timeFrame, props.filterType]);
useEffect(() => {
getMyTopThreads();
}, [getMyTopThreads]);
const imageProps = useMemo(() => ({
onImageHeightChanged: () => {},
onImageLoaded: () => {},
}), []);
const closeModal = useCallback(() => {
props.closeModal();
}, [props.closeModal]);
const openRHSOrJoinChannel = useCallback((thread: TopThread, isChannelMember: boolean) => {
if (!isChannelMember && (license.Compliance === 'true' && complianceExportEnabled === 'true')) {
dispatch(openModal({
modalId: ModalIdentifiers.INSIGHTS,
dialogType: JoinChannelModal,
dialogProps: {
thread,
currentTeamId,
},
}));
} else {
trackEvent('insights', 'open_thread_from_top_threads_modal');
dispatch(selectPostAndParentChannel(thread.post));
}
closeModal();
}, [currentTeamId]);
const getPreview = useCallback((thread: TopThread, isChannelMember: boolean) => {
if (!isChannelMember && (license.Compliance === 'true' && complianceExportEnabled === 'true')) {
return (
<span className='compliance-information'>
<FormattedMessage
id='insights.topThreadItem.notChannelMember'
defaultMessage={'You\'ll need to join the {channel} channel to see this thread.'}
values={{
channel: <strong>{thread.channel_display_name}</strong>,
}}
/>
</span>
);
}
if (thread.post.message) {
return (
<Markdown
message={thread.post.message}
options={{
singleline: true,
mentionHighlight: false,
atMentions: false,
}}
imagesMetadata={thread.post?.metadata && thread.post?.metadata?.images}
imageProps={imageProps}
/>
);
}
return (
<Attachment post={thread.post}/>
);
}, []);
const getColumns = useMemo((): Column[] => {
const columns: Column[] = [
{
name: (
<FormattedMessage
id='insights.topReactions.rank'
defaultMessage='Rank'
/>
),
field: 'rank',
className: 'rankCell',
width: 0.07,
},
{
name: (
<FormattedMessage
id='insights.topThreads.thread'
defaultMessage='Thread'
/>
),
field: 'thread',
width: 0.7,
},
{
name: (
<FormattedMessage
id='insights.topThreads.totalMessages'
defaultMessage='Participants'
/>
),
field: 'participants',
width: 0.15,
},
{
name: (
<FormattedMessage
id='insights.topThreads.replies'
defaultMessage='Replies'
/>
),
field: 'replies',
width: 0.08,
},
];
return columns;
}, []);
const getRows = useMemo((): Row[] => {
return topThreads.map((thread, i) => {
const channelMembership = myChannelMemberships[thread.channel_id];
let isChannelMember = false;
if (typeof channelMembership !== 'undefined') {
isChannelMember = true;
}
return (
{
cells: {
rank: (
<span className='cell-text'>
{i + 1}
</span>
),
thread: (
<div className='thread-item'>
<div className='thread-details'>
<Avatar
url={imageURLForUser(thread.user_information.id)}
size={'xs'}
/>
<span className='display-name'>{displayUsername(thread.user_information as UserProfile, teammateNameDisplaySetting)}</span>
<Tag text={thread.channel_display_name}/>
</div>
<div
className='preview'
>
{getPreview(thread, isChannelMember)}
</div>
</div>
),
participants: (
<>
{thread.participants && thread.participants.length > 0 ? (
<Avatars
userIds={thread.participants}
size='xs'
disableProfileOverlay={true}
/>
) : null}
</>
),
replies: (
<span className='replies'>{thread.post.reply_count}</span>
),
},
onClick: () => {
openRHSOrJoinChannel(thread, isChannelMember);
},
}
);
});
}, [topThreads, myChannelMemberships]);
return (
<DataGrid
columns={getColumns}
rows={getRows}
loading={loading}
page={0}
nextPage={() => {}}
previousPage={() => {}}
startCount={1}
endCount={10}
total={0}
className={classNames('InsightsTable', 'TopThreadsTable')}
/>
);
};
export default memo(TopThreadsTable);

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

@@ -1,27 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {FormattedMessage} from 'react-intl';
type Props = {
icon: string;
}
const WidgetEmptyState = (props: Props) => {
return (
<div className='empty-state'>
<div className='empty-state-emoticon'>
<i className={`icon icon-${props.icon}`}/>
</div>
<div className='empty-state-text'>
<FormattedMessage
id='insights.topReactions.empty'
defaultMessage='Not enough data yet for this insight'
/>
</div>
</div>
);
};
export default memo(WidgetEmptyState);

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

@@ -1,63 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ComponentType} from 'react';
import {Provider} from 'react-redux';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {renderWithIntl, screen} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import {InsightsScopes} from 'utils/constants';
import widgetHoc from './widget_hoc';
describe('components/activity_and_insights/insights/widget_hoc', () => {
let TestComponent: ComponentType;
const store = mockStore({});
const props = {
size: CardSizes.small,
widgetType: InsightsWidgetTypes.TOP_REACTIONS,
filterType: InsightsScopes.MY,
class: 'top-reactions-card',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
timeFrameLabel: 'Last 7 days',
};
beforeEach(() => {
TestComponent = () => <div/>;
});
test('should display my top reactions', () => {
const EnhancedComponent = widgetHoc(TestComponent);
renderWithIntl(
<Provider store={store}>
<EnhancedComponent
{...props}
/>
</Provider>,
);
expect(screen.getByText('My top reactions')).toBeInTheDocument();
expect(screen.getByText('Reactions I\'ve used the most')).toBeInTheDocument();
});
test('should display team top reactions', () => {
const EnhancedComponent = widgetHoc(TestComponent);
renderWithIntl(
<Provider store={store}>
<EnhancedComponent
{...props}
filterType={InsightsScopes.TEAM}
/>
</Provider>,
);
expect(screen.getByText('Top reactions')).toBeInTheDocument();
expect(screen.getByText('The team\'s most-used reactions')).toBeInTheDocument();
});
});

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

@@ -1,90 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ComponentType, useCallback, useState} from 'react';
import {useDispatch} from 'react-redux';
import {useIntl} from 'react-intl';
import {openModal} from 'actions/views/modals';
import {trackEvent} from 'actions/telemetry_actions';
import {CardSize, InsightsWidgetTypes, TimeFrame} from '@mattermost/types/insights';
import InsightsCard from '../card/card';
import InsightsModal from '../insights_modal/insights_modal';
import {InsightsScopes, InsightsCardTitles, ModalIdentifiers} from 'utils/constants';
import {DispatchFunc} from 'mattermost-redux/types/actions';
export interface WidgetHocProps {
size: CardSize;
widgetType: InsightsWidgetTypes;
filterType: string;
class: string;
timeFrame: TimeFrame;
}
function widgetHoc<T>(WrappedComponent: ComponentType<T>) {
const Component = (props: T & WidgetHocProps) => {
const {formatMessage} = useIntl();
const dispatch = useDispatch<DispatchFunc>();
const [showModal, setShowModal] = useState(false);
const title = useCallback(() => {
if (props.filterType === InsightsScopes.MY && Object.keys(InsightsCardTitles[props.widgetType].myTitle).length !== 0) {
return formatMessage(InsightsCardTitles[props.widgetType].myTitle);
}
if (props.filterType === InsightsScopes.TEAM && Object.keys(InsightsCardTitles[props.widgetType].teamTitle).length !== 0) {
return formatMessage(InsightsCardTitles[props.widgetType].teamTitle);
}
return '';
}, [props.filterType, props.widgetType]);
const subTitle = useCallback(() => {
if (props.filterType === InsightsScopes.MY && Object.keys(InsightsCardTitles[props.widgetType].mySubTitle).length !== 0) {
return formatMessage(InsightsCardTitles[props.widgetType].mySubTitle);
}
if (props.filterType === InsightsScopes.TEAM && Object.keys(InsightsCardTitles[props.widgetType].teamSubTitle).length !== 0) {
return formatMessage(InsightsCardTitles[props.widgetType].teamSubTitle);
}
return '';
}, [props.filterType, props.widgetType]);
const openInsightsModal = useCallback(() => {
trackEvent('insights', `open_modal_${props.widgetType.toLowerCase()}`);
setShowModal(true);
dispatch(openModal({
modalId: ModalIdentifiers.INSIGHTS,
dialogType: InsightsModal,
dialogProps: {
widgetType: props.widgetType,
title: title(),
subtitle: subTitle(),
filterType: props.filterType,
timeFrame: props.timeFrame,
setShowModal,
},
}));
}, [props.widgetType, title, subTitle, props.filterType, props.timeFrame]);
return (
<InsightsCard
class={props.class}
title={title()}
subTitle={subTitle()}
size={props.size}
onClick={openInsightsModal}
>
<WrappedComponent
{...(props as unknown as T)}
showModal={showModal}
/>
</InsightsCard>
);
};
return Component;
}
export default widgetHoc;

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

@@ -23,7 +23,6 @@ describe('components/channel_layout/CenterChannel', () => {
rhsMenuOpen: true,
isCollapsedThreadsEnabled: true,
currentUserId: 'testUserId',
insightsAreEnabled: true,
isMobileView: false,
actions: {
getProfiles: jest.fn,

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

@@ -38,16 +38,6 @@ const LazyDrafts = makeAsyncComponent(
),
);
const LazyActivityAndInsights = makeAsyncComponent(
'LazyActivityAndInsights',
React.lazy(() => import('components/activity_and_insights/activity_and_insights')),
(
<div className='app__content'>
<LoadingScreen/>
</div>
),
);
type Props = PropsFromRedux & OwnProps;
type State = {
@@ -80,7 +70,7 @@ export default class CenterChannel extends React.PureComponent<Props, State> {
}
render() {
const {lastChannelPath, isCollapsedThreadsEnabled, insightsAreEnabled, isMobileView} = this.props;
const {lastChannelPath, isCollapsedThreadsEnabled, isMobileView} = this.props;
const url = this.props.match.url;
return (
@@ -129,12 +119,7 @@ export default class CenterChannel extends React.PureComponent<Props, State> {
path='/:team/drafts'
component={LazyDrafts}
/>
{insightsAreEnabled ? (
<Route
path='/:team/activity-and-insights'
component={LazyActivityAndInsights}
/>
) : null}
<Redirect to={lastChannelPath}/>
</Switch>
</div>

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

@@ -9,7 +9,7 @@ import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
import {getProfiles} from 'mattermost-redux/actions/users';
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {getRedirectChannelNameForTeam} from 'mattermost-redux/selectors/entities/channels';
import {isCollapsedThreadsEnabled, insightsAreEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getIsMobileView} from 'selectors/views/browser';
@@ -49,8 +49,6 @@ const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
let lastChannelPath;
if (isCollapsedThreadsEnabled(state) && (previousTeamLastViewedType === PreviousViewedTypes.THREADS || lastViewedType === PreviousViewedTypes.THREADS)) {
lastChannelPath = `${ownProps.match.url}/threads`;
} else if (insightsAreEnabled(state) && lastViewedType === PreviousViewedTypes.INSIGHTS) {
lastChannelPath = `${ownProps.match.url}/activity-and-insights`;
} else {
lastChannelPath = `${ownProps.match.url}/channels/${channelName}`;
}
@@ -62,7 +60,6 @@ const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
rhsMenuOpen: getIsRhsMenuOpen(state),
isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state),
currentUserId: getCurrentUserId(state),
insightsAreEnabled: insightsAreEnabled(state),
isMobileView: getIsMobileView(state),
};
};

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

@@ -46,7 +46,6 @@ describe('components/FaviconTitleHandler', () => {
currentTeammate: null,
inGlobalThreads: false,
inDrafts: false,
inActivityAndInsights: false,
};
test('set correctly the title when needed', () => {

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

@@ -48,7 +48,6 @@ type Props = {
currentTeammate: Channel | null;
inGlobalThreads: boolean;
inDrafts: boolean;
inActivityAndInsights: boolean;
};
export class FaviconTitleHandlerClass extends React.PureComponent<Props> {
@@ -84,7 +83,6 @@ export class FaviconTitleHandlerClass extends React.PureComponent<Props> {
unreadStatus,
inGlobalThreads,
inDrafts,
inActivityAndInsights,
} = this.props;
const {formatMessage} = this.props.intl;
@@ -121,14 +119,6 @@ export class FaviconTitleHandlerClass extends React.PureComponent<Props> {
displayName: currentTeam.display_name,
siteName: currentSiteName,
});
} else if (currentTeam && inActivityAndInsights) {
document.title = formatMessage({
id: 'activityAndInsights.title',
defaultMessage: 'Activity and Insights - {displayName} {siteName}',
}, {
displayName: currentTeam.display_name,
siteName: currentSiteName,
});
} else {
document.title = formatMessage({id: 'sidebar.team_select', defaultMessage: '{siteName} - Join a team'}, {siteName: currentSiteName || 'Mattermost'});
}

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

@@ -30,7 +30,6 @@ function mapStateToProps(state: GlobalState, {location: {pathname}}: Props): Com
unreadStatus: getUnreadStatus(state),
inGlobalThreads: matchPath(pathname, {path: '/:team/threads/:threadIdentifier?'}) != null,
inDrafts: matchPath(pathname, {path: '/:team/drafts'}) != null,
inActivityAndInsights: matchPath(pathname, {path: '/:team/activity-and-insights'}) != null,
};
}

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

@@ -5,14 +5,13 @@ import React, {useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {Channel} from '@mattermost/types/channels';
import {LeastActiveChannel} from '@mattermost/types/insights';
import Constants from 'utils/constants';
import ConfirmModal from 'components/confirm_modal';
type Props = {
channel: Channel | LeastActiveChannel;
channel: Channel;
onExited: () => void;
callback?: () => any;
actions: {

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

@@ -2,7 +2,6 @@
exports[`SidebarList should match snapshot 1`] = `
<Fragment>
<ActivityAndInsightsLink />
<GlobalThreadsLink />
<Memo(DraftsLink) />
<div

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

@@ -23,8 +23,6 @@ import {StaticPage} from 'types/store/lhs';
import GlobalThreadsLink from 'components/threading/global_threads_link';
import DraftsLink from 'components/drafts/drafts_link/drafts_link';
import ActivityAndInsightsLink
from 'components/activity_and_insights/activity_and_insights_link/activity_and_insights_link';
import {Team} from '@mattermost/types/teams';
import {ChannelCategory} from '@mattermost/types/channel_categories';
@@ -535,7 +533,6 @@ export default class SidebarList extends React.PureComponent<Props, State> {
// NOTE: id attribute added to temporarily support the desktop app's at-mention DOM scraping of the old sidebar
<>
<ActivityAndInsightsLink/>
<GlobalThreadsLink/>
<DraftsLink/>
<div

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

@@ -917,97 +917,4 @@ describe('components/SwitchChannelProvider', () => {
terms: expectedOrder,
}));
});
it('Should show insights as the first item in the list if search term matches', async () => {
const modifiedState = {
...defaultState,
entities: {
...defaultState.entities,
general: {
config: {
CollapsedThreads: 'default_off',
FeatureFlagInsightsEnabled: 'true',
InsightsEnabled: 'true',
},
},
threads: {
countsIncludingDirect: {
currentTeamId: {
total: 0,
total_unread_threads: 0,
total_unread_mentions: 0,
},
},
counts: {
currentTeamId: {
total: 0,
total_unread_threads: 0,
total_unread_mentions: 0,
},
},
},
preferences: {
...defaultState.entities.preferences,
myPreferences: {
...defaultState.entities.preferences.myPreferences,
[`${Preferences.CATEGORY_DISPLAY_SETTINGS}--${Preferences.COLLAPSED_REPLY_THREADS}`]: {
value: 'on',
},
},
},
channels: {
...defaultState.entities.channels,
myMembers: {
current_channel_id: {
channel_id: 'current_channel_id',
user_id: 'current_user_id',
roles: 'channel_role',
mention_count: 1,
msg_count: 9,
},
insight_gm_channel: {
channel_id: 'insight_gm_channel',
msg_count: 1,
last_viewed_at: 3,
},
thread_user1: {},
},
channels: {
insight_gm_channel: {
id: 'insight_gm_channel',
msg_count: 1,
last_viewed_at: 3,
type: 'G',
name: 'insight_gm_channel',
delete_at: 0,
display_name: 'insight_gm_channel',
},
},
channelsInTeam: {
'': ['insight_gm_channel'],
},
},
},
};
getState.mockClear();
const switchProvider = new SwitchChannelProvider();
const store = mockStore(modifiedState);
getState.mockImplementation(store.getState);
const searchText = 'insight';
const resultsCallback = jest.fn();
switchProvider.startNewRequest(searchText);
await switchProvider.fetchUsersAndChannels(searchText, resultsCallback);
const expectedOrder = [
'insights',
'insight_gm_channel',
];
expect(resultsCallback).toBeCalledWith(expect.objectContaining({
terms: expectedOrder,
}));
});
});

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

@@ -28,7 +28,7 @@ import {
getAllTeamsUnreadChannelIds,
} from 'mattermost-redux/selectors/entities/channels';
import {getMyPreferences, isGroupChannelManuallyVisible, isCollapsedThreadsEnabled, insightsAreEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getMyPreferences, isGroupChannelManuallyVisible, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {
getCurrentTeamId,
@@ -73,14 +73,6 @@ const ThreadsChannel: FakeChannel = {
delete_at: 0,
};
const InsightsChannel: FakeChannel = {
id: 'insights',
name: 'activity-and-insights',
display_name: 'Insights',
type: Constants.INSIGHTS,
delete_at: 0,
};
type FakeChannel = Pick<Channel, 'id' | 'name' | 'display_name' | 'delete_at'> & {
type: string;
}
@@ -183,12 +175,6 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLDivElement, Props>((props,
<i className='icon icon-message-text-outline'/>
</span>
);
} else if (channel.type === Constants.INSIGHTS) {
icon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<i className='icon icon-chart-line'/>
</span>
);
} else if (channel.type === Constants.GM_CHANNEL) {
icon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
@@ -457,7 +443,7 @@ export default class SwitchChannelProvider extends Provider {
// Dispatch suggestions for local data (filter out deleted and archived channels from local store data)
const channels = getChannelsInAllTeams(getState()).concat(getDirectAndGroupChannels(getState())).filter((c) => c.delete_at === 0);
const users = searchProfilesMatchingWithTerm(getState(), channelPrefix, false);
const formattedData = this.formatList(channelPrefix, [ThreadsChannel, InsightsChannel, ...channels], users, true, true);
const formattedData = this.formatList(channelPrefix, [ThreadsChannel, ...channels], users, true, true);
if (formattedData) {
resultsCallback(formattedData);
}
@@ -510,7 +496,7 @@ export default class SwitchChannelProvider extends Provider {
// filter out deleted and archived channels from local store data
const localChannelData = getChannelsInAllTeams(state).concat(getDirectAndGroupChannels(state)).filter((c) => c.delete_at === 0) || [];
const localUserData = searchProfilesMatchingWithTerm(state, channelPrefix, false);
const localFormattedData = this.formatList(channelPrefix, [ThreadsChannel, InsightsChannel, ...localChannelData], localUserData);
const localFormattedData = this.formatList(channelPrefix, [ThreadsChannel, ...localChannelData], localUserData);
const remoteChannelData = channelsFromServer.concat(getGroupChannels(state)) || [];
const remoteUserData = usersFromServer.users || [];
const remoteFormattedData = this.formatList(channelPrefix, remoteChannelData, remoteUserData, false);
@@ -593,7 +579,7 @@ export default class SwitchChannelProvider extends Provider {
let wrappedChannel: WrappedChannel = {channel: newChannel, name: newChannel.name, deactivated: false};
if (members[channel.id]) {
wrappedChannel.last_viewed_at = members[channel.id].last_viewed_at;
} else if (skipNotMember && (newChannel.type !== Constants.THREADS && newChannel.type !== Constants.INSIGHTS)) {
} else if (skipNotMember && (newChannel.type !== Constants.THREADS)) {
continue;
}
@@ -614,13 +600,6 @@ export default class SwitchChannelProvider extends Provider {
} else {
continue;
}
} else if (newChannel.type === Constants.INSIGHTS) {
const insightsItem = this.getInsightsItem();
if (insightsItem) {
wrappedChannel = insightsItem;
} else {
continue;
}
} else if (newChannel.type === Constants.GM_CHANNEL) {
newChannel.name = newChannel.display_name;
wrappedChannel.name = newChannel.name;
@@ -767,27 +746,6 @@ export default class SwitchChannelProvider extends Provider {
return null;
}
getInsightsItem() {
const state = getState();
const insightsEnabled = insightsAreEnabled(state);
// adding last viewed at equal to Date.now() to push it to the top of the list
const insightsItem = {
channel: InsightsChannel,
name: InsightsChannel.name,
unread: false,
unread_mentions: 0,
deactivated: false,
last_viewed_at: Date.now(),
};
if (insightsEnabled) {
return insightsItem;
}
return null;
}
getTimestampFromPrefs(myPreferences: Record<string, PreferenceType>, category: string, name: string) {
const pref = myPreferences[getPreferenceKey(category, name)];
const prefValue = pref ? pref.value : '0';

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

@@ -66,12 +66,6 @@
"activity_log.os": "OS: {os}",
"activity_log.sessionId": "Session ID: {id}",
"activity_log.sessionsDescription": "Sessions are created when you log in through a new browser on a device. Sessions let you use Mattermost without having to log in again for a time period specified by the System Admin. To end the session sooner, use the 'Log Out' button.",
"activityAndInsights.sidebarLink": "Insights",
"activityAndInsights.title": "Activity and Insights - {displayName} {siteName}",
"activityAndInsights.tutorial_tip.notNow": "Not now",
"activityAndInsights.tutorial_tip.viewInsights": "View insights",
"activityAndInsights.tutorialTip.description": "Check out the new Insights feature added to your workspace. See what content is most active, and learn how you and your teammates are using your workspace.",
"activityAndInsights.tutorialTip.title": "Introducing: Insights",
"add_channels_to_scheme.title": "Add Channels to **Channel Selection** List",
"add_command.autocomplete": "Autocomplete",
"add_command.autocomplete.help": "(Optional) Show your slash command on the autocomplete list when someone types / in the input box.",
@@ -2617,7 +2611,6 @@
"analytics.system.skippedIntensiveQueries": "To maximize performance, some statistics are disabled. You can <link>re-enable them in config.json</link>.",
"analytics.system.textPosts": "Posts with Text-only",
"analytics.system.title": "System Statistics",
"analytics.system.topChannels": "Top Channels",
"analytics.system.totalBotPosts": "Total Posts from Bots",
"analytics.system.totalChannels": "Total Channels",
"analytics.system.totalCommands": "Total Commands",
@@ -3652,92 +3645,6 @@
"incoming_webhooks.header": "Incoming Webhooks",
"inProduct_notices.adminOnlyMessage": "Visible to Admins only",
"input.clear": "Clear",
"insights.accessModal.cloudFreeTrial": "During your trial you are able to view Team Insights.",
"insights.accessModal.contactSales": "contact our Sales team",
"insights.accessModal.docsLink": "Insights",
"insights.accessModal.messageAdminPostTrial": "To access your complete {insightsDoc} dashboard, including {teamInsights}, please upgrade your plan to Professional or Enterprise. For questions on upgrading your plan, please {contactSales} for support.",
"insights.accessModal.messageAdminPreTrial": "Use {teamInsights} with one of our paid plans. Get the full experience of Enterprise when you start a free, {trialLength} day trial.",
"insights.accessModal.messageEndUser": "To access your complete {insightsDoc} dashboard, including {teamInsights}, please notify your Admin to upgrade your plan to Professional or Enterprise. For questions on upgrading your plan, please {contactSales} for support.",
"insights.accessModal.teamDocsLink": "Team Insights",
"insights.accessModal.titleAdminPostTrial": "Upgrade to access team insights",
"insights.accessModal.titleAdminPreTrial": "Try team insights with a free trial",
"insights.accessModal.titleEndUser": "Team insights are available in paid plans",
"insights.card.ariaLabel": "Open modal for {title}",
"insights.filter.ariaLabel": "Insights Filter Menu",
"insights.filter.myInsights": "My insights",
"insights.filter.teamInsights": "Team insights",
"insights.leastActiveChannels.channel": "Channel",
"insights.leastActiveChannels.copyLink": "Copy link",
"insights.leastActiveChannels.lastActivity": "Last activity: {time}",
"insights.leastActiveChannels.lastActivityCell": "Last activity",
"insights.leastActiveChannels.lastActivityNone": "No activity",
"insights.leastActiveChannels.leaveChannel": "Leave channel",
"insights.leastActiveChannels.members": "Members",
"insights.leastActiveChannels.menuAriaLabel": "Manage channel menu",
"insights.leastActiveChannels.menuButtonAriaLabel": "Open manage channel menu",
"insights.leastActiveChannels.mySubTitle": "My channels with the least posts",
"insights.leastActiveChannels.myTitle": "My least active channels",
"insights.leastActiveChannels.subTitle": "Channels with the least posts",
"insights.leastActiveChannels.title": "Least active channels",
"insights.myHeading": "My insights",
"insights.newMembers.joined": "Date joined",
"insights.newMembers.lastSevenDays": "Joined the team in the last 7 days",
"insights.newMembers.lastTwentyEightDays": "Joined the team in the last 28 days",
"insights.newMembers.member": "Team member",
"insights.newMembers.position": "Position",
"insights.newMembers.sayHello": "Say hello",
"insights.newMembers.seeAll": "See all",
"insights.newMembers.today": "Joined the team today",
"insights.newTeamMembers.title": "New team members",
"insights.teamHeading": "Team insights",
"insights.timeFrame.longRange": "Last 28 days",
"insights.timeFrame.mediumRange": "Last 7 days",
"insights.timeFrame.today": "Today",
"insights.topBoards.mySubTitle": "Most active boards I've participated in",
"insights.topBoards.myTitle": "My top boards",
"insights.topBoards.subTitle": "Most active boards for the team",
"insights.topBoards.title": "Top boards",
"insights.topBoards.updates": "{updateCount} updates",
"insights.topBoardsTable.board": "Board",
"insights.topBoardsTable.participants": "Participants",
"insights.topBoardsTable.updates": "Updates",
"insights.topChannels.channel": "Channel",
"insights.topChannels.messageCount": "{messageCount} total messages",
"insights.topChannels.mySubTitle": "Most active channels that I'm a member of",
"insights.topChannels.myTitle": "My top channels",
"insights.topChannels.subTitle": "Most active channels for the team",
"insights.topChannels.title": "Top channels",
"insights.topChannels.totalMessages": "Total messages",
"insights.topDMs.myTitle": "My most active direct messages",
"insights.topDMs.receivedMessages": "Received",
"insights.topDMs.sentMessages": "Sent",
"insights.topDMs.totalMessages": "Total messages",
"insights.topDMs.user": "User",
"insights.topPlaybooks.lastRun": "Last run: {relativeTime}",
"insights.topPlaybooks.mySubTitle": "Playbooks I've used with the most runs",
"insights.topPlaybooks.myTitle": "My top playbooks",
"insights.topPlaybooks.subTitle": "Playbooks with the most runs",
"insights.topPlaybooks.title": "Top playbooks",
"insights.topPlaybooks.totalRuns": "{total} runs",
"insights.topPlaybooksTable.participants": "Total runs",
"insights.topPlaybooksTable.playbook": "Playbook",
"insights.topPlaybooksTable.updates": "Last run",
"insights.topReactions.empty": "Not enough data yet for this insight",
"insights.topReactions.mySubTitle": "Reactions I've used the most",
"insights.topReactions.myTitle": "My top reactions",
"insights.topReactions.rank": "Rank",
"insights.topReactions.reaction": "Reaction",
"insights.topReactions.subTitle": "The team's most-used reactions",
"insights.topReactions.timesUsed": "Times used",
"insights.topReactions.title": "Top reactions",
"insights.topThreadItem.notChannelMember": "You'll need to join the {channel} channel to see this thread.",
"insights.topThreads.mySubTitle": "Most active threads I've followed",
"insights.topThreads.myTitle": "My top threads",
"insights.topThreads.replies": "Replies",
"insights.topThreads.subTitle": "Most active threads for the team",
"insights.topThreads.thread": "Thread",
"insights.topThreads.title": "Top threads",
"insights.topThreads.totalMessages": "Participants",
"installed_command.header": "Slash Commands",
"installed_commands.add": "Add Slash Command",
"installed_commands.delete.confirm": "This action permanently deletes the slash command and breaks any integrations using it. Are you sure you want to delete it?",
@@ -3911,11 +3818,8 @@
"invite.rate-limit-exceeded": "Invite emails rate limit exceeded.",
"join_team_group_constrained_denied": "You need to be a member of a linked group to join this team.",
"join_team_group_constrained_denied_admin": "You need to be a member of a linked group to join this team. You can add a group to this team [here]({siteURL}/admin_console/user_management/groups).",
"joinChannel.cancelButton": "Cancel",
"joinChannel.desciption": "You'll need to join the {channel} channel to see this thread. Do you want to join {channel} now?",
"joinChannel.JoinButton": "Join",
"joinChannel.joiningButton": "Joining...",
"joinChannel.title": "Join channel?",
"katex.error": "Couldn't compile your Latex code. Please review the syntax and try again.",
"last_users_message.added_to_channel.type": "were **added to the channel** by {actor}.",
"last_users_message.added_to_team.type": "were **added to the team** by {actor}.",

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

@@ -25,7 +25,6 @@ import ChannelCategoryTypes from './channel_categories';
import CloudTypes from './cloud';
import AppsTypes from './apps';
import ThreadTypes from './threads';
import InsightTypes from './insights';
import HostedCustomerTypes from './hosted_customer';
import PlaybookType from './playbooks';
@@ -53,7 +52,6 @@ export {
CloudTypes,
AppsTypes,
ThreadTypes,
InsightTypes,
HostedCustomerTypes,
DraftTypes,
PlaybookType,

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

@@ -1,12 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import keyMirror from 'mattermost-redux/utils/key_mirror';
export default keyMirror({
RECEIVED_TOP_REACTIONS: null,
RECEIVED_MY_TOP_REACTIONS: null,
RECEIVED_TOP_THREADS: null,
RECEIVED_MY_TOP_THREADS: null,
});

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

@@ -23,7 +23,6 @@ import * as teams from './teams';
import * as timezone from './timezone';
import * as websocket from './websocket';
import * as users from './users';
import * as insights from './insights';
export {
admin,
@@ -48,6 +47,5 @@ export {
timezone,
websocket,
users,
insights,
};

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

@@ -1,180 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {InsightTypes} from 'mattermost-redux/action_types';
import {GetStateFunc, DispatchFunc, ActionFunc} from 'mattermost-redux/types/actions';
import {Client4} from 'mattermost-redux/client';
import {LeastActiveChannelsActionResult, LeastActiveChannelsResponse, TimeFrame, TopChannelActionResult, TopChannelResponse, TopThreadActionResult, TopThreadResponse, TopDMsActionResult, TopDMsResponse} from '@mattermost/types/insights';
import {forceLogoutIfNecessary} from './helpers';
import {logError} from './errors';
export function getTopReactionsForTeam(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data;
try {
data = await Client4.getTopReactionsForTeam(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
dispatch({
type: InsightTypes.RECEIVED_TOP_REACTIONS,
data: {data, timeFrame},
id: teamId,
});
return {data};
};
}
export function getMyTopReactions(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data;
try {
data = await Client4.getMyTopReactions(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
dispatch({
type: InsightTypes.RECEIVED_MY_TOP_REACTIONS,
data: {data, timeFrame},
id: teamId,
});
return {data};
};
}
export function getTopChannelsForTeam(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<TopChannelActionResult> | TopChannelActionResult {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data: TopChannelResponse;
try {
data = await Client4.getTopChannelsForTeam(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
return {data};
};
}
export function getMyTopChannels(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<TopChannelActionResult> | TopChannelActionResult {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data: TopChannelResponse;
try {
data = await Client4.getMyTopChannels(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
return {data};
};
}
export function getTopThreadsForTeam(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<TopThreadActionResult> | TopThreadActionResult {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data: TopThreadResponse;
try {
data = await Client4.getTopThreadsForTeam(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
dispatch({
type: InsightTypes.RECEIVED_TOP_THREADS,
data,
});
return {data};
};
}
export function getMyTopThreads(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<TopThreadActionResult> | TopThreadActionResult {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data: TopThreadResponse;
try {
data = await Client4.getMyTopThreads(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
dispatch({
type: InsightTypes.RECEIVED_MY_TOP_THREADS,
data,
});
return {data};
};
}
export function getLeastActiveChannelsForTeam(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<LeastActiveChannelsActionResult> | LeastActiveChannelsActionResult {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data: LeastActiveChannelsResponse;
try {
data = await Client4.getLeastActiveChannelsForTeam(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
return {data};
};
}
export function getMyTopDMs(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<TopDMsActionResult> | TopDMsActionResult {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data: TopDMsResponse;
try {
data = await Client4.getMyTopDMs(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
return {data};
};
}
export function getMyLeastActiveChannels(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<LeastActiveChannelsActionResult> | LeastActiveChannelsActionResult {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data: LeastActiveChannelsResponse;
try {
data = await Client4.getMyLeastActiveChannels(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
return {data};
};
}
export function getNewTeamMembers(teamId: string, page: number, perPage: number, timeFrame: TimeFrame): (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<TopDMsActionResult> | TopDMsActionResult {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data: TopDMsResponse;
try {
data = await Client4.getNewTeamMembers(teamId, page, perPage, timeFrame);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
return {data};
};
}

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

@@ -121,20 +121,6 @@ export function setActionsMenuInitialisationState(initializationState: Record<st
};
}
export function setInsightsInitialisationState(initializationState: Record<string, boolean>) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const currentUserId = getCurrentUserId(state);
const preference: PreferenceType = {
user_id: currentUserId,
category: Preferences.CATEGORY_INSIGHTS,
name: Preferences.NAME_INSIGHTS_TUTORIAL_STATE,
value: JSON.stringify(initializationState),
};
await dispatch(savePreferences(currentUserId, [preference]));
};
}
export function setCustomStatusInitialisationState(initializationState: Record<string, boolean>) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();

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

@@ -67,10 +67,6 @@ const Preferences = {
UNREAD_SCROLL_POSITION_START_FROM_LEFT: 'start_from_left_off',
UNREAD_SCROLL_POSITION_START_FROM_NEWEST: 'start_from_newest',
CATEGORY_INSIGHTS: 'insights',
NAME_INSIGHTS_TUTORIAL_STATE: 'insights_tutorial_state',
INSIGHTS_VIEWED: 'insights_modal_viewed',
CATEGORY_UPGRADE_CLOUD: 'upgrade_cloud',
SYSTEM_CONSOLE_LIMIT_REACHED: 'system_console_limit_reached',

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

@@ -3,11 +3,10 @@
import {combineReducers} from 'redux';
import {FileTypes, InsightTypes, PostTypes, UserTypes} from 'mattermost-redux/action_types';
import {FileTypes, PostTypes, UserTypes} from 'mattermost-redux/action_types';
import {GenericAction} from 'mattermost-redux/types/actions';
import {Post} from '@mattermost/types/posts';
import {FileInfo, FileSearchResultItem} from '@mattermost/types/files';
import {TopThread} from '@mattermost/types/insights';
export function files(state: Record<string, FileInfo> = {}, action: GenericAction) {
switch (action.type) {
@@ -30,15 +29,6 @@ export function files(state: Record<string, FileInfo> = {}, action: GenericActio
return storeAllFilesForPost(storeFilesForPost, state, post);
}
case InsightTypes.RECEIVED_TOP_THREADS:
case InsightTypes.RECEIVED_MY_TOP_THREADS: {
const threads: TopThread[] = Object.values(action.data.items);
return threads.reduce((nextState, thread) => {
return storeAllFilesForPost(storeFilesForPost, nextState, thread.post);
}, state);
}
case PostTypes.RECEIVED_POSTS: {
const posts: Post[] = Object.values(action.data.posts);

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше