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:
commit
68dc26647b
@ -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",
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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/>}
|
||||
|
@ -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() &&
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -186,6 +186,8 @@ class Constants {
|
||||
Y: ['y', 89],
|
||||
Z: ['z', 90],
|
||||
}
|
||||
|
||||
static readonly globalTeamId = '0'
|
||||
}
|
||||
|
||||
export {Constants, Permission}
|
||||
|
@ -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'})
|
||||
|
||||
|
@ -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}`
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
},
|
||||
)
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user