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:
parent
b6826f8509
commit
4ccb714d64
@ -150,6 +150,7 @@
|
|||||||
"GroupBy.hideEmptyGroups": "Hide {count} empty groups",
|
"GroupBy.hideEmptyGroups": "Hide {count} empty groups",
|
||||||
"GroupBy.showHiddenGroups": "Show {count} hidden groups",
|
"GroupBy.showHiddenGroups": "Show {count} hidden groups",
|
||||||
"GroupBy.ungroup": "Ungroup",
|
"GroupBy.ungroup": "Ungroup",
|
||||||
|
"HideBoard.MenuOption": "Hide board",
|
||||||
"KanbanCard.untitled": "Untitled",
|
"KanbanCard.untitled": "Untitled",
|
||||||
"Mutator.new-board-from-template": "new board from template",
|
"Mutator.new-board-from-template": "new board from template",
|
||||||
"Mutator.new-card-from-template": "new card from template",
|
"Mutator.new-card-from-template": "new card from template",
|
||||||
|
@ -1,5 +1,214 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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`] = `
|
exports[`components/sidebarSidebar sidebar expect hidden 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
@ -50,7 +50,10 @@ describe('components/sidebarSidebar', () => {
|
|||||||
views: [],
|
views: [],
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
me: {},
|
me: {
|
||||||
|
id: 'user_id_1',
|
||||||
|
props: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
categoryAttributes: [
|
categoryAttributes: [
|
||||||
@ -103,7 +106,10 @@ describe('components/sidebarSidebar', () => {
|
|||||||
views: [],
|
views: [],
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
me: {},
|
me: {
|
||||||
|
id: 'user_id_1',
|
||||||
|
props: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
categoryAttributes: [
|
categoryAttributes: [
|
||||||
@ -133,6 +139,61 @@ describe('components/sidebarSidebar', () => {
|
|||||||
customGlobal.innerWidth = 1024
|
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
|
// TODO: Fix this later
|
||||||
// test('global templates', () => {
|
// test('global templates', () => {
|
||||||
// const store = mockStore({
|
// const store = mockStore({
|
||||||
|
@ -31,6 +31,8 @@ import {getCurrentTeam} from '../../store/teams'
|
|||||||
|
|
||||||
import {Constants} from "../../constants"
|
import {Constants} from "../../constants"
|
||||||
|
|
||||||
|
import {getMe} from "../../store/users"
|
||||||
|
|
||||||
import SidebarCategory from './sidebarCategory'
|
import SidebarCategory from './sidebarCategory'
|
||||||
import SidebarSettingsMenu from './sidebarSettingsMenu'
|
import SidebarSettingsMenu from './sidebarSettingsMenu'
|
||||||
import SidebarUserMenu from './sidebarUserMenu'
|
import SidebarUserMenu from './sidebarUserMenu'
|
||||||
@ -57,6 +59,8 @@ const Sidebar = (props: Props) => {
|
|||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const partialCategories = useAppSelector<Array<CategoryBoards>>(getSidebarCategories)
|
const partialCategories = useAppSelector<Array<CategoryBoards>>(getSidebarCategories)
|
||||||
const sidebarCategories = addMissingItems(partialCategories, boards)
|
const sidebarCategories = addMissingItems(partialCategories, boards)
|
||||||
|
const me = useAppSelector(getMe)
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
wsClient.addOnChange((_: WSClient, categories: Category[]) => {
|
wsClient.addOnChange((_: WSClient, categories: Category[]) => {
|
||||||
@ -107,6 +111,10 @@ const Sidebar = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!me) {
|
||||||
|
return <div/>
|
||||||
|
}
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
return (
|
return (
|
||||||
<div className='Sidebar octo-sidebar hidden'>
|
<div className='Sidebar octo-sidebar hidden'>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import React, {useCallback, useRef, useState} from 'react'
|
import React, {useCallback, useRef, useState} from 'react'
|
||||||
import {useIntl} from 'react-intl'
|
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 {Board} from '../../blocks/board'
|
||||||
import {BoardView, IViewType} from '../../blocks/boardView'
|
import {BoardView, IViewType} from '../../blocks/boardView'
|
||||||
@ -17,7 +17,7 @@ import BoardPermissionGate from '../permissions/boardPermissionGate'
|
|||||||
import './sidebarBoardItem.scss'
|
import './sidebarBoardItem.scss'
|
||||||
import {CategoryBoards} from '../../store/sidebar'
|
import {CategoryBoards} from '../../store/sidebar'
|
||||||
import CreateNewFolder from '../../widgets/icons/newFolder'
|
import CreateNewFolder from '../../widgets/icons/newFolder'
|
||||||
import {useAppSelector} from '../../store/hooks'
|
import {useAppDispatch, useAppSelector} from '../../store/hooks'
|
||||||
import {getCurrentBoardViews, getCurrentViewId} from '../../store/views'
|
import {getCurrentBoardViews, getCurrentViewId} from '../../store/views'
|
||||||
import Folder from '../../widgets/icons/folder'
|
import Folder from '../../widgets/icons/folder'
|
||||||
import Check from '../../widgets/icons/checkIcon'
|
import Check from '../../widgets/icons/checkIcon'
|
||||||
@ -33,6 +33,12 @@ import DuplicateIcon from "../../widgets/icons/duplicate"
|
|||||||
import {Utils} from "../../utils"
|
import {Utils} from "../../utils"
|
||||||
|
|
||||||
import AddIcon from "../../widgets/icons/add"
|
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 => {
|
const iconForViewType = (viewType: IViewType): JSX.Element => {
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
@ -63,9 +69,13 @@ const SidebarBoardItem = (props: Props) => {
|
|||||||
const boardViews = useAppSelector(getCurrentBoardViews)
|
const boardViews = useAppSelector(getCurrentBoardViews)
|
||||||
const currentViewId = useAppSelector(getCurrentViewId)
|
const currentViewId = useAppSelector(getCurrentViewId)
|
||||||
const teamID = team?.id || ''
|
const teamID = team?.id || ''
|
||||||
|
const me = useAppSelector(getMe)
|
||||||
|
|
||||||
const match = useRouteMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>()
|
const match = useRouteMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const myAllBoards = useAppSelector(getMySortedBoards)
|
||||||
|
const currentBoardID = useAppSelector(getCurrentBoardId)
|
||||||
|
|
||||||
const generateMoveToCategoryOptions = (boardID: string) => {
|
const generateMoveToCategoryOptions = (boardID: string) => {
|
||||||
return props.allCategories.map((category) => (
|
return props.allCategories.map((category) => (
|
||||||
@ -107,6 +117,67 @@ const SidebarBoardItem = (props: Props) => {
|
|||||||
|
|
||||||
}, [board.id])
|
}, [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 boardItemRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const title = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'})
|
const title = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'})
|
||||||
@ -179,6 +250,12 @@ const SidebarBoardItem = (props: Props) => {
|
|||||||
icon={<AddIcon/>}
|
icon={<AddIcon/>}
|
||||||
onClick={() => handleDuplicateBoard(true)}
|
onClick={() => handleDuplicateBoard(true)}
|
||||||
/>
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
id='hideBoard'
|
||||||
|
name={intl.formatMessage({id: 'HideBoard.MenuOption', defaultMessage: 'Hide board'})}
|
||||||
|
icon={<CloseIcon/>}
|
||||||
|
onClick={() => handleHideBoard()}
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,6 +48,7 @@ describe('components/sidebarCategory', () => {
|
|||||||
users: {
|
users: {
|
||||||
me: {
|
me: {
|
||||||
id: 'user_id_1',
|
id: 'user_id_1',
|
||||||
|
props: {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
boards: {
|
boards: {
|
||||||
|
@ -78,7 +78,19 @@ const SidebarCategory = (props: Props) => {
|
|||||||
props.hideSidebar()
|
props.hideSidebar()
|
||||||
}, [match, history])
|
}, [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 blocks = props.categoryBoards.boardIDs || []
|
||||||
|
const visibleBlocks = props.categoryBoards.boardIDs.filter((boardID) => isBoardVisible(boardID))
|
||||||
|
|
||||||
const handleCreateNewCategory = () => {
|
const handleCreateNewCategory = () => {
|
||||||
setShowCreateCategoryModal(true)
|
setShowCreateCategoryModal(true)
|
||||||
@ -189,7 +201,7 @@ const SidebarCategory = (props: Props) => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
</div>
|
</div>
|
||||||
{!collapsed && blocks.length === 0 &&
|
{!collapsed && visibleBlocks.length === 0 &&
|
||||||
<div className='octo-sidebar-item subitem no-views'>
|
<div className='octo-sidebar-item subitem no-views'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='Sidebar.no-boards-in-category'
|
id='Sidebar.no-boards-in-category'
|
||||||
@ -197,7 +209,7 @@ const SidebarCategory = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>}
|
</div>}
|
||||||
{!collapsed && props.boards.map((board: Board) => {
|
{!collapsed && props.boards.map((board: Board) => {
|
||||||
if (!blocks.includes(board.id)) {
|
if (!isBoardVisible(board.id)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -21,10 +21,13 @@ import wsClient, {WSClient} from '../wsclient'
|
|||||||
import {ClientConfig} from '../config/clientConfig'
|
import {ClientConfig} from '../config/clientConfig'
|
||||||
import {Utils} from '../utils'
|
import {Utils} from '../utils'
|
||||||
|
|
||||||
|
import {getMe} from "../store/users"
|
||||||
|
|
||||||
import CenterPanel from './centerPanel'
|
import CenterPanel from './centerPanel'
|
||||||
import BoardTemplateSelector from './boardTemplateSelector/boardTemplateSelector'
|
import BoardTemplateSelector from './boardTemplateSelector/boardTemplateSelector'
|
||||||
|
|
||||||
import Sidebar from './sidebar/sidebar'
|
import Sidebar from './sidebar/sidebar'
|
||||||
|
|
||||||
import './workspace.scss'
|
import './workspace.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -46,6 +49,12 @@ function CenterContent(props: Props) {
|
|||||||
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
|
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const me = useAppSelector(getMe)
|
||||||
|
|
||||||
|
const isBoardHidden = () => {
|
||||||
|
const hiddenBoardIDs = me?.props.hiddenBoardIDs || {}
|
||||||
|
return hiddenBoardIDs[board.id]
|
||||||
|
}
|
||||||
|
|
||||||
const showCard = useCallback((cardId?: string) => {
|
const showCard = useCallback((cardId?: string) => {
|
||||||
const params = {...match.params, cardId}
|
const params = {...match.params, cardId}
|
||||||
@ -76,7 +85,7 @@ function CenterContent(props: Props) {
|
|||||||
}
|
}
|
||||||
}, [cardLimitTimestamp, match.params.boardId, templates])
|
}, [cardLimitTimestamp, match.params.boardId, templates])
|
||||||
|
|
||||||
if (board && activeView) {
|
if (board && !isBoardHidden() && activeView) {
|
||||||
let property = groupByProperty
|
let property = groupByProperty
|
||||||
if ((!property || property.type !== 'select') && activeView.fields.viewType === 'board') {
|
if ((!property || property.type !== 'select') && activeView.fields.viewType === 'board') {
|
||||||
property = board?.cardProperties.find((o) => o.type === 'select')
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import octoClient from '../../octoClient'
|
|||||||
import {Subscription, WSClient} from '../../wsclient'
|
import {Subscription, WSClient} from '../../wsclient'
|
||||||
import {Utils} from '../../utils'
|
import {Utils} from '../../utils'
|
||||||
import {useWebsockets} from '../../hooks/websockets'
|
import {useWebsockets} from '../../hooks/websockets'
|
||||||
import {IUser} from '../../user'
|
import {IUser, UserConfigPatch} from '../../user'
|
||||||
import {Block} from '../../blocks/block'
|
import {Block} from '../../blocks/block'
|
||||||
import {ContentBlock} from '../../blocks/contentBlock'
|
import {ContentBlock} from '../../blocks/contentBlock'
|
||||||
import {CommentBlock} from '../../blocks/commentBlock'
|
import {CommentBlock} from '../../blocks/commentBlock'
|
||||||
@ -37,7 +37,7 @@ import {
|
|||||||
fetchUserBlockSubscriptions,
|
fetchUserBlockSubscriptions,
|
||||||
getMe,
|
getMe,
|
||||||
followBlock,
|
followBlock,
|
||||||
unfollowBlock,
|
unfollowBlock, patchProps,
|
||||||
} from '../../store/users'
|
} from '../../store/users'
|
||||||
import {setGlobalError} from '../../store/globalError'
|
import {setGlobalError} from '../../store/globalError'
|
||||||
import {UserSettings} from '../../userSettings'
|
import {UserSettings} from '../../userSettings'
|
||||||
@ -201,6 +201,41 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}, [teamId, match.params.boardId, viewId, me?.id])
|
}, [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) {
|
if (props.readonly) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeBoardId && activeViewId) {
|
if (activeBoardId && activeViewId) {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import {createSlice, createAsyncThunk, PayloadAction, createSelector} from '@reduxjs/toolkit'
|
import {createSlice, createAsyncThunk, PayloadAction, createSelector} from '@reduxjs/toolkit'
|
||||||
|
|
||||||
import {default as client} from '../octoClient'
|
import {default as client} from '../octoClient'
|
||||||
import {IUser} from '../user'
|
import {IUser, parseUserProps} from '../user'
|
||||||
|
|
||||||
import {Utils} from '../utils'
|
import {Utils} from '../utils'
|
||||||
|
|
||||||
@ -47,6 +47,9 @@ const usersSlice = createSlice({
|
|||||||
reducers: {
|
reducers: {
|
||||||
setMe: (state, action: PayloadAction<IUser|null>) => {
|
setMe: (state, action: PayloadAction<IUser|null>) => {
|
||||||
state.me = action.payload
|
state.me = action.payload
|
||||||
|
if (state.me) {
|
||||||
|
state.me.props = parseUserProps(state.me.props)
|
||||||
|
}
|
||||||
state.loggedIn = Boolean(state.me)
|
state.loggedIn = Boolean(state.me)
|
||||||
},
|
},
|
||||||
setBoardUsers: (state, action: PayloadAction<IUser[]>) => {
|
setBoardUsers: (state, action: PayloadAction<IUser[]>) => {
|
||||||
@ -74,13 +77,16 @@ const usersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
patchProps: (state, action: PayloadAction<Record<string, string>>) => {
|
patchProps: (state, action: PayloadAction<Record<string, string>>) => {
|
||||||
if (state.me) {
|
if (state.me) {
|
||||||
state.me.props = action.payload
|
state.me.props = parseUserProps(action.payload)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(fetchMe.fulfilled, (state, action) => {
|
builder.addCase(fetchMe.fulfilled, (state, action) => {
|
||||||
state.me = action.payload || null
|
state.me = action.payload || null
|
||||||
|
if (state.me) {
|
||||||
|
state.me.props = parseUserProps(state.me.props)
|
||||||
|
}
|
||||||
state.loggedIn = Boolean(state.me)
|
state.loggedIn = Boolean(state.me)
|
||||||
})
|
})
|
||||||
builder.addCase(fetchMe.rejected, (state) => {
|
builder.addCase(fetchMe.rejected, (state) => {
|
||||||
|
@ -27,6 +27,20 @@ interface UserConfigPatch {
|
|||||||
deletedFields?: string[]
|
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_'
|
const UserPropPrefix = 'focalboard_'
|
||||||
|
|
||||||
export {IUser, UserWorkspace, UserConfigPatch, UserPropPrefix}
|
export {
|
||||||
|
IUser,
|
||||||
|
UserWorkspace,
|
||||||
|
UserConfigPatch,
|
||||||
|
UserPropPrefix,
|
||||||
|
parseUserProps,
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user