1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Better UI to handle conflicts

This commit is contained in:
Laurent Cozic 2017-07-15 16:35:40 +01:00
parent 1c16e493f6
commit dde0da571e
13 changed files with 113 additions and 35 deletions

View File

@ -57,12 +57,14 @@ class Application {
this.updatePrompt();
}
async loadItem(type, pattern) {
let output = await this.loadItems(type, pattern);
async loadItem(type, pattern, options = null) {
let output = await this.loadItems(type, pattern, options);
return output.length ? output[0] : null;
}
async loadItems(type, pattern, options = null) {
if (type == BaseModel.TYPE_FOLDER && (pattern == Folder.conflictFolderTitle() || pattern == Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (!options) options = {};
const parent = options.parent ? options.parent : app().currentFolder();

View File

@ -11,13 +11,11 @@ function quotePromptArg(s) {
}
function autocompleteFolders() {
return Folder.all().then((folders) => {
return Folder.all({ includeConflictFolder: true }).then((folders) => {
let output = [];
for (let i = 0; i < folders.length; i++) {
output.push(quotePromptArg(folders[i].title));
}
output.push('..');
output.push('.');
return output;
});
}

View File

@ -29,7 +29,7 @@ class Command extends BaseCommand {
async action(args) {
let title = args['title'];
let item = await app().loadItem(BaseModel.TYPE_NOTE, title);
let item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
if (!item) throw new Error(_('No item "%s" found.', title));
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);

View File

@ -34,11 +34,7 @@ class Command extends BaseCommand {
if (!notes.length) throw new Error(_('No note matches this pattern: "%s"', args['pattern']));
for (let i = 0; i < notes.length; i++) {
const newNote = await Note.duplicate(notes[i].id, {
changes: {
parent_id: folder.id
},
});
const newNote = await Note.copyToFolder(notes[i].id, folder.id);
Note.updateGeolocation(newNote.id);
}
}

View File

@ -56,6 +56,7 @@ class Command extends BaseCommand {
let modelType = null;
if (pattern == '/' || !app().currentFolder()) {
queryOptions.includeConflictFolder = true;
items = await Folder.all(queryOptions);
suffix = '/';
modelType = Folder.modelType();

View File

@ -17,8 +17,12 @@ class Command extends BaseCommand {
return ['mkdir'];
}
async action(args, end) {
let folder = await Folder.save({ title: args['notebook'] }, { duplicateCheck: true });
async action(args) {
let folder = await Folder.save({ title: args['notebook'] }, {
duplicateCheck: true,
reservedTitleCheck: true,
});
app().switchCurrentFolder(folder);
}

View File

@ -30,7 +30,7 @@ class Command extends BaseCommand {
if (!notes.length) throw new Error(_('No note matches this pattern: "%s"', pattern));
for (let i = 0; i < notes.length; i++) {
await Note.save({ id: notes[i].id, parent_id: folder.id });
await Note.moveToFolder(notes[i].id, folder.id);
}
}

View File

@ -25,7 +25,7 @@ class Command extends BaseCommand {
async action(args) {
let folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
if (!folder) throw new Error(_('No folder "%s"', title));
if (!folder) throw new Error(_('No folder "%s"', args['notebook']));
app().switchCurrentFolder(folder);
}

View File

@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 12
versionName "0.8.10"
versionCode 13
versionName "0.9.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@ -96,7 +96,6 @@ class BaseModel {
return this.loadByField('id', id);
}
static loadByPartialId(partialId) {
return this.modelSelectOne('SELECT * FROM `' + this.tableName() + '` WHERE `id` LIKE ?', [partialId + '%']);
}

View File

@ -1,6 +1,7 @@
import { BaseModel } from 'lib/base-model.js';
import { Log } from 'lib/log.js';
import { promiseChain } from 'lib/promise-utils.js';
import { time } from 'lib/time-utils.js';
import { Note } from 'lib/models/note.js';
import { Setting } from 'lib/models/setting.js';
import { Database } from 'lib/database.js';
@ -76,28 +77,55 @@ class Folder extends BaseItem {
});
}
static conflictFolderTitle() {
return _('Conflicts');
}
static conflictFolderId() {
return 'c04f1c7c04f1c7c04f1c7c04f1c7c04f';
}
static conflictFolder() {
return {
type_: this.TYPE_FOLDER,
id: this.conflictFolderId(),
title: this.conflictFolderTitle(),
updated_time: time.unixMs(),
};
}
static async all(options = null) {
if (!options) options = {};
let output = await super.all(options);
if (options && options.includeConflictFolder) {
let conflictCount = await Note.conflictedCount();
if (conflictCount) output.push(this.conflictFolder());
}
return output;
}
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);
static load(id) {
if (id == this.conflictFolderId()) return this.conflictFolder();
return super.load(id);
}
static defaultFolder() {
return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1');
}
// These "duplicateCheck" and "reservedTitleCheck" should only be done when a user is
// manually creating a folder. They shouldn't be done for example when the folders
// are being synced to avoid any strange side-effect. Technically it's possible to
// have folders and notes with duplicate titles (or no title), or with reserved words,
static async save(o, options = null) {
if (options && options.duplicateCheck === true && o.title) {
let existingFolder = await Folder.loadByTitle(o.title);
if (existingFolder) throw new Error(_('A notebook with this title already exists: "%s"', o.title));
}
if (options && options.reservedTitleCheck === true && o.title) {
if (o.title == Folder.conflictFolderTitle()) throw new Error(_('Notebooks cannot be named "%s", which is a reserved title.', o.title));
}
return super.save(o, options).then((folder) => {
this.dispatch({
type: 'FOLDERS_UPDATE_ONE',

View File

@ -5,6 +5,7 @@ import { BaseItem } from 'lib/models/base-item.js';
import { Setting } from 'lib/models/setting.js';
import { shim } from 'lib/shim.js';
import { time } from 'lib/time-utils.js';
import { _ } from 'lib/locale.js';
import moment from 'moment';
import lodash from 'lodash';
@ -17,7 +18,8 @@ class Note extends BaseItem {
static async serialize(note, type = null, shownKeys = null) {
let fieldNames = this.fieldNames();
fieldNames.push('type_');
lodash.pull(fieldNames, 'is_conflict', 'sync_time');
//lodash.pull(fieldNames, 'is_conflict', 'sync_time');
lodash.pull(fieldNames, 'sync_time');
return super.serialize(note, 'note', fieldNames);
}
@ -64,9 +66,19 @@ class Note extends BaseItem {
return this.db().escapeFields(this.previewFields()).join(',');
}
static loadFolderNoteByField(folderId, field, value) {
static async loadFolderNoteByField(folderId, field, value) {
if (!folderId) throw new Error('folderId is undefined');
return this.modelSelectOne('SELECT * FROM notes WHERE is_conflict = 0 AND `parent_id` = ? AND `' + field + '` = ?', [folderId, value]);
let options = {
conditions: ['`' + field + '` = ?'],
conditionsParams: [value],
fields: '*',
}
// TODO: add support for limits on .search()
let results = await this.previews(folderId, options);
return results.length ? results[0] : null;
}
static previews(parentId, options = null) {
@ -77,10 +89,13 @@ class Note extends BaseItem {
if (!options.conditionsParams) options.conditionsParams = [];
if (!options.fields) options.fields = this.previewFields();
options.conditions.push('is_conflict = 0');
options.conditions.push('parent_id = ?');
options.conditionsParams.push(parentId);
if (parentId == Folder.conflictFolderId()) {
options.conditions.push('is_conflict = 1');
} else {
options.conditions.push('is_conflict = 0');
options.conditions.push('parent_id = ?');
options.conditionsParams.push(parentId);
}
if (options.itemTypes && options.itemTypes.length) {
if (options.itemTypes.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) {
@ -103,6 +118,11 @@ class Note extends BaseItem {
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 1');
}
static async conflictedCount() {
let r = await this.db().selectOne('SELECT count(*) as total FROM notes WHERE is_conflict = 1');
return r && r.total ? r.total : 0;
}
static unconflictedNotes() {
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
}
@ -154,6 +174,27 @@ class Note extends BaseItem {
return output;
}
static async copyToFolder(noteId, folderId) {
if (folderId == Folder.conflictFolderId()) throw new Error(_('Cannot copy note to "%s" notebook', Folder.conflictFolderIdTitle()));
return Note.duplicate(noteId, {
changes: {
parent_id: folderId,
is_conflict: 0, // Also reset the conflict flag in case we're moving the note out of the conflict folder
},
});
}
static async moveToFolder(noteId, folderId) {
if (folderId == Folder.conflictFolderId()) throw new Error(_('Cannot move note to "%s" notebook', Folder.conflictFolderIdTitle()));
return Note.save({
id: noteId,
parent_id: folderId,
is_conflict: 0,
});
}
static async duplicate(noteId, options = null) {
const changes = options && options.changes;

View File

@ -1,6 +1,7 @@
import { time } from 'lib/time-utils'
import { BaseItem } from 'lib/models/base-item.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
import { _ } from 'lib/locale.js';
class ReportService {
@ -34,6 +35,12 @@ class ReportService {
total: await BaseItem.deletedItemCount(),
};
output.conflicted = {
total: await Note.conflictedCount(),
};
output.items['Note'].total -= output.conflicted.total;
return output;
}
@ -50,8 +57,10 @@ class ReportService {
section.body.push(_('%s: %d/%d', n, r.items[n].synced, r.items[n].total));
}
if (r.total) section.body.push(_('Total: %d/%d', r.total.synced, r.total.total));
if (r.toDelete) section.body.push(_('To delete: %d', r.toDelete.total));
section.body.push(_('Total: %d/%d', r.total.synced, r.total.total));
section.body.push('');
section.body.push(_('Conflicted: %d', r.conflicted.total));
section.body.push(_('To delete: %d', r.toDelete.total));
sections.push(section);