main

mattermost/focalboard

Last updated at: 29/12/2023 09:42

sidebar.ts

TLDR

This file contains the implementation of the sidebar functionality for a project management application. It defines interfaces and types for categories and boards, and provides methods and selectors to update and fetch sidebar data.

Classes

No classes are defined in this file.

Methods

fetchSidebarCategories

This method is an asynchronous thunk that fetches sidebar categories for a specific team.

updateCategories

This method updates the categories in the sidebar state. It takes an array of Category objects as the payload and updates the existing categories accordingly.

updateBoardCategories

This method updates the categories of boards in the sidebar state. It takes an array of BoardCategoryWebsocketData objects as the payload and updates the board categories accordingly.

updateCategoryOrder

This method updates the order of categories in the sidebar state. It takes an array of category IDs as the payload and reorders the categories accordingly.

updateCategoryBoardsOrder

This method updates the order of boards within a category in the sidebar state. It takes a CategoryBoardsReorderData object as the payload and updates the board order accordingly.

END

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {createAsyncThunk, createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit'

import {default as client} from '../octoClient'

import {Utils} from '../utils'

import {RootState} from './index'

export type CategoryType = 'system' | 'custom'

interface Category {
    id: string
    name: string
    userID: string
    teamID: string
    createAt: number
    updateAt: number
    deleteAt: number
    collapsed: boolean
    sortOrder: number
    type: CategoryType
    isNew: boolean
}

interface CategoryBoardMetadata {
    boardID: string
    hidden: boolean
}

interface CategoryBoards extends Category {
    boardMetadata: CategoryBoardMetadata[]
}

interface BoardCategoryWebsocketData {
    boardID: string
    categoryID: string
    hidden: boolean
}

interface CategoryBoardsReorderData {
    categoryID: string
    boardsMetadata: CategoryBoardMetadata[]
}

export const DefaultCategory: CategoryBoards = {
    id: '',
    name: 'Boards',
} as CategoryBoards

export const fetchSidebarCategories = createAsyncThunk(
    'sidebarCategories/fetch',
    async (teamID: string) => {
        return client.getSidebarCategories(teamID)
    },
)

type Sidebar = {
    categoryAttributes: CategoryBoards[]
    hiddenBoardIDs: string[]
}

const sidebarSlice = createSlice({
    name: 'sidebar',
    initialState: {categoryAttributes: [], hiddenBoardIDs: []} as Sidebar,
    reducers: {
        updateCategories: (state, action: PayloadAction<Category[]>) => {
            action.payload.forEach((updatedCategory) => {
                const index = state.categoryAttributes.findIndex((c) => c.id === updatedCategory.id)

                // when new category got created,
                if (index === -1) {
                    // new categories should always show up on the top
                    state.categoryAttributes.unshift({
                        ...updatedCategory,
                        boardMetadata: [],
                        isNew: true,
                    })
                } else if (updatedCategory.deleteAt) {
                    // when category is deleted
                    state.categoryAttributes.splice(index, 1)
                } else {
                    // else all, update the category
                    state.categoryAttributes[index] = {
                        ...state.categoryAttributes[index],
                        name: updatedCategory.name,
                        updateAt: updatedCategory.updateAt,
                        isNew: false,
                    }
                }
            })
        },
        updateBoardCategories: (state, action: PayloadAction<BoardCategoryWebsocketData[]>) => {
            const updatedCategoryAttributes: CategoryBoards[] = []
            let updatedHiddenBoardIDs = state.hiddenBoardIDs

            action.payload.forEach((boardCategory) => {
                for (let i = 0; i < state.categoryAttributes.length; i++) {
                    const categoryAttribute = state.categoryAttributes[i]

                    if (categoryAttribute.id === boardCategory.categoryID) {
                        const categoryBoardMetadataIndex = categoryAttribute.boardMetadata.findIndex((boardMetadata) => boardMetadata.boardID === boardCategory.boardID)
                        if (categoryBoardMetadataIndex >= 0) {
                            categoryAttribute.boardMetadata[categoryBoardMetadataIndex] = {
                                ...categoryAttribute.boardMetadata[categoryBoardMetadataIndex],
                                hidden: boardCategory.hidden,
                            }
                        } else {
                            categoryAttribute.boardMetadata.unshift({boardID: boardCategory.boardID, hidden: boardCategory.hidden})
                            categoryAttribute.isNew = false
                        }
                    } else {
                        // remove the board from other categories
                        categoryAttribute.boardMetadata = categoryAttribute.boardMetadata.filter((metadata) => metadata.boardID !== boardCategory.boardID)
                    }

                    updatedCategoryAttributes[i] = categoryAttribute

                    if (boardCategory.hidden) {
                        if (updatedHiddenBoardIDs.indexOf(boardCategory.boardID) < 0) {
                            updatedHiddenBoardIDs.push(boardCategory.boardID)
                        }
                    } else {
                        updatedHiddenBoardIDs = updatedHiddenBoardIDs.filter((hiddenBoardID) => hiddenBoardID !== boardCategory.boardID)
                    }
                }
            })

            if (updatedCategoryAttributes.length > 0) {
                state.categoryAttributes = updatedCategoryAttributes
            }
            state.hiddenBoardIDs = updatedHiddenBoardIDs
        },
        updateCategoryOrder: (state, action: PayloadAction<string[]>) => {
            if (action.payload.length === 0) {
                return
            }

            const categoryById = new Map<string, CategoryBoards>()
            state.categoryAttributes.forEach((categoryBoards: CategoryBoards) => categoryById.set(categoryBoards.id, categoryBoards))

            const newOrderedCategories: CategoryBoards[] = []
            action.payload.forEach((categoryId) => {
                const category = categoryById.get(categoryId)
                if (!category) {
                    Utils.logError('Category ID from updated category order not found in store. CategoryID: ' + categoryId)
                    return
                }
                newOrderedCategories.push(category)
            })

            state.categoryAttributes = newOrderedCategories
        },
        updateCategoryBoardsOrder: (state, action: PayloadAction<CategoryBoardsReorderData>) => {
            if (action.payload.boardsMetadata.length === 0) {
                return
            }

            const categoryIndex = state.categoryAttributes.findIndex((categoryBoards) => categoryBoards.id === action.payload.categoryID)
            if (categoryIndex < 0) {
                Utils.logError('Category ID from updated category boards order not found in store. CategoryID: ' + action.payload.categoryID)
                return
            }

            const category = state.categoryAttributes[categoryIndex]
            const updatedCategory: CategoryBoards = {
                ...category,
                boardMetadata: action.payload.boardsMetadata,
                isNew: false,
            }

            // creating a new reference of array so redux knows it changed
            state.categoryAttributes = state.categoryAttributes.map((original, i) => (i === categoryIndex ? updatedCategory : original))
        },
    },
    extraReducers: (builder) => {
        builder.addCase(fetchSidebarCategories.fulfilled, (state, action) => {
            state.categoryAttributes = action.payload || []
            state.hiddenBoardIDs = state.categoryAttributes.flatMap(
                (ca) => {
                    return ca.boardMetadata.reduce((collector, m) => {
                        if (m.hidden) {
                            collector.push(m.boardID)
                        }

                        return collector
                    }, [] as string[])
                },
            )
        })
    },
})

export const getSidebarCategories = createSelector(
    (state: RootState): CategoryBoards[] => state.sidebar.categoryAttributes,
    (sidebarCategories) => sidebarCategories,
)

export const getHiddenBoardIDs = (state: RootState): string[] => state.sidebar.hiddenBoardIDs

export function getCategoryOfBoard(boardID: string): (state: RootState) => CategoryBoards | undefined {
    return createSelector(
        (state: RootState): CategoryBoards[] => state.sidebar.categoryAttributes,
        (sidebarCategories) => sidebarCategories.find((category) => category.boardMetadata.findIndex((m) => m.boardID === boardID) >= 0),
    )
}

export const {reducer} = sidebarSlice

export const {updateCategories, updateBoardCategories, updateCategoryOrder, updateCategoryBoardsOrder} = sidebarSlice.actions

export {Category, CategoryBoards, BoardCategoryWebsocketData, CategoryBoardsReorderData, CategoryBoardMetadata}