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.
This commit is contained in:
@@ -8,6 +8,7 @@ import { attr, fk, many } from 'redux-orm';
|
|||||||
import BaseModel from './BaseModel';
|
import BaseModel from './BaseModel';
|
||||||
import buildSearchParts from '../utils/build-search-parts';
|
import buildSearchParts from '../utils/build-search-parts';
|
||||||
import { isListKanban } from '../utils/record-helpers';
|
import { isListKanban } from '../utils/record-helpers';
|
||||||
|
import { recallBoardView } from '../utils/board-view-memory';
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
import Config from '../constants/Config';
|
import Config from '../constants/Config';
|
||||||
import { BoardContexts, BoardViews } from '../constants/Enums';
|
import { BoardContexts, BoardViews } from '../constants/Enums';
|
||||||
@@ -16,7 +17,10 @@ const prepareFetchedBoard = (board) => ({
|
|||||||
...board,
|
...board,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
context: BoardContexts.BOARD,
|
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: '',
|
search: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import selectors from '../../../selectors';
|
|||||||
import actions from '../../../actions';
|
import actions from '../../../actions';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import { createLocalId } from '../../../utils/local-id';
|
import { createLocalId } from '../../../utils/local-id';
|
||||||
|
import { rememberBoardView } from '../../../utils/board-view-memory';
|
||||||
import ActionTypes from '../../../constants/ActionTypes';
|
import ActionTypes from '../../../constants/ActionTypes';
|
||||||
import ModalTypes from '../../../constants/ModalTypes';
|
import ModalTypes from '../../../constants/ModalTypes';
|
||||||
|
|
||||||
@@ -195,6 +196,15 @@ export function* updateContextInCurrentBoard(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function* updateBoardView(id, 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(
|
yield put(
|
||||||
actions.updateBoard(id, {
|
actions.updateBoard(id, {
|
||||||
view: value,
|
view: value,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import actions from '../../../actions';
|
|||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import { removeAccessToken } from '../../../utils/access-token-storage';
|
import { removeAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
import { forgetBoardViews } from '../../../utils/board-view-memory';
|
||||||
|
|
||||||
export function* initializeCore() {
|
export function* initializeCore() {
|
||||||
const { item: bootstrap } = yield call(request, api.getBootstrap); // TODO: handle error
|
const { item: bootstrap } = yield call(request, api.getBootstrap); // TODO: handle error
|
||||||
@@ -117,6 +118,9 @@ export function* updateHomeView(value) {
|
|||||||
|
|
||||||
export function* logout(revokeAccessToken) {
|
export function* logout(revokeAccessToken) {
|
||||||
yield call(removeAccessToken);
|
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) {
|
if (revokeAccessToken) {
|
||||||
yield put(actions.logout.revokeAccessToken());
|
yield put(actions.logout.revokeAccessToken());
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user