Right-click on a card that's part of a ≥2-card selection now offers
"Join N cards" at the top of the context menu, and the same action
sits next to "Group" in the bottom select-bar.
Title strategy:
* sequential ctrl-click selection → first selected wins
* bulk gesture (rubber-band) → longest title wins
The bulk vs sequential distinction is tracked via a new
`state.selectionWasBulk` flag set in board.js when the rubber-band
becomes active and cleared in clearSelection() and after a successful
join. ctrl-clicks alone don't touch it, so bulk + ctrl-click hybrid
selections stay bulk.
Body format: list of every original title as `## heading` lines, then
each card's body separated by `---`. The winner keeps its color, tags,
bucket, due_date, parent_id; only the body is rewritten. Losers are
deleted via DELETE /api/cards/{id}.
Undo: a single pushUndo entry restores the winner's original body and
recreates every loser via POST + due_date PATCH (since POST doesn't
take due_date). Ctrl+Z reverts the whole join.
Implementation:
* state.js: new selectionWasBulk boolean in state.
* board.js: rubber-band onMove sets state.selectionWasBulk = true on
first activation; clearSelection clears it. New select-join click
handler dynamic-imports joinSelectedCards from context-menu.js to
keep the dependency lazy.
* context-menu.js: joinSelectedCards is now exported. Inserts
data-action="join" into the menu when the right-clicked card is in
a ≥2-card selection. Action handler dispatches to joinSelectedCards
before falling through to the regular if-chain so it can early-return.
* index.php: new #select-join button in #select-bar with ph-arrows-merge
icon, sits between Group and Bucket.
Tests (public/plugins/tests/card-join.test.js, registered in tests.js):
* visibility: shows for ≥2 selected with right-clicked card in the
selection; hidden for solo selection; hidden when right-click target
isn't selected.
* sequential winner: first ctrl-clicked card survives, others deleted.
* merged body lists every title at the top and contains every body.
* selection cleared after a successful join.
* bulk winner: longest title survives regardless of insertion order.
* undo: Ctrl+Z restores winner body + recreates losers.
undo.test.js race-fix:
* New waitForStateCard(id, predicate, timeout=2000) helper polls
state.cards every 30ms until the in-memory state reflects the
expected change. Replaces fragile fixed `sleep(300)` waits between
chained user actions where the second action reads from state.cards
before the first action's _reloadAll has landed.
* Multi-undo color test: between the two color changes, wait for
state.cards to actually contain the pink color before doing the
second rightClick. Without this the second action's contextmenu
handler reads the stale card and pushes the wrong undo snapshot,
collapsing both undo entries onto the original color.
* Due_date undo test: after PATCHing due_date directly via API, wait
for state.cards to reflect it before opening the editor — the
editor's snapshot for the undo entry reads from state.cards.find()
and would otherwise capture due_date=null.
chore: bump version to 0.9.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
57 lines
1.6 KiB
JavaScript
57 lines
1.6 KiB
JavaScript
// ── hyprcrd Test Index ──
|
|
//
|
|
// Loads all test suites via dynamic imports with a shared cache-busting
|
|
// query string. This ensures edits to any test file are picked up without
|
|
// requiring a hard refresh — each module URL becomes unique per page load.
|
|
|
|
const V = '?v=' + Date.now();
|
|
|
|
const FILES = [
|
|
'./shell.test.js',
|
|
'./settings.test.js',
|
|
'./board.test.js',
|
|
'./toolbar.test.js',
|
|
'./buckets.test.js',
|
|
'./bucket-context.test.js',
|
|
'./super-buckets.test.js',
|
|
'./context-menu.test.js',
|
|
'./card-interactions.test.js',
|
|
'./card-join.test.js',
|
|
'./filtering.test.js',
|
|
'./sorting.test.js',
|
|
'./state-functions.test.js',
|
|
'./favorites.test.js',
|
|
'./search.test.js',
|
|
'./search-logic.test.js',
|
|
'./calendar.test.js',
|
|
'./calendar-popup.test.js',
|
|
'./rrule.test.js',
|
|
'./datepicker.test.js',
|
|
'./gallery.test.js',
|
|
'./help.test.js',
|
|
'./multiselect.test.js',
|
|
'./detail.test.js',
|
|
'./detail-interactions.test.js',
|
|
'./editor.test.js',
|
|
'./editor-features.test.js',
|
|
'./editor-save.test.js',
|
|
'./editor-toolbar.test.js',
|
|
'./line-numbers.test.js',
|
|
'./canvas.test.js',
|
|
'./snippets.test.js',
|
|
'./image-fullscreen.test.js',
|
|
'./markdown.test.js',
|
|
'./markdown-edge.test.js',
|
|
'./undo.test.js',
|
|
'./api.test.js',
|
|
'./api-edge.test.js',
|
|
'./export.test.js',
|
|
'./relay.test.js',
|
|
'./notifications.test.js',
|
|
'./quick-capture.test.js',
|
|
'./theme.test.js',
|
|
];
|
|
|
|
// Await all imports so the runner can start in a deterministic state.
|
|
await Promise.all(FILES.map(f => import(f + V)));
|