Skip to content

Commit

Permalink
Notification box for card limit (#3074)
Browse files Browse the repository at this point in the history
* Notification box for card limit

* Updating snapshots

* adding notification box tests

* Fix linter errors

* Working on limits store

* Implemeting the redux integration with the new limits

* Refining store

* Simplifying cards hidding/showing

* Adding the secondary warning about hidding cards in other boards

* Fixing tests

* Fixed isLimited to limited

* Fixing snooze of other boards hidden warning

* Fixing linter error

* Removing some unneeded coode

* Moving limit logic to the cards selector

* Changing the approach to limit cards

* Fixing eslint errors

Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
  • Loading branch information
jespino and sbishel authored May 24, 2022
1 parent 118a0bd commit 548c6b8
Show file tree
Hide file tree
Showing 22 changed files with 643 additions and 28 deletions.
20 changes: 11 additions & 9 deletions webapp/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,29 @@ const App = (props: Props): JSX.Element => {
dispatch(fetchClientConfig())
}, [])

if (Utils.isFocalboardPlugin()) {
useEffect(() => {
useEffect(() => {
if (Utils.isFocalboardPlugin()) {
if (window.frontendBaseURL) {
browserHistory.replace(window.location.pathname.replace(window.frontendBaseURL, ''))
}
}, [])
}
}
}, [])

