feat: add notes list filtering

This commit is contained in:
mirivlad 2026-06-28 16:45:43 +08:00
parent 7a0964892b
commit f2e837a776
2 changed files with 75 additions and 8 deletions

View File

@ -22,6 +22,10 @@
'.notes-btn:disabled{opacity:.45;cursor:default}', '.notes-btn:disabled{opacity:.45;cursor:default}',
'.notes-btn.primary{background:#1a3a2a;border-color:#4ecca3;color:#4ecca3}', '.notes-btn.primary{background:#1a3a2a;border-color:#4ecca3;color:#4ecca3}',
'.notes-btn.primary:hover{background:#2a4a3a}', '.notes-btn.primary:hover{background:#2a4a3a}',
'.notes-filter,.notes-sort{font-size:.78rem;padding:.32rem .5rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none}',
'.notes-filter{width:11rem}',
'.notes-sort{width:8rem}',
'.notes-filter:focus,.notes-sort:focus{border-color:#4ecca3}',
'.notes-list{flex:1;overflow:auto;min-height:0}', '.notes-list{flex:1;overflow:auto;min-height:0}',
'.notes-item{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;border-bottom:1px solid rgba(22,33,62,.55);cursor:pointer;font-size:.85rem}', '.notes-item{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;border-bottom:1px solid rgba(22,33,62,.55);cursor:pointer;font-size:.85rem}',
'.notes-item:hover{background:#17172d}', '.notes-item:hover{background:#17172d}',
@ -172,6 +176,8 @@
var statusClass = ''; var statusClass = '';
var disposed = false; var disposed = false;
var noteActions = []; var noteActions = [];
var filterText = '';
var sortMode = 'title-asc';
function notesParent() { function notesParent() {
return workspaceRoot || ''; return workspaceRoot || '';
@ -182,23 +188,26 @@
title: titleFromFilename(entry.name), title: titleFromFilename(entry.name),
filename: entry.name, filename: entry.name,
path: entry.relativePath, path: entry.relativePath,
parentPath: cleanPath(parent) parentPath: cleanPath(parent),
modifiedAt: entry.modifiedAt || ''
}; };
} }
function sortNotes(list) { function sortNotes(list, mode) {
return list.sort(function (a, b) { return list.sort(function (a, b) {
return String(a.title || '').toLowerCase().localeCompare(String(b.title || '').toLowerCase()); var byTitle = String(a.title || '').toLowerCase().localeCompare(String(b.title || '').toLowerCase());
if (mode === 'title-desc') return -byTitle;
return byTitle;
}); });
} }
function listNotes(parent) { function listNotes(parent) {
return api.files.list(notesFolderPath(parent)).then(function (entries) { return api.files.list(notesFolderPath(parent)).then(function (entries) {
return sortNotes((entries || []).filter(function (entry) { return (entries || []).filter(function (entry) {
return entry.type === 'file' && /\.(md|markdown)$/i.test(entry.name || ''); return entry.type === 'file' && /\.(md|markdown)$/i.test(entry.name || '');
}).map(function (entry) { }).map(function (entry) {
return noteFromEntry(parent, entry); return noteFromEntry(parent, entry);
})); });
}).catch(function (err) { }).catch(function (err) {
if (isNotFoundError(err)) return []; if (isNotFoundError(err)) return [];
throw err; throw err;
@ -250,8 +259,15 @@
var toolbar = el('div', { className: 'notes-toolbar' }); var toolbar = el('div', { className: 'notes-toolbar' });
var createBtn = el('button', { className: 'notes-btn', 'data-action': 'create', innerHTML: iconSvg('add') + ' New Note' }); var createBtn = el('button', { className: 'notes-btn', 'data-action': 'create', innerHTML: iconSvg('add') + ' New Note' });
var filterInput = el('input', { className: 'notes-filter', 'data-notes-filter': '', placeholder: 'Filter notes' });
var sortSelect = el('select', { className: 'notes-sort', 'data-notes-sort': '' }, [
el('option', { value: 'title-asc' }, ['A-Z']),
el('option', { value: 'title-desc' }, ['Z-A'])
]);
var statusEl = el('span', { className: 'notes-status' }); var statusEl = el('span', { className: 'notes-status' });
toolbar.appendChild(createBtn); toolbar.appendChild(createBtn);
toolbar.appendChild(filterInput);
toolbar.appendChild(sortSelect);
toolbar.appendChild(el('span', { style: { flex: '1' } })); toolbar.appendChild(el('span', { style: { flex: '1' } }));
toolbar.appendChild(statusEl); toolbar.appendChild(statusEl);
containerEl.appendChild(toolbar); containerEl.appendChild(toolbar);
@ -351,13 +367,28 @@
}); });
} }
function visibleNotes() {
var q = filterText.trim().toLowerCase();
var filtered = (notes || []).filter(function (note) {
if (!q) return true;
return String(note.title || '').toLowerCase().indexOf(q) !== -1 ||
String(note.path || '').toLowerCase().indexOf(q) !== -1;
});
return sortNotes(filtered, sortMode);
}
function renderList() { function renderList() {
listContainer.innerHTML = ''; listContainer.innerHTML = '';
if (!notes || notes.length === 0) { if (!notes || notes.length === 0) {
renderEmpty('No notes yet'); renderEmpty('No notes yet');
return; return;
} }
notes.forEach(function (note) { var shown = visibleNotes();
if (shown.length === 0) {
renderEmpty('No matching notes', 'Clear the filter to show all notes');
return;
}
shown.forEach(function (note) {
var actionButtons = [ var actionButtons = [
el('button', { el('button', {
className: 'notes-item-btn', className: 'notes-item-btn',
@ -405,12 +436,12 @@
}); });
} }
function renderEmpty(msg) { function renderEmpty(msg, hint) {
listContainer.innerHTML = ''; listContainer.innerHTML = '';
listContainer.appendChild(el('div', { className: 'notes-empty' }, [ listContainer.appendChild(el('div', { className: 'notes-empty' }, [
el('div', { innerHTML: iconSvg('note') }), el('div', { innerHTML: iconSvg('note') }),
el('div', {}, [msg]), el('div', {}, [msg]),
el('div', { className: 'notes-empty-hint' }, ['Click "New Note" to create one']) el('div', { className: 'notes-empty-hint' }, [hint || 'Click "New Note" to create one'])
])); ]));
} }
@ -581,6 +612,14 @@
// ─── Event Wiring ─────────────────────────────────────── // ─── Event Wiring ───────────────────────────────────────
createBtn.addEventListener('click', showCreate); createBtn.addEventListener('click', showCreate);
filterInput.addEventListener('input', function () {
filterText = filterInput.value;
renderList();
});
sortSelect.addEventListener('change', function () {
sortMode = sortSelect.value || 'title-asc';
renderList();
});
createConfirm.addEventListener('click', confirmCreate); createConfirm.addEventListener('click', confirmCreate);
createCancel.addEventListener('click', hideCreate); createCancel.addEventListener('click', hideCreate);
renameConfirm.addEventListener('click', confirmRename); renameConfirm.addEventListener('click', confirmRename);

View File

@ -237,6 +237,8 @@ async function mountNotes(api) {
} }
const createApi = makeApi({ metadataAlwaysExists: true }); const createApi = makeApi({ metadataAlwaysExists: true });
createApi.entries.set('Project/Notes', { type: 'folder' });
createApi.entries.set('Project/Notes/Second_Note.md', { type: 'file', content: '# Second Note\n' });
const { container, document } = await mountNotes(createApi); const { container, document } = await mountNotes(createApi);
const createButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-action') === 'create'); const createButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-action') === 'create');
if (!createButton) throw new Error('create button not found'); if (!createButton) throw new Error('create button not found');
@ -260,6 +262,32 @@ async function mountNotes(api) {
throw new Error('create note did not open the newly created file'); throw new Error('create note did not open the newly created file');
} }
const filterInput = walk(container, (node) => node.getAttribute && node.getAttribute('data-notes-filter') !== undefined);
if (!filterInput) throw new Error('notes filter input not found');
filterInput.value = 'second';
filterInput.dispatchEvent('input');
if (!container.textContent.includes('Second Note') || container.textContent.includes('First Note')) {
throw new Error(`notes filter did not narrow the rendered list: ${container.textContent}`);
}
filterInput.value = 'missing';
filterInput.dispatchEvent('input');
if (!container.textContent.includes('No matching notes')) {
throw new Error('notes filter empty state not shown');
}
filterInput.value = '';
filterInput.dispatchEvent('input');
const sortSelect = walk(container, (node) => node.getAttribute && node.getAttribute('data-notes-sort') !== undefined);
if (!sortSelect) throw new Error('notes sort select not found');
sortSelect.value = 'title-desc';
sortSelect.dispatchEvent('change');
const renderedText = container.textContent;
if (renderedText.indexOf('Second Note') === -1 || renderedText.indexOf('First Note') === -1 || renderedText.indexOf('Second Note') > renderedText.indexOf('First Note')) {
throw new Error(`notes descending sort order is wrong: ${renderedText}`);
}
sortSelect.value = 'title-asc';
sortSelect.dispatchEvent('change');
const providerAction = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-contribution-action') === 'provider.note.action'); const providerAction = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-contribution-action') === 'provider.note.action');
if (!providerAction) throw new Error('provider note action button not found'); if (!providerAction) throw new Error('provider note action button not found');
providerAction.click(); providerAction.click();