Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3f444a8
feat: implement unified view for all open boards
e-kotov Apr 9, 2026
12f22b0
feat: add board name indicators to cards in unified view
e-kotov Apr 9, 2026
bf36c3c
feat: make unified board view an optional tab
e-kotov Apr 9, 2026
79fba2c
fix: restore task movement functionality and handle unified state cor…
e-kotov Apr 9, 2026
bdeae06
feat: improve unified drag-and-drop and implement drag-to-tab
e-kotov Apr 9, 2026
3a5f920
fix: improve drag card to board tab detection
e-kotov Apr 9, 2026
c84d0a6
fix: restore tab switching and improve drag-to-tab reliability
e-kotov Apr 9, 2026
c6002cf
fix: improve unified reordering and drag-to-tab visual feedback
e-kotov Apr 9, 2026
fcd66c9
fix: reliable drag-to-tab detection and restore tab switching
e-kotov Apr 9, 2026
32c1537
fix: robust reordering and reliable drag-to-tab detection
e-kotov Apr 9, 2026
f00630e
fix: robust task movement between lists and boards
e-kotov Apr 9, 2026
136dcc3
fix: final robust drag-to-board and reordering implementation
e-kotov Apr 9, 2026
f24e200
fix: resolve race condition in drag-to-tab and support cross-device m…
e-kotov Apr 9, 2026
124c8a5
fix: definitive board-to-board dragging and reordering fixes
e-kotov Apr 9, 2026
a93b046
fix: hardened drag-and-drop and reliable tab drop detection
e-kotov Apr 9, 2026
59e6c43
fix: definitive drag-and-drop implementation with manual hit-testing
e-kotov Apr 9, 2026
4443039
docs: document Unified View and drag-and-drop architecture in readme.md
e-kotov Apr 9, 2026
d7715de
Fix unified board drag-and-drop duplicate list bug and stuck card UI …
e-kotov Apr 9, 2026
acb1ecf
feat: add visual styling for unopened unified board tab
e-kotov Apr 9, 2026
400eb65
Implement Bulk Board Scanner and improve All Boards tab styling
e-kotov Apr 9, 2026
bea9a83
Fix Unified View sync when closing board tabs
e-kotov Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions app/board/boardLabels.js
Original file line number Diff line number Diff line change
Expand Up @@ -1674,9 +1674,11 @@ async function applyThemeOverridesToOpenBoards() {
return;
}

const UNIFIED_BOARD_PATH = '__unified__';
const sourceOverrides = getBoardThemeOverrides();
const sourceBoard = window.boardRoot;
const openBoards = typeof getStoredOpenBoards === 'function' ? getStoredOpenBoards() : [sourceBoard];
const storedBoards = typeof getStoredOpenBoards === 'function' ? getStoredOpenBoards() : [sourceBoard];
const openBoards = storedBoards.filter(b => b !== UNIFIED_BOARD_PATH);