// this is a temporary solution while we're using legacy routes
// for shared boards as a way to disable websockets, and should be
// removed when anonymous plugin routes are implemented. This
// check is used to detect if we're running inside the plugin but
// in a legacy route
if (!Utils.isFocalboardLegacy()) {
useEffect(() => {
useEffect(() => {
if (!Utils.isFocalboardLegacy()) {
wsClient.open()
return () => {
}
return () => {
if (!Utils.isFocalboardLegacy()) {
wsClient.close()
}
}, [])
}
}
}, [])

useEffect(() => {
if (me) {
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/components/cardBadges.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ describe('components/cardBadges', () => {
const state: Partial<RootState> = {
cards: {
current: '',
limitTimestamp: 0,
cards: blocksById([card, emptyCard]),
templates: {},
cardHiddenWarning: true,
},
comments: {
comments: blocksById(comments),
Expand Down
Empty file.
149 changes: 149 additions & 0 deletions webapp/src/components/cardLimitNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react'
import {useIntl, FormattedMessage} from 'react-intl'

import AlertIcon from '../widgets/icons/alert'

import {useAppSelector, useAppDispatch} from '../store/hooks'
import {IUser, UserConfigPatch} from '../user'
import {getMe, patchProps, getCardLimitSnoozeUntil, getCardHiddenWarningSnoozeUntil} from '../store/users'
import {getHiddenByLimitCards, getCardHiddenWarning} from '../store/cards'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../telemetry/telemetryClient'
import octoClient from '../octoClient'

import NotificationBox from '../widgets/notification-box'
import './cardLimitNotification.scss'

const snoozeTime = 1000 * 60 * 60 * 24 * 10
const checkSnoozeInterval = 1000 * 60 * 5

const CardLimitNotification = () => {
const intl = useIntl()
const [time, setTime] = useState(Date.now())

const hiddenCards = useAppSelector<number>(getHiddenByLimitCards)
const cardHiddenWarning = useAppSelector<boolean>(getCardHiddenWarning)
const me = useAppSelector<IUser|null>(getMe)
const snoozedUntil = useAppSelector<number>(getCardLimitSnoozeUntil)
const snoozedCardHiddenWarningUntil = useAppSelector<number>(getCardHiddenWarningSnoozeUntil)
const dispatch = useAppDispatch()

const onCloseHidden = useCallback(async () => {
if (me) {
const patch: UserConfigPatch = {
updatedFields: {
focalboard_cardLimitSnoozeUntil: `${Date.now() + snoozeTime}`,
},
}

const patchedProps = await octoClient.patchUserConfig(me.id, patch)
if (patchedProps) {
dispatch(patchProps(patchedProps))
}
}
}, [me])

const onCloseWarning = useCallback(async () => {
if (me) {
const patch: UserConfigPatch = {
updatedFields: {
focalboard_cardHiddenWarningSnoozeUntil: `${Date.now() + snoozeTime}`,
},
}

const patchedProps = await octoClient.patchUserConfig(me.id, patch)
if (patchedProps) {
dispatch(patchProps(patchedProps))
}
}
}, [me])

let show = false
let onClose = onCloseHidden
let title = intl.formatMessage(
{
id: 'notification-box-card-limit-reached.title',
defaultMessage: '{cards} cards hidden from board',
},
{cards: hiddenCards},
)

if (hiddenCards > 0 && time > snoozedUntil) {
show = true
}

if (!show && cardHiddenWarning) {
show = time > snoozedCardHiddenWarningUntil
onClose = onCloseWarning
title = intl.formatMessage(
{
id: 'notification-box-cards-hidden.title',
defaultMessage: 'Your action hidden another card',
},
)
}

useEffect(() => {
if (!show) {
const interval = setInterval(() => setTime(Date.now()), checkSnoozeInterval)
return () => {
clearInterval(interval)
}
}
return () => null
}, [show])

useEffect(() => {
if (show) {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.LimitCardLimitReached, {})
}
}, [show])

const onClick = useCallback(() => {
// TODO: Show the modal to upgrade
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.LimitCardLimitLinkOpen, {})
}, [])

const hasPermissionToUpgrade = me?.roles?.split(' ').indexOf('system_admin') !== -1

if (!show) {
return null
}

return (
<NotificationBox
icon={<AlertIcon/>}
title={title}
onClose={onClose}
closeTooltip={intl.formatMessage({
id: 'notification-box-card-limit-reached.close-tooltip',
defaultMessage: 'Snooze for 10 days',
})}
>
{hasPermissionToUpgrade &&
<FormattedMessage
id='notification-box.card-limit-reached.text'
defaultMessage='Card limit reached, to view older cards, {link}'
values={{
link: (
<a
onClick={onClick}
>
<FormattedMessage
id='notification-box-card-limit-reached.link'
defaultMessage='upgrade to a paid plan'
/>
</a>),
}}
/>}
{!hasPermissionToUpgrade &&
<FormattedMessage
id='notification-box.card-limit-reached.not-admin.text'
defaultMessage='To access archived cards, contact your admin to upgrade to a paid plan.'
/>}
</NotificationBox>
)
}

export default React.memo(CardLimitNotification)
7 changes: 7 additions & 0 deletions webapp/src/components/centerPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,11 @@
position: relative;
flex: 0 0 auto;
}

.NotificationBox {
.AlertIcon {
color: #ffbc1f;
font-size: 24px;
}
}
}
8 changes: 7 additions & 1 deletion webapp/src/components/centerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {CardFilter} from '../cardFilter'
import mutator from '../mutator'
import {Utils} from '../utils'
import {UserSettings} from '../userSettings'
import {addCard, addTemplate} from '../store/cards'
import {addCard, addTemplate, showCardHiddenWarning} from '../store/cards'
import {updateView} from '../store/views'
import {getVisibleAndHiddenGroups} from '../boardUtils'
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient'
Expand Down Expand Up @@ -51,6 +51,8 @@ import Table from './table/table'

import CalendarFullView from './calendar/fullCalendar'

import CardLimitNotification from './cardLimitNotification'

import Gallery from './gallery/gallery'
import {BoardTourSteps, FINISHED, TOUR_BOARD, TOUR_CARD} from './onboardingTour'
import ShareBoardTourStep from './onboardingTour/shareBoard/shareBoard'
Expand All @@ -66,6 +68,7 @@ type Props = {
intl: IntlShape
readonly: boolean
addCard: (card: Card) => void
showCardHiddenWarning: (hidden: boolean) => void
updateView: (view: BoardView) => void
addTemplate: (template: Card) => void
shownCardId?: string
Expand Down Expand Up @@ -187,6 +190,7 @@ class CenterPanel extends React.Component<Props, State> {
this.backgroundClicked(e)
}}
>
<CardLimitNotification/>
<Hotkeys
keyName='ctrl+d,del,esc,backspace'
onKeyDown={this.keydownHandler}
Expand Down Expand Up @@ -366,6 +370,7 @@ class CenterPanel extends React.Component<Props, State> {
this.showCard(undefined)
},
)
this.props.showCardHiddenWarning(true)
await mutator.changeViewCardOrder(activeView, [...activeView.fields.cardOrder, newCard.id], 'add-card')
})
}
Expand Down Expand Up @@ -494,6 +499,7 @@ function mapStateToProps(state: RootState) {
}

