Skip to content
This repository was archived by the owner on May 18, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions server/api/boards.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,25 @@ func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)

isGuest, err := a.userIsGuest(userID)
isAdmin, err := a.isHardcodedAdmin(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}

// retrieve boards list
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
var boards []*model.Board
if isAdmin {
// Admin sees all boards for the team
boards, err = a.app.GetAllBoardsForTeam(teamID)
} else {
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
boards, err = a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
}

if err != nil {
a.errorResponse(w, r, err)
return
Expand Down
4 changes: 4 additions & 0 deletions server/app/boards.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ func (a *App) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards
return a.store.GetBoardsForUserAndTeam(userID, teamID, includePublicBoards)
}

func (a *App) GetAllBoardsForTeam(teamID string) ([]*model.Board, error) {
return a.store.GetAllBoardsForTeam(teamID)
}

func (a *App) GetTemplateBoards(teamID, userID string) ([]*model.Board, error) {
return a.store.GetTemplateBoards(teamID, userID)
}
Expand Down
17 changes: 17 additions & 0 deletions server/services/store/sqlstore/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,23 @@ func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID stri
return s.boardsFromRows(rows)
}

func (s *SQLStore) getAllBoardsForTeam(db sq.BaseRunner, teamID string) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b").
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Eq{"b.is_template": false})

