Add Version banner to hold help link. (#3522)
* implement version banner to display link to help documentation * cleanup * turn off banner in cypress * prefix property name with 'focalboard_' * update to actual url Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
parent
84858d2466
commit
9c6cfa68aa
9 changed files with 339 additions and 6 deletions
|
@ -3,6 +3,8 @@
|
|||
|
||||
import {Board} from '../../src/blocks/board'
|
||||
import {UserConfigPatch} from '../../src/user'
|
||||
import {versionProperty} from '../../src/store/users'
|
||||
|
||||
|
||||
Cypress.Commands.add('apiRegisterUser', (data: Cypress.UserData, token?: string, failOnError?: boolean) => {
|
||||
return cy.request({
|
||||
|
@ -86,6 +88,7 @@ Cypress.Commands.add('apiSkipTour', (userID: string) => {
|
|||
const body: UserConfigPatch = {
|
||||
updatedFields: {
|
||||
focalboard_welcomePageViewed: '1',
|
||||
[versionProperty]: 'true',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -260,6 +260,7 @@
|
|||
"ValueSelector.noOptions": "No options. Start typing to add the first one!",
|
||||
"ValueSelector.valueSelector": "Value selector",
|
||||
"ValueSelectorLabel.openMenu": "Open menu",
|
||||
"VersionMessage.help": "Check out what's new in this version.",
|
||||
"View.AddView": "Add view",
|
||||
"View.Board": "Board",
|
||||
"View.DeleteView": "Delete view",
|
||||
|
|
12
webapp/package-lock.json
generated
12
webapp/package-lock.json
generated
|
@ -14767,9 +14767,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.14.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz",
|
||||
"integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==",
|
||||
"version": "5.14.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
|
||||
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
|
@ -27171,9 +27171,9 @@
|
|||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.14.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz",
|
||||
"integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==",
|
||||
"version": "5.14.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
|
||||
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
|
|
38
webapp/src/components/messages/versionMessage.scss
Normal file
38
webapp/src/components/messages/versionMessage.scss
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
.VersionMessage {
|
||||
background-color: rgb(var(--sidebar-text-active-border-rgb));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .banner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
|
||||
.CompassIcon {
|
||||
font-size: 18px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.Button {
|
||||
margin-left: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
float: right;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
182
webapp/src/components/messages/versionMessage.test.tsx
Normal file
182
webapp/src/components/messages/versionMessage.test.tsx
Normal file
|
@ -0,0 +1,182 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {render, screen} from '@testing-library/react'
|
||||
import {mocked} from 'jest-mock'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {IUser} from '../../user'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
||||
import client from '../../octoClient'
|
||||
|
||||
import {versionProperty} from '../../store/users'
|
||||
|
||||
import VersionMessage from './versionMessage'
|
||||
|
||||
jest.mock('../../octoClient')
|
||||
const mockedOctoClient = mocked(client, true)
|
||||
|
||||
describe('components/messages/VersionMessage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const mockStore = configureStore([])
|
||||
|
||||
if (versionProperty){
|
||||
test('single user mode, no display', () => {
|
||||
const me: IUser = {
|
||||
id: 'single-user',
|
||||
username: 'username_1',
|
||||
email: '',
|
||||
nickname: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
props: {},
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const state = {
|
||||
users: {
|
||||
me,
|
||||
},
|
||||
}
|
||||
|
||||
const store = mockStore(state)
|
||||
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<VersionMessage/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
test('property set, no message', () => {
|
||||
const me: IUser = {
|
||||
id: 'user-id-1',
|
||||
username: 'username_1',
|
||||
email: '',
|
||||
nickname: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
props: {
|
||||
[versionProperty]: 'true',
|
||||
},
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const state = {
|
||||
users: {
|
||||
me,
|
||||
},
|
||||
}
|
||||
const store = mockStore(state)
|
||||
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<VersionMessage/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
test('show message, click close', () => {
|
||||
const me: IUser = {
|
||||
id: 'user-id-1',
|
||||
username: 'username_1',
|
||||
email: '',
|
||||
nickname: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
props: {},
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const state = {
|
||||
users: {
|
||||
me,
|
||||
},
|
||||
}
|
||||
const store = mockStore(state)
|
||||
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<VersionMessage/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
render(component)
|
||||
const buttonElement = screen.getByRole('button', {name: 'Close dialog'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(mockedOctoClient.patchUserConfig).toBeCalledWith('user-id-1', {
|
||||
updatedFields: {
|
||||
[versionProperty]: 'true',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('no me, no message', () => {
|
||||
const state = {
|
||||
users: {},
|
||||
}
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<VersionMessage/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
} else {
|
||||
test('no version, does not display', () => {
|
||||
const me: IUser = {
|
||||
id: 'user-id-1',
|
||||
username: 'username_1',
|
||||
email: '',
|
||||
nickname: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
props: {
|
||||
},
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const state = {
|
||||
users: {
|
||||
me,
|
||||
},
|
||||
}
|
||||
const store = mockStore(state)
|
||||
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<VersionMessage/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
}
|
||||
})
|
91
webapp/src/components/messages/versionMessage.tsx
Normal file
91
webapp/src/components/messages/versionMessage.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {useIntl, FormattedMessage} from 'react-intl'
|
||||
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
|
||||
import CloseIcon from '../../widgets/icons/close'
|
||||
|
||||
import {useAppSelector, useAppDispatch} from '../../store/hooks'
|
||||
import octoClient from '../../octoClient'
|
||||
import {IUser, UserConfigPatch} from '../../user'
|
||||
import {getMe, patchProps, getVersionMessageCanceled, versionProperty} from '../../store/users'
|
||||
|
||||
import CompassIcon from '../../widgets/icons/compassIcon'
|
||||
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../telemetry/telemetryClient'
|
||||
|
||||
import './versionMessage.scss'
|
||||
const helpURL = 'https://docs.mattermost.com/welcome/whats-new-in-v72.html'
|
||||
|
||||
const VersionMessage = React.memo(() => {
|
||||
const intl = useIntl()
|
||||
const dispatch = useAppDispatch()
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const versionMessageCanceled = useAppSelector(getVersionMessageCanceled)
|
||||
|
||||
if (!me || me.id === 'single-user' || versionMessageCanceled) {
|
||||
return null
|
||||
}
|
||||
|
||||
const closeDialogText = intl.formatMessage({
|
||||
id: 'Dialog.closeDialog',
|
||||
defaultMessage: 'Close dialog',
|
||||
})
|
||||
|
||||
const onClose = async () => {
|
||||
if (me) {
|
||||
const patch: UserConfigPatch = {
|
||||
updatedFields: {
|
||||
[versionProperty]: 'true'
|
||||
},
|
||||
}
|
||||
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
|
||||
if (patchedProps) {
|
||||
dispatch(patchProps(patchedProps))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='VersionMessage'>
|
||||
<div className='banner'>
|
||||
<CompassIcon
|
||||
icon='information-outline'
|
||||
className='CompassIcon'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='VersionMessage.help'
|
||||
defaultMessage="Check out what's new in this version."
|
||||
/>
|
||||
|
||||
<Button
|
||||
title='Learn more'
|
||||
size='xsmall'
|
||||
emphasis='primary'
|
||||
onClick={() => {
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.VersionMoreInfo)
|
||||
window.open(helpURL)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='cloudMessage.learn-more'
|
||||
defaultMessage='Learn more'
|
||||
/>
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className='margin-right'
|
||||
onClick={onClose}
|
||||
icon={<CloseIcon/>}
|
||||
title={closeDialogText}
|
||||
size='small'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
export default VersionMessage
|
|
@ -7,6 +7,7 @@ import {useRouteMatch} from 'react-router-dom'
|
|||
|
||||
import Workspace from '../../components/workspace'
|
||||
import CloudMessage from '../../components/messages/cloudMessage'
|
||||
import VersionMessage from '../../components/messages/versionMessage'
|
||||
import octoClient from '../../octoClient'
|
||||
import {Subscription, WSClient} from '../../wsclient'
|
||||
import {Utils} from '../../utils'
|
||||
|
@ -252,6 +253,7 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||
<UndoRedoHotKeys/>
|
||||
<WebsocketConnection/>
|
||||
<CloudMessage/>
|
||||
<VersionMessage/>
|
||||
|
||||
{!mobileWarningClosed &&
|
||||
<div className='mobileWarning'>
|
||||
|
|
|
@ -21,6 +21,8 @@ export const fetchMe = createAsyncThunk(
|
|||
async () => client.getMe(),
|
||||
)
|
||||
|
||||
export const versionProperty = 'focalboard_version72MessageCanceled'
|
||||
|
||||
type UsersStatus = {
|
||||
me: IUser|null
|
||||
boardUsers: {[key: string]: IUser}
|
||||
|
@ -167,6 +169,19 @@ export const getCloudMessageCanceled = createSelector(
|
|||
},
|
||||
)
|
||||
|
||||
export const getVersionMessageCanceled = createSelector(
|
||||
getMe,
|
||||
(me): boolean => {
|
||||
if (versionProperty && me){
|
||||
if (me.id === 'single-user') {
|
||||
return true
|
||||
}
|
||||
return Boolean(me.props[versionProperty])
|
||||
}
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
export const getCardLimitSnoozeUntil = createSelector(
|
||||
getMe,
|
||||
(me): number => {
|
||||
|
|
|
@ -49,6 +49,7 @@ export const TelemetryActions = {
|
|||
LimitCardCTAPerformed: 'limit_CardLimitCTAPerformed',
|
||||
LimitCardLimitReached: 'limit_cardLimitReached',
|
||||
LimitCardLimitLinkOpen: 'limit_cardLimitLinkOpen',
|
||||
VersionMoreInfo: 'version_more_info',
|
||||
}
|
||||
|
||||
interface IEventProps {
|
||||
|
|
Loading…
Reference in a new issue