1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-13 22:12:50 +02:00

Desktop: Resolves #206: Added support for sorting notebooks by title or last modified

This commit is contained in:
Laurent Cozic
2019-03-02 17:35:57 +00:00
parent 2256b0c5ec
commit 833d473268
8 changed files with 204 additions and 25 deletions

View File

@@ -52,4 +52,53 @@ describe('models_Folder', function() {
expect(all.length).toBe(0);
}));
it('should sort by last modified, based on content', asyncTest(async () => {
let folders;
let f1 = await Folder.save({ title: "folder1" }); await sleep(0.1);
let f2 = await Folder.save({ title: "folder2" }); await sleep(0.1);
let f3 = await Folder.save({ title: "folder3" }); await sleep(0.1);
let n1 = await Note.save({ title: 'note1', parent_id: f2.id });
folders = await Folder.orderByLastModified(await Folder.all(), 'desc');
expect(folders.length).toBe(3);
expect(folders[0].id).toBe(f2.id);
expect(folders[1].id).toBe(f3.id);
expect(folders[2].id).toBe(f1.id);
let n2 = await Note.save({ title: 'note1', parent_id: f1.id });
folders = await Folder.orderByLastModified(await Folder.all(), 'desc');
expect(folders[0].id).toBe(f1.id);
expect(folders[1].id).toBe(f2.id);
expect(folders[2].id).toBe(f3.id);
await Note.save({ id: n1.id, title: 'note1 mod' });
folders = await Folder.orderByLastModified(await Folder.all(), 'desc');
expect(folders[0].id).toBe(f2.id);
expect(folders[1].id).toBe(f1.id);
expect(folders[2].id).toBe(f3.id);
folders = await Folder.orderByLastModified(await Folder.all(), 'asc');
expect(folders[0].id).toBe(f3.id);
expect(folders[1].id).toBe(f1.id);
expect(folders[2].id).toBe(f2.id);
}));
it('should sort by last modified, based on content (sub-folders too)', asyncTest(async () => {
let folders;
let f1 = await Folder.save({ title: "folder1" }); await sleep(0.1);
let f2 = await Folder.save({ title: "folder2" }); await sleep(0.1);
let f3 = await Folder.save({ title: "folder3", parent_id: f1.id }); await sleep(0.1);
let n1 = await Note.save({ title: 'note1', parent_id: f3.id });
folders = await Folder.orderByLastModified(await Folder.all(), 'desc');
expect(folders.length).toBe(3);
expect(folders[0].id).toBe(f1.id);
expect(folders[1].id).toBe(f3.id);
expect(folders[2].id).toBe(f2.id);
}));
});

View File

