diff --git a/config.php b/config.php index 0cf3ce2..ca438a9 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@
Confirm before delete
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(); + } + }); +});