You've already forked joplin
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:
@@ -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);
|
||||
}));
|
||||
|
||||
});
|
@@ -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',
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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 };
|
@@ -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) {
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user