From ca034e7d4794b10ac2f73a18f2e56c2bc6447025 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Mon, 22 Sep 2025 19:30:02 +0530 Subject: [PATCH 01/16] Rollback of sidebar order when API response false --- webapp/src/components/sidebar/sidebar.tsx | 59 +++++++++++++++-------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index 11f4c036a..e41cbc1b0 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -44,7 +44,7 @@ import octoClient from '../../octoClient' import {useWebsockets} from '../../hooks/websockets' -import mutator from '../../mutator' +// import mutator from '../../mutator' import {Board} from '../../blocks/board' @@ -214,32 +214,34 @@ const Sidebar = (props: Props) => { const toCategoryID = destination.droppableId const boardID = draggableId - if (fromCategoryID === toCategoryID) { - // board re-arranged withing the same category - const toSidebarCategory = sidebarCategories.find((category) => category.id === toCategoryID) - if (!toSidebarCategory) { - Utils.logError(`toCategoryID not found in list of sidebar categories. toCategoryID: ${toCategoryID}`) - return - } + const toSidebarCategory = sidebarCategories.find((category) => category.id === toCategoryID) + if (!toSidebarCategory) { + Utils.logError(`toCategoryID not found in list of sidebar categories. toCategoryID: ${toCategoryID}`) + return + } + const previousToBoardsMetadata = [...toSidebarCategory.boardMetadata] + if (fromCategoryID === toCategoryID) { const categoryBoardMetadata = [...toSidebarCategory.boardMetadata] categoryBoardMetadata.splice(source.index, 1) categoryBoardMetadata.splice(destination.index, 0, toSidebarCategory.boardMetadata[source.index]) dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: categoryBoardMetadata})) - const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) - await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) + try { + const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) + const updatedOrder = await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) + if (reorderedBoardIDs.length > 0 && updatedOrder.length === 0) { + throw new Error('reorderSidebarCategoryBoards failed') + } + } catch (err) { + Utils.logError(`Failed to persist boards reorder for category ${toCategoryID}: ${err}`) + dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) + } } else { // board moved to a different category - const toSidebarCategory = sidebarCategories.find((category) => category.id === toCategoryID) const fromSidebarCategory = sidebarCategories.find((category) => category.id === fromCategoryID) - if (!toSidebarCategory) { - Utils.logError(`toCategoryID not found in list of sidebar categories. toCategoryID: ${toCategoryID}`) - return - } - if (!fromSidebarCategory) { Utils.logError(`fromCategoryID not found in list of sidebar categories. fromCategoryID: ${fromCategoryID}`) return @@ -249,14 +251,31 @@ const Sidebar = (props: Props) => { const fromCategoryBoardMetadata = fromSidebarCategory.boardMetadata[source.index] categoryBoardMetadata.splice(destination.index, 0, fromCategoryBoardMetadata) - // optimistically updating the store to create a lag-free UI. await dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: categoryBoardMetadata})) dispatch(updateBoardCategories([{...fromCategoryBoardMetadata, categoryID: toCategoryID}])) - await mutator.moveBoardToCategory(team.id, boardID, toCategoryID, fromCategoryID) + try { + const moveResp = await octoClient.moveBoardToCategory(team.id, boardID, toCategoryID, fromCategoryID) + if (!moveResp || !moveResp.ok) { + throw new Error('moveBoardToCategory failed') + } + } catch (err) { + Utils.logError(`Failed to move board ${boardID} from ${fromCategoryID} to ${toCategoryID}: ${err}`) + dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) + dispatch(updateBoardCategories([{...fromCategoryBoardMetadata, categoryID: fromCategoryID}])) + return + } - const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) - await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) + try { + const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) + const updatedOrder = await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) + if (reorderedBoardIDs.length > 0 && updatedOrder.length === 0) { + throw new Error('reorderSidebarCategoryBoards failed after move') + } + } catch (err) { + Utils.logError(`Failed to persist boards reorder for destination category ${toCategoryID}: ${err}`) + dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) + } } }, [team, sidebarCategories]) From a9d4dde108e8649da752e811b5b6001a9114aba3 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Mon, 22 Sep 2025 22:35:47 +0530 Subject: [PATCH 02/16] Fixed the join the private board issue --- .../components/sidebar/sidebarBoardItem.tsx | 17 +++++++----- webapp/src/pages/boardPage/boardPage.tsx | 8 +++++- webapp/src/router.tsx | 27 +++++++++++++++++++ webapp/src/store/sidebar.ts | 24 ++++++++++++++++- 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/webapp/src/components/sidebar/sidebarBoardItem.tsx b/webapp/src/components/sidebar/sidebarBoardItem.tsx index 2a8b88d8f..5363ef1a4 100644 --- a/webapp/src/components/sidebar/sidebarBoardItem.tsx +++ b/webapp/src/components/sidebar/sidebarBoardItem.tsx @@ -23,6 +23,7 @@ import {CategoryBoards, updateBoardCategories} from '../../store/sidebar' import CreateNewFolder from '../../widgets/icons/newFolder' import {useAppDispatch, useAppSelector} from '../../store/hooks' import {getCurrentBoardViews, getCurrentViewId} from '../../store/views' +import {getMySortedBoards} from '../../store/boards' import Folder from '../../widgets/icons/folder' import Check from '../../widgets/icons/checkIcon' import CompassIcon from '../../widgets/icons/compassIcon' @@ -83,6 +84,7 @@ const SidebarBoardItem = (props: Props) => { const history = useHistory() const dispatch = useAppDispatch() const currentBoardID = useAppSelector(getCurrentBoardId) + const allBoards = useAppSelector(getMySortedBoards) const generateMoveToCategoryOptions = (boardID: string) => { return props.allCategories.map((category) => ( @@ -175,21 +177,24 @@ const SidebarBoardItem = (props: Props) => { // Empty board ID navigates to template picker, which is // fine if there are no more visible boards to switch to. - // find the first visible board - let visibleBoardID: string | null = null + const validBoardIDs = new Set(allBoards.filter((b) => !b.deleteAt).map((b) => b.id)) + let nextValidBoardID: string | null = null for (const iterCategory of props.allCategories) { const visibleBoardMetadata = iterCategory.boardMetadata.find((categoryBoardMetadata) => !categoryBoardMetadata.hidden && categoryBoardMetadata.boardID !== props.board.id) - if (visibleBoardMetadata) { - visibleBoardID = visibleBoardMetadata.boardID + if (!visibleBoardMetadata) { + continue + } + if (validBoardIDs.has(visibleBoardMetadata.boardID)) { + nextValidBoardID = visibleBoardMetadata.boardID break } } - if (visibleBoardID === null) { + if (nextValidBoardID === null) { UserSettings.setLastBoardID(match.params.teamId!, null) showTemplatePicker() } else { - props.showBoard(visibleBoardID) + props.showBoard(nextValidBoardID) } } } diff --git a/webapp/src/pages/boardPage/boardPage.tsx b/webapp/src/pages/boardPage/boardPage.tsx index 9960e32b3..ba2c8b17c 100644 --- a/webapp/src/pages/boardPage/boardPage.tsx +++ b/webapp/src/pages/boardPage/boardPage.tsx @@ -53,7 +53,7 @@ import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../teleme import {Constants} from '../../constants' -import {getCategoryOfBoard, getHiddenBoardIDs} from '../../store/sidebar' +import {getCategoryOfBoard, getHiddenBoardIDs, removeBoardsFromAllCategories} from '../../store/sidebar' import SetWindowTitleAndIcon from './setWindowTitleAndIcon' import TeamToBoardAndViewRedirect from './teamToBoardAndViewRedirect' @@ -139,6 +139,12 @@ const BoardPage = (props: Props): JSX.Element => { boardId: activeBoardId, })) } + + // remove boards from all categories if they are deleted + const deletedBoardIds = teamBoards.filter((b: Board) => b.deleteAt && b.deleteAt !== 0).map((b) => b.id) + if (deletedBoardIds.length > 0) { + dispatch(removeBoardsFromAllCategories(deletedBoardIds)) + } } const incrementalBoardMemberUpdate = (_: WSClient, members: BoardMember[]) => { diff --git a/webapp/src/router.tsx b/webapp/src/router.tsx index 2a0dae434..d2fa6161f 100644 --- a/webapp/src/router.tsx +++ b/webapp/src/router.tsx @@ -22,6 +22,8 @@ import octoClient from './octoClient' import {setGlobalError, getGlobalError} from './store/globalError' import {useAppSelector, useAppDispatch} from './store/hooks' import {getFirstTeam, fetchTeams, Team} from './store/teams' +import {getSidebarCategories, CategoryBoards} from './store/sidebar' +import {getMySortedBoards} from './store/boards' import {UserSettings} from './userSettings' import FBRoute from './route' @@ -51,6 +53,31 @@ function HomeToCurrentTeam(props: {path: string, exact: boolean}) { const lastBoardID = UserSettings.lastBoardId[teamID] const lastViewID = UserSettings.lastViewId[lastBoardID] + if (lastBoardID) { + const categories = useAppSelector(getSidebarCategories) + const myBoards = useAppSelector(getMySortedBoards) + const validBoardIds = new Set(myBoards.filter((b) => !b.deleteAt).map((b) => b.id)) + + if (!validBoardIds.has(lastBoardID)) { + let fallbackBoardId: string | null = null + for (const category of categories) { + const visible = category.boardMetadata.find((m) => !m.hidden && validBoardIds.has(m.boardID)) + if (visible) { + fallbackBoardId = visible.boardID + break + } + } + + if (fallbackBoardId) { + UserSettings.setLastBoardID(teamID, fallbackBoardId) + return + } + + UserSettings.setLastBoardID(teamID, null) + return + } + } + if (lastBoardID && lastViewID) { return } diff --git a/webapp/src/store/sidebar.ts b/webapp/src/store/sidebar.ts index 306d3bd54..22e068231 100644 --- a/webapp/src/store/sidebar.ts +++ b/webapp/src/store/sidebar.ts @@ -175,6 +175,28 @@ const sidebarSlice = createSlice({ // creating a new reference of array so redux knows it changed state.categoryAttributes = state.categoryAttributes.map((original, i) => (i === categoryIndex ? updatedCategory : original)) }, + removeBoardsFromAllCategories: (state, action: PayloadAction) => { + if (!action.payload || action.payload.length === 0) { + return + } + + const toRemove = new Set(action.payload) + state.categoryAttributes = state.categoryAttributes.map((categoryBoards: CategoryBoards) => { + const filtered = categoryBoards.boardMetadata.filter((m) => !toRemove.has(m.boardID)) + return { + ...categoryBoards, + boardMetadata: filtered, + } + }) + + // Recompute hiddenBoardIDs after pruning + state.hiddenBoardIDs = state.categoryAttributes.flatMap((ca) => ca.boardMetadata.reduce((collector, m) => { + if (m.hidden) { + collector.push(m.boardID) + } + return collector + }, [] as string[])) + }, }, extraReducers: (builder) => { builder.addCase(fetchSidebarCategories.fulfilled, (state, action) => { @@ -210,7 +232,7 @@ export function getCategoryOfBoard(boardID: string): (state: RootState) => Categ export const {reducer} = sidebarSlice -export const {updateCategories, updateBoardCategories, updateCategoryOrder, updateCategoryBoardsOrder} = sidebarSlice.actions +export const {updateCategories, updateBoardCategories, updateCategoryOrder, updateCategoryBoardsOrder, removeBoardsFromAllCategories} = sidebarSlice.actions export {Category, CategoryBoards, BoardCategoryWebsocketData, CategoryBoardsReorderData, CategoryBoardMetadata} From a1b8d16dda89db2361b5202800d49371c6ee2504 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Tue, 23 Sep 2025 14:30:53 +0530 Subject: [PATCH 03/16] Minor --- webapp/src/components/sidebar/sidebar.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index e41cbc1b0..f60796c20 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -44,8 +44,6 @@ import octoClient from '../../octoClient' import {useWebsockets} from '../../hooks/websockets' -// import mutator from '../../mutator' - import {Board} from '../../blocks/board' import SidebarCategory from './sidebarCategory' From 4d16e4ecd84635ca96394ac7b0b815d863b50544 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Fri, 26 Sep 2025 12:56:48 +0530 Subject: [PATCH 04/16] review comments --- webapp/src/components/sidebar/sidebar.tsx | 26 +++++++++-------------- webapp/src/store/sidebar.ts | 9 +++----- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index f60796c20..0cb2b05de 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -226,15 +226,10 @@ const Sidebar = (props: Props) => { dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: categoryBoardMetadata})) - try { - const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) - const updatedOrder = await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) - if (reorderedBoardIDs.length > 0 && updatedOrder.length === 0) { - throw new Error('reorderSidebarCategoryBoards failed') - } - } catch (err) { - Utils.logError(`Failed to persist boards reorder for category ${toCategoryID}: ${err}`) - dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) + const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) + const updatedOrder = await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) + if (reorderedBoardIDs.length > 0 && updatedOrder.length === 0) { + dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) } } else { // board moved to a different category @@ -252,13 +247,12 @@ const Sidebar = (props: Props) => { await dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: categoryBoardMetadata})) dispatch(updateBoardCategories([{...fromCategoryBoardMetadata, categoryID: toCategoryID}])) - try { - const moveResp = await octoClient.moveBoardToCategory(team.id, boardID, toCategoryID, fromCategoryID) - if (!moveResp || !moveResp.ok) { - throw new Error('moveBoardToCategory failed') - } - } catch (err) { - Utils.logError(`Failed to move board ${boardID} from ${fromCategoryID} to ${toCategoryID}: ${err}`) + // Persist the move; if request fails or server rejects, rollback silently + const moveResp = await octoClient + .moveBoardToCategory(team.id, boardID, toCategoryID, fromCategoryID) + .catch(() => undefined) + + if (!moveResp || !moveResp.ok) { dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) dispatch(updateBoardCategories([{...fromCategoryBoardMetadata, categoryID: fromCategoryID}])) return diff --git a/webapp/src/store/sidebar.ts b/webapp/src/store/sidebar.ts index 22e068231..d8f9b781c 100644 --- a/webapp/src/store/sidebar.ts +++ b/webapp/src/store/sidebar.ts @@ -190,12 +190,9 @@ const sidebarSlice = createSlice({ }) // Recompute hiddenBoardIDs after pruning - state.hiddenBoardIDs = state.categoryAttributes.flatMap((ca) => ca.boardMetadata.reduce((collector, m) => { - if (m.hidden) { - collector.push(m.boardID) - } - return collector - }, [] as string[])) + state.hiddenBoardIDs = state.categoryAttributes.flatMap((ca) => + ca.boardMetadata.filter((m) => m.hidden).map((m) => m.boardID), + ) }, }, extraReducers: (builder) => { From 6ad75ce1ea2ba79b08d871a41294e29fe8d9dd05 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Sun, 28 Sep 2025 16:58:32 +0530 Subject: [PATCH 05/16] Minor --- webapp/src/components/sidebar/sidebar.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index 0cb2b05de..073e3b8e6 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -258,14 +258,11 @@ const Sidebar = (props: Props) => { return } - try { - const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) - const updatedOrder = await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) - if (reorderedBoardIDs.length > 0 && updatedOrder.length === 0) { - throw new Error('reorderSidebarCategoryBoards failed after move') - } - } catch (err) { - Utils.logError(`Failed to persist boards reorder for destination category ${toCategoryID}: ${err}`) + const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) + const updatedOrder = await octoClient + .reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) + .catch(() => []) + if (reorderedBoardIDs.length > 0 && updatedOrder.length === 0) { dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) } } From c1642499194abf214fc002f6332bcd5010690233 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Sun, 28 Sep 2025 17:00:15 +0530 Subject: [PATCH 06/16] Minor --- webapp/src/components/sidebar/sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index 073e3b8e6..cdd80c723 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -261,7 +261,6 @@ const Sidebar = (props: Props) => { const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) const updatedOrder = await octoClient .reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) - .catch(() => []) if (reorderedBoardIDs.length > 0 && updatedOrder.length === 0) { dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) } From efaf17018107e1f109ce1f4ed96fd03217f8e163 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Tue, 30 Sep 2025 16:46:42 +0530 Subject: [PATCH 07/16] Added logs and comments --- webapp/src/components/sidebar/sidebar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index cdd80c723..ef6260d31 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -228,6 +228,7 @@ const Sidebar = (props: Props) => { const reorderedBoardIDs = categoryBoardMetadata.map((m) => m.boardID) const updatedOrder = await octoClient.reorderSidebarCategoryBoards(team.id, toCategoryID, reorderedBoardIDs) + // if the request failed, rollback the pre updated state if (reorderedBoardIDs.length > 0 && updatedOrder.length === 0) { dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) } @@ -250,7 +251,7 @@ const Sidebar = (props: Props) => { // Persist the move; if request fails or server rejects, rollback silently const moveResp = await octoClient .moveBoardToCategory(team.id, boardID, toCategoryID, fromCategoryID) - .catch(() => undefined) + .catch(() => Utils.logError('Failed to move board to category')) if (!moveResp || !moveResp.ok) { dispatch(updateCategoryBoardsOrder({categoryID: toCategoryID, boardsMetadata: previousToBoardsMetadata})) From 208a4875bda7bb091e69d2e0752c07fba88592fe Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Wed, 1 Oct 2025 12:49:39 +0530 Subject: [PATCH 08/16] Board deleted undo in same category --- webapp/src/components/sidebar/sidebarCategory.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/sidebar/sidebarCategory.tsx b/webapp/src/components/sidebar/sidebarCategory.tsx index 466fd77be..0c4c7f50b 100644 --- a/webapp/src/components/sidebar/sidebarCategory.tsx +++ b/webapp/src/components/sidebar/sidebarCategory.tsx @@ -179,6 +179,10 @@ const SidebarCategory = (props: Props) => { return } telemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteBoard, {board: deleteBoard.id}) + + // Capture the category ID before deletion + const deletedFromCategoryID = props.categoryBoards.id + mutator.deleteBoard( deleteBoard, intl.formatMessage({id: 'Sidebar.delete-board', defaultMessage: 'Delete board'}), @@ -197,10 +201,12 @@ const SidebarCategory = (props: Props) => { } }, async () => { + // Restore the board to the category it was deleted from + await mutator.moveBoardToCategory(teamID, deleteBoard.id, deletedFromCategoryID, '') showBoard(deleteBoard.id) }, ) - }, [showBoard, deleteBoard, props.boards]) + }, [showBoard, deleteBoard, props.boards, props.categoryBoards.id, teamID]) const updateCategory = useCallback(async (value: boolean) => { const updatedCategory: Category = { From d931c0443cec5cb7dfd700e07efb7398545cea7c Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Mon, 6 Oct 2025 10:45:51 +0530 Subject: [PATCH 09/16] Fixed the broken SQL query for recovering blocks --- server/services/store/sqlstore/blocks.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/services/store/sqlstore/blocks.go b/server/services/store/sqlstore/blocks.go index e541d4b54..b696d9474 100644 --- a/server/services/store/sqlstore/blocks.go +++ b/server/services/store/sqlstore/blocks.go @@ -1038,6 +1038,8 @@ func (s *SQLStore) undeleteBlockChildren(db sq.BaseRunner, boardID string, paren return fmt.Errorf("undeleteBlockChildren unable to generate subquery: %w", err) } + joinArgs := append([]interface{}{modifiedBy}, subQueryArgs...) + selectQuery := s.getQueryBuilder(db). Select( "bh.board_id", @@ -1055,7 +1057,7 @@ func (s *SQLStore) undeleteBlockChildren(db sq.BaseRunner, boardID string, paren "bh.created_by", ). From(s.tablePrefix+"blocks_history AS bh"). - InnerJoin("("+subQuerySQL+") AS sub ON bh.id=sub.id AND bh.insert_at=sub.max_insert_at", append(subQueryArgs, modifiedBy)...). + InnerJoin("("+subQuerySQL+") AS sub ON bh.id=sub.id AND bh.insert_at=sub.max_insert_at", joinArgs...). Where(sq.NotEq{"bh.delete_at": 0}) columns := []string{ From 7c7ca26993ca8cac5ebfd6ae8ef95c6c3b6b2172 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 9 Oct 2025 17:39:21 +0530 Subject: [PATCH 10/16] Fix the continue redirect on itself issue for boards --- .../components/sidebar/sidebarCategory.tsx | 5 + webapp/src/error_boundary.tsx | 2 +- .../boardPage/teamToBoardAndViewRedirect.tsx | 104 +++++++++++++----- webapp/src/router.tsx | 2 +- webapp/src/utils.ts | 8 +- 5 files changed, 89 insertions(+), 32 deletions(-) diff --git a/webapp/src/components/sidebar/sidebarCategory.tsx b/webapp/src/components/sidebar/sidebarCategory.tsx index 0c4c7f50b..43c35c72f 100644 --- a/webapp/src/components/sidebar/sidebarCategory.tsx +++ b/webapp/src/components/sidebar/sidebarCategory.tsx @@ -39,6 +39,7 @@ import {TOUR_SIDEBAR, SidebarTourSteps, TOUR_BOARD, FINISHED} from '../../compon import telemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient' import {getCurrentTeam} from '../../store/teams' +import {UserSettings} from '../../userSettings' import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox' @@ -183,6 +184,10 @@ const SidebarCategory = (props: Props) => { // Capture the category ID before deletion const deletedFromCategoryID = props.categoryBoards.id + // Clear localStorage entries for the deleted board + UserSettings.setLastBoardID(teamID, null) + UserSettings.setLastViewId(deleteBoard.id, null) + mutator.deleteBoard( deleteBoard, intl.formatMessage({id: 'Sidebar.delete-board', defaultMessage: 'Delete board'}), diff --git a/webapp/src/error_boundary.tsx b/webapp/src/error_boundary.tsx index f10dfc7e2..13fc61b7d 100644 --- a/webapp/src/error_boundary.tsx +++ b/webapp/src/error_boundary.tsx @@ -19,7 +19,7 @@ export default class ErrorBoundary extends React.Component { msg = 'Redirecting to error page...' handleError = (): void => { - const url = Utils.getBaseURL() + '/error?id=unknown' + const url = Utils.getFrontendBaseURL(true) + '/error?id=unknown' Utils.log('error boundary redirecting to ' + url) window.location.replace(url) } diff --git a/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx b/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx index 5f9e5203f..34a2a6100 100644 --- a/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx +++ b/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx @@ -4,13 +4,15 @@ import {useEffect} from 'react' import {generatePath, useHistory, useRouteMatch} from 'react-router-dom' -import {getBoards, getCurrentBoardId} from '../../store/boards' +import {getBoards, getCurrentBoardId, setCurrent as setCurrentBoard} from '../../store/boards' import {setCurrent as setCurrentView, getCurrentBoardViews} from '../../store/views' import {useAppSelector, useAppDispatch} from '../../store/hooks' import {UserSettings} from '../../userSettings' import {Utils} from '../../utils' -import {getSidebarCategories} from '../../store/sidebar' +import {getSidebarCategories, fetchSidebarCategories} from '../../store/sidebar' import {Constants} from '../../constants' +import {getCurrentTeam} from '../../store/teams' +import {loadBoardData} from '../../store/initialLoad' const TeamToBoardAndViewRedirect = (): null => { const boardId = useAppSelector(getCurrentBoardId) @@ -20,11 +22,30 @@ const TeamToBoardAndViewRedirect = (): null => { const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string, teamId?: string}>() const categories = useAppSelector(getSidebarCategories) const boards = useAppSelector(getBoards) + const team = useAppSelector(getCurrentTeam) const teamId = match.params.teamId || UserSettings.lastTeamId || Constants.globalTeamId useEffect(() => { + // Check if we're showing template selector (avoid redirect loop) + const urlParams = new URLSearchParams(window.location.search) + if (urlParams.get('template') === 'true') { + return + } + + // Load categories if team is available but categories are empty + if (team && categories.length === 0) { + dispatch(fetchSidebarCategories(team.id)) + return + } + + // Wait for team and categories to be loaded + if (!team || categories.length === 0) { + return + } + let boardID = match.params.boardId - if (!match.params.boardId) { + + if (!boardID) { // first preference is for last visited board boardID = UserSettings.lastBoardId[teamId] @@ -49,39 +70,64 @@ const TeamToBoardAndViewRedirect = (): null => { } if (boardID) { - const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewID: undefined}) - history.replace(newPath) + // Search for view when board is found + let viewID = match.params.viewId - // return from here because the loadBoardData() call - // will fetch the data to be used below. We'll - // use it in the next render cycle. - return - } - } + // Load board data if boardViews is empty + if (!boardViews || boardViews.length === 0) { + console.log("Loading board data for board:", boardID) + // Set current board first so getCurrentBoardViews can work + dispatch(setCurrentBoard(boardID)) + dispatch(loadBoardData(boardID)) + return + } - let viewID = match.params.viewId + // when a view isn't open, + // but the data is available, try opening a view + if ((!viewID || viewID === '0') && boardViews && boardViews.length > 0) { + // most recent view gets the first preference + viewID = UserSettings.lastViewId[boardID] + + if (viewID) { + UserSettings.setLastViewId(boardID, viewID) + dispatch(setCurrentView(viewID)) + } else if (boardViews.length > 0) { + // if most recent view is unavailable, pick the first view + viewID = boardViews[0].id + UserSettings.setLastViewId(boardID, viewID) + dispatch(setCurrentView(viewID)) + } - // when a view isn't open, - // but the data is available, try opening a view - if ((!viewID || viewID === '0') && boardId && boardId === match.params.boardId && boardViews && boardViews.length > 0) { - // most recent view gets the first preference - viewID = UserSettings.lastViewId[boardID] - if (viewID) { - UserSettings.setLastViewId(boardID, viewID) - dispatch(setCurrentView(viewID)) - } else if (boardViews.length > 0) { - // if most recent view is unavailable, pick the first view - viewID = boardViews[0].id - UserSettings.setLastViewId(boardID, viewID) - dispatch(setCurrentView(viewID)) - } + if (viewID) { + const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewId: viewID}) + history.replace(newPath) + return + } + } + + // If no viewID in localStorage and no view found above, load the first view + if ((!viewID || viewID === '0') && boardViews && boardViews.length > 0) { + viewID = boardViews[0].id + UserSettings.setLastViewId(boardID, viewID) + dispatch(setCurrentView(viewID)) + + const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewId: viewID}) + history.replace(newPath) + return + } - if (viewID) { - const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, viewId: viewID}) + // If no view found or view logic didn't redirect, redirect to board without view + const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewID: undefined}) + history.replace(newPath) + return + } else { + // No boardID found, redirect to template selector + const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: undefined, viewID: undefined}) + '?template=true' history.replace(newPath) + return } } - }, [teamId, match.params.boardId, match.params.viewId, categories.length, boardViews.length, boardId]) + }, [teamId, match.params.boardId, match.params.viewId, categories.length, boardViews.length, boardId, team]) return null } diff --git a/webapp/src/router.tsx b/webapp/src/router.tsx index d2fa6161f..dd391ef7f 100644 --- a/webapp/src/router.tsx +++ b/webapp/src/router.tsx @@ -51,7 +51,7 @@ function HomeToCurrentTeam(props: {path: string, exact: boolean}) { if (UserSettings.lastBoardId) { const lastBoardID = UserSettings.lastBoardId[teamID] - const lastViewID = UserSettings.lastViewId[lastBoardID] + const lastViewID = lastBoardID ? UserSettings.lastViewId[lastBoardID] : undefined if (lastBoardID) { const categories = useAppSelector(getSidebarCategories) diff --git a/webapp/src/utils.ts b/webapp/src/utils.ts index ae590fccc..1912997be 100644 --- a/webapp/src/utils.ts +++ b/webapp/src/utils.ts @@ -20,6 +20,7 @@ import {IAppWindow} from './types' import {ChangeHandlerType, WSMessage} from './wsclient' import {BoardCategoryWebsocketData, Category} from './store/sidebar' import {UserSettings} from './userSettings' +import {Constants} from './constants' declare let window: IAppWindow @@ -575,7 +576,8 @@ class Utils { } static getFrontendBaseURL(absolute?: boolean): string { - let frontendBaseURL = window.frontendBaseURL || Utils.getBaseURL() + // Always use /boards as the frontend base URL, never fall back to baseURL + let frontendBaseURL = window.frontendBaseURL || '/boards' frontendBaseURL = frontendBaseURL.replace(/\/+$/, '') if (frontendBaseURL.indexOf('/') === 0) { frontendBaseURL = frontendBaseURL.slice(1) @@ -785,6 +787,10 @@ class Utils { match: routerMatch<{boardId: string, viewId?: string, cardId?: string, teamId?: string}>, history: History, ) { + // Set the board as last viewed board in localStorage + const teamId = match.params.teamId || UserSettings.lastTeamId || Constants.globalTeamId + UserSettings.setLastBoardID(teamId, boardId) + // if the same board, reuse the match params // otherwise remove viewId and cardId, results in first view being selected const params = {...match.params, boardId: boardId || ''} From ed386d2f155bfd45a724bad0c0d84f580b8889a5 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 9 Oct 2025 19:07:54 +0530 Subject: [PATCH 11/16] Reverted the unnecessary code --- .../boardPage/teamToBoardAndViewRedirect.tsx | 104 +++++------------- 1 file changed, 29 insertions(+), 75 deletions(-) diff --git a/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx b/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx index 34a2a6100..5f9e5203f 100644 --- a/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx +++ b/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx @@ -4,15 +4,13 @@ import {useEffect} from 'react' import {generatePath, useHistory, useRouteMatch} from 'react-router-dom' -import {getBoards, getCurrentBoardId, setCurrent as setCurrentBoard} from '../../store/boards' +import {getBoards, getCurrentBoardId} from '../../store/boards' import {setCurrent as setCurrentView, getCurrentBoardViews} from '../../store/views' import {useAppSelector, useAppDispatch} from '../../store/hooks' import {UserSettings} from '../../userSettings' import {Utils} from '../../utils' -import {getSidebarCategories, fetchSidebarCategories} from '../../store/sidebar' +import {getSidebarCategories} from '../../store/sidebar' import {Constants} from '../../constants' -import {getCurrentTeam} from '../../store/teams' -import {loadBoardData} from '../../store/initialLoad' const TeamToBoardAndViewRedirect = (): null => { const boardId = useAppSelector(getCurrentBoardId) @@ -22,30 +20,11 @@ const TeamToBoardAndViewRedirect = (): null => { const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string, teamId?: string}>() const categories = useAppSelector(getSidebarCategories) const boards = useAppSelector(getBoards) - const team = useAppSelector(getCurrentTeam) const teamId = match.params.teamId || UserSettings.lastTeamId || Constants.globalTeamId useEffect(() => { - // Check if we're showing template selector (avoid redirect loop) - const urlParams = new URLSearchParams(window.location.search) - if (urlParams.get('template') === 'true') { - return - } - - // Load categories if team is available but categories are empty - if (team && categories.length === 0) { - dispatch(fetchSidebarCategories(team.id)) - return - } - - // Wait for team and categories to be loaded - if (!team || categories.length === 0) { - return - } - let boardID = match.params.boardId - - if (!boardID) { + if (!match.params.boardId) { // first preference is for last visited board boardID = UserSettings.lastBoardId[teamId] @@ -70,64 +49,39 @@ const TeamToBoardAndViewRedirect = (): null => { } if (boardID) { - // Search for view when board is found - let viewID = match.params.viewId - - // Load board data if boardViews is empty - if (!boardViews || boardViews.length === 0) { - console.log("Loading board data for board:", boardID) - // Set current board first so getCurrentBoardViews can work - dispatch(setCurrentBoard(boardID)) - dispatch(loadBoardData(boardID)) - return - } + const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewID: undefined}) + history.replace(newPath) - // when a view isn't open, - // but the data is available, try opening a view - if ((!viewID || viewID === '0') && boardViews && boardViews.length > 0) { - // most recent view gets the first preference - viewID = UserSettings.lastViewId[boardID] - - if (viewID) { - UserSettings.setLastViewId(boardID, viewID) - dispatch(setCurrentView(viewID)) - } else if (boardViews.length > 0) { - // if most recent view is unavailable, pick the first view - viewID = boardViews[0].id - UserSettings.setLastViewId(boardID, viewID) - dispatch(setCurrentView(viewID)) - } + // return from here because the loadBoardData() call + // will fetch the data to be used below. We'll + // use it in the next render cycle. + return + } + } - if (viewID) { - const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewId: viewID}) - history.replace(newPath) - return - } - } + let viewID = match.params.viewId - // If no viewID in localStorage and no view found above, load the first view - if ((!viewID || viewID === '0') && boardViews && boardViews.length > 0) { - viewID = boardViews[0].id - UserSettings.setLastViewId(boardID, viewID) - dispatch(setCurrentView(viewID)) - - const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewId: viewID}) - history.replace(newPath) - return - } + // when a view isn't open, + // but the data is available, try opening a view + if ((!viewID || viewID === '0') && boardId && boardId === match.params.boardId && boardViews && boardViews.length > 0) { + // most recent view gets the first preference + viewID = UserSettings.lastViewId[boardID] + if (viewID) { + UserSettings.setLastViewId(boardID, viewID) + dispatch(setCurrentView(viewID)) + } else if (boardViews.length > 0) { + // if most recent view is unavailable, pick the first view + viewID = boardViews[0].id + UserSettings.setLastViewId(boardID, viewID) + dispatch(setCurrentView(viewID)) + } - // If no view found or view logic didn't redirect, redirect to board without view - const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewID: undefined}) - history.replace(newPath) - return - } else { - // No boardID found, redirect to template selector - const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: undefined, viewID: undefined}) + '?template=true' + if (viewID) { + const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, viewId: viewID}) history.replace(newPath) - return } } - }, [teamId, match.params.boardId, match.params.viewId, categories.length, boardViews.length, boardId, team]) + }, [teamId, match.params.boardId, match.params.viewId, categories.length, boardViews.length, boardId]) return null } From e37580aec0e215767dba378abb0345296220ea80 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 9 Oct 2025 19:54:14 +0530 Subject: [PATCH 12/16] revert --- webapp/src/router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/router.tsx b/webapp/src/router.tsx index dd391ef7f..d2fa6161f 100644 --- a/webapp/src/router.tsx +++ b/webapp/src/router.tsx @@ -51,7 +51,7 @@ function HomeToCurrentTeam(props: {path: string, exact: boolean}) { if (UserSettings.lastBoardId) { const lastBoardID = UserSettings.lastBoardId[teamID] - const lastViewID = lastBoardID ? UserSettings.lastViewId[lastBoardID] : undefined + const lastViewID = UserSettings.lastViewId[lastBoardID] if (lastBoardID) { const categories = useAppSelector(getSidebarCategories) From 8f4875798b510514cbbb0670a5d549f0e895ccde Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 16 Oct 2025 17:20:33 +0530 Subject: [PATCH 13/16] changing ids in url leads to error page --- webapp/src/errors.ts | 9 ++++ webapp/src/pages/boardPage/boardPage.tsx | 62 +++++++++++++++++++++--- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/webapp/src/errors.ts b/webapp/src/errors.ts index c867eccb8..16b5b4f28 100644 --- a/webapp/src/errors.ts +++ b/webapp/src/errors.ts @@ -11,6 +11,7 @@ enum ErrorId { NotLoggedIn = 'not-logged-in', InvalidReadOnlyBoard = 'invalid-read-only-board', BoardNotFound = 'board-not-found', + ViewNotFound = 'view-not-found', } type ErrorDef = { @@ -66,6 +67,14 @@ function errorDefFromId(id: ErrorId | null): ErrorDef { errDef.button1Fill = true break } + case ErrorId.ViewNotFound: { + errDef.title = intl.formatMessage({id: 'error.view-not-found', defaultMessage: 'View not found.'}) + errDef.button1Enabled = true + errDef.button1Text = intl.formatMessage({id: 'error.back-to-board', defaultMessage: 'Back to board'}) + errDef.button1Redirect = '/' + errDef.button1Fill = true + break + } case ErrorId.NotLoggedIn: { errDef.title = intl.formatMessage({id: 'error.not-logged-in', defaultMessage: 'Your session may have expired or you\'re not logged in. Log in again to access Boards.'}) errDef.button1Enabled = true diff --git a/webapp/src/pages/boardPage/boardPage.tsx b/webapp/src/pages/boardPage/boardPage.tsx index ba2c8b17c..8c47b2481 100644 --- a/webapp/src/pages/boardPage/boardPage.tsx +++ b/webapp/src/pages/boardPage/boardPage.tsx @@ -27,12 +27,13 @@ import { setCurrent as setCurrentBoard, fetchBoardMembers, addMyBoardMemberships, + getBoards, } from '../../store/boards' -import {getCurrentViewId, setCurrent as setCurrentView, updateViews} from '../../store/views' +import {getCurrentViewId, getCurrentBoardViews, setCurrent as setCurrentView, updateViews} from '../../store/views' import ConfirmationDialog from '../../components/confirmationDialogBox' import {initialLoad, initialReadOnlyLoad, loadBoardData} from '../../store/initialLoad' import {useAppSelector, useAppDispatch} from '../../store/hooks' -import {setTeam} from '../../store/teams' +import {getAllTeams, setTeam} from '../../store/teams' import {updateCards} from '../../store/cards' import {updateComments} from '../../store/comments' import {updateAttachments} from '../../store/attachments' @@ -82,6 +83,9 @@ const BoardPage = (props: Props): JSX.Element => { const category = useAppSelector(getCategoryOfBoard(activeBoardId)) const [showJoinBoardDialog, setShowJoinBoardDialog] = useState(false) const history = useHistory() + const allTeams = useAppSelector(getAllTeams) + const allBoards = useAppSelector(getBoards) + const boardViews = useAppSelector(getCurrentBoardViews) // if we're in a legacy route and not showing a shared board, // redirect to the new URL schema equivalent @@ -102,10 +106,18 @@ const BoardPage = (props: Props): JSX.Element => { // TODO: Make this less brittle. This only works because this is the root render function useEffect(() => { - UserSettings.lastTeamId = teamId - octoClient.teamId = teamId - dispatch(setTeam(teamId)) - }, [teamId]) + if (allTeams.length === 0) { + return + } + const isValidTeam = allTeams.some((team) => team.id === match.params.teamId) || match.params.teamId === Constants.globalTeamId + if (isValidTeam) { + UserSettings.lastTeamId = teamId + octoClient.teamId = teamId + dispatch(setTeam(teamId)) + } else { + dispatch(setGlobalError('team-undefined')) + } + }, [teamId, allTeams]) const loadAction: (boardId: string) => any = useMemo(() => { if (props.readonly) { @@ -194,6 +206,21 @@ const BoardPage = (props: Props): JSX.Element => { } const joinBoard = async (myUser: IUser, boardTeamId: string, boardId: string, allowAdmin: boolean) => { + const boardExists = await octoClient.getBoard(boardId) + if (!boardExists) { + UserSettings.setLastBoardID(boardTeamId, null) + UserSettings.setLastViewId(boardId, null) + dispatch(setGlobalError('board-not-found')) + return + } + + if (boardExists.teamId !== boardTeamId && boardExists.teamId !== Constants.globalTeamId) { + UserSettings.setLastBoardID(boardTeamId, null) + UserSettings.setLastViewId(boardId, null) + dispatch(setGlobalError('board-not-found')) + return + } + const member = await octoClient.joinBoard(boardId, allowAdmin) if (!member) { if (myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) { @@ -247,10 +274,31 @@ const BoardPage = (props: Props): JSX.Element => { }, [teamId, match.params.boardId, viewId, me?.id]) useEffect(() => { + if (Object.keys(allBoards).length === 0) { + return + } if (match.params.boardId && !props.readonly && me) { + const board = allBoards[match.params.boardId] + if (board && board.teamId !== teamId && board.teamId !== Constants.globalTeamId) { + dispatch(setGlobalError('board-not-found')) + return + } loadOrJoinBoard(me, teamId, match.params.boardId) } - }, [teamId, match.params.boardId, me?.id]) + }, [teamId, match.params.boardId, me?.id, allBoards]) + + // Validate that the viewId exists in the board's views + useEffect(() => { + if (!match.params.viewId || props.readonly || props.new || !activeBoardId || boardViews.length === 0) { + return + } + + const viewExists = boardViews.some((view) => view.id === match.params.viewId) + if (!viewExists) { + UserSettings.setLastViewId(activeBoardId, null) + dispatch(setGlobalError('view-not-found')) + } + }, [match.params.viewId, activeBoardId, boardViews, props.readonly, props.new]) const handleUnhideBoard = async (boardID: string) => { if (!me || !category) { From 47d8958b7bd5cddd233c3765b938eb1d97982281 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 16 Oct 2025 18:11:45 +0530 Subject: [PATCH 14/16] when swtiching board fix no board error found --- webapp/src/pages/boardPage/boardPage.tsx | 58 ++++++----- .../boardPage/teamToBoardAndViewRedirect.tsx | 98 +++++++++++-------- 2 files changed, 92 insertions(+), 64 deletions(-) diff --git a/webapp/src/pages/boardPage/boardPage.tsx b/webapp/src/pages/boardPage/boardPage.tsx index 8c47b2481..5f4ffb257 100644 --- a/webapp/src/pages/boardPage/boardPage.tsx +++ b/webapp/src/pages/boardPage/boardPage.tsx @@ -86,6 +86,7 @@ const BoardPage = (props: Props): JSX.Element => { const allTeams = useAppSelector(getAllTeams) const allBoards = useAppSelector(getBoards) const boardViews = useAppSelector(getCurrentBoardViews) + const [loadedBoardId, setLoadedBoardId] = useState('') // if we're in a legacy route and not showing a shared board, // redirect to the new URL schema equivalent @@ -256,36 +257,49 @@ const BoardPage = (props: Props): JSX.Element => { }, []) useEffect(() => { - dispatch(loadAction(match.params.boardId)) + if (!match.params.boardId) { + return + } + dispatch(setCurrentBoard(match.params.boardId)) - if (match.params.boardId) { - // set the active board - dispatch(setCurrentBoard(match.params.boardId)) - - if (viewId !== Constants.globalTeamId) { - // reset current, even if empty string - dispatch(setCurrentView(viewId)) - if (viewId) { - // don't reset per board if empty string - UserSettings.setLastViewId(match.params.boardId, viewId) - } + if (viewId !== Constants.globalTeamId) { + dispatch(setCurrentView(viewId)) + if (viewId) { + UserSettings.setLastViewId(match.params.boardId, viewId) } } - }, [teamId, match.params.boardId, viewId, me?.id]) + + dispatch(loadAction(match.params.boardId)) + }, [teamId, match.params.boardId, viewId, me?.id, dispatch, loadAction]) + + + useEffect(() => { + setLoadedBoardId('') + }, [teamId]) useEffect(() => { - if (Object.keys(allBoards).length === 0) { + if (!match.params.boardId || !me || props.readonly) { return } - if (match.params.boardId && !props.readonly && me) { - const board = allBoards[match.params.boardId] - if (board && board.teamId !== teamId && board.teamId !== Constants.globalTeamId) { - dispatch(setGlobalError('board-not-found')) - return - } - loadOrJoinBoard(me, teamId, match.params.boardId) + + if (loadedBoardId === match.params.boardId) { + return + } + + const boardsLoaded = Object.keys(allBoards).length > 0 + if (!boardsLoaded) { + return } - }, [teamId, match.params.boardId, me?.id, allBoards]) + + const board = allBoards[match.params.boardId] + if (board && board.teamId !== teamId && board.teamId !== Constants.globalTeamId) { + dispatch(setGlobalError('board-not-found')) + return + } + + setLoadedBoardId(match.params.boardId) + loadOrJoinBoard(me, teamId, match.params.boardId) + }, [teamId, match.params.boardId, me, props.readonly, allBoards, loadOrJoinBoard, dispatch, loadedBoardId]) // Validate that the viewId exists in the board's views useEffect(() => { diff --git a/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx b/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx index 5f9e5203f..6f3bcca4e 100644 --- a/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx +++ b/webapp/src/pages/boardPage/teamToBoardAndViewRedirect.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {useEffect} from 'react' +import {useEffect, useMemo} from 'react' import {generatePath, useHistory, useRouteMatch} from 'react-router-dom' import {getBoards, getCurrentBoardId} from '../../store/boards' @@ -22,66 +22,80 @@ const TeamToBoardAndViewRedirect = (): null => { const boards = useAppSelector(getBoards) const teamId = match.params.teamId || UserSettings.lastTeamId || Constants.globalTeamId + const boardCount = useMemo(() => Object.keys(boards).length, [boards]) + const categoryCount = useMemo(() => categories.length, [categories]) + useEffect(() => { - let boardID = match.params.boardId - if (!match.params.boardId) { - // first preference is for last visited board - boardID = UserSettings.lastBoardId[teamId] - - // if last visited board is unavailable, use the first board in categories list - if (!boardID && categories.length > 0) { - let goToBoardID: string | null = null - - for (const category of categories) { - for (const boardMetadata of category.boardMetadata) { - // pick the first category board that exists and is not hidden - if (!boardMetadata.hidden && boards[boardMetadata.boardID]) { - goToBoardID = boardMetadata.boardID - break - } + if (match.params.boardId) { + return + } + + if (boardCount === 0 && categoryCount === 0) { + return + } + + let boardID: string | undefined = undefined + + const lastBoardId = UserSettings.lastBoardId[teamId] + if (lastBoardId) { + const board = boards[lastBoardId] + if (board && (board.teamId === teamId || board.teamId === Constants.globalTeamId)) { + boardID = lastBoardId + } else { + UserSettings.setLastBoardID(teamId, null) + } + } + + if (!boardID && categoryCount > 0) { + for (const category of categories) { + for (const boardMetadata of category.boardMetadata) { + const board = boards[boardMetadata.boardID] + // Pick the first category board that exists, is not hidden, and belongs to this team + if (!boardMetadata.hidden && board && (board.teamId === teamId || board.teamId === Constants.globalTeamId)) { + boardID = boardMetadata.boardID + break } } - - // there may even be no boards at all - if (goToBoardID) { - boardID = goToBoardID + if (boardID) { + break } } + } - if (boardID) { - const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewID: undefined}) - history.replace(newPath) - - // return from here because the loadBoardData() call - // will fetch the data to be used below. We'll - // use it in the next render cycle. - return - } + if (boardID) { + const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, boardId: boardID, viewID: undefined}) + history.replace(newPath) + return } - let viewID = match.params.viewId + }, [teamId, match.params.boardId, boardCount, categoryCount, boards, categories, history, match.path, match.params]) + + const viewCount = useMemo(() => boardViews.length, [boardViews]) + + useEffect(() => { + const viewID = match.params.viewId // when a view isn't open, // but the data is available, try opening a view - if ((!viewID || viewID === '0') && boardId && boardId === match.params.boardId && boardViews && boardViews.length > 0) { + if ((!viewID || viewID === '0') && boardId && boardId === match.params.boardId && viewCount > 0) { // most recent view gets the first preference - viewID = UserSettings.lastViewId[boardID] - if (viewID) { - UserSettings.setLastViewId(boardID, viewID) - dispatch(setCurrentView(viewID)) + let selectedViewID = UserSettings.lastViewId[boardId] + if (selectedViewID) { + UserSettings.setLastViewId(boardId, selectedViewID) + dispatch(setCurrentView(selectedViewID)) } else if (boardViews.length > 0) { // if most recent view is unavailable, pick the first view - viewID = boardViews[0].id - UserSettings.setLastViewId(boardID, viewID) - dispatch(setCurrentView(viewID)) + selectedViewID = boardViews[0].id + UserSettings.setLastViewId(boardId, selectedViewID) + dispatch(setCurrentView(selectedViewID)) } - if (viewID) { - const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, viewId: viewID}) + if (selectedViewID) { + const newPath = generatePath(Utils.getBoardPagePath(match.path), {...match.params, viewId: selectedViewID}) history.replace(newPath) } } - }, [teamId, match.params.boardId, match.params.viewId, categories.length, boardViews.length, boardId]) + }, [match.params.boardId, match.params.viewId, viewCount, boardId, boardViews, dispatch, history, match.path, match.params]) return null } From e46cad3d9fdadd749cf568b942b6a6757a0cb983 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 16 Oct 2025 18:23:04 +0530 Subject: [PATCH 15/16] Fix the bot notification link --- server/boards/boardsapp_util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/boards/boardsapp_util.go b/server/boards/boardsapp_util.go index ba22451c4..6f9aab86f 100644 --- a/server/boards/boardsapp_util.go +++ b/server/boards/boardsapp_util.go @@ -80,7 +80,7 @@ func createBoardsConfig(mmconfig mm_model.Config, baseURL string, serverID strin showFullName = *mmconfig.PrivacySettings.ShowFullName } - serverRoot := baseURL + "/plugins/focalboard" + serverRoot := baseURL + "/boards" return &config.Configuration{ ServerRoot: serverRoot, From 3e0d08cb145360c5bb4ac5f8d05e479fd7d67fee Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 16 Oct 2025 18:29:45 +0530 Subject: [PATCH 16/16] Fix review comment --- webapp/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/utils.ts b/webapp/src/utils.ts index 1912997be..80da3923c 100644 --- a/webapp/src/utils.ts +++ b/webapp/src/utils.ts @@ -577,7 +577,7 @@ class Utils { static getFrontendBaseURL(absolute?: boolean): string { // Always use /boards as the frontend base URL, never fall back to baseURL - let frontendBaseURL = window.frontendBaseURL || '/boards' + let frontendBaseURL = window.frontendBaseURL ? window.frontendBaseURL : '/boards' frontendBaseURL = frontendBaseURL.replace(/\/+$/, '') if (frontendBaseURL.indexOf('/') === 0) { frontendBaseURL = frontendBaseURL.slice(1)