Compare commits
4
Commits
d5ebbab6a5
...
c2485b1307
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2485b1307 | ||
|
|
b08bdbaeb2 | ||
|
|
0e3088db9a | ||
|
|
29f92dbf9e |
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('APP_NAME', 'hyprcrd');
|
||||
define('APP_VERSION', '1.9.15');
|
||||
define('APP_VERSION', '1.9.17');
|
||||
|
||||
// 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.15",
|
||||
"version": "1.9.17",
|
||||
"description": "hyprcrd — self-hosted card manager",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -226,8 +226,18 @@ export function openModal(card = null) {
|
||||
inpTitle.value = card ? card.title : (isJournal ? today : '');
|
||||
body.value = card ? card.body : '';
|
||||
const virtualBuckets = ['all', 'none', 'current', 'favorites', 'journal'];
|
||||
// New appointments (#termin / #geburtstag pre-filled from the
|
||||
// calendar's day-popup template) must never inherit the active
|
||||
// bucket — they belong to a calendar, not a kanban column. For
|
||||
// every other new card, inherit the active real bucket if we're
|
||||
// in one so the user doesn't have to re-pick it.
|
||||
// `card` can be `null` (add-button new) or a shell `{id: null, ...}`
|
||||
// from the calendar day-popup — both count as "new", so key on
|
||||
// card?.id not the card ref itself.
|
||||
const isNewAppointment = !card?.id && /#(termin|geburtstag)\b/.test(body.value);
|
||||
inpBucket.value = card?.bucket_id ? card.bucket_id
|
||||
: (!virtualBuckets.includes(state.activeBucket) ? state.activeBucket : '');
|
||||
: (isNewAppointment ? ''
|
||||
: (!virtualBuckets.includes(state.activeBucket) ? state.activeBucket : ''));
|
||||
selectColor(card ? card.color : '#38bdf8');
|
||||
setDueDate(card?.due_date || '');
|
||||
updateDoneToggle();
|
||||
|
||||
@@ -226,6 +226,66 @@ function hitHtml(card, idx, words) {
|
||||
${htags.length ? `<div class="flex flex-wrap gap-1 mt-1">${htags.slice(0, 3).map(t => `<span class="tag-chip" style="font-size:8px;padding:0 4px;">#${esc(t)}</span>`).join('')}</div>` : ''}</div></div>`;
|
||||
}
|
||||
|
||||
function openBucketPicker(cardId, x, y) {
|
||||
document.querySelector('.search-bucket-picker')?.remove();
|
||||
const picker = document.createElement('div');
|
||||
picker.className = 'search-bucket-picker';
|
||||
// Clamp to viewport so a click near the bottom-right doesn't flow
|
||||
// off-screen — rendered invisibly first to measure, then placed.
|
||||
picker.style.cssText = `position:fixed;left:0;top:0;visibility:hidden;z-index:300;background:var(--t-surface);border:1px solid var(--t-border);border-radius:4px;box-shadow:0 4px 20px rgba(0,0,0,0.4);min-width:180px;max-height:280px;display:flex;flex-direction:column;`;
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.style.cssText = 'overflow-y:auto;flex:1;';
|
||||
|
||||
const addRow = (label, color, onClick) => {
|
||||
const item = document.createElement('button');
|
||||
item.style.cssText = 'display:flex;align-items:center;gap:8px;width:100%;padding:6px 12px;background:none;border:none;color:var(--t-muted);font-size:12px;cursor:pointer;text-align:left;';
|
||||
item.innerHTML = `<span style="width:8px;height:8px;border-radius:2px;background:${color};flex-shrink:0;"></span><span style="font-family:var(--t-font-pixel);font-size:10px;">${esc(label)}</span>`;
|
||||
item.addEventListener('mouseenter', () => { item.style.color = 'var(--t-bright)'; });
|
||||
item.addEventListener('mouseleave', () => { item.style.color = 'var(--t-muted)'; });
|
||||
item.addEventListener('click', onClick);
|
||||
list.appendChild(item);
|
||||
};
|
||||
|
||||
addRow('(unsorted)', 'transparent', async () => {
|
||||
picker.remove();
|
||||
await api(`api/cards/${cardId}`, { method: 'PATCH', body: { bucket_id: null } });
|
||||
await window._reloadAll();
|
||||
search(input.value);
|
||||
});
|
||||
state.buckets.forEach(b => {
|
||||
addRow(b.name, b.color || '#64748b', async () => {
|
||||
picker.remove();
|
||||
await api(`api/cards/${cardId}`, { method: 'PATCH', body: { bucket_id: b.id } });
|
||||
await window._reloadAll();
|
||||
search(input.value);
|
||||
});
|
||||
});
|
||||
|
||||
picker.appendChild(list);
|
||||
document.body.appendChild(picker);
|
||||
|
||||
// Place, then flip if we'd overflow the viewport.
|
||||
const r = picker.getBoundingClientRect();
|
||||
let left = x, top = y;
|
||||
if (left + r.width > window.innerWidth - 8) left = Math.max(8, window.innerWidth - r.width - 8);
|
||||
if (top + r.height > window.innerHeight - 8) top = Math.max(8, window.innerHeight - r.height - 8);
|
||||
picker.style.left = left + 'px';
|
||||
picker.style.top = top + 'px';
|
||||
picker.style.visibility = '';
|
||||
|
||||
const onDown = (e) => { if (!picker.contains(e.target)) { picker.remove(); cleanup(); } };
|
||||
const onEsc = (e) => { if (e.key === 'Escape') { e.stopPropagation(); picker.remove(); cleanup(); } };
|
||||
const cleanup = () => {
|
||||
document.removeEventListener('mousedown', onDown, true);
|
||||
document.removeEventListener('keydown', onEsc, true);
|
||||
};
|
||||
setTimeout(() => {
|
||||
document.addEventListener('mousedown', onDown, true);
|
||||
document.addEventListener('keydown', onEsc, true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function bindHits() {
|
||||
results.querySelectorAll('.search-hit').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
@@ -234,6 +294,16 @@ function bindHits() {
|
||||
else { active = i; updateActive(); }
|
||||
});
|
||||
el.addEventListener('dblclick', () => { const i = parseInt(el.dataset.idx); if (hits[i]) window._openDetail(hits[i].id); });
|
||||
el.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
const id = el.dataset.cardId;
|
||||
if (!id) return;
|
||||
// Highlight the row so the user sees which card the picker
|
||||
// will move — mirrors the active-on-click behaviour.
|
||||
const i = parseInt(el.dataset.idx);
|
||||
if (!isNaN(i)) { active = i; updateActive(); }
|
||||
openBucketPicker(id, e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
// Drag: only non-group, non-child cards
|
||||
if (el.draggable) {
|
||||
|
||||
@@ -72,3 +72,41 @@ describe('Editor: Card Create', () => {
|
||||
expect(document.querySelector(`[data-id="${res.id}"]`)).toExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Editor: new-appointment bucket default', () => {
|
||||
// New appointments (body pre-seeded with #termin / #geburtstag from
|
||||
// the calendar day-popup template) must not inherit the currently
|
||||
// active real bucket — they belong to a calendar. Regular new
|
||||
// cards keep inheriting so the user doesn't have to re-pick.
|
||||
|
||||
afterEach(async () => {
|
||||
// Close any open modal and restore the runner's baseline bucket.
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||||
await sleep(80);
|
||||
window.state.activeBucket = 'all';
|
||||
});
|
||||
|
||||
it('#termin body forces empty bucket; plain body inherits active', async () => {
|
||||
const buckets = await fetch('api/buckets').then(r => r.json());
|
||||
if (!Array.isArray(buckets) || !buckets.length) return; // nothing to inherit from
|
||||
const b = buckets[0];
|
||||
window.state.activeBucket = b.id;
|
||||
|
||||
// Appointment-flavoured body → inp-bucket stays empty.
|
||||
window._openModal({ id: null, title: '9999-01-01 — probe', body: '#termin', color: '#fb923c', bucket_id: null });
|
||||
await sleep(80);
|
||||
const apptBucket = $('#inp-bucket').value;
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||||
await sleep(120);
|
||||
|
||||
// Plain body → inp-bucket inherits the active bucket id.
|
||||
window._openModal({ id: null, title: '__test_inherit__', body: '', color: '#38bdf8', bucket_id: null });
|
||||
await sleep(80);
|
||||
const regBucket = $('#inp-bucket').value;
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||||
await sleep(120);
|
||||
|
||||
expect(apptBucket).toBe('');
|
||||
expect(regBucket).toBe(String(b.id));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user