1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-20 18:28:25 +02:00

Merge pull request #2694 from mattermost/missing-board-options

Missing board options in sidebar
This commit is contained in:
Scott Bishel 2022-03-31 07:40:50 -06:00 committed by GitHub
commit 68dc26647b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 130 additions and 33 deletions

View File

@ -185,6 +185,8 @@
"Sidebar.add-board": "+ Add board",
"Sidebar.changePassword": "Change password",
"Sidebar.delete-board": "Delete board",
"Sidebar.duplicate-board": "Duplicate board",
"Sidebar.template-from-board": "New template from board",
"Sidebar.export-archive": "Export archive",
"Sidebar.import": "Import",
"Sidebar.import-archive": "Import archive",

View File

@ -231,7 +231,6 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
const editIcon = screen.getByText(template1Title).parentElement?.querySelector('.EditIcon')
expect(editIcon).not.toBeNull()
userEvent.click(editIcon!)
expect(history.push).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click to add board from template', async () => {
render(wrapDNDIntl(

View File

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useEffect, useState, useCallback, useMemo} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
import {useHistory, useRouteMatch} from 'react-router-dom'
import {Board} from '../../blocks/board'
import IconButton from '../../widgets/buttons/iconButton'
@ -23,6 +23,10 @@ import {IUser, UserConfigPatch, UserPropPrefix} from '../../user'
import {getMe, patchProps} from '../../store/users'
import {BaseTourSteps, TOUR_BASE} from '../onboardingTour'
import {Utils} from "../../utils"
import {Constants} from "../../constants"
import BoardTemplateSelectorPreview from './boardTemplateSelectorPreview'
import BoardTemplateSelectorItem from './boardTemplateSelectorItem'
@ -44,17 +48,14 @@ const BoardTemplateSelector = (props: Props) => {
const me = useAppSelector<IUser|null>(getMe)
const showBoard = useCallback(async (boardId) => {
const params = {...match.params, boardId: boardId || ''}
delete params.viewId
const newPath = generatePath(match.path, params)
history.push(newPath)
Utils.showBoard(boardId, match, history)
if (onClose) {
onClose()
}
}, [match, history, onClose])
useEffect(() => {
if (octoClient.teamId !== '0' && globalTemplates.length === 0) {
if (octoClient.teamId !== Constants.globalTeamId && globalTemplates.length === 0) {
dispatch(fetchGlobalTemplates())
}
}, [octoClient.teamId])
@ -96,7 +97,7 @@ const BoardTemplateSelector = (props: Props) => {
}
const handleUseTemplate = async () => {
await mutator.addBoardFromTemplate(currentTeam?.id || '0', intl, showBoard, () => showBoard(currentBoardId), activeTemplate.id, currentTeam?.id)
await mutator.addBoardFromTemplate(currentTeam?.id || Constants.globalTeamId, intl, showBoard, () => showBoard(currentBoardId), activeTemplate.id, currentTeam?.id)
if (activeTemplate.title === OnboardingBoardTitle) {
resetTour()
}

View File

@ -10,6 +10,7 @@ import EditIcon from '../../widgets/icons/edit'
import DeleteBoardDialog from '../sidebar/deleteBoardDialog'
import './boardTemplateSelectorItem.scss'
import {Constants} from "../../constants"
type Props = {
isActive: boolean
@ -38,7 +39,9 @@ const BoardTemplateSelectorItem = (props: Props) => {
>
<span className='template-icon'>{template.icon}</span>
<span className='template-name'>{template.title}</span>
{!template.templateVersion &&
{/* don't show template menu options for default templates */}
{template.teamId !== Constants.globalTeamId &&
<div className='actions'>
<IconButton
icon={<DeleteIcon/>}

View File

@ -29,6 +29,8 @@ import wsClient, {WSClient} from '../../wsclient'
import {getCurrentTeam} from '../../store/teams'
import {Constants} from "../../constants"
import SidebarCategory from './sidebarCategory'
import SidebarSettingsMenu from './sidebarSettingsMenu'
import SidebarUserMenu from './sidebarUserMenu'
@ -152,7 +154,7 @@ const Sidebar = (props: Props) => {
</div>
</div>}
{team && team.id !== '0' &&
{team && team.id !== Constants.globalTeamId &&
<div className='WorkspaceTitle'>
{Utils.isFocalboardPlugin() &&
<>

View File

@ -1,7 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react'
import React, {useCallback, useState} from 'react'
import {useIntl} from 'react-intl'
import {useHistory, useRouteMatch} from "react-router-dom"
import {Board} from '../../blocks/board'
import {BoardView, IViewType} from '../../blocks/boardView'
@ -27,6 +28,10 @@ import CalendarIcon from '../../widgets/icons/calendar'
import {getCurrentTeam} from '../../store/teams'
import {Permission} from '../../constants'
import DuplicateIcon from "../../widgets/icons/duplicate"
import {Utils} from "../../utils"
import AddIcon from "../../widgets/icons/add"
const iconForViewType = (viewType: IViewType): JSX.Element => {
switch (viewType) {
@ -58,6 +63,9 @@ const SidebarBoardItem = (props: Props) => {
const currentViewId = useAppSelector(getCurrentViewId)
const teamID = team?.id || ''
const match = useRouteMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>()
const history = useHistory()
const generateMoveToCategoryOptions = (blockID: string) => {
return props.allCategories.map((category) => (
<Menu.Text
@ -74,6 +82,28 @@ const SidebarBoardItem = (props: Props) => {
}
const board = props.board
const handleDuplicateBoard = useCallback(async(asTemplate: boolean) => {
const blocksAndBoards = await mutator.duplicateBoard(
board.id,
undefined,
asTemplate,
undefined,
() => {
Utils.showBoard(board.id, match, history)
return Promise.resolve()
}
)
if (blocksAndBoards.boards.length === 0) {
return
}
const boardId = blocksAndBoards.boards[0].id
Utils.showBoard(boardId, match, history)
}, [board.id])
const title = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'})
return (
<>
@ -129,6 +159,18 @@ const SidebarBoardItem = (props: Props) => {
>
{generateMoveToCategoryOptions(board.id)}
</Menu.SubMenu>
<Menu.Text
id='duplicateBoard'
name={intl.formatMessage({id: 'Sidebar.duplicate-board', defaultMessage: 'Duplicate board'})}
icon={<DuplicateIcon/>}
onClick={() => handleDuplicateBoard(board.isTemplate)}
/>
<Menu.Text
id='templateFromBoard'
name={intl.formatMessage({id: 'Sidebar.template-from-board', defaultMessage: 'New template from board'})}
icon={<AddIcon/>}
onClick={() => handleDuplicateBoard(true)}
/>
</Menu>
</MenuWrapper>
</div>

View File

@ -60,15 +60,7 @@ const SidebarCategory = (props: Props) => {
const teamID = team?.id || ''
const showBoard = useCallback((boardId) => {
// if the same board, reuse the match params
// otherwise remove viewId and cardId, results in first view being selected
const params = {...match.params, boardId: boardId || ''}
if (boardId !== match.params.boardId) {
params.viewId = undefined
params.cardId = undefined
}
const newPath = generatePath(match.path, params)
history.push(newPath)
Utils.showBoard(boardId, match, history)
props.hideSidebar()
}, [match, history])

View File

@ -17,6 +17,8 @@
background-color: rgba(230, 220, 192, 0.9);
text-align: center;
padding: 10px;
color: rgb(63, 67, 80);
font-weight: bold;
}
}
}

View File

@ -186,6 +186,8 @@ class Constants {
Y: ['y', 89],
Z: ['z', 90],
}
static readonly globalTeamId = '0'
}
export {Constants, Permission}

View File

@ -1014,7 +1014,7 @@ class Mutator {
afterRedo?: (newBoardId: string) => Promise<void>,
beforeUndo?: () => Promise<void>,
toTeam?: string,
): Promise<[Block[], string]> {
): Promise<BoardsAndBlocks> {
return undoManager.perform(
async () => {
const boardsAndBlocks = await octoClient.duplicateBoard(boardId, asTemplate, toTeam)
@ -1047,7 +1047,7 @@ class Mutator {
beforeUndo: () => Promise<void>,
boardTemplateId: string,
toTeam?: string,
): Promise<[Block[], string]> {
): Promise<BoardsAndBlocks> {
const asTemplate = false
const actionDescription = intl.formatMessage({id: 'Mutator.new-board-from-template', defaultMessage: 'new board from template'})

View File

@ -12,6 +12,7 @@ import {Category, CategoryBlocks} from './store/sidebar'
import {Team} from './store/teams'
import {Subscription} from './wsclient'
import {PrepareOnboardingResponse} from './onboardingTour'
import {Constants} from "./constants"
//
// OctoClient is the client interface to the server APIs
@ -45,7 +46,7 @@ class OctoClient {
localStorage.setItem('focalboardSessionId', value)
}
constructor(serverUrl?: string, public teamId = '0') {
constructor(serverUrl?: string, public teamId = Constants.globalTeamId) {
this.serverUrl = serverUrl
}
@ -144,7 +145,7 @@ class OctoClient {
private teamPath(teamId?: string): string {
let teamIdToUse = teamId
if (!teamId) {
teamIdToUse = this.teamId === '0' ? UserSettings.lastTeamId || this.teamId : this.teamId
teamIdToUse = this.teamId === Constants.globalTeamId ? UserSettings.lastTeamId || this.teamId : this.teamId
}
return `/api/v1/teams/${teamIdToUse}`

View File

@ -22,6 +22,8 @@ import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../teleme
import {fetchUserBlockSubscriptions, getMe} from '../../store/users'
import {IUser} from '../../user'
import {Constants} from "../../constants"
import SetWindowTitleAndIcon from './setWindowTitleAndIcon'
import TeamToBoardAndViewRedirect from './teamToBoardAndViewRedirect'
import UndoRedoHotKeys from './undoRedoHotKeys'
@ -41,7 +43,7 @@ const BoardPage = (props: Props): JSX.Element => {
const dispatch = useAppDispatch()
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string, teamId?: string}>()
const [mobileWarningClosed, setMobileWarningClosed] = useState(UserSettings.mobileWarningClosed)
const teamId = match.params.teamId || UserSettings.lastTeamId || '0'
const teamId = match.params.teamId || UserSettings.lastTeamId || Constants.globalTeamId
const me = useAppSelector<IUser|null>(getMe)
// if we're in a legacy route and not showing a shared board,
@ -110,7 +112,7 @@ const BoardPage = (props: Props): JSX.Element => {
// and set it as most recently viewed board
UserSettings.setLastBoardID(teamId, match.params.boardId)
if (match.params.viewId && match.params.viewId !== '0') {
if (match.params.viewId && match.params.viewId !== Constants.globalTeamId) {
dispatch(setCurrentView(match.params.viewId))
UserSettings.setLastViewId(match.params.boardId, match.params.viewId)
}

View File

@ -8,6 +8,7 @@ import {setCurrent as setCurrentView, getCurrentBoardViews} from '../../store/vi
import {useAppSelector, useAppDispatch} from '../../store/hooks'
import {UserSettings} from '../../userSettings'
import {getSidebarCategories} from '../../store/sidebar'
import {Constants} from "../../constants"
const TeamToBoardAndViewRedirect = (): null => {
const boardId = useAppSelector(getCurrentBoardId)
@ -16,7 +17,7 @@ const TeamToBoardAndViewRedirect = (): null => {
const history = useHistory()
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string, teamId?: string}>()
const categories = useAppSelector(getSidebarCategories)
const teamId = match.params.teamId || UserSettings.lastTeamId || '0'
const teamId = match.params.teamId || UserSettings.lastTeamId || Constants.globalTeamId
useEffect(() => {
let boardID = match.params.boardId

View File

@ -22,6 +22,7 @@ import {useAppSelector, useAppDispatch} from '../../store/hooks'
import {followBlock, getMe, unfollowBlock} from '../../store/users'
import {IUser} from '../../user'
import {Constants} from "../../constants"
const websocketTimeoutForBanner = 5000
@ -49,8 +50,8 @@ const WebsocketConnection = (props: Props) => {
useEffect(() => {
let subscribedToTeam = false
if (wsClient.state === 'open') {
wsClient.authenticate(props.teamId || '0', token)
wsClient.subscribeToTeam(props.teamId || '0')
wsClient.authenticate(props.teamId || Constants.globalTeamId, token)
wsClient.subscribeToTeam(props.teamId || Constants.globalTeamId)
subscribedToTeam = true
}
@ -71,7 +72,7 @@ const WebsocketConnection = (props: Props) => {
const incrementalBoardUpdate = (_: WSClient, boards: Board[]) => {
// only takes into account the entities that belong to the team or the user boards
const teamBoards = boards.filter((b: Board) => b.teamId === '0' || b.teamId === props.teamId)
const teamBoards = boards.filter((b: Board) => b.teamId === Constants.globalTeamId || b.teamId === props.teamId)
dispatch(updateBoards(teamBoards))
}
@ -83,8 +84,8 @@ const WebsocketConnection = (props: Props) => {
const updateWebsocketState = (_: WSClient, newState: 'init'|'open'|'close'): void => {
if (newState === 'open') {
const newToken = localStorage.getItem('focalboardSessionId') || ''
wsClient.authenticate(props.teamId || '0', newToken)
wsClient.subscribeToTeam(props.teamId || '0')
wsClient.authenticate(props.teamId || Constants.globalTeamId, newToken)
wsClient.subscribeToTeam(props.teamId || Constants.globalTeamId)
subscribedToTeam = true
}

View File

@ -6,6 +6,8 @@ import {createSlice, createAsyncThunk} from '@reduxjs/toolkit'
import {default as client} from '../octoClient'
import {Board} from '../blocks/board'
import {Constants} from "../constants"
import {RootState} from './index'
// ToDo: move this to team templates or simply templates
@ -13,7 +15,7 @@ import {RootState} from './index'
export const fetchGlobalTemplates = createAsyncThunk(
'globalTemplates/fetch',
async () => {
const templates = await client.getTeamTemplates('0')
const templates = await client.getTeamTemplates(Constants.globalTeamId)
return templates.sort((a, b) => a.title.localeCompare(b.title))
},
)

View File

@ -3,6 +3,10 @@
import {createIntl} from 'react-intl'
import {createMemoryHistory} from "history"
import {match as routerMatch} from "react-router-dom"
import {Utils, IDType} from './utils'
import {IAppWindow} from './types'
@ -161,4 +165,25 @@ describe('utils', () => {
expect(Utils.compareVersions('10.9.4', '10.9.2')).toBe(-1)
})
})
describe('showBoard test', () => {
it('should switch boards', () => {
const match = {
params: {
boardId: 'board_id_1',
viewId: 'view_id_1',
cardId: 'card_id_1',
teamId: 'team_id_1',
},
path: '/team/:teamId/:boardId?/:viewId?/:cardId?',
} as unknown as routerMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>
const history = createMemoryHistory()
history.push = jest.fn()
Utils.showBoard('board_id_2', match, history)
expect(history.push).toBeCalledWith('/team/team_id_1/board_id_2')
})
})
})

View File

@ -4,6 +4,10 @@ import {marked} from 'marked'
import {IntlShape} from 'react-intl'
import moment from 'moment'
import {generatePath, match as routerMatch} from "react-router-dom"
import {History} from "history"
import {Block} from './blocks/block'
import {Board as BoardType, BoardMember, createBoard} from './blocks/board'
import {createBoardView} from './blocks/boardView'
@ -703,6 +707,22 @@ class Utils {
}
return (Utils.isMac() && e.metaKey) || (!Utils.isMac() && e.ctrlKey && !e.altKey)
}
static showBoard(
boardId: string,
match: routerMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>,
history: History,
) {
// if the same board, reuse the match params
// otherwise remove viewId and cardId, results in first view being selected
const params = {...match.params, boardId: boardId || ''}
if (boardId !== match.params.boardId) {
params.viewId = undefined
params.cardId = undefined
}
const newPath = generatePath(match.path, params)
history.push(newPath)
}
}
export {Utils, IDType}