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: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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue