mirror of
https://github.com/zadam/trilium.git
synced 2025-10-28 16:56:34 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6a687c4a6 | ||
|
|
f2aaf8b0a3 | ||
|
|
01ede22504 | ||
|
|
b6d617aefa | ||
|
|
7921850186 | ||
|
|
244a4562b1 | ||
|
|
07c33979c3 | ||
|
|
353a9b24c1 | ||
|
|
548ecd4171 |
@@ -67,10 +67,7 @@ CREATE INDEX `IDX_sync_sync_date` ON `sync` (
|
|||||||
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
||||||
`is_deleted`
|
`is_deleted`
|
||||||
);
|
);
|
||||||
CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` (
|
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
`note_tree_id`
|
|
||||||
);
|
|
||||||
CREATE UNIQUE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
|
||||||
`note_id`,
|
`note_id`,
|
||||||
`parent_note_id`
|
`parent_note_id`
|
||||||
);
|
);
|
||||||
|
|||||||
9
migrations/0062__change_index_back_to_non_unique.sql
Normal file
9
migrations/0062__change_index_back_to_non_unique.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
DROP INDEX IDX_notes_tree_note_id_parent_note_id;
|
||||||
|
|
||||||
|
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
|
`note_id`,
|
||||||
|
`parent_note_id`
|
||||||
|
);
|
||||||
|
|
||||||
|
-- dropping this as it's just duplicate of primary key
|
||||||
|
DROP INDEX IDX_notes_tree_note_tree_id;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www",
|
"start": "node ./bin/www",
|
||||||
"test-electron": "xo",
|
"test-electron": "xo",
|
||||||
|
|||||||
@@ -490,12 +490,14 @@ const noteTree = (function() {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
"ctrl+return": node => {
|
|
||||||
noteDetailEl.focus();
|
|
||||||
},
|
|
||||||
"return": node => {
|
"return": node => {
|
||||||
noteDetailEl.focus();
|
noteDetailEl.focus();
|
||||||
},
|
},
|
||||||
|
"backspace": node => {
|
||||||
|
if (!isTopLevelNode(node)) {
|
||||||
|
node.getParent().setActive().then(() => clearSelectedNodes());
|
||||||
|
}
|
||||||
|
},
|
||||||
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
|
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
|
||||||
// after opening context menu, standard shortcuts don't work, but they are detected here
|
// after opening context menu, standard shortcuts don't work, but they are detected here
|
||||||
// so we essentially takeover the standard handling with our implementation.
|
// so we essentially takeover the standard handling with our implementation.
|
||||||
@@ -532,13 +534,15 @@ const noteTree = (function() {
|
|||||||
const node = data.node;
|
const node = data.node;
|
||||||
|
|
||||||
if (targetType === 'title' || targetType === 'icon') {
|
if (targetType === 'title' || targetType === 'icon') {
|
||||||
node.setActive();
|
|
||||||
|
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
|
node.setActive();
|
||||||
|
node.setSelected(true);
|
||||||
|
|
||||||
clearSelectedNodes();
|
clearSelectedNodes();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
node.setSelected(true);
|
node.setSelected(!node.isSelected());
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
631
public/libraries/jquery.ui-contextmenu.js
Normal file
631
public/libraries/jquery.ui-contextmenu.js
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* jquery.ui-contextmenu.js plugin.
|
||||||
|
*
|
||||||
|
* jQuery plugin that provides a context menu (based on the jQueryUI menu widget).
|
||||||
|
*
|
||||||
|
* @see https://github.com/mar10/jquery-ui-contextmenu
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013-2017, Martin Wendt (http://wwWendt.de). Licensed MIT.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function( factory ) {
|
||||||
|
"use strict";
|
||||||
|
if ( typeof define === "function" && define.amd ) {
|
||||||
|
// AMD. Register as an anonymous module.
|
||||||
|
define([ "jquery", "jquery-ui/ui/widgets/menu" ], factory );
|
||||||
|
} else {
|
||||||
|
// Browser globals
|
||||||
|
factory( jQuery );
|
||||||
|
}
|
||||||
|
}(function( $ ) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var supportSelectstart = "onselectstart" in document.createElement("div"),
|
||||||
|
match = $.ui.menu.version.match(/^(\d)\.(\d+)/),
|
||||||
|
uiVersion = {
|
||||||
|
major: parseInt(match[1], 10),
|
||||||
|
minor: parseInt(match[2], 10)
|
||||||
|
},
|
||||||
|
isLTE110 = ( uiVersion.major < 2 && uiVersion.minor <= 10 ),
|
||||||
|
isLTE111 = ( uiVersion.major < 2 && uiVersion.minor <= 11 );
|
||||||
|
|
||||||
|
$.widget("moogle.contextmenu", {
|
||||||
|
version: "@VERSION",
|
||||||
|
options: {
|
||||||
|
addClass: "ui-contextmenu", // Add this class to the outer <ul>
|
||||||
|
closeOnWindowBlur: true, // Close menu when window loses focus
|
||||||
|
autoFocus: false, // Set keyboard focus to first entry on open
|
||||||
|
autoTrigger: true, // open menu on browser's `contextmenu` event
|
||||||
|
delegate: null, // selector
|
||||||
|
hide: { effect: "fadeOut", duration: "fast" },
|
||||||
|
ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents
|
||||||
|
menu: null, // selector or jQuery pointing to <UL>, or a definition hash
|
||||||
|
position: null, // popup positon
|
||||||
|
preventContextMenuForPopup: false, // prevent opening the browser's system
|
||||||
|
// context menu on menu entries
|
||||||
|
preventSelect: false, // disable text selection of target
|
||||||
|
show: { effect: "slideDown", duration: "fast" },
|
||||||
|
taphold: false, // open menu on taphold events (requires external plugins)
|
||||||
|
uiMenuOptions: {}, // Additional options, used when UI Menu is created
|
||||||
|
// Events:
|
||||||
|
beforeOpen: $.noop, // menu about to open; return `false` to prevent opening
|
||||||
|
blur: $.noop, // menu option lost focus
|
||||||
|
close: $.noop, // menu was closed
|
||||||
|
create: $.noop, // menu was initialized
|
||||||
|
createMenu: $.noop, // menu was initialized (original UI Menu)
|
||||||
|
focus: $.noop, // menu option got focus
|
||||||
|
open: $.noop, // menu was opened
|
||||||
|
select: $.noop // menu option was selected; return `false` to prevent closing
|
||||||
|
},
|
||||||
|
/** Constructor */
|
||||||
|
_create: function() {
|
||||||
|
var cssText, eventNames, targetId,
|
||||||
|
opts = this.options;
|
||||||
|
|
||||||
|
this.$headStyle = null;
|
||||||
|
this.$menu = null;
|
||||||
|
this.menuIsTemp = false;
|
||||||
|
this.currentTarget = null;
|
||||||
|
this.extraData = {};
|
||||||
|
this.previousFocus = null;
|
||||||
|
|
||||||
|
if (opts.delegate == null) {
|
||||||
|
$.error("ui-contextmenu: Missing required option `delegate`.");
|
||||||
|
}
|
||||||
|
if (opts.preventSelect) {
|
||||||
|
// Create a global style for all potential menu targets
|
||||||
|
// If the contextmenu was bound to `document`, we apply the
|
||||||
|
// selector relative to the <body> tag instead
|
||||||
|
targetId = ($(this.element).is(document) ? $("body")
|
||||||
|
: this.element).uniqueId().attr("id");
|
||||||
|
cssText = "#" + targetId + " " + opts.delegate + " { " +
|
||||||
|
"-webkit-user-select: none; " +
|
||||||
|
"-khtml-user-select: none; " +
|
||||||
|
"-moz-user-select: none; " +
|
||||||
|
"-ms-user-select: none; " +
|
||||||
|
"user-select: none; " +
|
||||||
|
"}";
|
||||||
|
this.$headStyle = $("<style class='moogle-contextmenu-style' />")
|
||||||
|
.prop("type", "text/css")
|
||||||
|
.appendTo("head");
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.$headStyle.html(cssText);
|
||||||
|
} catch ( e ) {
|
||||||
|
// issue #47: fix for IE 6-8
|
||||||
|
this.$headStyle[0].styleSheet.cssText = cssText;
|
||||||
|
}
|
||||||
|
// TODO: the selectstart is not supported by FF?
|
||||||
|
if (supportSelectstart) {
|
||||||
|
this.element.on("selectstart" + this.eventNamespace, opts.delegate,
|
||||||
|
function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._createUiMenu(opts.menu);
|
||||||
|
|
||||||
|
eventNames = "contextmenu" + this.eventNamespace;
|
||||||
|
if (opts.taphold) {
|
||||||
|
eventNames += " taphold" + this.eventNamespace;
|
||||||
|
}
|
||||||
|
this.element.on(eventNames, opts.delegate, $.proxy(this._openMenu, this));
|
||||||
|
},
|
||||||
|
/** Destructor, called on $().contextmenu("destroy"). */
|
||||||
|
_destroy: function() {
|
||||||
|
this.element.off(this.eventNamespace);
|
||||||
|
|
||||||
|
this._createUiMenu(null);
|
||||||
|
|
||||||
|
if (this.$headStyle) {
|
||||||
|
this.$headStyle.remove();
|
||||||
|
this.$headStyle = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** (Re)Create jQuery UI Menu. */
|
||||||
|
_createUiMenu: function(menuDef) {
|
||||||
|
var ct, ed,
|
||||||
|
opts = this.options;
|
||||||
|
|
||||||
|
// Remove temporary <ul> if any
|
||||||
|
if (this.isOpen()) {
|
||||||
|
// #58: 'replaceMenu' in beforeOpen causing select: to lose ui.target
|
||||||
|
ct = this.currentTarget;
|
||||||
|
ed = this.extraData;
|
||||||
|
// close without animation, to force async mode
|
||||||
|
this._closeMenu(true);
|
||||||
|
this.currentTarget = ct;
|
||||||
|
this.extraData = ed;
|
||||||
|
}
|
||||||
|
if (this.menuIsTemp) {
|
||||||
|
this.$menu.remove(); // this will also destroy ui.menu
|
||||||
|
} else if (this.$menu) {
|
||||||
|
this.$menu
|
||||||
|
.menu("destroy")
|
||||||
|
.removeClass(this.options.addClass)
|
||||||
|
.hide();
|
||||||
|
}
|
||||||
|
this.$menu = null;
|
||||||
|
this.menuIsTemp = false;
|
||||||
|
// If a menu definition array was passed, create a hidden <ul>
|
||||||
|
// and generate the structure now
|
||||||
|
if ( !menuDef ) {
|
||||||
|
return;
|
||||||
|
} else if ($.isArray(menuDef)) {
|
||||||
|
this.$menu = $.moogle.contextmenu.createMenuMarkup(menuDef);
|
||||||
|
this.menuIsTemp = true;
|
||||||
|
}else if ( typeof menuDef === "string" ) {
|
||||||
|
this.$menu = $(menuDef);
|
||||||
|
} else {
|
||||||
|
this.$menu = menuDef;
|
||||||
|
}
|
||||||
|
// Create - but hide - the jQuery UI Menu widget
|
||||||
|
this.$menu
|
||||||
|
.hide()
|
||||||
|
.addClass(opts.addClass)
|
||||||
|
// Create a menu instance that delegates events to our widget
|
||||||
|
.menu($.extend(true, {}, opts.uiMenuOptions, {
|
||||||
|
items: "> :not(.ui-widget-header)",
|
||||||
|
blur: $.proxy(opts.blur, this),
|
||||||
|
create: $.proxy(opts.createMenu, this),
|
||||||
|
focus: $.proxy(opts.focus, this),
|
||||||
|
select: $.proxy(function(event, ui) {
|
||||||
|
// User selected a menu entry
|
||||||
|
var retval,
|
||||||
|
isParent = $.moogle.contextmenu.isMenu(ui.item),
|
||||||
|
actionHandler = ui.item.data("actionHandler");
|
||||||
|
|
||||||
|
ui.cmd = ui.item.attr("data-command");
|
||||||
|
ui.target = $(this.currentTarget);
|
||||||
|
ui.extraData = this.extraData;
|
||||||
|
// ignore clicks, if they only open a sub-menu
|
||||||
|
if ( !isParent || !opts.ignoreParentSelect) {
|
||||||
|
retval = this._trigger.call(this, "select", event, ui);
|
||||||
|
if ( actionHandler ) {
|
||||||
|
retval = actionHandler.call(this, event, ui);
|
||||||
|
}
|
||||||
|
if ( retval !== false ) {
|
||||||
|
this._closeMenu.call(this);
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, this)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
/** Open popup (called on 'contextmenu' event). */
|
||||||
|
_openMenu: function(event, recursive) {
|
||||||
|
var res, promise, ui,
|
||||||
|
opts = this.options,
|
||||||
|
posOption = opts.position,
|
||||||
|
self = this,
|
||||||
|
manualTrigger = !!event.isTrigger;
|
||||||
|
|
||||||
|
if ( !opts.autoTrigger && !manualTrigger ) {
|
||||||
|
// ignore browser's `contextmenu` events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prevent browser from opening the system context menu
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.currentTarget = event.target;
|
||||||
|
this.extraData = event._extraData || {};
|
||||||
|
|
||||||
|
ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData,
|
||||||
|
originalEvent: event, result: null };
|
||||||
|
|
||||||
|
if ( !recursive ) {
|
||||||
|
res = this._trigger("beforeOpen", event, ui);
|
||||||
|
promise = (ui.result && $.isFunction(ui.result.promise)) ? ui.result : null;
|
||||||
|
ui.result = null;
|
||||||
|
if ( res === false ) {
|
||||||
|
this.currentTarget = null;
|
||||||
|
return false;
|
||||||
|
} else if ( promise ) {
|
||||||
|
// Handler returned a Deferred or Promise. Delay menu open until
|
||||||
|
// the promise is resolved
|
||||||
|
promise.done(function() {
|
||||||
|
self._openMenu(event, true);
|
||||||
|
});
|
||||||
|
this.currentTarget = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ui.menu = this.$menu; // Might have changed in beforeOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register global event handlers that close the dropdown-menu
|
||||||
|
$(document).on("keydown" + this.eventNamespace, function(event) {
|
||||||
|
if ( event.which === $.ui.keyCode.ESCAPE ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
}).on("mousedown" + this.eventNamespace + " touchstart" + this.eventNamespace,
|
||||||
|
function(event) {
|
||||||
|
// Close menu when clicked outside menu
|
||||||
|
if ( !$(event.target).closest(".ui-menu-item").length ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(window).on("blur" + this.eventNamespace, function(event) {
|
||||||
|
if ( opts.closeOnWindowBlur ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// required for custom positioning (issue #18 and #13).
|
||||||
|
if ($.isFunction(posOption)) {
|
||||||
|
posOption = posOption(event, ui);
|
||||||
|
}
|
||||||
|
posOption = $.extend({
|
||||||
|
my: "left top",
|
||||||
|
at: "left bottom",
|
||||||
|
// if called by 'open' method, event does not have pageX/Y
|
||||||
|
of: (event.pageX === undefined) ? event.target : event,
|
||||||
|
collision: "fit"
|
||||||
|
}, posOption);
|
||||||
|
|
||||||
|
// Update entry statuses from callbacks
|
||||||
|
this._updateEntries(this.$menu);
|
||||||
|
|
||||||
|
// Finally display the popup
|
||||||
|
this.$menu
|
||||||
|
.show() // required to fix positioning error
|
||||||
|
.css({
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0
|
||||||
|
}).position(posOption)
|
||||||
|
.hide(); // hide again, so we can apply nice effects
|
||||||
|
|
||||||
|
if ( opts.preventContextMenuForPopup ) {
|
||||||
|
this.$menu.on("contextmenu" + this.eventNamespace, function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._show(this.$menu, opts.show, function() {
|
||||||
|
var $first;
|
||||||
|
|
||||||
|
// Set focus to first active menu entry
|
||||||
|
if ( opts.autoFocus ) {
|
||||||
|
self.previousFocus = $(event.target);
|
||||||
|
// self.$menu.focus();
|
||||||
|
$first = self.$menu
|
||||||
|
.children("li.ui-menu-item")
|
||||||
|
.not(".ui-state-disabled")
|
||||||
|
.first();
|
||||||
|
self.$menu.menu("focus", null, $first).focus();
|
||||||
|
}
|
||||||
|
self._trigger.call(self, "open", event, ui);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** Close popup. */
|
||||||
|
_closeMenu: function(immediately) {
|
||||||
|
var self = this,
|
||||||
|
hideOpts = immediately ? false : this.options.hide,
|
||||||
|
ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
|
||||||
|
|
||||||
|
// Note: we don't want to unbind the 'contextmenu' event
|
||||||
|
$(document)
|
||||||
|
.off("mousedown" + this.eventNamespace)
|
||||||
|
.off("touchstart" + this.eventNamespace)
|
||||||
|
.off("keydown" + this.eventNamespace);
|
||||||
|
$(window)
|
||||||
|
.off("blur" + this.eventNamespace);
|
||||||
|
|
||||||
|
self.currentTarget = null; // issue #44 after hide animation is too late
|
||||||
|
self.extraData = {};
|
||||||
|
if ( this.$menu ) { // #88: widget might have been destroyed already
|
||||||
|
this.$menu
|
||||||
|
.off("contextmenu" + this.eventNamespace);
|
||||||
|
this._hide(this.$menu, hideOpts, function() {
|
||||||
|
if ( self.previousFocus ) {
|
||||||
|
self.previousFocus.focus();
|
||||||
|
self.previousFocus = null;
|
||||||
|
}
|
||||||
|
self._trigger("close", null, ui);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self._trigger("close", null, ui);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Handle $().contextmenu("option", key, value) calls. */
|
||||||
|
_setOption: function(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case "menu":
|
||||||
|
this.replaceMenu(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$.Widget.prototype._setOption.apply(this, arguments);
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry (<LI> tag). */
|
||||||
|
_getMenuEntry: function(cmd) {
|
||||||
|
return this.$menu.find("li[data-command=" + cmd + "]");
|
||||||
|
},
|
||||||
|
/** Close context menu. */
|
||||||
|
close: function() {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
this._closeMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* Apply status callbacks when menu is opened. */
|
||||||
|
_updateEntries: function() {
|
||||||
|
var self = this,
|
||||||
|
ui = {
|
||||||
|
menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
|
||||||
|
|
||||||
|
$.each(this.$menu.find(".ui-menu-item"), function(i, o) {
|
||||||
|
var $entry = $(o),
|
||||||
|
fn = $entry.data("disabledHandler"),
|
||||||
|
res = fn ? fn({ type: "disabled" }, ui) : null;
|
||||||
|
|
||||||
|
ui.item = $entry;
|
||||||
|
ui.cmd = $entry.attr("data-command");
|
||||||
|
// Evaluate `disabled()` callback
|
||||||
|
if ( res != null ) {
|
||||||
|
self.enableEntry(ui.cmd, !res);
|
||||||
|
self.showEntry(ui.cmd, res !== "hide");
|
||||||
|
}
|
||||||
|
// Evaluate `title()` callback
|
||||||
|
fn = $entry.data("titleHandler"),
|
||||||
|
res = fn ? fn({ type: "title" }, ui) : null;
|
||||||
|
if ( res != null ) {
|
||||||
|
self.setTitle(ui.cmd, "" + res);
|
||||||
|
}
|
||||||
|
// Evaluate `tooltip()` callback
|
||||||
|
fn = $entry.data("tooltipHandler"),
|
||||||
|
res = fn ? fn({ type: "tooltip" }, ui) : null;
|
||||||
|
if ( res != null ) {
|
||||||
|
$entry.attr("title", "" + res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** Enable or disable the menu command. */
|
||||||
|
enableEntry: function(cmd, flag) {
|
||||||
|
this._getMenuEntry(cmd).toggleClass("ui-state-disabled", (flag === false));
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry (LI tag) as jQuery object. */
|
||||||
|
getEntry: function(cmd) {
|
||||||
|
return this._getMenuEntry(cmd);
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry wrapper as jQuery object.
|
||||||
|
UI 1.10: this is the <a> tag inside the LI
|
||||||
|
UI 1.11: this is the LI istself
|
||||||
|
UI 1.12: this is the <div> tag inside the LI
|
||||||
|
*/
|
||||||
|
getEntryWrapper: function(cmd) {
|
||||||
|
return this._getMenuEntry(cmd).find(">[role=menuitem]").addBack("[role=menuitem]");
|
||||||
|
},
|
||||||
|
/** Return Menu element (UL). */
|
||||||
|
getMenu: function() {
|
||||||
|
return this.$menu;
|
||||||
|
},
|
||||||
|
/** Return true if menu is open. */
|
||||||
|
isOpen: function() {
|
||||||
|
// return this.$menu && this.$menu.is(":visible");
|
||||||
|
return !!this.$menu && !!this.currentTarget;
|
||||||
|
},
|
||||||
|
/** Open context menu on a specific target (must match options.delegate)
|
||||||
|
* Optional `extraData` is passed to event handlers as `ui.extraData`.
|
||||||
|
*/
|
||||||
|
open: function(targetOrEvent, extraData) {
|
||||||
|
// Fake a 'contextmenu' event
|
||||||
|
extraData = extraData || {};
|
||||||
|
|
||||||
|
var isEvent = (targetOrEvent && targetOrEvent.type && targetOrEvent.target),
|
||||||
|
event = isEvent ? targetOrEvent : {},
|
||||||
|
target = isEvent ? targetOrEvent.target : targetOrEvent,
|
||||||
|
e = jQuery.Event("contextmenu", {
|
||||||
|
target: $(target).get(0),
|
||||||
|
pageX: event.pageX,
|
||||||
|
pageY: event.pageY,
|
||||||
|
originalEvent: isEvent ? targetOrEvent : undefined,
|
||||||
|
_extraData: extraData
|
||||||
|
});
|
||||||
|
return this.element.trigger(e);
|
||||||
|
},
|
||||||
|
/** Replace the menu altogether. */
|
||||||
|
replaceMenu: function(data) {
|
||||||
|
this._createUiMenu(data);
|
||||||
|
},
|
||||||
|
/** Redefine a whole menu entry. */
|
||||||
|
setEntry: function(cmd, entry) {
|
||||||
|
var $ul,
|
||||||
|
$entryLi = this._getMenuEntry(cmd);
|
||||||
|
|
||||||
|
if (typeof entry === "string") {
|
||||||
|
window.console && window.console.warn(
|
||||||
|
"setEntry(cmd, t) with a plain string title is deprecated since v1.18." +
|
||||||
|
"Use setTitle(cmd, '" + entry + "') instead.");
|
||||||
|
return this.setTitle(cmd, entry);
|
||||||
|
}
|
||||||
|
$entryLi.empty();
|
||||||
|
entry.cmd = entry.cmd || cmd;
|
||||||
|
$.moogle.contextmenu.createEntryMarkup(entry, $entryLi);
|
||||||
|
if ($.isArray(entry.children)) {
|
||||||
|
$ul = $("<ul/>").appendTo($entryLi);
|
||||||
|
$.moogle.contextmenu.createMenuMarkup(entry.children, $ul);
|
||||||
|
}
|
||||||
|
// #110: jQuery UI 1.12: refresh only works when this class is not set:
|
||||||
|
$entryLi.removeClass("ui-menu-item");
|
||||||
|
this.getMenu().menu("refresh");
|
||||||
|
},
|
||||||
|
/** Set icon (pass null to remove). */
|
||||||
|
setIcon: function(cmd, icon) {
|
||||||
|
return this.updateEntry(cmd, { uiIcon: icon });
|
||||||
|
},
|
||||||
|
/** Set title. */
|
||||||
|
setTitle: function(cmd, title) {
|
||||||
|
return this.updateEntry(cmd, { title: title });
|
||||||
|
},
|
||||||
|
// /** Set tooltip (pass null to remove). */
|
||||||
|
// setTooltip: function(cmd, tooltip) {
|
||||||
|
// this._getMenuEntry(cmd).attr("title", tooltip);
|
||||||
|
// },
|
||||||
|
/** Show or hide the menu command. */
|
||||||
|
showEntry: function(cmd, flag) {
|
||||||
|
this._getMenuEntry(cmd).toggle(flag !== false);
|
||||||
|
},
|
||||||
|
/** Redefine selective attributes of a menu entry. */
|
||||||
|
updateEntry: function(cmd, entry) {
|
||||||
|
var $icon, $wrapper,
|
||||||
|
$entryLi = this._getMenuEntry(cmd);
|
||||||
|
|
||||||
|
if ( entry.title !== undefined ) {
|
||||||
|
$.moogle.contextmenu.updateTitle($entryLi, "" + entry.title);
|
||||||
|
}
|
||||||
|
if ( entry.tooltip !== undefined ) {
|
||||||
|
if ( entry.tooltip === null ) {
|
||||||
|
$entryLi.removeAttr("title");
|
||||||
|
} else {
|
||||||
|
$entryLi.attr("title", entry.tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( entry.uiIcon !== undefined ) {
|
||||||
|
$wrapper = this.getEntryWrapper(cmd),
|
||||||
|
$icon = $wrapper.find("span.ui-icon").not(".ui-menu-icon");
|
||||||
|
$icon.remove();
|
||||||
|
if ( entry.uiIcon ) {
|
||||||
|
$wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( entry.hide !== undefined ) {
|
||||||
|
$entryLi.toggle(!entry.hide);
|
||||||
|
} else if ( entry.show !== undefined ) {
|
||||||
|
// Note: `show` is an undocumented variant. `hide: false` is preferred
|
||||||
|
$entryLi.toggle(!!entry.show);
|
||||||
|
}
|
||||||
|
// if ( entry.isHeader !== undefined ) {
|
||||||
|
// $entryLi.toggleClass("ui-widget-header", !!entry.isHeader);
|
||||||
|
// }
|
||||||
|
if ( entry.data !== undefined ) {
|
||||||
|
$entryLi.data(entry.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set/clear class names, but handle ui-state-disabled separately
|
||||||
|
if ( entry.disabled === undefined ) {
|
||||||
|
entry.disabled = $entryLi.hasClass("ui-state-disabled");
|
||||||
|
}
|
||||||
|
if ( entry.setClass ) {
|
||||||
|
if ( $entryLi.hasClass("ui-menu-item") ) {
|
||||||
|
entry.setClass += " ui-menu-item";
|
||||||
|
}
|
||||||
|
$entryLi.removeClass();
|
||||||
|
$entryLi.addClass(entry.setClass);
|
||||||
|
} else if ( entry.addClass ) {
|
||||||
|
$entryLi.addClass(entry.addClass);
|
||||||
|
}
|
||||||
|
$entryLi.toggleClass("ui-state-disabled", !!entry.disabled);
|
||||||
|
// // #110: jQuery UI 1.12: refresh only works when this class is not set:
|
||||||
|
// $entryLi.removeClass("ui-menu-item");
|
||||||
|
// this.getMenu().menu("refresh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global functions
|
||||||
|
*/
|
||||||
|
$.extend($.moogle.contextmenu, {
|
||||||
|
/** Convert a menu description into a into a <li> content. */
|
||||||
|
createEntryMarkup: function(entry, $parentLi) {
|
||||||
|
var $wrapper = null;
|
||||||
|
|
||||||
|
$parentLi.attr("data-command", entry.cmd);
|
||||||
|
|
||||||
|
if ( !/[^\-\u2014\u2013\s]/.test( entry.title ) ) {
|
||||||
|
// hyphen, em dash, en dash: separator as defined by UI Menu 1.10
|
||||||
|
$parentLi.text(entry.title);
|
||||||
|
} else {
|
||||||
|
if ( isLTE110 ) {
|
||||||
|
// jQuery UI Menu 1.10 or before required an `<a>` tag
|
||||||
|
$wrapper = $("<a/>", {
|
||||||
|
html: "" + entry.title,
|
||||||
|
href: "#"
|
||||||
|
}).appendTo($parentLi);
|
||||||
|
|
||||||
|
} else if ( isLTE111 ) {
|
||||||
|
// jQuery UI Menu 1.11 preferes to avoid `<a>` tags or <div> wrapper
|
||||||
|
$parentLi.html("" + entry.title);
|
||||||
|
$wrapper = $parentLi;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// jQuery UI Menu 1.12 introduced `<div>` wrappers
|
||||||
|
$wrapper = $("<div/>", {
|
||||||
|
html: "" + entry.title
|
||||||
|
}).appendTo($parentLi);
|
||||||
|
}
|
||||||
|
if ( entry.uiIcon ) {
|
||||||
|
$wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
|
||||||
|
}
|
||||||
|
// Store option callbacks in entry's data
|
||||||
|
$.each( [ "action", "disabled", "title", "tooltip" ], function(i, attr) {
|
||||||
|
if ( $.isFunction(entry[attr]) ) {
|
||||||
|
$parentLi.data(attr + "Handler", entry[attr]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ( entry.disabled === true ) {
|
||||||
|
$parentLi.addClass("ui-state-disabled");
|
||||||
|
}
|
||||||
|
if ( entry.isHeader ) {
|
||||||
|
$parentLi.addClass("ui-widget-header");
|
||||||
|
}
|
||||||
|
if ( entry.addClass ) {
|
||||||
|
$parentLi.addClass(entry.addClass);
|
||||||
|
}
|
||||||
|
if ( $.isPlainObject(entry.data) ) {
|
||||||
|
$parentLi.data(entry.data);
|
||||||
|
}
|
||||||
|
if ( typeof entry.tooltip === "string" ) {
|
||||||
|
$parentLi.attr("title", entry.tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Convert a nested array of command objects into a <ul> structure. */
|
||||||
|
createMenuMarkup: function(options, $parentUl) {
|
||||||
|
var i, menu, $ul, $li;
|
||||||
|
if ( $parentUl == null ) {
|
||||||
|
$parentUl = $("<ul class='ui-helper-hidden' />").appendTo("body");
|
||||||
|
}
|
||||||
|
for (i = 0; i < options.length; i++) {
|
||||||
|
menu = options[i];
|
||||||
|
$li = $("<li/>").appendTo($parentUl);
|
||||||
|
|
||||||
|
$.moogle.contextmenu.createEntryMarkup(menu, $li);
|
||||||
|
|
||||||
|
if ( $.isArray(menu.children) ) {
|
||||||
|
$ul = $("<ul/>").appendTo($li);
|
||||||
|
$.moogle.contextmenu.createMenuMarkup(menu.children, $ul);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $parentUl;
|
||||||
|
},
|
||||||
|
/** Returns true if the menu item has child menu items */
|
||||||
|
isMenu: function(item) {
|
||||||
|
if ( isLTE110 ) {
|
||||||
|
return item.has(">a[aria-haspopup='true']").length > 0;
|
||||||
|
} else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
|
||||||
|
return item.is("[aria-haspopup='true']");
|
||||||
|
} else {
|
||||||
|
return item.has(">div[aria-haspopup='true']").length > 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Replace the title of elem', but retain icons andchild entries. */
|
||||||
|
replaceFirstTextNodeChild: function(elem, html) {
|
||||||
|
var $icons = elem.find(">span.ui-icon,>ul.ui-menu").detach();
|
||||||
|
|
||||||
|
elem
|
||||||
|
.empty()
|
||||||
|
.html(html)
|
||||||
|
.append($icons);
|
||||||
|
},
|
||||||
|
/** Updates the menu item's title */
|
||||||
|
updateTitle: function(item, title) {
|
||||||
|
if ( isLTE110 ) { // jQuery UI 1.10 and before used <a> tags
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild($("a", item), title);
|
||||||
|
} else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild(item, title);
|
||||||
|
} else { // jQuery UI 1.12+ introduced <div> tag wrappers
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild($("div", item), title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
1
public/libraries/jquery.ui-contextmenu.min.js.map
Normal file
1
public/libraries/jquery.ui-contextmenu.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -3,31 +3,24 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const utils = require('../../services/utils');
|
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const utils = require('../../services/utils');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code in this file deals with moving and cloning note tree rows. Relationship between note and parent note is unique
|
||||||
|
* for not deleted note trees. There may be multiple deleted note-parent note relationships.
|
||||||
|
*/
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(parentNoteId, noteToMove.note_id);
|
if (!await validateParentChild(res, parentNoteId, noteToMove.note_id, noteTreeId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await checkTreeCycle(parentNoteId, noteToMove.note_id)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Moving note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
||||||
@@ -50,47 +43,30 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn
|
|||||||
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]);
|
const beforeNote = await getNoteTree(beforeNoteTreeId);
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(beforeNote.parent_note_id, noteToMove.note_id);
|
if (!await validateParentChild(res, beforeNote.parent_note_id, noteToMove.note_id, noteTreeId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await checkTreeCycle(beforeNote.parent_note_id, noteToMove.note_id)) {
|
await sql.doInTransaction(async () => {
|
||||||
return res.send({
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
success: false,
|
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||||
message: 'Moving note here would create cycle.'
|
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0",
|
||||||
});
|
[beforeNote.parent_note_id, beforeNote.note_position]);
|
||||||
}
|
|
||||||
|
|
||||||
if (beforeNote) {
|
await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId);
|
||||||
await sql.doInTransaction(async () => {
|
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
|
||||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
|
||||||
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0",
|
|
||||||
[beforeNote.parent_note_id, beforeNote.note_position]);
|
|
||||||
|
|
||||||
await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId);
|
const now = utils.nowDate();
|
||||||
|
|
||||||
const now = utils.nowDate();
|
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
||||||
|
[beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]);
|
||||||
|
|
||||||
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
[beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]);
|
});
|
||||||
|
|
||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
res.send({ success: true });
|
||||||
});
|
|
||||||
|
|
||||||
res.send({ success: true });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist.");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
||||||
@@ -98,45 +74,28 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async
|
|||||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
const afterNote = await getNoteTree(afterNoteTreeId);
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(afterNote.parent_note_id, noteToMove.note_id);
|
if (!await validateParentChild(res, afterNote.parent_note_id, noteToMove.note_id, noteTreeId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await checkTreeCycle(afterNote.parent_note_id, noteToMove.note_id)) {
|
await sql.doInTransaction(async () => {
|
||||||
return res.send({
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
success: false,
|
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||||
message: 'Moving note here would create cycle.'
|
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0",
|
||||||
});
|
[afterNote.parent_note_id, afterNote.note_position]);
|
||||||
}
|
|
||||||
|
|
||||||
if (afterNote) {
|
await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId);
|
||||||
await sql.doInTransaction(async () => {
|
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
|
||||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
|
||||||
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0",
|
|
||||||
[afterNote.parent_note_id, afterNote.note_position]);
|
|
||||||
|
|
||||||
await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId);
|
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
||||||
|
[afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]);
|
||||||
|
|
||||||
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
[afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]);
|
});
|
||||||
|
|
||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
res.send({ success: true });
|
||||||
});
|
|
||||||
|
|
||||||
res.send({ success: true });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
||||||
@@ -145,20 +104,8 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req
|
|||||||
const prefix = req.body.prefix;
|
const prefix = req.body.prefix;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
if (!await validateParentChild(res, parentNoteId, childNoteId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await checkTreeCycle(parentNoteId, childNoteId)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Cloning note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
||||||
@@ -191,26 +138,10 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
|
|||||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
const afterNote = await getNoteTree(afterNoteTreeId);
|
||||||
|
|
||||||
if (!afterNote) {
|
if (!await validateParentChild(res, afterNote.parent_note_id, noteId)) {
|
||||||
return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(afterNote.parent_note_id, noteId);
|
|
||||||
|
|
||||||
if (existing && !existing.is_deleted) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await checkTreeCycle(afterNote.parent_note_id, noteId)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Cloning note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
@@ -242,15 +173,43 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
|
|||||||
async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
|
async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
|
||||||
subTreeNoteIds.push(parentNoteId);
|
subTreeNoteIds.push(parentNoteId);
|
||||||
|
|
||||||
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ?", [parentNoteId]);
|
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [parentNoteId]);
|
||||||
|
|
||||||
for (const childNoteId of children) {
|
for (const childNoteId of children) {
|
||||||
await loadSubTreeNoteIds(childNoteId, subTreeNoteIds);
|
await loadSubTreeNoteIds(childNoteId, subTreeNoteIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getNoteTree(noteTreeId) {
|
||||||
|
return sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) {
|
||||||
|
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
||||||
|
|
||||||
|
if (existing && (noteTreeId === null || existing.note_tree_id !== noteTreeId)) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'This note already exists in target parent note.'
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await checkTreeCycle(parentNoteId, childNoteId)) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'Moving note here would create cycle.'
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function getExistingNoteTree(parentNoteId, childNoteId) {
|
async function getExistingNoteTree(parentNoteId, childNoteId) {
|
||||||
return await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [childNoteId, parentNoteId]);
|
return await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ? AND is_deleted = 0', [childNoteId, parentNoteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,7 +232,7 @@ async function checkTreeCycle(parentNoteId, childNoteId) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]);
|
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ? AND is_deleted = 0", [parentNoteId]);
|
||||||
|
|
||||||
for (const pid of parentNoteIds) {
|
for (const pid of parentNoteIds) {
|
||||||
if (!await checkTreeCycleInner(pid)) {
|
if (!await checkTreeCycleInner(pid)) {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ router.get('/notes_reordering/:noteTreeParentId', auth.checkApiAuth, async (req,
|
|||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
parent_note_id: noteTreeParentId,
|
parent_note_id: noteTreeParentId,
|
||||||
ordering: await sql.getMap("SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ?", [noteTreeParentId])
|
ordering: await sql.getMap("SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [noteTreeParentId])
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,20 @@ const notes = require('../../services/notes');
|
|||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
||||||
const notes = await sql.getAll("SELECT "
|
const notes = await sql.getAll(`
|
||||||
+ "notes_tree.*, "
|
SELECT
|
||||||
+ "notes.note_title, "
|
notes_tree.*,
|
||||||
+ "notes.is_protected "
|
notes.note_title,
|
||||||
+ "FROM notes_tree "
|
notes.is_protected
|
||||||
+ "JOIN notes ON notes.note_id = notes_tree.note_id "
|
FROM
|
||||||
+ "WHERE notes.is_deleted = 0 AND notes_tree.is_deleted = 0 "
|
notes_tree
|
||||||
+ "ORDER BY note_position");
|
JOIN
|
||||||
|
notes ON notes.note_id = notes_tree.note_id
|
||||||
|
WHERE
|
||||||
|
notes.is_deleted = 0
|
||||||
|
AND notes_tree.is_deleted = 0
|
||||||
|
ORDER BY
|
||||||
|
note_position`);
|
||||||
|
|
||||||
const dataKey = protected_session.getDataKey(req);
|
const dataKey = protected_session.getDataKey(req);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
const build = require('./build');
|
const build = require('./build');
|
||||||
const packageJson = require('../package');
|
const packageJson = require('../package');
|
||||||
|
|
||||||
const APP_DB_VERSION = 61;
|
const APP_DB_VERSION = 62;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
app_version: packageJson.version,
|
app_version: packageJson.version,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = { build_date:"2018-01-02T22:46:50-05:00", build_revision: "96a44a9a0c1b90ba7b58ef37a52cadbaffdf918d" };
|
module.exports = { build_date:"2018-01-03T23:05:00-05:00", build_revision: "f2aaf8b0a3b761fb6a1ec79e7c6b95e3eb9e4db0" };
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async function runCheck(query, errorText, errorList) {
|
|||||||
|
|
||||||
async function checkTreeCycles(errorList) {
|
async function checkTreeCycles(errorList) {
|
||||||
const childToParents = {};
|
const childToParents = {};
|
||||||
const rows = await sql.getAll("SELECT note_id, parent_note_id FROM notes_tree");
|
const rows = await sql.getAll("SELECT note_id, parent_note_id FROM notes_tree WHERE is_deleted = 0");
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const childNoteId = row.note_id;
|
const childNoteId = row.note_id;
|
||||||
@@ -161,6 +161,20 @@ async function runChecks() {
|
|||||||
notes.note_id IS NULL`,
|
notes.note_id IS NULL`,
|
||||||
"Missing notes records for following note history ID > note ID", errorList);
|
"Missing notes records for following note history ID > note ID", errorList);
|
||||||
|
|
||||||
|
await runCheck(`
|
||||||
|
SELECT
|
||||||
|
notes_tree.parent_note_id || ' > ' || notes_tree.note_id
|
||||||
|
FROM
|
||||||
|
notes_tree
|
||||||
|
WHERE
|
||||||
|
notes_tree.is_deleted = 0
|
||||||
|
GROUP BY
|
||||||
|
notes_tree.parent_note_id,
|
||||||
|
notes_tree.note_id
|
||||||
|
HAVING
|
||||||
|
COUNT(*) > 1`,
|
||||||
|
"Duplicate undeleted parent note <-> note relationship - parent note ID > note ID", errorList);
|
||||||
|
|
||||||
await runSyncRowChecks("notes", "note_id", errorList);
|
await runSyncRowChecks("notes", "note_id", errorList);
|
||||||
await runSyncRowChecks("notes_history", "note_history_id", errorList);
|
await runSyncRowChecks("notes_history", "note_history_id", errorList);
|
||||||
await runSyncRowChecks("notes_tree", "note_tree_id", errorList);
|
await runSyncRowChecks("notes_tree", "note_tree_id", errorList);
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ async function protectNoteRecursively(noteId, dataKey, protect, sourceId) {
|
|||||||
|
|
||||||
await protectNote(note, dataKey, protect, sourceId);
|
await protectNote(note, dataKey, protect, sourceId);
|
||||||
|
|
||||||
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ?", [noteId]);
|
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [noteId]);
|
||||||
|
|
||||||
for (const childNoteId of children) {
|
for (const childNoteId of children) {
|
||||||
await protectNoteRecursively(childNoteId, dataKey, protect, sourceId);
|
await protectNoteRecursively(childNoteId, dataKey, protect, sourceId);
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ const dbReady = new Promise((resolve, reject) => {
|
|||||||
await executeScript(notesSql);
|
await executeScript(notesSql);
|
||||||
await executeScript(notesTreeSql);
|
await executeScript(notesTreeSql);
|
||||||
|
|
||||||
const noteId = await getFirstValue("SELECT note_id FROM notes_tree WHERE parent_note_id = 'root' ORDER BY note_position");
|
const startNoteId = await getFirstValue("SELECT note_id FROM notes_tree WHERE parent_note_id = 'root' AND is_deleted = 0 ORDER BY note_position");
|
||||||
|
|
||||||
await require('./options').initOptions(noteId);
|
await require('./options').initOptions(startNoteId);
|
||||||
await require('./sync_table').fillAllSyncRows();
|
await require('./sync_table').fillAllSyncRows();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ async function pushEntity(sync, syncContext) {
|
|||||||
else if (sync.entity_name === 'notes_reordering') {
|
else if (sync.entity_name === 'notes_reordering') {
|
||||||
entity = {
|
entity = {
|
||||||
parent_note_id: sync.entity_id,
|
parent_note_id: sync.entity_id,
|
||||||
ordering: await sql.getMap('SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ?', [sync.entity_id])
|
ordering: await sql.getMap('SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [sync.entity_id])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (sync.entity_name === 'options') {
|
else if (sync.entity_name === 'options') {
|
||||||
|
|||||||
Reference in New Issue
Block a user