1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-06-18 23:07:45 +02:00
Files
Assets
CliClient
Clipper
ElectronClient
ReactNativeClient
android
ios
lib
MdToHtml
components
images
migrations
models
Alarm.js
BaseItem.js
Folder.js
ItemChange.js
MasterKey.js
Migration.js
Note.js
NoteResource.js
NoteTag.js
Resource.js
ResourceLocalState.js
Revision.js
Search.js
Setting.js
Tag.js
services
vendor
ArrayUtils.js
BaseApplication.js
BaseModel.js
BaseSyncTarget.js
Cache.js
ClipperServer.js
DropboxApi.js
EventDispatcher.js
HtmlToMd.js
JoplinError.js
MdToHtml.js
ModelCache.js
ObjectUtils.js
SyncTargetDropbox.js
SyncTargetFilesystem.js
SyncTargetMemory.js
SyncTargetNextcloud.js
SyncTargetOneDrive.js
SyncTargetOneDriveDev.js
SyncTargetRegistry.js
SyncTargetWebDAV.js
WebDavApi.js
WelcomeUtils.js
database-driver-node.js
database-driver-react-native.js
database.js
dialogs.js
file-api-driver-dropbox.js
file-api-driver-local.js
file-api-driver-memory.js
file-api-driver-onedrive.js
file-api-driver-webdav.js
file-api.js
folders-screen-utils.js
fs-driver-base.js
fs-driver-dummy.js
fs-driver-node.js
fs-driver-rn.js
geolocation-node.js
geolocation-react.js
import-enex-md-gen.js
import-enex.js
joplin-database.js
layout-utils.js
locale.js
logger.js
markJsUtils.js
markdownUtils.js
mime-utils.js
net-utils.js
onedrive-api.js
package.json
parameters.js
parseUri.js
path-utils.js
poor-man-intervals.js
promise-utils.js
randomClipperPort.js
react-logger.js
reducer.js
registry.js
shim-init-node.js
shim-init-react.js
shim.js
string-utils-common.js
string-utils.js
synchronizer.js
time-utils.js
urlUtils.js
uuid.js
welcomeAssets.js
locales
.babelrc
.buckconfig
.flowconfig
.gitattributes
.gitignore
.watchmanconfig
App.js
app.json
build_android.bat
build_android_prod.bat
clean_build.bat
debug_log.bat
debug_log.sh
index.android.js
index.ios.js
index.js
main.js
package-lock.json
package.json
root.js
start_emulator.bat
start_server.bat
start_server.sh
Tools
docs
readme
.gitignore
.travis.yml
BUILD.md
CONTRIBUTING.md
Joplin_install_and_update.sh
LICENSE
README.md
_config.yml
appveyor.yml
joplin.sublime-project
linkToLocal.sh
joplin/ReactNativeClient/lib/models/Folder.js

378 lines
11 KiB
JavaScript
Raw Normal View History

