mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +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,26 +43,13 @@ 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)) { |  | ||||||
|         return res.send({ |  | ||||||
|             success: false, |  | ||||||
|             message: 'Moving note here would create cycle.' |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (beforeNote) { |  | ||||||
|     await sql.doInTransaction(async () => { |     await sql.doInTransaction(async () => { | ||||||
|         // we don't change date_modified so other changes are prioritized in case of conflict |         // 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 |         // also we would have to sync all those modified note trees otherwise hash checks would fail | ||||||
| @@ -87,10 +67,6 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     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,26 +74,13 @@ 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)) { |  | ||||||
|         return res.send({ |  | ||||||
|             success: false, |  | ||||||
|             message: 'Moving note here would create cycle.' |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (afterNote) { |  | ||||||
|     await sql.doInTransaction(async () => { |     await sql.doInTransaction(async () => { | ||||||
|         // we don't change date_modified so other changes are prioritized in case of conflict |         // 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 |         // also we would have to sync all those modified note trees otherwise hash checks would fail | ||||||
| @@ -133,10 +96,6 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     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