search: integrate find/replace into search overlay, add toolbar buttons
Moves board-wide find/replace from standalone overlay into the search panel as a toggleable replace row. Adds editor toolbar find/replace button. Ctrl+Shift+H now opens search in replace mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
838dfe2a6e
commit
d1b26e625a
7 files changed
+159
-232
No files matched your search
@@ -13,59 +13,37 @@ async function getCard(id) {
|
||||
return all.find(c => c.id == id);
|
||||
}
|
||||
|
||||
function openBoardFR() {
|
||||
// Ctrl+Shift+H opens the board find/replace overlay
|
||||
function openSearchReplace() {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'H', ctrlKey: true, shiftKey: true, bubbles: true,
|
||||
}));
|
||||
}
|
||||
|
||||
describe('Board: Find/Replace', () => {
|
||||
describe('Search: Find/Replace', () => {
|
||||
afterEach(async () => {
|
||||
document.getElementById('board-find-replace')?.remove();
|
||||
await closeAll();
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+H opens the overlay', async () => {
|
||||
openBoardFR();
|
||||
// Wait for the overlay (it does an async api('api/cards') fetch)
|
||||
for (let i = 0; i < 40; i++) {
|
||||
if (document.getElementById('board-find-replace')) break;
|
||||
await sleep(50);
|
||||
}
|
||||
expect(document.getElementById('board-find-replace')).toExist();
|
||||
expect(document.getElementById('bfr-find')).toExist();
|
||||
expect(document.getElementById('bfr-replace')).toExist();
|
||||
it('Ctrl+Shift+H opens search with replace row visible', async () => {
|
||||
openSearchReplace();
|
||||
await sleep(200);
|
||||
const overlay = document.getElementById('search');
|
||||
expect(overlay?.classList.contains('open')).toBe(true);
|
||||
expect(document.getElementById('search-replace-row')).toBeVisible();
|
||||
expect(document.getElementById('search-replace-input')).toExist();
|
||||
expect(document.getElementById('search-replace-all')).toExist();
|
||||
});
|
||||
|
||||
it('typing in find shows matching cards', async () => {
|
||||
const c1 = await makeCard('__bfr_match_a__', 'the quick brown fox');
|
||||
const c2 = await makeCard('__bfr_match_b__', 'the quick red fox');
|
||||
const c3 = await makeCard('__bfr_nomatch__', 'no match here');
|
||||
await window._reloadAll?.();
|
||||
it('replace toggle shows/hides replace row', async () => {
|
||||
openSearchReplace();
|
||||
await sleep(200);
|
||||
|
||||
openBoardFR();
|
||||
for (let i = 0; i < 40; i++) {
|
||||
if (document.getElementById('bfr-find')) break;
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
const findInp = document.getElementById('bfr-find');
|
||||
if (!findInp) return;
|
||||
findInp.value = 'quick';
|
||||
findInp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
await sleep(100);
|
||||
|
||||
const status = document.getElementById('bfr-status');
|
||||
// Should find 'quick' in at least 2 cards (c1 + c2)
|
||||
expect(status.textContent).toContain('2');
|
||||
// The list should mention both matching card titles
|
||||
const list = document.getElementById('bfr-list');
|
||||
expect(list.textContent).toContain('__bfr_match_a__');
|
||||
expect(list.textContent).toContain('__bfr_match_b__');
|
||||
// Non-matching card should NOT be in the list
|
||||
expect(list.textContent.includes('__bfr_nomatch__')).toBe(false);
|
||||
expect(document.getElementById('search-replace-row')).toBeVisible();
|
||||
click('#search-replace-toggle');
|
||||
await sleep(50);
|
||||
expect(document.getElementById('search-replace-row')?.classList.contains('hidden')).toBe(true);
|
||||
click('#search-replace-toggle');
|
||||
await sleep(50);
|
||||
expect(document.getElementById('search-replace-row')).toBeVisible();
|
||||
});
|
||||
|
||||
it('replace all updates card bodies after confirmation', async () => {
|
||||
@@ -74,29 +52,26 @@ describe('Board: Find/Replace', () => {
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
openBoardFR();
|
||||
for (let i = 0; i < 40; i++) {
|
||||
if (document.getElementById('bfr-find')) break;
|
||||
await sleep(50);
|
||||
}
|
||||
openSearchReplace();
|
||||
await sleep(200);
|
||||
|
||||
const findInp = document.getElementById('bfr-find');
|
||||
const replInp = document.getElementById('bfr-replace');
|
||||
if (!findInp || !replInp) return;
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const replInput = document.getElementById('search-replace-input');
|
||||
if (!searchInput || !replInput) return;
|
||||
|
||||
findInp.value = 'hello';
|
||||
findInp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
await sleep(50);
|
||||
replInp.value = 'goodbye';
|
||||
replInp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
searchInput.value = 'hello';
|
||||
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
await sleep(200);
|
||||
|
||||
replInput.value = 'goodbye';
|
||||
replInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
await sleep(50);
|
||||
|
||||
const applyBtn = document.getElementById('bfr-apply');
|
||||
const applyBtn = document.getElementById('search-replace-all');
|
||||
expect(applyBtn.disabled).toBe(false);
|
||||
click(applyBtn);
|
||||
await sleep(200);
|
||||
|
||||
// The confirmation dialog should appear
|
||||
const dialog = document.getElementById('dialog');
|
||||
if (dialog?.classList.contains('open')) {
|
||||
const okBtn = document.getElementById('dialog-ok');
|
||||
@@ -104,7 +79,6 @@ describe('Board: Find/Replace', () => {
|
||||
await sleep(600);
|
||||
}
|
||||
|
||||
// Verify card bodies were updated
|
||||
const updated1 = await getCard(c1.id);
|
||||
const updated2 = await getCard(c2.id);
|
||||
expect(updated1.body).toBe('goodbye world goodbye');
|
||||
@@ -116,20 +90,16 @@ describe('Board: Find/Replace', () => {
|
||||
await window._reloadAll?.();
|
||||
await sleep(200);
|
||||
|
||||
openBoardFR();
|
||||
for (let i = 0; i < 40; i++) {
|
||||
if (document.getElementById('bfr-find')) break;
|
||||
await sleep(50);
|
||||
}
|
||||
openSearchReplace();
|
||||
await sleep(200);
|
||||
|
||||
const findInp = document.getElementById('bfr-find');
|
||||
if (!findInp) return;
|
||||
findInp.value = 'some';
|
||||
findInp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
await sleep(50);
|
||||
const searchInput = document.getElementById('search-input');
|
||||
if (!searchInput) return;
|
||||
searchInput.value = 'some';
|
||||
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
await sleep(100);
|
||||
|
||||
const applyBtn = document.getElementById('bfr-apply');
|
||||
// Replace input is empty → button should be disabled
|
||||
const applyBtn = document.getElementById('search-replace-all');
|
||||
expect(applyBtn.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user