const BaseModel = require('lib/BaseModel.js');
const { promiseChain } = require('lib/promise-utils.js');
const { time } = require('lib/time-utils.js');
const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const { Database } = require('lib/database.js');
const { _ } = require('lib/locale.js');
const moment = require('moment');
const BaseItem = require('lib/models/BaseItem.js');
const { substrWithEllipsis } = require('lib/string-utils.js');
2017-05-15 19:10:00 +00:00
2017-06-15 19:18:48 +01:00
class Folder extends BaseItem {
2017-05-15 19:10:00 +00:00
static tableName() {
return 'folders';
2017-05-15 19:10:00 +00:00
}
2017-07-03 20:50:45 +01:00
static modelType() {
return BaseModel.TYPE_FOLDER;
2017-05-18 19:58:01 +00:00
}
2017-05-15 19:10:00 +00:00
static newFolder() {
return {
id: null,
title: '',
}
2017-05-15 19:10:00 +00:00
}
static fieldToLabel(field) {
const fieldsToLabels = {
title: _('title'),
last_note_user_updated_time: _('updated date'),
};
return field in fieldsToLabels ? fieldsToLabels[field] : field;
}
2017-06-19 23:18:24 +01:00
static noteIds(parentId) {
return this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]).then((rows) => {
let output = [];
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
output.push(row.id);
}
return output;
});
2017-05-18 22:31:40 +02:00
}
static async subFolderIds(parentId) {
const rows = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [parentId]);
return rows.map(r => r.id);
}
2017-07-12 21:39:47 +01:00
static async noteCount(parentId) {
let r = await this.db().selectOne('SELECT count(*) as total FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]);
2017-07-12 21:39:47 +01:00
return r ? r.total : 0;
}
static markNotesAsConflict(parentId) {
let query = Database.updateQuery('notes', { is_conflict: 1 }, { parent_id: parentId });
return this.db().exec(query);
}
2017-06-25 08:52:25 +01:00
static async delete(folderId, options = null) {
if (!options) options = {};
if (!('deleteChildren' in options)) options.deleteChildren = true;
2017-06-25 08:52:25 +01:00
let folder = await Folder.load(folderId);
if (!folder) return; // noop
if (options.deleteChildren) {
let noteIds = await Folder.noteIds(folderId);
for (let i = 0; i < noteIds.length; i++) {
await Note.delete(noteIds[i]);
}
let subFolderIds = await Folder.subFolderIds(folderId);
for (let i = 0; i < subFolderIds.length; i++) {
await Folder.delete(subFolderIds[i]);
}
2017-06-25 08:52:25 +01:00
}
2017-06-25 16:17:40 +01:00
await super.delete(folderId, options);
2017-06-25 08:52:25 +01:00
this.dispatch({
type: 'FOLDER_DELETE',
2017-11-08 21:39:07 +00:00
id: folderId,
});
}
2017-07-15 16:35:40 +01:00
static conflictFolderTitle() {
return _('Conflicts');
2017-07-15 16:35:40 +01:00
}
2017-06-18 00:49:52 +01:00
2017-07-15 16:35:40 +01:00
static conflictFolderId() {
return 'c04f1c7c04f1c7c04f1c7c04f1c7c04f';
2017-07-15 16:35:40 +01:00
}
static conflictFolder() {
return {
type_: this.TYPE_FOLDER,
id: this.conflictFolderId(),
parent_id: '',
2017-07-15 16:35:40 +01:00
title: this.conflictFolderTitle(),
updated_time: time.unixMs(),
2017-08-20 22:11:32 +02:00
user_updated_time: time.unixMs(),
2017-07-15 16:35:40 +01:00
};
}
2017-06-25 10:00:54 +01:00
// 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') {
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 = {};
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
folderIdToTime[row.parent_id] = row.content_updated_time;
}
const findFolderParent = folderId => {
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;
for (let i = 0; i < folders.length; i++) {
if (folders[i].id === folder.parent_id) return folders[i];
}
throw new Error('Could not find parent');
}
const applyChildTimeToParent = folderId => {
const parent = findFolderParent(folderId);
if (!parent) return;
if (folderIdToTime[parent.id] && folderIdToTime[parent.id] >= folderIdToTime[folderId]) {
// Don't change so that parent has the same time as the last updated child
} else {
folderIdToTime[parent.id] = folderIdToTime[folderId];
}
applyChildTimeToParent(parent.id);
}
for (let folderId in folderIdToTime) {
if (!folderIdToTime.hasOwnProperty(folderId)) continue;
applyChildTimeToParent(folderId);
}
const mod = dir === 'DESC' ? +1 : -1;
const output = folders.slice();
output.sort((a, b) => {
const aTime = folderIdToTime[a.id] ? folderIdToTime[a.id] : a.user_updated_time;
const bTime = folderIdToTime[b.id] ? folderIdToTime[b.id] : b.user_updated_time;
if (aTime < bTime) return +1 * mod;
if (aTime > bTime) return -1 * mod;
return 0;
});
return output;
}
2017-07-15 16:35:40 +01:00
static async all(options = null) {
let output = await super.all(options);
if (options && options.includeConflictFolder) {
let conflictCount = await Note.conflictedCount();
if (conflictCount) output.push(this.conflictFolder());
}
return output;
}
2017-06-25 10:00:54 +01:00
static async childrenIds(folderId, recursive) {
if (recursive === false) throw new Error('Not implemented');
const folders = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [folderId]);
let output = [];
for (let i = 0; i < folders.length; i++) {
const f = folders[i];
output.push(f.id);
const subChildrenIds = await this.childrenIds(f.id, true);
output = output.concat(subChildrenIds);
}
return output;
}
static async allAsTree(folders = null, options = null) {
const all = folders ? folders : await this.all(options);
// https://stackoverflow.com/a/49387427/561309
function getNestedChildren(models, parentId) {
const nestedTreeStructure = [];
const length = models.length;
for (let i = 0; i < length; i++) {
const model = models[i];
if (model.parent_id == parentId) {
const children = getNestedChildren(models, model.id);
if (children.length > 0) {
model.children = children;
}
nestedTreeStructure.push(model);
}
}
return nestedTreeStructure;
}
return getNestedChildren(all, '');
}
static folderPath(folders, folderId) {
const idToFolders = {};
for (let i = 0; i < folders.length; i++) {
idToFolders[folders[i].id] = folders[i];
}
const path = [];
while (folderId) {
const folder = idToFolders[folderId];
if (!folder) break; // Shouldn't happen
path.push(folder);
folderId = folder.parent_id;
}
path.reverse();
return path;
}
2019-04-04 08:01:16 +01:00
static folderPathString(folders, folderId, maxTotalLength = 80) {
const path = this.folderPath(folders, folderId);
2019-04-04 08:01:16 +01:00
let currentTotalLength = 0;
for (let i = 0; i < path.length; i++) {
currentTotalLength += path[i].title.length;
}
let pieceLength = maxTotalLength;
if (currentTotalLength > maxTotalLength) {
pieceLength = maxTotalLength / path.length;
}
const output = [];
for (let i = 0; i < path.length; i++) {
2019-04-04 08:01:16 +01:00
output.push(substrWithEllipsis(path[i].title, 0, pieceLength));
}
2019-04-04 08:01:16 +01:00
return output.join(' / ');
}
static buildTree(folders) {
const idToFolders = {};
for (let i = 0; i < folders.length; i++) {
idToFolders[folders[i].id] = folders[i];
idToFolders[folders[i].id].children = [];
}
const rootFolders = [];
for (let folderId in idToFolders) {
if (!idToFolders.hasOwnProperty(folderId)) continue;
const folder = idToFolders[folderId];
if (!folder.parent_id) {
rootFolders.push(folder);
} else {
if (!idToFolders[folder.parent_id]) {
// It means the notebook is refering a folder that doesn't exist. In theory it shouldn't happen
// but sometimes does - https://github.com/laurent22/joplin/issues/1068#issuecomment-450594708
rootFolders.push(folder);
} else {
idToFolders[folder.parent_id].children.push(folder);
}
}
}
return rootFolders;
}
2017-07-15 16:35:40 +01:00
static load(id) {
if (id == this.conflictFolderId()) return this.conflictFolder();
return super.load(id);
2017-06-19 18:58:49 +00:00
}
2017-06-27 20:16:03 +00:00
static defaultFolder() {
return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1');
2017-06-25 00:19:11 +01:00
}
static async canNestUnder(folderId, targetFolderId) {
if (folderId === targetFolderId) return false;
const conflictFolderId = Folder.conflictFolderId();
if (folderId == conflictFolderId || targetFolderId == conflictFolderId) return false;
if (!targetFolderId) return true;
while (true) {
let folder = await Folder.load(targetFolderId);
if (!folder.parent_id) break;
if (folder.parent_id === folderId) return false;
targetFolderId = folder.parent_id;
}
return true;
}
static async moveToFolder(folderId, targetFolderId) {
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.
// However updated_time is updated so that the note can be synced later on.
const modifiedFolder = {
id: folderId,
parent_id: targetFolderId,
updated_time: time.unixMs(),
};
return Folder.save(modifiedFolder, { autoTimestamp: false });
}
2017-07-15 16:35:40 +01:00
// These "duplicateCheck" and "reservedTitleCheck" should only be done when a user is
// manually creating a folder. They shouldn't be done for example when the folders
// are being synced to avoid any strange side-effects. Technically it's possible to
2017-07-17 19:19:01 +00:00
// have folders and notes with duplicate titles (or no title), or with reserved words.
2017-07-03 18:58:01 +00:00
static async save(o, options = null) {
2017-07-17 19:19:01 +00:00
if (!options) options = {};
if (options.userSideValidation === true) {
if (!('duplicateCheck' in options)) options.duplicateCheck = true;
if (!('reservedTitleCheck' in options)) options.reservedTitleCheck = true;
if (!('stripLeftSlashes' in options)) options.stripLeftSlashes = true;
2017-07-17 19:19:01 +00:00
}
if (options.stripLeftSlashes === true && o.title) {
while (o.title.length && (o.title[0] == '/' || o.title[0] == "\\")) {
2017-07-17 19:19:01 +00:00
o.title = o.title.substr(1);
}
}
// We allow folders with duplicate titles so that folders with the same title can exist under different parent folder. For example:
//
// PHP
// Code samples
// Doc
// Java
// My project
// Doc
// if (options.duplicateCheck === true && o.title) {
// let existingFolder = await Folder.loadByTitle(o.title);
// if (existingFolder && existingFolder.id != o.id) throw new Error(_('A notebook with this title already exists: "%s"', o.title));
// }
2017-07-03 18:58:01 +00:00
2017-07-17 19:19:01 +00:00
if (options.reservedTitleCheck === true && o.title) {
2017-07-15 16:35:40 +01:00
if (o.title == Folder.conflictFolderTitle()) throw new Error(_('Notebooks cannot be named "%s", which is a reserved title.', o.title));
}
return super.save(o, options).then((folder) => {
this.dispatch({
type: 'FOLDER_UPDATE_ONE',
item: folder,
2017-05-18 22:31:40 +02:00
});
return folder;
2017-05-18 22:31:40 +02:00
});
}
2017-05-15 19:10:00 +00:00
}
module.exports = Folder;