1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-26 18:58:21 +02:00

sync tags

This commit is contained in:
Laurent Cozic 2017-07-02 19:38:34 +01:00
parent f04c1c58f1
commit a11acb0fa8
12 changed files with 92 additions and 156 deletions

File diff suppressed because one or more lines are too long

View File

@ -76,7 +76,7 @@ async function saveNoteResources(note) {
}
async function saveNoteTags(note) {
let noteTagged = 0;
let notesTagged = 0;
for (let i = 0; i < note.tags.length; i++) {
let tagTitle = note.tags[i];
@ -85,9 +85,9 @@ async function saveNoteTags(note) {
await Tag.addNote(tag.id, note.id);
noteTagged++;
notesTagged++;
}
return noteTagged;
return notesTagged;
}
async function saveNoteToStorage(note, fuzzyMatching = false) {
@ -100,14 +100,14 @@ async function saveNoteToStorage(note, fuzzyMatching = false) {
noteUpdated: false,
noteSkipped: false,
resourcesCreated: 0,
noteTagged: 0,
notesTagged: 0,
};
let resourcesCreated = await saveNoteResources(note);
result.resourcesCreated += resourcesCreated;
let noteTagged = await saveNoteTags(note);
result.noteTagged += noteTagged;
let notesTagged = await saveNoteTags(note);
result.notesTagged += notesTagged;
if (existingNote) {
let diff = BaseModel.diffObjects(existingNote, note);
@ -148,7 +148,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
updated: 0,
skipped: 0,
resourcesCreated: 0,
noteTagged: 0,
notesTagged: 0,
};
let stream = fs.createReadStream(filePath);
@ -213,7 +213,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
progressState.skipped++;
}
progressState.resourcesCreated += result.resourcesCreated;
progressState.noteTagged += result.noteTagged;
progressState.notesTagged += result.notesTagged;
importOptions.onProgress(progressState);
});
});

View File

