From c934673c4aa3d9d0cc48e2dd24f0082a3516adfd Mon Sep 17 00:00:00 2001
From: th
Date: Thu, 16 Apr 2026 23:39:36 +0200
Subject: [PATCH] detail: optional bucket-switch on open from search / link
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
New toggle under Settings → Behavior → Interactions: "Switch to
card's bucket on open (search / link)". When enabled, openDetail
calls activateBucket(card.bucket_id) so the board and bucket bar
update to show the card's context. Skipped when already on 'all'
or the correct bucket. Default off.
Uses activateBucket() (from buckets.js) instead of raw state
assignment so the bucket-bar highlight tracks the switch.
Tests: two cases — setting on switches activeBucket + verifies the
bucket-tab.active in the bar matches; setting off leaves the
bucket unchanged. Both create/delete probe cards in try/finally
and restore the setting + activeBucket in afterEach.
Bumps to 1.9.27.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
config.php | 2 +-
electron/package.json | 2 +-
public/index.php | 1 +
public/js/modules/detail.js | 13 +++++
public/js/modules/theme.js | 1 +
public/plugins/tests/detail.test.js | 80 ++++++++++++++++++++++++++++-
6 files changed, 96 insertions(+), 3 deletions(-)
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();
+ }
+ });
+});
|