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:
parent
1c16e493f6
commit
dde0da571e
@ -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();
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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 + '%']);
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user