Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ pub struct Settings {
pub folders_enabled: Option<bool>,
#[serde(rename = "ignoredPatterns")]
pub ignored_patterns: Option<Vec<String>>,
#[serde(rename = "sortOrder")]
pub sort_order: Option<String>,
#[serde(rename = "customColorsLight")]
pub custom_colors_light: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "customColorsDark")]
Expand Down
6 changes: 3 additions & 3 deletions src/components/notes/FolderTreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ interface FolderTreeViewProps {

export function FolderTreeView({
pinnedIds,
settings: _settings,
settings,
multiSelectedNoteIds,
setMultiSelectedNoteIds,
lastClickedNoteId,
Expand Down Expand Up @@ -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) => {
Expand Down
13 changes: 11 additions & 2 deletions src/components/notes/NoteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(() => {
Expand Down
67 changes: 66 additions & 1 deletion src/components/settings/GeneralSettingsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -306,6 +307,22 @@ export function GeneralSettingsSection() {
{/* Divider */}
<div className="border-t border-border border-dashed" />

{/* Note Sorting Section */}
<section className="pb-2">
<div className="flex items-center justify-between gap-6">
<div className="flex flex-col gap-0.75">
<h2 className="text-xl font-medium">Sidebar Note Order</h2>
<p className="text-sm text-text-muted max-w-lg">
Choose how notes and folder contents are ordered in the sidebar.
</p>
</div>
<NoteSortSelector />
Comment thread
DevHolako marked this conversation as resolved.
</div>
</section>

{/* Divider */}
<div className="border-t border-border border-dashed" />

{/* Git Section */}
<section className="pb-2 flex flex-col gap-4">
<div className="flex items-center justify-between gap-6">
Expand Down Expand Up @@ -815,6 +832,54 @@ function FoldersToggle() {
);
}

function NoteSortSelector() {
const [sortOrder, setSortOrder] = useState<SortOrder>(DEFAULT_SORT_ORDER);
const [isUpdating, setIsUpdating] = useState(false);
const { notesFolder, refreshNotes } = useNotes();

useEffect(() => {
invoke<Settings>("get_settings")
.then((settings) => {
setSortOrder(settings.sortOrder || DEFAULT_SORT_ORDER);
})
.catch((err) => {
console.error("Failed to load sortOrder:", err);
});
}, [notesFolder]);
Comment thread
DevHolako marked this conversation as resolved.

const handleChange = async (newOrder: SortOrder) => {
setIsUpdating(true);
try {
const settings = await invoke<Settings>("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 (
<select
value={sortOrder}
onChange={(e) => handleChange(e.target.value as SortOrder)}
disabled={isUpdating}
className="px-3 py-1.5 text-sm bg-bg border border-border rounded-[10px] text-text cursor-pointer focus:outline-none focus:ring-1 focus:ring-accent shrink-0"
>
{Object.values(SORT_STRATEGIES).map((strategy) => (
<option key={strategy.id} value={strategy.id}>
{strategy.label}
</option>
))}
</select>
);
}

function IgnoredFoldersEditor() {
const [patterns, setPatterns] = useState<string[] | null>(null);
const [defaults, setDefaults] = useState<string[]>([]);
Expand Down
16 changes: 11 additions & 5 deletions src/lib/folderTree.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NoteMetadata, FolderNode } from "../types/note";
import { getSortStrategy } from "./sorting";

export interface FolderTreeData {
rootNotes: NoteMetadata[];
Expand All @@ -9,7 +10,9 @@ export function buildFolderTree(
notes: NoteMetadata[],
pinnedIds: Set<string>,
knownFolders?: string[],
sortOrder?: string,
): FolderTreeData {
const strategy = getSortStrategy(sortOrder);
const rootNotes: NoteMetadata[] = [];
const folderMap = new Map<string, FolderNode>();

Expand Down Expand Up @@ -51,29 +54,32 @@ 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);
}

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 };
Expand Down
57 changes: 57 additions & 0 deletions src/lib/sorting.ts
Original file line number Diff line number Diff line change
@@ -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<SortOrder, SortStrategy> = {
"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 && (sortOrder as SortOrder) in SORT_STRATEGIES) {
return SORT_STRATEGIES[sortOrder as SortOrder];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return SORT_STRATEGIES[DEFAULT_SORT_ORDER];
}
2 changes: 2 additions & 0 deletions src/types/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,6 +51,7 @@ export interface Settings {
gitEnabled?: boolean;
foldersEnabled?: boolean;
pinnedNoteIds?: string[];
sortOrder?: SortOrder;
textDirection?: TextDirection;
editorWidth?: EditorWidth;
customEditorWidthPx?: number;
Expand Down