From 20307222422407e9bd399994e143c9ac5af504f0 Mon Sep 17 00:00:00 2001 From: Daniel Hiller Date: Thu, 17 Sep 2026 00:42:11 +0200 Subject: [PATCH] feat: Remember which view a board was last put into Choosing a view and finding the board's default again after every reload is the board forgetting something the reader clearly meant. The choice lives in `sessionStorage`, not on the account. A view is how one person is looking at a board right now, not a property of the board: two windows may hold the same board open as a grid and as a list, and neither is wrong. A stored preference would make one of them change under the other's hands. The context is stored alongside and has to match on the way back out, so a view picked in the archive does not follow the board into its own context. A view this build no longer knows falls through to the board's default, as does anything stored while the browser refuses to keep it. --- client/src/models/Board.js | 6 ++- client/src/sagas/core/services/boards.js | 10 ++++ client/src/sagas/core/services/core.js | 4 ++ client/src/utils/board-view-memory.js | 68 ++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 client/src/utils/board-view-memory.js diff --git a/client/src/models/Board.js b/client/src/models/Board.js index a4697fe5..3877b519 100755 --- a/client/src/models/Board.js +++ b/client/src/models/Board.js @@ -8,6 +8,7 @@ import { attr, fk, many } from 'redux-orm'; import BaseModel from './BaseModel'; import buildSearchParts from '../utils/build-search-parts'; import { isListKanban } from '../utils/record-helpers'; +import { recallBoardView } from '../utils/board-view-memory'; import ActionTypes from '../constants/ActionTypes'; import Config from '../constants/Config'; import { BoardContexts, BoardViews } from '../constants/Enums'; @@ -16,7 +17,10 @@ const prepareFetchedBoard = (board) => ({ ...board, isFetching: false, context: BoardContexts.BOARD, - view: board.defaultView, + // Whatever this window was last looking at, where that choice still applies — + // see `utils/board-view-memory`. A board's configured default is where it + // starts, not where it has to stay. + view: recallBoardView(board.id, BoardContexts.BOARD) || board.defaultView, search: '', }); diff --git a/client/src/sagas/core/services/boards.js b/client/src/sagas/core/services/boards.js index 77a40534..3acfbdc9 100644 --- a/client/src/sagas/core/services/boards.js +++ b/client/src/sagas/core/services/boards.js @@ -12,6 +12,7 @@ import selectors from '../../../selectors'; import actions from '../../../actions'; import api from '../../../api'; import { createLocalId } from '../../../utils/local-id'; +import { rememberBoardView } from '../../../utils/board-view-memory'; import ActionTypes from '../../../constants/ActionTypes'; import ModalTypes from '../../../constants/ModalTypes'; @@ -195,6 +196,15 @@ export function* updateContextInCurrentBoard(value) { } export function* updateBoardView(id, value) { + const board = yield select(selectors.selectBoardById, id); + + // Written down so a reload comes back to it. Stored against the context it + // was chosen in, since that is what makes it meaningful — see + // `utils/board-view-memory`. + if (board) { + yield call(rememberBoardView, id, board.context, value); + } + yield put( actions.updateBoard(id, { view: value, diff --git a/client/src/sagas/core/services/core.js b/client/src/sagas/core/services/core.js index 7f2e7bb3..2f7f94e5 100644 --- a/client/src/sagas/core/services/core.js +++ b/client/src/sagas/core/services/core.js @@ -12,6 +12,7 @@ import actions from '../../../actions'; import api from '../../../api'; import i18n from '../../../i18n'; import { removeAccessToken } from '../../../utils/access-token-storage'; +import { forgetBoardViews } from '../../../utils/board-view-memory'; export function* initializeCore() { const { item: bootstrap } = yield call(request, api.getBootstrap); // TODO: handle error @@ -117,6 +118,9 @@ export function* updateHomeView(value) { export function* logout(revokeAccessToken) { yield call(removeAccessToken); + // Everything this window was holding about the boards it had open. None of it + // belongs to the next person to sign in here. + yield call(forgetBoardViews); if (revokeAccessToken) { yield put(actions.logout.revokeAccessToken()); diff --git a/client/src/utils/board-view-memory.js b/client/src/utils/board-view-memory.js new file mode 100644 index 00000000..0646f677 --- /dev/null +++ b/client/src/utils/board-view-memory.js @@ -0,0 +1,68 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +// Which view a board was last put into, so a reload comes back to it rather +// than to the board's configured default. Choosing kanban and finding list +// again after every refresh is the board forgetting something the reader +// clearly meant. +// +// `sessionStorage`, not `localStorage`. A view is how one person is looking at +// a board right now, not a property of the board or of the account: two windows +// may hold the same board open as a grid and as a list, and neither is wrong. +// `localStorage` would make one of them change under the other's hands. +// +// The context is stored with the choice and has to match on the way back out. +// A view belongs to the context it was picked in — list makes sense in the +// trash, kanban does not — and a reload always lands a board in its own +// context, so an archive choice must not follow it there. + +import { BoardViews } from '../constants/Enums'; + +const KEY = 'planka:boardView'; + +const VIEWS = new Set(Object.values(BoardViews)); + +const read = () => { + try { + const stored = window.sessionStorage.getItem(KEY); + const parsed = stored ? JSON.parse(stored) : null; + + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch { + return {}; + } +}; + +const write = (views) => { + try { + window.sessionStorage.setItem(KEY, JSON.stringify(views)); + } catch { + /* storage blocked or full — boards simply open in their default view */ + } +}; + +export const rememberBoardView = (boardId, context, view) => { + write({ ...read(), [boardId]: { view, context } }); +}; + +// Anything that does not survive both checks falls through to the board's own +// default: a view this build no longer has, or one picked in another context. +export const recallBoardView = (boardId, context) => { + const stored = read()[boardId]; + + if (!stored || stored.context !== context || !VIEWS.has(stored.view)) { + return null; + } + + return stored.view; +}; + +export const forgetBoardViews = () => { + try { + window.sessionStorage.removeItem(KEY); + } catch { + /* nothing to do — the store was unreachable to begin with */ + } +};