diff --git a/CliClient/app/main.js b/CliClient/app/main.js index b962d7d111..cbf93e73ba 100644 --- a/CliClient/app/main.js +++ b/CliClient/app/main.js @@ -196,18 +196,13 @@ async function main() { usage: 'use ', aliases: ['cd'], description: 'Switches to [notebook-title] - all further operations will happen within this notebook.', - action: function(args, end) { + action: async function(args, end) { let folderTitle = args['notebook-title']; - if (folderTitle == '.' || folderTitle == '..') { - end(); - return; - } - - Folder.loadByField('title', folderTitle).then((folder) => { - switchCurrentFolder(folder); - end(); - }); + let folder = await Folder.loadByField('title', folderTitle); + if (!folder) return commandError(this, _('Invalid folder title: %s', folderTitle), end); + switchCurrentFolder(folder); + end(); }, autocomplete: autocompleteFolders, }); @@ -355,14 +350,32 @@ async function main() { description: 'Displays the notes in [notebook-title]. Use `ls ..` to display the list of notebooks.', options: [ ['-n, --lines ', 'Displays only the first top lines.'], + ['-s, --sort ', 'Sorts the item by (eg. title, updated_time, created_time).'], + ['-r, --reverse', 'Reverses the sorting order.'], + ['-t, --type ', 'Displays only the items of the specific type(s). Can be `n` for notes, `t` for todos, or `nt` for notes and todos (eg. `-tt` would display only the todos, while `-ttd` would display notes and todos.'], ], action: async function(args, end) { let folderTitle = args['notebook-title']; let suffix = ''; let items = []; + let options = args.options; + + let queryOptions = {}; + if (options.lines) queryOptions.limit = options.lines; + if (options.sort) { + queryOptions.orderBy = options.sort; + queryOptions.orderByDir = 'ASC'; + } + if (options.reverse === true) queryOptions.orderByDir = queryOptions.orderByDir == 'ASC' ? 'DESC' : 'ASC'; + queryOptions.caseInsensitive = true; + if (options.type) { + queryOptions.itemTypes = []; + if (options.type.indexOf('n') >= 0) queryOptions.itemTypes.push('note'); + if (options.type.indexOf('t') >= 0) queryOptions.itemTypes.push('todo'); + } if (folderTitle == '..') { - items = await Folder.all(); + items = await Folder.all(queryOptions); suffix = '/'; } else { let folder = null; @@ -375,12 +388,17 @@ async function main() { if (!folder) return commandError(this, _('Unknown notebook: "%s"', folderTitle), end); - items = await Note.previews(folder.id); + items = await Note.previews(folder.id, queryOptions); } - + for (let i = 0; i < items.length; i++) { let item = items[i]; - this.log(item.title + suffix); + let line = ''; + if (!!item.is_todo) { + line += sprintf('[%s] ', !!item.todo_completed ? 'X' : ' '); + } + line += item.title + suffix; + this.log(line); } end(); diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index a81a1f0194..90023fe1fa 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -46,7 +46,7 @@ describe('Synchronizer', function() { let folder = await Folder.save({ title: "folder1" }); await Note.save({ title: "un", parent_id: folder.id }); - let all = await Folder.all(true); + let all = await Folder.all({ includeNotes: true }); await synchronizer().start(); @@ -64,7 +64,7 @@ describe('Synchronizer', function() { await Note.save({ title: "un UPDATE", id: note.id }); - let all = await Folder.all(true); + let all = await Folder.all({ includeNotes: true }); await synchronizer().start(); await localItemsSameAsRemote(all, expect); @@ -81,7 +81,7 @@ describe('Synchronizer', function() { await synchronizer().start(); - let all = await Folder.all(true); + let all = await Folder.all({ includeNotes: true }); await localItemsSameAsRemote(all, expect); done(); @@ -109,7 +109,7 @@ describe('Synchronizer', function() { await synchronizer().start(); - let all = await Folder.all(true); + let all = await Folder.all({ includeNotes: true }); let files = await fileApi().list(); await localItemsSameAsRemote(all, expect); @@ -250,7 +250,7 @@ describe('Synchronizer', function() { await synchronizer().start(); - let items = await Folder.all(true); + let items = await Folder.all({ includeNotes: true }); expect(items.length).toBe(1); @@ -288,7 +288,7 @@ describe('Synchronizer', function() { expect(conflictedNotes.length).toBe(1); expect(conflictedNotes[0].title).toBe(newTitle); - let items = await Folder.all(true); + let items = await Folder.all({ includeNotes: true }); expect(items.length).toBe(1); @@ -319,7 +319,7 @@ describe('Synchronizer', function() { await synchronizer().start(); - let items = await Folder.all(true); + let items = await Folder.all({ includeNotes: true }); expect(items.length).toBe(0); diff --git a/lib/base-model.js b/lib/base-model.js index dc9f63b0fd..4395d0c1f0 100644 --- a/lib/base-model.js +++ b/lib/base-model.js @@ -120,6 +120,24 @@ class BaseModel { return this.loadByField('id', id); } + static applySqlOptions(options, sql, params = null) { + if (!options) options = {}; + + if (options.orderBy) { + sql += ' ORDER BY ' + options.orderBy; + if (options.caseInsensitive === true) sql += ' COLLATE NOCASE'; + if (options.orderByDir) sql += ' ' + options.orderByDir; + } + if (options.limit) sql += ' LIMIT ' + options.limit; + + return { sql: sql, params: params }; + } + + static async all(options = null) { + let q = this.applySqlOptions(options, 'SELECT * FROM `' + this.tableName() + '`'); + return await this.modelSelectAll(q.sql); + } + static modelSelectOne(sql, params = null) { if (params === null) params = []; return this.db().selectOne(sql, params).then((model) => { diff --git a/lib/models/folder.js b/lib/models/folder.js index 77ece58365..7fa32746c7 100644 --- a/lib/models/folder.js +++ b/lib/models/folder.js @@ -81,11 +81,15 @@ class Folder extends BaseItem { return this.modelSelectOne('SELECT * FROM notes WHERE is_conflict = 0 AND `parent_id` = ? AND `' + field + '` = ?', [folderId, value]); } - static async all(includeNotes = false) { - let folders = await Folder.modelSelectAll('SELECT * FROM folders'); - if (!includeNotes) return folders; + static async all(options = null) { + if (!options) options = {}; - let notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0'); + let folders = await super.all(options); + if (!options.includeNotes) return folders; + + if (options.limit) options.limit -= folders.length; + + let notes = await Note.all(options); return folders.concat(notes); } diff --git a/lib/models/note.js b/lib/models/note.js index c29ad86f4f..bee677c26c 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -43,8 +43,24 @@ class Note extends BaseItem { return '`id`, `title`, `body`, `is_todo`, `todo_completed`, `parent_id`, `updated_time`' } - static previews(parentId) { - return this.modelSelectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]); + static previews(parentId, options = null) { + if (!options) options = {}; + if (!options.orderBy) options.orderBy = 'updated_time'; + if (!options.orderByDir) options.orderByDir = 'DESC'; + + let sql = 'SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE is_conflict = 0 AND parent_id = ?'; + if (options.itemTypes && options.itemTypes.length) { + if (options.itemTypes.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) { + // Fetch everything + } else if (options.itemTypes.indexOf('note') >= 0) { + sql += ' AND is_todo = 0'; + } else if (options.itemTypes.indexOf('todo') >= 0) { + sql += ' AND is_todo = 1'; + } + } + let query = this.applySqlOptions(options, sql, [parentId]); + + return this.modelSelectAll(query.sql, query.params); } static preview(noteId) {