You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
Converted models and services to TypeScript
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
const Note = require('./Note.js');
|
||||
import BaseModel from '../BaseModel';
|
||||
import Note from './Note';
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
|
||||
@@ -1,15 +1,45 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
const { Database } = require('../database.js');
|
||||
const Setting = require('./Setting').default;
|
||||
const ItemChange = require('./ItemChange.js');
|
||||
const JoplinError = require('../JoplinError.js');
|
||||
const time = require('../time').default;
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { _ } = require('../locale');
|
||||
const moment = require('moment');
|
||||
const markdownUtils = require('../markdownUtils').default;
|
||||
import { ModelType } from '../BaseModel';
|
||||
import { NoteEntity } from '../services/database/types';
|
||||
import Setting from './Setting';
|
||||
import BaseModel from '../BaseModel';
|
||||
import time from '../time';
|
||||
import markdownUtils from '../markdownUtils';
|
||||
import { _ } from '../locale';
|
||||
|
||||
const { Database } = require('../database.js');
|
||||
import ItemChange from './ItemChange';
|
||||
const JoplinError = require('../JoplinError.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const moment = require('moment');
|
||||
|
||||
export interface ItemsThatNeedDecryptionResult {
|
||||
hasMore: boolean;
|
||||
items: any[];
|
||||
}
|
||||
|
||||
export default class BaseItem extends BaseModel {
|
||||
|
||||
public static encryptionService_: any = null;
|
||||
public static revisionService_: any = null;
|
||||
|
||||
// Also update:
|
||||
// - itemsThatNeedSync()
|
||||
// - syncedItems()
|
||||
|
||||
public static syncItemDefinitions_: any[] = [
|
||||
{ type: BaseModel.TYPE_NOTE, className: 'Note' },
|
||||
{ type: BaseModel.TYPE_FOLDER, className: 'Folder' },
|
||||
{ type: BaseModel.TYPE_RESOURCE, className: 'Resource' },
|
||||
{ type: BaseModel.TYPE_TAG, className: 'Tag' },
|
||||
{ type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' },
|
||||
{ type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' },
|
||||
{ type: BaseModel.TYPE_REVISION, className: 'Revision' },
|
||||
];
|
||||
|
||||
public static SYNC_ITEM_LOCATION_LOCAL = 1;
|
||||
public static SYNC_ITEM_LOCATION_REMOTE = 2;
|
||||
|
||||
|
||||
class BaseItem extends BaseModel {
|
||||
static useUuid() {
|
||||
return true;
|
||||
}
|
||||
@@ -18,7 +48,7 @@ class BaseItem extends BaseModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
static loadClass(className, classRef) {
|
||||
static loadClass(className: string, classRef: any) {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
if (BaseItem.syncItemDefinitions_[i].className == className) {
|
||||
BaseItem.syncItemDefinitions_[i].classRef = classRef;
|
||||
@@ -29,7 +59,7 @@ class BaseItem extends BaseModel {
|
||||
throw new Error(`Invalid class name: ${className}`);
|
||||
}
|
||||
|
||||
static async findUniqueItemTitle(title, parentId = null) {
|
||||
static async findUniqueItemTitle(title: string, parentId: string = null) {
|
||||
let counter = 1;
|
||||
let titleToTry = title;
|
||||
while (true) {
|
||||
@@ -53,7 +83,7 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
// Need to dynamically load the classes like this to avoid circular dependencies
|
||||
static getClass(name) {
|
||||
static getClass(name: string) {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
if (BaseItem.syncItemDefinitions_[i].className == name) {
|
||||
const classRef = BaseItem.syncItemDefinitions_[i].classRef;
|
||||
@@ -65,7 +95,7 @@ class BaseItem extends BaseModel {
|
||||
throw new Error(`Invalid class name: ${name}`);
|
||||
}
|
||||
|
||||
static getClassByItemType(itemType) {
|
||||
static getClassByItemType(itemType: ModelType) {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
if (BaseItem.syncItemDefinitions_[i].type == itemType) {
|
||||
return BaseItem.syncItemDefinitions_[i].classRef;
|
||||
@@ -75,7 +105,7 @@ class BaseItem extends BaseModel {
|
||||
throw new Error(`Invalid item type: ${itemType}`);
|
||||
}
|
||||
|
||||
static async syncedCount(syncTarget) {
|
||||
static async syncedCount(syncTarget: number) {
|
||||
const ItemClass = this.itemClass(this.modelType());
|
||||
const itemType = ItemClass.modelType();
|
||||
// The fact that we don't check if the item_id still exist in the corresponding item table, means
|
||||
@@ -85,24 +115,24 @@ class BaseItem extends BaseModel {
|
||||
return r.total;
|
||||
}
|
||||
|
||||
static systemPath(itemOrId, extension = null) {
|
||||
static systemPath(itemOrId: any, extension: string = null) {
|
||||
if (extension === null) extension = 'md';
|
||||
|
||||
if (typeof itemOrId === 'string') return `${itemOrId}.${extension}`;
|
||||
else return `${itemOrId.id}.${extension}`;
|
||||
}
|
||||
|
||||
static isSystemPath(path) {
|
||||
static isSystemPath(path: string) {
|
||||
// 1b175bb38bba47baac22b0b47f778113.md
|
||||
if (!path || !path.length) return false;
|
||||
let p = path.split('/');
|
||||
let p: any = path.split('/');
|
||||
p = p[p.length - 1];
|
||||
p = p.split('.');
|
||||
if (p.length != 2) return false;
|
||||
return p[0].length == 32 && p[1] == 'md';
|
||||
}
|
||||
|
||||
static itemClass(item) {
|
||||
static itemClass(item: any): any {
|
||||
if (!item) throw new Error('Item cannot be null');
|
||||
|
||||
if (typeof item === 'object') {
|
||||
@@ -118,7 +148,7 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
// Returns the IDs of the items that have been synced at least once
|
||||
static async syncedItemIds(syncTarget) {
|
||||
static async syncedItemIds(syncTarget: number) {
|
||||
if (!syncTarget) throw new Error('No syncTarget specified');
|
||||
const temp = await this.db().selectAll('SELECT item_id FROM sync_items WHERE sync_time > 0 AND sync_target = ?', [syncTarget]);
|
||||
const output = [];
|
||||
@@ -128,25 +158,25 @@ class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async allSyncItems(syncTarget) {
|
||||
static async allSyncItems(syncTarget: number) {
|
||||
const output = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_target = ?', [syncTarget]);
|
||||
return output;
|
||||
}
|
||||
|
||||
static pathToId(path) {
|
||||
static pathToId(path: string) {
|
||||
const p = path.split('/');
|
||||
const s = p[p.length - 1].split('.');
|
||||
let name = s[0];
|
||||
let name: any = s[0];
|
||||
if (!name) return name;
|
||||
name = name.split('-');
|
||||
return name[name.length - 1];
|
||||
}
|
||||
|
||||
static loadItemByPath(path) {
|
||||
static loadItemByPath(path: string) {
|
||||
return this.loadItemById(this.pathToId(path));
|
||||
}
|
||||
|
||||
static async loadItemById(id) {
|
||||
static async loadItemById(id: string) {
|
||||
const classes = this.syncItemClassNames();
|
||||
for (let i = 0; i < classes.length; i++) {
|
||||
const item = await this.getClass(classes[i]).load(id);
|
||||
@@ -155,11 +185,11 @@ class BaseItem extends BaseModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
static async loadItemsByIds(ids) {
|
||||
static async loadItemsByIds(ids: string[]) {
|
||||
if (!ids.length) return [];
|
||||
|
||||
const classes = this.syncItemClassNames();
|
||||
let output = [];
|
||||
let output: any[] = [];
|
||||
for (let i = 0; i < classes.length; i++) {
|
||||
const ItemClass = this.getClass(classes[i]);
|
||||
const sql = `SELECT * FROM ${ItemClass.tableName()} WHERE id IN ("${ids.join('","')}")`;
|
||||
@@ -169,26 +199,26 @@ class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
static loadItemByField(itemType, field, value) {
|
||||
static loadItemByField(itemType: number, field: string, value: any) {
|
||||
const ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.loadByField(field, value);
|
||||
}
|
||||
|
||||
static loadItem(itemType, id) {
|
||||
static loadItem(itemType: ModelType, id: string) {
|
||||
const ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.load(id);
|
||||
}
|
||||
|
||||
static deleteItem(itemType, id) {
|
||||
static deleteItem(itemType: ModelType, id: string) {
|
||||
const ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.delete(id);
|
||||
}
|
||||
|
||||
static async delete(id, options = null) {
|
||||
static async delete(id: string, options: any = null) {
|
||||
return this.batchDelete([id], options);
|
||||
}
|
||||
|
||||
static async batchDelete(ids, options = null) {
|
||||
static async batchDelete(ids: string[], options: any = null) {
|
||||
if (!options) options = {};
|
||||
let trackDeleted = true;
|
||||
if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
|
||||
@@ -198,7 +228,7 @@ class BaseItem extends BaseModel {
|
||||
let conflictNoteIds = [];
|
||||
if (this.modelType() == BaseModel.TYPE_NOTE) {
|
||||
const conflictNotes = await this.db().selectAll(`SELECT id FROM notes WHERE id IN ("${ids.join('","')}") AND is_conflict = 1`);
|
||||
conflictNoteIds = conflictNotes.map(n => {
|
||||
conflictNoteIds = conflictNotes.map((n: NoteEntity) => {
|
||||
return n.id;
|
||||
});
|
||||
}
|
||||
@@ -234,20 +264,20 @@ class BaseItem extends BaseModel {
|
||||
// - Client 1 syncs with target 2 only => the note is *not* deleted from target 2 because no information
|
||||
// that it was previously deleted exist (deleted_items entry has been deleted).
|
||||
// The solution would be to permanently store the list of deleted items on each client.
|
||||
static deletedItems(syncTarget) {
|
||||
static deletedItems(syncTarget: number) {
|
||||
return this.db().selectAll('SELECT * FROM deleted_items WHERE sync_target = ?', [syncTarget]);
|
||||
}
|
||||
|
||||
static async deletedItemCount(syncTarget) {
|
||||
static async deletedItemCount(syncTarget: number) {
|
||||
const r = await this.db().selectOne('SELECT count(*) as total FROM deleted_items WHERE sync_target = ?', [syncTarget]);
|
||||
return r['total'];
|
||||
}
|
||||
|
||||
static remoteDeletedItem(syncTarget, itemId) {
|
||||
static remoteDeletedItem(syncTarget: number, itemId: string) {
|
||||
return this.db().exec('DELETE FROM deleted_items WHERE item_id = ? AND sync_target = ?', [itemId, syncTarget]);
|
||||
}
|
||||
|
||||
static serialize_format(propName, propValue) {
|
||||
static serialize_format(propName: string, propValue: any) {
|
||||
if (['created_time', 'updated_time', 'sync_time', 'user_updated_time', 'user_created_time'].indexOf(propName) >= 0) {
|
||||
if (!propValue) return '';
|
||||
propValue = `${moment.unix(propValue / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS')}Z`;
|
||||
@@ -269,7 +299,7 @@ class BaseItem extends BaseModel {
|
||||
.replace(/\r/g, '\\r');
|
||||
}
|
||||
|
||||
static unserialize_format(type, propName, propValue) {
|
||||
static unserialize_format(type: ModelType, propName: string, propValue: any) {
|
||||
if (propName[propName.length - 1] == '_') return propValue; // Private property
|
||||
|
||||
const ItemClass = this.itemClass(type);
|
||||
@@ -297,7 +327,7 @@ class BaseItem extends BaseModel {
|
||||
: propValue;
|
||||
}
|
||||
|
||||
static async serialize(item, shownKeys = null) {
|
||||
static async serialize(item: any, shownKeys: any[] = null) {
|
||||
if (shownKeys === null) {
|
||||
shownKeys = this.itemClass(item).fieldNames();
|
||||
shownKeys.push('type_');
|
||||
@@ -305,7 +335,7 @@ class BaseItem extends BaseModel {
|
||||
|
||||
item = this.filter(item);
|
||||
|
||||
const output = {};
|
||||
const output: any = {};
|
||||
|
||||
if ('title' in item && shownKeys.indexOf('title') >= 0) {
|
||||
output.title = item.title;
|
||||
@@ -352,7 +382,7 @@ class BaseItem extends BaseModel {
|
||||
return this.revisionService_;
|
||||
}
|
||||
|
||||
static async serializeForSync(item) {
|
||||
static async serializeForSync(item: any) {
|
||||
const ItemClass = this.itemClass(item);
|
||||
const shownKeys = ItemClass.fieldNames();
|
||||
shownKeys.push('type_');
|
||||
@@ -366,7 +396,7 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
if (item.encryption_applied) {
|
||||
const e = new Error('Trying to encrypt item that is already encrypted');
|
||||
const e: any = new Error('Trying to encrypt item that is already encrypted');
|
||||
e.code = 'cannotEncryptEncrypted';
|
||||
throw e;
|
||||
}
|
||||
@@ -386,7 +416,7 @@ class BaseItem extends BaseModel {
|
||||
// List of keys that won't be encrypted - mostly foreign keys required to link items
|
||||
// with each others and timestamp required for synchronisation.
|
||||
const keepKeys = ['id', 'note_id', 'tag_id', 'parent_id', 'updated_time', 'type_'];
|
||||
const reducedItem = {};
|
||||
const reducedItem: any = {};
|
||||
|
||||
for (let i = 0; i < keepKeys.length; i++) {
|
||||
const n = keepKeys[i];
|
||||
@@ -399,7 +429,7 @@ class BaseItem extends BaseModel {
|
||||
return ItemClass.serialize(reducedItem);
|
||||
}
|
||||
|
||||
static async decrypt(item) {
|
||||
static async decrypt(item: any) {
|
||||
if (!item.encryption_cipher_text) throw new Error(`Item is not encrypted: ${item.id}`);
|
||||
|
||||
const ItemClass = this.itemClass(item);
|
||||
@@ -413,11 +443,11 @@ class BaseItem extends BaseModel {
|
||||
return ItemClass.save(plainItem, { autoTimestamp: false, changeSource: ItemChange.SOURCE_DECRYPTION });
|
||||
}
|
||||
|
||||
static async unserialize(content) {
|
||||
static async unserialize(content: string) {
|
||||
const lines = content.split('\n');
|
||||
let output = {};
|
||||
let output: any = {};
|
||||
let state = 'readingProps';
|
||||
const body = [];
|
||||
const body: string[] = [];
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
let line = lines[i];
|
||||
@@ -506,7 +536,7 @@ class BaseItem extends BaseModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
static async itemsThatNeedDecryption(exclusions = [], limit = 100) {
|
||||
static async itemsThatNeedDecryption(exclusions: string[] = [], limit = 100): Promise<ItemsThatNeedDecryptionResult> {
|
||||
const classNames = this.encryptableItemClassNames();
|
||||
|
||||
for (let i = 0; i < classNames.length; i++) {
|
||||
@@ -546,7 +576,7 @@ class BaseItem extends BaseModel {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
|
||||
static async itemsThatNeedSync(syncTarget, limit = 100) {
|
||||
static async itemsThatNeedSync(syncTarget: number, limit = 100) {
|
||||
const classNames = this.syncItemClassNames();
|
||||
|
||||
for (let i = 0; i < classNames.length; i++) {
|
||||
@@ -560,7 +590,7 @@ class BaseItem extends BaseModel {
|
||||
// // CHANGED:
|
||||
// 'SELECT * FROM [ITEMS] items JOIN sync_items s ON s.item_id = items.id WHERE sync_target = ? AND'
|
||||
|
||||
let extraWhere = [];
|
||||
let extraWhere: any = [];
|
||||
if (className == 'Note') extraWhere.push('is_conflict = 0');
|
||||
if (className == 'Resource') extraWhere.push('encryption_blob_encrypted = 0');
|
||||
if (ItemClass.encryptionSupported()) extraWhere.push('encryption_applied = 0');
|
||||
@@ -636,7 +666,7 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
static syncItemClassNames() {
|
||||
return BaseItem.syncItemDefinitions_.map(def => {
|
||||
return BaseItem.syncItemDefinitions_.map((def: any) => {
|
||||
return def.className;
|
||||
});
|
||||
}
|
||||
@@ -652,19 +682,19 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
static syncItemTypes() {
|
||||
return BaseItem.syncItemDefinitions_.map(def => {
|
||||
return BaseItem.syncItemDefinitions_.map((def: any) => {
|
||||
return def.type;
|
||||
});
|
||||
}
|
||||
|
||||
static modelTypeToClassName(type) {
|
||||
static modelTypeToClassName(type: number) {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
if (BaseItem.syncItemDefinitions_[i].type == type) return BaseItem.syncItemDefinitions_[i].className;
|
||||
}
|
||||
throw new Error(`Invalid type: ${type}`);
|
||||
}
|
||||
|
||||
static async syncDisabledItems(syncTargetId) {
|
||||
static async syncDisabledItems(syncTargetId: number) {
|
||||
const rows = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_disabled = 1 AND sync_target = ?', [syncTargetId]);
|
||||
const output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
@@ -681,7 +711,7 @@ class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
static updateSyncTimeQueries(syncTarget, item, syncTime, syncDisabled = false, syncDisabledReason = '', itemLocation = null) {
|
||||
static updateSyncTimeQueries(syncTarget: number, item: any, syncTime: number, syncDisabled = false, syncDisabledReason = '', itemLocation: number = null) {
|
||||
const itemType = item.type_;
|
||||
const itemId = item.id;
|
||||
if (!itemType || !itemId || syncTime === undefined) throw new Error(sprintf('Invalid parameters in updateSyncTimeQueries(): %d, %s, %d', syncTarget, JSON.stringify(item), syncTime));
|
||||
@@ -700,12 +730,12 @@ class BaseItem extends BaseModel {
|
||||
];
|
||||
}
|
||||
|
||||
static async saveSyncTime(syncTarget, item, syncTime) {
|
||||
static async saveSyncTime(syncTarget: number, item: any, syncTime: number) {
|
||||
const queries = this.updateSyncTimeQueries(syncTarget, item, syncTime);
|
||||
return this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
static async saveSyncDisabled(syncTargetId, item, syncDisabledReason, itemLocation = null) {
|
||||
static async saveSyncDisabled(syncTargetId: number, item: any, syncDisabledReason: string, itemLocation: number = null) {
|
||||
const syncTime = 'sync_time' in item ? item.sync_time : 0;
|
||||
const queries = this.updateSyncTimeQueries(syncTargetId, item, syncTime, true, syncDisabledReason, itemLocation);
|
||||
return this.db().transactionExecBatch(queries);
|
||||
@@ -731,7 +761,7 @@ class BaseItem extends BaseModel {
|
||||
await this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
static displayTitle(item) {
|
||||
static displayTitle(item: any) {
|
||||
if (!item) return '';
|
||||
if (item.encryption_applied) return `🔑 ${_('Encrypted')}`;
|
||||
return item.title ? item.title : _('Untitled');
|
||||
@@ -753,7 +783,7 @@ class BaseItem extends BaseModel {
|
||||
);
|
||||
|
||||
const items = await ItemClass.modelSelectAll(sql);
|
||||
const ids = items.map(item => {
|
||||
const ids = items.map((item: any) => {
|
||||
return item.id;
|
||||
});
|
||||
if (!ids.length) continue;
|
||||
@@ -762,7 +792,7 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
static async updateShareStatus(item, isShared) {
|
||||
static async updateShareStatus(item: any, isShared: boolean) {
|
||||
if (!item.id || !item.type_) throw new Error('Item must have an ID and a type');
|
||||
if (!!item.is_shared === !!isShared) return false;
|
||||
const ItemClass = this.getClassByItemType(item.type_);
|
||||
@@ -781,7 +811,7 @@ class BaseItem extends BaseModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
static async forceSync(itemId) {
|
||||
static async forceSync(itemId: string) {
|
||||
await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id = ?', [itemId]);
|
||||
}
|
||||
|
||||
@@ -789,7 +819,7 @@ class BaseItem extends BaseModel {
|
||||
await this.db().exec('UPDATE sync_items SET force_sync = 1');
|
||||
}
|
||||
|
||||
static async save(o, options = null) {
|
||||
static async save(o: any, options: any = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.userSideValidation === true) {
|
||||
@@ -799,7 +829,7 @@ class BaseItem extends BaseModel {
|
||||
return super.save(o, options);
|
||||
}
|
||||
|
||||
static markdownTag(itemOrId) {
|
||||
static markdownTag(itemOrId: any) {
|
||||
const item = typeof itemOrId === 'object' ? itemOrId : {
|
||||
id: itemOrId,
|
||||
title: '',
|
||||
@@ -813,23 +843,9 @@ class BaseItem extends BaseModel {
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
static isMarkdownTag(md) {
|
||||
static isMarkdownTag(md: any) {
|
||||
if (!md) return false;
|
||||
return !!md.match(/^\[.*?\]\(:\/[0-9a-zA-Z]{32}\)$/);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BaseItem.encryptionService_ = null;
|
||||
BaseItem.revisionService_ = null;
|
||||
|
||||
// Also update:
|
||||
// - itemsThatNeedSync()
|
||||
// - syncedItems()
|
||||
|
||||
BaseItem.syncItemDefinitions_ = [{ type: BaseModel.TYPE_NOTE, className: 'Note' }, { type: BaseModel.TYPE_FOLDER, className: 'Folder' }, { type: BaseModel.TYPE_RESOURCE, className: 'Resource' }, { type: BaseModel.TYPE_TAG, className: 'Tag' }, { type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' }, { type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' }, { type: BaseModel.TYPE_REVISION, className: 'Revision' }];
|
||||
|
||||
BaseItem.SYNC_ITEM_LOCATION_LOCAL = 1;
|
||||
BaseItem.SYNC_ITEM_LOCATION_REMOTE = 2;
|
||||
|
||||
module.exports = BaseItem;
|
||||
@@ -1,12 +1,18 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
const time = require('../time').default;
|
||||
const Note = require('./Note.js');
|
||||
import { FolderEntity } from '../services/database/types';
|
||||
import BaseModel from '../BaseModel';
|
||||
import time from '../time';
|
||||
import { _ } from '../locale';
|
||||
|
||||
import Note from './Note';
|
||||
const { Database } = require('../database.js');
|
||||
const { _ } = require('../locale');
|
||||
const BaseItem = require('./BaseItem.js');
|
||||
import BaseItem from './BaseItem';
|
||||
const { substrWithEllipsis } = require('../string-utils.js');
|
||||
|
||||
class Folder extends BaseItem {
|
||||
interface FolderEntityWithChildren extends FolderEntity {
|
||||
children?: FolderEntity[];
|
||||
}
|
||||
|
||||
export default class Folder extends BaseItem {
|
||||
static tableName() {
|
||||
return 'folders';
|
||||
}
|
||||
@@ -15,15 +21,15 @@ class Folder extends BaseItem {
|
||||
return BaseModel.TYPE_FOLDER;
|
||||
}
|
||||
|
||||
static newFolder() {
|
||||
static newFolder(): FolderEntity {
|
||||
return {
|
||||
id: null,
|
||||
title: '',
|
||||
};
|
||||
}
|
||||
|
||||
static fieldToLabel(field) {
|
||||
const fieldsToLabels = {
|
||||
static fieldToLabel(field: string) {
|
||||
const fieldsToLabels: any = {
|
||||
title: _('title'),
|
||||
last_note_user_updated_time: _('updated date'),
|
||||
};
|
||||
@@ -31,7 +37,7 @@ class Folder extends BaseItem {
|
||||
return field in fieldsToLabels ? fieldsToLabels[field] : field;
|
||||
}
|
||||
|
||||
static noteIds(parentId, options = null) {
|
||||
static noteIds(parentId: string, options: any = null) {
|
||||
options = Object.assign({}, {
|
||||
includeConflicts: false,
|
||||
}, options);
|
||||
@@ -43,7 +49,7 @@ class Folder extends BaseItem {
|
||||
|
||||
return this.db()
|
||||
.selectAll(`SELECT id FROM notes WHERE ${where.join(' AND ')}`, [parentId])
|
||||
.then(rows => {
|
||||
.then((rows: any[]) => {
|
||||
const output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
@@ -53,22 +59,22 @@ class Folder extends BaseItem {
|
||||
});
|
||||
}
|
||||
|
||||
static async subFolderIds(parentId) {
|
||||
static async subFolderIds(parentId: string) {
|
||||
const rows = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [parentId]);
|
||||
return rows.map(r => r.id);
|
||||
return rows.map((r: FolderEntity) => r.id);
|
||||
}
|
||||
|
||||
static async noteCount(parentId) {
|
||||
static async noteCount(parentId: string) {
|
||||
const r = await this.db().selectOne('SELECT count(*) as total FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]);
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static markNotesAsConflict(parentId) {
|
||||
static markNotesAsConflict(parentId: string) {
|
||||
const query = Database.updateQuery('notes', { is_conflict: 1 }, { parent_id: parentId });
|
||||
return this.db().exec(query);
|
||||
}
|
||||
|
||||
static async delete(folderId, options = null) {
|
||||
static async delete(folderId: string, options: any = null) {
|
||||
if (!options) options = {};
|
||||
if (!('deleteChildren' in options)) options.deleteChildren = true;
|
||||
|
||||
@@ -114,8 +120,8 @@ class Folder extends BaseItem {
|
||||
|
||||
// Calculates note counts for all folders and adds the note_count attribute to each folder
|
||||
// Note: this only calculates the overall number of nodes for this folder and all its descendants
|
||||
static async addNoteCounts(folders, includeCompletedTodos = true) {
|
||||
const foldersById = {};
|
||||
static async addNoteCounts(folders: any[], includeCompletedTodos = true) {
|
||||
const foldersById: any = {};
|
||||
for (const f of folders) {
|
||||
foldersById[f.id] = f;
|
||||
|
||||
@@ -137,7 +143,7 @@ class Folder extends BaseItem {
|
||||
`;
|
||||
|
||||
const noteCounts = await this.db().selectAll(sql);
|
||||
noteCounts.forEach((noteCount) => {
|
||||
noteCounts.forEach((noteCount: any) => {
|
||||
let parentId = noteCount.folder_id;
|
||||
do {
|
||||
const folder = foldersById[parentId];
|
||||
@@ -155,18 +161,18 @@ class Folder extends BaseItem {
|
||||
|
||||
// Folders that contain notes that have been modified recently go on top.
|
||||
// The remaining folders, that don't contain any notes are sorted by their own user_updated_time
|
||||
static async orderByLastModified(folders, dir = 'DESC') {
|
||||
static async orderByLastModified(folders: FolderEntity[], dir = 'DESC') {
|
||||
dir = dir.toUpperCase();
|
||||
const sql = 'select parent_id, max(user_updated_time) content_updated_time from notes where parent_id != "" group by parent_id';
|
||||
const rows = await this.db().selectAll(sql);
|
||||
|
||||
const folderIdToTime = {};
|
||||
const folderIdToTime: Record<string, number> = {};
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
folderIdToTime[row.parent_id] = row.content_updated_time;
|
||||
}
|
||||
|
||||
const findFolderParent = folderId => {
|
||||
const findFolderParent = (folderId: string) => {
|
||||
const folder = BaseModel.byId(folders, folderId);
|
||||
if (!folder) return null; // For the rare case of notes that are associated with a no longer existing folder
|
||||
if (!folder.parent_id) return null;
|
||||
@@ -180,7 +186,7 @@ class Folder extends BaseItem {
|
||||
return null;
|
||||
};
|
||||
|
||||
const applyChildTimeToParent = folderId => {
|
||||
const applyChildTimeToParent = (folderId: string) => {
|
||||
const parent = findFolderParent(folderId);
|
||||
if (!parent) return;
|
||||
|
||||
@@ -213,7 +219,7 @@ class Folder extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async all(options = null) {
|
||||
static async all(options: any = null) {
|
||||
const output = await super.all(options);
|
||||
if (options && options.includeConflictFolder) {
|
||||
const conflictCount = await Note.conflictedCount();
|
||||
@@ -222,24 +228,22 @@ class Folder extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async childrenIds(folderId, recursive) {
|
||||
if (recursive === false) throw new Error('Not implemented');
|
||||
|
||||
static async childrenIds(folderId: string) {
|
||||
const folders = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [folderId]);
|
||||
|
||||
let output = [];
|
||||
let output: string[] = [];
|
||||
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const f = folders[i];
|
||||
output.push(f.id);
|
||||
const subChildrenIds = await this.childrenIds(f.id, true);
|
||||
const subChildrenIds = await this.childrenIds(f.id);
|
||||
output = output.concat(subChildrenIds);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async expandTree(folders, parentId) {
|
||||
static async expandTree(folders: FolderEntity[], parentId: string) {
|
||||
const folderPath = await this.folderPath(folders, parentId);
|
||||
folderPath.pop(); // We don't expand the leaft notebook
|
||||
|
||||
@@ -252,11 +256,11 @@ class Folder extends BaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
static async allAsTree(folders = null, options = null) {
|
||||
static async allAsTree(folders: FolderEntity[] = null, options: any = null) {
|
||||
const all = folders ? folders : await this.all(options);
|
||||
|
||||
// https://stackoverflow.com/a/49387427/561309
|
||||
function getNestedChildren(models, parentId) {
|
||||
function getNestedChildren(models: FolderEntityWithChildren[], parentId: string) {
|
||||
const nestedTreeStructure = [];
|
||||
const length = models.length;
|
||||
|
||||
@@ -280,8 +284,8 @@ class Folder extends BaseItem {
|
||||
return getNestedChildren(all, '');
|
||||
}
|
||||
|
||||
static folderPath(folders, folderId) {
|
||||
const idToFolders = {};
|
||||
static folderPath(folders: FolderEntity[], folderId: string) {
|
||||
const idToFolders: Record<string, FolderEntity> = {};
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
idToFolders[folders[i].id] = folders[i];
|
||||
}
|
||||
@@ -299,7 +303,7 @@ class Folder extends BaseItem {
|
||||
return path;
|
||||
}
|
||||
|
||||
static folderPathString(folders, folderId, maxTotalLength = 80) {
|
||||
static folderPathString(folders: FolderEntity[], folderId: string, maxTotalLength = 80) {
|
||||
const path = this.folderPath(folders, folderId);
|
||||
|
||||
let currentTotalLength = 0;
|
||||
@@ -320,8 +324,8 @@ class Folder extends BaseItem {
|
||||
return output.join(' / ');
|
||||
}
|
||||
|
||||
static buildTree(folders) {
|
||||
const idToFolders = {};
|
||||
static buildTree(folders: FolderEntity[]) {
|
||||
const idToFolders: Record<string, any> = {};
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
idToFolders[folders[i].id] = Object.assign({}, folders[i]);
|
||||
idToFolders[folders[i].id].children = [];
|
||||
@@ -348,19 +352,20 @@ class Folder extends BaseItem {
|
||||
return rootFolders;
|
||||
}
|
||||
|
||||
static async sortFolderTree(folders) {
|
||||
static async sortFolderTree(folders: FolderEntityWithChildren[] = null) {
|
||||
const output = folders ? folders : await this.allAsTree();
|
||||
|
||||
const sortFoldersAlphabetically = (folders) => {
|
||||
folders.sort((a, b) => {
|
||||
if (a.parentId === b.parentId) {
|
||||
const sortFoldersAlphabetically = (folders: FolderEntityWithChildren[]) => {
|
||||
folders.sort((a: FolderEntityWithChildren, b: FolderEntityWithChildren) => {
|
||||
if (a.parent_id === b.parent_id) {
|
||||
return a.title.localeCompare(b.title, undefined, { sensitivity: 'accent' });
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return folders;
|
||||
};
|
||||
|
||||
const sortFolders = (folders) => {
|
||||
const sortFolders = (folders: FolderEntityWithChildren[]) => {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
if (folder.children) {
|
||||
@@ -375,7 +380,7 @@ class Folder extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static load(id) {
|
||||
static load(id: string) {
|
||||
if (id == this.conflictFolderId()) return this.conflictFolder();
|
||||
return super.load(id);
|
||||
}
|
||||
@@ -384,7 +389,7 @@ class Folder extends BaseItem {
|
||||
return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1');
|
||||
}
|
||||
|
||||
static async canNestUnder(folderId, targetFolderId) {
|
||||
static async canNestUnder(folderId: string, targetFolderId: string) {
|
||||
if (folderId === targetFolderId) return false;
|
||||
|
||||
const conflictFolderId = Folder.conflictFolderId();
|
||||
@@ -402,7 +407,7 @@ class Folder extends BaseItem {
|
||||
return true;
|
||||
}
|
||||
|
||||
static async moveToFolder(folderId, targetFolderId) {
|
||||
static async moveToFolder(folderId: string, targetFolderId: string) {
|
||||
if (!(await this.canNestUnder(folderId, targetFolderId))) throw new Error(_('Cannot move notebook to this location'));
|
||||
|
||||
// When moving a note to a different folder, the user timestamp is not updated.
|
||||
@@ -421,7 +426,7 @@ class Folder extends BaseItem {
|
||||
// manually creating a folder. They shouldn't be done for example when the folders
|
||||
// are being synced to avoid any strange side-effects. 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: FolderEntity, options: any = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.userSideValidation === true) {
|
||||
@@ -458,7 +463,7 @@ class Folder extends BaseItem {
|
||||
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: FolderEntity) => {
|
||||
this.dispatch({
|
||||
type: 'FOLDER_UPDATE_ONE',
|
||||
item: folder,
|
||||
@@ -467,5 +472,3 @@ class Folder extends BaseItem {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Folder;
|
||||
@@ -1,9 +1,21 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
import BaseModel, { ModelType } from '../BaseModel';
|
||||
import shim from '../shim';
|
||||
import eventManager from '../eventManager';
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
const shim = require('../shim').default;
|
||||
const eventManager = require('../eventManager').default;
|
||||
|
||||
class ItemChange extends BaseModel {
|
||||
export default class ItemChange extends BaseModel {
|
||||
|
||||
private static addChangeMutex_: any = new Mutex();
|
||||
private static saveCalls_: any[] = [];
|
||||
|
||||
public static TYPE_CREATE = 1;
|
||||
public static TYPE_UPDATE = 2;
|
||||
public static TYPE_DELETE = 3;
|
||||
|
||||
public static SOURCE_UNSPECIFIED = 1;
|
||||
public static SOURCE_SYNC = 2;
|
||||
public static SOURCE_DECRYPTION = 2; // CAREFUL - SAME ID AS SOURCE_SYNC!
|
||||
|
||||
static tableName() {
|
||||
return 'item_changes';
|
||||
}
|
||||
@@ -12,7 +24,7 @@ class ItemChange extends BaseModel {
|
||||
return BaseModel.TYPE_ITEM_CHANGE;
|
||||
}
|
||||
|
||||
static async add(itemType, itemId, type, changeSource = null, beforeChangeItemJson = null) {
|
||||
static async add(itemType: ModelType, itemId: string, type: number, changeSource: any = null, beforeChangeItemJson: string = null) {
|
||||
if (changeSource === null) changeSource = ItemChange.SOURCE_UNSPECIFIED;
|
||||
if (!beforeChangeItemJson) beforeChangeItemJson = '';
|
||||
|
||||
@@ -57,27 +69,14 @@ class ItemChange extends BaseModel {
|
||||
const iid = shim.setInterval(() => {
|
||||
if (!ItemChange.saveCalls_.length) {
|
||||
shim.clearInterval(iid);
|
||||
resolve();
|
||||
resolve(null);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
static async deleteOldChanges(lowestChangeId) {
|
||||
static async deleteOldChanges(lowestChangeId: number) {
|
||||
if (!lowestChangeId) return;
|
||||
return this.db().exec('DELETE FROM item_changes WHERE id <= ?', [lowestChangeId]);
|
||||
}
|
||||
}
|
||||
|
||||
ItemChange.addChangeMutex_ = new Mutex();
|
||||
ItemChange.saveCalls_ = [];
|
||||
|
||||
ItemChange.TYPE_CREATE = 1;
|
||||
ItemChange.TYPE_UPDATE = 2;
|
||||
ItemChange.TYPE_DELETE = 3;
|
||||
|
||||
ItemChange.SOURCE_UNSPECIFIED = 1;
|
||||
ItemChange.SOURCE_SYNC = 2;
|
||||
ItemChange.SOURCE_DECRYPTION = 2; // CAREFUL - SAME ID AS SOURCE_SYNC!
|
||||
|
||||
module.exports = ItemChange;
|
||||
@@ -1,7 +1,8 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
const BaseItem = require('./BaseItem.js');
|
||||
import BaseModel from '../BaseModel';
|
||||
import { MasterKeyEntity } from '../services/database/types';
|
||||
import BaseItem from './BaseItem';
|
||||
|
||||
class MasterKey extends BaseItem {
|
||||
export default class MasterKey extends BaseItem {
|
||||
static tableName() {
|
||||
return 'master_keys';
|
||||
}
|
||||
@@ -18,11 +19,11 @@ class MasterKey extends BaseItem {
|
||||
return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)');
|
||||
}
|
||||
|
||||
static allWithoutEncryptionMethod(masterKeys, method) {
|
||||
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) {
|
||||
return masterKeys.filter(m => m.encryption_method !== method);
|
||||
}
|
||||
|
||||
static async save(o, options = null) {
|
||||
static async save(o: MasterKeyEntity, options: any = null) {
|
||||
return super.save(o, options).then(item => {
|
||||
this.dispatch({
|
||||
type: 'MASTERKEY_UPDATE_ONE',
|
||||
@@ -32,5 +33,3 @@ class MasterKey extends BaseItem {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MasterKey;
|
||||
@@ -1,12 +1,12 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
import BaseModel from '../BaseModel';
|
||||
|
||||
const migrationScripts = {
|
||||
const migrationScripts: Record<number, any> = {
|
||||
20: require('../migrations/20.js'),
|
||||
27: require('../migrations/27.js'),
|
||||
33: require('../migrations/33.js'),
|
||||
};
|
||||
|
||||
class Migration extends BaseModel {
|
||||
export default class Migration extends BaseModel {
|
||||
static tableName() {
|
||||
return 'migrations';
|
||||
}
|
||||
@@ -19,10 +19,8 @@ class Migration extends BaseModel {
|
||||
return this.modelSelectAll('SELECT * FROM migrations ORDER BY number ASC');
|
||||
}
|
||||
|
||||
static script(number) {
|
||||
static script(number: number) {
|
||||
if (!migrationScripts[number]) throw new Error('Migration script has not been added to "migrationScripts" array');
|
||||
return migrationScripts[number];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Migration;
|
||||
@@ -1,28 +1,36 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
import BaseModel, { ModelType } from '../BaseModel';
|
||||
import BaseItem from './BaseItem';
|
||||
import ItemChange from './ItemChange';
|
||||
import Setting from './Setting';
|
||||
import shim from '../shim';
|
||||
import time from '../time';
|
||||
import markdownUtils from '../markdownUtils';
|
||||
import { NoteEntity } from '../services/database/types';
|
||||
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const BaseItem = require('./BaseItem.js');
|
||||
const ItemChange = require('./ItemChange.js');
|
||||
const Resource = require('./Resource.js');
|
||||
const Setting = require('./Setting').default;
|
||||
const shim = require('../shim').default;
|
||||
import Resource from './Resource';
|
||||
const { pregQuote } = require('../string-utils.js');
|
||||
const time = require('../time').default;
|
||||
const { _ } = require('../locale');
|
||||
const ArrayUtils = require('../ArrayUtils.js');
|
||||
const lodash = require('lodash');
|
||||
const urlUtils = require('../urlUtils.js');
|
||||
const markdownUtils = require('../markdownUtils').default;
|
||||
const { isImageMimeType } = require('../resourceUtils');
|
||||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
const { ALL_NOTES_FILTER_ID } = require('../reserved-ids');
|
||||
|
||||
class Note extends BaseItem {
|
||||
export default class Note extends BaseItem {
|
||||
|
||||
public static updateGeolocationEnabled_ = true;
|
||||
private static geolocationUpdating_ = false;
|
||||
private static geolocationCache_: any;
|
||||
private static dueDateObjects_: any;
|
||||
|
||||
static tableName() {
|
||||
return 'notes';
|
||||
}
|
||||
|
||||
static fieldToLabel(field) {
|
||||
const fieldsToLabels = {
|
||||
static fieldToLabel(field: string) {
|
||||
const fieldsToLabels: Record<string, string> = {
|
||||
title: _('title'),
|
||||
user_updated_time: _('updated date'),
|
||||
user_created_time: _('created date'),
|
||||
@@ -32,11 +40,11 @@ class Note extends BaseItem {
|
||||
return field in fieldsToLabels ? fieldsToLabels[field] : field;
|
||||
}
|
||||
|
||||
static async serializeForEdit(note) {
|
||||
static async serializeForEdit(note: NoteEntity) {
|
||||
return this.replaceResourceInternalToExternalLinks(await super.serialize(note, ['title', 'body']));
|
||||
}
|
||||
|
||||
static async unserializeForEdit(content) {
|
||||
static async unserializeForEdit(content: string) {
|
||||
content += `\n\ntype_: ${BaseModel.TYPE_NOTE}`;
|
||||
const output = await super.unserialize(content);
|
||||
if (!output.title) output.title = '';
|
||||
@@ -45,14 +53,14 @@ class Note extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async serializeAllProps(note) {
|
||||
static async serializeAllProps(note: NoteEntity) {
|
||||
const fieldNames = this.fieldNames();
|
||||
fieldNames.push('type_');
|
||||
lodash.pull(fieldNames, 'title', 'body');
|
||||
return super.serialize(note, fieldNames);
|
||||
}
|
||||
|
||||
static minimalSerializeForDisplay(note) {
|
||||
static minimalSerializeForDisplay(note: NoteEntity) {
|
||||
const n = Object.assign({}, note);
|
||||
|
||||
const fieldNames = this.fieldNames();
|
||||
@@ -80,21 +88,21 @@ class Note extends BaseItem {
|
||||
return super.serialize(n, fieldNames);
|
||||
}
|
||||
|
||||
static defaultTitle(noteBody) {
|
||||
static defaultTitle(noteBody: string) {
|
||||
return this.defaultTitleFromBody(noteBody);
|
||||
}
|
||||
|
||||
static defaultTitleFromBody(body) {
|
||||
static defaultTitleFromBody(body: string) {
|
||||
return markdownUtils.titleFromBody(body);
|
||||
}
|
||||
|
||||
static geolocationUrl(note) {
|
||||
static geolocationUrl(note: NoteEntity) {
|
||||
if (!('latitude' in note) || !('longitude' in note)) throw new Error('Latitude or longitude is missing');
|
||||
if (!Number(note.latitude) && !Number(note.longitude)) throw new Error(_('This note does not have geolocation information.'));
|
||||
return this.geoLocationUrlFromLatLong(note.latitude, note.longitude);
|
||||
}
|
||||
|
||||
static geoLocationUrlFromLatLong(lat, long) {
|
||||
static geoLocationUrlFromLatLong(lat: number, long: number) {
|
||||
return sprintf('https://www.openstreetmap.org/?lat=%s&lon=%s&zoom=20', lat, long);
|
||||
}
|
||||
|
||||
@@ -102,21 +110,21 @@ class Note extends BaseItem {
|
||||
return BaseModel.TYPE_NOTE;
|
||||
}
|
||||
|
||||
static linkedItemIds(body) {
|
||||
static linkedItemIds(body: string) {
|
||||
if (!body || body.length <= 32) return [];
|
||||
|
||||
const links = urlUtils.extractResourceUrls(body);
|
||||
const itemIds = links.map(l => l.itemId);
|
||||
const itemIds = links.map((l: any) => l.itemId);
|
||||
return ArrayUtils.unique(itemIds);
|
||||
}
|
||||
|
||||
static async linkedItems(body) {
|
||||
static async linkedItems(body: string) {
|
||||
const itemIds = this.linkedItemIds(body);
|
||||
const r = await BaseItem.loadItemsByIds(itemIds);
|
||||
return r;
|
||||
}
|
||||
|
||||
static async linkedItemIdsByType(type, body) {
|
||||
static async linkedItemIdsByType(type: ModelType, body: string) {
|
||||
const items = await this.linkedItems(body);
|
||||
const output = [];
|
||||
|
||||
@@ -128,15 +136,15 @@ class Note extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async linkedResourceIds(body) {
|
||||
static async linkedResourceIds(body: string) {
|
||||
return this.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, body);
|
||||
}
|
||||
|
||||
static async linkedNoteIds(body) {
|
||||
static async linkedNoteIds(body: string) {
|
||||
return this.linkedItemIdsByType(BaseModel.TYPE_NOTE, body);
|
||||
}
|
||||
|
||||
static async replaceResourceInternalToExternalLinks(body, options = null) {
|
||||
static async replaceResourceInternalToExternalLinks(body: string, options: any = null) {
|
||||
options = Object.assign({}, {
|
||||
useAbsolutePaths: false,
|
||||
}, options);
|
||||
@@ -166,7 +174,7 @@ class Note extends BaseItem {
|
||||
return body;
|
||||
}
|
||||
|
||||
static async replaceResourceExternalToInternalLinks(body, options = null) {
|
||||
static async replaceResourceExternalToInternalLinks(body: string, options: any = null) {
|
||||
options = Object.assign({}, {
|
||||
useAbsolutePaths: false,
|
||||
}, options);
|
||||
@@ -233,19 +241,19 @@ class Note extends BaseItem {
|
||||
}
|
||||
|
||||
// Note: sort logic must be duplicated in previews();
|
||||
static sortNotes(notes, orders, uncompletedTodosOnTop) {
|
||||
const noteOnTop = note => {
|
||||
static sortNotes(notes: NoteEntity[], orders: any[], uncompletedTodosOnTop: boolean) {
|
||||
const noteOnTop = (note: NoteEntity) => {
|
||||
return uncompletedTodosOnTop && note.is_todo && !note.todo_completed;
|
||||
};
|
||||
|
||||
const noteFieldComp = (f1, f2) => {
|
||||
const noteFieldComp = (f1: any, f2: any) => {
|
||||
if (f1 === f2) return 0;
|
||||
return f1 < f2 ? -1 : +1;
|
||||
};
|
||||
|
||||
// Makes the sort deterministic, so that if, for example, a and b have the
|
||||
// same updated_time, they aren't swapped every time a list is refreshed.
|
||||
const sortIdenticalNotes = (a, b) => {
|
||||
const sortIdenticalNotes = (a: NoteEntity, b: NoteEntity) => {
|
||||
let r = null;
|
||||
r = noteFieldComp(a.user_updated_time, b.user_updated_time);
|
||||
if (r) return r;
|
||||
@@ -262,7 +270,7 @@ class Note extends BaseItem {
|
||||
|
||||
const collator = this.getNaturalSortingCollator();
|
||||
|
||||
return notes.sort((a, b) => {
|
||||
return notes.sort((a: NoteEntity, b: NoteEntity) => {
|
||||
if (noteOnTop(a) && !noteOnTop(b)) return -1;
|
||||
if (!noteOnTop(a) && noteOnTop(b)) return +1;
|
||||
|
||||
@@ -270,8 +278,8 @@ class Note extends BaseItem {
|
||||
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
const order = orders[i];
|
||||
let aProp = a[order.by];
|
||||
let bProp = b[order.by];
|
||||
let aProp = (a as any)[order.by];
|
||||
let bProp = (b as any)[order.by];
|
||||
if (typeof aProp === 'string') aProp = aProp.toLowerCase();
|
||||
if (typeof bProp === 'string') bProp = bProp.toLowerCase();
|
||||
|
||||
@@ -289,11 +297,11 @@ class Note extends BaseItem {
|
||||
});
|
||||
}
|
||||
|
||||
static previewFieldsWithDefaultValues(options = null) {
|
||||
static previewFieldsWithDefaultValues(options: any = null) {
|
||||
return Note.defaultValues(this.previewFields(options));
|
||||
}
|
||||
|
||||
static previewFields(options = null) {
|
||||
static previewFields(options: any = null) {
|
||||
options = Object.assign({
|
||||
includeTimestamps: true,
|
||||
}, options);
|
||||
@@ -309,12 +317,12 @@ class Note extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static previewFieldsSql(fields = null) {
|
||||
static previewFieldsSql(fields: string[] = null) {
|
||||
if (fields === null) fields = this.previewFields();
|
||||
return this.db().escapeFields(fields).join(',');
|
||||
}
|
||||
|
||||
static async loadFolderNoteByField(folderId, field, value) {
|
||||
static async loadFolderNoteByField(folderId: string, field: string, value: any) {
|
||||
if (!folderId) throw new Error('folderId is undefined');
|
||||
|
||||
const options = {
|
||||
@@ -327,7 +335,7 @@ class Note extends BaseItem {
|
||||
return results.length ? results[0] : null;
|
||||
}
|
||||
|
||||
static async previews(parentId, options = null) {
|
||||
static async previews(parentId: string, options: any = null) {
|
||||
// Note: ordering logic must be duplicated in sortNotes(), which is used
|
||||
// to sort already loaded notes.
|
||||
|
||||
@@ -416,12 +424,12 @@ class Note extends BaseItem {
|
||||
return results;
|
||||
}
|
||||
|
||||
static preview(noteId, options = null) {
|
||||
static preview(noteId: string, options: any = null) {
|
||||
if (!options) options = { fields: null };
|
||||
return this.modelSelectOne(`SELECT ${this.previewFieldsSql(options.fields)} FROM notes WHERE is_conflict = 0 AND id = ?`, [noteId]);
|
||||
}
|
||||
|
||||
static async search(options = null) {
|
||||
static async search(options: any = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.conditions) options.conditions = [];
|
||||
if (!options.conditionsParams) options.conditionsParams = [];
|
||||
@@ -448,7 +456,7 @@ class Note extends BaseItem {
|
||||
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
|
||||
}
|
||||
|
||||
static async updateGeolocation(noteId) {
|
||||
static async updateGeolocation(noteId: string) {
|
||||
if (!Setting.value('trackLocation')) return;
|
||||
if (!Note.updateGeolocationEnabled_) return;
|
||||
|
||||
@@ -496,7 +504,7 @@ class Note extends BaseItem {
|
||||
return Note.save(note);
|
||||
}
|
||||
|
||||
static filter(note) {
|
||||
static filter(note: NoteEntity) {
|
||||
if (!note) return note;
|
||||
|
||||
const output = super.filter(note);
|
||||
@@ -506,7 +514,7 @@ class Note extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async copyToFolder(noteId, folderId) {
|
||||
static async copyToFolder(noteId: string, folderId: string) {
|
||||
if (folderId == this.getClass('Folder').conflictFolderId()) throw new Error(_('Cannot copy note to "%s" notebook', this.getClass('Folder').conflictFolderTitle()));
|
||||
|
||||
return Note.duplicate(noteId, {
|
||||
@@ -517,7 +525,7 @@ class Note extends BaseItem {
|
||||
});
|
||||
}
|
||||
|
||||
static async moveToFolder(noteId, folderId) {
|
||||
static async moveToFolder(noteId: string, folderId: string) {
|
||||
if (folderId == this.getClass('Folder').conflictFolderId()) throw new Error(_('Cannot move note to "%s" notebook', this.getClass('Folder').conflictFolderTitle()));
|
||||
|
||||
// When moving a note to a different folder, the user timestamp is not updated.
|
||||
@@ -533,7 +541,7 @@ class Note extends BaseItem {
|
||||
return Note.save(modifiedNote, { autoTimestamp: false });
|
||||
}
|
||||
|
||||
static changeNoteType(note, type) {
|
||||
static changeNoteType(note: NoteEntity, type: string) {
|
||||
if (!('is_todo' in note)) throw new Error('Missing "is_todo" property');
|
||||
|
||||
const newIsTodo = type === 'todo' ? 1 : 0;
|
||||
@@ -548,11 +556,11 @@ class Note extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static toggleIsTodo(note) {
|
||||
static toggleIsTodo(note: NoteEntity) {
|
||||
return this.changeNoteType(note, note.is_todo ? 'note' : 'todo');
|
||||
}
|
||||
|
||||
static toggleTodoCompleted(note) {
|
||||
static toggleTodoCompleted(note: NoteEntity) {
|
||||
if (!('todo_completed' in note)) throw new Error('Missing "todo_completed" property');
|
||||
|
||||
note = Object.assign({}, note);
|
||||
@@ -565,12 +573,12 @@ class Note extends BaseItem {
|
||||
return note;
|
||||
}
|
||||
|
||||
static async duplicateMultipleNotes(noteIds, options = null) {
|
||||
static async duplicateMultipleNotes(noteIds: string[], options: any = null) {
|
||||
// if options.uniqueTitle is true, a unique title for the duplicated file will be assigned.
|
||||
const ensureUniqueTitle = options && options.ensureUniqueTitle;
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
const noteOptions = {};
|
||||
const noteOptions: any = {};
|
||||
|
||||
// If ensureUniqueTitle is truthy, set the original note's name as root for the unique title.
|
||||
if (ensureUniqueTitle) {
|
||||
@@ -582,7 +590,7 @@ class Note extends BaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
static async duplicate(noteId, options = null) {
|
||||
static async duplicate(noteId: string, options: any = null) {
|
||||
const changes = options && options.changes;
|
||||
const uniqueTitle = options && options.uniqueTitle;
|
||||
|
||||
@@ -609,13 +617,13 @@ class Note extends BaseItem {
|
||||
return this.save(newNote);
|
||||
}
|
||||
|
||||
static async noteIsOlderThan(noteId, date) {
|
||||
static async noteIsOlderThan(noteId: string, date: number) {
|
||||
const n = await this.db().selectOne('SELECT updated_time FROM notes WHERE id = ?', [noteId]);
|
||||
if (!n) throw new Error(`No such note: ${noteId}`);
|
||||
return n.updated_time < date;
|
||||
}
|
||||
|
||||
static async save(o, options = null) {
|
||||
static async save(o: NoteEntity, options: any = null) {
|
||||
const isNew = this.isNew(o, options);
|
||||
const isProvisional = options && !!options.provisional;
|
||||
const dispatchUpdateAction = options ? options.dispatchUpdateAction !== false : true;
|
||||
@@ -647,7 +655,7 @@ class Note extends BaseItem {
|
||||
if (oldNote) {
|
||||
for (const field in o) {
|
||||
if (!o.hasOwnProperty(field)) continue;
|
||||
if (o[field] !== oldNote[field]) {
|
||||
if ((o as any)[field] !== oldNote[field]) {
|
||||
changedFields.push(field);
|
||||
}
|
||||
}
|
||||
@@ -656,7 +664,7 @@ class Note extends BaseItem {
|
||||
const note = await super.save(o, options);
|
||||
|
||||
const changeSource = options && options.changeSource ? options.changeSource : null;
|
||||
ItemChange.add(BaseModel.TYPE_NOTE, note.id, isNew ? ItemChange.TYPE_CREATE : ItemChange.TYPE_UPDATE, changeSource, beforeNoteJson);
|
||||
void ItemChange.add(BaseModel.TYPE_NOTE, note.id, isNew ? ItemChange.TYPE_CREATE : ItemChange.TYPE_UPDATE, changeSource, beforeNoteJson);
|
||||
|
||||
if (dispatchUpdateAction) {
|
||||
this.dispatch({
|
||||
@@ -677,14 +685,14 @@ class Note extends BaseItem {
|
||||
return note;
|
||||
}
|
||||
|
||||
static async batchDelete(ids, options = null) {
|
||||
static async batchDelete(ids: string[], options: any = null) {
|
||||
ids = ids.slice();
|
||||
|
||||
while (ids.length) {
|
||||
const processIds = ids.splice(0, 50);
|
||||
|
||||
const notes = await Note.byIds(processIds);
|
||||
const beforeChangeItems = {};
|
||||
const beforeChangeItems: any = {};
|
||||
for (const note of notes) {
|
||||
beforeChangeItems[note.id] = JSON.stringify(note);
|
||||
}
|
||||
@@ -693,7 +701,7 @@ class Note extends BaseItem {
|
||||
const changeSource = options && options.changeSource ? options.changeSource : null;
|
||||
for (let i = 0; i < processIds.length; i++) {
|
||||
const id = processIds[i];
|
||||
ItemChange.add(BaseModel.TYPE_NOTE, id, ItemChange.TYPE_DELETE, changeSource, beforeChangeItems[id]);
|
||||
void ItemChange.add(BaseModel.TYPE_NOTE, id, ItemChange.TYPE_DELETE, changeSource, beforeChangeItems[id]);
|
||||
|
||||
this.dispatch({
|
||||
type: 'NOTE_DELETE',
|
||||
@@ -707,11 +715,11 @@ class Note extends BaseItem {
|
||||
return this.modelSelectAll('SELECT id, title, body, is_todo, todo_due, todo_completed, is_conflict FROM notes WHERE is_conflict = 0 AND is_todo = 1 AND todo_completed = 0 AND todo_due > ?', [time.unixMs()]);
|
||||
}
|
||||
|
||||
static needAlarm(note) {
|
||||
static needAlarm(note: NoteEntity) {
|
||||
return note.is_todo && !note.todo_completed && note.todo_due >= time.unixMs() && !note.is_conflict;
|
||||
}
|
||||
|
||||
static dueDateObject(note) {
|
||||
static dueDateObject(note: NoteEntity) {
|
||||
if (!!note.is_todo && note.todo_due) {
|
||||
if (!this.dueDateObjects_) this.dueDateObjects_ = {};
|
||||
if (this.dueDateObjects_[note.todo_due]) return this.dueDateObjects_[note.todo_due];
|
||||
@@ -723,7 +731,7 @@ class Note extends BaseItem {
|
||||
}
|
||||
|
||||
// Tells whether the conflict between the local and remote note can be ignored.
|
||||
static mustHandleConflict(localNote, remoteNote) {
|
||||
static mustHandleConflict(localNote: NoteEntity, remoteNote: NoteEntity) {
|
||||
// That shouldn't happen so throw an exception
|
||||
if (localNote.id !== remoteNote.id) throw new Error('Cannot handle conflict for two different notes');
|
||||
|
||||
@@ -737,7 +745,7 @@ class Note extends BaseItem {
|
||||
return false;
|
||||
}
|
||||
|
||||
static markupLanguageToLabel(markupLanguageId) {
|
||||
static markupLanguageToLabel(markupLanguageId: number) {
|
||||
if (markupLanguageId === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN) return 'Markdown';
|
||||
if (markupLanguageId === MarkupToHtml.MARKUP_LANGUAGE_HTML) return 'HTML';
|
||||
throw new Error(`Invalid markup language ID: ${markupLanguageId}`);
|
||||
@@ -745,7 +753,7 @@ class Note extends BaseItem {
|
||||
|
||||
// When notes are sorted in "custom order", they are sorted by the "order" field first and,
|
||||
// in those cases, where the order field is the same for some notes, by created time.
|
||||
static customOrderByColumns(type = null) {
|
||||
static customOrderByColumns(type: string = null) {
|
||||
if (!type) type = 'object';
|
||||
if (type === 'object') return [{ by: 'order', dir: 'DESC' }, { by: 'user_created_time', dir: 'DESC' }];
|
||||
if (type === 'string') return 'ORDER BY `order` DESC, user_created_time DESC';
|
||||
@@ -754,7 +762,7 @@ class Note extends BaseItem {
|
||||
|
||||
// Update the note "order" field without changing the user timestamps,
|
||||
// which is generally what we want.
|
||||
static async updateNoteOrder_(note, order) {
|
||||
static async updateNoteOrder_(note: NoteEntity, order: any) {
|
||||
return Note.save(Object.assign({}, note, {
|
||||
order: order,
|
||||
user_updated_time: note.user_updated_time,
|
||||
@@ -765,7 +773,7 @@ class Note extends BaseItem {
|
||||
// of unecessary updates, so it's the caller's responsability to update
|
||||
// the UI once the call is finished. This is done by listening to the
|
||||
// NOTE_IS_INSERTING_NOTES action in the application middleware.
|
||||
static async insertNotesAt(folderId, noteIds, index) {
|
||||
static async insertNotesAt(folderId: string, noteIds: string[], index: number) {
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const defer = () => {
|
||||
@@ -874,7 +882,7 @@ class Note extends BaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
static handleTitleNaturalSorting(items, options) {
|
||||
static handleTitleNaturalSorting(items: NoteEntity[], options: any) {
|
||||
if (options.order.length > 0 && options.order[0].by === 'title') {
|
||||
const collator = this.getNaturalSortingCollator();
|
||||
items.sort((a, b) => ((options.order[0].dir === 'ASC') ? 1 : -1) * collator.compare(a.title, b.title));
|
||||
@@ -886,8 +894,3 @@ class Note extends BaseItem {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Note.updateGeolocationEnabled_ = true;
|
||||
Note.geolocationUpdating_ = false;
|
||||
|
||||
module.exports = Note;
|
||||
@@ -1,7 +1,7 @@
|
||||
const BaseItem = require('./BaseItem.js');
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
import BaseItem from './BaseItem';
|
||||
import BaseModel from '../BaseModel';
|
||||
|
||||
class NoteTag extends BaseItem {
|
||||
export default class NoteTag extends BaseItem {
|
||||
static tableName() {
|
||||
return 'note_tags';
|
||||
}
|
||||
@@ -10,12 +10,12 @@ class NoteTag extends BaseItem {
|
||||
return BaseModel.TYPE_NOTE_TAG;
|
||||
}
|
||||
|
||||
static async byNoteIds(noteIds) {
|
||||
static async byNoteIds(noteIds: string[]) {
|
||||
if (!noteIds.length) return [];
|
||||
return this.modelSelectAll(`SELECT * FROM note_tags WHERE note_id IN ("${noteIds.join('","')}")`);
|
||||
}
|
||||
|
||||
static async tagIdsByNoteId(noteId) {
|
||||
static async tagIdsByNoteId(noteId: string) {
|
||||
const rows = await this.db().selectAll('SELECT tag_id FROM note_tags WHERE note_id = ?', [noteId]);
|
||||
const output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
@@ -24,5 +24,3 @@ class NoteTag extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NoteTag;
|
||||
@@ -1,18 +1,29 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
const BaseItem = require('./BaseItem.js');
|
||||
const ItemChange = require('./ItemChange.js');
|
||||
const NoteResource = require('./NoteResource').default;
|
||||
const ResourceLocalState = require('./ResourceLocalState.js');
|
||||
const Setting = require('./Setting').default;
|
||||
import BaseModel from '../BaseModel';
|
||||
import BaseItem from './BaseItem';
|
||||
import ItemChange from './ItemChange';
|
||||
import NoteResource from './NoteResource';
|
||||
import Setting from './Setting';
|
||||
import markdownUtils from '../markdownUtils';
|
||||
import { _ } from '../locale';
|
||||
import { ResourceEntity, ResourceLocalStateEntity } from '../services/database/types';
|
||||
import ResourceLocalState from './ResourceLocalState';
|
||||
const pathUtils = require('../path-utils');
|
||||
const { mime } = require('../mime-utils.js');
|
||||
const { filename, safeFilename } = require('../path-utils');
|
||||
const { FsDriverDummy } = require('../fs-driver-dummy.js');
|
||||
const markdownUtils = require('../markdownUtils').default;
|
||||
const JoplinError = require('../JoplinError');
|
||||
const { _ } = require('../locale');
|
||||
|
||||
class Resource extends BaseItem {
|
||||
export default class Resource extends BaseItem {
|
||||
|
||||
public static IMAGE_MAX_DIMENSION = 1920;
|
||||
|
||||
public static FETCH_STATUS_IDLE = 0;
|
||||
public static FETCH_STATUS_STARTED = 1;
|
||||
public static FETCH_STATUS_DONE = 2;
|
||||
public static FETCH_STATUS_ERROR = 3;
|
||||
|
||||
public static fsDriver_: any;
|
||||
|
||||
static tableName() {
|
||||
return 'resources';
|
||||
}
|
||||
@@ -26,12 +37,12 @@ class Resource extends BaseItem {
|
||||
return this.encryptionService_;
|
||||
}
|
||||
|
||||
static isSupportedImageMimeType(type) {
|
||||
static isSupportedImageMimeType(type: string) {
|
||||
const imageMimeTypes = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'];
|
||||
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
static fetchStatuses(resourceIds) {
|
||||
static fetchStatuses(resourceIds: string[]) {
|
||||
if (!resourceIds.length) return [];
|
||||
return this.db().selectAll(`SELECT resource_id, fetch_status FROM resource_local_states WHERE resource_id IN ("${resourceIds.join('","')}")`);
|
||||
}
|
||||
@@ -45,7 +56,7 @@ class Resource extends BaseItem {
|
||||
`, [Resource.FETCH_STATUS_ERROR]);
|
||||
}
|
||||
|
||||
static needToBeFetched(resourceDownloadMode = null, limit = null) {
|
||||
static needToBeFetched(resourceDownloadMode: string = null, limit: number = null) {
|
||||
const sql = ['SELECT * FROM resources WHERE encryption_applied = 0 AND id IN (SELECT resource_id FROM resource_local_states WHERE fetch_status = ?)'];
|
||||
if (resourceDownloadMode !== 'always') {
|
||||
sql.push('AND resources.id IN (SELECT resource_id FROM resources_to_download)');
|
||||
@@ -59,7 +70,7 @@ class Resource extends BaseItem {
|
||||
return await this.db().exec('UPDATE resource_local_states SET fetch_status = ? WHERE fetch_status = ?', [Resource.FETCH_STATUS_IDLE, Resource.FETCH_STATUS_STARTED]);
|
||||
}
|
||||
|
||||
static resetErrorStatus(resourceId) {
|
||||
static resetErrorStatus(resourceId: string) {
|
||||
return this.db().exec('UPDATE resource_local_states SET fetch_status = ?, fetch_error = "" WHERE resource_id = ?', [Resource.FETCH_STATUS_IDLE, resourceId]);
|
||||
}
|
||||
|
||||
@@ -69,7 +80,7 @@ class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
// DEPRECATED IN FAVOUR OF friendlySafeFilename()
|
||||
static friendlyFilename(resource) {
|
||||
static friendlyFilename(resource: ResourceEntity) {
|
||||
let output = safeFilename(resource.title); // Make sure not to allow spaces or any special characters as it's not supported in HTTP headers
|
||||
if (!output) output = resource.id;
|
||||
let extension = resource.file_extension;
|
||||
@@ -86,14 +97,14 @@ class Resource extends BaseItem {
|
||||
return Setting.value('resourceDirName');
|
||||
}
|
||||
|
||||
static filename(resource, encryptedBlob = false) {
|
||||
static filename(resource: ResourceEntity, encryptedBlob = false) {
|
||||
let extension = encryptedBlob ? 'crypted' : resource.file_extension;
|
||||
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
|
||||
extension = extension ? `.${extension}` : '';
|
||||
return resource.id + extension;
|
||||
}
|
||||
|
||||
static friendlySafeFilename(resource) {
|
||||
static friendlySafeFilename(resource: ResourceEntity) {
|
||||
let ext = resource.file_extension;
|
||||
if (!ext) ext = resource.mime ? mime.toFileExtension(resource.mime) : '';
|
||||
const safeExt = ext ? pathUtils.safeFileExtension(ext).toLowerCase() : '';
|
||||
@@ -102,20 +113,20 @@ class Resource extends BaseItem {
|
||||
return pathUtils.friendlySafeFilename(title) + (safeExt ? `.${safeExt}` : '');
|
||||
}
|
||||
|
||||
static relativePath(resource, encryptedBlob = false) {
|
||||
static relativePath(resource: ResourceEntity, encryptedBlob = false) {
|
||||
return `${Setting.value('resourceDirName')}/${this.filename(resource, encryptedBlob)}`;
|
||||
}
|
||||
|
||||
static fullPath(resource, encryptedBlob = false) {
|
||||
static fullPath(resource: ResourceEntity, encryptedBlob = false) {
|
||||
return `${Setting.value('resourceDir')}/${this.filename(resource, encryptedBlob)}`;
|
||||
}
|
||||
|
||||
static async isReady(resource) {
|
||||
static async isReady(resource: ResourceEntity) {
|
||||
const r = await this.readyStatus(resource);
|
||||
return r === 'ok';
|
||||
}
|
||||
|
||||
static async readyStatus(resource) {
|
||||
static async readyStatus(resource: ResourceEntity) {
|
||||
const ls = await this.localState(resource);
|
||||
if (!resource) return 'notFound';
|
||||
if (ls.fetch_status !== Resource.FETCH_STATUS_DONE) return 'notDownloaded';
|
||||
@@ -123,13 +134,13 @@ class Resource extends BaseItem {
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
static async requireIsReady(resource) {
|
||||
static async requireIsReady(resource: ResourceEntity) {
|
||||
const readyStatus = await Resource.readyStatus(resource);
|
||||
if (readyStatus !== 'ok') throw new Error(`Resource is not ready. Status: ${readyStatus}`);
|
||||
}
|
||||
|
||||
// For resources, we need to decrypt the item (metadata) and the resource binary blob.
|
||||
static async decrypt(item) {
|
||||
static async decrypt(item: ResourceEntity) {
|
||||
// The item might already be decrypted but not the blob (for instance if it crashes while
|
||||
// decrypting the blob or was otherwise interrupted).
|
||||
const decryptedItem = item.encryption_cipher_text ? await super.decrypt(item) : Object.assign({}, item);
|
||||
@@ -177,7 +188,7 @@ class Resource extends BaseItem {
|
||||
// as it should be uploaded to the sync target. Note that this may be different from what is stored
|
||||
// in the database. In particular, the flag encryption_blob_encrypted might be 1 on the sync target
|
||||
// if the resource is encrypted, but will be 0 locally because the device has the decrypted resource.
|
||||
static async fullPathForSyncUpload(resource) {
|
||||
static async fullPathForSyncUpload(resource: ResourceEntity) {
|
||||
const plainTextPath = this.fullPath(resource);
|
||||
|
||||
if (!Setting.value('encryption.enabled')) {
|
||||
@@ -201,7 +212,7 @@ class Resource extends BaseItem {
|
||||
return { path: encryptedPath, resource: resourceCopy };
|
||||
}
|
||||
|
||||
static markdownTag(resource) {
|
||||
static markdownTag(resource: any) {
|
||||
let tagAlt = resource.alt ? resource.alt : resource.title;
|
||||
if (!tagAlt) tagAlt = '';
|
||||
const lines = [];
|
||||
@@ -217,36 +228,36 @@ class Resource extends BaseItem {
|
||||
return lines.join('');
|
||||
}
|
||||
|
||||
static internalUrl(resource) {
|
||||
static internalUrl(resource: ResourceEntity) {
|
||||
return `:/${resource.id}`;
|
||||
}
|
||||
|
||||
static pathToId(path) {
|
||||
static pathToId(path: string) {
|
||||
return filename(path);
|
||||
}
|
||||
|
||||
static async content(resource) {
|
||||
static async content(resource: ResourceEntity) {
|
||||
return this.fsDriver().readFile(this.fullPath(resource), 'Buffer');
|
||||
}
|
||||
|
||||
static setContent(resource, content) {
|
||||
static setContent(resource: ResourceEntity, content: any) {
|
||||
return this.fsDriver().writeBinaryFile(this.fullPath(resource), content);
|
||||
}
|
||||
|
||||
static isResourceUrl(url) {
|
||||
static isResourceUrl(url: string) {
|
||||
return url && url.length === 34 && url[0] === ':' && url[1] === '/';
|
||||
}
|
||||
|
||||
static urlToId(url) {
|
||||
static urlToId(url: string) {
|
||||
if (!this.isResourceUrl(url)) throw new Error(`Not a valid resource URL: ${url}`);
|
||||
return url.substr(2);
|
||||
}
|
||||
|
||||
static async localState(resourceOrId) {
|
||||
static async localState(resourceOrId: any) {
|
||||
return ResourceLocalState.byResourceId(typeof resourceOrId === 'object' ? resourceOrId.id : resourceOrId);
|
||||
}
|
||||
|
||||
static async setLocalState(resourceOrId, state) {
|
||||
static async setLocalState(resourceOrId: any, state: ResourceLocalStateEntity) {
|
||||
const id = typeof resourceOrId === 'object' ? resourceOrId.id : resourceOrId;
|
||||
await ResourceLocalState.save(Object.assign({}, state, { resource_id: id }));
|
||||
}
|
||||
@@ -258,11 +269,11 @@ class Resource extends BaseItem {
|
||||
// Only set the `size` field and nothing else, not even the update_time
|
||||
// This is because it's only necessary to do it once after migration 20
|
||||
// and each client does it so there's no need to sync the resource.
|
||||
static async setFileSizeOnly(resourceId, fileSize) {
|
||||
static async setFileSizeOnly(resourceId: string, fileSize: number) {
|
||||
return this.db().exec('UPDATE resources set `size` = ? WHERE id = ?', [fileSize, resourceId]);
|
||||
}
|
||||
|
||||
static async batchDelete(ids, options = null) {
|
||||
static async batchDelete(ids: string[], options: any = null) {
|
||||
// For resources, there's not really batch deleting since there's the file data to delete
|
||||
// too, so each is processed one by one with the item being deleted last (since the db
|
||||
// call is the less likely to fail).
|
||||
@@ -280,13 +291,13 @@ class Resource extends BaseItem {
|
||||
await ResourceLocalState.batchDelete(ids);
|
||||
}
|
||||
|
||||
static async markForDownload(resourceId) {
|
||||
static async markForDownload(resourceId: string) {
|
||||
// Insert the row only if it's not already there
|
||||
const t = Date.now();
|
||||
await this.db().exec('INSERT INTO resources_to_download (resource_id, updated_time, created_time) SELECT ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM resources_to_download WHERE resource_id = ?)', [resourceId, t, t, resourceId]);
|
||||
}
|
||||
|
||||
static async downloadedButEncryptedBlobCount(excludedIds = null) {
|
||||
static async downloadedButEncryptedBlobCount(excludedIds: string[] = null) {
|
||||
let excludedSql = '';
|
||||
if (excludedIds && excludedIds.length) {
|
||||
excludedSql = `AND resource_id NOT IN ("${excludedIds.join('","')}")`;
|
||||
@@ -303,7 +314,7 @@ class Resource extends BaseItem {
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static async downloadStatusCounts(status) {
|
||||
static async downloadStatusCounts(status: number) {
|
||||
const r = await this.db().selectOne(`
|
||||
SELECT count(*) as total
|
||||
FROM resource_local_states
|
||||
@@ -324,7 +335,7 @@ class Resource extends BaseItem {
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static fetchStatusToLabel(status) {
|
||||
static fetchStatusToLabel(status: number) {
|
||||
if (status === Resource.FETCH_STATUS_IDLE) return _('Not downloaded');
|
||||
if (status === Resource.FETCH_STATUS_STARTED) return _('Downloading');
|
||||
if (status === Resource.FETCH_STATUS_DONE) return _('Downloaded');
|
||||
@@ -332,7 +343,7 @@ class Resource extends BaseItem {
|
||||
throw new Error(`Invalid status: ${status}`);
|
||||
}
|
||||
|
||||
static async updateResourceBlobContent(resourceId, newBlobFilePath) {
|
||||
static async updateResourceBlobContent(resourceId: string, newBlobFilePath: string) {
|
||||
const resource = await Resource.load(resourceId);
|
||||
await this.requireIsReady(resource);
|
||||
|
||||
@@ -345,13 +356,13 @@ class Resource extends BaseItem {
|
||||
});
|
||||
}
|
||||
|
||||
static async resourceBlobContent(resourceId, encoding = 'Buffer') {
|
||||
static async resourceBlobContent(resourceId: string, encoding = 'Buffer') {
|
||||
const resource = await Resource.load(resourceId);
|
||||
await this.requireIsReady(resource);
|
||||
return await this.fsDriver().readFile(Resource.fullPath(resource), encoding);
|
||||
}
|
||||
|
||||
static async duplicateResource(resourceId) {
|
||||
static async duplicateResource(resourceId: string) {
|
||||
const resource = await Resource.load(resourceId);
|
||||
const localState = await Resource.localState(resource);
|
||||
|
||||
@@ -373,7 +384,7 @@ class Resource extends BaseItem {
|
||||
return newResource;
|
||||
}
|
||||
|
||||
static async createConflictResourceNote(resource) {
|
||||
static async createConflictResourceNote(resource: ResourceEntity) {
|
||||
const Note = this.getClass('Note');
|
||||
|
||||
const conflictResource = await Resource.duplicateResource(resource.id);
|
||||
@@ -386,12 +397,3 @@ class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Resource.IMAGE_MAX_DIMENSION = 1920;
|
||||
|
||||
Resource.FETCH_STATUS_IDLE = 0;
|
||||
Resource.FETCH_STATUS_STARTED = 1;
|
||||
Resource.FETCH_STATUS_DONE = 2;
|
||||
Resource.FETCH_STATUS_ERROR = 3;
|
||||
|
||||
module.exports = Resource;
|
||||
@@ -1,7 +1,8 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
import BaseModel from '../BaseModel';
|
||||
import { ResourceLocalStateEntity } from '../services/database/types';
|
||||
const { Database } = require('../database.js');
|
||||
|
||||
class ResourceLocalState extends BaseModel {
|
||||
export default class ResourceLocalState extends BaseModel {
|
||||
static tableName() {
|
||||
return 'resource_local_states';
|
||||
}
|
||||
@@ -10,7 +11,7 @@ class ResourceLocalState extends BaseModel {
|
||||
return BaseModel.TYPE_RESOURCE_LOCAL_STATE;
|
||||
}
|
||||
|
||||
static async byResourceId(resourceId) {
|
||||
static async byResourceId(resourceId: string) {
|
||||
if (!resourceId) throw new Error('Resource ID not provided'); // Sanity check
|
||||
|
||||
const result = await this.modelSelectOne('SELECT * FROM resource_local_states WHERE resource_id = ?', [resourceId]);
|
||||
@@ -25,17 +26,15 @@ class ResourceLocalState extends BaseModel {
|
||||
return result;
|
||||
}
|
||||
|
||||
static async save(o) {
|
||||
static async save(o: ResourceLocalStateEntity) {
|
||||
const queries = [{ sql: 'DELETE FROM resource_local_states WHERE resource_id = ?', params: [o.resource_id] }, Database.insertQuery(this.tableName(), o)];
|
||||
|
||||
return this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
static batchDelete(ids, options = null) {
|
||||
static batchDelete(ids: string[], options: any = null) {
|
||||
options = options ? Object.assign({}, options) : {};
|
||||
options.idFieldName = 'resource_id';
|
||||
return super.batchDelete(ids, options);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResourceLocalState;
|
||||
@@ -1,5 +1,6 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
const BaseItem = require('./BaseItem.js');
|
||||
import BaseModel, { ModelType } from '../BaseModel';
|
||||
import { RevisionEntity } from '../services/database/types';
|
||||
import BaseItem from './BaseItem';
|
||||
const DiffMatchPatch = require('diff-match-patch');
|
||||
const ArrayUtils = require('../ArrayUtils.js');
|
||||
const JoplinError = require('../JoplinError');
|
||||
@@ -7,7 +8,7 @@ const { sprintf } = require('sprintf-js');
|
||||
|
||||
const dmp = new DiffMatchPatch();
|
||||
|
||||
class Revision extends BaseItem {
|
||||
export default class Revision extends BaseItem {
|
||||
static tableName() {
|
||||
return 'revisions';
|
||||
}
|
||||
@@ -16,21 +17,21 @@ class Revision extends BaseItem {
|
||||
return BaseModel.TYPE_REVISION;
|
||||
}
|
||||
|
||||
static createTextPatch(oldText, newText) {
|
||||
static createTextPatch(oldText: string, newText: string) {
|
||||
return dmp.patch_toText(dmp.patch_make(oldText, newText));
|
||||
}
|
||||
|
||||
static applyTextPatch(text, patch) {
|
||||
static applyTextPatch(text: string, patch: string) {
|
||||
patch = dmp.patch_fromText(patch);
|
||||
const result = dmp.patch_apply(patch, text);
|
||||
if (!result || !result.length) throw new Error('Could not apply patch');
|
||||
return result[0];
|
||||
}
|
||||
|
||||
static createObjectPatch(oldObject, newObject) {
|
||||
static createObjectPatch(oldObject: any, newObject: any) {
|
||||
if (!oldObject) oldObject = {};
|
||||
|
||||
const output = {
|
||||
const output: any = {
|
||||
new: {},
|
||||
deleted: [],
|
||||
};
|
||||
@@ -49,7 +50,7 @@ class Revision extends BaseItem {
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
|
||||
static applyObjectPatch(object, patch) {
|
||||
static applyObjectPatch(object: any, patch: any) {
|
||||
patch = JSON.parse(patch);
|
||||
const output = Object.assign({}, object);
|
||||
|
||||
@@ -64,10 +65,10 @@ class Revision extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static patchStats(patch) {
|
||||
static patchStats(patch: string) {
|
||||
if (typeof patch === 'object') throw new Error('Not implemented');
|
||||
|
||||
const countChars = diffLine => {
|
||||
const countChars = (diffLine: string) => {
|
||||
return unescape(diffLine).length - 1;
|
||||
};
|
||||
|
||||
@@ -93,7 +94,7 @@ class Revision extends BaseItem {
|
||||
};
|
||||
}
|
||||
|
||||
static revisionPatchStatsText(rev) {
|
||||
static revisionPatchStatsText(rev: RevisionEntity) {
|
||||
const titleStats = this.patchStats(rev.title_diff);
|
||||
const bodyStats = this.patchStats(rev.body_diff);
|
||||
const total = {
|
||||
@@ -107,28 +108,28 @@ class Revision extends BaseItem {
|
||||
return output.join(', ');
|
||||
}
|
||||
|
||||
static async countRevisions(itemType, itemId) {
|
||||
static async countRevisions(itemType: ModelType, itemId: string) {
|
||||
const r = await this.db().selectOne('SELECT count(*) as total FROM revisions WHERE item_type = ? AND item_id = ?', [itemType, itemId]);
|
||||
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static latestRevision(itemType, itemId) {
|
||||
static latestRevision(itemType: ModelType, itemId: string) {
|
||||
return this.modelSelectOne('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time DESC LIMIT 1', [itemType, itemId]);
|
||||
}
|
||||
|
||||
static allByType(itemType, itemId) {
|
||||
static allByType(itemType: ModelType, itemId: string) {
|
||||
return this.modelSelectAll('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time ASC', [itemType, itemId]);
|
||||
}
|
||||
|
||||
static async itemsWithRevisions(itemType, itemIds) {
|
||||
static async itemsWithRevisions(itemType: ModelType, itemIds: string[]) {
|
||||
if (!itemIds.length) return [];
|
||||
const rows = await this.db().selectAll(`SELECT distinct item_id FROM revisions WHERE item_type = ? AND item_id IN ("${itemIds.join('","')}")`, [itemType]);
|
||||
|
||||
return rows.map(r => r.item_id);
|
||||
return rows.map((r: RevisionEntity) => r.item_id);
|
||||
}
|
||||
|
||||
static async itemsWithNoRevisions(itemType, itemIds) {
|
||||
static async itemsWithNoRevisions(itemType: ModelType, itemIds: string[]) {
|
||||
const withRevs = await this.itemsWithRevisions(itemType, itemIds);
|
||||
const output = [];
|
||||
for (let i = 0; i < itemIds.length; i++) {
|
||||
@@ -137,7 +138,7 @@ class Revision extends BaseItem {
|
||||
return ArrayUtils.unique(output);
|
||||
}
|
||||
|
||||
static moveRevisionToTop(revision, revs) {
|
||||
static moveRevisionToTop(revision: RevisionEntity, revs: RevisionEntity[]) {
|
||||
let targetIndex = -1;
|
||||
for (let i = revs.length - 1; i >= 0; i--) {
|
||||
const rev = revs[i];
|
||||
@@ -160,7 +161,7 @@ class Revision extends BaseItem {
|
||||
}
|
||||
|
||||
// Note: revs must be sorted by update_time ASC (as returned by allByType)
|
||||
static async mergeDiffs(revision, revs = null) {
|
||||
static async mergeDiffs(revision: RevisionEntity, revs: RevisionEntity[] = null) {
|
||||
if (!('encryption_applied' in revision) || !!revision.encryption_applied) throw new JoplinError('Target revision is encrypted', 'revision_encrypted');
|
||||
|
||||
if (!revs) {
|
||||
@@ -202,7 +203,7 @@ class Revision extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async deleteOldRevisions(ttl) {
|
||||
static async deleteOldRevisions(ttl: number) {
|
||||
// When deleting old revisions, we need to make sure that the oldest surviving revision
|
||||
// is a "merged" one (as opposed to a diff from a now deleted revision). So every time
|
||||
// we deleted a revision, we need to find if there's a corresponding surviving revision
|
||||
@@ -210,7 +211,7 @@ class Revision extends BaseItem {
|
||||
|
||||
const cutOffDate = Date.now() - ttl;
|
||||
const revisions = await this.modelSelectAll('SELECT * FROM revisions WHERE item_updated_time < ? ORDER BY item_updated_time DESC', [cutOffDate]);
|
||||
const doneItems = {};
|
||||
const doneItems: Record<string, boolean> = {};
|
||||
|
||||
for (const rev of revisions) {
|
||||
const doneKey = `${rev.item_type}_${rev.item_id}`;
|
||||
@@ -249,10 +250,8 @@ class Revision extends BaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
static async revisionExists(itemType, itemId, updatedTime) {
|
||||
static async revisionExists(itemType: ModelType, itemId: string, updatedTime: number) {
|
||||
const existingRev = await Revision.latestRevision(itemType, itemId);
|
||||
return existingRev && existingRev.item_updated_time === updatedTime;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Revision;
|
||||
@@ -1,20 +0,0 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
|
||||
class Search extends BaseModel {
|
||||
static tableName() {
|
||||
throw new Error('Not using database');
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_SEARCH;
|
||||
}
|
||||
|
||||
static keywords(query) {
|
||||
let output = query.trim();
|
||||
output = output.split(/[\s\t\n]+/);
|
||||
output = output.filter(o => !!o);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Search;
|
||||
20
packages/lib/models/Search.ts
Normal file
20
packages/lib/models/Search.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// This class doesn't appear to be used at all
|
||||
|
||||
import BaseModel from '../BaseModel';
|
||||
|
||||
export default class Search extends BaseModel {
|
||||
static tableName(): string {
|
||||
throw new Error('Not using database');
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_SEARCH;
|
||||
}
|
||||
|
||||
static keywords(query: string) {
|
||||
let output: any = query.trim();
|
||||
output = output.split(/[\s\t\n]+/);
|
||||
output = output.filter((o: any) => !!o);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ import shim from '../shim';
|
||||
import { _, supportedLocalesToLanguages, defaultLocale } from '../locale';
|
||||
import { ltrimSlashes } from '../path-utils';
|
||||
import eventManager from '../eventManager';
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
import BaseModel from '../BaseModel';
|
||||
const { Database } = require('../database.js');
|
||||
const SyncTargetRegistry = require('../SyncTargetRegistry.js');
|
||||
const time = require('../time').default;
|
||||
import time from '../time';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const ObjectUtils = require('../ObjectUtils');
|
||||
const { toTitleCase } = require('../string-utils.js');
|
||||
@@ -81,6 +81,81 @@ interface SettingSections {
|
||||
|
||||
class Setting extends BaseModel {
|
||||
|
||||
// For backward compatibility
|
||||
public static TYPE_INT = SettingItemType.Int;
|
||||
public static TYPE_STRING = SettingItemType.String;
|
||||
public static TYPE_BOOL = SettingItemType.Bool;
|
||||
public static TYPE_ARRAY = SettingItemType.Array;
|
||||
public static TYPE_OBJECT = SettingItemType.Object;
|
||||
public static TYPE_BUTTON = SettingItemType.Button;
|
||||
|
||||
public static THEME_LIGHT = 1;
|
||||
public static THEME_DARK = 2;
|
||||
public static THEME_OLED_DARK = 22;
|
||||
public static THEME_SOLARIZED_LIGHT = 3;
|
||||
public static THEME_SOLARIZED_DARK = 4;
|
||||
public static THEME_DRACULA = 5;
|
||||
public static THEME_NORD = 6;
|
||||
public static THEME_ARITIM_DARK = 7;
|
||||
|
||||
public static FONT_DEFAULT = 0;
|
||||
public static FONT_MENLO = 1;
|
||||
public static FONT_COURIER_NEW = 2;
|
||||
public static FONT_AVENIR = 3;
|
||||
public static FONT_MONOSPACE = 4;
|
||||
|
||||
public static LAYOUT_ALL = 0;
|
||||
public static LAYOUT_EDITOR_VIEWER = 1;
|
||||
public static LAYOUT_EDITOR_SPLIT = 2;
|
||||
public static LAYOUT_VIEWER_SPLIT = 3;
|
||||
|
||||
public static DATE_FORMAT_1 = 'DD/MM/YYYY';
|
||||
public static DATE_FORMAT_2 = 'DD/MM/YY';
|
||||
public static DATE_FORMAT_3 = 'MM/DD/YYYY';
|
||||
public static DATE_FORMAT_4 = 'MM/DD/YY';
|
||||
public static DATE_FORMAT_5 = 'YYYY-MM-DD';
|
||||
public static DATE_FORMAT_6 = 'DD.MM.YYYY';
|
||||
public static DATE_FORMAT_7 = 'YYYY.MM.DD';
|
||||
|
||||
public static TIME_FORMAT_1 = 'HH:mm';
|
||||
public static TIME_FORMAT_2 = 'h:mm A';
|
||||
|
||||
public static SHOULD_REENCRYPT_NO = 0; // Data doesn't need to be re-encrypted
|
||||
public static SHOULD_REENCRYPT_YES = 1; // Data should be re-encrypted
|
||||
public static SHOULD_REENCRYPT_NOTIFIED = 2; // Data should be re-encrypted, and user has been notified
|
||||
|
||||
public static SYNC_UPGRADE_STATE_IDLE = 0; // Doesn't need to be upgraded
|
||||
public static SYNC_UPGRADE_STATE_SHOULD_DO = 1; // Should be upgraded, but waiting for user to confirm
|
||||
public static SYNC_UPGRADE_STATE_MUST_DO = 2; // Must be upgraded - on next restart, the upgrade will start
|
||||
|
||||
public static custom_css_files = {
|
||||
JOPLIN_APP: 'userchrome.css',
|
||||
RENDERED_MARKDOWN: 'userstyle.css',
|
||||
};
|
||||
|
||||
|
||||
// Contains constants that are set by the application and
|
||||
// cannot be modified by the user:
|
||||
public static constants_: any = {
|
||||
env: 'SET_ME',
|
||||
isDemo: false,
|
||||
appName: 'joplin',
|
||||
appId: 'SET_ME', // Each app should set this identifier
|
||||
appType: 'SET_ME', // 'cli' or 'mobile'
|
||||
resourceDirName: '',
|
||||
resourceDir: '',
|
||||
profileDir: '',
|
||||
templateDir: '',
|
||||
tempDir: '',
|
||||
cacheDir: '',
|
||||
pluginDir: '',
|
||||
flagOpenDevTools: false,
|
||||
syncVersion: 2,
|
||||
startupDevPlugins: [],
|
||||
};
|
||||
|
||||
public static autoSaveEnabled = true;
|
||||
|
||||
private static metadata_: SettingItems = null;
|
||||
private static keychainService_: any = null;
|
||||
private static keys_: string[] = null;
|
||||
@@ -540,7 +615,7 @@ class Setting extends BaseModel {
|
||||
appTypes: ['cli'],
|
||||
label: () => _('Sort notes by'),
|
||||
options: () => {
|
||||
const Note = require('./Note');
|
||||
const Note = require('./Note').default;
|
||||
const noteSortFields = ['user_updated_time', 'user_created_time', 'title', 'order'];
|
||||
const options: any = {};
|
||||
for (let i = 0; i < noteSortFields.length; i++) {
|
||||
@@ -566,7 +641,7 @@ class Setting extends BaseModel {
|
||||
appTypes: ['cli'],
|
||||
label: () => _('Sort notebooks by'),
|
||||
options: () => {
|
||||
const Folder = require('./Folder');
|
||||
const Folder = require('./Folder').default;
|
||||
const folderSortFields = ['title', 'last_note_user_updated_time'];
|
||||
const options: any = {};
|
||||
for (let i = 0; i < folderSortFields.length; i++) {
|
||||
@@ -1609,79 +1684,4 @@ class Setting extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
Setting.TYPE_INT = SettingItemType.Int;
|
||||
Setting.TYPE_STRING = SettingItemType.String;
|
||||
Setting.TYPE_BOOL = SettingItemType.Bool;
|
||||
Setting.TYPE_ARRAY = SettingItemType.Array;
|
||||
Setting.TYPE_OBJECT = SettingItemType.Object;
|
||||
Setting.TYPE_BUTTON = SettingItemType.Button;
|
||||
|
||||
Setting.THEME_LIGHT = 1;
|
||||
Setting.THEME_DARK = 2;
|
||||
Setting.THEME_OLED_DARK = 22;
|
||||
Setting.THEME_SOLARIZED_LIGHT = 3;
|
||||
Setting.THEME_SOLARIZED_DARK = 4;
|
||||
Setting.THEME_DRACULA = 5;
|
||||
Setting.THEME_NORD = 6;
|
||||
Setting.THEME_ARITIM_DARK = 7;
|
||||
|
||||
Setting.FONT_DEFAULT = 0;
|
||||
Setting.FONT_MENLO = 1;
|
||||
Setting.FONT_COURIER_NEW = 2;
|
||||
Setting.FONT_AVENIR = 3;
|
||||
Setting.FONT_MONOSPACE = 4;
|
||||
|
||||
Setting.LAYOUT_ALL = 0;
|
||||
Setting.LAYOUT_EDITOR_VIEWER = 1;
|
||||
Setting.LAYOUT_EDITOR_SPLIT = 2;
|
||||
Setting.LAYOUT_VIEWER_SPLIT = 3;
|
||||
|
||||
Setting.DATE_FORMAT_1 = 'DD/MM/YYYY';
|
||||
Setting.DATE_FORMAT_2 = 'DD/MM/YY';
|
||||
Setting.DATE_FORMAT_3 = 'MM/DD/YYYY';
|
||||
Setting.DATE_FORMAT_4 = 'MM/DD/YY';
|
||||
Setting.DATE_FORMAT_5 = 'YYYY-MM-DD';
|
||||
Setting.DATE_FORMAT_6 = 'DD.MM.YYYY';
|
||||
Setting.DATE_FORMAT_7 = 'YYYY.MM.DD';
|
||||
|
||||
Setting.TIME_FORMAT_1 = 'HH:mm';
|
||||
Setting.TIME_FORMAT_2 = 'h:mm A';
|
||||
|
||||
Setting.SHOULD_REENCRYPT_NO = 0; // Data doesn't need to be re-encrypted
|
||||
Setting.SHOULD_REENCRYPT_YES = 1; // Data should be re-encrypted
|
||||
Setting.SHOULD_REENCRYPT_NOTIFIED = 2; // Data should be re-encrypted, and user has been notified
|
||||
|
||||
Setting.SYNC_UPGRADE_STATE_IDLE = 0; // Doesn't need to be upgraded
|
||||
Setting.SYNC_UPGRADE_STATE_SHOULD_DO = 1; // Should be upgraded, but waiting for user to confirm
|
||||
Setting.SYNC_UPGRADE_STATE_MUST_DO = 2; // Must be upgraded - on next restart, the upgrade will start
|
||||
|
||||
Setting.custom_css_files = {
|
||||
JOPLIN_APP: 'userchrome.css',
|
||||
RENDERED_MARKDOWN: 'userstyle.css',
|
||||
};
|
||||
|
||||
|
||||
// Contains constants that are set by the application and
|
||||
// cannot be modified by the user:
|
||||
Setting.constants_ = {
|
||||
env: 'SET_ME',
|
||||
isDemo: false,
|
||||
appName: 'joplin',
|
||||
appId: 'SET_ME', // Each app should set this identifier
|
||||
appType: 'SET_ME', // 'cli' or 'mobile'
|
||||
resourceDirName: '',
|
||||
resourceDir: '',
|
||||
profileDir: '',
|
||||
templateDir: '',
|
||||
tempDir: '',
|
||||
cacheDir: '',
|
||||
pluginDir: '',
|
||||
flagOpenDevTools: false,
|
||||
syncVersion: 2,
|
||||
startupDevPlugins: [],
|
||||
};
|
||||
|
||||
Setting.autoSaveEnabled = true;
|
||||
|
||||
export default Setting;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
|
||||
class SmartFilter extends BaseModel {
|
||||
static tableName() {
|
||||
throw new Error('Not using database');
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_SMART_FILTER;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SmartFilter;
|
||||
13
packages/lib/models/SmartFilter.ts
Normal file
13
packages/lib/models/SmartFilter.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// Not used??
|
||||
|
||||
import BaseModel from '../BaseModel';
|
||||
|
||||
export default class SmartFilter extends BaseModel {
|
||||
static tableName(): string {
|
||||
throw new Error('Not using database');
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_SMART_FILTER;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
const BaseModel = require('../BaseModel').default;
|
||||
const BaseItem = require('./BaseItem.js');
|
||||
const NoteTag = require('./NoteTag.js');
|
||||
const Note = require('./Note.js');
|
||||
const { _ } = require('../locale');
|
||||
import { TagEntity } from '../services/database/types';
|
||||
|
||||
class Tag extends BaseItem {
|
||||
import BaseModel from '../BaseModel';
|
||||
import BaseItem from './BaseItem';
|
||||
import NoteTag from './NoteTag';
|
||||
import Note from './Note';
|
||||
import { _ } from '../locale';
|
||||
|
||||
export default class Tag extends BaseItem {
|
||||
static tableName() {
|
||||
return 'tags';
|
||||
}
|
||||
@@ -13,7 +15,7 @@ class Tag extends BaseItem {
|
||||
return BaseModel.TYPE_TAG;
|
||||
}
|
||||
|
||||
static async noteIds(tagId) {
|
||||
static async noteIds(tagId: string) {
|
||||
const rows = await this.db().selectAll('SELECT note_id FROM note_tags WHERE tag_id = ?', [tagId]);
|
||||
const output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
@@ -22,7 +24,7 @@ class Tag extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async notes(tagId, options = null) {
|
||||
static async notes(tagId: string, options: any = null) {
|
||||
if (options === null) options = {};
|
||||
|
||||
const noteIds = await this.noteIds(tagId);
|
||||
@@ -37,7 +39,7 @@ class Tag extends BaseItem {
|
||||
}
|
||||
|
||||
// Untag all the notes and delete tag
|
||||
static async untagAll(tagId) {
|
||||
static async untagAll(tagId: string) {
|
||||
const noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ?', [tagId]);
|
||||
for (let i = 0; i < noteTags.length; i++) {
|
||||
await NoteTag.delete(noteTags[i].id);
|
||||
@@ -46,7 +48,7 @@ class Tag extends BaseItem {
|
||||
await Tag.delete(tagId);
|
||||
}
|
||||
|
||||
static async delete(id, options = null) {
|
||||
static async delete(id: string, options: any = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
await super.delete(id, options);
|
||||
@@ -57,7 +59,7 @@ class Tag extends BaseItem {
|
||||
});
|
||||
}
|
||||
|
||||
static async addNote(tagId, noteId) {
|
||||
static async addNote(tagId: string, noteId: string) {
|
||||
const hasIt = await this.hasNote(tagId, noteId);
|
||||
if (hasIt) return;
|
||||
|
||||
@@ -87,7 +89,7 @@ class Tag extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async removeNote(tagId, noteId) {
|
||||
static async removeNote(tagId: string, noteId: string) {
|
||||
const noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ? and note_id = ?', [tagId, noteId]);
|
||||
for (let i = 0; i < noteTags.length; i++) {
|
||||
await NoteTag.delete(noteTags[i].id);
|
||||
@@ -99,12 +101,12 @@ class Tag extends BaseItem {
|
||||
});
|
||||
}
|
||||
|
||||
static loadWithCount(tagId) {
|
||||
static loadWithCount(tagId: string) {
|
||||
const sql = 'SELECT * FROM tags_with_note_count WHERE id = ?';
|
||||
return this.modelSelectOne(sql, [tagId]);
|
||||
}
|
||||
|
||||
static async hasNote(tagId, noteId) {
|
||||
static async hasNote(tagId: string, noteId: string) {
|
||||
const r = await this.db().selectOne('SELECT note_id FROM note_tags WHERE tag_id = ? AND note_id = ? LIMIT 1', [tagId, noteId]);
|
||||
return !!r;
|
||||
}
|
||||
@@ -113,24 +115,24 @@ class Tag extends BaseItem {
|
||||
return await Tag.modelSelectAll('SELECT * FROM tags_with_note_count');
|
||||
}
|
||||
|
||||
static async searchAllWithNotes(options) {
|
||||
static async searchAllWithNotes(options: any) {
|
||||
if (!options) options = {};
|
||||
if (!options.conditions) options.conditions = [];
|
||||
options.conditions.push('id IN (SELECT distinct id FROM tags_with_note_count)');
|
||||
return this.search(options);
|
||||
}
|
||||
|
||||
static async tagsByNoteId(noteId) {
|
||||
static async tagsByNoteId(noteId: string) {
|
||||
const tagIds = await NoteTag.tagIdsByNoteId(noteId);
|
||||
if (!tagIds.length) return [];
|
||||
return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${tagIds.join('","')}")`);
|
||||
}
|
||||
|
||||
static async commonTagsByNoteIds(noteIds) {
|
||||
static async commonTagsByNoteIds(noteIds: string[]) {
|
||||
if (!noteIds || noteIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
let commonTagIds = await NoteTag.tagIdsByNoteId(noteIds[0]);
|
||||
let commonTagIds: string[] = await NoteTag.tagIdsByNoteId(noteIds[0]);
|
||||
for (let i = 1; i < noteIds.length; i++) {
|
||||
const tagIds = await NoteTag.tagIdsByNoteId(noteIds[i]);
|
||||
commonTagIds = commonTagIds.filter(value => tagIds.includes(value));
|
||||
@@ -141,17 +143,17 @@ class Tag extends BaseItem {
|
||||
return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${commonTagIds.join('","')}")`);
|
||||
}
|
||||
|
||||
static async loadByTitle(title) {
|
||||
static async loadByTitle(title: string) {
|
||||
return this.loadByField('title', title, { caseInsensitive: true });
|
||||
}
|
||||
|
||||
static async addNoteTagByTitle(noteId, tagTitle) {
|
||||
static async addNoteTagByTitle(noteId: string, tagTitle: string) {
|
||||
let tag = await this.loadByTitle(tagTitle);
|
||||
if (!tag) tag = await Tag.save({ title: tagTitle }, { userSideValidation: true });
|
||||
return await this.addNote(tag.id, noteId);
|
||||
}
|
||||
|
||||
static async setNoteTagsByTitles(noteId, tagTitles) {
|
||||
static async setNoteTagsByTitles(noteId: string, tagTitles: string[]) {
|
||||
const previousTags = await this.tagsByNoteId(noteId);
|
||||
const addedTitles = [];
|
||||
|
||||
@@ -171,7 +173,7 @@ class Tag extends BaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
static async setNoteTagsByIds(noteId, tagIds) {
|
||||
static async setNoteTagsByIds(noteId: string, tagIds: string[]) {
|
||||
const previousTags = await this.tagsByNoteId(noteId);
|
||||
const addedIds = [];
|
||||
|
||||
@@ -188,7 +190,7 @@ class Tag extends BaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
static async save(o, options = null) {
|
||||
static async save(o: TagEntity, options: any = null) {
|
||||
options = Object.assign({}, {
|
||||
dispatchUpdateAction: true,
|
||||
userSideValidation: false,
|
||||
@@ -203,7 +205,7 @@ class Tag extends BaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
return super.save(o, options).then(tag => {
|
||||
return super.save(o, options).then((tag: TagEntity) => {
|
||||
if (options.dispatchUpdateAction) {
|
||||
this.dispatch({
|
||||
type: 'TAG_UPDATE_ONE',
|
||||
@@ -215,5 +217,3 @@ class Tag extends BaseItem {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tag;
|
||||
Reference in New Issue
Block a user