From c9aeeb38bfb3eeda7977c61b5c74439c77b89a78 Mon Sep 17 00:00:00 2001 From: Hossein Date: Mon, 11 Oct 2021 14:16:59 -0400 Subject: [PATCH] GH-1333: Implement Screen 3 and 4 of v6.0 navigation and FTUE (#1341) --- webapp/src/components/emptyCenterPanel.scss | 80 +++++++- webapp/src/components/emptyCenterPanel.tsx | 179 ++++++++++++++++-- .../sidebar/boardTemplateMenuItem.tsx | 79 +++++--- webapp/src/components/sidebar/sidebar.tsx | 2 +- .../sidebar/sidebarAddBoardMenu.tsx | 5 +- webapp/src/components/workspace.tsx | 4 +- .../workspaceSwitcher/workspaceSwitcher.scss | 20 +- .../workspaceSwitcher/workspaceSwitcher.tsx | 27 ++- webapp/src/pages/boardPage.tsx | 9 +- 9 files changed, 338 insertions(+), 67 deletions(-) diff --git a/webapp/src/components/emptyCenterPanel.scss b/webapp/src/components/emptyCenterPanel.scss index f19469a06..89bc813f0 100644 --- a/webapp/src/components/emptyCenterPanel.scss +++ b/webapp/src/components/emptyCenterPanel.scss @@ -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; + } + } + + } } diff --git a/webapp/src/components/emptyCenterPanel.tsx b/webapp/src/components/emptyCenterPanel.tsx index 998af12f6..a3962d501 100644 --- a/webapp/src/components/emptyCenterPanel.tsx +++ b/webapp/src/components/emptyCenterPanel.tsx @@ -1,36 +1,173 @@ // 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' -const EmptyCenterPanel = React.memo(() => { - const workspace = useAppSelector(getCurrentWorkspace) +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 ( -
- {workspace && workspace.id !== '0' && -
- + {buttonIcon} + {title} + {!readonly && showBoard && boardTemplate && + - - {workspace.title} - -
} -
- -
) }) +const EmptyCenterPanel = React.memo(() => { + const workspace = useAppSelector(getCurrentWorkspace) + const templates = useAppSelector(getSortedTemplates) + const globalTemplates = useAppSelector(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 ( +
+
+ +
+
+ ) + } + + return ( +
+
+ + + + + {workspace?.title}, + lineBreak:
, + }} + /> +
+ + + +
+ {templates.map((template) => + ( + addBoardFromTemplate(intl, showBoard, template.id)} + showBoard={showBoard} + boardTemplate={template} + /> + ), + )} + {globalTemplates.map((template) => + ( + addBoardFromTemplate(intl, showBoard, template.id, undefined, true)} + /> + ), + )} + } + readonly={true} + onClick={newTemplateClicked} + classNames='new-template' + /> +
+ + + + } + readonly={true} + onClick={emptyBoardClicked} + /> + +
+
+ + ) +}) + export default EmptyCenterPanel diff --git a/webapp/src/components/sidebar/boardTemplateMenuItem.tsx b/webapp/src/components/sidebar/boardTemplateMenuItem.tsx index 33b9851b0..971843f28 100644 --- a/webapp/src/components/sidebar/boardTemplateMenuItem.tsx +++ b/webapp/src/components/sidebar/boardTemplateMenuItem.tsx @@ -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,8 +34,49 @@ const addBoardFromTemplate = async (intl: IntlShape, showBoard: (id: string) => } } +type ButtonProps = { + showBoard: (id: string) => void + boardTemplate: Board +} + +export const BoardTemplateButtonMenu = React.memo((props: ButtonProps) => { + const intl = useIntl() + const {showBoard, boardTemplate} = props + + return ( + + }/> + + } + id='edit' + name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})} + onClick={() => { + showBoard(boardTemplate.id || '') + }} + /> + } + id='delete' + name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})} + onClick={async () => { + await mutator.deleteBlock(boardTemplate, 'delete board template') + }} + /> + + + ) +}) + +type Props = { + boardTemplate: Board + isGlobal: boolean + showBoard: (id: string) => void + activeBoardId?: string +} + const BoardTemplateMenuItem = React.memo((props: Props) => { - const {boardTemplate, isGlobal, activeBoardId} = props + const {boardTemplate, isGlobal, activeBoardId, showBoard} = props const intl = useIntl() const displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'}) @@ -54,30 +88,13 @@ const BoardTemplateMenuItem = React.memo((props: Props) => { name={displayName} icon={
{boardTemplate.fields.icon}
} onClick={() => { - addBoardFromTemplate(intl, props.showBoard, boardTemplate.id || '', activeBoardId, isGlobal) + addBoardFromTemplate(intl, showBoard, boardTemplate.id || '', activeBoardId, isGlobal) }} rightIcon={!isGlobal && - - }/> - - } - id='edit' - name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})} - onClick={() => { - props.showBoard(boardTemplate.id || '') - }} - /> - } - id='delete' - name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})} - onClick={async () => { - await mutator.deleteBlock(boardTemplate, 'delete board template') - }} - /> - - + } /> ) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index 8b4a58228..9471446c4 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -145,7 +145,7 @@ const Sidebar = React.memo((props: Props) => {
{ - !props.isDashboard && + (!props.isDashboard && !Utils.isFocalboardPlugin()) && diff --git a/webapp/src/components/sidebar/sidebarAddBoardMenu.tsx b/webapp/src/components/sidebar/sidebarAddBoardMenu.tsx index 68eb649d1..3641e81c6 100644 --- a/webapp/src/components/sidebar/sidebarAddBoardMenu.tsx +++ b/webapp/src/components/sidebar/sidebarAddBoardMenu.tsx @@ -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' diff --git a/webapp/src/components/workspace.tsx b/webapp/src/components/workspace.tsx index 710533846..ec06cba1d 100644 --- a/webapp/src/components/workspace.tsx +++ b/webapp/src/components/workspace.tsx @@ -100,7 +100,9 @@ const Workspace = React.memo((props: Props) => { defaultMessage="You're editing a board template." />
} - + ) diff --git a/webapp/src/components/workspaceSwitcher/workspaceSwitcher.scss b/webapp/src/components/workspaceSwitcher/workspaceSwitcher.scss index a3180c488..21af1ab01 100644 --- a/webapp/src/components/workspaceSwitcher/workspaceSwitcher.scss +++ b/webapp/src/components/workspaceSwitcher/workspaceSwitcher.scss @@ -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%; diff --git a/webapp/src/components/workspaceSwitcher/workspaceSwitcher.tsx b/webapp/src/components/workspaceSwitcher/workspaceSwitcher.tsx index 47dfa7677..f3dede2eb 100644 --- a/webapp/src/components/workspaceSwitcher/workspaceSwitcher.tsx +++ b/webapp/src/components/workspaceSwitcher/workspaceSwitcher.tsx @@ -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(false) + const goToEmptyCenterPanel = () => { + UserSettings.lastBoardId = null + UserSettings.lastViewId = null + dispatch(setCurrentBoard('')) + dispatch(setCurrentView('')) + history.replace(`/workspace/${activeWorkspace?.id}`) + } + return (
{ } }} > - {props.activeWorkspace?.title || DashboardOption.label} + {activeWorkspace?.title || DashboardOption.label}
{ showMenu && { setShowMenu(false) }} @@ -56,6 +69,14 @@ const WorkspaceSwitcher = (props: Props): JSX.Element => { }} /> } + {activeWorkspace && + + + + }
) } diff --git a/webapp/src/pages/boardPage.tsx b/webapp/src/pages/boardPage.tsx index cd97d3533..16108b973 100644 --- a/webapp/src/pages/boardPage.tsx +++ b/webapp/src/pages/boardPage.tsx @@ -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 => {
{intl.formatMessage({id: 'BoardPage.syncFailed', defaultMessage: 'Board may be deleted or access revoked.'})}
} - + ) }