hotkeys, gitea ticket-from-card, collab unread, runner auto-retry
board hotkeys (public/js/app.js):
* "c" toggles the calendar from the board (no modifier, body focus,
no full-screen overlay in front).
* "t" opens a tag quick-pick popover that lists every unique tag from
state.cards. Filter input + arrow-key/enter pick + ESC/click-outside
close. Picking sets state.activeTags = [tag] and re-renders the
board + tag bar. Toggle: a second "t" press closes the picker.
* "1"-"9" picks the Nth user bucket in the bar. The fixed filters
(all/current/favorites/journal/none) are skipped. Super-buckets
count as one slot and activate via the new activateBucket() helper
in buckets.js — that bypasses the click handler so the dropdown
popover does NOT open; the currently shown member (parent or
active child) is selected directly.
* ESC chain in the existing handler now also closes #calendar (was
missing — calendar.js only handles ESC for the day-popup, not the
whole overlay).
buckets (public/js/modules/buckets.js):
* New exported activateBucket(idOrSpecial) helper: setActiveBucket
+ clear activeGroup + renderBar + window._renderBoard. Used by
the number-key hotkey to select super-buckets without triggering
their dropdown.
board split panel (public/js/modules/board.js):
* closeBoardPreview() now removes the .active class from the
toolbar's #board-split-toggle. The previous code removed
.text-t-accent which was a no-op since toggleBoardSplit() uses
.active — the X button on the panel closed the split but left
the toolbar showing the toggle as still on.
email plugin (public/plugins/email/email.js):
* ESC closes the overlay reliably: the previous handler was on
#email-overlay, which only fires when focus is *inside* the
overlay (rare after open). Moved to a document-level keydown.
* Bare "m" toggles the mail overlay open/closed. Suppressed when
Ctrl/Meta/Alt is held, focus is in an input/textarea/contenteditable,
or any other full-screen overlay is in front.
collab-buckets (public/plugins/collab-buckets/backend.php):
* When a card change is applied in the pull phase from a foreign
author (insert / update / tombstone), bump
collab_buckets.updated_at to NOW for that bucket. has_unread is
computed as updated_at > last_seen_at and was previously never
triggered for incoming cards — the bucket-bar tab now gets the
unread dot + 3-pulse when the user is looking at a different
bucket. Own pushes are skipped (the relay echoes them back too).
gitea plugin: make ticket from card (public/plugins/gitea/gitea.js):
* New window._giteaMakeIssueFromCard(card) helper. Opens the gitea
overlay, switches to the create-issue form, fetches the card body
if it's lazy/null, and pre-fills #gitea-create-title and
#gitea-create-body. Does NOT submit — the user picks the repo and
reviews before clicking submit. Soft-bails on the welcome view
if no gitea account is configured yet.
context-menu (public/js/modules/context-menu.js):
* New "Make ticket" entry in the card right-click menu, only rendered
when window._giteaMakeIssueFromCard is exposed. Action handler
hides the menu and calls the helper. Sits next to "Copy text".
* joinSelectedCards() now dedupes hashtag mentions in the merged body
(first occurrence wins, case-insensitive) so a tag that appeared on
multiple source cards isn't repeated in the resulting body. Followed
by a tab/space/blank-line cleanup pass.
test runner auto-retry (public/plugins/tests/runner.js):
* runAll() takes an optional Set<string> of `${suite}::${test}` keys
and runs only those when set. Progress bar denominator reflects
the filtered count.
* run() collects every failed test after the first pass, restores the
backup so the retry runs against the same baseline state, then
re-runs only those via runAll(failedKeys). Retry results are merged
into the original results array with `retried: true` and the
original error preserved on `originalError`.
* renderResults distinguishes three states: passed first try (green ✓),
pass-on-retry (amber ⚠ "flaky — passed on retry" with the first-run
error shown), failed twice (red ✗ "failed twice"). Summary line
carries a "N flaky" count and goes amber if only flaky tests
appeared in the run.
tests (public/plugins/tests/context-menu.test.js):
* Two new tests for the make-ticket action:
- action exists in the menu when window._giteaMakeIssueFromCard
is exposed (soft-skip otherwise)
- clicking it opens the gitea overlay, surfaces the create-issue
form, and pre-fills title + body without submitting. Soft-skips
the assertion when no gitea account is configured (welcome view).
chore: bump version to 0.9.7
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
285e3b4abe
commit
923de0cdd6
10 files changed
+436
-38
No files matched your search
@@ -312,8 +312,11 @@ function closeBoardPreview() {
|
||||
boardPreview?.classList.add('hidden');
|
||||
state.boardSplitOpen = false;
|
||||
localStorage.setItem('hyprcrd-board-split', '0');
|
||||
// The toolbar's active state lives on the `.active` class — the
|
||||
// previous `text-t-accent` removal was a no-op since that class
|
||||
// isn't what toggleBoardSplit uses.
|
||||
const btn = $('board-split-toggle');
|
||||
if (btn) btn.classList.remove('text-t-accent');
|
||||
if (btn) btn.classList.remove('active');
|
||||
board.querySelectorAll('.board-card').forEach(el => el.classList.remove('preview-active'));
|
||||
state.boardPreviewCardId = null;
|
||||
resetBoardPreview();
|
||||
|
||||
@@ -186,6 +186,18 @@ const inpBucket = $('inp-bucket');
|
||||
const searchFilter = $('search-bucket-filter');
|
||||
const tasksFilter = $('tasks-bucket-filter');
|
||||
|
||||
/** Switch the active bucket without going through the click path —
|
||||
* used by the number-key hotkeys (and any other code that wants to
|
||||
* activate a bucket programmatically without triggering the super-
|
||||
* bucket dropdown). Persists via setActiveBucket and re-renders the
|
||||
* bar + board so the active highlight tracks. */
|
||||
export function activateBucket(idOrSpecial) {
|
||||
setActiveBucket(idOrSpecial);
|
||||
state.activeGroup = null;
|
||||
renderBar();
|
||||
window._renderBoard?.();
|
||||
}
|
||||
|
||||
export async function loadBuckets() {
|
||||
state.buckets = await api('api/buckets');
|
||||
// The persisted activeBucket may point to a bucket that no longer
|
||||
|
||||
@@ -74,7 +74,21 @@ export async function joinSelectedCards() {
|
||||
const ordered = [winner, ...losers];
|
||||
const titleList = ordered.map(c => `## ${c.title || ''}`).join('\n');
|
||||
const bodyChunks = ordered.map(c => (c.body || '').trim()).filter(Boolean);
|
||||
const mergedBody = titleList + '\n\n---\n\n' + bodyChunks.join('\n\n---\n\n');
|
||||
let mergedBody = titleList + '\n\n---\n\n' + bodyChunks.join('\n\n---\n\n');
|
||||
// Dedupe hashtag mentions across the merged body — first occurrence
|
||||
// wins, subsequent ones get stripped (case-insensitive). Without
|
||||
// this the merged card has every shared tag listed once per source
|
||||
// card, which clutters the body and makes the rendered tag chips
|
||||
// look duplicated to a casual reader (the server's extractTags also
|
||||
// dedupes the tags column, but the body text itself is what the
|
||||
// user actually edits).
|
||||
const seenTags = new Set();
|
||||
mergedBody = mergedBody.replace(/(^|\s)#([a-zA-Z0-9_/-]+)/g, (match, lead, tag) => {
|
||||
const key = tag.toLowerCase();
|
||||
if (seenTags.has(key)) return lead; // strip the dupe, keep the leading whitespace
|
||||
seenTags.add(key);
|
||||
return match;
|
||||
}).replace(/[ \t]{2,}/g, ' ').replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
// Snapshot for undo BEFORE we mutate anything
|
||||
const winnerPrev = { title: winner.title, body: winner.body || '' };
|
||||
@@ -123,6 +137,7 @@ export function showContextMenu(x, y, card) {
|
||||
<button class="ctx-item" data-action="due-done"><i class="ph ph-check-circle text-sm ${parseTags(card).includes('done') ? 'text-t-accent' : ''}"></i><span class="font-pixel text-[10px]">${parseTags(card).includes('done') ? 'Unmark done' : 'Mark done'}</span></button>
|
||||
<button class="ctx-item" data-action="save-md"><i class="ph ph-file-md text-sm"></i><span class="font-pixel text-[10px]">Save .md</span></button>
|
||||
<button class="ctx-item" data-action="copy-text"><i class="ph ph-clipboard-text text-sm"></i><span class="font-pixel text-[10px]">Copy text</span></button>
|
||||
${typeof window._giteaMakeIssueFromCard === 'function' ? `<button class="ctx-item" data-action="make-ticket"><i class="ph ph-git-pull-request text-sm"></i><span class="font-pixel text-[10px]">Make ticket</span></button>` : ''}
|
||||
<button class="ctx-item" data-action="push-relay"><i class="ph ph-broadcast text-sm"></i><span class="font-pixel text-[10px]">Push to relay</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
||||
<button class="ctx-item" data-action="favorite"><i class="ph${isFavorite(card.id) ? '-fill' : ''} ph-star text-sm ${isFavorite(card.id) ? 'text-amber-400' : ''}"></i><span class="font-pixel text-[10px]">${isFavorite(card.id) ? 'Unfavorite' : 'Favorite'}</span></button>
|
||||
<div class="ctx-sep"></div>
|
||||
@@ -306,6 +321,12 @@ export function showContextMenu(x, y, card) {
|
||||
await window._reloadAll();
|
||||
}
|
||||
else if (a === 'save-md') { hideContextMenu(); window.location.href = `api/export-md?ids=${card.id}`; }
|
||||
else if (a === 'make-ticket') {
|
||||
hideContextMenu();
|
||||
// Gitea plugin pre-fills the create-issue form from this
|
||||
// card and leaves it open — the user reviews and submits.
|
||||
await window._giteaMakeIssueFromCard?.(card);
|
||||
}
|
||||
else if (a === 'copy-text') {
|
||||
hideContextMenu();
|
||||
let body = card.body || '';
|
||||
|
||||
Reference in new issue
Block a user