Skip to content

Support multi-attribute note sorting#10060

Open
Rajesh270712 wants to merge 1 commit into
TriliumNext:mainfrom
Rajesh270712:reputation/bou-380-multi-attribute-sorting
Open

Support multi-attribute note sorting#10060
Rajesh270712 wants to merge 1 commit into
TriliumNext:mainfrom
Rajesh270712:reputation/bou-380-multi-attribute-sorting

Conversation

@Rajesh270712

Copy link
Copy Markdown

Summary

Adds multi-attribute sorting support for child notes when #sorted contains a comma-separated sequence such as dueDate:asc,priority:desc.

Linked Issue

Related to #6829.

Problem

The current note tree sorting path only uses one sorted attribute, so ties on the first attribute fall through to the existing fallback ordering instead of applying additional user-provided sort keys.

Fix

  • Parse multiple #sorted entries with optional :asc / :desc direction.
  • Compare child notes by each configured attribute in order before falling back to the existing behavior.
  • Add focused tree service specs for multi-key ordering, descending secondary sort, and malformed sort direction fallback.

Validation

  • git diff --check origin/main...HEAD
  • npx --yes pnpm@11.5.0 --filter server test ../../packages/trilium-core/src/services/tree.spec.ts
  • npx --yes pnpm@11.5.0 typecheck
  • npx --yes pnpm@11.5.0 --filter server build

The server build completed with existing bundler warnings outside this patch surface.

Risk Notes

  • Scope is limited to packages/trilium-core/src/services/tree.ts and packages/trilium-core/src/services/tree.spec.ts.
  • #top, #bottom, natural sorting, and existing fallback ordering are preserved.
  • I did not change user-facing docs because this repo appears to route documentation edits through the separate edit-docs workflow; I can update that path if maintainers prefer.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 5, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for sorting notes by multiple attributes and explicit sort directions (e.g., 'attribute:asc' or 'attribute:desc') in tree.ts, accompanied by comprehensive unit tests in tree.spec.ts. The sorting logic is refactored to parse multiple criteria, apply sorting directions directly within the comparator, and simplify the pinning logic for 'top' and 'bottom' notes. Feedback on the changes identifies a potential issue in the new compareSortValues helper, where equal values could return a non-zero result when sortNatural is false, violating the comparator contract; a code suggestion is provided to add an explicit equality check.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +166 to +168
