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..737cfe64 100644 --- a/src/components/settings/GeneralSettingsSection.tsx +++ b/src/components/settings/GeneralSettingsSection.tsx @@ -15,7 +15,8 @@ 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 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: SortOrder) => { + 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..6e55775c --- /dev/null +++ b/src/lib/sorting.ts @@ -0,0 +1,57 @@ +import type { NoteMetadata, FolderNode, SortOrder } from "../types/note"; + +export interface SortStrategy { + id: SortOrder; + 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 naturalCollator = new Intl.Collator(undefined, { + numeric: true, + sensitivity: "base", +}); +const naturalCompare = (a: string, b: string) => + naturalCollator.compare(a, b) || a.localeCompare(b); + +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: SortOrder = "modified-desc"; + +export function getSortStrategy(sortOrder?: string | null): SortStrategy { + if (sortOrder && Object.hasOwn(SORT_STRATEGIES, sortOrder)) { + return SORT_STRATEGIES[sortOrder as SortOrder]; + } + return SORT_STRATEGIES[DEFAULT_SORT_ORDER]; +} diff --git a/src/types/note.ts b/src/types/note.ts index 387a9b61..09184178 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"; 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;