const targets = Array.isArray(openBoards) ? openBoards : [];
for (const boardPath of targets) {
Expand All @@ -1701,9 +1703,11 @@ async function applyNotificationSettingsToOpenBoards() {
return;
}

const UNIFIED_BOARD_PATH = '__unified__';
const sourceNotifications = getBoardNotificationSettings();
const sourceBoard = window.boardRoot;
const openBoards = typeof getStoredOpenBoards === 'function' ? getStoredOpenBoards() : [sourceBoard];
const storedBoards = typeof getStoredOpenBoards === 'function' ? getStoredOpenBoards() : [sourceBoard];
const openBoards = storedBoards.filter(b => b !== UNIFIED_BOARD_PATH);
const targets = Array.isArray(openBoards) ? openBoards : [];

for (const boardPath of targets) {
Expand Down Expand Up @@ -1886,10 +1890,11 @@ function scheduleBoardLabelSettingsSave() {

function persistBoardSettings() {
const state = getBoardLabelState();
const UNIFIED_BOARD_PATH = '__unified__';

state.settingsSaveInFlight = state.settingsSaveInFlight
.then(async () => {
if (!window.boardRoot) {
if (!window.boardRoot || window.boardRoot === UNIFIED_BOARD_PATH) {
return;
}

Expand Down Expand Up @@ -2322,6 +2327,7 @@ function resetBoardLabelFilter() {
}

async function ensureBoardLabelsLoaded() {
const UNIFIED_BOARD_PATH = '__unified__';
if (!window.boardRoot) {
const state = getBoardLabelState();
state.importSummaryBoardRoot = '';
Expand All @@ -2339,6 +2345,33 @@ async function ensureBoardLabelsLoaded() {
return;
}

if (window.boardRoot === UNIFIED_BOARD_PATH) {
const storedBoards = typeof getStoredOpenBoards === 'function' ? getStoredOpenBoards() : [];
const openBoards = storedBoards.filter(b => b !== UNIFIED_BOARD_PATH);
const allLabels = [];
const seenLabelIds = new Set();

for (const boardPath of openBoards) {
try {
const settings = await window.board.readBoardSettings(boardPath);
if (settings && Array.isArray(settings.labels)) {
for (const label of settings.labels) {
if (!seenLabelIds.has(label.id)) {
allLabels.push(label);
seenLabelIds.add(label.id);
}
}
}
} catch (e) {
console.warn(`Failed to load labels for ${boardPath}`, e);
}
}
setBoardLabels(allLabels);
// Use default theme for unified view for now
applyColorSchemeById('default', { renderControls: false });
return;
}

const settings = await window.board.readBoardSettings(window.boardRoot);
setBoardLabels(settings.labels || []);
const loadedSchemeId = settings.colorScheme || '';
Expand Down
117 changes: 112 additions & 5 deletions app/board/boardTabs.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
const OPEN_BOARDS_STORAGE_KEY = 'openBoardPaths';
const ACTIVE_BOARD_STORAGE_KEY = 'activeBoardPath';
const MAX_OPEN_BOARDS = 6;
const UNIFIED_BOARD_PATH = '__unified__';
let boardTabsSortable = null;

function normalizeBoardPath(dir) {
if (!dir || typeof dir !== 'string') {
return '';
}

if (dir === UNIFIED_BOARD_PATH) {
return UNIFIED_BOARD_PATH;
}

const normalizedDir = dir.replace(/\\/g, '/').trim();
if (!normalizedDir) {
return '';
Expand Down Expand Up @@ -38,6 +43,10 @@ async function authorizeBoardAccess(selection) {
return '';
}

if (normalizedPath === UNIFIED_BOARD_PATH) {
return UNIFIED_BOARD_PATH;
}

let result = null;
if (
selection &&
Expand Down Expand Up @@ -66,6 +75,9 @@ async function clearAuthorizedBoardAccess() {
}

function getBoardLabelFromPath(boardPath) {
if (boardPath === UNIFIED_BOARD_PATH) {
return 'All Boards';
}
const normalizedPath = normalizeBoardPath(boardPath).replace(/\/+$/, '');
const pathParts = normalizedPath.split('/').filter(Boolean);
return pathParts[pathParts.length - 1] || 'Board';
Expand Down Expand Up @@ -239,7 +251,7 @@ async function closeBoardTab(boardPath) {
return;
}

if (!closedActiveBoard) {
if (!closedActiveBoard && activeBoard !== UNIFIED_BOARD_PATH) {
renderBoardTabs();
return;
}
Expand Down Expand Up @@ -366,12 +378,18 @@ function renderBoardTabs() {
tabsWrapper.classList.remove('hidden');
const activeBoard = normalizeBoardPath(window.boardRoot || getStoredActiveBoard());

for (const boardPath of openBoards) {
const boardsToDisplay = [...openBoards];

for (const boardPath of boardsToDisplay) {
const boardTab = document.createElement('div');
boardTab.classList.add('board-tab');
boardTab.setAttribute('data-board-path', boardPath);
boardTab.setAttribute('role', 'presentation');
boardTab.title = boardPath;
boardTab.title = boardPath === UNIFIED_BOARD_PATH ? 'View all open boards' : boardPath;

if (boardPath === UNIFIED_BOARD_PATH) {
boardTab.classList.add('board-tab-unified');
}

const tabButton = document.createElement('button');
tabButton.type = 'button';
Expand Down Expand Up @@ -418,6 +436,15 @@ function renderBoardTabs() {
await renderBoard();
});

if (boardPath === UNIFIED_BOARD_PATH) {
const icon = document.createElement('span');
icon.className = 'board-tab-unified-icon';
icon.innerHTML = '<i data-feather="layers"></i>';
tabButton.prepend(icon);
}

boardTab.appendChild(tabButton);

const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.classList.add('board-tab-close');
Expand All @@ -429,12 +456,53 @@ function renderBoardTabs() {
event.stopPropagation();
await closeBoardTab(boardPath);
});

boardTab.appendChild(tabButton);
boardTab.appendChild(closeButton);
tabsEl.appendChild(boardTab);
}

// Global drag tracking for tabs
if (!window.__tabDragTrackerInitialized) {
window.__tabDragTrackerInitialized = true;
document.addEventListener('mousemove', (e) => {
if (document.body.classList.contains('board-card-drag-active')) {
const hit = document.elementFromPoint(e.clientX, e.clientY);
const boardTab = hit ? hit.closest('.board-tab[data-board-path]') : null;

// Clear highlights from other tabs
document.querySelectorAll('.board-tab--drop-target').forEach(el => {
if (el !== boardTab) el.classList.remove('board-tab--drop-target');
});

if (boardTab) {
const boardPath = boardTab.getAttribute('data-board-path');
const UNIFIED_BOARD_PATH = '__unified__';

// Don't highlight if it's the "All Boards" tab or if the card already belongs to this board
if (boardPath === UNIFIED_BOARD_PATH) {
window.__activeBoardDropTarget = null;
return;
}

if (typeof Sortable !== 'undefined' && Sortable.active && Sortable.active.options.group.name === 'cards') {
const draggedItem = Sortable.active.dragged;
const cardPath = draggedItem ? draggedItem.getAttribute('data-path') : null;
if (cardPath && cardPath.startsWith(boardPath)) {
window.__activeBoardDropTarget = null;
return;
}
}

window.__activeBoardDropTarget = boardPath;
boardTab.classList.add('board-tab--drop-target');
} else {
window.__activeBoardDropTarget = null;
}
} else {
window.__activeBoardDropTarget = null;
}
});
}

const addBoardTab = document.createElement('div');
addBoardTab.classList.add('board-tab', 'board-tab-add');
addBoardTab.setAttribute('role', 'presentation');
Expand Down Expand Up @@ -462,5 +530,44 @@ function renderBoardTabs() {
addBoardTab.appendChild(addBoardButton);
tabsEl.appendChild(addBoardTab);

const actualBoardsCount = openBoards.filter(b => b !== UNIFIED_BOARD_PATH).length;
if (actualBoardsCount > 1 && !openBoards.includes(UNIFIED_BOARD_PATH)) {
const addUnifiedTab = document.createElement('div');
addUnifiedTab.classList.add('board-tab', 'board-tab-add', 'board-tab-unified-unopened');
addUnifiedTab.setAttribute('role', 'presentation');

const addUnifiedButton = document.createElement('button');
addUnifiedButton.type = 'button';
addUnifiedButton.classList.add('board-tab-label', 'board-tab-add-label');
addUnifiedButton.innerHTML = '<i data-feather="layers" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 4px;"></i> All Boards';
addUnifiedButton.title = 'Open All Boards view';

if (openBoards.length >= MAX_OPEN_BOARDS) {
addUnifiedTab.classList.add('is-disabled');
addUnifiedButton.disabled = true;
addUnifiedButton.title = `Maximum ${MAX_OPEN_BOARDS} open boards`;
} else {
addUnifiedButton.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
const updatedOpenBoards = [...openBoards, UNIFIED_BOARD_PATH];
setStoredOpenBoards(updatedOpenBoards);

window.boardRoot = UNIFIED_BOARD_PATH;
setStoredActiveBoard(UNIFIED_BOARD_PATH);
if (typeof resetBoardLabelFilter === 'function') resetBoardLabelFilter();
if (typeof resetBoardSearch === 'function') resetBoardSearch();
await renderBoard();
});
}

addUnifiedTab.appendChild(addUnifiedButton);
tabsEl.appendChild(addUnifiedTab);
}

initializeBoardTabsSortable(tabsEl, openBoards.length > 1);

if (typeof feather !== 'undefined' && feather && typeof feather.replace === 'function') {
feather.replace();
}
}
68 changes: 47 additions & 21 deletions app/board/boardViews.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,30 +352,34 @@ function createTemporalPlacementForDate(cardEntry, dueDateValue) {
};
}

async function collectCardsForCalendar(boardRoot, lists) {
const listNames = Array.isArray(lists) ? lists : [];
async function collectCardsForCalendar(boardRoots, lists) {
const roots = Array.isArray(boardRoots) ? boardRoots : [boardRoots];
const cardPaths = [];

const listEntries = await Promise.all(
listNames.map(async (listName) => {
const listPath = `${boardRoot}${listName}`;
const cardNames = await window.board.listCards(listPath);
return {
listName,
listDisplayName: getBoardListDisplayName(listName),
listPath,
cardNames: Array.isArray(cardNames) ? cardNames : [],
};
}),
);
for (const boardRoot of roots) {
const listNames = Array.isArray(lists) ? lists : await window.board.listLists(boardRoot);

const listEntries = await Promise.all(
listNames.map(async (listName) => {
const listPath = `${boardRoot}${listName}`;
const cardNames = await window.board.listCards(listPath);
return {
listName,
listDisplayName: getBoardListDisplayName(listName),
listPath,
cardNames: Array.isArray(cardNames) ? cardNames : [],
};
}),
);

for (const { listName, listDisplayName, listPath, cardNames } of listEntries) {
for (const cardName of cardNames) {
cardPaths.push({
listName,
listDisplayName,
cardPath: `${listPath}/${cardName}`,
});
for (const { listName, listDisplayName, listPath, cardNames } of listEntries) {
for (const cardName of cardNames) {
cardPaths.push({
listName,
listDisplayName,
cardPath: `${listPath}/${cardName}`,
});
}
}
}

Expand Down Expand Up @@ -746,6 +750,28 @@ function createTemporalCardElement(cardEntry, isoDate, className) {
const footer = document.createElement('span');
footer.className = 'board-temporal-card-footer';

const UNIFIED_BOARD_PATH = '__unified__';
if (window.boardRoot === UNIFIED_BOARD_PATH) {
const storedBoards = typeof getStoredOpenBoards === 'function' ? getStoredOpenBoards() : [];
const openBoards = storedBoards.filter(b => b !== UNIFIED_BOARD_PATH);
let boardPath = '';
for (const root of openBoards) {
if (cardEntry.cardPath.startsWith(root)) {
boardPath = root;
break;
}
}
const boardName = typeof getBoardLabelFromPath === 'function'
? getBoardLabelFromPath(boardPath)
: boardPath.split('/').filter(Boolean).pop();

const boardLabel = document.createElement('span');
boardLabel.className = 'board-temporal-card-board-name';
boardLabel.textContent = boardName;
boardLabel.title = `Board: ${boardName}`;
footer.appendChild(boardLabel);
}

const listNameText = String(cardEntry.listDisplayName || '').trim();
if (listNameText) {
const listName = document.createElement('span');
Expand Down
Loading