Files
odysseus/static/js/features/notes/ui/toolbar.js
T
Alexandre TeixeiraandLéo f1a47f3812 refactor(notes): decompose Notes into feature-owned modules (#6713)
* refactor(notes): decompose Notes into feature-owned modules

* fix(notes): avoid HTML interpolation in reminder picker

* docs(notes): drop checkpoint archives, session logs and publication drafts

The N0-N6 tarballs and raw pytest/node logs are not read by any script or
test, and the logs carry absolute home-directory paths. Once squashed into
dev they would stay in history for good. notes-pr.md and notes-issue.md
were the drafts used to open #6712 and #6713, not documentation.

notes-validation.md keeps the counts, the checkpoint hashes and
implementation.sha256 as the record; local interpreter paths and the local
Docker image name are replaced with portable placeholders.

* fix(notes): keep the state store raw and stop masonry re-observing after close

- state: writes built from views (filter, spread, splice) stored nested
  read-only views in the store, so structuredClone on a stored note threw
  DataCloneError. Store writes now unwrap nested views.
- layout: a save that resolves after closePanel re-renders the leaving pane
  and re-created the masonry ResizeObserver that teardown had disposed.
  Skip creating it once Notes is closed.
- index.js: drop 30 imported or declared names that were never used and
  group the imports at the top. Import order is unchanged.
- Revert unrelated punctuation edits in .gitattributes and keep the header
  comment and docstring first in compare/vote.js and
  test_notes_dom_xss_helpers.py.

Both fixes have a node test that fails on the previous head.

* test(notes): make script rejection assertion case-insensitive

Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum.

---------

Co-authored-by: Léo <leograndcontact@gmail.com>
2026-10-15 09:29:41 +01:00

79 lines
3.7 KiB
JavaScript

/** Panel search, archive and list/grid controls. */
export function createToolbar({
app,
state,
persistence,
_renderNotes,
_fetchNotes,
_applyMasonry
}) {
function wireToolbar(pane) {
// Search
const searchEl = document.getElementById('notes-search');
if (searchEl) {
searchEl.addEventListener('input', () => {
app.setSearchQuery(searchEl.value.trim().toLowerCase());
_renderNotes();
});
}
// View toggle
const archiveBtn = document.getElementById('notes-archive-toggle');
if (archiveBtn) {
const ARCHIVE_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 002 2h12a2 2 0 002-2V8"/><path d="M10 12h4"/></svg><span class="notes-header-btn-label">Archive</span>';
const CLOSE_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg><span class="notes-header-btn-label">Archive</span>';
const syncArchiveBtn = () => {
archiveBtn.classList.toggle('active', state.showingArchived);
archiveBtn.title = state.showingArchived ? 'Exit archive' : 'View archive';
archiveBtn.style.opacity = state.showingArchived ? '1' : '0.8';
// Swap to an X while in archive view so it doubles as a close-back-
// to-active-notes toggle.
archiveBtn.innerHTML = state.showingArchived ? CLOSE_ICON : ARCHIVE_ICON;
// Tint the whole pane so it's obvious you're not in the active list.
pane.classList.toggle('notes-pane-archive', state.showingArchived);
};
syncArchiveBtn();
archiveBtn.addEventListener('click', async () => {
app.setShowingArchived(!state.showingArchived);
app.clearSelection();
syncArchiveBtn();
// Brief fade so the body content swap doesn't snap : the bg-tint
// change is already eased by CSS transitions on .notes-pane*.
const _bodyEl = document.querySelector('#notes-pane .notes-pane-body');
if (_bodyEl) {
_bodyEl.style.transition = 'opacity 0.18s ease';
_bodyEl.style.opacity = '0.25';
}
await _fetchNotes();
_renderNotes();
if (_bodyEl) {
requestAnimationFrame(() => {
_bodyEl.style.opacity = '';
_bodyEl.addEventListener('transitionend', () => { _bodyEl.style.transition = ''; }, { once: true });
});
}
});
}
const viewBtn = document.getElementById('notes-view-toggle');
if (viewBtn) {
pane.classList.toggle('notes-view-grid', state.viewMode === 'grid');
// Label shows what you'll switch TO : "Grid" while in list, "List" while in grid.
const _setViewLabel = () => {
const lbl = viewBtn.querySelector('.notes-header-btn-label');
if (lbl) lbl.textContent = state.viewMode === 'grid' ? 'List' : 'Grid';
};
_setViewLabel();
requestAnimationFrame(() => _applyMasonry(document.querySelector('#notes-pane .notes-pane-body')));
viewBtn.addEventListener('click', () => {
app.setViewMode(state.viewMode === 'grid' ? 'list' : 'grid');
try { persistence.setItem('odysseus-notes-view', state.viewMode); } catch {}
pane.classList.toggle('notes-view-grid', state.viewMode === 'grid');
_setViewLabel();
requestAnimationFrame(() => _applyMasonry(document.querySelector('#notes-pane .notes-pane-body')));
});
}
}
return { wireToolbar };
}