mirror of
https://github.com/zadam/trilium.git
synced 2025-11-10 23:35:50 +01:00
Merge branch 'master' into trilium-docs-noformat
This commit is contained in:
@@ -5,6 +5,7 @@ const favicon = require('serve-favicon');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const helmet = require('helmet');
|
||||
const session = require('express-session');
|
||||
const compression = require('compression')
|
||||
const FileStore = require('session-file-store')(session);
|
||||
const sessionSecret = require('./services/session_secret');
|
||||
const dataDir = require('./services/data_dir');
|
||||
@@ -18,9 +19,14 @@ const app = express();
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
if (!utils.isElectron()) {
|
||||
app.use(compression()); // HTTP compression
|
||||
}
|
||||
|
||||
app.use(helmet({
|
||||
hidePoweredBy: false, // errors out in electron
|
||||
contentSecurityPolicy: false
|
||||
contentSecurityPolicy: false,
|
||||
crossOriginEmbedderPolicy: false
|
||||
}));
|
||||
|
||||
app.use(express.text({limit: '500mb'}));
|
||||
|
||||
@@ -14,16 +14,19 @@ let becca = null;
|
||||
* Base class for all backend entities.
|
||||
*/
|
||||
class AbstractEntity {
|
||||
/** @protected */
|
||||
beforeSaving() {
|
||||
this.generateIdIfNecessary();
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
generateIdIfNecessary() {
|
||||
if (!this[this.constructor.primaryKeyName]) {
|
||||
this[this.constructor.primaryKeyName] = utils.newEntityId();
|
||||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
generateHash(isDeleted = false) {
|
||||
let contentToHash = "";
|
||||
|
||||
@@ -38,10 +41,12 @@ class AbstractEntity {
|
||||
return utils.hash(contentToHash).substr(0, 10);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
getUtcDateChanged() {
|
||||
return this.utcDateModified || this.utcDateCreated;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
get becca() {
|
||||
if (!becca) {
|
||||
becca = require('../becca');
|
||||
@@ -50,6 +55,7 @@ class AbstractEntity {
|
||||
return becca;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
addEntityChange(isDeleted = false) {
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: this.constructor.entityName,
|
||||
@@ -61,6 +67,7 @@ class AbstractEntity {
|
||||
});
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
getPojoToSave() {
|
||||
return this.getPojo();
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class Attribute extends AbstractEntity {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
init() {
|
||||
if (this.attributeId) {
|
||||
this.becca.attributes[this.attributeId] = this;
|
||||
|
||||
@@ -5,9 +5,9 @@ const AbstractEntity = require("./abstract_entity");
|
||||
const sql = require("../../services/sql");
|
||||
const dateUtils = require("../../services/date_utils");
|
||||
const utils = require("../../services/utils.js");
|
||||
const TaskContext = require("../../services/task_context.js");
|
||||
const cls = require("../../services/cls.js");
|
||||
const log = require("../../services/log.js");
|
||||
const TaskContext = require("../../services/task_context");
|
||||
const cls = require("../../services/cls");
|
||||
const log = require("../../services/log");
|
||||
|
||||
/**
|
||||
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
|
||||
@@ -70,21 +70,22 @@ class Branch extends AbstractEntity {
|
||||
|
||||
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||
|
||||
const childNote = this.childNote;
|
||||
|
||||
if (!childNote.parentBranches.includes(this)) {
|
||||
childNote.parentBranches.push(this);
|
||||
}
|
||||
|
||||
if (this.branchId === 'root') {
|
||||
return;
|
||||
}
|
||||
|
||||
const childNote = this.childNote;
|
||||
const parentNote = this.parentNote;
|
||||
|
||||
if (!childNote.parents.includes(parentNote)) {
|
||||
childNote.parents.push(parentNote);
|
||||
}
|
||||
|
||||
if (!childNote.parentBranches.includes(this)) {
|
||||
childNote.parentBranches.push(this);
|
||||
}
|
||||
|
||||
if (!parentNote.children.includes(childNote)) {
|
||||
parentNote.children.push(childNote);
|
||||
}
|
||||
@@ -104,9 +105,9 @@ class Branch extends AbstractEntity {
|
||||
return this.childNote;
|
||||
}
|
||||
|
||||
/** @returns {Note} */
|
||||
/** @returns {Note|undefined} - root branch will have undefined parent, all other branches have to have a parent note */
|
||||
get parentNote() {
|
||||
if (!(this.parentNoteId in this.becca.notes)) {
|
||||
if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== 'none') {
|
||||
// entities can come out of order in sync/import, create skeleton which will be filled later
|
||||
this.becca.addNote(this.parentNoteId, new Note({noteId: this.parentNoteId}));
|
||||
}
|
||||
@@ -137,6 +138,18 @@ class Branch extends AbstractEntity {
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
const note = this.getNote();
|
||||
|
||||
if (!taskContext.noteDeletionHandlerTriggered) {
|
||||
const parentBranches = note.getParentBranches();
|
||||
|
||||
if (parentBranches.length === 1 && parentBranches[0] === this) {
|
||||
// needs to be run before branches and attributes are deleted and thus attached relations disappear
|
||||
const handlers = require("../../services/handlers");
|
||||
handlers.runAttachedRelations(note, 'runOnNoteDeletion', note);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.branchId === 'root'
|
||||
|| this.noteId === 'root'
|
||||
|| this.noteId === cls.getHoistedNoteId()) {
|
||||
@@ -146,7 +159,6 @@ class Branch extends AbstractEntity {
|
||||
|
||||
this.markAsDeleted(deleteId);
|
||||
|
||||
const note = this.getNote();
|
||||
const notDeletedBranches = note.getParentBranches();
|
||||
|
||||
if (notDeletedBranches.length === 0) {
|
||||
|
||||
@@ -8,7 +8,10 @@ const dateUtils = require('../../services/date_utils');
|
||||
const entityChangesService = require('../../services/entity_changes');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
const NoteRevision = require("./note_revision");
|
||||
const TaskContext = require("../../services/task_context.js");
|
||||
const TaskContext = require("../../services/task_context");
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
@@ -84,13 +87,17 @@ class Note extends AbstractEntity {
|
||||
}
|
||||
|
||||
init() {
|
||||
/** @type {Branch[]} */
|
||||
/** @type {Branch[]}
|
||||
* @private */
|
||||
this.parentBranches = [];
|
||||
/** @type {Note[]} */
|
||||
/** @type {Note[]}
|
||||
* @private */
|
||||
this.parents = [];
|
||||
/** @type {Note[]} */
|
||||
/** @type {Note[]}
|
||||
* @private*/
|
||||
this.children = [];
|
||||
/** @type {Attribute[]} */
|
||||
/** @type {Attribute[]}
|
||||
* @private */
|
||||
this.ownedAttributes = [];
|
||||
|
||||
/** @type {Attribute[]|null}
|
||||
@@ -100,7 +107,8 @@ class Note extends AbstractEntity {
|
||||
* @private*/
|
||||
this.inheritableAttributeCache = null;
|
||||
|
||||
/** @type {Attribute[]} */
|
||||
/** @type {Attribute[]}
|
||||
* @private*/
|
||||
this.targetRelations = [];
|
||||
|
||||
this.becca.addNote(this.noteId, this);
|
||||
@@ -114,16 +122,19 @@ class Note extends AbstractEntity {
|
||||
/**
|
||||
* size of the content in bytes
|
||||
* @type {int|null}
|
||||
* @private
|
||||
*/
|
||||
this.contentSize = null;
|
||||
/**
|
||||
* size of the content and note revision contents in bytes
|
||||
* @type {int|null}
|
||||
* @private
|
||||
*/
|
||||
this.noteSize = null;
|
||||
/**
|
||||
* number of note revisions for this note
|
||||
* @type {int|null}
|
||||
* @private
|
||||
*/
|
||||
this.revisionCount = null;
|
||||
}
|
||||
@@ -225,6 +236,22 @@ class Note extends AbstractEntity {
|
||||
WHERE noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
get dateCreatedObj() {
|
||||
return this.dateCreated === null ? null : dayjs(this.dateCreated);
|
||||
}
|
||||
|
||||
get utcDateCreatedObj() {
|
||||
return this.utcDateCreated === null ? null : dayjs.utc(this.utcDateCreated);
|
||||
}
|
||||
|
||||
get dateModifiedObj() {
|
||||
return this.dateModified === null ? null : dayjs(this.dateModified);
|
||||
}
|
||||
|
||||
get utcDateModifiedObj() {
|
||||
return this.utcDateModified === null ? null : dayjs.utc(this.utcDateModified);
|
||||
}
|
||||
|
||||
/** @returns {*} */
|
||||
getJsonContent() {
|
||||
const content = this.getContent();
|
||||
@@ -350,6 +377,7 @@ class Note extends AbstractEntity {
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__getAttributes(path) {
|
||||
if (path.includes(this.noteId)) {
|
||||
return [];
|
||||
@@ -372,7 +400,11 @@ class Note extends AbstractEntity {
|
||||
const templateNote = this.becca.notes[ownedAttr.value];
|
||||
|
||||
if (templateNote) {
|
||||
templateAttributes.push(...templateNote.__getAttributes(newPath));
|
||||
templateAttributes.push(
|
||||
...templateNote.__getAttributes(newPath)
|
||||
// template attr is used as a marker for templates, but it's not meant to be inherited
|
||||
.filter(attr => !(attr.type === 'label' && attr.name === 'template'))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,7 +433,10 @@ class Note extends AbstractEntity {
|
||||
return this.__attributeCache;
|
||||
}
|
||||
|
||||
/** @returns {Attribute[]} */
|
||||
/**
|
||||
* @private
|
||||
* @returns {Attribute[]}
|
||||
*/
|
||||
__getInheritableAttributes(path) {
|
||||
if (path.includes(this.noteId)) {
|
||||
return [];
|
||||
@@ -414,8 +449,18 @@ class Note extends AbstractEntity {
|
||||
return this.inheritableAttributeCache;
|
||||
}
|
||||
|
||||
hasAttribute(type, name) {
|
||||
return !!this.getAttributes().find(attr => attr.type === type && attr.name === name);
|
||||
/**
|
||||
* @param type
|
||||
* @param name
|
||||
* @param [value]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasAttribute(type, name, value) {
|
||||
return !!this.getAttributes().find(attr =>
|
||||
attr.type === type
|
||||
&& attr.name === name
|
||||
&& (value === undefined || value === null || attr.value === value)
|
||||
);
|
||||
}
|
||||
|
||||
getAttributeCaseInsensitive(type, name, value) {
|
||||
@@ -436,27 +481,31 @@ class Note extends AbstractEntity {
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @param {string} [value] - label value
|
||||
* @returns {boolean} true if label exists (including inherited)
|
||||
*/
|
||||
hasLabel(name) { return this.hasAttribute(LABEL, name); }
|
||||
hasLabel(name, value) { return this.hasAttribute(LABEL, name, value); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @param {string} [value] - label value
|
||||
* @returns {boolean} true if label exists (excluding inherited)
|
||||
*/
|
||||
hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
|
||||
hasOwnedLabel(name, value) { return this.hasOwnedAttribute(LABEL, name, value); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @param {string} [value] - relation value
|
||||
* @returns {boolean} true if relation exists (including inherited)
|
||||
*/
|
||||
hasRelation(name) { return this.hasAttribute(RELATION, name); }
|
||||
hasRelation(name, value) { return this.hasAttribute(RELATION, name, value); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @param {string} [value] - relation value
|
||||
* @returns {boolean} true if relation exists (excluding inherited)
|
||||
*/
|
||||
hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
|
||||
hasOwnedRelation(name, value) { return this.hasOwnedAttribute(RELATION, name, value); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
@@ -509,10 +558,11 @@ class Note extends AbstractEntity {
|
||||
/**
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @param {string} [value] - attribute value
|
||||
* @returns {boolean} true if note has an attribute with given type and name (excluding inherited)
|
||||
*/
|
||||
hasOwnedAttribute(type, name) {
|
||||
return !!this.getOwnedAttribute(type, name);
|
||||
hasOwnedAttribute(type, name, value) {
|
||||
return !!this.getOwnedAttribute(type, name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -599,15 +649,19 @@ class Note extends AbstractEntity {
|
||||
/**
|
||||
* @param {string} [type] - (optional) attribute type to filter
|
||||
* @param {string} [name] - (optional) attribute name to filter
|
||||
* @param {string} [value] - (optional) attribute value to filter
|
||||
* @returns {Attribute[]} note's "owned" attributes - excluding inherited ones
|
||||
*/
|
||||
getOwnedAttributes(type, name) {
|
||||
getOwnedAttributes(type, name, value) {
|
||||
// it's a common mistake to include # or ~ into attribute name
|
||||
if (name && ["#", "~"].includes(name[0])) {
|
||||
name = name.substr(1);
|
||||
}
|
||||
|
||||
if (type && name) {
|
||||
if (type && name && value !== undefined && value !== null) {
|
||||
return this.ownedAttributes.filter(attr => attr.type === type && attr.name === name && attr.value === value);
|
||||
}
|
||||
else if (type && name) {
|
||||
return this.ownedAttributes.filter(attr => attr.type === type && attr.name === name);
|
||||
}
|
||||
else if (type) {
|
||||
@@ -626,8 +680,8 @@ class Note extends AbstractEntity {
|
||||
*
|
||||
* This method can be significantly faster than the getAttribute()
|
||||
*/
|
||||
getOwnedAttribute(type, name) {
|
||||
const attrs = this.getOwnedAttributes(type, name);
|
||||
getOwnedAttribute(type, name, value) {
|
||||
const attrs = this.getOwnedAttributes(type, name, value);
|
||||
|
||||
return attrs.length > 0 ? attrs[0] : null;
|
||||
}
|
||||
@@ -645,9 +699,11 @@ class Note extends AbstractEntity {
|
||||
sortParents() {
|
||||
this.parentBranches.sort((a, b) =>
|
||||
a.branchId.startsWith('virt-')
|
||||
|| a.parentNote.hasInheritableOwnedArchivedLabel() ? 1 : -1);
|
||||
|| a.parentNote?.hasInheritableOwnedArchivedLabel() ? 1 : -1);
|
||||
|
||||
this.parents = this.parentBranches.map(branch => branch.parentNote);
|
||||
this.parents = this.parentBranches
|
||||
.map(branch => branch.parentNote)
|
||||
.filter(note => !!note);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1142,6 +1198,10 @@ class Note extends AbstractEntity {
|
||||
return this.searchNotesInSubtree(searchString)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parentNoteId
|
||||
* @returns {{success: boolean, message: string}}
|
||||
*/
|
||||
cloneTo(parentNoteId) {
|
||||
const cloningService = require("../../services/cloning");
|
||||
|
||||
@@ -1157,6 +1217,10 @@ class Note extends AbstractEntity {
|
||||
* @param {TaskContext} [taskContext]
|
||||
*/
|
||||
deleteNote(deleteId, taskContext) {
|
||||
if (this.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deleteId) {
|
||||
deleteId = utils.randomString(10);
|
||||
}
|
||||
@@ -1165,6 +1229,11 @@ class Note extends AbstractEntity {
|
||||
taskContext = new TaskContext('no-progress-reporting');
|
||||
}
|
||||
|
||||
// needs to be run before branches and attributes are deleted and thus attached relations disappear
|
||||
const handlers = require("../../services/handlers");
|
||||
handlers.runAttachedRelations(this, 'runOnNoteDeletion', this);
|
||||
taskContext.noteDeletionHandlerTriggered = true;
|
||||
|
||||
for (const branch of this.getParentBranches()) {
|
||||
branch.deleteBranch(deleteId, taskContext);
|
||||
}
|
||||
@@ -1188,6 +1257,41 @@ class Note extends AbstractEntity {
|
||||
return !(this.noteId in this.becca.notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {NoteRevision|null}
|
||||
*/
|
||||
saveNoteRevision() {
|
||||
const content = this.getContent();
|
||||
|
||||
if (!content || (Buffer.isBuffer(content) && content.byteLength === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contentMetadata = this.getContentMetadata();
|
||||
|
||||
const noteRevision = new NoteRevision({
|
||||
noteId: this.noteId,
|
||||
// title and text should be decrypted now
|
||||
title: this.title,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
isProtected: this.isProtected,
|
||||
utcDateLastEdited: this.utcDateModified > contentMetadata.utcDateModified
|
||||
? this.utcDateModified
|
||||
: contentMetadata.utcDateModified,
|
||||
utcDateCreated: dateUtils.utcNowDateTime(),
|
||||
utcDateModified: dateUtils.utcNowDateTime(),
|
||||
dateLastEdited: this.dateModified > contentMetadata.dateModified
|
||||
? this.dateModified
|
||||
: contentMetadata.dateModified,
|
||||
dateCreated: dateUtils.localNowDateTime()
|
||||
}, true).save();
|
||||
|
||||
noteRevision.setContent(content);
|
||||
|
||||
return noteRevision;
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class NoteRevision extends AbstractEntity {
|
||||
static get primaryKeyName() { return "noteRevisionId"; }
|
||||
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
|
||||
|
||||
constructor(row) {
|
||||
constructor(row, titleDecrypted = false) {
|
||||
super();
|
||||
|
||||
/** @type {string} */
|
||||
@@ -47,13 +47,10 @@ class NoteRevision extends AbstractEntity {
|
||||
/** @type {number} */
|
||||
this.contentLength = row.contentLength;
|
||||
|
||||
if (this.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
this.title = protectedSessionService.decryptString(this.title);
|
||||
}
|
||||
else {
|
||||
this.title = "[protected]";
|
||||
}
|
||||
if (this.isProtected && !titleDecrypted) {
|
||||
this.title = protectedSessionService.isProtectedSessionAvailable()
|
||||
? protectedSessionService.decryptString(this.title)
|
||||
: "[protected]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +65,8 @@ class NoteRevision extends AbstractEntity {
|
||||
|
||||
/*
|
||||
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
|
||||
* part of NoteRevision entity with it's own sync. Reason behind this hybrid design is that
|
||||
* content can be quite large and it's not necessary to load it / fill memory for any note access even
|
||||
* part of NoteRevision entity with its own sync. Reason behind this hybrid design is that
|
||||
* content can be quite large, and it's not necessary to load it / fill memory for any note access even
|
||||
* if we don't need a content, especially for bulk operations like search.
|
||||
*
|
||||
* This is the same approach as is used for Note's content.
|
||||
|
||||
@@ -281,7 +281,7 @@ async function findSimilarNotes(noteId) {
|
||||
}
|
||||
|
||||
function gatherAncestorRewards(note) {
|
||||
if (ancestorNoteIds.has(note.noteId)) {
|
||||
if (!note || ancestorNoteIds.has(note.noteId)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ const eu = require("./etapi_utils");
|
||||
const passwordEncryptionService = require("../services/password_encryption");
|
||||
const etapiTokenService = require("../services/etapi_tokens");
|
||||
|
||||
function register(router) {
|
||||
eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', (req, res, next) => {
|
||||
function register(router, loginMiddleware) {
|
||||
eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', loginMiddleware, (req, res, next) => {
|
||||
const {password, tokenName} = req.body;
|
||||
|
||||
if (!passwordEncryptionService.verifyPassword(password)) {
|
||||
|
||||
@@ -2,8 +2,6 @@ const becca = require("../becca/becca");
|
||||
const eu = require("./etapi_utils");
|
||||
const mappers = require("./mappers");
|
||||
const Branch = require("../becca/entities/branch");
|
||||
const noteService = require("../services/notes");
|
||||
const TaskContext = require("../services/task_context");
|
||||
const entityChangesService = require("../services/entity_changes");
|
||||
const v = require("./validators");
|
||||
|
||||
|
||||
@@ -228,6 +228,38 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/notes/{noteId}/export:
|
||||
parameters:
|
||||
- name: noteId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityId'
|
||||
- name: format
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
enum:
|
||||
- html
|
||||
- markdown
|
||||
default: html
|
||||
get:
|
||||
description: Exports ZIP file export of a given note subtree. To export whole document, use "root" for noteId
|
||||
operationId: exportNoteSubtree
|
||||
responses:
|
||||
'200':
|
||||
description: export ZIP file
|
||||
content:
|
||||
application/zip:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/branches/{branchId}:
|
||||
parameters:
|
||||
- name: branchId
|
||||
@@ -570,6 +602,8 @@ paths:
|
||||
authToken:
|
||||
type: string
|
||||
example: Bc4bFn0Ffiok_4NpbVCDnFz7B2WU+pdhW8B5Ne3DiR5wXrEyqdjgRIsk=
|
||||
'429':
|
||||
description: Client IP has been blacklisted because too many requests (possibly failed authentications) were made within a short time frame, try again later
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
|
||||
@@ -66,8 +66,8 @@ function route(router, method, path, routeHandler) {
|
||||
router[method](path, checkEtapiAuth, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
|
||||
}
|
||||
|
||||
function NOT_AUTHENTICATED_ROUTE(router, method, path, routeHandler) {
|
||||
router[method](path, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
|
||||
function NOT_AUTHENTICATED_ROUTE(router, method, path, middleware, routeHandler) {
|
||||
router[method](path, ...middleware, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
|
||||
}
|
||||
|
||||
function getAndCheckNote(noteId) {
|
||||
@@ -106,7 +106,7 @@ function getAndCheckAttribute(attributeId) {
|
||||
function validateAndPatch(target, source, allowedProperties) {
|
||||
for (const key of Object.keys(source)) {
|
||||
if (!(key in allowedProperties)) {
|
||||
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for PATCH.`);
|
||||
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`);
|
||||
}
|
||||
else {
|
||||
for (const validator of allowedProperties[key]) {
|
||||
|
||||
@@ -7,6 +7,7 @@ const TaskContext = require("../services/task_context");
|
||||
const v = require("./validators");
|
||||
const searchService = require("../services/search/services/search");
|
||||
const SearchContext = require("../services/search/search_context");
|
||||
const zipExportService = require("../services/export/zip");
|
||||
|
||||
function register(router) {
|
||||
eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
|
||||
@@ -123,6 +124,25 @@ function register(router) {
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
eu.route(router, 'get' ,'/etapi/notes/:noteId/export', (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
const format = req.query.format || "html";
|
||||
|
||||
if (!["html", "markdown"].includes(format)) {
|
||||
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'`);
|
||||
}
|
||||
|
||||
const taskContext = new TaskContext('no-progress-reporting');
|
||||
|
||||
// technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain
|
||||
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
|
||||
const branch = note.getParentBranches()[0];
|
||||
|
||||
console.log(note.getParentBranches());
|
||||
|
||||
zipExportService.exportToZip(taskContext, branch, format, res);
|
||||
});
|
||||
}
|
||||
|
||||
function parseSearchParams(req) {
|
||||
|
||||
@@ -52,13 +52,13 @@ if (utils.isElectron()) {
|
||||
title: suggestion,
|
||||
command: "replaceMisspelling",
|
||||
spellingSuggestion: suggestion,
|
||||
uiIcon: "empty"
|
||||
uiIcon: "bx bx-empty"
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: `Add "${params.misspelledWord}" to dictionary`,
|
||||
uiIcon: "plus",
|
||||
uiIcon: "bx bx-plus",
|
||||
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canCut && hasText,
|
||||
title: `Cut <kbd>${platformModifier}+X`,
|
||||
uiIcon: "cut",
|
||||
uiIcon: "bx bx-cut",
|
||||
handler: () => webContents.cut()
|
||||
});
|
||||
}
|
||||
@@ -78,7 +78,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canCopy && hasText,
|
||||
title: `Copy <kbd>${platformModifier}+C`,
|
||||
uiIcon: "copy",
|
||||
uiIcon: "bx bx-copy",
|
||||
handler: () => webContents.copy()
|
||||
});
|
||||
}
|
||||
@@ -86,7 +86,7 @@ if (utils.isElectron()) {
|
||||
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') {
|
||||
items.push({
|
||||
title: `Copy link`,
|
||||
uiIcon: "copy",
|
||||
uiIcon: "bx bx-copy",
|
||||
handler: () => {
|
||||
electron.clipboard.write({
|
||||
bookmark: params.linkText,
|
||||
@@ -100,7 +100,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canPaste,
|
||||
title: `Paste <kbd>${platformModifier}+V`,
|
||||
uiIcon: "paste",
|
||||
uiIcon: "bx bx-paste",
|
||||
handler: () => webContents.paste()
|
||||
});
|
||||
}
|
||||
@@ -109,7 +109,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canPaste,
|
||||
title: `Paste as plain text <kbd>${platformModifier}+Shift+V`,
|
||||
uiIcon: "paste",
|
||||
uiIcon: "bx bx-paste",
|
||||
handler: () => webContents.pasteAndMatchStyle()
|
||||
});
|
||||
}
|
||||
@@ -122,7 +122,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canPaste,
|
||||
title: `Search for "${shortenedSelection}" with DuckDuckGo`,
|
||||
uiIcon: "search-alt",
|
||||
uiIcon: "bx bx-search-alt",
|
||||
handler: () => electron.shell.openExternal(`https://duckduckgo.com/?q=${encodeURIComponent(params.selectionText)}`)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#about-dialog");
|
||||
const $appVersion = $("#app-version");
|
||||
const $dbVersion = $("#db-version");
|
||||
const $syncVersion = $("#sync-version");
|
||||
const $buildDate = $("#build-date");
|
||||
const $buildRevision = $("#build-revision");
|
||||
const $dataDirectory = $("#data-directory");
|
||||
|
||||
export async function showDialog() {
|
||||
const appInfo = await server.get('app-info');
|
||||
|
||||
$appVersion.text(appInfo.appVersion);
|
||||
$dbVersion.text(appInfo.dbVersion);
|
||||
$syncVersion.text(appInfo.syncVersion);
|
||||
$buildDate.text(appInfo.buildDate);
|
||||
$buildRevision.text(appInfo.buildRevision);
|
||||
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
|
||||
$dataDirectory.text(appInfo.dataDirectory);
|
||||
|
||||
utils.openDialog($dialog);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#add-link-dialog");
|
||||
const $form = $("#add-link-form");
|
||||
const $autoComplete = $("#add-link-note-autocomplete");
|
||||
const $linkTitle = $("#link-title");
|
||||
const $addLinkTitleSettings = $("#add-link-title-settings");
|
||||
const $addLinkTitleRadios = $(".add-link-title-radios");
|
||||
const $addLinkTitleFormGroup = $("#add-link-title-form-group");
|
||||
|
||||
/** @var TextTypeWidget */
|
||||
let textTypeWidget;
|
||||
|
||||
export async function showDialog(widget, text = '') {
|
||||
textTypeWidget = widget;
|
||||
|
||||
$addLinkTitleSettings.toggle(!textTypeWidget.hasSelection());
|
||||
|
||||
$addLinkTitleSettings.find('input[type=radio]').on('change', updateTitleSettingsVisibility);
|
||||
|
||||
// with selection hyper link is implied
|
||||
if (textTypeWidget.hasSelection()) {
|
||||
$addLinkTitleSettings.find("input[value='hyper-link']").prop("checked", true);
|
||||
}
|
||||
else {
|
||||
$addLinkTitleSettings.find("input[value='reference-link']").prop("checked", true);
|
||||
}
|
||||
|
||||
updateTitleSettingsVisibility();
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$autoComplete.val('');
|
||||
$linkTitle.val('');
|
||||
|
||||
async function setDefaultLinkTitle(noteId) {
|
||||
const noteTitle = await treeService.getNoteTitle(noteId);
|
||||
|
||||
$linkTitle.val(noteTitle);
|
||||
}
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($autoComplete, {
|
||||
allowExternalLinks: true,
|
||||
allowCreatingNotes: true
|
||||
});
|
||||
|
||||
$autoComplete.on('autocomplete:noteselected', (event, suggestion, dataset) => {
|
||||
if (!suggestion.notePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateTitleSettingsVisibility();
|
||||
|
||||
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
|
||||
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
}
|
||||
});
|
||||
|
||||
$autoComplete.on('autocomplete:externallinkselected', (event, suggestion, dataset) => {
|
||||
if (!suggestion.externalLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateTitleSettingsVisibility();
|
||||
|
||||
$linkTitle.val(suggestion.externalLink);
|
||||
});
|
||||
|
||||
$autoComplete.on('autocomplete:cursorchanged', function(event, suggestion, dataset) {
|
||||
if (suggestion.externalLink) {
|
||||
$linkTitle.val(suggestion.externalLink)
|
||||
}
|
||||
else {
|
||||
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
|
||||
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (text && text.trim()) {
|
||||
noteAutocompleteService.setText($autoComplete, text);
|
||||
}
|
||||
else {
|
||||
noteAutocompleteService.showRecentNotes($autoComplete);
|
||||
}
|
||||
|
||||
$autoComplete
|
||||
.trigger('focus')
|
||||
.trigger('select'); // to be able to quickly remove entered text
|
||||
}
|
||||
|
||||
function getLinkType() {
|
||||
if ($autoComplete.getSelectedExternalLink()) {
|
||||
return 'external-link';
|
||||
}
|
||||
|
||||
return $addLinkTitleSettings.find('input[type=radio]:checked').val();
|
||||
}
|
||||
|
||||
function updateTitleSettingsVisibility() {
|
||||
const linkType = getLinkType();
|
||||
|
||||
$addLinkTitleFormGroup.toggle(linkType !== 'reference-link');
|
||||
$addLinkTitleRadios.toggle(linkType !== 'external-link')
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
if ($autoComplete.getSelectedNotePath()) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
const linkTitle = getLinkType() === 'reference-link' ? null : $linkTitle.val();
|
||||
|
||||
textTypeWidget.addLink($autoComplete.getSelectedNotePath(), linkTitle);
|
||||
}
|
||||
else if ($autoComplete.getSelectedExternalLink()) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
textTypeWidget.addLink($autoComplete.getSelectedExternalLink(), $linkTitle.val());
|
||||
}
|
||||
else {
|
||||
logError("No link to add.");
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#backend-log-dialog");
|
||||
const $backendLogTextArea = $("#backend-log-textarea");
|
||||
const $refreshBackendLog = $("#refresh-backend-log-button");
|
||||
|
||||
export async function showDialog() {
|
||||
utils.openDialog($dialog);
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
$backendLogTextArea.scrollTop($backendLogTextArea[0].scrollHeight);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const backendLog = await server.get('backend-log');
|
||||
|
||||
$backendLogTextArea.text(backendLog);
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
$refreshBackendLog.on('click', load);
|
||||
|
||||
$dialog.on('shown.bs.modal', scrollToBottom);
|
||||
@@ -1,59 +0,0 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import server from '../services/server.js';
|
||||
import froca from "../services/froca.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#branch-prefix-dialog");
|
||||
const $form = $("#branch-prefix-form");
|
||||
const $treePrefixInput = $("#branch-prefix-input");
|
||||
const $noteTitle = $('#branch-prefix-note-title');
|
||||
|
||||
let branchId;
|
||||
|
||||
export async function showDialog(notePath) {
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
|
||||
if (!noteId || !parentNoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
branchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
const branch = froca.getBranch(branchId);
|
||||
|
||||
if (!branch || branch.noteId === 'root') {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentNote = await froca.getNote(branch.parentNoteId);
|
||||
|
||||
if (parentNote.type === 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$treePrefixInput.val(branch.prefix);
|
||||
|
||||
const noteTitle = await treeService.getNoteTitle(noteId);
|
||||
|
||||
$noteTitle.text(" - " + noteTitle);
|
||||
}
|
||||
|
||||
async function savePrefix() {
|
||||
const prefix = $treePrefixInput.val();
|
||||
|
||||
await server.put('branches/' + branchId + '/set-prefix', { prefix: prefix });
|
||||
|
||||
$dialog.modal('hide');
|
||||
|
||||
toastService.showMessage("Branch prefix has been saved.");
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
savePrefix();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$dialog.on('shown.bs.modal', () => $treePrefixInput.trigger('focus'));
|
||||
@@ -1,73 +0,0 @@
|
||||
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||
import utils from "../services/utils.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import froca from "../services/froca.js";
|
||||
import branchService from "../services/branches.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
|
||||
const $dialog = $("#clone-to-dialog");
|
||||
const $form = $("#clone-to-form");
|
||||
const $noteAutoComplete = $("#clone-to-note-autocomplete");
|
||||
const $clonePrefix = $("#clone-prefix");
|
||||
const $noteList = $("#clone-to-note-list");
|
||||
|
||||
let clonedNoteIds;
|
||||
|
||||
export async function showDialog(noteIds) {
|
||||
if (!noteIds || noteIds.length === 0) {
|
||||
noteIds = [ appContext.tabManager.getActiveContextNoteId() ];
|
||||
}
|
||||
|
||||
clonedNoteIds = [];
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
if (!clonedNoteIds.includes(noteId)) {
|
||||
clonedNoteIds.push(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$noteAutoComplete.val('').trigger('focus');
|
||||
|
||||
$noteList.empty();
|
||||
|
||||
for (const noteId of clonedNoteIds) {
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
$noteList.append($("<li>").text(note.title));
|
||||
}
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($noteAutoComplete);
|
||||
noteAutocompleteService.showRecentNotes($noteAutoComplete);
|
||||
}
|
||||
|
||||
async function cloneNotesTo(notePath) {
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
|
||||
for (const cloneNoteId of clonedNoteIds) {
|
||||
await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, $clonePrefix.val());
|
||||
|
||||
const clonedNote = await froca.getNote(cloneNoteId);
|
||||
const targetNote = await froca.getBranch(targetBranchId).getNote();
|
||||
|
||||
toastService.showMessage(`Note "${clonedNote.title}" has been cloned into ${targetNote.title}`);
|
||||
}
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
const notePath = $noteAutoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
cloneNotesTo(notePath);
|
||||
}
|
||||
else {
|
||||
logError("No path to clone to.");
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
const $dialog = $("#confirm-dialog");
|
||||
const $confirmContent = $("#confirm-dialog-content");
|
||||
const $okButton = $("#confirm-dialog-ok-button");
|
||||
const $cancelButton = $("#confirm-dialog-cancel-button");
|
||||
const $custom = $("#confirm-dialog-custom");
|
||||
|
||||
const DELETE_NOTE_BUTTON_ID = "confirm-dialog-delete-note";
|
||||
|
||||
let resolve;
|
||||
let $originallyFocused; // element focused before the dialog was opened so we can return to it afterwards
|
||||
|
||||
export function confirm(message) {
|
||||
$originallyFocused = $(':focus');
|
||||
|
||||
$custom.hide();
|
||||
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
if (typeof message === 'string') {
|
||||
message = $("<div>").text(message);
|
||||
}
|
||||
|
||||
$confirmContent.empty().append(message);
|
||||
|
||||
$dialog.modal();
|
||||
|
||||
return new Promise((res, rej) => { resolve = res; });
|
||||
}
|
||||
|
||||
export function confirmDeleteNoteBoxWithNote(title) {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
$confirmContent.text(`Are you sure you want to remove the note "${title}" from relation map?`);
|
||||
|
||||
$custom.empty()
|
||||
.append("<br/>")
|
||||
.append($("<div>").addClass("form-check")
|
||||
.append($("<input>")
|
||||
.attr("id", DELETE_NOTE_BUTTON_ID)
|
||||
.attr("type", "checkbox")
|
||||
.addClass("form-check-input"))
|
||||
.append($("<label>")
|
||||
.attr("for", DELETE_NOTE_BUTTON_ID)
|
||||
.addClass("form-check-label")
|
||||
.attr("style", "text-decoration: underline dotted black")
|
||||
.attr("title", "If you don't check this, note will be only removed from relation map, but will stay as a note.")
|
||||
.html("Also delete note")));
|
||||
$custom.show();
|
||||
|
||||
$dialog.modal();
|
||||
|
||||
return new Promise((res, rej) => { resolve = res; });
|
||||
}
|
||||
|
||||
export function isDeleteNoteChecked() {
|
||||
return $("#" + DELETE_NOTE_BUTTON_ID + ":checked").length > 0;
|
||||
}
|
||||
|
||||
$dialog.on('shown.bs.modal', () => $okButton.trigger("focus"));
|
||||
|
||||
$dialog.on("hidden.bs.modal", () => {
|
||||
if (resolve) {
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
if ($originallyFocused) {
|
||||
$originallyFocused.trigger('focus');
|
||||
$originallyFocused = null;
|
||||
}
|
||||
});
|
||||
|
||||
function doResolve(ret) {
|
||||
resolve(ret);
|
||||
resolve = null;
|
||||
|
||||
$dialog.modal("hide");
|
||||
}
|
||||
|
||||
$cancelButton.on('click', () => doResolve(false));
|
||||
$okButton.on('click', () => doResolve(true));
|
||||
@@ -1,99 +0,0 @@
|
||||
import server from "../services/server.js";
|
||||
import froca from "../services/froca.js";
|
||||
import linkService from "../services/link.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#delete-notes-dialog");
|
||||
const $okButton = $("#delete-notes-dialog-ok-button");
|
||||
const $cancelButton = $("#delete-notes-dialog-cancel-button");
|
||||
const $deleteNotesList = $("#delete-notes-list");
|
||||
const $brokenRelationsList = $("#broken-relations-list");
|
||||
const $deletedNotesCount = $("#deleted-notes-count");
|
||||
const $noNoteToDeleteWrapper = $("#no-note-to-delete-wrapper");
|
||||
const $deleteNotesListWrapper = $("#delete-notes-list-wrapper");
|
||||
const $brokenRelationsListWrapper = $("#broken-relations-wrapper");
|
||||
const $brokenRelationsCount = $("#broke-relations-count");
|
||||
const $deleteAllClones = $("#delete-all-clones");
|
||||
const $eraseNotes = $("#erase-notes");
|
||||
|
||||
let branchIds = null;
|
||||
let resolve = null;
|
||||
|
||||
async function renderDeletePreview() {
|
||||
const response = await server.post('delete-notes-preview', {
|
||||
branchIdsToDelete: branchIds,
|
||||
deleteAllClones: isDeleteAllClonesChecked()
|
||||
});
|
||||
|
||||
$deleteNotesList.empty();
|
||||
$brokenRelationsList.empty();
|
||||
|
||||
$deleteNotesListWrapper.toggle(response.noteIdsToBeDeleted.length > 0);
|
||||
$noNoteToDeleteWrapper.toggle(response.noteIdsToBeDeleted.length === 0);
|
||||
|
||||
for (const note of await froca.getNotes(response.noteIdsToBeDeleted)) {
|
||||
$deleteNotesList.append(
|
||||
$("<li>").append(
|
||||
await linkService.createNoteLink(note.noteId, {showNotePath: true})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$deletedNotesCount.text(response.noteIdsToBeDeleted.length);
|
||||
|
||||
$brokenRelationsListWrapper.toggle(response.brokenRelations.length > 0);
|
||||
$brokenRelationsCount.text(response.brokenRelations.length);
|
||||
|
||||
await froca.getNotes(response.brokenRelations.map(br => br.noteId));
|
||||
|
||||
for (const attr of response.brokenRelations) {
|
||||
$brokenRelationsList.append(
|
||||
$("<li>")
|
||||
.append(`Note `)
|
||||
.append(await linkService.createNoteLink(attr.value))
|
||||
.append(` (to be deleted) is referenced by relation <code>${attr.name}</code> originating from `)
|
||||
.append(await linkService.createNoteLink(attr.noteId))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function showDialog(branchIdsToDelete) {
|
||||
branchIds = branchIdsToDelete;
|
||||
|
||||
await renderDeletePreview();
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$deleteAllClones.prop("checked", false);
|
||||
$eraseNotes.prop("checked", false);
|
||||
|
||||
return new Promise((res, rej) => resolve = res);
|
||||
}
|
||||
|
||||
export function isDeleteAllClonesChecked() {
|
||||
return $deleteAllClones.is(":checked");
|
||||
}
|
||||
|
||||
export function isEraseNotesChecked() {
|
||||
return $eraseNotes.is(":checked");
|
||||
}
|
||||
|
||||
$dialog.on('shown.bs.modal', () => $okButton.trigger("focus"));
|
||||
|
||||
$cancelButton.on('click', () => {
|
||||
utils.closeActiveDialog();
|
||||
|
||||
resolve({proceed: false});
|
||||
});
|
||||
|
||||
$okButton.on('click', () => {
|
||||
utils.closeActiveDialog();
|
||||
|
||||
resolve({
|
||||
proceed: true,
|
||||
deleteAllClones: isDeleteAllClonesChecked(),
|
||||
eraseNotes: isEraseNotesChecked()
|
||||
});
|
||||
});
|
||||
|
||||
$deleteAllClones.on('click', () => renderDeletePreview());
|
||||
@@ -1,137 +0,0 @@
|
||||
import treeService from "../services/tree.js";
|
||||
import utils from "../services/utils.js";
|
||||
import ws from "../services/ws.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import froca from "../services/froca.js";
|
||||
import openService from "../services/open.js";
|
||||
|
||||
const $dialog = $("#export-dialog");
|
||||
const $form = $("#export-form");
|
||||
const $noteTitle = $dialog.find(".export-note-title");
|
||||
const $subtreeFormats = $("#export-subtree-formats");
|
||||
const $singleFormats = $("#export-single-formats");
|
||||
const $subtreeType = $("#export-type-subtree");
|
||||
const $singleType = $("#export-type-single");
|
||||
const $exportButton = $("#export-button");
|
||||
const $opmlVersions = $("#opml-versions");
|
||||
|
||||
let taskId = '';
|
||||
let branchId = null;
|
||||
|
||||
export async function showDialog(notePath, defaultType) {
|
||||
// each opening of the dialog resets the taskId so we don't associate it with previous exports anymore
|
||||
taskId = '';
|
||||
$exportButton.removeAttr("disabled");
|
||||
|
||||
if (defaultType === 'subtree') {
|
||||
$subtreeType.prop("checked", true).trigger('change');
|
||||
|
||||
// to show/hide OPML versions
|
||||
$("input[name=export-subtree-format]:checked").trigger('change');
|
||||
}
|
||||
else if (defaultType === 'single') {
|
||||
$singleType.prop("checked", true).trigger('change');
|
||||
}
|
||||
else {
|
||||
throw new Error("Unrecognized type " + defaultType);
|
||||
}
|
||||
|
||||
$("#opml-v2").prop("checked", true); // setting default
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
|
||||
branchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
|
||||
const noteTitle = await treeService.getNoteTitle(noteId);
|
||||
|
||||
$noteTitle.html(noteTitle);
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
$dialog.modal('hide');
|
||||
|
||||
const exportType = $dialog.find("input[name='export-type']:checked").val();
|
||||
|
||||
if (!exportType) {
|
||||
// this shouldn't happen as we always choose default export type
|
||||
alert("Choose export type first please");
|
||||
return;
|
||||
}
|
||||
|
||||
const exportFormat = exportType === 'subtree'
|
||||
? $("input[name=export-subtree-format]:checked").val()
|
||||
: $("input[name=export-single-format]:checked").val();
|
||||
|
||||
const exportVersion = exportFormat === 'opml' ? $dialog.find("input[name='opml-version']:checked").val() : "1.0";
|
||||
|
||||
exportBranch(branchId, exportType, exportFormat, exportVersion);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
function exportBranch(branchId, type, format, version) {
|
||||
taskId = utils.randomString(10);
|
||||
|
||||
const url = openService.getUrlForDownload(`api/notes/${branchId}/export/${type}/${format}/${version}/${taskId}`);
|
||||
|
||||
openService.download(url);
|
||||
}
|
||||
|
||||
$('input[name=export-type]').on('change', function () {
|
||||
if (this.value === 'subtree') {
|
||||
if ($("input[name=export-subtree-format]:checked").length === 0) {
|
||||
$("input[name=export-subtree-format]:first").prop("checked", true);
|
||||
}
|
||||
|
||||
$subtreeFormats.slideDown();
|
||||
$singleFormats.slideUp();
|
||||
}
|
||||
else {
|
||||
if ($("input[name=export-single-format]:checked").length === 0) {
|
||||
$("input[name=export-single-format]:first").prop("checked", true);
|
||||
}
|
||||
|
||||
$subtreeFormats.slideUp();
|
||||
$singleFormats.slideDown();
|
||||
}
|
||||
});
|
||||
|
||||
$('input[name=export-subtree-format]').on('change', function () {
|
||||
if (this.value === 'opml') {
|
||||
$opmlVersions.slideDown();
|
||||
}
|
||||
else {
|
||||
$opmlVersions.slideUp();
|
||||
}
|
||||
});
|
||||
|
||||
function makeToast(id, message) {
|
||||
return {
|
||||
id: id,
|
||||
title: "Export status",
|
||||
message: message,
|
||||
icon: "arrow-square-up-right"
|
||||
};
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async message => {
|
||||
if (message.taskType !== 'export') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'taskError') {
|
||||
toastService.closePersistent(message.taskId);
|
||||
toastService.showError(message.message);
|
||||
}
|
||||
else if (message.type === 'taskProgressCount') {
|
||||
toastService.showPersistent(makeToast(message.taskId, "Export in progress: " + message.progressCount));
|
||||
}
|
||||
else if (message.type === 'taskSucceeded') {
|
||||
const toast = makeToast(message.taskId, "Export finished successfully.");
|
||||
toast.closeAfter = 5000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
}
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#help-dialog");
|
||||
|
||||
export async function showDialog() {
|
||||
utils.openDialog($dialog);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import utils from '../services/utils.js';
|
||||
import treeService from "../services/tree.js";
|
||||
import importService from "../services/import.js";
|
||||
import options from "../services/options.js";
|
||||
|
||||
const $dialog = $("#import-dialog");
|
||||
const $form = $("#import-form");
|
||||
const $noteTitle = $dialog.find(".import-note-title");
|
||||
const $fileUploadInput = $("#import-file-upload-input");
|
||||
const $importButton = $("#import-button");
|
||||
const $safeImportCheckbox = $("#safe-import-checkbox");
|
||||
const $shrinkImagesWrapper = $("shrink-images-wrapper");
|
||||
const $shrinkImagesCheckbox = $("#shrink-images-checkbox");
|
||||
const $textImportedAsTextCheckbox = $("#text-imported-as-text-checkbox");
|
||||
const $codeImportedAsCodeCheckbox = $("#code-imported-as-code-checkbox");
|
||||
const $explodeArchivesCheckbox = $("#explode-archives-checkbox");
|
||||
const $replaceUnderscoresWithSpacesCheckbox = $("#replace-underscores-with-spaces-checkbox");
|
||||
const $csrf = $("#import-csrf");
|
||||
|
||||
let parentNoteId = null;
|
||||
|
||||
export async function showDialog(noteId) {
|
||||
$fileUploadInput.val('').trigger('change'); // to trigger Import button disabling listener below
|
||||
|
||||
$safeImportCheckbox.prop("checked", true);
|
||||
$shrinkImagesCheckbox.prop("checked", options.is('compressImages'));
|
||||
$textImportedAsTextCheckbox.prop("checked", true);
|
||||
$codeImportedAsCodeCheckbox.prop("checked", true);
|
||||
$explodeArchivesCheckbox.prop("checked", true);
|
||||
$replaceUnderscoresWithSpacesCheckbox.prop("checked", true);
|
||||
|
||||
parentNoteId = noteId;
|
||||
|
||||
$noteTitle.text(await treeService.getNoteTitle(parentNoteId));
|
||||
|
||||
utils.openDialog($dialog);
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
// disabling so that import is not triggered again.
|
||||
$importButton.attr("disabled", "disabled");
|
||||
|
||||
importIntoNote(parentNoteId);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
async function importIntoNote(parentNoteId) {
|
||||
const files = Array.from($fileUploadInput[0].files); // shallow copy since we're resetting the upload button below
|
||||
|
||||
const options = {
|
||||
safeImport: boolToString($safeImportCheckbox),
|
||||
shrinkImages: boolToString($shrinkImagesCheckbox),
|
||||
textImportedAsText: boolToString($textImportedAsTextCheckbox),
|
||||
codeImportedAsCode: boolToString($codeImportedAsCodeCheckbox),
|
||||
explodeArchives: boolToString($explodeArchivesCheckbox),
|
||||
replaceUnderscoresWithSpaces: boolToString($replaceUnderscoresWithSpacesCheckbox)
|
||||
};
|
||||
|
||||
$dialog.modal('hide');
|
||||
|
||||
await importService.uploadFiles(parentNoteId, files, options);
|
||||
}
|
||||
|
||||
function boolToString($el) {
|
||||
return $el.is(":checked") ? "true" : "false";
|
||||
}
|
||||
|
||||
$fileUploadInput.on('change', () => {
|
||||
if ($fileUploadInput.val()) {
|
||||
$importButton.removeAttr("disabled");
|
||||
}
|
||||
else {
|
||||
$importButton.attr("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import noteAutocompleteService from '../services/note_autocomplete.js';
|
||||
import utils from "../services/utils.js";
|
||||
import froca from "../services/froca.js";
|
||||
|
||||
const $dialog = $("#include-note-dialog");
|
||||
const $form = $("#include-note-form");
|
||||
const $autoComplete = $("#include-note-autocomplete");
|
||||
|
||||
/** @var TextTypeWidget */
|
||||
let textTypeWidget;
|
||||
|
||||
export async function showDialog(widget) {
|
||||
textTypeWidget = widget;
|
||||
|
||||
$autoComplete.val('');
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($autoComplete, {
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowCreatingNotes: true
|
||||
});
|
||||
noteAutocompleteService.showRecentNotes($autoComplete);
|
||||
}
|
||||
|
||||
async function includeNote(notePath) {
|
||||
const noteId = treeService.getNoteIdFromNotePath(notePath);
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
const boxSize = $("input[name='include-note-box-size']:checked").val();
|
||||
|
||||
if (note.type === 'image') {
|
||||
// there's no benefit to use insert note functionlity for images
|
||||
// so we'll just add an IMG tag
|
||||
textTypeWidget.addImage(noteId);
|
||||
}
|
||||
else {
|
||||
textTypeWidget.addIncludeNote(noteId, boxSize);
|
||||
}
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
const notePath = $autoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
includeNote(notePath);
|
||||
}
|
||||
else {
|
||||
logError("No noteId to include.");
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#info-dialog");
|
||||
const $infoContent = $("#info-dialog-content");
|
||||
const $okButton = $("#info-dialog-ok-button");
|
||||
|
||||
let resolve;
|
||||
let $originallyFocused; // element focused before the dialog was opened so we can return to it afterwards
|
||||
|
||||
export function info(message) {
|
||||
$originallyFocused = $(':focus');
|
||||
|
||||
$infoContent.text(message);
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
return new Promise((res, rej) => { resolve = res; });
|
||||
}
|
||||
|
||||
$dialog.on('shown.bs.modal', () => $okButton.trigger("focus"));
|
||||
|
||||
$dialog.on("hidden.bs.modal", () => {
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
if ($originallyFocused) {
|
||||
$originallyFocused.trigger('focus');
|
||||
$originallyFocused = null;
|
||||
}
|
||||
});
|
||||
|
||||
$okButton.on('click', () => $dialog.modal("hide"));
|
||||
@@ -1,60 +0,0 @@
|
||||
import noteAutocompleteService from '../services/note_autocomplete.js';
|
||||
import utils from "../services/utils.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
|
||||
const $dialog = $("#jump-to-note-dialog");
|
||||
const $autoComplete = $("#jump-to-note-autocomplete");
|
||||
const $showInFullTextButton = $("#show-in-full-text-button");
|
||||
|
||||
let lastOpenedTs = 0;
|
||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||
|
||||
export async function showDialog() {
|
||||
utils.openDialog($dialog);
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true })
|
||||
// clear any event listener added in previous invocation of this function
|
||||
.off('autocomplete:noteselected')
|
||||
.on('autocomplete:noteselected', function(event, suggestion, dataset) {
|
||||
if (!suggestion.notePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
appContext.tabManager.getActiveContext().setNote(suggestion.notePath);
|
||||
});
|
||||
|
||||
// if you open the Jump To dialog soon after using it previously it can often mean that you
|
||||
// actually want to search for the same thing (e.g. you opened the wrong note at first try)
|
||||
// so we'll keep the content.
|
||||
// if it's outside of this time limit then we assume it's a completely new search and show recent notes instead.
|
||||
if (Date.now() - lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) {
|
||||
noteAutocompleteService.showRecentNotes($autoComplete);
|
||||
}
|
||||
else {
|
||||
$autoComplete
|
||||
// hack, the actual search value is stored in <pre> element next to the search input
|
||||
// this is important because the search input value is replaced with the suggestion note's title
|
||||
.autocomplete("val", $autoComplete.next().text())
|
||||
.trigger('focus')
|
||||
.trigger('select');
|
||||
}
|
||||
|
||||
lastOpenedTs = Date.now();
|
||||
}
|
||||
|
||||
function showInFullText(e) {
|
||||
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const searchString = $autoComplete.val();
|
||||
|
||||
appContext.triggerCommand('searchNotes', {searchString});
|
||||
|
||||
$dialog.modal('hide');
|
||||
}
|
||||
|
||||
|
||||
$showInFullTextButton.on('click', showInFullText);
|
||||
|
||||
utils.bindElShortcut($dialog, 'ctrl+return', showInFullText);
|
||||
@@ -1,61 +0,0 @@
|
||||
import libraryLoader from "../services/library_loader.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import utils from "../services/utils.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
|
||||
const $dialog = $('#markdown-import-dialog');
|
||||
const $importTextarea = $('#markdown-import-textarea');
|
||||
const $importButton = $('#markdown-import-button');
|
||||
|
||||
async function convertMarkdownToHtml(text) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.COMMONMARK);
|
||||
|
||||
const reader = new commonmark.Parser();
|
||||
const writer = new commonmark.HtmlRenderer();
|
||||
const parsed = reader.parse(text);
|
||||
|
||||
const result = writer.render(parsed);
|
||||
|
||||
appContext.triggerCommand('executeInActiveTextEditor', {
|
||||
callback: textEditor => {
|
||||
const viewFragment = textEditor.data.processor.toView(result);
|
||||
const modelFragment = textEditor.data.toModel(viewFragment);
|
||||
|
||||
textEditor.model.insertContent(modelFragment, textEditor.model.document.selection);
|
||||
|
||||
toastService.showMessage("Markdown content has been imported into the document.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function importMarkdownInline() {
|
||||
if (appContext.tabManager.getActiveContextNoteType() !== 'text') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const {clipboard} = utils.dynamicRequire('electron');
|
||||
const text = clipboard.readText();
|
||||
|
||||
convertMarkdownToHtml(text);
|
||||
}
|
||||
else {
|
||||
utils.openDialog($dialog);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendForm() {
|
||||
const text = $importTextarea.val();
|
||||
|
||||
$dialog.modal('hide');
|
||||
|
||||
await convertMarkdownToHtml(text);
|
||||
|
||||
$importTextarea.val('');
|
||||
}
|
||||
|
||||
$importButton.on('click', sendForm);
|
||||
|
||||
$dialog.on('shown.bs.modal', () => $importTextarea.trigger('focus'));
|
||||
|
||||
utils.bindElShortcut($dialog, 'ctrl+return', sendForm);
|
||||
@@ -1,58 +0,0 @@
|
||||
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||
import utils from "../services/utils.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import froca from "../services/froca.js";
|
||||
import branchService from "../services/branches.js";
|
||||
import treeService from "../services/tree.js";
|
||||
|
||||
const $dialog = $("#move-to-dialog");
|
||||
const $form = $("#move-to-form");
|
||||
const $noteAutoComplete = $("#move-to-note-autocomplete");
|
||||
const $noteList = $("#move-to-note-list");
|
||||
|
||||
let movedBranchIds;
|
||||
|
||||
export async function showDialog(branchIds) {
|
||||
movedBranchIds = branchIds;
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$noteAutoComplete.val('').trigger('focus');
|
||||
|
||||
$noteList.empty();
|
||||
|
||||
for (const branchId of movedBranchIds) {
|
||||
const branch = froca.getBranch(branchId);
|
||||
const note = await froca.getNote(branch.noteId);
|
||||
|
||||
$noteList.append($("<li>").text(note.title));
|
||||
}
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($noteAutoComplete);
|
||||
noteAutocompleteService.showRecentNotes($noteAutoComplete);
|
||||
}
|
||||
|
||||
async function moveNotesTo(parentBranchId) {
|
||||
await branchService.moveToParentNote(movedBranchIds, parentBranchId);
|
||||
|
||||
const parentBranch = froca.getBranch(parentBranchId);
|
||||
const parentNote = await parentBranch.getNote();
|
||||
|
||||
toastService.showMessage(`Selected notes have been moved into ${parentNote.title}`);
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
const notePath = $noteAutoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
froca.getBranchId(parentNoteId, noteId).then(branchId => moveNotesTo(branchId));
|
||||
}
|
||||
else {
|
||||
logError("No path to move to.");
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -1,225 +0,0 @@
|
||||
import utils from '../services/utils.js';
|
||||
import server from '../services/server.js';
|
||||
import toastService from "../services/toast.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
import libraryLoader from "../services/library_loader.js";
|
||||
import openService from "../services/open.js";
|
||||
|
||||
const $dialog = $("#note-revisions-dialog");
|
||||
const $list = $("#note-revision-list");
|
||||
const $listDropdown = $("#note-revision-list-dropdown");
|
||||
const $content = $("#note-revision-content");
|
||||
const $title = $("#note-revision-title");
|
||||
const $titleButtons = $("#note-revision-title-buttons");
|
||||
const $eraseAllRevisionsButton = $("#note-revisions-erase-all-revisions-button");
|
||||
|
||||
$listDropdown.dropdown();
|
||||
|
||||
$listDropdown.parent().on('hide.bs.dropdown', e => {
|
||||
// prevent closing dropdown by clicking outside
|
||||
if (e.clickEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
let revisionItems = [];
|
||||
let note;
|
||||
let noteRevisionId;
|
||||
|
||||
export async function showCurrentNoteRevisions() {
|
||||
await showNoteRevisionsDialog(appContext.tabManager.getActiveContextNoteId());
|
||||
}
|
||||
|
||||
export async function showNoteRevisionsDialog(noteId, noteRevisionId) {
|
||||
utils.openDialog($dialog);
|
||||
|
||||
await loadNoteRevisions(noteId, noteRevisionId);
|
||||
}
|
||||
|
||||
async function loadNoteRevisions(noteId, noteRevId) {
|
||||
$list.empty();
|
||||
$content.empty();
|
||||
$titleButtons.empty();
|
||||
|
||||
note = appContext.tabManager.getActiveContextNote();
|
||||
revisionItems = await server.get(`notes/${noteId}/revisions`);
|
||||
|
||||
for (const item of revisionItems) {
|
||||
$list.append(
|
||||
$('<a class="dropdown-item" tabindex="0">')
|
||||
.text(item.dateLastEdited.substr(0, 16) + ` (${item.contentLength} bytes)`)
|
||||
.attr('data-note-revision-id', item.noteRevisionId)
|
||||
.attr('title', 'This revision was last edited on ' + item.dateLastEdited)
|
||||
);
|
||||
}
|
||||
|
||||
$listDropdown.dropdown('show');
|
||||
|
||||
noteRevisionId = noteRevId;
|
||||
|
||||
if (revisionItems.length > 0) {
|
||||
if (!noteRevisionId) {
|
||||
noteRevisionId = revisionItems[0].noteRevisionId;
|
||||
}
|
||||
} else {
|
||||
$title.text("No revisions for this note yet...");
|
||||
noteRevisionId = null;
|
||||
}
|
||||
|
||||
$eraseAllRevisionsButton.toggle(revisionItems.length > 0);
|
||||
}
|
||||
|
||||
$dialog.on('shown.bs.modal', () => {
|
||||
$list.find(`[data-note-revision-id="${noteRevisionId}"]`)
|
||||
.trigger('focus');
|
||||
});
|
||||
|
||||
async function setContentPane() {
|
||||
const noteRevisionId = $list.find(".active").attr('data-note-revision-id');
|
||||
|
||||
const revisionItem = revisionItems.find(r => r.noteRevisionId === noteRevisionId);
|
||||
|
||||
$titleButtons.empty();
|
||||
$content.empty();
|
||||
|
||||
$title.html(revisionItem.title);
|
||||
|
||||
const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>');
|
||||
|
||||
$restoreRevisionButton.on('click', async () => {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.';
|
||||
|
||||
if (await confirmDialog.confirm(text)) {
|
||||
await server.put(`notes/${revisionItem.noteId}/restore-revision/${revisionItem.noteRevisionId}`);
|
||||
|
||||
$dialog.modal('hide');
|
||||
|
||||
toastService.showMessage('Note revision has been restored.');
|
||||
}
|
||||
});
|
||||
|
||||
const $eraseRevisionButton = $('<button class="btn btn-sm" type="button">Delete this revision</button>');
|
||||
|
||||
$eraseRevisionButton.on('click', async () => {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
const text = 'Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.';
|
||||
|
||||
if (await confirmDialog.confirm(text)) {
|
||||
await server.remove(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`);
|
||||
|
||||
loadNoteRevisions(revisionItem.noteId);
|
||||
|
||||
toastService.showMessage('Note revision has been deleted.');
|
||||
}
|
||||
});
|
||||
|
||||
$titleButtons
|
||||
.append($restoreRevisionButton)
|
||||
.append(' ')
|
||||
.append($eraseRevisionButton)
|
||||
.append(' ');
|
||||
|
||||
const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
|
||||
|
||||
$downloadButton.on('click', () => openService.downloadNoteRevision(revisionItem.noteId, revisionItem.noteRevisionId));
|
||||
|
||||
$titleButtons.append($downloadButton);
|
||||
|
||||
const fullNoteRevision = await server.get(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`);
|
||||
|
||||
if (revisionItem.type === 'text') {
|
||||
$content.html(fullNoteRevision.content);
|
||||
|
||||
if ($content.find('span.math-tex').length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.KATEX);
|
||||
|
||||
renderMathInElement($content[0], {trust: true});
|
||||
}
|
||||
}
|
||||
else if (revisionItem.type === 'code') {
|
||||
$content.html($("<pre>").text(fullNoteRevision.content));
|
||||
}
|
||||
else if (revisionItem.type === 'image') {
|
||||
$content.html($("<img>")
|
||||
// reason why we put this inline as base64 is that we do not want to let user to copy this
|
||||
// as a URL to be used in a note. Instead if they copy and paste it into a note, it will be a uploaded as a new note
|
||||
.attr("src", `data:${note.mime};base64,` + fullNoteRevision.content)
|
||||
.css("max-width", "100%")
|
||||
.css("max-height", "100%"));
|
||||
}
|
||||
else if (revisionItem.type === 'file') {
|
||||
const $table = $("<table cellpadding='10'>")
|
||||
.append($("<tr>").append(
|
||||
$("<th>").text("MIME: "),
|
||||
$("<td>").text(revisionItem.mime)
|
||||
))
|
||||
.append($("<tr>").append(
|
||||
$("<th>").text("File size:"),
|
||||
$("<td>").text(revisionItem.contentLength + " bytes")
|
||||
));
|
||||
|
||||
if (fullNoteRevision.content) {
|
||||
$table.append($("<tr>").append(
|
||||
$('<td colspan="2">').append(
|
||||
$('<div style="font-weight: bold;">').text("Preview:"),
|
||||
$('<pre class="file-preview-content"></pre>')
|
||||
.text(fullNoteRevision.content)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
$content.html($table);
|
||||
}
|
||||
else if (revisionItem.type === 'canvas') {
|
||||
/**
|
||||
* FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com
|
||||
* REMOVE external dependency!!!! This is defined in the svg in defs.style
|
||||
*/
|
||||
const content = fullNoteRevision.content;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(content)
|
||||
const svg = data.svg || "no svg present."
|
||||
|
||||
/**
|
||||
* maxWidth: 100% use full width of container but do not enlarge!
|
||||
* height:auto to ensure that height scales with width
|
||||
*/
|
||||
const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"});
|
||||
$content.html($('<div>').append($svgHtml));
|
||||
} catch(err) {
|
||||
console.error("error parsing fullNoteRevision.content as JSON", fullNoteRevision.content, err);
|
||||
$content.html($("<div>").text("Error parsing content. Please check console.error() for more details."));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$content.text("Preview isn't available for this note type.");
|
||||
}
|
||||
}
|
||||
|
||||
$eraseAllRevisionsButton.on('click', async () => {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
const text = 'Do you want to delete all revisions of this note? This action will erase revision title and content, but still preserve revision metadata.';
|
||||
|
||||
if (await confirmDialog.confirm(text)) {
|
||||
await server.remove(`notes/${note.noteId}/revisions`);
|
||||
|
||||
$dialog.modal('hide');
|
||||
|
||||
toastService.showMessage('Note revisions has been deleted.');
|
||||
}
|
||||
});
|
||||
|
||||
$list.on('click', '.dropdown-item', e => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
$list.on('focus', '.dropdown-item', e => {
|
||||
$list.find('.dropdown-item').each((i, el) => {
|
||||
$(el).toggleClass('active', el === e.target);
|
||||
});
|
||||
|
||||
setContentPane();
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import appContext from "../services/app_context.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#note-source-dialog");
|
||||
const $noteSource = $("#note-source");
|
||||
|
||||
export async function showDialog() {
|
||||
utils.openDialog($dialog);
|
||||
|
||||
const noteCompletement = await appContext.tabManager.getActiveContext().getNoteComplement();
|
||||
|
||||
$noteSource.text(formatHtml(noteCompletement.content));
|
||||
}
|
||||
|
||||
function formatHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = str.trim();
|
||||
|
||||
return formatNode(div, 0).innerHTML.trim();
|
||||
}
|
||||
|
||||
function formatNode(node, level) {
|
||||
const indentBefore = new Array(level++ + 1).join(' ');
|
||||
const indentAfter = new Array(level - 1).join(' ');
|
||||
let textNode;
|
||||
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
textNode = document.createTextNode('\n' + indentBefore);
|
||||
node.insertBefore(textNode, node.children[i]);
|
||||
|
||||
formatNode(node.children[i], level);
|
||||
|
||||
if (node.lastElementChild === node.children[i]) {
|
||||
textNode = document.createTextNode('\n' + indentAfter);
|
||||
node.appendChild(textNode);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import server from '../services/server.js';
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#options-dialog");
|
||||
|
||||
export async function showDialog(openTab) {
|
||||
const options = await server.get('options');
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
(await Promise.all([
|
||||
import('./options/appearance.js'),
|
||||
import('./options/shortcuts.js'),
|
||||
import('./options/code_notes.js'),
|
||||
import('./options/password.js'),
|
||||
import('./options/etapi.js'),
|
||||
import('./options/backup.js'),
|
||||
import('./options/sync.js'),
|
||||
import('./options/other.js'),
|
||||
import('./options/advanced.js')
|
||||
]))
|
||||
.map(m => new m.default)
|
||||
.forEach(tab => {
|
||||
if (tab.optionsLoaded) {
|
||||
tab.optionsLoaded(options)
|
||||
}
|
||||
});
|
||||
|
||||
if (openTab) {
|
||||
$(`.nav-link[href='#options-${openTab}']`).trigger("click");
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import utils from "../services/utils.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
|
||||
export function show() {
|
||||
const $dialog = $("#password-not-set-dialog");
|
||||
const $openPasswordOptionsButton = $("#open-password-options-button");
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$openPasswordOptionsButton.on("click", () => {
|
||||
appContext.triggerCommand("showOptions", { openTab: 'password' });
|
||||
});
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#prompt-dialog");
|
||||
const $dialogBody = $dialog.find(".modal-body");
|
||||
|
||||
let $question;
|
||||
let $answer;
|
||||
|
||||
const $form = $("#prompt-dialog-form");
|
||||
|
||||
let resolve;
|
||||
let shownCb;
|
||||
|
||||
export function ask({ title, message, defaultValue, shown }) {
|
||||
shownCb = shown;
|
||||
|
||||
$("#prompt-title").text(title || "Prompt");
|
||||
|
||||
$question = $("<label>")
|
||||
.prop("for", "prompt-dialog-answer")
|
||||
.text(message);
|
||||
|
||||
$answer = $("<input>")
|
||||
.prop("type", "text")
|
||||
.prop("id", "prompt-dialog-answer")
|
||||
.addClass("form-control")
|
||||
.val(defaultValue || "");
|
||||
|
||||
$dialogBody.empty().append(
|
||||
$("<div>")
|
||||
.addClass("form-group")
|
||||
.append($question)
|
||||
.append($answer));
|
||||
|
||||
utils.openDialog($dialog, false);
|
||||
|
||||
return new Promise((res, rej) => { resolve = res; });
|
||||
}
|
||||
|
||||
$dialog.on('shown.bs.modal', () => {
|
||||
if (shownCb) {
|
||||
shownCb({ $dialog, $question, $answer, $form });
|
||||
}
|
||||
|
||||
$answer.trigger('focus').select();
|
||||
});
|
||||
|
||||
$dialog.on("hidden.bs.modal", () => {
|
||||
if (resolve) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
$form.on('submit', e => {
|
||||
e.preventDefault();
|
||||
resolve($answer.val());
|
||||
|
||||
$dialog.modal('hide');
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#protected-session-password-dialog");
|
||||
const $passwordForm = $dialog.find(".protected-session-password-form");
|
||||
const $passwordInput = $dialog.find(".protected-session-password");
|
||||
|
||||
export function show() {
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$passwordInput.trigger('focus');
|
||||
}
|
||||
|
||||
export function close() {
|
||||
// this may fail if the dialog has not been previously opened (not sure if still true with Bootstrap modal)
|
||||
try {
|
||||
$dialog.modal('hide');
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
|
||||
$passwordForm.on('submit', () => {
|
||||
const password = $passwordInput.val();
|
||||
$passwordInput.val("");
|
||||
|
||||
protectedSessionService.setupProtectedSession(password);
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import linkService from '../services/link.js';
|
||||
import utils from '../services/utils.js';
|
||||
import server from '../services/server.js';
|
||||
import treeService from "../services/tree.js";
|
||||
import froca from "../services/froca.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
import hoistedNoteService from "../services/hoisted_note.js";
|
||||
|
||||
const $dialog = $("#recent-changes-dialog");
|
||||
const $content = $("#recent-changes-content");
|
||||
|
||||
export async function showDialog(ancestorNoteId) {
|
||||
utils.openDialog($dialog);
|
||||
|
||||
if (!ancestorNoteId) {
|
||||
ancestorNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
}
|
||||
|
||||
const recentChangesRows = await server.get('recent-changes/' + ancestorNoteId);
|
||||
|
||||
// preload all notes into cache
|
||||
await froca.getNotes(recentChangesRows.map(r => r.noteId), true);
|
||||
|
||||
$content.empty();
|
||||
|
||||
if (recentChangesRows.length === 0) {
|
||||
$content.append("No changes yet ...");
|
||||
}
|
||||
|
||||
const groupedByDate = groupByDate(recentChangesRows);
|
||||
|
||||
for (const [dateDay, dayChanges] of groupedByDate) {
|
||||
const $changesList = $('<ul>');
|
||||
|
||||
const dayEl = $('<div>').append($('<b>').text(dateDay)).append($changesList);
|
||||
|
||||
for (const change of dayChanges) {
|
||||
const formattedTime = change.date.substr(11, 5);
|
||||
|
||||
let $noteLink;
|
||||
|
||||
if (change.current_isDeleted) {
|
||||
$noteLink = $("<span>").text(change.current_title);
|
||||
|
||||
if (change.canBeUndeleted) {
|
||||
const $undeleteLink = $(`<a href="javascript:">`)
|
||||
.text("undelete")
|
||||
.on('click', async () => {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
const text = 'Do you want to undelete this note and its sub-notes?';
|
||||
|
||||
if (await confirmDialog.confirm(text)) {
|
||||
await server.put(`notes/${change.noteId}/undelete`);
|
||||
|
||||
$dialog.modal('hide');
|
||||
|
||||
await froca.reloadNotes([change.noteId]);
|
||||
|
||||
appContext.tabManager.getActiveContext().setNote(change.noteId);
|
||||
}
|
||||
});
|
||||
|
||||
$noteLink
|
||||
.append(' (')
|
||||
.append($undeleteLink)
|
||||
.append(')');
|
||||
}
|
||||
}
|
||||
else {
|
||||
const note = await froca.getNote(change.noteId);
|
||||
const notePath = treeService.getSomeNotePath(note);
|
||||
|
||||
if (notePath) {
|
||||
$noteLink = await linkService.createNoteLink(notePath, {
|
||||
title: change.title,
|
||||
showNotePath: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
$noteLink = $("<span>").text(note.title);
|
||||
}
|
||||
}
|
||||
|
||||
$changesList.append($('<li>')
|
||||
.append(
|
||||
$("<span>")
|
||||
.text(formattedTime)
|
||||
.attr("title", change.date)
|
||||
)
|
||||
.append(' - ')
|
||||
.append($noteLink));
|
||||
}
|
||||
|
||||
$content.append(dayEl);
|
||||
}
|
||||
}
|
||||
|
||||
function groupByDate(rows) {
|
||||
const groupedByDate = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
const dateDay = row.date.substr(0, 10);
|
||||
|
||||
if (!groupedByDate.has(dateDay)) {
|
||||
groupedByDate.set(dateDay, []);
|
||||
}
|
||||
|
||||
groupedByDate.get(dateDay).push(row);
|
||||
}
|
||||
|
||||
return groupedByDate;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const $dialog = $("#sort-child-notes-dialog");
|
||||
const $form = $("#sort-child-notes-form");
|
||||
|
||||
let parentNoteId = null;
|
||||
|
||||
$form.on('submit', async () => {
|
||||
const sortBy = $form.find("input[name='sort-by']:checked").val();
|
||||
const sortDirection = $form.find("input[name='sort-direction']:checked").val();
|
||||
|
||||
await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection});
|
||||
|
||||
utils.closeActiveDialog();
|
||||
});
|
||||
|
||||
export async function showDialog(noteId) {
|
||||
parentNoteId = noteId;
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$form.find('input:first').focus();
|
||||
}
|
||||
@@ -33,8 +33,19 @@ class Attribute {
|
||||
return this.froca.notes[this.noteId];
|
||||
}
|
||||
|
||||
/** @returns {Promise<NoteShort>} */
|
||||
async getTargetNote() {
|
||||
const targetNoteId = this.targetNoteId;
|
||||
|
||||
return await this.froca.getNote(targetNoteId, true);
|
||||
}
|
||||
|
||||
get targetNoteId() { // alias
|
||||
return this.type === 'relation' ? this.value : undefined;
|
||||
if (this.type !== 'relation') {
|
||||
throw new Error(`Attribute ${this.attributeId} is not a relation`);
|
||||
}
|
||||
|
||||
return this.value;
|
||||
}
|
||||
|
||||
get isAutoLink() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||
import ws from "../services/ws.js";
|
||||
import options from "../services/options.js";
|
||||
import froca from "../services/froca.js";
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
@@ -17,7 +18,8 @@ const NOTE_TYPE_ICONS = {
|
||||
"book": "bx bx-book",
|
||||
"note-map": "bx bx-map-alt",
|
||||
"mermaid": "bx bx-selection",
|
||||
"canvas": "bx bx-pen"
|
||||
"canvas": "bx bx-pen",
|
||||
"web-view": "bx bx-globe-alt"
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -262,7 +264,11 @@ class NoteShort {
|
||||
const templateNote = this.froca.notes[templateAttr.value];
|
||||
|
||||
if (templateNote && templateNote.noteId !== this.noteId) {
|
||||
attrArrs.push(templateNote.__getCachedAttributes(newPath));
|
||||
attrArrs.push(
|
||||
templateNote.__getCachedAttributes(newPath)
|
||||
// template attr is used as a marker for templates, but it's not meant to be inherited
|
||||
.filter(attr => !(attr.type === 'label' && attr.name === 'template'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,13 +641,18 @@ class NoteShort {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.getAttributes()
|
||||
const promotedAttrs = this.getAttributes()
|
||||
.filter(attr => attr.isDefinition())
|
||||
.filter(attr => {
|
||||
const def = attr.getDefinition();
|
||||
|
||||
return def && def.isPromoted;
|
||||
});
|
||||
|
||||
// attrs are not resorted if position changes after initial load
|
||||
promotedAttrs.sort((a, b) => a.position < b.position ? -1 : 1);
|
||||
|
||||
return promotedAttrs;
|
||||
}
|
||||
|
||||
hasAncestor(ancestorNoteId, visitedNoteIds = null) {
|
||||
@@ -802,6 +813,10 @@ class NoteShort {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isContentAvailable() {
|
||||
return !this.isProtected || protectedSessionHolder.isProtectedSessionAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteShort;
|
||||
|
||||
@@ -46,8 +46,38 @@ import OpenNoteButtonWidget from "../widgets/buttons/open_note_button_widget.js"
|
||||
import MermaidWidget from "../widgets/mermaid.js";
|
||||
import BookmarkButtons from "../widgets/bookmark_buttons.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import BacklinksWidget from "../widgets/backlinks.js";
|
||||
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import TocWidget from "../widgets/toc.js";
|
||||
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
|
||||
import AboutDialog from "../widgets/dialogs/about.js";
|
||||
import NoteSourceDialog from "../widgets/dialogs/note_source.js";
|
||||
import HelpDialog from "../widgets/dialogs/help.js";
|
||||
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
|
||||
import BackendLogDialog from "../widgets/dialogs/backend_log.js";
|
||||
import BranchPrefixDialog from "../widgets/dialogs/branch_prefix.js";
|
||||
import SortChildNotesDialog from "../widgets/dialogs/sort_child_notes.js";
|
||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||
import IncludeNoteDialog from "../widgets/dialogs/include_note.js";
|
||||
import NoteTypeChooserDialog from "../widgets/dialogs/note_type_chooser.js";
|
||||
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
|
||||
import AddLinkDialog from "../widgets/dialogs/add_link.js";
|
||||
import CloneToDialog from "../widgets/dialogs/clone_to.js";
|
||||
import MoveToDialog from "../widgets/dialogs/move_to.js";
|
||||
import ImportDialog from "../widgets/dialogs/import.js";
|
||||
import ExportDialog from "../widgets/dialogs/export.js";
|
||||
import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js";
|
||||
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
|
||||
import NoteRevisionsDialog from "../widgets/dialogs/note_revisions.js";
|
||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||
import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import ConfirmDialog from "../widgets/dialogs/confirm.js";
|
||||
import PromptDialog from "../widgets/dialogs/prompt.js";
|
||||
import OptionsDialog from "../widgets/dialogs/options.js";
|
||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||
import MermaidExportButton from "../widgets/floating_buttons/mermaid_export_button.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@@ -150,7 +180,11 @@ export default class DesktopLayout {
|
||||
)
|
||||
.child(new SharedInfoWidget())
|
||||
.child(new NoteUpdateStatusWidget())
|
||||
.child(new BacklinksWidget())
|
||||
.child(new FloatingButtons()
|
||||
.child(new RelationMapButtons())
|
||||
.child(new MermaidExportButton())
|
||||
.child(new BacklinksWidget())
|
||||
)
|
||||
.child(new MermaidWidget())
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
@@ -161,15 +195,45 @@ export default class DesktopLayout {
|
||||
.child(new SearchResultWidget())
|
||||
.child(new SqlResultWidget())
|
||||
)
|
||||
.child(...this.customWidgets.get('node-detail-pane'))
|
||||
.child(new FindWidget())
|
||||
.child(
|
||||
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC
|
||||
...this.customWidgets.get('note-detail-pane')
|
||||
)
|
||||
)
|
||||
)
|
||||
.child(...this.customWidgets.get('center-pane'))
|
||||
)
|
||||
.child(new RightPaneContainer()
|
||||
.child(new TocWidget())
|
||||
.child(...this.customWidgets.get('right-pane'))
|
||||
)
|
||||
)
|
||||
);
|
||||
)
|
||||
.child(new BulkActionsDialog())
|
||||
.child(new AboutDialog())
|
||||
.child(new NoteSourceDialog())
|
||||
.child(new HelpDialog())
|
||||
.child(new RecentChangesDialog())
|
||||
.child(new BackendLogDialog())
|
||||
.child(new BranchPrefixDialog())
|
||||
.child(new SortChildNotesDialog())
|
||||
.child(new PasswordNoteSetDialog())
|
||||
.child(new IncludeNoteDialog())
|
||||
.child(new NoteTypeChooserDialog())
|
||||
.child(new JumpToNoteDialog())
|
||||
.child(new AddLinkDialog())
|
||||
.child(new CloneToDialog())
|
||||
.child(new MoveToDialog())
|
||||
.child(new ImportDialog())
|
||||
.child(new ExportDialog())
|
||||
.child(new MarkdownImportDialog())
|
||||
.child(new ProtectedSessionPasswordDialog())
|
||||
.child(new NoteRevisionsDialog())
|
||||
.child(new DeleteNotesDialog())
|
||||
.child(new InfoDialog())
|
||||
.child(new ConfirmDialog())
|
||||
.child(new PromptDialog())
|
||||
.child(new OptionsDialog());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import CloseDetailButtonWidget from "../widgets/mobile_widgets/close_detail_butt
|
||||
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
|
||||
import ConfirmDialog from "../widgets/dialogs/confirm.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@@ -128,6 +130,8 @@ export default class MobileLayout {
|
||||
.css('padding', '5px 20px 10px 0')
|
||||
)
|
||||
)
|
||||
);
|
||||
)
|
||||
.child(new ProtectedSessionPasswordDialog())
|
||||
.child(new ConfirmDialog());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import Component from "../widgets/component.js";
|
||||
import keyboardActionsService from "./keyboard_actions.js";
|
||||
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
|
||||
import MainTreeExecutors from "./main_tree_executors.js";
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
import toast from "./toast.js";
|
||||
|
||||
class AppContext extends Component {
|
||||
|
||||
@@ -4,13 +4,14 @@ import toastService from "./toast.js";
|
||||
import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import ws from "./ws.js";
|
||||
import appContext from "./app_context.js";
|
||||
|
||||
async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
|
||||
branchIdsToMove = filterRootNote(branchIdsToMove);
|
||||
branchIdsToMove = filterSearchBranches(branchIdsToMove);
|
||||
|
||||
if (beforeBranchId === 'root') {
|
||||
alert('Cannot move notes before root note.');
|
||||
toastService.showError('Cannot move notes before root note.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,7 +19,7 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-before/${beforeBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
toastService.showError(resp.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +32,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
const afterNote = await froca.getBranch(afterBranchId).getNote();
|
||||
|
||||
if (afterNote.noteId === 'root' || afterNote.noteId === hoistedNoteService.getHoistedNoteId()) {
|
||||
alert('Cannot move notes after root note.');
|
||||
toastService.showError('Cannot move notes after root note.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,7 +42,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-after/${afterBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
toastService.showError(resp.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +62,7 @@ async function moveToParentNote(branchIdsToMove, newParentBranchId) {
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
toastService.showError(resp.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -81,8 +82,8 @@ async function deleteNotes(branchIdsToDelete) {
|
||||
deleteAllClones = false;
|
||||
}
|
||||
else {
|
||||
const deleteNotesDialog = await import("../dialogs/delete_notes.js");
|
||||
({proceed, deleteAllClones, eraseNotes} = await deleteNotesDialog.showDialog(branchIdsToDelete));
|
||||
({proceed, deleteAllClones, eraseNotes} = await new Promise(res =>
|
||||
appContext.triggerCommand('showDeleteNotesDialog', {branchIdsToDelete, callback: res})));
|
||||
}
|
||||
|
||||
if (!proceed) {
|
||||
@@ -126,7 +127,7 @@ async function moveNodeUpInHierarchy(node) {
|
||||
const resp = await server.put('branches/' + node.data.branchId + '/move-after/' + node.getParent().data.branchId);
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
toastService.showError(resp.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,7 +203,7 @@ async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) {
|
||||
});
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
toastService.showError(resp.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +213,7 @@ async function cloneNoteToNote(childNoteId, parentNoteId, prefix) {
|
||||
});
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
toastService.showError(resp.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +222,7 @@ async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
const resp = await server.put('notes/' + noteId + '/clone-after/' + afterBranchId);
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
toastService.showError(resp.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
94
src/public/app/services/bulk_action.js
Normal file
94
src/public/app/services/bulk_action.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import server from "./server.js";
|
||||
import ws from "./ws.js";
|
||||
import MoveNoteBulkAction from "../widgets/bulk_actions/note/move_note.js";
|
||||
import DeleteNoteBulkAction from "../widgets/bulk_actions/note/delete_note.js";
|
||||
import DeleteNoteRevisionsBulkAction from "../widgets/bulk_actions/note/delete_note_revisions.js";
|
||||
import DeleteLabelBulkAction from "../widgets/bulk_actions/label/delete_label.js";
|
||||
import DeleteRelationBulkAction from "../widgets/bulk_actions/relation/delete_relation.js";
|
||||
import RenameLabelBulkAction from "../widgets/bulk_actions/label/rename_label.js";
|
||||
import RenameRelationBulkAction from "../widgets/bulk_actions/relation/rename_relation.js";
|
||||
import UpdateLabelValueBulkAction from "../widgets/bulk_actions/label/update_label_value.js";
|
||||
import UpdateRelationTargetBulkAction from "../widgets/bulk_actions/relation/update_relation_target.js";
|
||||
import ExecuteScriptBulkAction from "../widgets/bulk_actions/execute_script.js";
|
||||
import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
|
||||
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
|
||||
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
|
||||
|
||||
const ACTION_GROUPS = [
|
||||
{
|
||||
title: 'Labels',
|
||||
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
||||
},
|
||||
{
|
||||
title: 'Relations',
|
||||
actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction]
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteNoteRevisionsBulkAction],
|
||||
},
|
||||
{
|
||||
title: 'Other',
|
||||
actions: [ExecuteScriptBulkAction]
|
||||
}
|
||||
];
|
||||
|
||||
const ACTION_CLASSES = [
|
||||
RenameNoteBulkAction,
|
||||
MoveNoteBulkAction,
|
||||
DeleteNoteBulkAction,
|
||||
DeleteNoteRevisionsBulkAction,
|
||||
DeleteLabelBulkAction,
|
||||
DeleteRelationBulkAction,
|
||||
RenameLabelBulkAction,
|
||||
RenameRelationBulkAction,
|
||||
AddLabelBulkAction,
|
||||
AddRelationBulkAction,
|
||||
UpdateLabelValueBulkAction,
|
||||
UpdateRelationTargetBulkAction,
|
||||
ExecuteScriptBulkAction
|
||||
];
|
||||
|
||||
async function addAction(noteId, actionName) {
|
||||
await server.post(`notes/${noteId}/attributes`, {
|
||||
type: 'label',
|
||||
name: 'action',
|
||||
value: JSON.stringify({
|
||||
name: actionName
|
||||
})
|
||||
});
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
}
|
||||
|
||||
function parseActions(note) {
|
||||
const actionLabels = note.getLabels('action');
|
||||
|
||||
return actionLabels.map(actionAttr => {
|
||||
let actionDef;
|
||||
|
||||
try {
|
||||
actionDef = JSON.parse(actionAttr.value);
|
||||
} catch (e) {
|
||||
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const ActionClass = ACTION_CLASSES.find(actionClass => actionClass.actionName === actionDef.name);
|
||||
|
||||
if (!ActionClass) {
|
||||
logError(`No action class for '${actionDef.name}' found.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ActionClass(actionAttr, actionDef);
|
||||
})
|
||||
.filter(action => !!action);
|
||||
}
|
||||
|
||||
export default {
|
||||
addAction,
|
||||
parseActions,
|
||||
ACTION_CLASSES,
|
||||
ACTION_GROUPS
|
||||
};
|
||||
@@ -82,7 +82,7 @@ class ContextMenu {
|
||||
const $icon = $("<span>");
|
||||
|
||||
if (item.uiIcon) {
|
||||
$icon.addClass("bx bx-" + item.uiIcon);
|
||||
$icon.addClass(item.uiIcon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
|
||||
70
src/public/app/services/debounce.js
Normal file
70
src/public/app/services/debounce.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Returns a function, that, as long as it continues to be invoked, will not
|
||||
* be triggered. The function will be called after it stops being called for
|
||||
* N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
* leading edge, instead of the trailing. The function also has a property 'clear'
|
||||
* that is a function which will clear the timer to prevent previously scheduled executions.
|
||||
*
|
||||
* @source underscore.js
|
||||
* @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/
|
||||
* @param {Function} function to wrap
|
||||
* @param {Number} timeout in ms (`100`)
|
||||
* @param {Boolean} whether to execute at the beginning (`false`)
|
||||
* @api public
|
||||
*/
|
||||
function debounce(func, wait_ms, immediate){
|
||||
var timeout, args, context, timestamp, result;
|
||||
if (null == wait_ms) wait_ms = 100;
|
||||
|
||||
function later() {
|
||||
var last = Date.now() - timestamp;
|
||||
|
||||
if (last < wait_ms && last >= 0) {
|
||||
timeout = setTimeout(later, wait_ms - last);
|
||||
} else {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
result = func.apply(context, args);
|
||||
context = args = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var debounced = function(){
|
||||
context = this;
|
||||
args = arguments;
|
||||
timestamp = Date.now();
|
||||
var callNow = immediate && !timeout;
|
||||
if (!timeout) timeout = setTimeout(later, wait_ms);
|
||||
if (callNow) {
|
||||
result = func.apply(context, args);
|
||||
context = args = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
debounced.clear = function() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
debounced.flush = function() {
|
||||
if (timeout) {
|
||||
result = func.apply(context, args);
|
||||
context = args = null;
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
};
|
||||
|
||||
// Adds compatibility for ES modules
|
||||
debounce.debounce = debounce;
|
||||
|
||||
export default debounce;
|
||||
@@ -5,7 +5,6 @@ import server from "./server.js";
|
||||
import appContext from "./app_context.js";
|
||||
import Component from "../widgets/component.js";
|
||||
import toastService from "./toast.js";
|
||||
import noteCreateService from "./note_create.js";
|
||||
import ws from "./ws.js";
|
||||
import bundleService from "./bundle.js";
|
||||
|
||||
@@ -19,18 +18,6 @@ export default class Entrypoints extends Component {
|
||||
jQuery.hotkeys.options.filterContentEditable = false;
|
||||
jQuery.hotkeys.options.filterTextInputs = false;
|
||||
}
|
||||
|
||||
$(document).on('click', "a[data-action='note-revision']", async event => {
|
||||
const linkEl = $(event.target);
|
||||
const noteId = linkEl.attr('data-note-path');
|
||||
const noteRevisionId = linkEl.attr('data-note-revision-id');
|
||||
|
||||
const attributesDialog = await import("../dialogs/note_revisions.js");
|
||||
|
||||
attributesDialog.showNoteRevisionsDialog(noteId, noteRevisionId);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
openDevToolsCommand() {
|
||||
@@ -39,29 +26,6 @@ export default class Entrypoints extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
findInTextCommand() {
|
||||
if (!utils.isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
const {FindInPage} = utils.dynamicRequire('electron-find');
|
||||
const findInPage = new FindInPage(remote.getCurrentWebContents(), {
|
||||
offsetTop: 10,
|
||||
offsetRight: 10,
|
||||
boxBgColor: 'var(--main-background-color)',
|
||||
boxShadowColor: '#000',
|
||||
inputColor: 'var(--input-text-color)',
|
||||
inputBgColor: 'var(--input-background-color)',
|
||||
inputFocusColor: '#555',
|
||||
textColor: 'var(--main-text-color)',
|
||||
textHoverBgColor: '#555',
|
||||
caseSelectedColor: 'var(--main-border-color)'
|
||||
});
|
||||
|
||||
findInPage.openFindWindow();
|
||||
}
|
||||
|
||||
async createNoteIntoInboxCommand() {
|
||||
const inboxNote = await dateNoteService.getInboxNote();
|
||||
|
||||
@@ -215,7 +179,7 @@ export default class Entrypoints extends Component {
|
||||
const resp = await server.post("sql/execute/" + note.noteId);
|
||||
|
||||
if (!resp.success) {
|
||||
alert("Error occurred while executing SQL query: " + resp.message);
|
||||
toastService.showError("Error occurred while executing SQL query: " + resp.message);
|
||||
}
|
||||
|
||||
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
|
||||
|
||||
@@ -176,7 +176,7 @@ class Froca {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchResultNoteIds = await server.get('search-note/' + note.noteId);
|
||||
const {searchResultNoteIds, highlightedTokens} = await server.get('search-note/' + note.noteId);
|
||||
|
||||
if (!Array.isArray(searchResultNoteIds)) {
|
||||
throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`);
|
||||
@@ -207,6 +207,7 @@ class Froca {
|
||||
});
|
||||
|
||||
froca.notes[note.noteId].searchResultsLoaded = true;
|
||||
froca.notes[note.noteId].highlightedTokens = highlightedTokens;
|
||||
}
|
||||
|
||||
/** @returns {NoteShort[]} */
|
||||
|
||||
@@ -101,6 +101,26 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a note in a new split.
|
||||
*
|
||||
* @param {string} notePath (or noteId)
|
||||
* @param {boolean} activate - set to true to activate the new split, false to stay on the current split
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
this.openSplitWithNote = async (notePath, activate) => {
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
|
||||
const {ntxId} = subContexts[subContexts.length - 1];
|
||||
|
||||
appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath});
|
||||
|
||||
if (activate) {
|
||||
appContext.triggerEvent('focusAndSelectTitle');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} ToolbarButtonOptions
|
||||
* @property {string} title
|
||||
@@ -339,25 +359,61 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
/**
|
||||
* Adds given text to the editor cursor
|
||||
*
|
||||
* @deprecated use addTextToActiveContextEditor() instead
|
||||
* @param {string} text - this must be clear text, HTML is not supported.
|
||||
* @method
|
||||
*/
|
||||
this.addTextToActiveTabEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text});
|
||||
this.addTextToActiveTabEditor = text => {
|
||||
console.warn("api.addTextToActiveTabEditor() is deprecated, use addTextToActiveContextEditor() instead.");
|
||||
|
||||
return appContext.triggerCommand('addTextToActiveEditor', {text});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds given text to the editor cursor
|
||||
*
|
||||
* @param {string} text - this must be clear text, HTML is not supported.
|
||||
* @method
|
||||
*/
|
||||
this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text});
|
||||
|
||||
/**
|
||||
* @method
|
||||
* @deprecated use getActiveContextNote() instead
|
||||
* @returns {NoteShort} active note (loaded into right pane)
|
||||
*/
|
||||
this.getActiveTabNote = () => {
|
||||
console.warn("api.getActiveTabNote() is deprecated, use getActiveContextNote() instead.");
|
||||
|
||||
return appContext.tabManager.getActiveContextNote();
|
||||
};
|
||||
|
||||
/**
|
||||
* @method
|
||||
* @returns {NoteShort} active note (loaded into right pane)
|
||||
*/
|
||||
this.getActiveTabNote = () => appContext.tabManager.getActiveContextNote();
|
||||
this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote();
|
||||
|
||||
/**
|
||||
* See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
|
||||
*
|
||||
* @deprecated use getActiveContextTextEditor()
|
||||
* @method
|
||||
* @param [callback] - callback receiving "textEditor" instance
|
||||
*/
|
||||
this.getActiveTabTextEditor = callback => {
|
||||
console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead.");
|
||||
|
||||
return appContext.tabManager.getActiveContext()?.getTextEditor(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
|
||||
*
|
||||
* @method
|
||||
* @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance
|
||||
* @returns {Promise<CKEditor>} instance of CKEditor
|
||||
*/
|
||||
this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve}));
|
||||
this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor();
|
||||
|
||||
/**
|
||||
* See https://codemirror.net/doc/manual.html#api
|
||||
@@ -365,7 +421,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
* @method
|
||||
* @returns {Promise<CodeMirror>} instance of CodeMirror
|
||||
*/
|
||||
this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve}));
|
||||
this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor();
|
||||
|
||||
/**
|
||||
* Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the
|
||||
@@ -378,9 +434,20 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
|
||||
/**
|
||||
* @method
|
||||
* @deprecated use getActiveContextNotePath() instead
|
||||
* @returns {Promise<string|null>} returns note path of active note or null if there isn't active note
|
||||
*/
|
||||
this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath();
|
||||
this.getActiveTabNotePath = () => {
|
||||
console.warn("api.getActiveTabNotePath() is deprecated, use getActiveContextNotePath() instead.");
|
||||
|
||||
return appContext.tabManager.getActiveContextNotePath();
|
||||
};
|
||||
|
||||
/**
|
||||
* @method
|
||||
* @returns {Promise<string|null>} returns note path of active note or null if there isn't active note
|
||||
*/
|
||||
this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath();
|
||||
|
||||
/**
|
||||
* Returns component which owns given DOM element (the nearest parent component in DOM tree)
|
||||
|
||||
@@ -23,11 +23,7 @@ function setupGlobs() {
|
||||
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
|
||||
|
||||
// for CKEditor integration (button on block toolbar)
|
||||
window.glob.importMarkdownInline = async () => {
|
||||
const dialog = await import("../dialogs/markdown_import.js");
|
||||
|
||||
dialog.importMarkdownInline();
|
||||
};
|
||||
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
|
||||
|
||||
window.glob.SEARCH_HELP_TEXT = `
|
||||
<strong>Search tips</strong> - also see <button class="btn btn-sm" type="button" data-help-page="Search">complete help on search</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import appContext from "./app_context.js";
|
||||
import treeService from "./tree.js";
|
||||
import dialogService from "../widgets/dialog.js";
|
||||
|
||||
function getHoistedNoteId() {
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
@@ -36,9 +37,7 @@ async function checkNoteAccess(notePath, noteContext) {
|
||||
const hoistedNoteId = noteContext.hoistedNoteId;
|
||||
|
||||
if (!resolvedNotePath.includes(hoistedNoteId) && !resolvedNotePath.includes("hidden")) {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
|
||||
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?")) {
|
||||
if (!await dialogService.confirm("Requested note is outside of hoisted note subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,13 @@ const EXCALIDRAW = {
|
||||
"node_modules/react/umd/react.production.min.js",
|
||||
"node_modules/react-dom/umd/react-dom.production.min.js",
|
||||
"node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js",
|
||||
],
|
||||
// css: [
|
||||
// "stylesheets/somestyle.css"
|
||||
// ]
|
||||
]
|
||||
};
|
||||
|
||||
const MARKJS = {
|
||||
js: [
|
||||
"libraries/jquery.mark.es6.min.js"
|
||||
]
|
||||
};
|
||||
|
||||
async function requireLibrary(library) {
|
||||
@@ -118,5 +121,6 @@ export default {
|
||||
WHEEL_ZOOM,
|
||||
FORCE_GRAPH,
|
||||
MERMAID,
|
||||
EXCALIDRAW
|
||||
EXCALIDRAW,
|
||||
MARKJS
|
||||
}
|
||||
|
||||
@@ -85,11 +85,16 @@ function getNotePathFromLink($link) {
|
||||
}
|
||||
|
||||
function goToLink(e) {
|
||||
const $link = $(e.target).closest("a,.block-link");
|
||||
const address = $link.attr('href');
|
||||
|
||||
if (address?.startsWith("data:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const $link = $(e.target).closest("a,.block-link");
|
||||
|
||||
const notePath = getNotePathFromLink($link);
|
||||
|
||||
if (notePath) {
|
||||
@@ -115,8 +120,6 @@ function goToLink(e) {
|
||||
|| $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices
|
||||
|| $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices
|
||||
) {
|
||||
const address = $link.attr('href');
|
||||
|
||||
if (address) {
|
||||
if (address.toLowerCase().startsWith('http')) {
|
||||
window.open(address, '_blank');
|
||||
@@ -178,6 +181,16 @@ $(document).on('dblclick', "a", e => {
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('mousedown', 'a', e => {
|
||||
if (e.which === 2) {
|
||||
// prevent paste on middle click
|
||||
// https://github.com/zadam/trilium/issues/2995
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
export default {
|
||||
getNotePathFromUrl,
|
||||
createNoteLink,
|
||||
|
||||
@@ -6,9 +6,9 @@ function openContextMenu(notePath, e) {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "empty"},
|
||||
{title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "dock-right"},
|
||||
{title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "window-open"}
|
||||
{title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "bx bx-empty"},
|
||||
{title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"},
|
||||
{title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"}
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => {
|
||||
if (command === 'openNoteInNewTab') {
|
||||
|
||||
@@ -5,7 +5,7 @@ import noteCreateService from './note_create.js';
|
||||
import treeService from './tree.js';
|
||||
import froca from "./froca.js";
|
||||
|
||||
// this key needs to have this value so it's hit by the tooltip
|
||||
// this key needs to have this value, so it's hit by the tooltip
|
||||
const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
||||
|
||||
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
|
||||
@@ -89,6 +89,11 @@ function showRecentNotes($el) {
|
||||
$el.setSelectedNotePath("");
|
||||
$el.autocomplete("val", "");
|
||||
$el.trigger('focus');
|
||||
|
||||
// simulate pressing down arrow to trigger autocomplete
|
||||
const e = $.Event('keydown');
|
||||
e.which = 40; // arrow down
|
||||
$el.trigger(e);
|
||||
}
|
||||
|
||||
function initNoteAutocomplete($el, options) {
|
||||
@@ -140,7 +145,9 @@ function initNoteAutocomplete($el, options) {
|
||||
appendTo: document.querySelector('body'),
|
||||
hint: false,
|
||||
autoselect: true,
|
||||
openOnFocus: true,
|
||||
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
|
||||
// re-querying of the autocomplete source which then changes currently selected suggestion
|
||||
openOnFocus: false,
|
||||
minLength: 0,
|
||||
tabAutocomplete: false
|
||||
}, [
|
||||
@@ -170,9 +177,17 @@ function initNoteAutocomplete($el, options) {
|
||||
}
|
||||
|
||||
if (suggestion.action === 'create-note') {
|
||||
const {success, noteType, templateNoteId} = await noteCreateService.chooseNoteType();
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {note} = await noteCreateService.createNote(suggestion.parentNoteId, {
|
||||
title: suggestion.noteTitle,
|
||||
activate: false
|
||||
activate: false,
|
||||
type: noteType,
|
||||
templateNoteId: templateNoteId
|
||||
});
|
||||
|
||||
suggestion.notePath = treeService.getSomeNotePath(note);
|
||||
@@ -261,7 +276,6 @@ function init() {
|
||||
}
|
||||
|
||||
export default {
|
||||
autocompleteSource,
|
||||
autocompleteSourceForCKEditor,
|
||||
initNoteAutocomplete,
|
||||
showRecentNotes,
|
||||
|
||||
@@ -61,9 +61,11 @@ async function getRenderedContent(note, options = {}) {
|
||||
$renderedContent.append($("<pre>").text(trim(fullNote.content, options.trim)));
|
||||
}
|
||||
else if (type === 'image') {
|
||||
const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");
|
||||
|
||||
$renderedContent.append(
|
||||
$("<img>")
|
||||
.attr("src", `api/images/${note.noteId}/${note.title}`)
|
||||
.attr("src", `api/images/${note.noteId}/${sanitizedTitle}`)
|
||||
.css("max-width", "100%")
|
||||
);
|
||||
}
|
||||
@@ -144,7 +146,7 @@ async function getRenderedContent(note, options = {}) {
|
||||
else if (type === 'canvas') {
|
||||
// make sure surrounding container has size of what is visible. Then image is shrinked to its boundaries
|
||||
$renderedContent.css({height: "100%", width:"100%"});
|
||||
|
||||
|
||||
const noteComplement = await froca.getNoteComplement(note.noteId);
|
||||
const content = noteComplement.content || "";
|
||||
|
||||
|
||||
@@ -226,6 +226,35 @@ class NoteContext extends Component {
|
||||
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
|
||||
&& !this.note.hasLabel('hideChildrenOverview');
|
||||
}
|
||||
|
||||
async getTextEditor(callback) {
|
||||
return new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', {
|
||||
callback,
|
||||
resolve,
|
||||
ntxId: this.ntxId
|
||||
}));
|
||||
}
|
||||
|
||||
async getCodeEditor() {
|
||||
return new Promise(resolve => appContext.triggerCommand('executeWithCodeEditor', {
|
||||
resolve,
|
||||
ntxId: this.ntxId
|
||||
}));
|
||||
}
|
||||
|
||||
async getContentElement() {
|
||||
return new Promise(resolve => appContext.triggerCommand('executeWithContentElement', {
|
||||
resolve,
|
||||
ntxId: this.ntxId
|
||||
}));
|
||||
}
|
||||
|
||||
async getTypeWidget() {
|
||||
return new Promise(resolve => appContext.triggerCommand('executeWithTypeWidget', {
|
||||
resolve,
|
||||
ntxId: this.ntxId
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteContext;
|
||||
|
||||
@@ -24,8 +24,8 @@ async function createNote(parentNotePath, options = {}) {
|
||||
options.saveSelection = false;
|
||||
}
|
||||
|
||||
if (options.saveSelection && utils.isCKEditorInitialized()) {
|
||||
[options.title, options.content] = parseSelectedHtml(window.cutToNote.getSelectedHtml());
|
||||
if (options.saveSelection) {
|
||||
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
|
||||
}
|
||||
|
||||
const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath);
|
||||
@@ -43,12 +43,13 @@ async function createNote(parentNotePath, options = {}) {
|
||||
content: options.content || "",
|
||||
isProtected: options.isProtected,
|
||||
type: options.type,
|
||||
mime: options.mime
|
||||
mime: options.mime,
|
||||
templateNoteId: options.templateNoteId
|
||||
});
|
||||
|
||||
if (options.saveSelection && utils.isCKEditorInitialized()) {
|
||||
if (options.saveSelection) {
|
||||
// we remove the selection only after it was saved to server to make sure we don't lose anything
|
||||
window.cutToNote.removeSelection();
|
||||
options.textEditor.removeSelection();
|
||||
}
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
@@ -74,6 +75,25 @@ async function createNote(parentNotePath, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function chooseNoteType() {
|
||||
return new Promise(res => {
|
||||
appContext.triggerCommand("chooseNoteType", {callback: res});
|
||||
});
|
||||
}
|
||||
|
||||
async function createNoteWithTypePrompt(parentNotePath, options = {}) {
|
||||
const {success, noteType, templateNoteId} = await chooseNoteType();
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.type = noteType;
|
||||
options.templateNoteId = templateNoteId;
|
||||
|
||||
return await createNote(parentNotePath, options);
|
||||
}
|
||||
|
||||
/* If first element is heading, parse it out and use it as a new heading. */
|
||||
function parseSelectedHtml(selectedHtml) {
|
||||
const dom = $.parseHTML(selectedHtml);
|
||||
@@ -105,5 +125,7 @@ async function duplicateSubtree(noteId, parentNotePath) {
|
||||
|
||||
export default {
|
||||
createNote,
|
||||
duplicateSubtree
|
||||
createNoteWithTypePrompt,
|
||||
duplicateSubtree,
|
||||
chooseNoteType
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import linkService from "./link.js";
|
||||
import noteContentRenderer from "./note_content_renderer.js";
|
||||
import froca from "./froca.js";
|
||||
import attributeRenderer from "./attribute_renderer.js";
|
||||
import libraryLoader from "./library_loader.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-list">
|
||||
@@ -60,27 +61,27 @@ const TPL = `
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.note-book-title {
|
||||
.note-book-header {
|
||||
margin-bottom: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* not-expanded title is limited to one line only */
|
||||
.note-book-card:not(.expanded) .note-book-title {
|
||||
.note-book-card:not(.expanded) .note-book-header {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.note-book-title .rendered-note-attributes {
|
||||
.note-book-header .rendered-note-attributes {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.note-book-title .rendered-note-attributes:before {
|
||||
.note-book-header .rendered-note-attributes:before {
|
||||
content: "\\00a0\\00a0";
|
||||
}
|
||||
|
||||
.note-book-title .note-icon {
|
||||
.note-book-header .note-icon {
|
||||
font-size: 100%;
|
||||
display: inline-block;
|
||||
padding-right: 7px;
|
||||
@@ -112,7 +113,7 @@ const TPL = `
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.note-book-title {
|
||||
.note-book-header {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
@@ -198,6 +199,15 @@ class NoteListRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightedTokens = this.parentNote.highlightedTokens || [];
|
||||
if (highlightedTokens.length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
|
||||
|
||||
this.highlightRegex = new RegExp(highlightedTokens.join("|"), 'gi');
|
||||
} else {
|
||||
this.highlightRegex = null;
|
||||
}
|
||||
|
||||
this.$noteList.show();
|
||||
|
||||
const $container = this.$noteList.find('.note-list-container').empty();
|
||||
@@ -262,12 +272,13 @@ class NoteListRenderer {
|
||||
const $card = $('<div class="note-book-card">')
|
||||
.attr('data-note-id', note.noteId)
|
||||
.append(
|
||||
$('<h5 class="note-book-title">')
|
||||
$('<h5 class="note-book-header">')
|
||||
.append($expander)
|
||||
.append($('<span class="note-icon">').addClass(note.getIcon()))
|
||||
.append(this.viewType === 'grid'
|
||||
? note.title
|
||||
: await linkService.createNoteLink(notePath, {showTooltip: false, showNotePath: this.showNotePath})
|
||||
? $('<span class="note-book-title">').text(note.title)
|
||||
: (await linkService.createNoteLink(notePath, {showTooltip: false, showNotePath: this.showNotePath}))
|
||||
.addClass("note-book-title")
|
||||
)
|
||||
.append($renderedAttributes)
|
||||
);
|
||||
@@ -281,6 +292,15 @@ class NoteListRenderer {
|
||||
|
||||
$expander.on('click', () => this.toggleContent($card, note, !$card.hasClass("expanded")));
|
||||
|
||||
if (this.highlightRegex) {
|
||||
$card.find(".note-book-title").markRegExp(this.highlightRegex, {
|
||||
element: "span",
|
||||
className: "ck-find-result",
|
||||
separateWordSearch: false,
|
||||
caseSensitive: false
|
||||
});
|
||||
}
|
||||
|
||||
await this.toggleContent($card, note, expand);
|
||||
|
||||
return $card;
|
||||
@@ -291,7 +311,7 @@ class NoteListRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
const $expander = $card.find('> .note-book-title .note-expander');
|
||||
const $expander = $card.find('> .note-book-header .note-expander');
|
||||
|
||||
if (expand || this.viewType === 'grid') {
|
||||
$card.addClass("expanded");
|
||||
@@ -315,6 +335,15 @@ class NoteListRenderer {
|
||||
trim: this.viewType === 'grid' // for grid only short content is needed
|
||||
});
|
||||
|
||||
if (this.highlightRegex) {
|
||||
$renderedContent.markRegExp(this.highlightRegex, {
|
||||
element: "span",
|
||||
className: "ck-find-result",
|
||||
separateWordSearch: false,
|
||||
caseSensitive: false
|
||||
});
|
||||
}
|
||||
|
||||
$content.append($renderedContent);
|
||||
$content.addClass("type-" + type);
|
||||
} catch (e) {
|
||||
|
||||
@@ -22,8 +22,7 @@ async function mouseEnterHandler() {
|
||||
const $link = $(this);
|
||||
|
||||
if ($link.hasClass("no-tooltip-preview")
|
||||
|| $link.hasClass("disabled")
|
||||
|| $link.attr("data-action") === 'note-revision') {
|
||||
|| $link.hasClass("disabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,10 +59,11 @@ async function mouseEnterHandler() {
|
||||
$(this).tooltip({
|
||||
delay: {"show": 300, "hide": 100},
|
||||
container: 'body',
|
||||
placement: 'auto',
|
||||
// https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988
|
||||
// with bottom this flickering happens a bit less
|
||||
placement: 'bottom',
|
||||
trigger: 'manual',
|
||||
boundary: 'window',
|
||||
offset: "0, 20", // workaround for https://github.com/zadam/trilium/issues/2794
|
||||
title: html,
|
||||
html: true,
|
||||
template: '<div class="tooltip note-tooltip" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
|
||||
|
||||
40
src/public/app/services/note_types.js
Normal file
40
src/public/app/services/note_types.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
|
||||
async function getNoteTypeItems(command) {
|
||||
const items = [
|
||||
{ title: "Text", command: command, type: "text", uiIcon: "bx bx-note" },
|
||||
{ title: "Code", command: command, type: "code", uiIcon: "bx bx-code" },
|
||||
{ title: "Saved Search", command: command, type: "search", uiIcon: "bx bx-file-find" },
|
||||
{ title: "Relation Map", command: command, type: "relation-map", uiIcon: "bx bx-map-alt" },
|
||||
{ title: "Note Map", command: command, type: "note-map", uiIcon: "bx bx-map-alt" },
|
||||
{ title: "Render Note", command: command, type: "render", uiIcon: "bx bx-extension" },
|
||||
{ title: "Book", command: command, type: "book", uiIcon: "bx bx-book" },
|
||||
{ title: "Mermaid Diagram", command: command, type: "mermaid", uiIcon: "bx bx-selection" },
|
||||
{ title: "Canvas", command: command, type: "canvas", uiIcon: "bx bx-pen" },
|
||||
{ title: "Web View", command: command, type: "web-view", uiIcon: "bx bx-globe-alt" },
|
||||
];
|
||||
|
||||
const templateNoteIds = await server.get("search-templates");
|
||||
const templateNotes = await froca.getNotes(templateNoteIds);
|
||||
|
||||
if (templateNotes.length > 0) {
|
||||
items.push({ title: "----" });
|
||||
|
||||
for (const templateNote of templateNotes) {
|
||||
items.push({
|
||||
title: templateNote.title,
|
||||
uiIcon: templateNote.getIcon(),
|
||||
command: command,
|
||||
type: templateNote.type,
|
||||
templateNoteId: templateNote.noteId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export default {
|
||||
getNoteTypeItems
|
||||
}
|
||||
@@ -20,7 +20,7 @@ function enterProtectedSession() {
|
||||
const dfd = $.Deferred();
|
||||
|
||||
if (!options.is("isPasswordSet")) {
|
||||
import("../dialogs/password_not_set.js").then(dialog => dialog.show());
|
||||
appContext.triggerCommand("showPasswordNotSet");
|
||||
return dfd;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ function enterProtectedSession() {
|
||||
// using deferred instead of promise because it allows resolving from outside
|
||||
protectedSessionDeferred = dfd;
|
||||
|
||||
import("../dialogs/protected_session.js").then(dialog => dialog.show());
|
||||
appContext.triggerCommand("showProtectedSessionPasswordDialog");
|
||||
}
|
||||
|
||||
return dfd.promise();
|
||||
@@ -61,13 +61,13 @@ ws.subscribeToMessages(async message => {
|
||||
if (message.type === 'protectedSessionLogin') {
|
||||
await reloadData();
|
||||
|
||||
await appContext.triggerEvent('frocaReloaded');
|
||||
await appContext.triggerEvent('frocaReloaded');
|
||||
|
||||
appContext.triggerEvent('protectedSessionStarted');
|
||||
|
||||
if (protectedSessionDeferred !== null) {
|
||||
import("../dialogs/protected_session.js").then(dialog => dialog.close());
|
||||
appContext.triggerCommand("closeProtectedSessionPasswordDialog");
|
||||
|
||||
if (protectedSessionDeferred !== null) {
|
||||
protectedSessionDeferred.resolve(true);
|
||||
protectedSessionDeferred = null;
|
||||
}
|
||||
|
||||
@@ -8,35 +8,6 @@ import options from "./options.js";
|
||||
import froca from "./froca.js";
|
||||
|
||||
export default class RootCommandExecutor extends Component {
|
||||
jumpToNoteCommand() {
|
||||
import("../dialogs/jump_to_note.js").then(d => d.showDialog());
|
||||
}
|
||||
|
||||
showRecentChangesCommand() {
|
||||
import("../dialogs/recent_changes.js").then(d => d.showDialog());
|
||||
}
|
||||
|
||||
showNoteRevisionsCommand() {
|
||||
import("../dialogs/note_revisions.js").then(d => d.showCurrentNoteRevisions());
|
||||
}
|
||||
|
||||
showNoteSourceCommand() {
|
||||
import("../dialogs/note_source.js").then(d => d.showDialog());
|
||||
}
|
||||
|
||||
pasteMarkdownIntoTextCommand() {
|
||||
import("../dialogs/markdown_import.js").then(d => d.importMarkdownInline());
|
||||
}
|
||||
|
||||
async editBranchPrefixCommand() {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
|
||||
if (notePath) {
|
||||
const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js");
|
||||
editBranchPrefixDialog.showDialog(notePath);
|
||||
}
|
||||
}
|
||||
|
||||
editReadOnlyNoteCommand() {
|
||||
const noteContext = appContext.tabManager.getActiveContext();
|
||||
noteContext.readOnlyTemporarilyDisabled = true;
|
||||
@@ -44,24 +15,6 @@ export default class RootCommandExecutor extends Component {
|
||||
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext });
|
||||
}
|
||||
|
||||
async cloneNoteIdsToCommand({noteIds}) {
|
||||
const d = await import("../dialogs/clone_to.js");
|
||||
d.showDialog(noteIds);
|
||||
}
|
||||
|
||||
async moveBranchIdsToCommand({branchIds}) {
|
||||
const d = await import("../dialogs/move_to.js");
|
||||
d.showDialog(branchIds);
|
||||
}
|
||||
|
||||
showOptionsCommand({openTab}) {
|
||||
import("../dialogs/options.js").then(d => d.showDialog(openTab));
|
||||
}
|
||||
|
||||
showHelpCommand() {
|
||||
import("../dialogs/help.js").then(d => d.showDialog());
|
||||
}
|
||||
|
||||
async showSQLConsoleCommand() {
|
||||
const sqlConsoleNote = await dateNoteService.createSqlConsole();
|
||||
|
||||
@@ -90,10 +43,6 @@ export default class RootCommandExecutor extends Component {
|
||||
this.searchNotesCommand({ancestorNoteId: noteId});
|
||||
}
|
||||
|
||||
showBackendLogCommand() {
|
||||
import("../dialogs/backend_log.js").then(d => d.showDialog());
|
||||
}
|
||||
|
||||
openNoteExternallyCommand() {
|
||||
const noteId = appContext.tabManager.getActiveContextNoteId();
|
||||
|
||||
|
||||
@@ -131,10 +131,10 @@ function ajax(url, method, data, headers) {
|
||||
headers: respHeaders
|
||||
});
|
||||
},
|
||||
error: async (jqXhr, status, error) => {
|
||||
await reportError(method, url, status, error);
|
||||
error: async (jqXhr, status) => {
|
||||
await reportError(method, url, status, jqXhr.responseText);
|
||||
|
||||
rej(error);
|
||||
rej(jqXhr.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -142,6 +142,11 @@ export default class TabManager extends Component {
|
||||
return this.noteContexts;
|
||||
}
|
||||
|
||||
/** @returns {NoteContext[]} */
|
||||
getMainNoteContexts() {
|
||||
return this.noteContexts.filter(nc => nc.isMainContext());
|
||||
}
|
||||
|
||||
/** @returns {NoteContext} */
|
||||
getNoteContextById(ntxId) {
|
||||
const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId);
|
||||
@@ -294,11 +299,23 @@ export default class TabManager extends Component {
|
||||
this.setCurrentNotePathToHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ntxId
|
||||
* @returns {Promise<boolean>} true if note context has been removed, false otherwise
|
||||
*/
|
||||
async removeNoteContext(ntxId) {
|
||||
// removing note context is async process which can take some time, if users presses CTRL-W quickly, two
|
||||
// close events could interleave which would then lead to attempting to activate already removed context.
|
||||
await this.mutex.runExclusively(async () => {
|
||||
const noteContextToRemove = this.getNoteContextById(ntxId);
|
||||
return await this.mutex.runExclusively(async () => {
|
||||
let noteContextToRemove;
|
||||
|
||||
try {
|
||||
noteContextToRemove = this.getNoteContextById(ntxId);
|
||||
}
|
||||
catch {
|
||||
// note context not found
|
||||
return false;
|
||||
}
|
||||
|
||||
if (noteContextToRemove.isMainContext()) {
|
||||
// forbid removing last main note context
|
||||
@@ -306,8 +323,9 @@ export default class TabManager extends Component {
|
||||
const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext());
|
||||
|
||||
if (mainNoteContexts.length === 1) {
|
||||
mainNoteContexts[0].setEmpty();
|
||||
return;
|
||||
await this.clearLastMainNoteContext(noteContextToRemove);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +335,7 @@ export default class TabManager extends Component {
|
||||
const noteContextsToRemove = noteContextToRemove.getSubContexts();
|
||||
const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId);
|
||||
|
||||
await this.triggerEvent('beforeTabRemove', { ntxIds: ntxIdsToRemove });
|
||||
await this.triggerEvent('beforeNoteContextRemove', { ntxIds: ntxIdsToRemove });
|
||||
|
||||
if (!noteContextToRemove.isMainContext()) {
|
||||
await this.activateNoteContext(noteContextToRemove.getMainContext().ntxId);
|
||||
@@ -336,16 +354,41 @@ export default class TabManager extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId));
|
||||
this.removeNoteContexts(noteContextsToRemove);
|
||||
|
||||
this.recentlyClosedTabs.push(noteContextsToRemove);
|
||||
|
||||
this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove});
|
||||
|
||||
this.tabsUpdate.scheduleUpdate();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async clearLastMainNoteContext(noteContextToClear) {
|
||||
noteContextToClear.setEmpty();
|
||||
|
||||
// activate main split
|
||||
await this.activateNoteContext(noteContextToClear.ntxId);
|
||||
|
||||
// remove all other splits
|
||||
const noteContextsToRemove = noteContextToClear.getSubContexts()
|
||||
.filter(ntx => ntx.ntxId !== noteContextToClear.ntxId);
|
||||
|
||||
const ntxIdsToRemove = noteContextsToRemove.map(ntx => ntx.ntxId);
|
||||
|
||||
await this.triggerEvent('beforeNoteContextRemove', {ntxIds: ntxIdsToRemove});
|
||||
|
||||
this.removeNoteContexts(noteContextsToRemove);
|
||||
}
|
||||
|
||||
removeNoteContexts(noteContextsToRemove) {
|
||||
const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId);
|
||||
|
||||
this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId));
|
||||
|
||||
this.recentlyClosedTabs.push(noteContextsToRemove);
|
||||
|
||||
this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove});
|
||||
|
||||
this.tabsUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
tabReorderEvent({ntxIdsInOrder}) {
|
||||
const order = {};
|
||||
|
||||
@@ -421,12 +464,14 @@ export default class TabManager extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
moveTabToNewWindowCommand({ntxId}) {
|
||||
async moveTabToNewWindowCommand({ntxId}) {
|
||||
const {notePath, hoistedNoteId} = this.getNoteContextById(ntxId);
|
||||
|
||||
this.removeNoteContext(ntxId);
|
||||
const removed = await this.removeNoteContext(ntxId);
|
||||
|
||||
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
|
||||
if (removed) {
|
||||
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
|
||||
}
|
||||
}
|
||||
|
||||
async reopenLastTabCommand() {
|
||||
@@ -458,7 +503,7 @@ export default class TabManager extends Component {
|
||||
|
||||
updateDocumentTitle(activeNoteContext) {
|
||||
const titleFragments = [
|
||||
// it helps navigating in history if note title is included in the title
|
||||
// it helps to navigate in history if note title is included in the title
|
||||
activeNoteContext.note?.title,
|
||||
"Trilium Notes"
|
||||
].filter(Boolean);
|
||||
|
||||
@@ -4,16 +4,17 @@ import utils from "./utils.js";
|
||||
function toast(options) {
|
||||
const $toast = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="mr-auto"><span class="bx bx-${options.icon}"></span> ${options.title}</strong>
|
||||
<strong class="mr-auto"><span class="bx bx-${options.icon}"></span> <span class="toast-title"></span></strong>
|
||||
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
${options.message}
|
||||
</div>
|
||||
<div class="toast-body"></div>
|
||||
</div>`);
|
||||
|
||||
$toast.find('.toast-title').text(options.title);
|
||||
$toast.find('.toast-body').text(options.message);
|
||||
|
||||
if (options.id) {
|
||||
$toast.attr("id", "toast-" + options.id);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import clipboard from './clipboard.js';
|
||||
import noteCreateService from "./note_create.js";
|
||||
import contextMenu from "./context_menu.js";
|
||||
import appContext from "./app_context.js";
|
||||
import noteTypesService from "./note_types.js";
|
||||
|
||||
class TreeContextMenu {
|
||||
/**
|
||||
@@ -24,20 +25,6 @@ class TreeContextMenu {
|
||||
})
|
||||
}
|
||||
|
||||
getNoteTypeItems(command) {
|
||||
return [
|
||||
{ title: "Text", command: command, type: "text", uiIcon: "note" },
|
||||
{ title: "Code", command: command, type: "code", uiIcon: "code" },
|
||||
{ title: "Saved search", command: command, type: "search", uiIcon: "file-find" },
|
||||
{ title: "Relation Map", command: command, type: "relation-map", uiIcon: "map-alt" },
|
||||
{ title: "Note Map", command: command, type: "note-map", uiIcon: "map-alt" },
|
||||
{ title: "Render HTML note", command: command, type: "render", uiIcon: "extension" },
|
||||
{ title: "Book", command: command, type: "book", uiIcon: "book" },
|
||||
{ title: "Mermaid diagram", command: command, type: "mermaid", uiIcon: "selection" },
|
||||
{ title: "Canvas", command: command, type: "canvas", uiIcon: "pen" },
|
||||
];
|
||||
}
|
||||
|
||||
async getMenuItems() {
|
||||
const note = await froca.getNote(this.node.data.noteId);
|
||||
const branch = froca.getBranch(this.node.data.branchId);
|
||||
@@ -57,58 +44,59 @@ class TreeContextMenu {
|
||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
||||
|
||||
return [
|
||||
{ title: 'Open in a new tab <kbd>Ctrl+Click</kbd>', command: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
|
||||
{ title: 'Open in a new split', command: "openNoteInSplit", uiIcon: "dock-right", enabled: noSelectedNotes },
|
||||
{ title: 'Insert note after <kbd data-command="createNoteAfter"></kbd>', command: "insertNoteAfter", uiIcon: "plus",
|
||||
items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null,
|
||||
{ title: 'Open in a new tab <kbd>Ctrl+Click</kbd>', command: "openInTab", uiIcon: "bx bx-empty", enabled: noSelectedNotes },
|
||||
{ title: 'Open in a new split', command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
|
||||
{ title: 'Insert note after <kbd data-command="createNoteAfter"></kbd>', command: "insertNoteAfter", uiIcon: "bx bx-plus",
|
||||
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
|
||||
enabled: insertNoteAfterEnabled && noSelectedNotes },
|
||||
{ title: 'Insert child note <kbd data-command="createNoteInto"></kbd>', command: "insertChildNote", uiIcon: "plus",
|
||||
items: notSearch ? this.getNoteTypeItems("insertChildNote") : null,
|
||||
{ title: 'Insert child note <kbd data-command="createNoteInto"></kbd>', command: "insertChildNote", uiIcon: "bx bx-plus",
|
||||
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
{ title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "trash",
|
||||
{ title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "bx bx-trash",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
||||
{ title: "----" },
|
||||
{ title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "search",
|
||||
{ title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "bx bx-search",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
||||
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "door-open" },
|
||||
{ title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "empty",
|
||||
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
|
||||
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||
{ title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "bx bx-empty",
|
||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
|
||||
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
|
||||
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes },
|
||||
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes },
|
||||
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
|
||||
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
||||
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes }
|
||||
{ title: "Advanced", uiIcon: "bx bx-empty", enabled: true, items: [
|
||||
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "bx bx-refresh", enabled: noSelectedNotes },
|
||||
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
|
||||
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes }
|
||||
] },
|
||||
{ title: "----" },
|
||||
{ title: "Protect subtree", command: "protectSubtree", uiIcon: "check-shield", enabled: noSelectedNotes },
|
||||
{ title: "Unprotect subtree", command: "unprotectSubtree", uiIcon: "shield", enabled: noSelectedNotes },
|
||||
{ title: "Protect subtree", command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
|
||||
{ title: "Unprotect subtree", command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
|
||||
{ title: "----" },
|
||||
{ title: 'Copy / clone <kbd data-command="copyNotesToClipboard"></kbd>', command: "copyNotesToClipboard", uiIcon: "copy",
|
||||
{ title: 'Copy / clone <kbd data-command="copyNotesToClipboard"></kbd>', command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
|
||||
enabled: isNotRoot && !isHoisted },
|
||||
{ title: 'Clone to ... <kbd data-command="cloneNotesTo"></kbd>', command: "cloneNotesTo", uiIcon: "empty",
|
||||
{ title: 'Clone to ... <kbd data-command="cloneNotesTo"></kbd>', command: "cloneNotesTo", uiIcon: "bx bx-empty",
|
||||
enabled: isNotRoot && !isHoisted },
|
||||
{ title: 'Cut <kbd data-command="cutNotesToClipboard"></kbd>', command: "cutNotesToClipboard", uiIcon: "cut",
|
||||
{ title: 'Cut <kbd data-command="cutNotesToClipboard"></kbd>', command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
||||
{ title: 'Move to ... <kbd data-command="moveNotesTo"></kbd>', command: "moveNotesTo", uiIcon: "empty",
|
||||
{ title: 'Move to ... <kbd data-command="moveNotesTo"></kbd>', command: "moveNotesTo", uiIcon: "bx bx-empty",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
||||
{ title: 'Paste into <kbd data-command="pasteNotesFromClipboard"></kbd>', command: "pasteNotesFromClipboard", uiIcon: "paste",
|
||||
{ title: 'Paste into <kbd data-command="pasteNotesFromClipboard"></kbd>', command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
|
||||
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
|
||||
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "paste",
|
||||
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
|
||||
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
|
||||
{ title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "empty",
|
||||
{ title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
|
||||
enabled: parentNotSearch && isNotRoot && !isHoisted },
|
||||
{ title: "----" },
|
||||
{ title: "Export", command: "exportNote", uiIcon: "empty",
|
||||
{ title: "Export", command: "exportNote", uiIcon: "bx bx-empty",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
{ title: "Import into note", command: "importIntoNote", uiIcon: "empty",
|
||||
enabled: notSearch && noSelectedNotes }
|
||||
{ title: "Import into note", command: "importIntoNote", uiIcon: "bx bx-empty",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
{ title: "Apply bulk actions", command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus",
|
||||
enabled: true }
|
||||
].filter(row => row !== null);
|
||||
}
|
||||
|
||||
async selectMenuItemHandler({command, type}) {
|
||||
const noteId = this.node.data.noteId;
|
||||
async selectMenuItemHandler({command, type, templateNoteId}) {
|
||||
const notePath = treeService.getNotePath(this.node);
|
||||
|
||||
if (command === 'openInTab') {
|
||||
@@ -122,7 +110,8 @@ class TreeContextMenu {
|
||||
target: 'after',
|
||||
targetBranchId: this.node.data.branchId,
|
||||
type: type,
|
||||
isProtected: isProtected
|
||||
isProtected: isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
});
|
||||
}
|
||||
else if (command === "insertChildNote") {
|
||||
@@ -130,7 +119,8 @@ class TreeContextMenu {
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
type: type,
|
||||
isProtected: this.node.data.isProtected
|
||||
isProtected: this.node.data.isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
});
|
||||
}
|
||||
else if (command === 'openNoteInSplit') {
|
||||
@@ -140,7 +130,12 @@ class TreeContextMenu {
|
||||
this.treeWidget.triggerCommand("openNewNoteSplit", {ntxId, notePath});
|
||||
}
|
||||
else {
|
||||
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
|
||||
this.treeWidget.triggerCommand(command, {
|
||||
node: this.node,
|
||||
notePath: notePath,
|
||||
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
|
||||
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ function isHtmlEmpty(html) {
|
||||
|
||||
async function clearBrowserCache() {
|
||||
if (isElectron()) {
|
||||
const win = utils.dynamicRequire('@electron/remote').getCurrentWindow();
|
||||
const win = dynamicRequire('@electron/remote').getCurrentWindow();
|
||||
await win.webContents.session.clearCache();
|
||||
}
|
||||
}
|
||||
@@ -292,10 +292,6 @@ function copySelectionToClipboard() {
|
||||
}
|
||||
}
|
||||
|
||||
function isCKEditorInitialized() {
|
||||
return !!(window && window.cutToNote);
|
||||
}
|
||||
|
||||
function dynamicRequire(moduleName) {
|
||||
if (typeof __non_webpack_require__ !== 'undefined') {
|
||||
return __non_webpack_require__(moduleName);
|
||||
@@ -365,6 +361,10 @@ function sleep(time_ms) {
|
||||
});
|
||||
}
|
||||
|
||||
function escapeRegExp(str) {
|
||||
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadFrontendApp,
|
||||
parseDate,
|
||||
@@ -401,7 +401,6 @@ export default {
|
||||
clearBrowserCache,
|
||||
normalizeShortcut,
|
||||
copySelectionToClipboard,
|
||||
isCKEditorInitialized,
|
||||
dynamicRequire,
|
||||
timeLimit,
|
||||
initHelpDropdown,
|
||||
@@ -410,4 +409,5 @@ export default {
|
||||
filterAttributeName,
|
||||
isValidAttributeName,
|
||||
sleep,
|
||||
escapeRegExp
|
||||
};
|
||||
|
||||
@@ -175,7 +175,7 @@ async function consumeFrontendUpdateData() {
|
||||
else {
|
||||
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
|
||||
|
||||
alert(`Encountered error "${e.message}", check out the console.`);
|
||||
toastService.showError(`Encountered error "${e.message}", check out the console.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,5 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggleMenuButton = document.getElementById('toggleMenuButton');
|
||||
const layout = document.getElementById('layout');
|
||||
|
||||
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
|
||||
if (toggleMenuButton && layout) {
|
||||
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
|
||||
}
|
||||
}, false);
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class Mutex {
|
||||
const unlock = await this.lock();
|
||||
|
||||
try {
|
||||
await cb();
|
||||
return await cb();
|
||||
}
|
||||
finally {
|
||||
unlock();
|
||||
|
||||
@@ -204,6 +204,7 @@ const ATTR_HELP = {
|
||||
"workspace": "marks this note as a workspace which allows easy hoisting",
|
||||
"workspaceIconClass": "defines box icon CSS class which will be used in tab when hoisted to this note",
|
||||
"workspaceTabBackgroundColor": "CSS color used in the note tab when hoisted to this note",
|
||||
"workspaceCalendarRoot": "Defines per-workspace calendar root",
|
||||
"searchHome": "new search notes will be created as children of this note",
|
||||
"hoistedSearchHome": "new search notes will be created as children of this note when hoisted to some ancestor of this note",
|
||||
"inbox": "default inbox location for new notes",
|
||||
@@ -215,8 +216,10 @@ const ATTR_HELP = {
|
||||
"shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]",
|
||||
"shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.",
|
||||
"shareRoot": "marks note which is served on /share root.",
|
||||
"shareDescription": "define text to be added to the HTML meta tag for description",
|
||||
"shareRaw": "note will be served in its raw format, without HTML wrapper",
|
||||
"shareDisallowRobotIndexing": `will forbid robot indexing of this note via <code>X-Robots-Tag: noindex</code> header`,
|
||||
"shareCredentials": "require credentials to access this shared note. Value is expected to be in format 'username:password'. Don't forget to make this inheritable to apply to child-notes/images.",
|
||||
"displayRelations": "comma delimited names of relations which should be displayed. All other ones will be hidden.",
|
||||
"hideRelations": "comma delimited names of relations which should be hidden. All other ones will be displayed.",
|
||||
"titleTemplate": `default title of notes created as children of this note. The value is evaluated as JavaScript string
|
||||
@@ -227,12 +230,17 @@ const ATTR_HELP = {
|
||||
<li><code>Log for \${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>
|
||||
</ul>
|
||||
|
||||
See <a href="https://github.com/zadam/trilium/wiki/Default-note-title">wiki with details</a>, API docs for <a href="https://zadam.github.io/trilium/backend_api/Note.html">parentNote</a> and <a href="https://day.js.org/docs/en/display/format">now</a> for details.`
|
||||
See <a href="https://github.com/zadam/trilium/wiki/Default-note-title">wiki with details</a>, API docs for <a href="https://zadam.github.io/trilium/backend_api/Note.html">parentNote</a> and <a href="https://day.js.org/docs/en/display/format">now</a> for details.`,
|
||||
"template": "This note will appear in the selection of available template when creating new note",
|
||||
"toc": "<code>#toc</code> or <code>#toc=show</code> will force the Table of Contents to be shown, <code>#toc=hide</code> will force hiding it. If the label doesn't exist, the global setting is observed"
|
||||
},
|
||||
"relation": {
|
||||
"runOnNoteCreation": "executes when note is created on backend",
|
||||
"runOnNoteTitleChange": "executes when note title is changed (includes note creation as well)",
|
||||
"runOnNoteChange": "executes when note is changed (includes note creation as well)",
|
||||
"runOnNoteDeletion": "executes when note is being deleted",
|
||||
"runOnBranchCreation": "executes when a branch is created. Branch is a link between parent note and child note and is created e.g. when cloning or moving note.",
|
||||
"runOnBranchDeletion": "executes when a branch is deleted. Branch is a link between parent note and child note and is deleted e.g. when moving note (old branch/link is deleted).",
|
||||
"runOnChildNoteCreation": "executes when new note is created under this note",
|
||||
"runOnAttributeCreation": "executes when new attribute is created under this note",
|
||||
"runOnAttributeChange": "executes when attribute is changed under this note",
|
||||
|
||||
@@ -217,11 +217,11 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
y: e.pageY,
|
||||
orientation: 'left',
|
||||
items: [
|
||||
{title: `Add new label <kbd data-command="addNewLabel"></kbd>`, command: "addNewLabel", uiIcon: "hash"},
|
||||
{title: `Add new relation <kbd data-command="addNewRelation"></kbd>`, command: "addNewRelation", uiIcon: "transfer"},
|
||||
{title: `Add new label <kbd data-command="addNewLabel"></kbd>`, command: "addNewLabel", uiIcon: "bx bx-hash"},
|
||||
{title: `Add new relation <kbd data-command="addNewRelation"></kbd>`, command: "addNewRelation", uiIcon: "bx bx-transfer"},
|
||||
{title: "----"},
|
||||
{title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "empty"},
|
||||
{title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "empty"},
|
||||
{title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "bx bx-empty"},
|
||||
{title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "bx bx-empty"},
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => this.handleAddNewAttributeCommand(command)
|
||||
});
|
||||
@@ -297,6 +297,12 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async save() {
|
||||
if (this.lastUpdatedNoteId !== this.noteId) {
|
||||
// https://github.com/zadam/trilium/issues/3090
|
||||
console.warn("Ignoring blur event because a different note is loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
const attributes = this.parseAttributes();
|
||||
|
||||
if (attributes) {
|
||||
@@ -354,6 +360,8 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
dataChanged() {
|
||||
this.lastUpdatedNoteId = this.noteId;
|
||||
|
||||
if (this.lastSavedContent === this.textEditor.getData()) {
|
||||
this.$saveAttributesButton.fadeOut();
|
||||
}
|
||||
@@ -468,6 +476,8 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
|
||||
async renderOwnedAttributes(ownedAttributes, saved) {
|
||||
ownedAttributes = ownedAttributes.filter(oa => !oa.isDeleted);
|
||||
// attrs are not resorted if position changes after initial load
|
||||
ownedAttributes.sort((a, b) => a.position < b.position ? -1 : 1);
|
||||
|
||||
let htmlAttrs = (await attributeRenderer.renderAttributes(ownedAttributes, true)).html();
|
||||
|
||||
@@ -485,7 +495,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async createNoteForReferenceLink(title) {
|
||||
const {note} = await noteCreateService.createNote(this.notePath, {
|
||||
const {note} = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
|
||||
activate: false,
|
||||
title: title
|
||||
});
|
||||
|
||||
@@ -103,10 +103,22 @@ class BasicWidget extends Component {
|
||||
this.$widget.toggleClass('hidden-int', !show);
|
||||
}
|
||||
|
||||
isHiddenInt() {
|
||||
return this.$widget.hasClass('hidden-int');
|
||||
}
|
||||
|
||||
toggleExt(show) {
|
||||
this.$widget.toggleClass('hidden-ext', !show);
|
||||
}
|
||||
|
||||
isHiddenExt() {
|
||||
return this.$widget.hasClass('hidden-ext');
|
||||
}
|
||||
|
||||
canBeShown() {
|
||||
return !this.isHiddenInt() && !this.isHiddenExt();
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
return this.$widget.is(":visible");
|
||||
}
|
||||
@@ -121,7 +133,7 @@ class BasicWidget extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getNtxId() {
|
||||
getClosestNtxId() {
|
||||
if (this.$widget) {
|
||||
return this.$widget.closest("[data-ntx-id]").attr("data-ntx-id");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import server from "../../services/server.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import Component from "../component.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
export default class AbstractSearchAction extends Component {
|
||||
export default class AbstractBulkAction {
|
||||
constructor(attribute, actionDef) {
|
||||
super();
|
||||
|
||||
this.attribute = attribute;
|
||||
this.actionDef = actionDef;
|
||||
}
|
||||
@@ -50,6 +47,6 @@ export default class AbstractSearchAction extends Component {
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
await this.triggerCommand('refreshSearchDefinition');
|
||||
//await this.triggerCommand('refreshSearchDefinition');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import AbstractBulkAction from "./abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@@ -33,8 +33,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class ExecuteScriptSearchAction extends AbstractSearchAction {
|
||||
export default class ExecuteScriptBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "executeScript"; }
|
||||
static get actionTitle() { return "Execute script"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
@@ -43,7 +44,7 @@ export default class ExecuteScriptSearchAction extends AbstractSearchAction {
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.saveAction({ script: $script.val() });
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
|
||||
$script.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Set label</div>
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Add label</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control label-name"
|
||||
@@ -37,8 +37,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class SetLabelValueSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "setLabelValue"; }
|
||||
export default class AddLabelBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "addLabel"; }
|
||||
static get actionTitle() { return "Add label"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
@@ -54,7 +55,7 @@ export default class SetLabelValueSearchAction extends AbstractSearchAction {
|
||||
labelName: $labelName.val(),
|
||||
labelValue: $labelValue.val()
|
||||
});
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
|
||||
$labelName.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
$labelValue.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
@@ -1,5 +1,5 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@@ -18,8 +18,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class DeleteLabelSearchAction extends AbstractSearchAction {
|
||||
export default class DeleteLabelBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "deleteLabel"; }
|
||||
static get actionTitle() { return "Delete label"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
@@ -1,11 +1,11 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;">Rename label from:</div>
|
||||
<div style="margin-right: 10px; flex-shrink: 0;">Rename label from:</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control old-label-name"
|
||||
@@ -27,8 +27,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class RenameLabelSearchAction extends AbstractSearchAction {
|
||||
export default class RenameLabelBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "renameLabel"; }
|
||||
static get actionTitle() { return "Rename label"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
@@ -44,7 +45,7 @@ export default class RenameLabelSearchAction extends AbstractSearchAction {
|
||||
oldLabelName: $oldLabelName.val(),
|
||||
newLabelName: $newLabelName.val()
|
||||
});
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
|
||||
$oldLabelName.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
$newLabelName.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
@@ -0,0 +1,60 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Update label value</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control label-name"
|
||||
placeholder="label name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
|
||||
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to value</div>
|
||||
|
||||
<input type="text" class="form-control label-value" placeholder="new value"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="button-column">
|
||||
<div class="dropdown help-dropdown">
|
||||
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu dropdown-menu-right p-4">
|
||||
<p>On all matched notes, change value of the existing label.</p>
|
||||
|
||||
<p>You can also call this method without value, in such case label will be assigned to the note without value.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="bx bx-x icon-action action-conf-del"></span>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class UpdateLabelValueBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "updateLabelValue"; }
|
||||
static get actionTitle() { return "Update label value"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
||||
const $labelName = $action.find('.label-name');
|
||||
$labelName.val(this.actionDef.labelName || "");
|
||||
|
||||
const $labelValue = $action.find('.label-value');
|
||||
$labelValue.val(this.actionDef.labelValue || "");
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.saveAction({
|
||||
labelName: $labelName.val(),
|
||||
labelValue: $labelValue.val()
|
||||
});
|
||||
}, 1000)
|
||||
|
||||
$labelName.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
$labelValue.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
33
src/public/app/widgets/bulk_actions/note/delete_note.js
Normal file
33
src/public/app/widgets/bulk_actions/note/delete_note.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<span class="bx bx-trash"></span>
|
||||
|
||||
Delete matched notes
|
||||
</td>
|
||||
<td class="button-column">
|
||||
<div class="dropdown help-dropdown">
|
||||
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu dropdown-menu-right p-4">
|
||||
<p>This will delete matched notes.</p>
|
||||
|
||||
<p>After the deletion, it's possible to undelete them from <span class="bx bx-history"></span> Recent Notes dialog.</p>
|
||||
|
||||
<p>To erase notes permanently, you can go after the deletion to the Option -> Other and click the "Erase deleted notes now" button.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="bx bx-x icon-action action-conf-del"></span>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class DeleteNoteBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "deleteNote"; }
|
||||
static get actionTitle() { return "Delete note"; }
|
||||
|
||||
doRender() {
|
||||
return $(TPL);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@@ -19,8 +19,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class DeleteNoteRevisionsSearchAction extends AbstractSearchAction {
|
||||
export default class DeleteNoteRevisionsBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "deleteNoteRevisions"; }
|
||||
static get actionTitle() { return "Delete note revisions"; }
|
||||
|
||||
doRender() {
|
||||
return $(TPL);
|
||||
@@ -1,6 +1,6 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@@ -33,8 +33,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class MoveNoteSearchAction extends AbstractSearchAction {
|
||||
export default class MoveNoteBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "moveNote"; }
|
||||
static get actionTitle() { return "Move note"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
56
src/public/app/widgets/bulk_actions/note/rename_note.js
Normal file
56
src/public/app/widgets/bulk_actions/note/rename_note.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px; flex-shrink: 0;">Rename note title to:</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control new-title"
|
||||
placeholder="new note title"
|
||||
title="Click help icon on the right to see all the options"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="button-column">
|
||||
<div class="dropdown help-dropdown">
|
||||
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu dropdown-menu-right p-4">
|
||||
<p>The given value is evaluated as JavaScript string and thus can be enriched with dynamic content via the injected <code>note</code> variable (note being renamed). Examples:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>Note</code> - all matched notes are renamed to "Note"</li>
|
||||
<li><code>NEW: \${note.title}</code> - matched notes titles are prefixed with "NEW: "</li>
|
||||
<li><code>\${note.dateCreatedObj.format('MM-DD:')}: \${note.title}</code> - matched notes are prefixed with note's creation month-date</li>
|
||||
</ul>
|
||||
|
||||
See API docs for <a href="https://zadam.github.io/trilium/backend_api/Note.html">note</a> and its <a href="https://day.js.org/docs/en/display/format">dateCreatedObj / utcDateCreatedObj properties</a> for details.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="bx bx-x icon-action action-conf-del"></span>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class RenameNoteBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "renameNote"; }
|
||||
static get actionTitle() { return "Rename note"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
||||
const $newTitle = $action.find('.new-title');
|
||||
$newTitle.val(this.actionDef.newTitle || "");
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.saveAction({
|
||||
newTitle: $newTitle.val(),
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
$newTitle.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
65
src/public/app/widgets/bulk_actions/relation/add_relation.js
Normal file
65
src/public/app/widgets/bulk_actions/relation/add_relation.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Add relation</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control relation-name"
|
||||
placeholder="relation name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
style="flex-shrink: 3"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
|
||||
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to</div>
|
||||
|
||||
<div class="input-group" style="flex-shrink: 2">
|
||||
<input type="text" class="form-control target-note" placeholder="target note"/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="button-column">
|
||||
<div class="dropdown help-dropdown">
|
||||
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu dropdown-menu-right p-4">
|
||||
<p>On all matched notes create given relation.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="bx bx-x icon-action action-conf-del"></span>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class AddRelationBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "addRelation"; }
|
||||
static get actionTitle() { return "Add relation"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
||||
const $relationName = $action.find('.relation-name');
|
||||
$relationName.val(this.actionDef.relationName || "");
|
||||
|
||||
const $targetNote = $action.find('.target-note');
|
||||
noteAutocompleteService.initNoteAutocomplete($targetNote);
|
||||
$targetNote.setNote(this.actionDef.targetNoteId);
|
||||
|
||||
$targetNote.on('autocomplete:closed', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.saveAction({
|
||||
relationName: $relationName.val(),
|
||||
targetNoteId: $targetNote.getSelectedNoteId()
|
||||
});
|
||||
}, 1000)
|
||||
|
||||
$relationName.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
$targetNote.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@@ -20,8 +20,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class DeleteRelationSearchAction extends AbstractSearchAction {
|
||||
export default class DeleteRelationBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "deleteRelation"; }
|
||||
static get actionTitle() { return "Delete relation"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
@@ -1,11 +1,11 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;">Rename relation from:</div>
|
||||
<div style="margin-right: 10px; flex-shrink: 0;">Rename relation from:</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control old-relation-name"
|
||||
@@ -27,8 +27,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class RenameRelationSearchAction extends AbstractSearchAction {
|
||||
export default class RenameRelationBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "renameRelation"; }
|
||||
static get actionTitle() { return "Rename relation"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
@@ -1,12 +1,12 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Set relation</div>
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Update relation</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control relation-name"
|
||||
@@ -39,8 +39,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class SetRelationTargetSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "setRelationTarget"; }
|
||||
export default class UpdateRelationTargetBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "updateRelationTarget"; }
|
||||
static get actionTitle() { return "Update relation target"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
@@ -4,6 +4,7 @@ import dateNoteService from "../../services/date_notes.js";
|
||||
import server from "../../services/server.js";
|
||||
import appContext from "../../services/app_context.js";
|
||||
import RightDropdownButtonWidget from "./right_dropdown_button.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
|
||||
const DROPDOWN_TPL = `
|
||||
<div class="calendar-dropdown-widget">
|
||||
@@ -62,7 +63,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
this.hideDropdown();
|
||||
}
|
||||
else {
|
||||
alert("Cannot find day note");
|
||||
toastService.showError("Cannot find day note");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default class ClosePaneButton extends ButtonWidget {
|
||||
// pane (which is being removed)
|
||||
e.stopPropagation();
|
||||
|
||||
widget.triggerCommand("closeThisNoteSplit", { ntxId: widget.getNtxId() });
|
||||
widget.triggerCommand("closeThisNoteSplit", { ntxId: widget.getClosestNtxId() });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ export default class CreatePaneButton extends ButtonWidget {
|
||||
this.icon("bx-dock-right")
|
||||
.title("Create new split")
|
||||
.titlePlacement("bottom")
|
||||
.onClick(widget => widget.triggerCommand("openNewNoteSplit", { ntxId: widget.getNtxId() }));
|
||||
.onClick(widget => widget.triggerCommand("openNewNoteSplit", { ntxId: widget.getClosestNtxId() }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import UpdateAvailableWidget from "./update_available.js";
|
||||
import options from "../../services/options.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="dropdown global-menu dropright">
|
||||
@@ -37,6 +38,10 @@ const TPL = `
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.update-to-latest-version-button {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button type="button" data-toggle="dropdown" data-placement="right"
|
||||
@@ -132,8 +137,7 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
$button.tooltip({ trigger: "hover" });
|
||||
$button.on("click", () => $button.tooltip("hide"));
|
||||
|
||||
this.$widget.find(".show-about-dialog-button").on('click',
|
||||
() => import("../../dialogs/about.js").then(d => d.showDialog()));
|
||||
this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog"));
|
||||
|
||||
const isElectron = utils.isElectron();
|
||||
|
||||
@@ -157,10 +161,12 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
}
|
||||
|
||||
async updateVersionStatus() {
|
||||
if (options.get("checkForUpdates") !== 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestVersion = await this.fetchLatestVersion();
|
||||
|
||||
this.updateAvailableWidget.updateVersionStatus(latestVersion);
|
||||
|
||||
this.$updateToLatestVersionButton.toggle(latestVersion > glob.triliumVersion);
|
||||
this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ export default class LeftPaneToggleWidget extends ButtonWidget {
|
||||
: "bx-chevrons-right";
|
||||
|
||||
this.settings.title = isLeftPaneVisible
|
||||
? "Hide panel."
|
||||
: "Open panel.";
|
||||
? "Hide panel"
|
||||
: "Open panel";
|
||||
|
||||
this.settings.command = isLeftPaneVisible
|
||||
? "hideLeftPane"
|
||||
|
||||
@@ -26,7 +26,7 @@ const TPL = `
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> Re-render note</a>
|
||||
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a>
|
||||
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
|
||||
<a data-trigger-command="openNoteSourceDialog" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
|
||||
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
|
||||
<a class="dropdown-item import-files-button">Import files</a>
|
||||
<a class="dropdown-item export-note-button">Export note</a>
|
||||
@@ -53,11 +53,14 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
import('../../dialogs/export.js').then(d => d.showDialog(this.noteContext.notePath, 'single'));
|
||||
this.triggerCommand("showExportDialog", {
|
||||
notePath: this.noteContext.notePath,
|
||||
defaultType: "single"
|
||||
});
|
||||
});
|
||||
|
||||
this.$importNoteButton = this.$widget.find('.import-files-button');
|
||||
this.$importNoteButton.on("click", () => import('../../dialogs/import.js').then(d => d.showDialog(this.noteId)));
|
||||
this.$importNoteButton.on("click", () => this.triggerCommand("showImportDialog", {noteId: this.noteId}));
|
||||
|
||||
this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class ShowNoteSourceButton extends ButtonWidget {
|
||||
|
||||
this.icon('bx bx-code')
|
||||
.title("Show Note Source")
|
||||
.command("showNoteSource")
|
||||
.command("openNoteSourceDialog")
|
||||
.titlePlacement("bottom");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ const WIDGET_TPL = `
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
/**
|
||||
* TODO: rename, it's not collapsible anymore
|
||||
*/
|
||||
export default class CollapsibleWidget extends NoteContextAwareWidget {
|
||||
get widgetTitle() { return "Untitled widget"; }
|
||||
|
||||
@@ -32,8 +35,4 @@ export default class CollapsibleWidget extends NoteContextAwareWidget {
|
||||
|
||||
/** for overriding */
|
||||
async doRenderBody() {}
|
||||
|
||||
isExpanded() {
|
||||
return this.$bodyWrapper.hasClass("show");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default class Component {
|
||||
this.componentId = `comp-` + this.sanitizedClassName + '-' + utils.randomString(8);
|
||||
/** @type Component[] */
|
||||
this.children = [];
|
||||
this.initialized = Promise.resolve();
|
||||
this.initialized = null;
|
||||
}
|
||||
|
||||
get sanitizedClassName() {
|
||||
@@ -42,10 +42,16 @@ export default class Component {
|
||||
|
||||
/** @returns {Promise} */
|
||||
handleEvent(name, data) {
|
||||
return Promise.all([
|
||||
this.initialized.then(() => this.callMethod(this[name + 'Event'], data)),
|
||||
this.handleEventInChildren(name, data)
|
||||
]);
|
||||
const callMethodPromise = this.initialized
|
||||
? this.initialized.then(() => this.callMethod(this[name + 'Event'], data))
|
||||
: this.callMethod(this[name + 'Event'], data);
|
||||
|
||||
const childrenPromise = this.handleEventInChildren(name, data);
|
||||
|
||||
// don't create promises if not needed (optimization)
|
||||
return callMethodPromise && childrenPromise
|
||||
? Promise.all([callMethodPromise, childrenPromise])
|
||||
: (callMethodPromise || childrenPromise);
|
||||
}
|
||||
|
||||
/** @returns {Promise} */
|
||||
@@ -58,10 +64,15 @@ export default class Component {
|
||||
const promises = [];
|
||||
|
||||
for (const child of this.children) {
|
||||
promises.push(child.handleEvent(name, data));
|
||||
const ret = child.handleEvent(name, data);
|
||||
|
||||
if (ret) {
|
||||
promises.push(ret);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
// don't create promises if not needed (optimization)
|
||||
return promises.length > 0 ? Promise.all(promises) : null;
|
||||
}
|
||||
|
||||
/** @returns {Promise} */
|
||||
@@ -76,9 +87,9 @@ export default class Component {
|
||||
}
|
||||
}
|
||||
|
||||
async callMethod(fun, data) {
|
||||
callMethod(fun, data) {
|
||||
if (typeof fun !== 'function') {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
@@ -91,14 +102,10 @@ export default class Component {
|
||||
console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`);
|
||||
}
|
||||
|
||||
if (glob.isDev) {
|
||||
await utils.timeLimit(promise, 20000, `Time limit failed on ${this.constructor.name} with ${fun.name}`);
|
||||
}
|
||||
else {
|
||||
// cheaper and in non-dev the extra reporting is lost anyway through reload
|
||||
await promise;
|
||||
if (glob.isDev && promise) {
|
||||
return utils.timeLimit(promise, 20000, `Time limit failed on ${this.constructor.name} with ${fun.name}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class Container extends BasicWidget {
|
||||
super.child(...components);
|
||||
|
||||
for (const component of components) {
|
||||
if (!component.position) {
|
||||
if (component.position === undefined) {
|
||||
component.position = this.positionCounter;
|
||||
this.positionCounter += 10;
|
||||
}
|
||||
|
||||
@@ -186,15 +186,27 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
const activeChild = this.getActiveRibbonWidget();
|
||||
|
||||
if (activeChild && (refreshActiveTab || !wasAlreadyActive)) {
|
||||
activeChild.handleEvent('noteSwitched', {noteContext: this.noteContext, notePath: this.notePath}).then(() => {
|
||||
activeChild.focus?.();
|
||||
});
|
||||
const handleEventPromise = activeChild.handleEvent('noteSwitched', {noteContext: this.noteContext, notePath: this.notePath});
|
||||
|
||||
if (refreshActiveTab) {
|
||||
if (handleEventPromise) {
|
||||
handleEventPromise.then(() => activeChild.focus?.());
|
||||
} else {
|
||||
activeChild.focus?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.lastActiveComponentId = null;
|
||||
}
|
||||
}
|
||||
|
||||
async noteSwitched() {
|
||||
this.lastActiveComponentId = null;
|
||||
|
||||
await super.noteSwitched();
|
||||
}
|
||||
|
||||
async refreshWithNote(note, noExplicitActivation = false) {
|
||||
this.lastNoteType = note.type;
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ export default class RightPaneContainer extends FlexContainer {
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.children.length > 0 && !!this.children.find(ch => ch.isEnabled());
|
||||
return super.isEnabled()
|
||||
&& this.children.length > 0
|
||||
&& !!this.children.find(ch => ch.isEnabled() && ch.canBeShown());
|
||||
}
|
||||
|
||||
handleEventInChildren(name, data) {
|
||||
@@ -21,13 +23,25 @@ export default class RightPaneContainer extends FlexContainer {
|
||||
// right pane is displayed only if some child widget is active
|
||||
// we'll reevaluate the visibility based on events which are probable to cause visibility change
|
||||
// but these events needs to be finished and only then we check
|
||||
promise.then(() => {
|
||||
this.toggleInt(this.isEnabled());
|
||||
|
||||
splitService.setupRightPaneResizer();
|
||||
});
|
||||
if (promise) {
|
||||
promise.then(() => this.reevaluateIsEnabledCommand());
|
||||
}
|
||||
else {
|
||||
this.reevaluateIsEnabledCommand();
|
||||
}
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
reevaluateIsEnabledCommand() {
|
||||
const oldToggle = !this.isHiddenInt();
|
||||
const newToggle = this.isEnabled();
|
||||
|
||||
if (oldToggle !== newToggle) {
|
||||
this.toggleInt(newToggle);
|
||||
|
||||
splitService.setupRightPaneResizer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import appContext from "../../services/app_context.js";
|
||||
|
||||
export default class RootContainer extends FlexContainer {
|
||||
constructor() {
|
||||
@@ -9,38 +7,4 @@ export default class RootContainer extends FlexContainer {
|
||||
this.id('root-widget');
|
||||
this.css('height', '100%');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.$widget.removeClass(); // remove all classes
|
||||
const note = appContext.tabManager.getActiveContextNote();
|
||||
|
||||
if (note) {
|
||||
this.$widget.addClass(note.getCssClass());
|
||||
|
||||
this.$widget.addClass(utils.getNoteTypeClass(note.type));
|
||||
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
|
||||
|
||||
this.$widget.toggleClass("protected", note.isProtected);
|
||||
}
|
||||
}
|
||||
|
||||
noteSwitchedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
activeContextChangedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
noteSwitchedAndActivatedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
const note = appContext.tabManager.getActiveContextNote();
|
||||
|
||||
if (note && loadResults.isNoteReloaded(note.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user