diff --git a/webapp/cypress/support/api_commands.ts b/webapp/cypress/support/api_commands.ts index 428926601..e3e96066a 100644 --- a/webapp/cypress/support/api_commands.ts +++ b/webapp/cypress/support/api_commands.ts @@ -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', }, } diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index a441f927a..a42cf48d9 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -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", diff --git a/webapp/package-lock.json b/webapp/package-lock.json index f32732107..be5c2a9f9 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -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", diff --git a/webapp/src/components/messages/versionMessage.scss b/webapp/src/components/messages/versionMessage.scss new file mode 100644 index 000000000..7c68fbb2a --- /dev/null +++ b/webapp/src/components/messages/versionMessage.scss @@ -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; + } +} diff --git a/webapp/src/components/messages/versionMessage.test.tsx b/webapp/src/components/messages/versionMessage.test.tsx new file mode 100644 index 000000000..53bde1055 --- /dev/null +++ b/webapp/src/components/messages/versionMessage.test.tsx @@ -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( + + + , + ) + 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( + + + , + ) + + 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( + + + , + ) + + 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( + + + , + ) + + 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( + + + , + ) + const {container} = render(component) + expect(container.firstChild).toBeNull() + }) + + } +}) diff --git a/webapp/src/components/messages/versionMessage.tsx b/webapp/src/components/messages/versionMessage.tsx new file mode 100644 index 000000000..a38ffda16 --- /dev/null +++ b/webapp/src/components/messages/versionMessage.tsx @@ -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(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 ( +
+
+ + + + + +
+ + } + title={closeDialogText} + size='small' + /> +
+ ) +}) +export default VersionMessage diff --git a/webapp/src/pages/boardPage/boardPage.tsx b/webapp/src/pages/boardPage/boardPage.tsx index e44dfffbd..f5753bb36 100644 --- a/webapp/src/pages/boardPage/boardPage.tsx +++ b/webapp/src/pages/boardPage/boardPage.tsx @@ -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 => { + {!mobileWarningClosed &&
diff --git a/webapp/src/store/users.ts b/webapp/src/store/users.ts index d3584a25d..359f5116f 100644 --- a/webapp/src/store/users.ts +++ b/webapp/src/store/users.ts @@ -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 => { diff --git a/webapp/src/telemetry/telemetryClient.ts b/webapp/src/telemetry/telemetryClient.ts index 6bec93a03..85656b727 100644 --- a/webapp/src/telemetry/telemetryClient.ts +++ b/webapp/src/telemetry/telemetryClient.ts @@ -49,6 +49,7 @@ export const TelemetryActions = { LimitCardCTAPerformed: 'limit_CardLimitCTAPerformed', LimitCardLimitReached: 'limit_cardLimitReached', LimitCardLimitLinkOpen: 'limit_cardLimitLinkOpen', + VersionMoreInfo: 'version_more_info', } interface IEventProps {