diff --git a/server/api/boards.go b/server/api/boards.go index af4e7cb7152..685de7b29ea 100644 --- a/server/api/boards.go +++ b/server/api/boards.go @@ -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 diff --git a/server/app/boards.go b/server/app/boards.go index 416e69891af..b6c6ab84274 100644 --- a/server/app/boards.go +++ b/server/app/boards.go @@ -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) } diff --git a/server/services/store/sqlstore/board.go b/server/services/store/sqlstore/board.go index 48faa76e20f..dea15bcb5b0 100644 --- a/server/services/store/sqlstore/board.go +++ b/server/services/store/sqlstore/board.go @@ -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.")...). diff --git a/server/services/store/sqlstore/public_methods.go b/server/services/store/sqlstore/public_methods.go index 631d162fa78..9e66e1f92f0 100644 --- a/server/services/store/sqlstore/public_methods.go +++ b/server/services/store/sqlstore/public_methods.go @@ -413,6 +413,11 @@ func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string, include } +func (s *SQLStore) GetAllBoardsForTeam(teamID string) ([]*model.Board, error) { + return s.getAllBoardsForTeam(s.db, teamID) + +} + func (s *SQLStore) GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error) { return s.getBoardsInTeamByIds(s.db, boardIDs, teamID) diff --git a/server/services/store/store.go b/server/services/store/store.go index 43f39700148..d6cbb3e2744 100644 --- a/server/services/store/store.go +++ b/server/services/store/store.go @@ -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 diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index bd50edfdc2a..b2f7800fa74 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -1,7 +1,7 @@ // 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' @@ -9,7 +9,7 @@ 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' @@ -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' @@ -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 @@ -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(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(getSidebarCategories) const me = useAppSelector(getMe) const activeViewID = useAppSelector(getCurrentViewId) const currentBoard = useAppSelector(getCurrentBoard) + const teamId = useAppSelector(getCurrentTeamId) + const team = useAppSelector(getCurrentTeam) useEffect(() => { const categoryOnChangeHandler = (_: WSClient, categories: Category[]) => { @@ -94,9 +114,6 @@ const Sidebar = (props: Props) => { } }, []) - const teamId = useAppSelector(getCurrentTeamId) - const team = useAppSelector(getCurrentTeam) - useEffect(() => { if (team) { dispatch(fetchSidebarCategories(team!.id)) @@ -418,12 +435,31 @@ const Sidebar = (props: Props) => { /> )) } - {provided.placeholder} )} + {/* Uncategorized boards for admin - simple list with export only */} + {isUserAdmin && adminOnlyBoards.length > 0 && ( +
+
+
+ + {'Exportable (Admin)'} +
+
+ {adminOnlyBoards.map((board) => ( + + ))} +
+ )} +
{ diff --git a/webapp/src/components/sidebar/sidebarBoardItem.tsx b/webapp/src/components/sidebar/sidebarBoardItem.tsx index c1d94b87dcc..51457759d08 100644 --- a/webapp/src/components/sidebar/sidebarBoardItem.tsx +++ b/webapp/src/components/sidebar/sidebarBoardItem.tsx @@ -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' @@ -79,6 +79,7 @@ const SidebarBoardItem = (props: Props) => { const history = useHistory() const dispatch = useAppDispatch() const currentBoardID = useAppSelector(getCurrentBoardId) + const isUserAdmin = useAppSelector(isAdmin); const generateMoveToCategoryOptions = (boardID: string) => { return props.allCategories.map((category) => ( @@ -261,12 +262,13 @@ const SidebarBoardItem = (props: Props) => { icon={} onClick={() => handleDuplicateBoard(true)} />} + {isUserAdmin && } onClick={() => Archiver.exportBoardArchive(board)} - /> + />} { + const intl = useIntl() + const board = props.board + const boardItemRef = useRef(null) + + const title = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'}) + + return ( +
+
+ {board.icon || } +
+
+ {title} +
+
+ + }/> + + } + onClick={() => Archiver.exportBoardArchive(board)} + /> + + +
+
+ ) +} + +export default React.memo(SidebarBoardItemReadOnly) diff --git a/webapp/src/components/sidebar/sidebarSettingsMenu.tsx b/webapp/src/components/sidebar/sidebarSettingsMenu.tsx index 93929831362..38c046838b1 100644 --- a/webapp/src/components/sidebar/sidebarSettingsMenu.tsx +++ b/webapp/src/components/sidebar/sidebarSettingsMenu.tsx @@ -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' @@ -27,6 +28,7 @@ import {Constants} from '../../constants' import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../telemetry/telemetryClient' + type Props = { activeTheme: string } @@ -35,6 +37,7 @@ const SidebarSettingsMenu = (props: Props) => { const intl = useIntl() const dispatch = useAppDispatch() const currentTeam = useAppSelector(getCurrentTeam) + const isUserAdmin = useAppSelector(isAdmin); // we need this as the sidebar doesn't always need to re-render // on theme change. This can cause props and the actual @@ -122,6 +125,7 @@ const SidebarSettingsMenu = (props: Props) => { )) } + {isUserAdmin && { Archiver.exportFullArchive(currentTeam.id) } }} - /> + />} { + 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(