From 7a6966405cc4eca4da6b77e71fcc40a32c6357b2 Mon Sep 17 00:00:00 2001 From: Jerry Zhao Date: Thu, 7 Jan 2021 08:29:53 -0800 Subject: [PATCH] All: Support natural sorting by title (#4272) --- packages/app-cli/tests/models_Note.ts | 44 ++++++++++++++++++++ packages/app-mobile/android/app/build.gradle | 5 ++- packages/lib/models/Note.js | 29 +++++++++++-- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/app-cli/tests/models_Note.ts b/packages/app-cli/tests/models_Note.ts index d86c510bf..86c048e95 100644 --- a/packages/app-cli/tests/models_Note.ts +++ b/packages/app-cli/tests/models_Note.ts @@ -271,4 +271,48 @@ describe('models_Note', function() { expect(result).toBe(`[](:/${note1.id})`); })); + it('should perform natural sorting', (async () => { + const folder1 = await Folder.save({}); + + const sortedNotes = await Note.previews(folder1.id, { + fields: ['id', 'title'], + order: [{ by: 'title', dir: 'ASC' }], + }); + expect(sortedNotes.length).toBe(0); + + const note0 = await Note.save({ title: 'A3', parent_id: folder1.id, is_todo: false }); + const note1 = await Note.save({ title: 'A20', parent_id: folder1.id, is_todo: false }); + const note2 = await Note.save({ title: 'A100', parent_id: folder1.id, is_todo: false }); + const note3 = await Note.save({ title: 'égalité', parent_id: folder1.id, is_todo: false }); + const note4 = await Note.save({ title: 'z', parent_id: folder1.id, is_todo: false }); + + const sortedNotes2 = await Note.previews(folder1.id, { + fields: ['id', 'title'], + order: [{ by: 'title', dir: 'ASC' }], + }); + expect(sortedNotes2.length).toBe(5); + expect(sortedNotes2[0].id).toBe(note0.id); + expect(sortedNotes2[1].id).toBe(note1.id); + expect(sortedNotes2[2].id).toBe(note2.id); + expect(sortedNotes2[3].id).toBe(note3.id); + expect(sortedNotes2[4].id).toBe(note4.id); + + const todo3 = Note.changeNoteType(note3, 'todo'); + const todo4 = Note.changeNoteType(note4, 'todo'); + await Note.save(todo3); + await Note.save(todo4); + + const sortedNotes3 = await Note.previews(folder1.id, { + fields: ['id', 'title'], + order: [{ by: 'title', dir: 'ASC' }], + uncompletedTodosOnTop: true, + }); + expect(sortedNotes3.length).toBe(5); + expect(sortedNotes3[0].id).toBe(note3.id); + expect(sortedNotes3[1].id).toBe(note4.id); + expect(sortedNotes3[2].id).toBe(note0.id); + expect(sortedNotes3[3].id).toBe(note1.id); + expect(sortedNotes3[4].id).toBe(note2.id); + })); + }); diff --git a/packages/app-mobile/android/app/build.gradle b/packages/app-mobile/android/app/build.gradle index afa240423..247ed846b 100644 --- a/packages/app-mobile/android/app/build.gradle +++ b/packages/app-mobile/android/app/build.gradle @@ -109,7 +109,10 @@ def enableProguardInReleaseBuilds = false * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ -def jscFlavor = 'org.webkit:android-jsc:+' + +// We need the intl variant to support natural sorting of notes. +// https://github.com/laurent22/joplin/pull/4272 +def jscFlavor = 'org.webkit:android-jsc-intl:+' /** * Whether to enable the Hermes VM. diff --git a/packages/lib/models/Note.js b/packages/lib/models/Note.js index 79cfbe883..e0ab13181 100644 --- a/packages/lib/models/Note.js +++ b/packages/lib/models/Note.js @@ -260,6 +260,8 @@ class Note extends BaseItem { return noteFieldComp(a.id, b.id); }; + const collator = this.getNaturalSortingCollator(); + return notes.sort((a, b) => { if (noteOnTop(a) && !noteOnTop(b)) return -1; if (!noteOnTop(a) && noteOnTop(b)) return +1; @@ -272,8 +274,13 @@ class Note extends BaseItem { let bProp = b[order.by]; if (typeof aProp === 'string') aProp = aProp.toLowerCase(); if (typeof bProp === 'string') bProp = bProp.toLowerCase(); - if (aProp < bProp) r = +1; - if (aProp > bProp) r = -1; + + if (order.by === 'title') { + r = -1 * collator.compare(aProp, bProp); + } else { + if (aProp < bProp) r = +1; + if (aProp > bProp) r = -1; + } if (order.dir == 'ASC') r = -r; if (r !== 0) return r; } @@ -377,6 +384,7 @@ class Note extends BaseItem { tempOptions.conditions = cond; const uncompletedTodos = await this.search(tempOptions); + this.handleTitleNaturalSorting(uncompletedTodos, tempOptions); cond = options.conditions.slice(); if (hasNotes && hasTodos) { @@ -389,6 +397,7 @@ class Note extends BaseItem { tempOptions.conditions = cond; if ('limit' in tempOptions) tempOptions.limit -= uncompletedTodos.length; const theRest = await this.search(tempOptions); + this.handleTitleNaturalSorting(theRest, tempOptions); return uncompletedTodos.concat(theRest); } @@ -401,7 +410,10 @@ class Note extends BaseItem { options.conditions.push('is_todo = 1'); } - return this.search(options); + const results = await this.search(options); + this.handleTitleNaturalSorting(results, options); + + return results; } static preview(noteId, options = null) { @@ -862,6 +874,17 @@ class Note extends BaseItem { } } + static handleTitleNaturalSorting(items, options) { + if (options.order.length > 0 && options.order[0].by === 'title') { + const collator = this.getNaturalSortingCollator(); + items.sort((a, b) => ((options.order[0].dir === 'ASC') ? 1 : -1) * collator.compare(a.title, b.title)); + } + } + + static getNaturalSortingCollator() { + return new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); + } + } Note.updateGeolocationEnabled_ = true;