Moving the history instance creation after the plugin initialization (to honor the SiteURL config) (#2109)

* Moving the history instance creation after the plugin initialization (to honor the SiteURL config)

* Fixing welcome page images urls generation

* Fixing share board url generation

* Fixing more subpath problems

* Adding some tests with subpath

* Adding subpath test to welcome page

* fix linter error
This commit is contained in:
Jesús Espino 2022-01-17 20:21:25 +01:00 committed by GitHub
parent 1aa60009ea
commit 02dfb6eceb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 324 additions and 66 deletions

View file

@ -118,7 +118,7 @@ module.exports = {
options: { options: {
name: '[name].[ext]', name: '[name].[ext]',
outputPath: 'static', outputPath: 'static',
publicPath: '/plugins/focalboard/static/', publicPath: '/static/',
}, },
}, },
{ {

View file

@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useEffect} from 'react' import React, {useEffect, useMemo} from 'react'
import { import {
Router, Router,
Redirect, Redirect,
@ -40,11 +40,19 @@ import {UserSettings} from './userSettings'
declare let window: IAppWindow declare let window: IAppWindow
export const history = createBrowserHistory({basename: Utils.getFrontendBaseURL()})
const UUID_REGEX = new RegExp(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/) const UUID_REGEX = new RegExp(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
if (Utils.isDesktop() && Utils.isFocalboardPlugin()) { const App = React.memo((): JSX.Element => {
const language = useAppSelector<string>(getLanguage)
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
const globalError = useAppSelector<string>(getGlobalError)
const me = useAppSelector<IUser|null>(getMe)
const dispatch = useAppDispatch()
const browserHistory: ReturnType<typeof createBrowserHistory> = useMemo(() => {
const history = createBrowserHistory({basename: Utils.getFrontendBaseURL()})
if (Utils.isDesktop() && Utils.isFocalboardPlugin()) {
window.addEventListener('message', (event: MessageEvent) => { window.addEventListener('message', (event: MessageEvent) => {
if (event.origin !== window.location.origin) { if (event.origin !== window.location.origin) {
return return
@ -58,9 +66,8 @@ if (Utils.isDesktop() && Utils.isFocalboardPlugin()) {
Utils.log(`Navigating Boards to ${pathName}`) Utils.log(`Navigating Boards to ${pathName}`)
history.replace(pathName.replace(window.frontendBaseURL, '')) history.replace(pathName.replace(window.frontendBaseURL, ''))
}) })
} }
return {
const browserHistory: typeof history = {
...history, ...history,
push: (path: string, state?: unknown) => { push: (path: string, state?: unknown) => {
if (Utils.isDesktop() && Utils.isFocalboardPlugin()) { if (Utils.isDesktop() && Utils.isFocalboardPlugin()) {
@ -77,14 +84,8 @@ const browserHistory: typeof history = {
history.push(path, state) history.push(path, state)
} }
}, },
} }
}, [])
const App = React.memo((): JSX.Element => {
const language = useAppSelector<string>(getLanguage)
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
const globalError = useAppSelector<string>(getGlobalError)
const me = useAppSelector<IUser|null>(getMe)
const dispatch = useAppDispatch()
useEffect(() => { useEffect(() => {
dispatch(fetchLanguage()) dispatch(fetchLanguage())
@ -95,7 +96,7 @@ const App = React.memo((): JSX.Element => {
if (Utils.isFocalboardPlugin()) { if (Utils.isFocalboardPlugin()) {
useEffect(() => { useEffect(() => {
if (window.frontendBaseURL) { if (window.frontendBaseURL) {
history.replace(window.location.pathname.replace(window.frontendBaseURL, '')) browserHistory.replace(window.location.pathname.replace(window.frontendBaseURL, ''))
} }
}, []) }, [])
} }
@ -137,9 +138,7 @@ const App = React.memo((): JSX.Element => {
> >
<DndProvider backend={Utils.isMobile() ? TouchBackend : HTML5Backend}> <DndProvider backend={Utils.isMobile() ? TouchBackend : HTML5Backend}>
<FlashMessages milliseconds={2000}/> <FlashMessages milliseconds={2000}/>
<Router <Router history={browserHistory}>
history={browserHistory}
>
<div id='frame'> <div id='frame'>
<div id='main'> <div id='main'>
<NewVersionBanner/> <NewVersionBanner/>

View file

@ -41,11 +41,11 @@ exports[`src/components/shareBoardComponent return shareBoardComponent and click
> >
<a <a
class="shareUrl" class="shareUrl"
href="http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=oneToken" href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >
http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=oneToken http://localhost/workspace/1/shared/1/1?r=oneToken
</a> </a>
<button <button
type="button" type="button"
@ -112,11 +112,11 @@ exports[`src/components/shareBoardComponent return shareBoardComponent and click
> >
<a <a
class="shareUrl" class="shareUrl"
href="http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=oneToken" href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >
http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=oneToken http://localhost/workspace/1/shared/1/1?r=oneToken
</a> </a>
<button <button
type="button" type="button"
@ -183,11 +183,11 @@ exports[`src/components/shareBoardComponent return shareBoardComponent and click
> >
<a <a
class="shareUrl" class="shareUrl"
href="http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=oneToken" href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >
http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=oneToken http://localhost/workspace/1/shared/1/1?r=oneToken
</a> </a>
<button <button
type="button" type="button"
@ -254,11 +254,11 @@ exports[`src/components/shareBoardComponent return shareBoardComponent and click
> >
<a <a
class="shareUrl" class="shareUrl"
href="http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=aToken" href="http://localhost/workspace/1/shared/1/1?r=aToken"
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >
http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=aToken http://localhost/workspace/1/shared/1/1?r=aToken
</a> </a>
<button <button
type="button" type="button"
@ -366,11 +366,82 @@ exports[`src/components/shareBoardComponent should match snapshot with sharing 1
> >
<a <a
class="shareUrl" class="shareUrl"
href="http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=oneToken" href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >
http://localhost/plugins/focalboard/workspace/1/shared/1/1?r=oneToken http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent should match snapshot with sharing and subpath 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/test-subpath/plugins/boards/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/test-subpath/plugins/boards/workspace/1/shared/1/1?r=oneToken
</a> </a>
<button <button
type="button" type="button"
@ -466,3 +537,74 @@ exports[`src/components/shareBoardComponent should match snapshot with sharing a
</div> </div>
</div> </div>
`; `;
exports[`src/components/shareBoardComponent should match snapshot with sharing and without workspaceId and subpath 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/test-subpath/plugins/boards/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/test-subpath/plugins/boards/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;

View file

@ -23,8 +23,10 @@ const viewId = boardId
jest.mock('../octoClient') jest.mock('../octoClient')
jest.mock('../utils') jest.mock('../utils')
const mockedOctoClient = mocked(client, true) const mockedOctoClient = mocked(client, true)
const mockedUtils = mocked(Utils, true) const mockedUtils = mocked(Utils, true)
let params = {} let params = {}
jest.mock('react-router', () => { jest.mock('react-router', () => {
const originalModule = jest.requireActual('react-router') const originalModule = jest.requireActual('react-router')
@ -45,14 +47,24 @@ const board = TestBlockFactory.createBoard()
board.id = boardId board.id = boardId
describe('src/components/shareBoardComponent', () => { describe('src/components/shareBoardComponent', () => {
const w = (window as any)
const oldBaseURL = w.baseURL
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
mockedUtils.buildURL.mockImplementation((path) => (w.baseURL || '') + path)
params = { params = {
boardId, boardId,
viewId, viewId,
workspaceId, workspaceId,
} }
}) })
afterEach(() => {
w.baseURL = oldBaseURL
})
test('should match snapshot', async () => { test('should match snapshot', async () => {
mockedOctoClient.getSharing.mockResolvedValue(undefined) mockedOctoClient.getSharing.mockResolvedValue(undefined)
let container let container
@ -215,4 +227,48 @@ describe('src/components/shareBoardComponent', () => {
expect(mockedUtils.createGuid).toBeCalledTimes(1) expect(mockedUtils.createGuid).toBeCalledTimes(1)
expect(container).toMatchSnapshot() expect(container).toMatchSnapshot()
}) })
test('should match snapshot with sharing and without workspaceId and subpath', async () => {
w.baseURL = '/test-subpath/plugins/boards'
const sharing:ISharing = {
id: boardId,
enabled: true,
token: 'oneToken',
}
params = {
boardId,
viewId,
}
mockedOctoClient.getSharing.mockResolvedValue(sharing)
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})
container = result.container
})
expect(container).toMatchSnapshot()
})
test('should match snapshot with sharing and subpath', async () => {
w.baseURL = '/test-subpath/plugins/boards'
const sharing:ISharing = {
id: boardId,
enabled: true,
token: 'oneToken',
}
mockedOctoClient.getSharing.mockResolvedValue(sharing)
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})
container = result.container
})
expect(container).toMatchSnapshot()
})
}) })

View file

@ -79,18 +79,18 @@ const ShareBoardComponent = React.memo((props: Props): JSX.Element => {
shareUrl.searchParams.set('r', readToken) shareUrl.searchParams.set('r', readToken)
if (match.params.workspaceId) { if (match.params.workspaceId) {
const newPath = generatePath('/plugins/focalboard/workspace/:workspaceId/shared/:boardId/:viewId', { const newPath = generatePath('/workspace/:workspaceId/shared/:boardId/:viewId', {
boardId: match.params.boardId, boardId: match.params.boardId,
viewId: match.params.viewId, viewId: match.params.viewId,
workspaceId: match.params.workspaceId, workspaceId: match.params.workspaceId,
}) })
shareUrl.pathname = newPath shareUrl.pathname = Utils.buildURL(newPath)
} else { } else {
const newPath = generatePath('/shared/:boardId/:viewId', { const newPath = generatePath('/shared/:boardId/:viewId', {
boardId: match.params.boardId, boardId: match.params.boardId,
viewId: match.params.viewId, viewId: match.params.viewId,
}) })
shareUrl.pathname = newPath shareUrl.pathname = Utils.buildURL(newPath)
} }
return ( return (

View file

@ -19,12 +19,54 @@ exports[`pages/welcome Welcome Page shows Explore Page 1`] = `
<img <img
alt="Boards Welcome Image" alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--large" class="WelcomePage__image WelcomePage__image--large"
src="test-file-stub" src="http://localhost/test-file-stub"
/> />
<img <img
alt="Boards Welcome Image" alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--small" class="WelcomePage__image WelcomePage__image--small"
src="test-file-stub" src="http://localhost/test-file-stub"
/>
<button
class="Button filled size--large"
type="button"
>
<span>
Explore
</span>
<i
class="CompassIcon icon-chevron-right Icon Icon--right"
/>
</button>
</div>
</div>
</div>
`;
exports[`pages/welcome Welcome Page shows Explore Page with subpath 1`] = `
<div>
<div
class="WelcomePage"
>
<div>
<h1
class="text-heading9"
>
Welcome To Boards
</h1>
<div
class="WelcomePage__subtitle"
>
Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view
</div>
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--large"
src="http://localhost/subpath/test-file-stub"
/>
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--small"
src="http://localhost/subpath/test-file-stub"
/> />
<button <button
class="Button filled size--large" class="Button filled size--large"

View file

@ -16,10 +16,17 @@ import {wrapIntl} from '../../testUtils'
import WelcomePage from './welcomePage' import WelcomePage from './welcomePage'
const w = (window as any)
const oldBaseURL = w.baseURL
beforeEach(() => { beforeEach(() => {
UserSettings.welcomePageViewed = null UserSettings.welcomePageViewed = null
}) })
afterEach(() => {
w.baseURL = oldBaseURL
})
describe('pages/welcome', () => { describe('pages/welcome', () => {
const history = createMemoryHistory() const history = createMemoryHistory()
test('Welcome Page shows Explore Page', () => { test('Welcome Page shows Explore Page', () => {
@ -32,6 +39,17 @@ describe('pages/welcome', () => {
expect(container).toMatchSnapshot() expect(container).toMatchSnapshot()
}) })
test('Welcome Page shows Explore Page with subpath', () => {
w.baseURL = '/subpath'
const {container} = render(wrapIntl(
<Router history={history}>
<WelcomePage/>
</Router>,
))
expect(screen.getByText('Explore')).toBeDefined()
expect(container).toMatchSnapshot()
})
test('Welcome Page shows Explore Page And Then Proceeds after Clicking Explore', () => { test('Welcome Page shows Explore Page And Then Proceeds after Clicking Explore', () => {
history.replace = jest.fn() history.replace = jest.fn()
render(wrapIntl( render(wrapIntl(

View file

@ -11,6 +11,7 @@ import BoardWelcomeSmallPNG from '../../../static/boards-welcome-small.png'
import Button from '../../widgets/buttons/button' import Button from '../../widgets/buttons/button'
import CompassIcon from '../../widgets/icons/compassIcon' import CompassIcon from '../../widgets/icons/compassIcon'
import {UserSettings} from '../../userSettings' import {UserSettings} from '../../userSettings'
import {Utils} from '../../utils'
import './welcomePage.scss' import './welcomePage.scss'
@ -52,14 +53,14 @@ const WelcomePage = React.memo(() => {
{/* This image will be rendered on large screens over 2000px */} {/* This image will be rendered on large screens over 2000px */}
<img <img
src={BoardWelcomePNG} src={Utils.buildURL(BoardWelcomePNG, true)}
className='WelcomePage__image WelcomePage__image--large' className='WelcomePage__image WelcomePage__image--large'
alt='Boards Welcome Image' alt='Boards Welcome Image'
/> />
{/* This image will be rendered on small screens below 2000px */} {/* This image will be rendered on small screens below 2000px */}
<img <img
src={BoardWelcomeSmallPNG} src={Utils.buildURL(BoardWelcomeSmallPNG, true)}
className='WelcomePage__image WelcomePage__image--small' className='WelcomePage__image WelcomePage__image--small'
alt='Boards Welcome Image' alt='Boards Welcome Image'
/> />

View file

@ -36,7 +36,7 @@ enum IDType {
class Utils { class Utils {
static createGuid(idType: IDType): string { static createGuid(idType: IDType): string {
const data = Utils.randomArray(16) const data = Utils.randomArray(16)
return idType + this.base32encode(data, false) return idType + Utils.base32encode(data, false)
} }
static blockTypeToIDType(blockType: string | undefined): IDType { static blockTypeToIDType(blockType: string | undefined): IDType {
@ -128,10 +128,10 @@ class Utils {
static canvas : HTMLCanvasElement | undefined static canvas : HTMLCanvasElement | undefined
static getTextWidth(displayText: string, fontDescriptor: string): number { static getTextWidth(displayText: string, fontDescriptor: string): number {
if (displayText !== '') { if (displayText !== '') {
if (!this.canvas) { if (!Utils.canvas) {
this.canvas = document.createElement('canvas') as HTMLCanvasElement Utils.canvas = document.createElement('canvas') as HTMLCanvasElement
} }
const context = this.canvas.getContext('2d') const context = Utils.canvas.getContext('2d')
if (context) { if (context) {
context.font = fontDescriptor context.font = fontDescriptor
const metrics = context.measureText(displayText) const metrics = context.measureText(displayText)
@ -500,7 +500,7 @@ class Utils {
} }
static getFrontendBaseURL(absolute?: boolean): string { static getFrontendBaseURL(absolute?: boolean): string {
let frontendBaseURL = window.frontendBaseURL || this.getBaseURL(absolute) let frontendBaseURL = window.frontendBaseURL || Utils.getBaseURL(absolute)
frontendBaseURL = frontendBaseURL.replace(/\/+$/, '') frontendBaseURL = frontendBaseURL.replace(/\/+$/, '')
if (frontendBaseURL.indexOf('/') === 0) { if (frontendBaseURL.indexOf('/') === 0) {
frontendBaseURL = frontendBaseURL.slice(1) frontendBaseURL = frontendBaseURL.slice(1)
@ -512,7 +512,7 @@ class Utils {
} }
static buildURL(path: string, absolute?: boolean): string { static buildURL(path: string, absolute?: boolean): string {
const baseURL = this.getBaseURL() const baseURL = Utils.getBaseURL()
let finalPath = baseURL + path let finalPath = baseURL + path
if (path.indexOf('/') !== 0) { if (path.indexOf('/') !== 0) {
finalPath = baseURL + '/' + path finalPath = baseURL + '/' + path