1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-27 08:31:20 +02:00

Start Welcome Page (#1231)

* start welcome page

* setup template

* setup forwarding logic

* fix linting errors

* Updating UI

* Use intl strings

* Address comments

* go to baords welcome

* Fix image problem

* fix linting

* fix getting shown wrong things

* fix build issues

* fix cypress test and non plugin

* Fix bugs

* remove console

* Add welcome page

* address comments

Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
This commit is contained in:
Hossein 2021-09-28 09:51:32 -04:00 committed by GitHub
parent cbfb4cb36f
commit 7fae65ea02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 242 additions and 25 deletions

View File

@ -1,9 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react'
import React, {useEffect} from 'react'
import {Store, Action} from 'redux'
import {Provider as ReduxProvider} from 'react-redux'
import {useHistory} from 'mm-react-router-dom'
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder'
@ -142,7 +141,7 @@ export default class Plugin {
window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener')
}
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, goToFocalboardWorkspace, '', 'Boards')
this.registry.registerProduct('/boards', 'product-boards', 'Boards', '/boards/dashboard', MainApp, HeaderComponent)
this.registry.registerProduct('/boards', 'product-boards', 'Boards', '/boards/welcome', MainApp, HeaderComponent)
} else {
windowAny.frontendBaseURL = subpath + '/plug/focalboard'
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, () => {

View File

@ -14,6 +14,7 @@ describe('Create and delete board / card', () => {
localStorage.setItem('focalboardSessionId', 'TESTTOKEN');
localStorage.setItem('language', 'en');
cy.expect(localStorage.getItem('focalboardSessionId')).to.eq('TESTTOKEN');
localStorage.setItem('welcomePageViewed', 'true');
});
it('Can create and delete a board and card', () => {

View File

@ -35,11 +35,10 @@
"ContentBlock.moveDown": "Move down",
"ContentBlock.moveUp": "Move up",
"ContentBlock.text": "text",
"DashboardPage.message": "Use Focalboard to create and track tasks for projects big and small using familiar Kanban boards, tables, and other views.",
"DashboardPage.title": "Dashboard",
"DashboardPage.CenterPanel.NoWorkspaces": "Sorry, we couldn't find any channels matching that term.",
"DashboardPage.CenterPanel.NoWorkspacesDescription": "Please try searching for another term.",
"DashboardPage.CenterPanel.ChangeChannels": "Use the switcher to easily change channels",
"DashboardPage.CenterPanel.NoWorkspaces": "Sorry, we could not find any channels matching that term",
"DashboardPage.CenterPanel.NoWorkspacesDescription": "Please try searching for another term",
"DashboardPage.title": "Dashboard",
"Dialog.closeDialog": "Close dialog",
"EditableDayPicker.today": "Today",
"EmptyCenterPanel.no-content": "Add or select a board from the sidebar to get started.",
@ -168,6 +167,9 @@
"ViewTitle.remove-icon": "Remove icon",
"ViewTitle.show-description": "show description",
"ViewTitle.untitled-board": "Untitled board",
"WelcomePage.Description": "Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view",
"WelcomePage.Explore.Button": "Explore",
"WelcomePage.Heading": "Welcome To Boards",
"Workspace.editing-board-template": "You're editing a board template.",
"default-properties.title": "Title",
"error.no-workspace": "Your session may have expired or you may not have access to this workspace.",
@ -177,4 +179,4 @@
"login.register-button": "or create an account if you don't have one",
"register.login-button": "or log in if you already have an account",
"register.signup-title": "Sign up for your account"
}
}

View File

@ -21,6 +21,7 @@ import {FlashMessages} from './components/flashMessages'
import BoardPage from './pages/boardPage'
import ChangePasswordPage from './pages/changePasswordPage'
import DashboardPage from './pages/dashboard/dashboardPage'
import WelcomePage from './pages/welcome/welcomePage'
import ErrorPage from './pages/errorPage'
import LoginPage from './pages/loginPage'
import RegisterPage from './pages/registerPage'
@ -33,9 +34,12 @@ import {useAppSelector, useAppDispatch} from './store/hooks'
import {fetchClientConfig} from './store/clientConfig'
import {IUser} from './user'
import {UserSettings} from './userSettings'
export const history = createBrowserHistory({basename: Utils.getFrontendBaseURL()})
const UUID_REGEX = new RegExp(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
if (Utils.isDesktop() && Utils.isFocalboardPlugin()) {
window.addEventListener('message', (event: MessageEvent) => {
if (event.origin !== window.location.origin) {
@ -102,6 +106,32 @@ const App = React.memo((): JSX.Element => {
setTimeout(() => dispatch(setGlobalError('')), 0)
}
const continueToWelcomeScreen = (boardIdIsValidUUIDV4 = true) => {
return Utils.isFocalboardPlugin() && loggedIn === true && (!UserSettings.welcomePageViewed || !boardIdIsValidUUIDV4)
}
const buildOriginalPath = (workspaceId = '', boardId = '', viewId = '', cardId = '') => {
let originalPath = ''
if (workspaceId) {
originalPath += `${workspaceId}/`
}
if (boardId) {
originalPath += `${boardId}/`
}
if (viewId) {
originalPath += `${viewId}/`
}
if (cardId) {
originalPath += `${cardId}/`
}
return originalPath
}
return (
<IntlProvider
locale={language.split(/[_]/)[0]}
@ -131,24 +161,44 @@ const App = React.memo((): JSX.Element => {
<Route path='/shared/:boardId?/:viewId?'>
<BoardPage readonly={true}/>
</Route>
<Route path='/board/:boardId?/:viewId?/:cardId?'>
{loggedIn === false && <Redirect to='/login'/>}
{loggedIn === true && <BoardPage/>}
</Route>
<Route
path='/board/:boardId?/:viewId?/:cardId?'
render={({match: {params: {boardId, viewId, cardId}}}) => {
if (loggedIn === false) {
return <Redirect to='/login'/>
}
if (continueToWelcomeScreen()) {
const originalPath = `/board/${buildOriginalPath('', boardId, viewId, cardId)}`
return <Redirect to={`/welcome?r=${originalPath}`}/>
}
if (loggedIn === true) {
return <BoardPage/>
}
return null
}}
/>
<Route path='/workspace/:workspaceId/shared/:boardId?/:viewId?'>
<BoardPage readonly={true}/>
</Route>
<Route
path='/workspace/:workspaceId/:boardId?/:viewId?/:cardId?'
render={({match}) => {
render={({match: {params: {workspaceId, boardId, viewId, cardId}}}) => {
const originalPath = `/workspace/${buildOriginalPath(workspaceId, boardId, viewId, cardId)}`
if (loggedIn === false) {
let redirectUrl = '/' + Utils.buildURL(`/workspace/${match.params.workspaceId}/`)
let redirectUrl = '/' + Utils.buildURL(originalPath)
if (redirectUrl.indexOf('//') === 0) {
redirectUrl = redirectUrl.slice(1)
}
const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}`
return <Redirect to={loginUrl}/>
} else if (loggedIn === true) {
if (continueToWelcomeScreen()) {
return <Redirect to={`/welcome?r=${originalPath}`}/>
}
return (
<BoardPage/>
)
@ -162,10 +212,38 @@ const App = React.memo((): JSX.Element => {
>
<DashboardPage/>
</Route>
<Route path='/:boardId?/:viewId?/:cardId?'>
{loggedIn === false && <Redirect to='/login'/>}
{loggedIn === true && <BoardPage/>}
<Route
exact={true}
path='/welcome'
>
<WelcomePage/>
</Route>
<Route
path='/:boardId?/:viewId?/:cardId?'
render={({match: {params: {boardId, viewId, cardId}}}) => {
// Since these 3 path values are optional and they can be anything, we can pass /x/y/z and it will
// match this route however these values may not be valid so we should at the very least check
// board id for descisions made below
const boardIdIsValidUUIDV4 = UUID_REGEX.test(boardId || '')
if (loggedIn === false) {
return <Redirect to='/login'/>
}
if (continueToWelcomeScreen(boardIdIsValidUUIDV4)) {
const originalPath = `/${buildOriginalPath('', boardId, viewId, cardId)}`
const queryString = boardIdIsValidUUIDV4 ? `r=${originalPath}` : ''
return <Redirect to={`/welcome?${queryString}`}/>
}
if (loggedIn === true) {
return <BoardPage/>
}
return null
}}
/>
</Switch>
</div>
</div>

View File

@ -45,7 +45,7 @@ const BoardPage = (props: Props) => {
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string, workspaceId?: string}>()
const [websocketClosed, setWebsocketClosed] = useState(false)
let workspaceId = UserSettings.lastWorkspaceId || '0'
let workspaceId = match.params.workspaceId || UserSettings.lastWorkspaceId || '0'
// TODO: Make this less brittle. This only works because this is the root render function
useEffect(() => {

View File

@ -0,0 +1,22 @@
.WelcomePage {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
padding: 0 40px;
text-align: center;
> div {
max-width: 640px;
}
.Button {
display: inline-flex;
}
}
.WelcomePage__image {
margin: 32px 0;
text-align: center;
}

View File

@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {FormattedMessage} from 'react-intl'
import {useLocation, useHistory} from 'react-router-dom'
import BoardWelcomePNG from '../../../static/boards-welcome.png'
import CompassIcon from '../../widgets/icons/compassIcon'
import {UserSettings} from '../../userSettings'
import './welcomePage.scss'
const WelcomePage = React.memo(() => {
const history = useHistory()
const queryString = new URLSearchParams(useLocation().search)
const goForward = () => {
UserSettings.welcomePageViewed = 'true'
if (queryString.get('r')) {
history.replace(queryString.get('r')!)
return
}
history.replace('/dashboard')
}
if (UserSettings.welcomePageViewed) {
goForward()
return null
}
return (
<div className='WelcomePage'>
<div>
<h1 className='text-heading6'>
<FormattedMessage
id='WelcomePage.Heading'
defaultMessage='Welcome To Boards'
/>
</h1>
<div className='text-base'>
<FormattedMessage
id='WelcomePage.Description'
defaultMessage='Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view'
/>
</div>
<div className='WelcomePage__image'>
<img
src={BoardWelcomePNG}
alt='Boards Welcome Image'
/>
</div>
<button
onClick={goForward}
className='Button filled size--large'
>
<FormattedMessage
id='WelcomePage.Explore.Button'
defaultMessage='Explore'
/>
<CompassIcon
icon='chevron-right'
className='Icon Icon--right'
/>
</button>
</div>
</div>
)
})
export default WelcomePage

7
webapp/src/types/images.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
declare module '*.jpg';
declare module '*.png';
declare module '*.gif';
declare module '*.apng';

View File

@ -14,7 +14,8 @@ enum UserSettingKey {
EmojiMartSkin = 'emoji-mart.skin',
EmojiMartLast = 'emoji-mart.last',
EmojiMartFrequently = 'emoji-mart.frequently',
RandomIcons = 'randomIcons'
RandomIcons = 'randomIcons',
WelcomePageViewed = 'welcomePageViewed'
}
export class UserSettings {
@ -42,6 +43,14 @@ export class UserSettings {
UserSettings.set(UserSettingKey.Language, newValue)
}
static get welcomePageViewed(): string | null {
return UserSettings.get(UserSettingKey.WelcomePageViewed)
}
static set welcomePageViewed(newValue: string | null) {
UserSettings.set(UserSettingKey.WelcomePageViewed, newValue)
}
static get theme(): string | null {
return UserSettings.get(UserSettingKey.Theme)
}

View File

@ -20,6 +20,21 @@
background-color: rgba(var(--center-channel-color-rgb), 0.08);
}
.Icon {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
margin-right: 0;
width: 14px;
}
.Icon--right {
height: 14px;
margin-left: 4px;
width: 14px;
}
&.filled {
color: #fff;
background-color: rgb(var(--button-bg-rgb));
@ -56,9 +71,17 @@
height: 32px;
}
.Icon {
width: 14px;
height: 14px;
margin-right: 0;
&.size--large {
font-size: 16px;
font-weight: 600;
height: 48px;
padding: 0 24px;
.Icon--right {
font-size: 24px;
height: 20px;
margin-left: 10px;
width: 20px;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -42,7 +42,7 @@ function makeCommonConfig() {
},
{
test: /\.html$/,
test: /\.(png|jpg|jpeg|gif|html)$/,
type: 'asset/resource',
},
{
@ -67,7 +67,7 @@ function makeCommonConfig() {
exclude: [/node_modules/],
},
{
test: /\.(png|eot|tiff|svg|woff2|woff|ttf|jpg)$/,
test: /\.(eot|tiff|svg|woff2|woff|ttf)$/,
use: [
{
loader: 'file-loader',