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