From 4b7ca3cde31a9aecf84b92adab401e67075b13b2 Mon Sep 17 00:00:00 2001 From: DevHolako Date: Wed, 26 Aug 2026 12:13:56 +0100 Subject: [PATCH 1/3] feat: add configurable sidebar note sorting to settings --- src-tauri/src/lib.rs | 2 + src/components/notes/FolderTreeView.tsx | 6 +- src/components/notes/NoteList.tsx | 13 +++- .../settings/GeneralSettingsSection.tsx | 65 +++++++++++++++++++ src/lib/folderTree.ts | 16 +++-- src/lib/sorting.ts | 53 +++++++++++++++ src/types/note.ts | 2 + 7 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 src/lib/sorting.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 72b6d15d..ebc1bb41 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -131,6 +131,8 @@ pub struct Settings { pub folders_enabled: Option, #[serde(rename = "ignoredPatterns")] pub ignored_patterns: Option>, + #[serde(rename = "sortOrder")] + pub sort_order: Option, #[serde(rename = "customColorsLight")] pub custom_colors_light: Option>, #[serde(rename = "customColorsDark")] diff --git a/src/components/notes/FolderTreeView.tsx b/src/components/notes/FolderTreeView.tsx index 3f7998a3..a642e11d 100644 --- a/src/components/notes/FolderTreeView.tsx +++ b/src/components/notes/FolderTreeView.tsx @@ -484,7 +484,7 @@ interface FolderTreeViewProps { export function FolderTreeView({ pinnedIds, - settings: _settings, + settings, multiSelectedNoteIds, setMultiSelectedNoteIds, lastClickedNoteId, @@ -535,8 +535,8 @@ export function FolderTreeView({ }, [collapsedFolders]); const tree = useMemo( - () => buildFolderTree(notes, pinnedIds, knownFolders), - [notes, pinnedIds, knownFolders], + () => buildFolderTree(notes, pinnedIds, knownFolders, settings?.sortOrder), + [notes, pinnedIds, knownFolders, settings?.sortOrder], ); const handleToggleCollapse = useCallback((path: string) => { diff --git a/src/components/notes/NoteList.tsx b/src/components/notes/NoteList.tsx index 3e761f39..e56e8b87 100644 --- a/src/components/notes/NoteList.tsx +++ b/src/components/notes/NoteList.tsx @@ -22,6 +22,7 @@ import { TrashIcon, } from "../icons"; import type { Settings } from "../../types/note"; +import { getSortStrategy } from "../../lib/sorting"; const menuItemClass = "px-3 py-1.5 text-sm text-text cursor-pointer outline-none hover:bg-bg-muted focus:bg-bg-muted flex items-center gap-2 rounded-sm"; @@ -304,8 +305,16 @@ export function NoteList({ modified: r.modified, })); } - return notes; - }, [searchQuery, searchResults, notes]); + const strategy = getSortStrategy(settings?.sortOrder); + const sorted = [...notes]; + sorted.sort((a, b) => { + const ap = pinnedIds.has(a.id); + const bp = pinnedIds.has(b.id); + if (ap !== bp) return ap ? -1 : 1; + return strategy.compareNotes(a, b); + }); + return sorted; + }, [searchQuery, searchResults, notes, settings?.sortOrder, pinnedIds]); // Listen for focus request from editor (when Escape is pressed) useEffect(() => { diff --git a/src/components/settings/GeneralSettingsSection.tsx b/src/components/settings/GeneralSettingsSection.tsx index 320657fa..052e44f6 100644 --- a/src/components/settings/GeneralSettingsSection.tsx +++ b/src/components/settings/GeneralSettingsSection.tsx @@ -16,6 +16,7 @@ import { XIcon, } from "../icons"; import type { Settings } from "../../types/note"; +import { SORT_STRATEGIES, DEFAULT_SORT_ORDER } from "../../lib/sorting"; // Format remote URL for display - extract user/repo from full URL function formatRemoteUrl(url: string | null): string { @@ -306,6 +307,22 @@ export function GeneralSettingsSection() { {/* Divider */}
+ {/* Note Sorting Section */} +
+
+
+

Sidebar Note Order

+

+ Choose how notes and folder contents are ordered in the sidebar. +

+
+ +
+
+ + {/* Divider */} +
+ {/* Git Section */}
@@ -815,6 +832,54 @@ function FoldersToggle() { ); } +function NoteSortSelector() { + const [sortOrder, setSortOrder] = useState(DEFAULT_SORT_ORDER); + const [isUpdating, setIsUpdating] = useState(false); + const { notesFolder, refreshNotes } = useNotes(); + + useEffect(() => { + invoke("get_settings") + .then((settings) => { + setSortOrder(settings.sortOrder || DEFAULT_SORT_ORDER); + }) + .catch((err) => { + console.error("Failed to load sortOrder:", err); + }); + }, [notesFolder]); + + const handleChange = async (newOrder: string) => { + setIsUpdating(true); + try { + const settings = await invoke("get_settings"); + await invoke("update_settings", { + newSettings: { ...settings, sortOrder: newOrder }, + }); + setSortOrder(newOrder); + refreshNotes(); + toast.success("Sidebar sorting updated"); + } catch { + toast.error("Failed to update sidebar sorting"); + } finally { + setIsUpdating(false); + } + }; + + return ( + + ); +} + function IgnoredFoldersEditor() { const [patterns, setPatterns] = useState(null); const [defaults, setDefaults] = useState([]); diff --git a/src/lib/folderTree.ts b/src/lib/folderTree.ts index a4a2d48d..bd6f78a0 100644 --- a/src/lib/folderTree.ts +++ b/src/lib/folderTree.ts @@ -1,4 +1,5 @@ import type { NoteMetadata, FolderNode } from "../types/note"; +import { getSortStrategy } from "./sorting"; export interface FolderTreeData { rootNotes: NoteMetadata[]; @@ -9,7 +10,9 @@ export function buildFolderTree( notes: NoteMetadata[], pinnedIds: Set, knownFolders?: string[], + sortOrder?: string, ): FolderTreeData { + const strategy = getSortStrategy(sortOrder); const rootNotes: NoteMetadata[] = []; const folderMap = new Map(); @@ -51,13 +54,16 @@ export function buildFolderTree( } } + const folderComparator = + strategy.compareFolders || ((a: FolderNode, b: FolderNode) => a.name.localeCompare(b.name)); + function sortNode(node: FolderNode) { - node.children.sort((a, b) => a.name.localeCompare(b.name)); + node.children.sort(folderComparator); node.notes.sort((a, b) => { const ap = pinnedIds.has(a.id); const bp = pinnedIds.has(b.id); if (ap !== bp) return ap ? -1 : 1; - return b.modified - a.modified; + return strategy.compareNotes(a, b); }); node.children.forEach(sortNode); } @@ -65,15 +71,15 @@ export function buildFolderTree( const topLevelFolders = Array.from(folderMap.values()).filter( (f) => !f.path.includes("/"), ); - topLevelFolders.sort((a, b) => a.name.localeCompare(b.name)); + topLevelFolders.sort(folderComparator); topLevelFolders.forEach(sortNode); - // Sort root notes: pinned first, then by modified desc + // Sort root notes: pinned first, then by strategy rootNotes.sort((a, b) => { const ap = pinnedIds.has(a.id); const bp = pinnedIds.has(b.id); if (ap !== bp) return ap ? -1 : 1; - return b.modified - a.modified; + return strategy.compareNotes(a, b); }); return { rootNotes, folders: topLevelFolders }; diff --git a/src/lib/sorting.ts b/src/lib/sorting.ts new file mode 100644 index 00000000..2c34aceb --- /dev/null +++ b/src/lib/sorting.ts @@ -0,0 +1,53 @@ +import type { NoteMetadata, FolderNode } from "../types/note"; + +export interface SortStrategy { + id: string; + label: string; + description: string; + compareNotes: (a: NoteMetadata, b: NoteMetadata) => number; + compareFolders?: (a: FolderNode, b: FolderNode) => number; +} + +// Natural collation for human-friendly sorting (e.g. "1 - Note", "2 - Note", "10 - Note") +const naturalCompare = (a: string, b: string) => + a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }); + +export const SORT_STRATEGIES: Record = { + "title-asc": { + id: "title-asc", + label: "Alphabetical / Folder Structure (A–Z)", + description: "Sorted by note title and natural number order", + compareNotes: (a, b) => naturalCompare(a.title || a.id, b.title || b.id), + compareFolders: (a, b) => naturalCompare(a.name, b.name), + }, + "modified-desc": { + id: "modified-desc", + label: "Last Modified (Newest First)", + description: "Recently edited notes appear at the top", + compareNotes: (a, b) => b.modified - a.modified, + compareFolders: (a, b) => naturalCompare(a.name, b.name), + }, + "modified-asc": { + id: "modified-asc", + label: "Last Modified (Oldest First)", + description: "Oldest edited notes appear at the top", + compareNotes: (a, b) => a.modified - b.modified, + compareFolders: (a, b) => naturalCompare(a.name, b.name), + }, + "title-desc": { + id: "title-desc", + label: "Alphabetical (Z–A)", + description: "Reverse alphabetical order", + compareNotes: (a, b) => naturalCompare(b.title || b.id, a.title || a.id), + compareFolders: (a, b) => naturalCompare(b.name, a.name), + }, +}; + +export const DEFAULT_SORT_ORDER = "title-asc"; + +export function getSortStrategy(sortOrder?: string): SortStrategy { + if (sortOrder && SORT_STRATEGIES[sortOrder]) { + return SORT_STRATEGIES[sortOrder]; + } + return SORT_STRATEGIES[DEFAULT_SORT_ORDER]; +} diff --git a/src/types/note.ts b/src/types/note.ts index 387a9b61..6a2dc7e7 100644 --- a/src/types/note.ts +++ b/src/types/note.ts @@ -20,6 +20,7 @@ export interface ThemeSettings { export type FontFamily = "system-sans" | "serif" | "monospace"; export type TextDirection = "auto" | "ltr" | "rtl"; export type EditorWidth = "narrow" | "normal" | "wide" | "full" | "custom"; +export type SortOrder = "title-asc" | "title-desc" | "modified-desc" | "modified-asc" | string; export interface EditorFontSettings { baseFontFamily?: FontFamily; @@ -50,6 +51,7 @@ export interface Settings { gitEnabled?: boolean; foldersEnabled?: boolean; pinnedNoteIds?: string[]; + sortOrder?: SortOrder; textDirection?: TextDirection; editorWidth?: EditorWidth; customEditorWidthPx?: number; From bcda70f0b1a56e735d4f579df3bcd87ddcde58fc Mon Sep 17 00:00:00 2001 From: DevHolako Date: Wed, 26 Aug 2026 12:33:07 +0100 Subject: [PATCH 2/3] fix(sorting): address PR review comments for defaults, collation, and types --- .../settings/GeneralSettingsSection.tsx | 8 ++++---- src/lib/sorting.ts | 20 +++++++++++-------- src/types/note.ts | 2 +- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/settings/GeneralSettingsSection.tsx b/src/components/settings/GeneralSettingsSection.tsx index 052e44f6..737cfe64 100644 --- a/src/components/settings/GeneralSettingsSection.tsx +++ b/src/components/settings/GeneralSettingsSection.tsx @@ -15,7 +15,7 @@ import { ChevronRightIcon, XIcon, } from "../icons"; -import type { Settings } from "../../types/note"; +import type { Settings, SortOrder } from "../../types/note"; import { SORT_STRATEGIES, DEFAULT_SORT_ORDER } from "../../lib/sorting"; // Format remote URL for display - extract user/repo from full URL @@ -833,7 +833,7 @@ function FoldersToggle() { } function NoteSortSelector() { - const [sortOrder, setSortOrder] = useState(DEFAULT_SORT_ORDER); + const [sortOrder, setSortOrder] = useState(DEFAULT_SORT_ORDER); const [isUpdating, setIsUpdating] = useState(false); const { notesFolder, refreshNotes } = useNotes(); @@ -847,7 +847,7 @@ function NoteSortSelector() { }); }, [notesFolder]); - const handleChange = async (newOrder: string) => { + const handleChange = async (newOrder: SortOrder) => { setIsUpdating(true); try { const settings = await invoke("get_settings"); @@ -867,7 +867,7 @@ function NoteSortSelector() { return (