GH-1333: Implement Screen 3 and 4 of v6.0 navigation and FTUE (#1341)

This commit is contained in:
Hossein 2021-10-11 14:16:59 -04:00 committed by GitHub
parent 319dcbfb5d
commit c9aeeb38bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 338 additions and 67 deletions

View File

@ -1,14 +1,90 @@
.EmptyCenterPanel {
display: flex;
flex-direction: column;
overflow: auto;
padding: 80px;
font-size: 15px;
color: rgba(var(--center-channel-color-rgb), 0.7);
color: rgba(var(--center-channel-color-rgb));
.WorkspaceInfo {
b {
padding-left: 5px;
}
}
.content {
display: flex;
flex-direction: column;
justify-items: center;
align-items: center;
.title {
font-size: 25px;
color: var(--sys-center-channel-color);
margin-bottom: 12px;
}
.description {
text-align: center;
margin-bottom: 32px;
color: var(--sys-center-channel-color);
}
.choose-template-text {
color: var(--center-channel-color);
font-weight: 600;
}
.button-container {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.button {
display: flex;
justify-items: center;
align-items: center;
padding: 16px 32px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
box-sizing: border-box;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.08);
border-radius: 8px;
margin: 20px;
max-width: 300px;
span {
margin-right: 10px;
}
.button-title {
font-weight: 600;
margin-right: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
svg {
display: flex;
}
&:hover {
cursor: pointer;
}
}
.new-template {
border: transparent;
box-shadow: none;
color: var(--button-bg);
i {
background: var(--button-bg);
color: var(--button-color);
margin: 0;
}
}
}
}

View File

@ -1,28 +1,82 @@
// 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 React, {useCallback, useEffect} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
import {getCurrentWorkspace} from '../store/workspace'
import {useAppSelector} from '../store/hooks'
import {useAppSelector, useAppDispatch} from '../store/hooks'
import {Utils} from '../utils'
import {Board} from '../blocks/board'
import {getGlobalTemplates, fetchGlobalTemplates} from '../store/globalTemplates'
import {getSortedTemplates} from '../store/boards'
import AddIcon from '../widgets/icons/add'
import BoardIcon from '../widgets/icons/board'
import octoClient from '../octoClient'
import {addBoardTemplateClicked, addBoardClicked} from './sidebar/sidebarAddBoardMenu'
import {addBoardFromTemplate, BoardTemplateButtonMenu} from './sidebar/boardTemplateMenuItem'
import './emptyCenterPanel.scss'
type ButtonProps = {
buttonIcon: string | React.ReactNode,
title: string,
readonly: boolean,
onClick: () => void,
showBoard?: (boardId: string) => void
boardTemplate?: Board
classNames?: string
}
const PanelButton = React.memo((props: ButtonProps) => {
const {onClick, buttonIcon, title, readonly, showBoard, boardTemplate, classNames} = props
return (
<div
onClick={onClick}
className={`button ${classNames || ''}`}
>
<span>{buttonIcon}</span>
<span className='button-title'>{title}</span>
{!readonly && showBoard && boardTemplate &&
<BoardTemplateButtonMenu
showBoard={showBoard}
boardTemplate={boardTemplate}
/>
}
</div>
)
})
const EmptyCenterPanel = React.memo(() => {
const workspace = useAppSelector(getCurrentWorkspace)
const templates = useAppSelector(getSortedTemplates)
const globalTemplates = useAppSelector<Board[]>(getGlobalTemplates)
const history = useHistory()
const dispatch = useAppDispatch()
const intl = useIntl()
const match = useRouteMatch<{boardId: string, viewId?: string}>()
useEffect(() => {
if (octoClient.workspaceId !== '0' && globalTemplates.length === 0) {
dispatch(fetchGlobalTemplates())
}
}, [octoClient.workspaceId])
const showBoard = useCallback((boardId) => {
const params = {...match.params, boardId: boardId || ''}
delete params.viewId
const newPath = generatePath(match.path, params)
history.push(newPath)
}, [match, history])
const newTemplateClicked = () => addBoardTemplateClicked(showBoard, intl)
const emptyBoardClicked = () => addBoardClicked(showBoard, intl)
if (!Utils.isFocalboardPlugin()) {
return (
<div className='EmptyCenterPanel'>
{workspace && workspace.id !== '0' &&
<div className='WorkspaceInfo'>
<FormattedMessage
id='EmptyCenterPanel.workspace'
defaultMessage='This is the workspace for:'
/>
<b>
{workspace.title}
</b>
</div>
}
<div className='Hint'>
<FormattedMessage
id='EmptyCenterPanel.no-content'
@ -31,6 +85,89 @@ const EmptyCenterPanel = React.memo(() => {
</div>
</div>
)
}
return (
<div className='EmptyCenterPanel'>
<div className='content'>
<span className='title'>
<FormattedMessage
id='EmptyCenterPanel.plugin.no-content-title'
defaultMessage='Create a Board in {workspaceName}'
values={{workspaceName: workspace?.title}}
/>
</span>
<span className='description'>
<FormattedMessage
id='EmptyCenterPanel.plugin.no-content-description'
defaultMessage='Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of "{workspaceName}" will have access to boards created here.'
values={{
workspaceName: <b>{workspace?.title}</b>,
lineBreak: <br/>,
}}
/>
</span>
<span className='choose-template-text'>
<FormattedMessage
id='EmptyCenterPanel.plugin.choose-a-template'
defaultMessage='Choose a template'
/>
</span>
<div className='button-container'>
{templates.map((template) =>
(
<PanelButton
key={template.id}
title={template.title}
buttonIcon={template.fields.icon}
readonly={false}
onClick={() => addBoardFromTemplate(intl, showBoard, template.id)}
showBoard={showBoard}
boardTemplate={template}
/>
),
)}
{globalTemplates.map((template) =>
(
<PanelButton
key={template.id}
title={template.title}
buttonIcon={template.fields.icon}
readonly={true}
onClick={() => addBoardFromTemplate(intl, showBoard, template.id, undefined, true)}
/>
),
)}
<PanelButton
key={'new-template'}
title={intl.formatMessage({id: 'EmptyCenterPanel.plugin.new-template', defaultMessage: 'New template'})}
buttonIcon={<AddIcon/>}
readonly={true}
onClick={newTemplateClicked}
classNames='new-template'
/>
</div>
<span className='choose-template-text'>
<FormattedMessage
id='EmptyCenterPanel.plugin.no-content-or'
defaultMessage='or'
/>
</span>
<PanelButton
key={'start-with-an-empty-board'}
title={intl.formatMessage({id: 'EmptyCenterPanel.plugin.empty-board', defaultMessage: 'Start with an Empty Board'})}
buttonIcon={<BoardIcon/>}
readonly={true}
onClick={emptyBoardClicked}
/>
<FormattedMessage
id='EmptyCenterPanel.plugin.end-message'
defaultMessage='You can change the channel using the switcher in the sidebar.'
/>
</div>
</div>
)
})
export default EmptyCenterPanel

View File

@ -13,14 +13,7 @@ import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../telemetry/telemetryClient'
type Props = {
boardTemplate: Board
isGlobal: boolean
showBoard: (id: string) => void
activeBoardId?: string
}
const addBoardFromTemplate = async (intl: IntlShape, showBoard: (id: string) => void, boardTemplateId: string, activeBoardId?: string, global = false) => {
export const addBoardFromTemplate = async (intl: IntlShape, showBoard: (id: string) => void, boardTemplateId: string, activeBoardId?: string, global = false) => {
const oldBoardId = activeBoardId
const afterRedo = async (newBoardId: string) => {
showBoard(newBoardId)
@ -41,22 +34,16 @@ const addBoardFromTemplate = async (intl: IntlShape, showBoard: (id: string) =>
}
}
const BoardTemplateMenuItem = React.memo((props: Props) => {
const {boardTemplate, isGlobal, activeBoardId} = props
const intl = useIntl()
type ButtonProps = {
showBoard: (id: string) => void
boardTemplate: Board
}
const displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'})
export const BoardTemplateButtonMenu = React.memo((props: ButtonProps) => {
const intl = useIntl()
const {showBoard, boardTemplate} = props
return (
<Menu.Text
key={boardTemplate.id || ''}
id={boardTemplate.id || ''}
name={displayName}
icon={<div className='Icon'>{boardTemplate.fields.icon}</div>}
onClick={() => {
addBoardFromTemplate(intl, props.showBoard, boardTemplate.id || '', activeBoardId, isGlobal)
}}
rightIcon={!isGlobal &&
<MenuWrapper stopPropagationOnToggle={true}>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='right'>
@ -65,7 +52,7 @@ const BoardTemplateMenuItem = React.memo((props: Props) => {
id='edit'
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
onClick={() => {
props.showBoard(boardTemplate.id || '')
showBoard(boardTemplate.id || '')
}}
/>
<Menu.Text
@ -78,6 +65,36 @@ const BoardTemplateMenuItem = React.memo((props: Props) => {
/>
</Menu>
</MenuWrapper>
)
})
type Props = {
boardTemplate: Board
isGlobal: boolean
showBoard: (id: string) => void
activeBoardId?: string
}
const BoardTemplateMenuItem = React.memo((props: Props) => {
const {boardTemplate, isGlobal, activeBoardId, showBoard} = props
const intl = useIntl()
const displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'})
return (
<Menu.Text
key={boardTemplate.id || ''}
id={boardTemplate.id || ''}
name={displayName}
icon={<div className='Icon'>{boardTemplate.fields.icon}</div>}
onClick={() => {
addBoardFromTemplate(intl, showBoard, boardTemplate.id || '', activeBoardId, isGlobal)
}}
rightIcon={!isGlobal &&
<BoardTemplateButtonMenu
boardTemplate={boardTemplate}
showBoard={showBoard}
/>
}
/>
)

View File

@ -145,7 +145,7 @@ const Sidebar = React.memo((props: Props) => {
<div className='octo-spacer'/>
{
!props.isDashboard &&
(!props.isDashboard && !Utils.isFocalboardPlugin()) &&
<SidebarAddBoardMenu
activeBoardId={props.activeBoardId}
/>

View File

@ -24,7 +24,7 @@ type Props = {
activeBoardId?: string
}
const addBoardClicked = async (showBoard: (id: string) => void, intl: IntlShape, activeBoardId?: string) => {
export const addBoardClicked = async (showBoard: (id: string) => void, intl: IntlShape, activeBoardId?: string) => {
const oldBoardId = activeBoardId
const board = createBoard()
@ -50,10 +50,11 @@ const addBoardClicked = async (showBoard: (id: string) => void, intl: IntlShape,
)
}
const addBoardTemplateClicked = async (showBoard: (id: string) => void, intl: IntlShape, activeBoardId?: string) => {
export const addBoardTemplateClicked = async (showBoard: (id: string) => void, intl: IntlShape, activeBoardId?: string) => {
const boardTemplate = createBoard()
boardTemplate.rootId = boardTemplate.id
boardTemplate.fields.isTemplate = true
boardTemplate.title = intl.formatMessage({id: 'View.NewTemplateTitle', defaultMessage: 'Untitled Template'})
const view = createBoardView()
view.fields.viewType = 'board'

View File

@ -100,7 +100,9 @@ const Workspace = React.memo((props: Props) => {
defaultMessage="You're editing a board template."
/>
</div>}
<CenterContent readonly={props.readonly}/>
<CenterContent
readonly={props.readonly}
/>
</div>
</div>
)

View File

@ -3,8 +3,8 @@
.WorkspaceSwitcherWrapper {
display: flex;
flex-direction: column;
gap: 4px;
flex-direction: row;
gap: 10px;
width: 100%;
position: relative;
padding: 0 16px;
@ -21,8 +21,22 @@
border-radius: 4px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.add-workspace-icon {
display: flex;
i {
color: rgb(var(--sidebar-text-rgb));
align-self: center;
}
&:hover {
cursor: pointer;
}
}
}
.WorkspaceSwitcher {
display: flex;
flex-direction: row;
@ -31,6 +45,8 @@
padding: 10px 16px;
cursor: pointer;
white-space: nowrap;
flex: 1;
max-width: 175px;
span {
max-width: 90%;

View File

@ -7,6 +7,10 @@ import {useHistory} from 'react-router-dom'
import {IWorkspace} from '../../blocks/workspace'
import ChevronDown from '../../widgets/icons/chevronDown'
import AddIcon from '../../widgets/icons/add'
import {setCurrent as setCurrentBoard} from '../../store/boards'
import {setCurrent as setCurrentView} from '../../store/views'
import {useAppDispatch} from '../../store/hooks'
import {UserSettings} from '../../userSettings'
@ -18,9 +22,18 @@ type Props = {
const WorkspaceSwitcher = (props: Props): JSX.Element => {
const history = useHistory()
const {activeWorkspace} = props
const dispatch = useAppDispatch()
const [showMenu, setShowMenu] = useState<boolean>(false)
const goToEmptyCenterPanel = () => {
UserSettings.lastBoardId = null
UserSettings.lastViewId = null
dispatch(setCurrentBoard(''))
dispatch(setCurrentView(''))
history.replace(`/workspace/${activeWorkspace?.id}`)
}
return (
<div className={'WorkspaceSwitcherWrapper'}>
<div
@ -31,13 +44,13 @@ const WorkspaceSwitcher = (props: Props): JSX.Element => {
}
}}
>
<span>{props.activeWorkspace?.title || DashboardOption.label}</span>
<span>{activeWorkspace?.title || DashboardOption.label}</span>
<ChevronDown/>
</div>
{
showMenu &&
<WorkspaceOptions
activeWorkspaceId={props.activeWorkspace?.id || DashboardOption.value}
activeWorkspaceId={activeWorkspace?.id || DashboardOption.value}
onBlur={() => {
setShowMenu(false)
}}
@ -56,6 +69,14 @@ const WorkspaceSwitcher = (props: Props): JSX.Element => {
}}
/>
}
{activeWorkspace &&
<span
className='add-workspace-icon'
onClick={goToEmptyCenterPanel}
>
<AddIcon/>
</span>
}
</div>
)
}

View File

@ -3,7 +3,7 @@
import React, {useEffect, useState} from 'react'
import {batch} from 'react-redux'
import {FormattedMessage, useIntl} from 'react-intl'
import {generatePath, Redirect, useHistory, useRouteMatch} from 'react-router-dom'
import {generatePath, Redirect, useHistory, useRouteMatch, useLocation} from 'react-router-dom'
import {useHotkeys} from 'react-hotkeys-hook'
import {Block} from '../blocks/block'
@ -47,6 +47,7 @@ const BoardPage = (props: Props): JSX.Element => {
const history = useHistory()
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string, workspaceId?: string}>()
const [websocketClosed, setWebsocketClosed] = useState(false)
const queryString = new URLSearchParams(useLocation().search)
const [mobileWarningClosed, setMobileWarningClosed] = useState(UserSettings.mobileWarningClosed)
let workspaceId = match.params.workspaceId || UserSettings.lastWorkspaceId || '0'
@ -66,7 +67,6 @@ const BoardPage = (props: Props): JSX.Element => {
useEffect(() => {
// Backward compatibility: This can be removed in the future, this is for
// transform the old query params into routes
const queryString = new URLSearchParams(history.location.search)
const queryBoardId = queryString.get('id')
const params = {...match.params}
let needsRedirect = false
@ -150,7 +150,6 @@ const BoardPage = (props: Props): JSX.Element => {
let token = localStorage.getItem('focalboardSessionId') || ''
if (props.readonly) {
loadAction = initialReadOnlyLoad
const queryString = new URLSearchParams(history.location.search)
token = token || queryString.get('r') || ''
}
dispatch(loadAction(match.params.boardId))
@ -286,7 +285,9 @@ const BoardPage = (props: Props): JSX.Element => {
<div className='error'>
{intl.formatMessage({id: 'BoardPage.syncFailed', defaultMessage: 'Board may be deleted or access revoked.'})}
</div>}
<Workspace readonly={props.readonly || false}/>
<Workspace
readonly={props.readonly || false}
/>
</div>
)
}