Compare commits
2
Commits
6fa42eb267
...
8e738ed9d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e738ed9d4 | ||
|
|
c934673c4a |
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('APP_NAME', 'hyprcrd');
|
||||
define('APP_VERSION', '1.9.26');
|
||||
define('APP_VERSION', '1.9.28');
|
||||
|
||||
// Database path — writable directory, outside webroot
|
||||
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hyprcrd",
|
||||
"version": "1.9.26",
|
||||
"version": "1.9.28",
|
||||
"description": "hyprcrd — self-hosted card manager",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1926,6 +1926,7 @@ if (str_starts_with($route, '/api/')) {
|
||||
</div>
|
||||
<div class="toggle-row" data-toggle="setting-confirm-delete"><span class="toggle-label">Confirm before delete</span><div class="toggle-switch" id="setting-confirm-delete"></div></div>
|
||||
<div class="toggle-row" data-toggle="setting-auto-save"><span class="toggle-label">Auto-save on close (editor)</span><div class="toggle-switch" id="setting-auto-save"></div></div>
|
||||
<div class="toggle-row" data-toggle="setting-open-switches-bucket"><span class="toggle-label">Switch to card's bucket on open (search / link)</span><div class="toggle-switch" id="setting-open-switches-bucket"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -77,6 +77,10 @@ async function save() {
|
||||
method: 'PATCH',
|
||||
body: { body },
|
||||
});
|
||||
// Keep the local state in sync so the next openOverlay (which
|
||||
// reads from state.cards, not the server) sees the latest body.
|
||||
const local = (window.state?.cards || []).find(c => c.id == activeCardId);
|
||||
if (local) local.body = body;
|
||||
}
|
||||
|
||||
// ── Board card decoration ────────────────────────────────────────
|
||||
|
||||
@@ -130,6 +130,55 @@ describe('Bookmarks: card links', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bookmarks: column persistence', () => {
|
||||
it('new columns survive close + reopen', async () => {
|
||||
let card;
|
||||
try {
|
||||
card = await fetch('api/cards', { method: 'POST', headers: hJson(), body: JSON.stringify({ title: '__test_bm_colsave__', body: emptyBody() }) }).then(r => r.json());
|
||||
await window._reloadAll();
|
||||
await sleep(200);
|
||||
|
||||
// Open overlay, add a column via the data model + save
|
||||
// (simulates what addColumn does without the dialog).
|
||||
const el = document.querySelector(`.board-card[data-id="${card.id}"]`);
|
||||
click(el);
|
||||
await sleep(150);
|
||||
|
||||
// Grab the plugin's internal data via a fresh body parse,
|
||||
// add a column, and save through the overlay's save path.
|
||||
const full = await fetch(`api/cards?ids=${card.id}`).then(r => r.json());
|
||||
const body = (full[0]?.body || '').trim();
|
||||
const jsonEnd = body.lastIndexOf('}');
|
||||
const parsed = JSON.parse(body.substring(0, jsonEnd + 1));
|
||||
parsed.columns.push({ name: 'Added', items: [] });
|
||||
const newBody = JSON.stringify(parsed) + '\n\n#bookmarks';
|
||||
await fetch(`api/cards/${card.id}`, {
|
||||
method: 'PATCH', headers: hJson(),
|
||||
body: JSON.stringify({ body: newBody }),
|
||||
});
|
||||
// Update local state like save() does.
|
||||
const local = (window.state?.cards || []).find(c => c.id == card.id);
|
||||
if (local) local.body = newBody;
|
||||
|
||||
click($('bookmarks-close'));
|
||||
await sleep(100);
|
||||
|
||||
// Reopen — the new column must still be there.
|
||||
const el2 = document.querySelector(`.board-card[data-id="${card.id}"]`);
|
||||
click(el2);
|
||||
await sleep(150);
|
||||
const names = [...document.querySelectorAll('.bm-col-name')].map(n => n.textContent);
|
||||
expect(names).toContain('General');
|
||||
expect(names).toContain('Added');
|
||||
click($('bookmarks-close'));
|
||||
await sleep(80);
|
||||
} finally {
|
||||
if (card?.id) await fetch(`api/cards/${card.id}`, { method: 'DELETE', headers: hDel() });
|
||||
await window._reloadAll();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bookmarks: column width persistence', () => {
|
||||
it('saves and restores columnWidth from card body', async () => {
|
||||
const cols = [{ name: 'W', items: [] }];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user