1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Desktop: Fixes #10077: Special characters in notebooks and tags are not sorted alphabetically (#10085)

Co-authored-by: Martin Dörfelt <martin.d@andix.de>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
cagnusmarlsen 2024-03-20 16:47:46 +05:30 committed by GitHub
parent ea29cf4e13
commit e9ebd845b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 70 additions and 12 deletions

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { useMemo } from 'react';
import { AppState } from '../app.reducer';
import TagItem from './TagItem';
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
@ -13,6 +14,7 @@ interface Props {
}
function TagList(props: Props) {
const collatorLocale = getCollatorLocale();
const style = useMemo(() => {
const theme = themeStyle(props.themeId);
@ -29,13 +31,13 @@ function TagList(props: Props) {
const tags = useMemo(() => {
const output = props.items.slice();
const collator = getCollator(collatorLocale);
output.sort((a: any, b: any) => {
return a.title < b.title ? -1 : +1;
return collator.compare(a.title, b.title);
});
return output;
}, [props.items]);
}, [props.items, collatorLocale]);
const tagItems = useMemo(() => {
const output = [];

View File

@ -483,6 +483,11 @@ export default class BaseApplication {
refreshNotes = true;
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'locale') {
refreshNotes = true;
doRefreshFolders = 'now';
}
if (action.type === 'SMART_FILTER_SELECT') {
refreshNotes = true;
refreshNotesUseSelectedNoteId = true;

View File

@ -2,6 +2,7 @@ import Folder from '../../models/Folder';
import BaseModel from '../../BaseModel';
import { FolderEntity, TagEntity } from '../../services/database/types';
import { getDisplayParentId, getTrashFolderId } from '../../services/trash';
import { getCollator } from '../../models/utils/getCollator';
interface Props {
folders: FolderEntity[];
@ -72,6 +73,7 @@ export const renderFolders = (props: Props, renderItem: RenderFolderItem) => {
export const renderTags = (props: Props, renderItem: RenderTagItem) => {
const tags = props.tags.slice();
const collator = getCollator();
tags.sort((a, b) => {
// It seems title can sometimes be undefined (perhaps when syncing
// and before tag has been decrypted?). It would be best to find
@ -83,7 +85,7 @@ export const renderTags = (props: Props, renderItem: RenderTagItem) => {
// Note: while newly created tags are normalized and lowercase
// imported tags might be any case, so we need to do case-insensitive
// sort.
return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : +1;
return collator.compare(a.title, b.title);
});
const tagItems = [];
const order: string[] = [];

View File

@ -223,6 +223,34 @@ describe('models/Folder', () => {
expect(sortedFolderTree[2].id).toBe(f6.id);
}));
it('should sort folders with special chars alphabetically', (async () => {
const unsortedFolderTitles = ['ç', 'd', 'c', 'Ä', 'b', 'a'].map(firstChar => `${firstChar} folder`);
for (const folderTitle of unsortedFolderTitles) {
await Folder.save({ title: folderTitle });
}
const folders = await Folder.allAsTree();
const sortedFolderTree = await Folder.sortFolderTree(folders);
// same set of titles, but in alphabetical order
const sortedFolderTitles = ['a', 'Ä', 'b', 'c', 'ç', 'd'].map(firstChar => `${firstChar} folder`);
expect(sortedFolderTree.map(f => f.title)).toEqual(sortedFolderTitles);
}));
it('should sort numbers ascending', (async () => {
const unsortedFolderTitles = ['10', '1', '2'].map(firstChar => `${firstChar} folder`);
for (const folderTitle of unsortedFolderTitles) {
await Folder.save({ title: folderTitle });
}
const folders = await Folder.allAsTree();
const sortedFolderTree = await Folder.sortFolderTree(folders);
// same set of titles, but in ascending order
const sortedFolderTitles = ['1', '2', '10'].map(firstChar => `${firstChar} folder`);
expect(sortedFolderTree.map(f => f.title)).toEqual(sortedFolderTitles);
}));
it('should not allow setting a folder parent as itself', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const hasThrown = await checkThrowAsync(() => Folder.save({ id: f1.id, parent_id: f1.id }, { userSideValidation: true }));

View File

@ -13,9 +13,11 @@ import syncDebugLog from '../services/synchronizer/syncDebugLog';
import ResourceService from '../services/ResourceService';
import { LoadOptions } from './utils/types';
import ActionLogger from '../utils/ActionLogger';
import { getTrashFolder } from '../services/trash';
import getConflictFolderId from './utils/getConflictFolderId';
import getTrashFolderId from '../services/trash/getTrashFolderId';
import { getCollator } from './utils/getCollator';
const { substrWithEllipsis } = require('../string-utils.js');
const logger = Logger.create('models/Folder');
@ -298,8 +300,18 @@ export default class Folder extends BaseItem {
return output;
}
public static handleTitleNaturalSorting(items: FolderEntity[], options: any) {
if (options.order?.length > 0 && options.order[0].by === 'title') {
const collator = getCollator();
items.sort((a, b) => ((options.order[0].dir === 'ASC') ? 1 : -1) * collator.compare(a.title, b.title));
}
}
public static async all(options: FolderLoadOptions = null) {
let output: FolderEntity[] = await super.all(options);
if (options) {
this.handleTitleNaturalSorting(output, options);
}
if (options && options.includeDeleted === false) {
output = output.filter(f => !f.deleted_time);
@ -768,9 +780,10 @@ export default class Folder extends BaseItem {
const output = folders ? folders : await this.allAsTree();
const sortFoldersAlphabetically = (folders: FolderEntityWithChildren[]) => {
const collator = getCollator();
folders.sort((a: FolderEntityWithChildren, b: FolderEntityWithChildren) => {
if (a.parent_id === b.parent_id) {
return a.title.localeCompare(b.title, undefined, { sensitivity: 'accent' });
return collator.compare(a.title, b.title);
}
return 0;
});

View File

@ -18,6 +18,7 @@ import { pull, removeElement, unique } from '../ArrayUtils';
import { LoadOptions, SaveOptions } from './utils/types';
import ActionLogger from '../utils/ActionLogger';
import { getDisplayParentId, getTrashFolderId } from '../services/trash';
import { getCollator } from './utils/getCollator';
const urlUtils = require('../urlUtils.js');
const { isImageMimeType } = require('../resourceUtils');
const { MarkupToHtml } = require('@joplin/renderer');
@ -294,8 +295,7 @@ export default class Note extends BaseItem {
return noteFieldComp(a.id, b.id);
};
const collator = this.getNaturalSortingCollator();
const collator = getCollator();
return notes.sort((a: NoteEntity, b: NoteEntity) => {
if (noteOnTop(a) && !noteOnTop(b)) return -1;
@ -1121,15 +1121,11 @@ export default class Note extends BaseItem {
public static handleTitleNaturalSorting(items: NoteEntity[], options: any) {
if (options.order.length > 0 && options.order[0].by === 'title') {
const collator = this.getNaturalSortingCollator();
const collator = getCollator();
items.sort((a, b) => ((options.order[0].dir === 'ASC') ? 1 : -1) * collator.compare(a.title, b.title));
}
}
public static getNaturalSortingCollator() {
return new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
}
public static async createConflictNote(sourceNote: NoteEntity, changeSource: number): Promise<NoteEntity> {
const conflictNote = { ...sourceNote };
delete conflictNote.id;

View File

@ -0,0 +1,12 @@
import { currentLocale, languageCodeOnly } from '../../locale';
function getCollator(locale: string = getCollatorLocale()) {
return new Intl.Collator(locale, { numeric: true, sensitivity: 'accent' });
}
function getCollatorLocale() {
const collatorLocale = languageCodeOnly(currentLocale());
return collatorLocale;
}
export { getCollator, getCollatorLocale };