feat: add notes list filtering
This commit is contained in:
parent
7a0964892b
commit
f2e837a776
|
|
@ -22,6 +22,10 @@
|
|||
'.notes-btn:disabled{opacity:.45;cursor:default}',
|
||||
'.notes-btn.primary{background:#1a3a2a;border-color:#4ecca3;color:#4ecca3}',
|
||||
'.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-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}',
|
||||
|
|
@ -172,6 +176,8 @@
|
|||
var statusClass = '';
|
||||
var disposed = false;
|
||||
var noteActions = [];
|
||||
var filterText = '';
|
||||
var sortMode = 'title-asc';
|
||||
|
||||
function notesParent() {
|
||||
return workspaceRoot || '';
|
||||
|
|
@ -182,23 +188,26 @@
|
|||
title: titleFromFilename(entry.name),
|
||||
filename: entry.name,
|
||||
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 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) {
|
||||
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 || '');
|
||||
}).map(function (entry) {
|
||||
return noteFromEntry(parent, entry);
|
||||
}));
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (isNotFoundError(err)) return [];
|
||||
throw err;
|
||||
|
|
@ -250,8 +259,15 @@
|
|||
|
||||
var toolbar = el('div', { className: 'notes-toolbar' });
|
||||
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' });
|
||||
toolbar.appendChild(createBtn);
|
||||
toolbar.appendChild(filterInput);
|
||||
toolbar.appendChild(sortSelect);
|
||||
toolbar.appendChild(el('span', { style: { flex: '1' } }));
|
||||
toolbar.appendChild(statusEl);
|
||||
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() {
|
||||
listContainer.innerHTML = '';
|
||||
if (!notes || notes.length === 0) {
|
||||
renderEmpty('No notes yet');
|
||||
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 = [
|
||||
el('button', {
|
||||
className: 'notes-item-btn',
|
||||
|
|
@ -405,12 +436,12 @@
|
|||
});
|
||||
}
|
||||
|
||||
function renderEmpty(msg) {
|
||||
function renderEmpty(msg, hint) {
|
||||
listContainer.innerHTML = '';
|
||||
listContainer.appendChild(el('div', { className: 'notes-empty' }, [
|
||||
el('div', { innerHTML: iconSvg('note') }),
|
||||
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 ───────────────────────────────────────
|
||||
|
||||
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);
|
||||
createCancel.addEventListener('click', hideCreate);
|
||||
renameConfirm.addEventListener('click', confirmRename);
|
||||
|
|
|
|||
|
|
@ -237,6 +237,8 @@ async function mountNotes(api) {
|
|||
}
|
||||
|
||||
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 createButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-action') === 'create');
|
||||
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');
|
||||
}
|
||||
|
||||
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');
|
||||
if (!providerAction) throw new Error('provider note action button not found');
|
||||
providerAction.click();
|
||||
|
|
|
|||
Loading…
Reference in New Issue