1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-03-17 20:37:54 +02:00

Hide board feature (#3526)

* Added logic to hide a board

* WIP

* Completed hide board implementation

* Updated and added new tests

* Lint fix

* Updated snapshots
This commit is contained in:
Harshil Sharma 2022-08-04 14:49:23 +05:30 committed by GitHub
parent b6826f8509
commit 4ccb714d64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 446 additions and 13 deletions

View File

@ -150,6 +150,7 @@
"GroupBy.hideEmptyGroups": "Hide {count} empty groups",
"GroupBy.showHiddenGroups": "Show {count} hidden groups",
"GroupBy.ungroup": "Ungroup",
"HideBoard.MenuOption": "Hide board",
"KanbanCard.untitled": "Untitled",
"Mutator.new-board-from-template": "new board from template",
"Mutator.new-card-from-template": "new card from template",

View File

@ -1,5 +1,214 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/sidebarSidebar dont show hidden boards 1`] = `
<div>
<div
class="Sidebar octo-sidebar"
>
<div
class="octo-sidebar-header"
>
<div
class="heading"
>
<div
class="SidebarUserMenu"
>
<div
class="ModalWrapper"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="logo"
>
<div
class="logo-title"
>
<svg
class="FocalboardLogoIcon Icon"
version="1.1"
viewBox="0 0 52.589677 64"
x="0px"
y="0px"
>
<path
d="m 33.071077,12.069805 c -12.663,-3.4670001 -27.0530002,3.289 -31.6760002,16.943 -4.655,13.75 2.719,28.67 16.4690002,33.325 13.75,4.655 28.67,-2.719 33.326,-16.469 3.804,-11.235 -0.462,-22.701 -8.976,-29.249 l -0.46,4.871 h -0.001 c 4.631,4.896 6.709,11.941 4.325,18.985 -3.362,9.931 -14.447,15.151 -24.76,11.66 -10.313,-3.49 -15.9480002,-14.37 -12.5870002,-24.301 2.9750002,-8.788 11.9980002,-13.715 20.7430002,-12.625 v -10e-4 z m -6.175,16.488 c 3.456,-0.665 6.986,2.754 5.762,6.37 -0.854,2.522 -3.67,3.85 -6.291,2.962 -2.62,-0.887 -4.052,-3.651 -3.197,-6.174 0.573,-1.697 2.034,-2.852 3.726,-3.158 z m -1.285,-4.944 c -1.786,0.323 -3.45,1.104 -4.812,2.258 -1.299,1.101 -2.319,2.545 -2.898,4.258 -0.879,2.597 -0.579,5.323 0.617,7.632 1.206,2.329 3.325,4.234 6.07,5.164 2.744,0.929 5.584,0.701 7.959,-0.417 2.352,-1.107 4.246,-3.091 5.125,-5.688 0.555,-1.639 0.633,-3.254 0.344,-4.761 -0.21,-1.093 -0.615,-2.134 -1.174,-3.091 l 1.019,-5.107 c 0.189,0.187 0.374,0.378 0.552,0.574 1.75,1.919 3.008,4.283 3.508,6.877 0.415,2.154 0.304,4.457 -0.484,6.784 -1.239,3.661 -3.898,6.453 -7.193,8.005 -3.273,1.541 -7.175,1.858 -10.93,0.588 -3.754,-1.271 -6.661,-3.895 -8.326,-7.108 -1.674,-3.233 -2.09,-7.065 -0.851,-10.728 0.819,-2.419 2.26,-4.46 4.097,-6.016 1.88,-1.593 4.181,-2.673 6.656,-3.125 l -0.001,-0.004 c 1.759,-0.339 3.522,-0.313 5.213,0.016 l -3.583,3.761 c -0.294,0.028 -0.588,0.071 -0.883,0.127 h -0.025 z"
/>
<polygon
points="26.057,32.594 37.495,11.658 36.79,8.44 41.066,0.207 43.683,4.611 48.803,4.434 44.185,12.48 40.902,13.697 29.542,34.491 "
transform="translate(7.6780426e-5,-0.21919512)"
/>
</svg>
<span>
Focalboard
</span>
<div
class="versionFrame"
>
<div
class="version"
title="v7.3.0"
>
v7.3.0
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="octo-spacer"
/>
<div
class="sidebarSwitcher"
>
<button
class="IconButton"
type="button"
>
<svg
class="HideSidebarIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
points="80,20 50,50 80,80"
/>
<polyline
points="50,20 20,50, 50,80"
/>
</svg>
</button>
</div>
</div>
<div
class="WorkspaceTitle"
/>
<div
class="BoardsSwitcherWrapper"
>
<div
class="BoardsSwitcher"
>
<i
class="CompassIcon icon-magnify MagnifyIcon"
/>
<div>
<span>
Find Boards
</span>
</div>
</div>
</div>
<div
class="octo-sidebar-list"
>
<div
class="SidebarCategory"
>
<div
class="octo-sidebar-item category ' expanded "
>
<div
class="octo-sidebar-title category-title"
title="Category 1"
>
<i
class="CompassIcon icon-chevron-down ChevronDownIcon"
/>
Category 1
</div>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
</div>
<div
class="octo-sidebar-item subitem no-views"
>
No boards inside
</div>
</div>
<div
class="SidebarCategory"
>
<div
class="octo-sidebar-item category ' expanded "
>
<div
class="octo-sidebar-title category-title"
title="Boards"
>
<i
class="CompassIcon icon-chevron-down ChevronDownIcon"
/>
Boards
</div>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
</div>
<div
class="octo-sidebar-item subitem no-views"
>
No boards inside
</div>
</div>
</div>
<div
class="octo-spacer"
/>
<div
class="add-board"
>
+ Add board
</div>
<div
class="SidebarSettingsMenu"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="menu-entry"
>
Settings
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/sidebarSidebar sidebar expect hidden 1`] = `
<div>
<div

View File

@ -50,7 +50,10 @@ describe('components/sidebarSidebar', () => {
views: [],
},
users: {
me: {},
me: {
id: 'user_id_1',
props: {},
},
},
sidebar: {
categoryAttributes: [
@ -103,7 +106,10 @@ describe('components/sidebarSidebar', () => {
views: [],
},
users: {
me: {},
me: {
id: 'user_id_1',
props: {},
},
},
sidebar: {
categoryAttributes: [
@ -133,6 +139,61 @@ describe('components/sidebarSidebar', () => {
customGlobal.innerWidth = 1024
})
test('dont show hidden boards', () => {
const store = mockStore({
teams: {
current: {id: 'team-id'},
},
boards: {
current: board.id,
boards: {
[board.id]: board,
},
myBoardMemberships: {
[board.id]: board,
},
},
views: {
views: [],
},
users: {
me: {
id: 'user_id_1',
props: {
hiddenBoardIDs: {
[board.id]: true,
}
},
},
},
sidebar: {
categoryAttributes: [
categoryAttribute1,
],
},
})
const history = createMemoryHistory()
const component = wrapIntl(
<ReduxProvider store={store}>
<Router history={history}>
<Sidebar/>
</Router>
</ReduxProvider>,
)
const {container, getAllByText} = render(component)
expect(container).toMatchSnapshot()
const sidebarBoards = container.getElementsByClassName('SidebarBoardItem')
// The only board in redux store is hidden, so there should
// be no boards visible in sidebar
expect(sidebarBoards.length).toBe(0)
const noBoardsText = getAllByText('No boards inside')
expect(noBoardsText.length).toBe(2) // one for custom category, one for default category
})
// TODO: Fix this later
// test('global templates', () => {
// const store = mockStore({

View File

@ -31,6 +31,8 @@ import {getCurrentTeam} from '../../store/teams'
import {Constants} from "../../constants"
import {getMe} from "../../store/users"
import SidebarCategory from './sidebarCategory'
import SidebarSettingsMenu from './sidebarSettingsMenu'
import SidebarUserMenu from './sidebarUserMenu'
@ -57,6 +59,8 @@ const Sidebar = (props: Props) => {
const dispatch = useAppDispatch()
const partialCategories = useAppSelector<Array<CategoryBoards>>(getSidebarCategories)
const sidebarCategories = addMissingItems(partialCategories, boards)
const me = useAppSelector(getMe)
useEffect(() => {
wsClient.addOnChange((_: WSClient, categories: Category[]) => {
@ -107,6 +111,10 @@ const Sidebar = (props: Props) => {
}
}
if (!me) {
return <div/>
}
if (isHidden) {
return (
<div className='Sidebar octo-sidebar hidden'>

View File

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useRef, useState} from 'react'
import {useIntl} from 'react-intl'
import {useHistory, useRouteMatch} from "react-router-dom"
import {generatePath, useHistory, useRouteMatch} from "react-router-dom"
import {Board} from '../../blocks/board'
import {BoardView, IViewType} from '../../blocks/boardView'
@ -17,7 +17,7 @@ import BoardPermissionGate from '../permissions/boardPermissionGate'
import './sidebarBoardItem.scss'
import {CategoryBoards} from '../../store/sidebar'
import CreateNewFolder from '../../widgets/icons/newFolder'
import {useAppSelector} from '../../store/hooks'
import {useAppDispatch, useAppSelector} from '../../store/hooks'
import {getCurrentBoardViews, getCurrentViewId} from '../../store/views'
import Folder from '../../widgets/icons/folder'
import Check from '../../widgets/icons/checkIcon'
@ -33,6 +33,12 @@ import DuplicateIcon from "../../widgets/icons/duplicate"
import {Utils} from "../../utils"
import AddIcon from "../../widgets/icons/add"
import CloseIcon from "../../widgets/icons/close"
import {UserConfigPatch} from "../../user"
import {getMe, patchProps} from "../../store/users"
import octoClient from "../../octoClient"
import {getCurrentBoardId, getMySortedBoards} from "../../store/boards"
import {UserSettings} from "../../userSettings"
const iconForViewType = (viewType: IViewType): JSX.Element => {
switch (viewType) {
@ -63,9 +69,13 @@ const SidebarBoardItem = (props: Props) => {
const boardViews = useAppSelector(getCurrentBoardViews)
const currentViewId = useAppSelector(getCurrentViewId)
const teamID = team?.id || ''
const me = useAppSelector(getMe)
const match = useRouteMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>()
const history = useHistory()
const dispatch = useAppDispatch()
const myAllBoards = useAppSelector(getMySortedBoards)
const currentBoardID = useAppSelector(getCurrentBoardId)
const generateMoveToCategoryOptions = (boardID: string) => {
return props.allCategories.map((category) => (
@ -107,6 +117,67 @@ const SidebarBoardItem = (props: Props) => {
}, [board.id])
const showTemplatePicker = () => {
// if the same board, reuse the match params
// otherwise remove viewId and cardId, results in first view being selected
const params = {teamId: match.params.teamId}
const newPath = generatePath('/team/:teamId?', params)
history.push(newPath)
}
const handleHideBoard = async() => {
console.log('handleHideBoard')
if (!me ) {
return
}
// creating new array from me.props.hiddenBoardIDs as
// me.props.hiddenBoardIDs belongs to Redux state and
// so is immutable.
const hiddenBoards = {...(me.props.hiddenBoardIDs || {})}
// check for already hidden board. Skip if so
// if (hiddenBoards.indexOf(board.id) > -1) {
// return
// }
hiddenBoards[board.id] = true
const hiddenBoardsArray = Object.keys(hiddenBoards)
const patch: UserConfigPatch = {
updatedFields: {
'hiddenBoardIDs': JSON.stringify(hiddenBoardsArray),
}
}
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
if (!patchedProps) {
return
}
await dispatch(patchProps(patchedProps))
// If we're hiding the board we're currently on,
// we need to switch to a different board once its hidden.
if (currentBoardID === props.board.id) {
// There's no special logic on what the next board needs to be.
// To keep things simple, we just switch to the first unhidden board
// Empty board ID navigates to template picker, which is
// fine if there are no more visible boards to switch to.
const visibleBoards = myAllBoards.filter((b) => !hiddenBoards[b.id])
if (visibleBoards.length === 0) {
UserSettings.setLastBoardID(match.params.teamId!, null)
showTemplatePicker()
} else {
let nextBoardID = ''
if (visibleBoards.length > 0) {
nextBoardID = visibleBoards[0].id
}
props.showBoard(nextBoardID)
}
}
}
const boardItemRef = useRef<HTMLDivElement>(null)
const title = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'})
@ -179,6 +250,12 @@ const SidebarBoardItem = (props: Props) => {
icon={<AddIcon/>}
onClick={() => handleDuplicateBoard(true)}
/>
<Menu.Text
id='hideBoard'
name={intl.formatMessage({id: 'HideBoard.MenuOption', defaultMessage: 'Hide board'})}
icon={<CloseIcon/>}
onClick={() => handleHideBoard()}
/>
</Menu>
</MenuWrapper>
</div>

View File

@ -48,6 +48,7 @@ describe('components/sidebarCategory', () => {
users: {
me: {
id: 'user_id_1',
props: {}
},
},
boards: {

View File

@ -78,7 +78,19 @@ const SidebarCategory = (props: Props) => {
props.hideSidebar()
}, [match, history])
const isBoardVisible = (boardID: string): boolean => {
// hide if board doesn't belong to current category
if (!blocks.includes(boardID)) {
return false
}
// hide if board was hidden by the user
const hiddenBoardIDs = me?.props.hiddenBoardIDs || {}
return !hiddenBoardIDs[boardID]
}
const blocks = props.categoryBoards.boardIDs || []
const visibleBlocks = props.categoryBoards.boardIDs.filter((boardID) => isBoardVisible(boardID))
const handleCreateNewCategory = () => {
setShowCreateCategoryModal(true)
@ -189,7 +201,7 @@ const SidebarCategory = (props: Props) => {
</Menu>
</MenuWrapper>
</div>
{!collapsed && blocks.length === 0 &&
{!collapsed && visibleBlocks.length === 0 &&
<div className='octo-sidebar-item subitem no-views'>
<FormattedMessage
id='Sidebar.no-boards-in-category'
@ -197,7 +209,7 @@ const SidebarCategory = (props: Props) => {
/>
</div>}
{!collapsed && props.boards.map((board: Board) => {
if (!blocks.includes(board.id)) {
if (!isBoardVisible(board.id)) {
return null
}
return (

View File

@ -21,10 +21,13 @@ import wsClient, {WSClient} from '../wsclient'
import {ClientConfig} from '../config/clientConfig'
import {Utils} from '../utils'
import {getMe} from "../store/users"
import CenterPanel from './centerPanel'
import BoardTemplateSelector from './boardTemplateSelector/boardTemplateSelector'
import Sidebar from './sidebar/sidebar'
import './workspace.scss'
type Props = {
@ -46,6 +49,12 @@ function CenterContent(props: Props) {
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
const history = useHistory()
const dispatch = useAppDispatch()
const me = useAppSelector(getMe)
const isBoardHidden = () => {
const hiddenBoardIDs = me?.props.hiddenBoardIDs || {}
return hiddenBoardIDs[board.id]
}
const showCard = useCallback((cardId?: string) => {
const params = {...match.params, cardId}
@ -76,7 +85,7 @@ function CenterContent(props: Props) {
}
}, [cardLimitTimestamp, match.params.boardId, templates])
if (board && activeView) {
if (board && !isBoardHidden() && activeView) {
let property = groupByProperty
if ((!property || property.type !== 'select') && activeView.fields.viewType === 'board') {
property = board?.cardProperties.find((o) => o.type === 'select')
@ -104,7 +113,7 @@ function CenterContent(props: Props) {
)
}
if (board || isLoading) {
if ((board && !isBoardHidden()) || isLoading) {
return null
}

View File

@ -11,7 +11,7 @@ import octoClient from '../../octoClient'
import {Subscription, WSClient} from '../../wsclient'
import {Utils} from '../../utils'
import {useWebsockets} from '../../hooks/websockets'
import {IUser} from '../../user'
import {IUser, UserConfigPatch} from '../../user'
import {Block} from '../../blocks/block'
import {ContentBlock} from '../../blocks/contentBlock'
import {CommentBlock} from '../../blocks/commentBlock'
@ -37,7 +37,7 @@ import {
fetchUserBlockSubscriptions,
getMe,
followBlock,
unfollowBlock,
unfollowBlock, patchProps,
} from '../../store/users'
import {setGlobalError} from '../../store/globalError'
import {UserSettings} from '../../userSettings'
@ -201,6 +201,41 @@ const BoardPage = (props: Props): JSX.Element => {
}
}, [teamId, match.params.boardId, viewId, me?.id])
const handleUnhideBoard = async (boardID: string) => {
console.log(`handleUnhideBoard called`)
if (!me) {
return
}
const hiddenBoards = {...(me.props.hiddenBoardIDs || {})}
// const index = hiddenBoards.indexOf(boardID)
// hiddenBoards.splice(index, 1)
delete hiddenBoards[boardID]
const hiddenBoardsArray = Object.keys(hiddenBoards)
const patch: UserConfigPatch = {
updatedFields: {
'hiddenBoardIDs': JSON.stringify(hiddenBoardsArray),
}
}
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
if (!patchedProps) {
return
}
await dispatch(patchProps(patchedProps))
}
useEffect(() => {
if (!teamId || !match.params.boardId) {
return
}
const hiddenBoardIDs = me?.props.hiddenBoardIDs || {}
if (hiddenBoardIDs[match.params.boardId]) {
handleUnhideBoard(match.params.boardId)
}
}, [me?.id, teamId, match.params.boardId])
if (props.readonly) {
useEffect(() => {
if (activeBoardId && activeViewId) {

View File

@ -4,7 +4,7 @@
import {createSlice, createAsyncThunk, PayloadAction, createSelector} from '@reduxjs/toolkit'
import {default as client} from '../octoClient'
import {IUser} from '../user'
import {IUser, parseUserProps} from '../user'
import {Utils} from '../utils'
@ -47,6 +47,9 @@ const usersSlice = createSlice({
reducers: {
setMe: (state, action: PayloadAction<IUser|null>) => {
state.me = action.payload
if (state.me) {
state.me.props = parseUserProps(state.me.props)
}
state.loggedIn = Boolean(state.me)
},
setBoardUsers: (state, action: PayloadAction<IUser[]>) => {
@ -74,13 +77,16 @@ const usersSlice = createSlice({
},
patchProps: (state, action: PayloadAction<Record<string, string>>) => {
if (state.me) {
state.me.props = action.payload
state.me.props = parseUserProps(action.payload)
}
},
},
extraReducers: (builder) => {
builder.addCase(fetchMe.fulfilled, (state, action) => {
state.me = action.payload || null
if (state.me) {
state.me.props = parseUserProps(state.me.props)
}
state.loggedIn = Boolean(state.me)
})
builder.addCase(fetchMe.rejected, (state) => {

View File

@ -27,6 +27,20 @@ interface UserConfigPatch {
deletedFields?: string[]
}
function parseUserProps(props: Record<string, any>): Record<string, any> {
const processedProps = props
const hiddenBoardIDs = props.hiddenBoardIDs ? JSON.parse(props.hiddenBoardIDs) : []
processedProps.hiddenBoardIDs = {}
hiddenBoardIDs.forEach((boardID: string) => processedProps.hiddenBoardIDs[boardID] = true)
return processedProps
}
const UserPropPrefix = 'focalboard_'
export {IUser, UserWorkspace, UserConfigPatch, UserPropPrefix}
export {
IUser,
UserWorkspace,
UserConfigPatch,
UserPropPrefix,
parseUserProps,
}