Remove insights (#23952)
* 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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e37459cd00
Коммит
26617fcbdc
@@ -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);
|
||||
|
||||
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user