@@ -245,22 +245,41 @@ class Application extends BaseApplication {
updateMenu(screen) {
if (this.lastMenuScreen_ === screen) return;
const sortNoteItems = [];
const sortNoteOptions = Setting.enumOptions('notes.sortOrder.field');
for (let field in sortNoteOptions) {
if (!sortNoteOptions.hasOwnProperty(field)) continue;
sortNoteItems.push({
label: sortNoteOptions[field],
screens: ['Main'],
const sortNoteFolderItems = (type) => {
const sortItems = [];
const sortOptions = Setting.enumOptions(type + '.sortOrder.field');
for (let field in sortOptions) {
if (!sortOptions.hasOwnProperty(field)) continue;
sortItems.push({
label: sortOptions[field],
screens: ['Main'],
type: 'checkbox',
checked: Setting.value(type + '.sortOrder.field') === field,
click: () => {
Setting.setValue(type + '.sortOrder.field', field);
this.refreshMenu();
}
});
}
sortItems.push({ type: 'separator' });
sortItems.push({
label: Setting.settingMetadata(type + '.sortOrder.reverse').label(),
type: 'checkbox',
checked: Setting.value('notes.sortOrder.field') === field,
checked: Setting.value(type + '.sortOrder.reverse'),
screens: ['Main'],
click: () => {
Setting.setValue('notes.sortOrder.field', field);
this.refreshMenu();
}
Setting.setValue(type + '.sortOrder.reverse', !Setting.value(type + '.sortOrder.reverse'));
},
});
return sortItems;
}
const sortNoteItems = sortNoteFolderItems('notes');
const sortFolderItems = sortNoteFolderItems('folders');
const focusItems = [];
focusItems.push({
@@ -580,13 +599,9 @@ class Application extends BaseApplication {
screens: ['Main'],
submenu: sortNoteItems,
}, {
label: Setting.settingMetadata('notes.sortOrder.reverse').label(),
type: 'checkbox',
checked: Setting.value('notes.sortOrder.reverse'),
label: Setting.settingMetadata('folders.sortOrder.field').label(),
screens: ['Main'],
click: () => {
Setting.setValue('notes.sortOrder.reverse', !Setting.value('notes.sortOrder.reverse'));
},
submenu: sortFolderItems,
}, {
label: Setting.settingMetadata('uncompletedTodosOnTop').label(),
type: 'checkbox',

View File

@@ -299,6 +299,7 @@ class BaseApplication {
const result = next(action);
const newState = store.getState();
let refreshNotes = false;
let refreshFolders = false;
// let refreshTags = false;
let refreshNotesUseSelectedNoteId = false;
@@ -389,10 +390,11 @@ class BaseApplication {
}
if (action.type === 'NOTE_UPDATE_ONE') {
// If there is a conflict, we refresh the folders so as to display "Conflicts" folder
if (action.note && action.note.is_conflict) {
await FoldersScreenUtils.refreshFolders();
}
refreshFolders = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('folders.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) {
refreshFolders = 'now';
}
if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') {
@@ -407,6 +409,14 @@ class BaseApplication {
ResourceFetcher.instance().queueDownload(action.id);
}
if (refreshFolders) {
if (refreshFolders === 'now') {
await FoldersScreenUtils.refreshFolders();
} else {
await FoldersScreenUtils.scheduleRefreshFolders();
}
}
return result;
}

View File

@@ -100,6 +100,17 @@ class Database {
return this.tryCall('selectAll', sql, params);
}
async selectAllFields(sql, params, field) {
const rows = await this.tryCall('selectAll', sql, params);
const output = [];
for (let i = 0; i < rows.length; i++) {
const v = rows[i][field];
if (!v) throw new Error('No such field: ' + field + '. Query was: ' + sql);
output.push(rows[i][field]);
}
return output;
}
async exec(sql, params = null) {
return this.tryCall('exec', sql, params);
}

View File

@@ -1,22 +1,39 @@
const Folder = require('lib/models/Folder.js');
const Setting = require('lib/models/Setting.js');
class FoldersScreenUtils {
static async refreshFolders() {
let initialFolders = await Folder.all({
const orderDir = Setting.value('folders.sortOrder.reverse') ? 'DESC' : 'ASC';
let folders = await Folder.all({
includeConflictFolder: true,
caseInsensitive: true,
order: [{
by: "title",
dir: "asc"
by: 'title',
dir: orderDir,
}]
});
if (Setting.value('folders.sortOrder.field') === 'last_note_user_updated_time') {
folders = await Folder.orderByLastModified(folders, orderDir);
}
this.dispatch({
type: 'FOLDER_UPDATE_ALL',
items: initialFolders,
items: folders,
});
}
static scheduleRefreshFolders() {
if (this.scheduleRefreshFoldersIID_) clearTimeout(this.scheduleRefreshFoldersIID_);
this.scheduleRefreshFoldersIID_ = setTimeout(() => {
this.scheduleRefreshFoldersIID_ = null;
this.refreshFolders();
}, 1000);
}
}
module.exports = { FoldersScreenUtils };

View File

@@ -26,6 +26,15 @@ class Folder extends BaseItem {
}
}
static fieldToLabel(field) {
const fieldsToLabels = {
title: _('title'),
last_note_user_updated_time: _('updated date'),
};
return field in fieldsToLabels ? fieldsToLabels[field] : field;
}
static noteIds(parentId) {
return this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]).then((rows) => {
let output = [];
@@ -98,6 +107,57 @@ class Folder extends BaseItem {
};
}
// Folders that contain notes that have been modified recently go on top.
// The remaining folders, that don't contain any notes are sorted by their own user_updated_time
static async orderByLastModified(folders, dir = 'DESC') {
dir = dir.toUpperCase();
const sql = 'select parent_id, max(user_updated_time) content_updated_time from notes where parent_id != "" group by parent_id';
const rows = await this.db().selectAll(sql);
const folderIdToTime = {};
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
folderIdToTime[row.parent_id] = row.content_updated_time;
}
const findFolderParent = folderId => {
const folder = BaseModel.byId(folders, folderId);
if (!folder) return null; // For the rare case of notes that are associated with a no longer existing folder
if (!folder.parent_id) return null;
for (let i = 0; i < folders.length; i++) {
if (folders[i].id === folder.parent_id) return folders[i];
}
throw new Error('Could not find parent');
}
const applyChildTimeToParent = folderId => {
const parent = findFolderParent(folderId);
if (!parent) return;
folderIdToTime[parent.id] = folderIdToTime[folderId];
applyChildTimeToParent(parent.id);
}
for (let folderId in folderIdToTime) {
if (!folderIdToTime.hasOwnProperty(folderId)) continue;
applyChildTimeToParent(folderId);
}
const mod = dir === 'DESC' ? +1 : -1;
const output = folders.slice();
output.sort((a, b) => {
const aTime = folderIdToTime[a.id] ? folderIdToTime[a.id] : a.user_updated_time;
const bTime = folderIdToTime[b.id] ? folderIdToTime[b.id] : b.user_updated_time;
if (aTime < bTime) return +1 * mod;
if (aTime > bTime) return -1 * mod;
return 0;
});
return output;
}
static async all(options = null) {
let output = await super.all(options);
if (options && options.includeConflictFolder) {

View File

@@ -71,6 +71,16 @@ class Setting extends BaseModel {
return options;
}},
'notes.sortOrder.reverse': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
'folders.sortOrder.field': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['cli'], label: () => _('Sort notebooks by'), options: () => {
const Folder = require('lib/models/Folder');
const folderSortFields = ['title', 'last_note_user_updated_time'];
const options = {};
for (let i = 0; i < folderSortFields.length; i++) {
options[folderSortFields[i]] = toTitleCase(Folder.fieldToLabel(folderSortFields[i]));
}
return options;
}},
'folders.sortOrder.reverse': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
'trackLocation': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Save geo-location with notes') },
'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, section: 'note', isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => {
return {

View File

@@ -59,6 +59,13 @@ stateUtils.notesOrder = function(stateSettings) {
}];
}
stateUtils.foldersOrder = function(stateSettings) {
return [{
by: stateSettings['folders.sortOrder.field'],
dir: stateSettings['folders.sortOrder.reverse'] ? 'DESC' : 'ASC',
}];
}
stateUtils.parentItem = function(state) {
const t = state.notesParentType;
let id = null;