1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-21 13:38:56 +02:00

Improve the board creation from channels (#3415) (#3433)

* Improve the board creation from channels

* Fixing linter problem and adding channelID to the telemetry information

* Fixing and expanding a bit the tests

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
(cherry picked from commit 93bc9de731)

Co-authored-by: Jesús Espino <jespinog@gmail.com>
This commit is contained in:
Mattermost Build 2022-07-27 13:26:17 +03:00 committed by GitHub
parent 6d5fa273b6
commit 3da7ca8e74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 94 additions and 38 deletions

View File

@ -12,8 +12,7 @@ import {useWebsockets} from '../../../../webapp/src/hooks/websockets'
import octoClient from '../../../../webapp/src/octoClient'
import mutator from '../../../../webapp/src/mutator'
import {getCurrentTeamId, getAllTeams, Team} from '../../../../webapp/src/store/teams'
import {createBoard, BoardsAndBlocks, Board} from '../../../../webapp/src/blocks/board'
import {createBoardView} from '../../../../webapp/src/blocks/boardView'
import {createBoard, Board} from '../../../../webapp/src/blocks/board'
import {useAppSelector, useAppDispatch} from '../../../../webapp/src/store/hooks'
import {EmptySearch, EmptyResults} from '../../../../webapp/src/components/searchDialog/searchDialog'
import ConfirmationDialog from '../../../../webapp/src/components/confirmationDialogBox'
@ -21,11 +20,13 @@ import Dialog from '../../../../webapp/src/components/dialog'
import SearchIcon from '../../../../webapp/src/widgets/icons/search'
import Button from '../../../../webapp/src/widgets/buttons/button'
import {getCurrentLinkToChannel, setLinkToChannel} from '../../../../webapp/src/store/boards'
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../../webapp/src/telemetry/telemetryClient'
import {WSClient} from '../../../../webapp/src/wsclient'
import {SuiteWindow} from '../../../../webapp/src/types/index'
import BoardSelectorItem from './boardSelectorItem'
const windowAny = (window as SuiteWindow)
import './boardSelector.scss'
const BoardSelector = () => {
@ -107,27 +108,8 @@ const BoardSelector = () => {
}
const newLinkedBoard = async (): Promise<void> => {
const board = {...createBoard(), teamId, channelId: currentChannel}
const view = createBoardView()
view.fields.viewType = 'board'
view.parentId = board.id
view.boardId = board.id
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board view'})
await mutator.createBoardsAndBlocks(
{boards: [board], blocks: [view]},
'add linked board',
async (bab: BoardsAndBlocks): Promise<void> => {
const windowAny: any = window
const newBoard = bab.boards[0]
// TODO: Maybe create a new event for create linked board
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoard, {board: newBoard?.id})
windowAny.WebappUtils.browserHistory.push(`/boards/team/${teamId}/${newBoard.id}`)
dispatch(setLinkToChannel(''))
},
async () => {return},
)
window.open(`${windowAny.frontendBaseURL}/team/${teamId}/new/${currentChannel}`, '_blank', 'noopener')
dispatch(setLinkToChannel(''))
}
return (

View File

@ -284,8 +284,9 @@ export default class Plugin {
const goToFocalboardTemplate = () => {
const currentTeam = mmStore.getState().entities.teams.currentTeamId
const currentChannel = mmStore.getState().entities.channels.currentChannelId
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {teamID: currentTeam})
window.open(`${windowAny.frontendBaseURL}/team/${currentTeam}`, '_blank', 'noopener')
window.open(`${windowAny.frontendBaseURL}/team/${currentTeam}/new/${currentChannel}`, '_blank', 'noopener')
}
if (registry.registerChannelIntroButtonAction) {

View File

@ -15,6 +15,7 @@ import {MemoryRouter, Router} from 'react-router-dom'
import Mutator from '../../mutator'
import {Utils} from '../../utils'
import {Team} from '../../store/teams'
import {createBoard, Board} from '../../blocks/board'
import {IUser} from '../../user'
import {mockDOM, mockStateStore, wrapDNDIntl} from '../../testUtils'
@ -227,17 +228,22 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
userEvent.click(divNewTemplate!)
expect(mockedMutator.addEmptyBoardTemplate).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click empty board', () => {
test('return BoardTemplateSelector and click empty board', async () => {
const newBoard = createBoard({id: 'new-board'} as Board)
mockedMutator.addEmptyBoard.mockResolvedValue({boards: [newBoard], blocks: []})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divEmptyboard = screen.getByText('Create empty board').parentElement
expect(divEmptyboard).not.toBeNull()
userEvent.click(divEmptyboard!)
expect(mockedMutator.addEmptyBoard).toBeCalledTimes(1)
await waitFor(() => expect(mockedMutator.updateBoard).toBeCalledWith(newBoard, newBoard, 'linked channel'))
})
test('return BoardTemplateSelector and click delete template icon', async () => {
const root = document.createElement('div')
@ -279,6 +285,9 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
userEvent.click(editIcon!)
})
test('return BoardTemplateSelector and click to add board from template', async () => {
const newBoard = createBoard({id: 'new-board'} as Board)
mockedMutator.addBoardFromTemplate.mockResolvedValue({boards: [newBoard], blocks: []})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
@ -300,8 +309,44 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), '1', team1.id))
await waitFor(() => expect(mockedMutator.updateBoard).toBeCalledWith(newBoard, newBoard, 'linked channel'))
})
test('return BoardTemplateSelector and click to add board from template with channelId', async () => {
const newBoard = createBoard({id: 'new-board'} as Board)
mockedMutator.addBoardFromTemplate.mockResolvedValue({boards: [newBoard], blocks: []})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector
onClose={jest.fn()}
channelId='test-channel'
/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divBoardToSelect = screen.getByText(template1Title).parentElement
expect(divBoardToSelect).not.toBeNull()
act(() => {
userEvent.click(divBoardToSelect!)
})
const useTemplateButton = screen.getByText('Use this template').parentElement
expect(useTemplateButton).not.toBeNull()
act(() => {
userEvent.click(useTemplateButton!)
})
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), '1', team1.id))
await waitFor(() => expect(mockedMutator.updateBoard).toBeCalledWith({...newBoard, channelId: 'test-channel'}, newBoard, 'linked channel'))
})
test('return BoardTemplateSelector and click to add board from global template', async () => {
const newBoard = createBoard({id: 'new-board'} as Board)
mockedMutator.addBoardFromTemplate.mockResolvedValue({boards: [newBoard], blocks: []})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
@ -323,8 +368,12 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), 'global-1', team1.id))
await waitFor(() => expect(mockedTelemetry.trackEvent).toBeCalledWith('boards', 'createBoardViaTemplate', {boardTemplateId: 'template_id_global'}))
await waitFor(() => expect(mockedMutator.updateBoard).toBeCalledWith(newBoard, newBoard, 'linked channel'))
})
test('should start product tour on choosing welcome template', async () => {
const newBoard = createBoard({id: 'new-board'} as Board)
mockedMutator.addBoardFromTemplate.mockResolvedValue({boards: [newBoard], blocks: []})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
@ -347,6 +396,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), '2', team1.id))
await waitFor(() => expect(mockedTelemetry.trackEvent).toBeCalledWith('boards', 'createBoardViaTemplate', {boardTemplateId: 'template_id_2'}))
await waitFor(() => expect(mockedMutator.updateBoard).toBeCalledWith(newBoard, newBoard, 'linked channel'))
expect(mockedOctoClient.patchUserConfig).toBeCalledWith('user-id-1', {
updatedFields: {
'focalboard_onboardingTourStarted': '1',

View File

@ -34,6 +34,7 @@ type Props = {
title?: React.ReactNode
description?: React.ReactNode
onClose?: () => void
channelId?: string
}
const BoardTemplateSelector = (props: Props) => {
@ -99,10 +100,12 @@ const BoardTemplateSelector = (props: Props) => {
const handleUseTemplate = async () => {
if (activeTemplate.teamId === '0') {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardViaTemplate, {boardTemplateId: activeTemplate.properties.trackingTemplateId as string})
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardViaTemplate, {boardTemplateId: activeTemplate.properties.trackingTemplateId as string, channelID: props.channelId})
}
await mutator.addBoardFromTemplate(currentTeam?.id || Constants.globalTeamId, intl, showBoard, () => showBoard(currentBoardId), activeTemplate.id, currentTeam?.id)
const boardsAndBlocks = await mutator.addBoardFromTemplate(currentTeam?.id || Constants.globalTeamId, intl, showBoard, () => showBoard(currentBoardId), activeTemplate.id, currentTeam?.id)
const board = boardsAndBlocks.boards[0]
await mutator.updateBoard({...board, channelId: props.channelId || ''}, board, 'linked channel')
if (activeTemplate.title === OnboardingBoardTitle) {
resetTour()
}
@ -193,7 +196,11 @@ const BoardTemplateSelector = (props: Props) => {
filled={false}
emphasis={'secondary'}
size={'medium'}
onClick={() => mutator.addEmptyBoard(currentTeam?.id || '', intl, showBoard, () => showBoard(currentBoardId))}
onClick={async () => {
const boardsAndBlocks = await mutator.addEmptyBoard(currentTeam?.id || '', intl, showBoard, () => showBoard(currentBoardId))
const board = boardsAndBlocks.boards[0]
await mutator.updateBoard({...board, channelId: props.channelId || ''}, board, 'linked channel')
}}
>
<FormattedMessage
id='BoardTemplateSelector.create-empty-board'

View File

@ -14,6 +14,7 @@ import LockOutline from '../../widgets/icons/lockOutline'
import {useAppSelector} from '../../store/hooks'
import {getAllTeams, getCurrentTeam, Team} from '../../store/teams'
import {getMe} from '../../store/users'
import {Utils} from '../../utils'
import {BoardTypeOpen, BoardTypePrivate} from '../../blocks/board'
type Props = {
@ -42,7 +43,7 @@ const BoardSwitcherDialog = (props: Props): JSX.Element => {
if (!me) {
return
}
const newPath = generatePath(match.path, {...match.params, teamId, boardId, viewId: undefined})
const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, teamId, boardId, viewId: undefined})
history.push(newPath)
props.onClose()
}

View File

@ -71,7 +71,7 @@ const SidebarCategory = (props: Props) => {
if (boardId !== match.params.boardId && viewId !== match.params.viewId) {
params.cardId = undefined
}
const newPath = generatePath(match.path, params)
const newPath = generatePath(Utils.getBoardPagePath(match.path), params)
history.push(newPath)
props.hideSidebar()
}, [match, history])

View File

@ -37,7 +37,7 @@ const ViewMenu = (props: Props) => {
const match = useRouteMatch()
const showView = useCallback((viewId) => {
let newPath = generatePath(match.path, {...match.params, viewId: viewId || ''})
let newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, viewId: viewId || ''})
if (props.readonly) {
newPath += `?r=${Utils.getReadToken()}`
}

View File

@ -35,7 +35,7 @@ type Props = {
function CenterContent(props: Props) {
const team = useAppSelector(getCurrentTeam)
const isLoading = useAppSelector(isLoadingBoard)
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string}>()
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string, channelId?: string}>()
const board = useAppSelector(getCurrentBoard)
const templates = useAppSelector(getTemplates)
const cards = useAppSelector(getCurrentViewCardsSortedFilteredAndGrouped)
@ -51,7 +51,7 @@ function CenterContent(props: Props) {
const showCard = useCallback((cardId?: string) => {
const params = {...match.params, cardId}
let newPath = generatePath(match.path, params)
let newPath = generatePath(Utils.getBoardPagePath(match.path), params)
if (props.readonly) {
newPath += `?r=${Utils.getReadToken()}`
}
@ -129,6 +129,7 @@ function CenterContent(props: Props) {
}}
/>
}
channelId={match.params.channelId}
/>
)
}

View File

@ -58,6 +58,7 @@ import './boardPage.scss'
type Props = {
readonly?: boolean
new?: boolean
}
const BoardPage = (props: Props): JSX.Element => {
@ -202,7 +203,7 @@ const BoardPage = (props: Props): JSX.Element => {
return (
<div className='BoardPage'>
<TeamToBoardAndViewRedirect/>
{!props.new && <TeamToBoardAndViewRedirect/>}
<BackwardCompatibilityQueryParamsRedirect/>
<SetWindowTitleAndIcon/>
<UndoRedoHotKeys/>

View File

@ -7,6 +7,7 @@ import {getBoards, getCurrentBoardId} from '../../store/boards'
import {setCurrent as setCurrentView, getCurrentBoardViews} from '../../store/views'
import {useAppSelector, useAppDispatch} from '../../store/hooks'
import {UserSettings} from '../../userSettings'
import {Utils} from '../../utils'
import {getSidebarCategories} from '../../store/sidebar'
import {Constants} from "../../constants"
@ -49,7 +50,7 @@ const TeamToBoardAndViewRedirect = (): null => {
}
if (boardID) {
const newPath = generatePath(match.path, {...match.params, boardId: boardID, viewID: undefined})
const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewID: undefined})
history.replace(newPath)
// return from here because the loadBoardData() call
@ -77,7 +78,7 @@ const TeamToBoardAndViewRedirect = (): null => {
}
if (viewID) {
const newPath = generatePath(match.path, {...match.params, viewId: viewID})
const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, viewId: viewID})
history.replace(newPath)
}
}

View File

@ -165,6 +165,10 @@ const FocalboardRouter = (props: Props): JSX.Element => {
<ChangePasswordPage/>
</FBRoute>}
<FBRoute path={['/team/:teamId/new/:channelId']}>
<BoardPage new={true}/>
</FBRoute>
<FBRoute path={['/team/:teamId/shared/:boardId?/:viewId?/:cardId?', '/shared/:boardId?/:viewId?/:cardId?']}>
<BoardPage readonly={true}/>
</FBRoute>

View File

@ -21,4 +21,5 @@ export type SuiteWindow = Window & {
baseURL?: string
frontendBaseURL?: string
isFocalboardPlugin?: boolean
WebappUtils?: any
}

View File

@ -757,6 +757,13 @@ class Utils {
return (Utils.isMac() && e.metaKey) || (!Utils.isMac() && e.ctrlKey && !e.altKey)
}
static getBoardPagePath(currentPath: string) {
if (currentPath == "/team/:teamId/new/:channelId") {
return "/team/:teamId/:boardId?/:viewId?/:cardId?"
}
return currentPath
}
static showBoard(
boardId: string,
match: routerMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>,
@ -769,7 +776,7 @@ class Utils {
params.viewId = undefined
params.cardId = undefined
}
const newPath = generatePath(match.path, params)
const newPath = generatePath(Utils.getBoardPagePath(match.path), params)
history.push(newPath)
}