function compareSortValues(a: string, b: string, reverse = false) {
return compare(a, b) * (reverse ? -1 : 1);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When sortNatural is false, the underlying compare function returns 1 for equal values (since a < b is false). When sorting by the fallback title, if two notes have the exact same title, compareSortValues will be called with equal values and return a non-zero result (either 1 or -1 depending on reverse). This violates the comparator contract (which requires returning 0 for equal elements) and can lead to unstable sorting or unpredictable behavior in some JavaScript engines. Adding an explicit equality check to return 0 prevents this issue.

Suggested change
function compareSortValues(a: string, b: string, reverse = false) {
return compare(a, b) * (reverse ? -1 : 1);
}
function compareSortValues(a: string, b: string, reverse = false) {
if (a === b) return 0;
return compare(a, b) * (reverse ? -1 : 1);
}

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds multi-attribute note sorting by extending the #sorted label to accept a comma-separated list of attribute names with optional :asc/:desc direction suffixes (e.g. dueDate:asc,priority:desc). It also removes the end-of-sort notes.reverse() call in favour of applying direction inline per criterion, and cleans up the #top/#bottom pinning logic so it is no longer entangled with the global reverse flag.

  • parseSortCriteria() splits the #sorted value, recognises :asc/:desc suffixes via lastIndexOf(":"), and falls back to the global #sortDirection for criteria without an explicit direction.
  • The comparator now loops over all criteria in order, returning on the first difference; a final title tiebreaker runs after the loop, but its direction is governed by hasExplicitDirection (true if any criterion used explicit direction) rather than tracking per-criterion state, which can silently ignore #sortDirection in mixed-direction configurations.
  • Two of the three spec cases mentioned in the PR description are present; the "malformed sort direction fallback" test is missing.

Confidence Score: 4/5

Safe to merge with minor follow-up work; core sorting logic and all existing tests are preserved, and the new multi-attribute path is straightforward.

The refactor correctly replaces the notes.reverse() end-flip with inline per-criterion direction, and the #top/#bottom pinning cleanup is a genuine improvement. The main risk is the title fallback direction: when any criterion carries an explicit :asc/:desc the post-loop fallback ignores the global #sortDirection, which could silently change sort order for users who combine the two. The missing malformed-direction test means that edge case is unverified. Neither issue corrupts data or breaks existing single-attribute sorting.

packages/trilium-core/src/services/tree.ts — the hasExplicitDirection fallback logic; packages/trilium-core/src/services/tree.spec.ts — the missing malformed-direction test case.

Important Files Changed

Filename Overview
packages/trilium-core/src/services/tree.ts Adds parseSortCriteria() to parse comma-separated sort keys with optional :asc/:desc suffixes; refactors sortNotes() to iterate criteria in order, removes the trailing notes.reverse() in favour of per-criterion direction, and fixes #top/#bottom to be unconditionally pinned regardless of the global reverse flag.
packages/trilium-core/src/services/tree.spec.ts Adds two focused tests for multi-attribute sorting and per-attribute explicit direction. The third test case mentioned in the PR description—malformed sort direction fallback—is absent.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["sortNotesIfNeeded()"] --> B{Has #sorted?}
    B -- No/off --> Z[Return]
    B -- Yes --> D["sortNotes(..., reverse)"]
    D --> E["parseSortCriteria(customSortBy, reverse)"]
    E --> G{":asc/:desc suffix?"}
    G -- Yes --> H["criterion.reverse = explicit dir\nhasExplicitDirection=true"]
    G -- No --> I["criterion.reverse = global reverse"]
    H & I --> L["notes.sort(comparator)"]
    L --> M{#top check} --> N["#top always first"]
    L --> O{#bottom check} --> P["#bottom always last"]
    L --> S["for each criterion"]
    S --> T{values differ?}
    T -- Yes --> U["compareSortValues(a,b,criterion.reverse)"]
    T -- No --> S
    S -- all equal --> V["Title fallback:\nhasExplicitDirection ? false : reverse"]
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "Support multi-attribute note sorting" | Re-trigger Greptile

Comment on lines +97 to +131
it("sorts notes by multiple attributes in order", () => {
const note = buildNote({
children: [
{title: "first", "#priority": "1", "#dueDate": "2026-05-20"},
{title: "third", "#priority": "2", "#dueDate": "2026-05-01"},
{title: "second", "#priority": "1", "#dueDate": "2026-05-10"}
],
"#sorted": "priority,dueDate"
});

getContext().init(() => {
tree.sortNotesIfNeeded(note.noteId);
});

const orderedTitles = note.children.map((child) => child.title);
expect(orderedTitles).toStrictEqual(["second", "first", "third"]);
});

it("supports explicit sort direction for each sorted attribute", () => {
const note = buildNote({
children: [
{title: "low-priority", "#priority": "1", "#dueDate": "2026-05-01"},
{title: "next-date", "#priority": "2", "#dueDate": "2026-05-02"},
{title: "high-priority", "#priority": "3", "#dueDate": "2026-05-01"}
],
"#sorted": "dueDate:asc,priority:desc"
});

getContext().init(() => {
tree.sortNotesIfNeeded(note.noteId);
});

const orderedTitles = note.children.map((child) => child.title);
expect(orderedTitles).toStrictEqual(["high-priority", "low-priority", "next-date"]);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing "malformed sort direction fallback" test

The PR description explicitly lists "malformed sort direction fallback" as one of three new spec cases, but only two tests were added. The missing test would cover the case where #sorted = "priority:typo" (invalid direction) — with the current logic, rawDirection = "typo" fails the hasExplicitDirection check, so the entire string "priority:typo" becomes the attribute key, not "priority". Since that attribute doesn't exist, sorting silently falls back to title. A test case would make this documented, intentional behaviour explicit and guard against future regressions.

Fix in Claude Code

Comment on lines 119 to +122
}

const sortCriteria = parseSortCriteria(customSortBy, reverse);
const hasExplicitDirection = sortCriteria.some((criterion) => criterion.hasExplicitDirection);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Title fallback direction is surprising when explicit and non-explicit criteria are mixed

hasExplicitDirection is true if any criterion carries :asc/:desc, and controls the post-loop title fallback: compareSortValues(titleAEl, titleBEl, hasExplicitDirection ? false : reverse). When a note has #sorted = "dueDate:asc" (one explicit) and #sortDirection = "desc" (global reverse), the final title-based tiebreaker is sorted ascending — the user's #sortDirection is silently ignored for the fallback. Users who previously relied on #sorted = "dueDate" + #sortDirection = "desc" to get a descending-dueDate, descending-title-fallback order and switch to the new "dueDate:asc" spelling will get an ascending title fallback instead.

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant