1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-16 08:56:40 +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(); this.updatePrompt();
} }
async loadItem(type, pattern) { async loadItem(type, pattern, options = null) {
let output = await this.loadItems(type, pattern); let output = await this.loadItems(type, pattern, options);
return output.length ? output[0] : null; return output.length ? output[0] : null;
} }
async loadItems(type, pattern, options = null) { async loadItems(type, pattern, options = null) {
if (type == BaseModel.TYPE_FOLDER && (pattern == Folder.conflictFolderTitle() || pattern == Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (!options) options = {}; if (!options) options = {};
const parent = options.parent ? options.parent : app().currentFolder(); const parent = options.parent ? options.parent : app().currentFolder();

View File

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

View File

@@ -29,7 +29,7 @@ class Command extends BaseCommand {
async action(args) { async action(args) {
let title = args['title']; 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)); if (!item) throw new Error(_('No item "%s" found.', title));
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item); 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'])); if (!notes.length) throw new Error(_('No note matches this pattern: "%s"', args['pattern']));
for (let i = 0; i < notes.length; i++) { for (let i = 0; i < notes.length; i++) {
const newNote = await Note.duplicate(notes[i].id, { const newNote = await Note.copyToFolder(notes[i].id, folder.id);
changes: {
parent_id: folder.id
},
});
Note.updateGeolocation(newNote.id); Note.updateGeolocation(newNote.id);
} }
} }

View File

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

View File

@@ -17,8 +17,12 @@ class Command extends BaseCommand {
return ['mkdir']; return ['mkdir'];
} }
async action(args, end) { async action(args) {
let folder = await Folder.save({ title: args['notebook'] }, { duplicateCheck: true }); let folder = await Folder.save({ title: args['notebook'] }, {
duplicateCheck: true,
reservedTitleCheck: true,
});
app().switchCurrentFolder(folder); 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)); if (!notes.length) throw new Error(_('No note matches this pattern: "%s"', pattern));
for (let i = 0; i < notes.length; i++) { 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) { async action(args) {
let folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']); 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); app().switchCurrentFolder(folder);
} }

View File

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

View File

@@ -96,7 +96,6 @@ class BaseModel {
return this.loadByField('id', id); return this.loadByField('id', id);
} }
static loadByPartialId(partialId) { static loadByPartialId(partialId) {
return this.modelSelectOne('SELECT * FROM `' + this.tableName() + '` WHERE `id` LIKE ?', [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 { BaseModel } from 'lib/base-model.js';
import { Log } from 'lib/log.js'; import { Log } from 'lib/log.js';
import { promiseChain } from 'lib/promise-utils.js'; import { promiseChain } from 'lib/promise-utils.js';
import { time } from 'lib/time-utils.js';
import { Note } from 'lib/models/note.js'; import { Note } from 'lib/models/note.js';
import { Setting } from 'lib/models/setting.js'; import { Setting } from 'lib/models/setting.js';
import { Database } from 'lib/database.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) { 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); static load(id) {
if (!options.includeNotes) return folders; if (id == this.conflictFolderId()) return this.conflictFolder();
return super.load(id);
if (options.limit) options.limit -= folders.length;
let notes = await Note.all(options);
return folders.concat(notes);
} }
static defaultFolder() { static defaultFolder() {
return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1'); 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) { static async save(o, options = null) {
if (options && options.duplicateCheck === true && o.title) { if (options && options.duplicateCheck === true && o.title) {
let existingFolder = await Folder.loadByTitle(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 (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) => { return super.save(o, options).then((folder) => {
this.dispatch({ this.dispatch({
type: 'FOLDERS_UPDATE_ONE', 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 { Setting } from 'lib/models/setting.js';
import { shim } from 'lib/shim.js'; import { shim } from 'lib/shim.js';
import { time } from 'lib/time-utils.js'; import { time } from 'lib/time-utils.js';
import { _ } from 'lib/locale.js';
import moment from 'moment'; import moment from 'moment';
import lodash from 'lodash'; import lodash from 'lodash';
@@ -17,7 +18,8 @@ class Note extends BaseItem {
static async serialize(note, type = null, shownKeys = null) { static async serialize(note, type = null, shownKeys = null) {
let fieldNames = this.fieldNames(); let fieldNames = this.fieldNames();
fieldNames.push('type_'); 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); return super.serialize(note, 'note', fieldNames);
} }
@@ -64,9 +66,19 @@ class Note extends BaseItem {
return this.db().escapeFields(this.previewFields()).join(','); 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'); 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) { static previews(parentId, options = null) {
@@ -77,10 +89,13 @@ class Note extends BaseItem {
if (!options.conditionsParams) options.conditionsParams = []; if (!options.conditionsParams) options.conditionsParams = [];
if (!options.fields) options.fields = this.previewFields(); if (!options.fields) options.fields = this.previewFields();
options.conditions.push('is_conflict = 0'); if (parentId == Folder.conflictFolderId()) {
options.conditions.push('is_conflict = 1');
options.conditions.push('parent_id = ?'); } else {
options.conditionsParams.push(parentId); options.conditions.push('is_conflict = 0');
options.conditions.push('parent_id = ?');
options.conditionsParams.push(parentId);
}
if (options.itemTypes && options.itemTypes.length) { if (options.itemTypes && options.itemTypes.length) {
if (options.itemTypes.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) { 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'); 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() { static unconflictedNotes() {
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0'); return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
} }
@@ -154,6 +174,27 @@ class Note extends BaseItem {
return output; 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) { static async duplicate(noteId, options = null) {
const changes = options && options.changes; const changes = options && options.changes;

View File

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