GH-1333: Implement Screen 3 and 4 of v6.0 navigation and FTUE (#1341)
This commit is contained in:
parent
319dcbfb5d
commit
c9aeeb38bf
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div className='EmptyCenterPanel'>
|
||||
{workspace && workspace.id !== '0' &&
|
||||
<div className='WorkspaceInfo'>
|
||||
<FormattedMessage
|
||||
id='EmptyCenterPanel.workspace'
|
||||
defaultMessage='This is the workspace for:'
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`button ${classNames || ''}`}
|
||||
>
|
||||
<span>{buttonIcon}</span>
|
||||
<span className='button-title'>{title}</span>
|
||||
{!readonly && showBoard && boardTemplate &&
|
||||
<BoardTemplateButtonMenu
|
||||
showBoard={showBoard}
|
||||
boardTemplate={boardTemplate}
|
||||
/>
|
||||
<b>
|
||||
{workspace.title}
|
||||
</b>
|
||||
</div>
|
||||
}
|
||||
<div className='Hint'>
|
||||
<FormattedMessage
|
||||
id='EmptyCenterPanel.no-content'
|
||||
defaultMessage='Add or select a board from the sidebar to get started.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const EmptyCenterPanel = React.memo(() => {
|
||||
const workspace = useAppSelector(getCurrentWorkspace)
|
||||
const templates = useAppSelector(getSortedTemplates)
|
||||
const globalTemplates = useAppSelector<Board[]>(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 (
|
||||
<div className='EmptyCenterPanel'>
|
||||
<div className='Hint'>
|
||||
<FormattedMessage
|
||||
id='EmptyCenterPanel.no-content'
|
||||
defaultMessage='Add or select a board from the sidebar to get started.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='EmptyCenterPanel'>
|
||||
<div className='content'>
|
||||
<span className='title'>
|
||||
<FormattedMessage
|
||||
id='EmptyCenterPanel.plugin.no-content-title'
|
||||
defaultMessage='Create a Board in {workspaceName}'
|
||||
values={{workspaceName: workspace?.title}}
|
||||
/>
|
||||
</span>
|
||||
<span className='description'>
|
||||
<FormattedMessage
|
||||
id='EmptyCenterPanel.plugin.no-content-description'
|
||||
defaultMessage='Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of "{workspaceName}" will have access to boards created here.'
|
||||
values={{
|
||||
workspaceName: <b>{workspace?.title}</b>,
|
||||
lineBreak: <br/>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className='choose-template-text'>
|
||||
<FormattedMessage
|
||||
id='EmptyCenterPanel.plugin.choose-a-template'
|
||||
defaultMessage='Choose a template'
|
||||
/>
|
||||
</span>
|
||||
<div className='button-container'>
|
||||
{templates.map((template) =>
|
||||
(
|
||||
<PanelButton
|
||||
key={template.id}
|
||||
title={template.title}
|
||||
buttonIcon={template.fields.icon}
|
||||
readonly={false}
|
||||
onClick={() => addBoardFromTemplate(intl, showBoard, template.id)}
|
||||
showBoard={showBoard}
|
||||
boardTemplate={template}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{globalTemplates.map((template) =>
|
||||
(
|
||||
<PanelButton
|
||||
key={template.id}
|
||||
title={template.title}
|
||||
buttonIcon={template.fields.icon}
|
||||
readonly={true}
|
||||
onClick={() => addBoardFromTemplate(intl, showBoard, template.id, undefined, true)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<PanelButton
|
||||
key={'new-template'}
|
||||
title={intl.formatMessage({id: 'EmptyCenterPanel.plugin.new-template', defaultMessage: 'New template'})}
|
||||
buttonIcon={<AddIcon/>}
|
||||
readonly={true}
|
||||
onClick={newTemplateClicked}
|
||||
classNames='new-template'
|
||||
/>
|
||||
</div>
|
||||
<span className='choose-template-text'>
|
||||
<FormattedMessage
|
||||
id='EmptyCenterPanel.plugin.no-content-or'
|
||||
defaultMessage='or'
|
||||
/>
|
||||
</span>
|
||||
<PanelButton
|
||||
key={'start-with-an-empty-board'}
|
||||
title={intl.formatMessage({id: 'EmptyCenterPanel.plugin.empty-board', defaultMessage: 'Start with an Empty Board'})}
|
||||
buttonIcon={<BoardIcon/>}
|
||||
readonly={true}
|
||||
onClick={emptyBoardClicked}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='EmptyCenterPanel.plugin.end-message'
|
||||
defaultMessage='You can change the channel using the switcher in the sidebar.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
})
|
||||
|
||||
export default EmptyCenterPanel
|
||||
|
|
|
@ -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 (
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='right'>
|
||||
<Menu.Text
|
||||
icon={<EditIcon/>}
|
||||
id='edit'
|
||||
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
|
||||
onClick={() => {
|
||||
showBoard(boardTemplate.id || '')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})}
|
||||
onClick={async () => {
|
||||
await mutator.deleteBlock(boardTemplate, 'delete board template')
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
)
|
||||
})
|
||||
|
||||
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={<div className='Icon'>{boardTemplate.fields.icon}</div>}
|
||||
onClick={() => {
|
||||
addBoardFromTemplate(intl, props.showBoard, boardTemplate.id || '', activeBoardId, isGlobal)
|
||||
addBoardFromTemplate(intl, showBoard, boardTemplate.id || '', activeBoardId, isGlobal)
|
||||
}}
|
||||
rightIcon={!isGlobal &&
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='right'>
|
||||
<Menu.Text
|
||||
icon={<EditIcon/>}
|
||||
id='edit'
|
||||
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
|
||||
onClick={() => {
|
||||
props.showBoard(boardTemplate.id || '')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})}
|
||||
onClick={async () => {
|
||||
await mutator.deleteBlock(boardTemplate, 'delete board template')
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<BoardTemplateButtonMenu
|
||||
boardTemplate={boardTemplate}
|
||||
showBoard={showBoard}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -145,7 +145,7 @@ const Sidebar = React.memo((props: Props) => {
|
|||
<div className='octo-spacer'/>
|
||||
|
||||
{
|
||||
!props.isDashboard &&
|
||||
(!props.isDashboard && !Utils.isFocalboardPlugin()) &&
|
||||
<SidebarAddBoardMenu
|
||||
activeBoardId={props.activeBoardId}
|
||||
/>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -100,7 +100,9 @@ const Workspace = React.memo((props: Props) => {
|
|||
defaultMessage="You're editing a board template."
|
||||
/>
|
||||
</div>}
|
||||
<CenterContent readonly={props.readonly}/>
|
||||
<CenterContent
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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<boolean>(false)
|
||||
|
||||
const goToEmptyCenterPanel = () => {
|
||||
UserSettings.lastBoardId = null
|
||||
UserSettings.lastViewId = null
|
||||
dispatch(setCurrentBoard(''))
|
||||
dispatch(setCurrentView(''))
|
||||
history.replace(`/workspace/${activeWorkspace?.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'WorkspaceSwitcherWrapper'}>
|
||||
<div
|
||||
|
@ -31,13 +44,13 @@ const WorkspaceSwitcher = (props: Props): JSX.Element => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<span>{props.activeWorkspace?.title || DashboardOption.label}</span>
|
||||
<span>{activeWorkspace?.title || DashboardOption.label}</span>
|
||||
<ChevronDown/>
|
||||
</div>
|
||||
{
|
||||
showMenu &&
|
||||
<WorkspaceOptions
|
||||
activeWorkspaceId={props.activeWorkspace?.id || DashboardOption.value}
|
||||
activeWorkspaceId={activeWorkspace?.id || DashboardOption.value}
|
||||
onBlur={() => {
|
||||
setShowMenu(false)
|
||||
}}
|
||||
|
@ -56,6 +69,14 @@ const WorkspaceSwitcher = (props: Props): JSX.Element => {
|
|||
}}
|
||||
/>
|
||||
}
|
||||
{activeWorkspace &&
|
||||
<span
|
||||
className='add-workspace-icon'
|
||||
onClick={goToEmptyCenterPanel}
|
||||
>
|
||||
<AddIcon/>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 => {
|
|||
<div className='error'>
|
||||
{intl.formatMessage({id: 'BoardPage.syncFailed', defaultMessage: 'Board may be deleted or access revoked.'})}
|
||||
</div>}
|
||||
<Workspace readonly={props.readonly || false}/>
|
||||
<Workspace
|
||||
readonly={props.readonly || false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user