rows, err := query.Query()
if err != nil {
s.logger.Error(`getAllBoardsForTeam ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)

return s.boardsFromRows(rows)
}

func (s *SQLStore) getBoardsInTeamByIds(db sq.BaseRunner, boardIDs []string, teamID string) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
Expand Down
5 changes: 5 additions & 0 deletions server/services/store/sqlstore/public_methods.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/services/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ type Store interface {
PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error)
GetBoard(id string) (*model.Board, error)
GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error)
GetAllBoardsForTeam(teamID string) ([]*model.Board, error)
GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error)
// @withTransaction
DeleteBoard(boardID, userID string) error
Expand Down
54 changes: 45 additions & 9 deletions webapp/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react'
import {FormattedMessage} from 'react-intl'
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {DragDropContext, Droppable, DropResult} from 'react-beautiful-dnd'

import {getActiveThemeName, loadTheme} from '../../theme'
import IconButton from '../../widgets/buttons/iconButton'
import HamburgerIcon from '../../widgets/icons/hamburger'
import HideSidebarIcon from '../../widgets/icons/hideSidebar'
import ShowSidebarIcon from '../../widgets/icons/showSidebar'
import {getCurrentBoard, getMySortedBoards} from '../../store/boards'
import {getAllSortedBoards, getCurrentBoard, getMySortedBoards} from '../../store/boards'
import {useAppDispatch, useAppSelector} from '../../store/hooks'
import {Utils} from '../../utils'
import {IUser} from '../../user'
Expand All @@ -36,7 +36,7 @@ import {getCurrentTeam, getCurrentTeamId} from '../../store/teams'

import {Constants} from '../../constants'

import {getMe} from '../../store/users'
import {getMe, isAdmin} from '../../store/users'
import {getCurrentViewId} from '../../store/views'

import octoClient from '../../octoClient'
Expand All @@ -50,6 +50,9 @@ import {Board} from '../../blocks/board'
import SidebarCategory from './sidebarCategory'
import SidebarSettingsMenu from './sidebarSettingsMenu'
import SidebarUserMenu from './sidebarUserMenu'
import SidebarBoardItemReadOnly from './sidebarBoardItemReadOnly'
import CompassIcon from '../../widgets/icons/compassIcon'


type Props = {
activeBoardId?: string
Expand All @@ -69,12 +72,29 @@ const Sidebar = (props: Props) => {
const [isHidden, setHidden] = useState(false)
const [userHidden, setUserHidden] = useState(false)
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
const boards = useAppSelector(getMySortedBoards)
const isUserAdmin = useAppSelector<boolean>(isAdmin);
const myBoards = useAppSelector(getMySortedBoards)
const allBoards = useAppSelector(getAllSortedBoards)
// For regular category display, always use myBoards (even for admin)
const boards = myBoards

// Admin-only boards = allBoards minus myBoards
const adminOnlyBoards = useMemo((): Board[] => {
if (!isUserAdmin) {
return []
}

const myBoardIDs = new Set(myBoards.map(b => b.id))
return allBoards.filter((board) => !myBoardIDs.has(board.id))
}, [isUserAdmin, myBoards, allBoards])

const dispatch = useAppDispatch()
const sidebarCategories = useAppSelector<CategoryBoards[]>(getSidebarCategories)
const me = useAppSelector<IUser|null>(getMe)
const activeViewID = useAppSelector(getCurrentViewId)
const currentBoard = useAppSelector(getCurrentBoard)
const teamId = useAppSelector(getCurrentTeamId)
const team = useAppSelector(getCurrentTeam)

useEffect(() => {
const categoryOnChangeHandler = (_: WSClient, categories: Category[]) => {
Expand All @@ -94,9 +114,6 @@ const Sidebar = (props: Props) => {
}
}, [])

const teamId = useAppSelector(getCurrentTeamId)
const team = useAppSelector(getCurrentTeam)

useEffect(() => {
if (team) {
dispatch(fetchSidebarCategories(team!.id))
Expand Down Expand Up @@ -418,12 +435,31 @@ const Sidebar = (props: Props) => {
/>
))
}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>

{/* Uncategorized boards for admin - simple list with export only */}
{isUserAdmin && adminOnlyBoards.length > 0 && (
<div className='SidebarCategory'>
<div
className='octo-sidebar-item category expanded'
>
<div className='octo-sidebar-title category-title'>
<CompassIcon icon='export-variant'/>
{'Exportable (Admin)'}
</div>
</div>
{adminOnlyBoards.map((board) => (
<SidebarBoardItemReadOnly
key={board.id}
board={board}
/>
))}
</div>
)}

<div className='octo-spacer'/>

{
Expand Down
6 changes: 4 additions & 2 deletions webapp/src/components/sidebar/sidebarBoardItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {Utils} from '../../utils'

import AddIcon from '../../widgets/icons/add'
import CloseIcon from '../../widgets/icons/close'
import {getMe} from '../../store/users'
import {getMe, isAdmin} from '../../store/users'
import octoClient from '../../octoClient'
import {getCurrentBoardId} from '../../store/boards'
import {UserSettings} from '../../userSettings'
Expand Down Expand Up @@ -79,6 +79,7 @@ const SidebarBoardItem = (props: Props) => {
const history = useHistory()
const dispatch = useAppDispatch()
const currentBoardID = useAppSelector(getCurrentBoardId)
const isUserAdmin = useAppSelector<boolean>(isAdmin);

const generateMoveToCategoryOptions = (boardID: string) => {
return props.allCategories.map((category) => (
Expand Down Expand Up @@ -261,12 +262,13 @@ const SidebarBoardItem = (props: Props) => {
icon={<AddIcon/>}
onClick={() => handleDuplicateBoard(true)}
/>}
{isUserAdmin &&
<Menu.Text
id='exportBoardArchive'
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export board archive'})}
icon={<CompassIcon icon='export-variant'/>}
onClick={() => Archiver.exportBoardArchive(board)}
/>
/>}
<Menu.Text
id='hideBoard'
name={intl.formatMessage({id: 'HideBoard.MenuOption', defaultMessage: 'Hide board'})}
Expand Down
58 changes: 58 additions & 0 deletions webapp/src/components/sidebar/sidebarBoardItemReadOnly.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, {useRef} from 'react'
import {useIntl} from 'react-intl'

import {Board} from '../../blocks/board'
import IconButton from '../../widgets/buttons/iconButton'
import OptionsIcon from '../../widgets/icons/options'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import CompassIcon from '../../widgets/icons/compassIcon'
import {Archiver} from '../../archiver'

type Props = {
board: Board
}

const SidebarBoardItemReadOnly = (props: Props) => {
const intl = useIntl()
const board = props.board
const boardItemRef = useRef<HTMLDivElement>(null)

const title = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'})

return (
<div
className='SidebarBoardItem subitem'
ref={boardItemRef}
>
<div className='octo-sidebar-icon'>
{board.icon || <CompassIcon icon='product-boards'/>}
</div>
<div
className='octo-sidebar-title'
title={title}
>
{title}
</div>
<div>
<MenuWrapper stopPropagationOnToggle={true}>
<IconButton icon={<OptionsIcon/>}/>
<Menu
fixed={true}
position='auto'
parentRef={boardItemRef}
>
<Menu.Text
id='exportBoardArchive'
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export board archive'})}
icon={<CompassIcon icon='export-variant'/>}
onClick={() => Archiver.exportBoardArchive(board)}
/>
</Menu>
</MenuWrapper>
</div>
</div>
)
}

export default React.memo(SidebarBoardItemReadOnly)
6 changes: 5 additions & 1 deletion webapp/src/components/sidebar/sidebarSettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import MenuWrapper from '../../widgets/menuWrapper'
import {useAppDispatch, useAppSelector} from '../../store/hooks'
import {storeLanguage} from '../../store/language'
import {getCurrentTeam, Team} from '../../store/teams'
import {isAdmin} from '../../store/users'
import {UserSettings} from '../../userSettings'

import './sidebarSettingsMenu.scss'
Expand All @@ -27,6 +28,7 @@ import {Constants} from '../../constants'

import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../telemetry/telemetryClient'


type Props = {
activeTheme: string
}
Expand All @@ -35,6 +37,7 @@ const SidebarSettingsMenu = (props: Props) => {
const intl = useIntl()
const dispatch = useAppDispatch()
const currentTeam = useAppSelector<Team|null>(getCurrentTeam)
const isUserAdmin = useAppSelector<boolean>(isAdmin);

// we need this as the sidebar doesn't always need to re-render
// on theme change. This can cause props and the actual
Expand Down Expand Up @@ -122,6 +125,7 @@ const SidebarSettingsMenu = (props: Props) => {
))
}
</Menu.SubMenu>
{isUserAdmin &&
<Menu.Text
id='export'
name={intl.formatMessage({id: 'Sidebar.export-archive', defaultMessage: 'Export archive'})}
Expand All @@ -131,7 +135,7 @@ const SidebarSettingsMenu = (props: Props) => {
Archiver.exportFullArchive(currentTeam.id)
}
}}
/>
/>}
<Menu.SubMenu
id='lang'
name={intl.formatMessage({id: 'Sidebar.set-language', defaultMessage: 'Set language'})}
Expand Down
7 changes: 7 additions & 0 deletions webapp/src/store/boards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@ export const getMySortedBoards = createSelector(
},
)

export const getAllSortedBoards = createSelector(
getBoards,
(boards) => {
return Object.values(boards).sort((a, b) => a.title.localeCompare(b.title))
},
)

export const getTemplates = (state: RootState): {[key: string]: Board} => state.boards.templates

export const getSortedTemplates = createSelector(
Expand Down
Loading