v2.0.2 — English tag aliases, English-only website, undo-body flaky fix
- tags: accept #birthday and #appointment as English synonyms for
#geburtstag and #termin. Normalised to the German canonical form at
extractTags() write-time so every downstream check (parseTags /
calendar logic / board filters) keeps working without per-call-site
aliasing. Editor body-regex extended so the "new appointment"
heuristic and auto-sync trigger on either variant.
- website: replace #termin/#geburtstag with #appointment/#birthday in
tag chips, calendar feature list, and calendar legend.
- tests: fix flaky "Undo: Editor Save → Ctrl+Z reverts body change"
that raced doSave(). The test polled the server for the patched body
and fired Ctrl+Z as soon as it landed, but the undo shortcut is
guarded by !guards.isOpen('modal'), and closeModal() only runs after
the post-patch _reloadAll — on fast servers the key was swallowed.
Poll now also waits for the modal `.open` class to drop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
89d84d3538
commit
48b5907f4c
6 files changed
+25
-12
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('APP_NAME', 'hyprcrd');
|
define('APP_NAME', 'hyprcrd');
|
||||||
define('APP_VERSION', '2.0.1');
|
define('APP_VERSION', '2.0.2');
|
||||||
|
|
||||||
// APP_DEBUG: gates verbose error responses (curl traces, upstream bodies).
|
// APP_DEBUG: gates verbose error responses (curl traces, upstream bodies).
|
||||||
// Off by default; enable via env for local troubleshooting only.
|
// Off by default; enable via env for local troubleshooting only.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyprcrd",
|
"name": "hyprcrd",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"description": "hyprcrd — self-hosted card manager",
|
"description": "hyprcrd — self-hosted card manager",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+8
-1
@@ -273,7 +273,14 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
function validInt($v, $default = 0) { return is_numeric($v) ? (int)$v : $default; }
|
function validInt($v, $default = 0) { return is_numeric($v) ? (int)$v : $default; }
|
||||||
function extractTags($body) {
|
function extractTags($body) {
|
||||||
preg_match_all('/(?<=\s|^)#([a-zA-Z0-9_\/-][a-zA-Z0-9_\/-]*[a-zA-Z0-9_-]|[a-zA-Z0-9_-]+)/m', $body ?? '', $matches);
|
preg_match_all('/(?<=\s|^)#([a-zA-Z0-9_\/-][a-zA-Z0-9_\/-]*[a-zA-Z0-9_-]|[a-zA-Z0-9_-]+)/m', $body ?? '', $matches);
|
||||||
return implode(',', array_unique(array_map('strtolower', $matches[1] ?? [])));
|
// English ↔ German tag aliases: the calendar + journal logic is
|
||||||
|
// hardcoded against 'geburtstag' / 'termin', so accept the English
|
||||||
|
// synonyms and normalise to the canonical German form at write
|
||||||
|
// time. Keeps every downstream check (card.tags includes 'geburtstag')
|
||||||
|
// working without touching dozens of JS call sites.
|
||||||
|
$aliases = ['birthday' => 'geburtstag', 'appointment' => 'termin'];
|
||||||
|
$tags = array_map(fn($t) => $aliases[$t] ?? $t, array_map('strtolower', $matches[1] ?? []));
|
||||||
|
return implode(',', array_unique($tags));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin route handlers (registered by plugin backend.php files).
|
// Plugin route handlers (registered by plugin backend.php files).
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ export function openModal(card = null) {
|
|||||||
// `card` can be `null` (add-button new) or a shell `{id: null, ...}`
|
// `card` can be `null` (add-button new) or a shell `{id: null, ...}`
|
||||||
// from the calendar day-popup — both count as "new", so key on
|
// from the calendar day-popup — both count as "new", so key on
|
||||||
// card?.id not the card ref itself.
|
// card?.id not the card ref itself.
|
||||||
const isNewAppointment = !card?.id && /#(termin|geburtstag)\b/.test(body.value);
|
const isNewAppointment = !card?.id && /#(termin|appointment|geburtstag|birthday)\b/.test(body.value);
|
||||||
inpBucket.value = card?.bucket_id ? card.bucket_id
|
inpBucket.value = card?.bucket_id ? card.bucket_id
|
||||||
: (isNewAppointment ? ''
|
: (isNewAppointment ? ''
|
||||||
: (!virtualBuckets.includes(state.activeBucket) ? state.activeBucket : ''));
|
: (!virtualBuckets.includes(state.activeBucket) ? state.activeBucket : ''));
|
||||||
@@ -512,7 +512,7 @@ async function doSave(close, { animate = true } = {}) {
|
|||||||
}
|
}
|
||||||
await window._reloadAll();
|
await window._reloadAll();
|
||||||
// Auto-sync if card has #termin or #geburtstag
|
// Auto-sync if card has #termin or #geburtstag
|
||||||
if (data.body && /#(termin|geburtstag)\b/.test(data.body)) {
|
if (data.body && /#(termin|appointment|geburtstag|birthday)\b/.test(data.body)) {
|
||||||
window._triggerSync?.();
|
window._triggerSync?.();
|
||||||
}
|
}
|
||||||
if (close) {
|
if (close) {
|
||||||
|
|||||||
@@ -109,11 +109,17 @@ describe('Undo: Editor Save', () => {
|
|||||||
bodyEl.dispatchEvent(new Event('input', { bubbles: true }));
|
bodyEl.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
click('#modal-save-close');
|
click('#modal-save-close');
|
||||||
|
|
||||||
|
// Wait for both the server patch AND the modal close before firing
|
||||||
|
// Ctrl+Z — the global undo shortcut is guarded by
|
||||||
|
// `!guards.isOpen('modal')`, so if doSave's closeModal() hasn't run
|
||||||
|
// yet (it only fires after the post-save _reloadAll), the undo key
|
||||||
|
// is swallowed silently and the test races the client's save flow.
|
||||||
let card;
|
let card;
|
||||||
for (let i = 0; i < 40; i++) {
|
for (let i = 0; i < 40; i++) {
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
card = (await fetch(`api/cards?ids=${res.id}`).then(r => r.json())).find(c => c.id == res.id);
|
||||||
if ((card?.body || '').includes('after edit')) break;
|
const modalClosed = !document.getElementById('modal')?.classList.contains('open');
|
||||||
|
if ((card?.body || '').includes('after edit') && modalClosed) break;
|
||||||
}
|
}
|
||||||
expect(card.body).toContain('after edit');
|
expect(card.body).toContain('after edit');
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -352,10 +352,10 @@ footer {
|
|||||||
<span class="vtag" style="background:rgba(52,211,153,0.06);color:var(--gn);border-color:rgba(52,211,153,0.2);">#ops</span>
|
<span class="vtag" style="background:rgba(52,211,153,0.06);color:var(--gn);border-color:rgba(52,211,153,0.2);">#ops</span>
|
||||||
<span class="vtag" style="background:rgba(52,211,153,0.06);color:var(--gn);border-color:rgba(52,211,153,0.2);">#done</span>
|
<span class="vtag" style="background:rgba(52,211,153,0.06);color:var(--gn);border-color:rgba(52,211,153,0.2);">#done</span>
|
||||||
<span class="vtag" style="background:rgba(251,191,36,0.06);color:var(--yl);border-color:rgba(251,191,36,0.2);">#meeting</span>
|
<span class="vtag" style="background:rgba(251,191,36,0.06);color:var(--yl);border-color:rgba(251,191,36,0.2);">#meeting</span>
|
||||||
<span class="vtag" style="background:rgba(251,191,36,0.06);color:var(--yl);border-color:rgba(251,191,36,0.2);">#termin</span>
|
<span class="vtag" style="background:rgba(251,191,36,0.06);color:var(--yl);border-color:rgba(251,191,36,0.2);">#appointment</span>
|
||||||
<span class="vtag" style="background:rgba(56,189,248,0.06);color:var(--ac);border-color:rgba(56,189,248,0.2);">#research</span>
|
<span class="vtag" style="background:rgba(56,189,248,0.06);color:var(--ac);border-color:rgba(56,189,248,0.2);">#research</span>
|
||||||
<span class="vtag" style="background:rgba(244,114,182,0.06);color:var(--pk);border-color:rgba(244,114,182,0.2);">#idea</span>
|
<span class="vtag" style="background:rgba(244,114,182,0.06);color:var(--pk);border-color:rgba(244,114,182,0.2);">#idea</span>
|
||||||
<span class="vtag" style="background:rgba(244,114,182,0.06);color:var(--pk);border-color:rgba(244,114,182,0.2);">#geburtstag</span>
|
<span class="vtag" style="background:rgba(244,114,182,0.06);color:var(--pk);border-color:rgba(244,114,182,0.2);">#birthday</span>
|
||||||
<span class="vtag" style="background:rgba(56,189,248,0.06);color:var(--ac);border-color:rgba(56,189,248,0.2);">#personal</span>
|
<span class="vtag" style="background:rgba(56,189,248,0.06);color:var(--ac);border-color:rgba(56,189,248,0.2);">#personal</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -407,8 +407,8 @@ See also: <span class="link">[[Infra checklist]]</span>
|
|||||||
<p>A month-grid calendar built into hyprcrd. Journal entries, appointments, and birthdays — all just cards with the right title or tag. Bidirectional CalDAV sync with ownCloud and Nextcloud.</p>
|
<p>A month-grid calendar built into hyprcrd. Journal entries, appointments, and birthdays — all just cards with the right title or tag. Bidirectional CalDAV sync with ownCloud and Nextcloud.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Journal entries: title = <code>YYYY-MM-DD</code>, one per day, no tag needed</li>
|
<li>Journal entries: title = <code>YYYY-MM-DD</code>, one per day, no tag needed</li>
|
||||||
<li>Appointments: <code>#termin</code> tag + structured template (time, end, location, attendees)</li>
|
<li>Appointments: <code>#appointment</code> tag + structured template (time, end, location, attendees)</li>
|
||||||
<li>Birthdays: <code>#geburtstag</code> tag, recurring annually</li>
|
<li>Birthdays: <code>#birthday</code> tag, recurring annually</li>
|
||||||
<li>Click any day to create a journal entry, appointment, or birthday</li>
|
<li>Click any day to create a journal entry, appointment, or birthday</li>
|
||||||
<li>CalDAV sync: bidirectional push/pull with ownCloud, Nextcloud, or any CalDAV server</li>
|
<li>CalDAV sync: bidirectional push/pull with ownCloud, Nextcloud, or any CalDAV server</li>
|
||||||
<li>Per-calendar color + icon configuration</li>
|
<li>Per-calendar color + icon configuration</li>
|
||||||
@@ -434,8 +434,8 @@ See also: <span class="link">[[Infra checklist]]</span>
|
|||||||
</div>
|
</div>
|
||||||
<div style="margin-top:12px;font-size:10px;display:flex;gap:12px;justify-content:center;">
|
<div style="margin-top:12px;font-size:10px;display:flex;gap:12px;justify-content:center;">
|
||||||
<span><span style="color:var(--ac);">•</span> journal</span>
|
<span><span style="color:var(--ac);">•</span> journal</span>
|
||||||
<span><span style="color:var(--yl);">•</span> termin</span>
|
<span><span style="color:var(--yl);">•</span> appointment</span>
|
||||||
<span><span style="color:var(--pk);">•</span> geburtstag</span>
|
<span><span style="color:var(--pk);">•</span> birthday</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user