export default connect(mapStateToProps, {
showCardHiddenWarning,
addCard,
addTemplate,
updateView,
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/components/gallery/gallery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ describe('src/components/gallery/Gallery', () => {
},
cards: {
current: '',
limitTimestamp: 0,
cards: {
[card.id]: card,
},
templates: {},
cardHiddenWarning: true,
},
comments: {
comments: {},
Expand Down
13 changes: 9 additions & 4 deletions webapp/src/components/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {generatePath, useRouteMatch, useHistory} from 'react-router-dom'
import {FormattedMessage} from 'react-intl'

import {getCurrentWorkspace} from '../store/workspace'
import {getCurrentBoard} from '../store/boards'
import {getCurrentViewCardsSortedFilteredAndGrouped, setCurrent as setCurrentCard} from '../store/cards'
import {getCurrentBoard, getTemplates} from '../store/boards'
import {refreshCards, getCardLimitTimestamp, setLimitTimestamp, getCurrentViewCardsSortedFilteredAndGrouped, setCurrent as setCurrentCard} from '../store/cards'
import {getView, getCurrentBoardViews, getCurrentViewGroupBy, getCurrentView, getCurrentViewDisplayBy} from '../store/views'
import {useAppSelector, useAppDispatch} from '../store/hooks'

Expand All @@ -30,12 +30,14 @@ function CenterContent(props: Props) {
const workspace = useAppSelector(getCurrentWorkspace)
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string}>()
const board = useAppSelector(getCurrentBoard)
const templates = useAppSelector(getTemplates)
const cards = useAppSelector(getCurrentViewCardsSortedFilteredAndGrouped)
const activeView = useAppSelector(getView(match.params.viewId))
const views = useAppSelector(getCurrentBoardViews)
const groupByProperty = useAppSelector(getCurrentViewGroupBy)
const dateDisplayProperty = useAppSelector(getCurrentViewDisplayBy)
const clientConfig = useAppSelector(getClientConfig)
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
const history = useHistory()
const dispatch = useAppDispatch()

Expand All @@ -56,15 +58,18 @@ function CenterContent(props: Props) {
wsClient.addOnConfigChange(onConfigChangeHandler)

const onCardLimitTimestampChangeHandler = (_: WSClient, timestamp: number) => {
Utils.log(`HANDLING TIMESTAMP: ${timestamp}`)
dispatch(setLimitTimestamp({timestamp, templates}))
if (cardLimitTimestamp > timestamp) {
dispatch(refreshCards(timestamp))
}
}
wsClient.addOnCardLimitTimestampChange(onCardLimitTimestampChangeHandler)

return () => {
wsClient.removeOnConfigChange(onConfigChangeHandler)
wsClient.removeOnCardLimitTimestampChange(onCardLimitTimestampChangeHandler)
}
}, [])
}, [cardLimitTimestamp, match.params.boardId, templates])

if (board && activeView) {
let property = groupByProperty
Expand Down
Loading

0 comments on commit 548c6b8

Please sign in to comment.