@ -391,8 +391,8 @@ commands.push({
if (report.remotesToDelete) line.push(_('Remote items to delete: %d/%d.', report.deleteRemote, report.remotesToDelete));
if (report.localsToUdpate) line.push(_('Items to download: %d/%d.', report.createLocal + report.updateLocal, report.localsToUdpate));
if (report.localsToDelete) line.push(_('Local items to delete: %d/%d.', report.deleteLocal, report.localsToDelete));
//redrawnCalled = true;
//vorpal.ui.redraw(line.join(' '));
// redrawnCalled = true;
// vorpal.ui.redraw(line.join(' '));
},
onMessage: (msg) => {
if (redrawnCalled) vorpal.ui.redraw.done();

View File

@ -45,7 +45,6 @@ function clearDatabase(id = null) {
if (id === null) id = currentClient_;
let queries = [
'DELETE FROM changes',
'DELETE FROM notes',
'DELETE FROM folders',
'DELETE FROM resources',

View File

@ -37,10 +37,6 @@ class BaseModel {
throw new Error('Must be overriden');
}
static trackChanges() {
return false;
}
static trackDeleted() {
return false;
}
@ -106,7 +102,6 @@ class BaseModel {
} else {
options = Object.assign({}, options);
}
if (!('trackChanges' in options)) options.trackChanges = true;
if (!('trackDeleted' in options)) options.trackDeleted = null;
if (!('isNew' in options)) options.isNew = 'auto';
if (!('autoTimestamp' in options)) options.autoTimestamp = true;
@ -321,6 +316,7 @@ BaseModel.MODEL_TYPE_NOTE = 1;
BaseModel.MODEL_TYPE_FOLDER = 2;
BaseModel.MODEL_TYPE_SETTING = 3;
BaseModel.MODEL_TYPE_RESOURCE = 4;
BaseModel.MODEL_TYPE_TAG = 5;
BaseModel.tableInfo_ = null;
BaseModel.tableKeys_ = null;
BaseModel.db_ = null;

View File

@ -60,7 +60,8 @@ CREATE TABLE tags (
id TEXT PRIMARY KEY,
title TEXT NOT NULL DEFAULT "",
created_time INT NOT NULL,
updated_time INT NOT NULL
updated_time INT NOT NULL,
sync_time INT NOT NULL DEFAULT 0
);
CREATE TABLE note_tags (
@ -79,14 +80,6 @@ CREATE TABLE resources (
sync_time INT NOT NULL DEFAULT 0
);
CREATE TABLE changes (
id INTEGER PRIMARY KEY,
\`type\` INT,
item_id TEXT,
item_type INT,
item_field TEXT
);
CREATE TABLE settings (
\`key\` TEXT PRIMARY KEY,
\`value\` TEXT,

View File

@ -32,6 +32,7 @@ class BaseItem extends BaseModel {
if (Number(item) === BaseModel.MODEL_TYPE_NOTE) return this.getClass('Note');
if (Number(item) === BaseModel.MODEL_TYPE_FOLDER) return this.getClass('Folder');
if (Number(item) === BaseModel.MODEL_TYPE_RESOURCE) return this.getClass('Resource');
if (Number(item) === BaseModel.MODEL_TYPE_TAG) return this.getClass('Tag');
throw new Error('Unknown type: ' + item);
}
}
@ -40,7 +41,9 @@ class BaseItem extends BaseModel {
static async syncedItems() {
let folders = await this.getClass('Folder').modelSelectAll('SELECT id FROM folders WHERE sync_time > 0');
let notes = await this.getClass('Note').modelSelectAll('SELECT id FROM notes WHERE is_conflict = 0 AND sync_time > 0');
return folders.concat(notes);
let resources = await this.getClass('Resource').modelSelectAll('SELECT id FROM resources WHERE sync_time > 0');
let tags = await this.getClass('Tag').modelSelectAll('SELECT id FROM tags WHERE sync_time > 0');
return folders.concat(notes).concat(resources).concat(tags);
}
static pathToId(path) {
@ -52,11 +55,13 @@ class BaseItem extends BaseModel {
return this.loadItemById(this.pathToId(path));
}
static loadItemById(id) {
return this.getClass('Note').load(id).then((item) => {
static async loadItemById(id) {
let classes = ['Note', 'Folder', 'Resource', 'Tag'];
for (let i = 0; i < classes.length; i++) {
let item = await this.getClass(classes[i]).load(id);
if (item) return item;
return this.getClass('Folder').load(id);
});
}
return null;
}
static loadItemByField(itemType, field, value) {
@ -86,7 +91,7 @@ class BaseItem extends BaseModel {
}
static unserialize_format(type, propName, propValue) {
if (propName == 'type_') return propValue;
if (propName[propName.length - 1] == '_') return propValue; // Private property
let ItemClass = this.itemClass(type);
@ -110,9 +115,17 @@ class BaseItem extends BaseModel {
output.push(type == 'note' ? item.body : '');
output.push('');
for (let i = 0; i < shownKeys.length; i++) {
let v = item[shownKeys[i]];
v = this.serialize_format(shownKeys[i], v);
output.push(shownKeys[i] + ': ' + v);
let key = shownKeys[i];
let value = null;
if (typeof key === 'function') {
let r = await key();
key = r.key;
value = r.value;
} else {
value = this.serialize_format(key, item[key]);
}
output.push(key + ': ' + value);
}
return output.join("\n");
@ -170,6 +183,9 @@ class BaseItem extends BaseModel {
if (items.length) return { hasMore: true, items: items };
items = await this.getClass('Note').modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time AND is_conflict = 0 LIMIT ' + limit);
if (items.length) return { hasMore: true, items: items };
items = await this.getClass('Tag').modelSelectAll('SELECT * FROM tags WHERE sync_time < updated_time LIMIT ' + limit);
return { hasMore: items.length >= limit, items: items };
}

View File

@ -1,101 +0,0 @@
import { BaseModel } from 'lib/base-model.js';
import { Log } from 'lib/log.js';
class Change extends BaseModel {
static tableName() {
return 'changes';
}
static newChange() {
return {
id: null,
type: null,
item_id: null,
item_type: null,
item_field: null,
};
}
static all() {
return this.db().selectAll('SELECT * FROM changes');
}
static deleteMultiple(ids) {
if (ids.length == 0) return Promise.resolve();
let queries = [];
for (let i = 0; i < ids.length; i++) {
queries.push(['DELETE FROM changes WHERE id = ?', [ids[i]]]);
}
return this.db().transactionExecBatch(queries);
}
static mergeChanges(changes) {
let createdItems = [];
let deletedItems = [];
let itemChanges = {};
for (let i = 0; i < changes.length; i++) {
let change = changes[i];
let mergedChange = null;
if (itemChanges[change.item_id]) {
mergedChange = itemChanges[change.item_id];
} else {
mergedChange = {
item_id: change.item_id,
item_type: change.item_type,
fields: [],
ids: [],
type: change.type,
}
}
if (change.type == this.TYPE_CREATE) {
createdItems.push(change.item_id);
} else if (change.type == this.TYPE_DELETE) {
deletedItems.push(change.item_id);
} else if (change.type == this.TYPE_UPDATE) {
if (mergedChange.fields.indexOf(change.item_field) < 0) {
mergedChange.fields.push(change.item_field);
}
}
mergedChange.ids.push(change.id);
itemChanges[change.item_id] = mergedChange;
}
let output = [];
for (let itemId in itemChanges) {
if (!itemChanges.hasOwnProperty(itemId)) continue;
let change = itemChanges[itemId];
if (createdItems.indexOf(itemId) >= 0 && deletedItems.indexOf(itemId) >= 0) {
// Item both created then deleted - skip
change.type = this.TYPE_NOOP;
} else if (deletedItems.indexOf(itemId) >= 0) {
// Item was deleted at some point - just return one 'delete' event
change.type = this.TYPE_DELETE;
} else if (createdItems.indexOf(itemId) >= 0) {
// Item was created then updated - just return one 'create' event with the latest changes
change.type = this.TYPE_CREATE;
}
output.push(change);
}
return output;
}
}
Change.TYPE_NOOP = 0;
Change.TYPE_CREATE = 1;
Change.TYPE_UPDATE = 2;
Change.TYPE_DELETE = 3;
export { Change };

View File

@ -25,10 +25,6 @@ class Folder extends BaseItem {
return BaseModel.MODEL_TYPE_FOLDER;
}
static trackChanges() {
return true;
}
static trackDeleted() {
return true;
}

View File

@ -24,10 +24,6 @@ class Note extends BaseItem {
return BaseModel.MODEL_TYPE_NOTE;
}
static trackChanges() {
return true;
}
static trackDeleted() {
return true;
}

View File

@ -16,17 +16,31 @@ class Tag extends BaseItem {
static async serialize(item, type = null, shownKeys = null) {
let fieldNames = this.fieldNames();
fieldNames.push('type_');
fieldNames.push(() => {
fieldNames.push(async () => {
let noteIds = await this.tagNoteIds(item.id);
console.info('NOTE IDS', noteIds);
return {
key: 'notes_',
value: noteIds.join(','),
};
});
lodash.pull(fieldNames, 'sync_time');
return super.serialize(item, 'tag', fieldNames);
}
static tagNoteIds(tagId) {
return this.db().selectAll('SELECT note_id FROM note_tags WHERE tag_id = ?', [tagId]);
static async tagNoteIds(tagId) {
let rows = await this.db().selectAll('SELECT note_id FROM note_tags WHERE tag_id = ?', [tagId]);
let output = [];
for (let i = 0; i < rows.length; i++) {
output.push(rows[i].note_id);
}
return output;
}
// TODO: in order for a sync to happen, the updated_time property should somehow be changed
// whenever an tag is applied or removed from an item. Either the updated_time property
// is changed here or by the caller?
static async addNote(tagId, noteId) {
let hasIt = await this.hasNote(tagId, noteId);
if (hasIt) return;
@ -35,7 +49,30 @@ class Tag extends BaseItem {
tag_id: tagId,
note_id: noteId,
});
return this.db().exec(query);
await this.db().exec(query);
//await this.save({ id: tagId, updated_time: time.unixMs() }); //type_: BaseModel.MODEL_TYPE_TAG
}
static async addNotes(tagId, noteIds) {
for (let i = 0; i < noteIds.length; i++) {
await this.addNote(tagId, noteIds[i]);
}
}
// Note: updated_time must not change since this is only called from
// the synchronizer, which manages and sets the correct updated_time
static async setAssociatedNotes(tagId, noteIds) {
let queries = [{
sql: 'DELETE FROM note_tags WHERE tag_id = ?',
params: [tagId],
}];
for (let i = 0; i < noteIds.length; i++) {
queries.push(Database.insertQuery('note_tags', { tag_id: tagId, note_id: noteIds[i] }));
}
return this.db().transactionExecBatch(queries);
}
static async hasNote(tagId, noteId) {

View File

@ -331,6 +331,11 @@ class Synchronizer {
}
}
if (newContent.type_ == BaseModel.MODEL_TYPE_TAG) {
let noteIds = newContent.notes_.split(',');
await ItemClass.setAssociatedNotes(newContent.id, noteIds);
}
this.logSyncOperation(action, local, content, reason);
} else {
this.logSyncOperation(action, local, remote, reason);