diff --git a/config.php b/config.php
index 0cf3ce2..ca438a9 100644
--- a/config.php
+++ b/config.php
@@ -1,7 +1,7 @@
Auto-save on close (editor)
+ Switch to card's bucket on open (search / link)
diff --git a/public/js/modules/detail.js b/public/js/modules/detail.js
index 995cbcc..e35e97a 100644
--- a/public/js/modules/detail.js
+++ b/public/js/modules/detail.js
@@ -5,6 +5,7 @@ import { state, parseTags, tagChipHtml, getChildren, getGroupCards, getRecent, m
import { renderMd, stripMd, firstImage, addCodeCopyButtons, formatTodoItems } from './markdown.js';
import { pushCardDelete, pushCardEdit, snapshotCard } from './undo.js';
import { showDialog } from './dialog.js';
+import { activateBucket } from './buckets.js';
const detail = $('detail');
const scroll = $('detail-scroll');
@@ -57,6 +58,18 @@ export function openDetail(id, options = {}) {
if (!card) return;
markRecent(id);
+ // Optional: switch the board to the card's bucket so the user
+ // sees context. Only when opened via search, wikilink, or
+ // programmatic call — not when the user clicked a board card
+ // (they're already in the right bucket). Gated by a setting
+ // under Behavior → Interactions.
+ if (card.bucket_id
+ && localStorage.getItem('hyprcrd-open-switches-bucket') === '1'
+ && state.activeBucket !== 'all'
+ && String(state.activeBucket) !== String(card.bucket_id)) {
+ activateBucket(card.bucket_id);
+ }
+
const el = $('board')?.querySelector(`[data-id="${id}"]`);
if (el) el.style.borderBottom = `3px solid ${card.color}`;
diff --git a/public/js/modules/theme.js b/public/js/modules/theme.js
index a72889d..c383677 100644
--- a/public/js/modules/theme.js
+++ b/public/js/modules/theme.js
@@ -450,6 +450,7 @@ function initBehaviorSettings() {
'setting-show-done-badge': { key: 'hyprcrd-show-done-badge', def: '1' },
'setting-confirm-delete': { key: 'hyprcrd-confirm-delete', def: '1' },
'setting-auto-save': { key: 'hyprcrd-auto-save', def: '0' },
+ 'setting-open-switches-bucket': { key: 'hyprcrd-open-switches-bucket', def: '0' },
};
for (const [id, cfg] of Object.entries(selects)) {
const el = $(id);
diff --git a/public/plugins/tests/detail.test.js b/public/plugins/tests/detail.test.js
index f8fe6a4..7cdf0bd 100644
--- a/public/plugins/tests/detail.test.js
+++ b/public/plugins/tests/detail.test.js
@@ -1,4 +1,4 @@
-import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll } from './runner.js?v=4';
+import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=4';
describe('Detail View', () => {
afterEach(() => closeAll());
@@ -71,3 +71,81 @@ describe('Detail View', () => {
expect($('#detail')).toBeHidden();
});
});
+
+describe('Detail: open-switches-bucket', () => {
+ const SETTING_KEY = 'hyprcrd-open-switches-bucket';
+ const BUCKET_KEY = 'hyprcrd-active-bucket';
+
+ afterEach(async () => {
+ await closeAll();
+ // Restore runner baseline.
+ window.state.activeBucket = 'all';
+ localStorage.setItem(BUCKET_KEY, 'all');
+ });
+
+ it('switches to the card bucket when the setting is on', async () => {
+ const buckets = await fetch('api/buckets').then(r => r.json());
+ if (!Array.isArray(buckets) || !buckets.length) return;
+ const b = buckets[0];
+ const token = csrf();
+ const h = { 'Content-Type': 'application/json', 'X-CSRF-Token': token };
+ let card;
+ const prev = localStorage.getItem(SETTING_KEY);
+ try {
+ card = await fetch('api/cards', { method: 'POST', headers: h,
+ body: JSON.stringify({ title: '__test_switch_bucket__', body: '', bucket_id: b.id })
+ }).then(r => r.json());
+ await window._reloadAll();
+ await sleep(100);
+
+ // Start on a DIFFERENT bucket so the switch is visible.
+ const other = buckets.length > 1 ? buckets[1] : buckets[0];
+ window.state.activeBucket = String(other.id);
+
+ // Enable the setting.
+ localStorage.setItem(SETTING_KEY, '1');
+
+ window._openDetail(card.id);
+ await sleep(150);
+
+ expect(String(window.state.activeBucket)).toBe(String(b.id));
+ // Bucket bar must highlight the switched-to bucket.
+ const activeTab = document.querySelector('#bucket-bar .bucket-tab.active[data-bucket]');
+ expect(activeTab).toExist();
+ expect(activeTab.dataset.bucket).toBe(String(b.id));
+ } finally {
+ localStorage.setItem(SETTING_KEY, prev ?? '0');
+ if (card?.id) await fetch(`api/cards/${card.id}`, { method: 'DELETE', headers: { 'X-CSRF-Token': token } });
+ await window._reloadAll();
+ }
+ });
+
+ it('does NOT switch when the setting is off', async () => {
+ const buckets = await fetch('api/buckets').then(r => r.json());
+ if (!Array.isArray(buckets) || buckets.length < 2) return;
+ const token = csrf();
+ const h = { 'Content-Type': 'application/json', 'X-CSRF-Token': token };
+ let card;
+ const prev = localStorage.getItem(SETTING_KEY);
+ try {
+ card = await fetch('api/cards', { method: 'POST', headers: h,
+ body: JSON.stringify({ title: '__test_no_switch__', body: '', bucket_id: buckets[0].id })
+ }).then(r => r.json());
+ await window._reloadAll();
+ await sleep(100);
+
+ window.state.activeBucket = String(buckets[1].id);
+ localStorage.setItem(SETTING_KEY, '0');
+
+ window._openDetail(card.id);
+ await sleep(100);
+
+ // Should stay on the other bucket.
+ expect(String(window.state.activeBucket)).toBe(String(buckets[1].id));
+ } finally {
+ localStorage.setItem(SETTING_KEY, prev ?? '0');
+ if (card?.id) await fetch(`api/cards/${card.id}`, { method: 'DELETE', headers: { 'X-CSRF-Token': token } });
+ await window._reloadAll();
+ }
+ });
+});