Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 && Object.hasOwn(SORT_STRATEGIES, sortOrder)) {
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