Compare commits

..

12 Commits

Author SHA1 Message Date
azivner
e8c52e25f0 release 0.7.0-beta 2018-02-19 23:03:30 -05:00
azivner
a149c6a105 lazy / dynamic loading of CKEditor and Code mirror 2018-02-19 22:02:03 -05:00
azivner
131af9ab12 fix attachment sync 2018-02-18 22:55:36 -05:00
azivner
aa2bbc6575 attachment download now works also in electron, added option to open the attachment 2018-02-18 22:19:07 -05:00
azivner
78e8c15786 attachment upload and download now works for browser 2018-02-18 21:28:24 -05:00
azivner
fda4146150 correct handling of inclusion of dependencies 2018-02-18 10:47:02 -05:00
azivner
ddc885066e support passing functions to the backend as parameters 2018-02-18 09:53:36 -05:00
azivner
08bc2afb49 now it's possible to add comment to the weight, closes #54 2018-02-17 11:47:22 -05:00
azivner
1d0220b03d add weight causes updating old chart instead of creating new chart, closes #53 2018-02-17 10:45:00 -05:00
azivner
3033f7cc08 attribute value is now non-null, fixes #52 2018-02-16 19:07:59 -05:00
azivner
6b9ff47c88 Merge branch 'stable' 2018-02-15 23:24:02 -05:00
azivner
cdde6a4d8e file/attachment upload, wiP 2018-02-14 23:31:20 -05:00
33 changed files with 671 additions and 254 deletions

View File

@@ -0,0 +1,23 @@
UPDATE attributes SET value = '' WHERE value IS NULL;
CREATE TABLE IF NOT EXISTS "attributes_mig"
(
attributeId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL DEFAULT '',
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
INSERT INTO attributes_mig (attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted)
SELECT attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted FROM attributes;
DROP TABLE attributes;
ALTER TABLE attributes_mig RENAME TO attributes;
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);

View File

@@ -15,6 +15,8 @@ require('electron-debug')();
// Prevent window being garbage collected
let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() {
// Dereference the window
// For multiple windows store them in an array

72
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.6.1",
"version": "0.6.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3206,6 +3206,16 @@
"electron-localshortcut": "3.1.0"
}
},
"electron-dl": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.11.0.tgz",
"integrity": "sha512-iL9qHzzWOuL9bus+UT+P72SwrDQcFTV6QHqcbhwgqjCC9/K5jhdRzG0dIMB3TzYlk6rmApanPqh9DvWykwIH1Q==",
"requires": {
"ext-name": "5.0.0",
"pupa": "1.0.0",
"unused-filename": "1.0.0"
}
},
"electron-download": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz",
@@ -4374,6 +4384,23 @@
}
}
},
"ext-list": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
"integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
"requires": {
"mime-db": "1.30.0"
}
},
"ext-name": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
"integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
"requires": {
"ext-list": "2.2.2",
"sort-keys-length": "1.0.1"
}
},
"extend": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@@ -5922,8 +5949,7 @@
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
"dev": true
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"is-png": {
"version": "1.1.0",
@@ -7152,6 +7178,11 @@
}
}
},
"modify-filename": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
},
"moment": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
@@ -7564,6 +7595,11 @@
"mimic-fn": "1.1.0"
}
},
"open": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz",
"integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw="
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
@@ -8391,6 +8427,11 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"pupa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz",
"integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y="
},
"q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -9186,11 +9227,18 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
"dev": true,
"requires": {
"is-plain-obj": "1.1.0"
}
},
"sort-keys-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
"requires": {
"sort-keys": "1.1.2"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -10963,6 +11011,22 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unused-filename": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz",
"integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=",
"requires": {
"modify-filename": "1.1.0",
"path-exists": "3.0.0"
},
"dependencies": {
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
}
}
},
"unzip-response": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.6.2",
"version": "0.7.0-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -29,6 +29,7 @@
"ejs": "~2.5.7",
"electron": "^1.8.2",
"electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4",
"express": "~4.16.2",
"express-promise-wrap": "^0.2.2",
@@ -45,6 +46,7 @@
"jimp": "^0.2.28",
"moment": "^2.20.1",
"multer": "^1.3.0",
"open": "0.0.5",
"rand-token": "^0.4.0",
"request": "^2.83.0",
"request-promise": "^4.2.2",

View File

@@ -23,6 +23,10 @@ class Note extends Entity {
return this.type === "code" && this.mime === "application/json";
}
isJavaScript() {
return this.type === "code" && this.mime === "application/javascript";
}
async getAttributes() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}

View File

@@ -1,5 +1,5 @@
const api = (function() {
const pluginButtonsEl = $("#plugin-buttons");
const $pluginButtons = $("#plugin-buttons");
async function activateNote(notePath) {
await noteTree.activateNode(notePath);
@@ -10,7 +10,7 @@ const api = (function() {
button.attr('id', buttonId);
pluginButtonsEl.append(button);
$pluginButtons.append(button);
}

View File

@@ -1,7 +1,7 @@
"use strict";
const contextMenu = (function() {
const treeEl = $("#tree");
const $tree = $("#tree");
let clipboardIds = [];
let clipboardMode = null;
@@ -93,8 +93,8 @@ const contextMenu = (function() {
beforeOpen: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
// Modify menu entries depending on node status
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
// Activate node on right-click
node.setActive();

View File

@@ -17,26 +17,34 @@ const sqlConsole = (function() {
width: $(window).width(),
height: $(window).height(),
open: function() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.focus();
initEditor();
}
});
}
async function initEditor() {
if (!codeEditor) {
await requireLibrary(CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
}
codeEditor.focus();
}
async function execute() {
const sqlQuery = codeEditor.getValue();

View File

@@ -213,4 +213,26 @@ if (isElectron()) {
noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
});
}
}
function uploadAttachment() {
$("#file-upload").trigger('click');
}
$("#file-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
const resp = await $.ajax({
url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(),
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await noteTree.reload();
await noteTree.activateNode(resp.noteId);
});

View File

@@ -41,11 +41,11 @@ const link = (function() {
function goToLink(e) {
e.preventDefault();
const linkEl = $(e.target);
let notePath = linkEl.attr("note-path");
const $link = $(e.target);
let notePath = $link.attr("note-path");
if (!notePath) {
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
if (!address) {
return;

View File

@@ -1,7 +1,7 @@
"use strict";
const messaging = (function() {
const changesToPushCountEl = $("#changes-to-push-count");
const $changesToPushCount = $("#changes-to-push-count");
function logError(message) {
console.log(now(), message); // needs to be separate from .trace()
@@ -52,7 +52,7 @@ const messaging = (function() {
// we don't detect image changes here since images themselves are immutable and references should be
// updated in note detail as well
changesToPushCountEl.html(message.changesToPushCount);
$changesToPushCount.html(message.changesToPushCount);
}
else if (message.type === 'sync-hash-check-failed') {
showError("Sync check failed!", 60000);

View File

@@ -1,16 +1,24 @@
"use strict";
const noteEditor = (function() {
const noteTitleEl = $("#note-title");
const noteDetailEl = $('#note-detail');
const noteDetailCodeEl = $('#note-detail-code');
const noteDetailRenderEl = $('#note-detail-render');
const protectButton = $("#protect-button");
const unprotectButton = $("#unprotect-button");
const noteDetailWrapperEl = $("#note-detail-wrapper");
const noteIdDisplayEl = $("#note-id-display");
const attributeListEl = $("#attribute-list");
const attributeListInnerEl = $("#attribute-list-inner");
const $noteTitle = $("#note-title");
const $noteDetail = $('#note-detail');
const $noteDetailCode = $('#note-detail-code');
const $noteDetailRender = $('#note-detail-render');
const $noteDetailAttachment = $('#note-detail-attachment');
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $attributeList = $("#attribute-list");
const $attributeListInner = $("#attribute-list-inner");
const $attachmentFileName = $("#attachment-filename");
const $attachmentFileType = $("#attachment-filetype");
const $attachmentFileSize = $("#attachment-filesize");
const $attachmentDownload = $("#attachment-download");
const $attachmentOpen = $("#attachment-open");
let editor = null;
let codeEditor = null;
@@ -80,14 +88,14 @@ const noteEditor = (function() {
else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue();
}
else if (note.detail.type === 'render') {
else if (note.detail.type === 'render' || note.detail.type === 'file') {
// nothing
}
else {
throwError("Unrecognized type: " + note.detail.type);
}
const title = noteTitleEl.val();
const title = $noteTitle.val();
note.detail.title = title;
@@ -105,9 +113,9 @@ const noteEditor = (function() {
function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.detail.isProtected;
noteDetailWrapperEl.toggleClass("protected", isProtected);
protectButton.toggle(!isProtected);
unprotectButton.toggle(isProtected);
$noteDetailWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected);
$unprotectButton.toggle(isProtected);
}
let isNewNoteCreated = false;
@@ -116,19 +124,43 @@ const noteEditor = (function() {
isNewNoteCreated = true;
}
function setContent(content) {
async function setContent(content) {
if (currentNote.detail.type === 'text') {
if (!editor) {
await requireLibrary(CKEDITOR);
editor = await BalloonEditor.create($noteDetail[0], {});
editor.document.on('change', noteChanged);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>");
noteDetailEl.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').hide();
$noteDetail.show();
}
else if (currentNote.detail.type === 'code') {
noteDetailEl.hide();
noteDetailCodeEl.show();
noteDetailRenderEl.html('').hide();
if (!codeEditor) {
await requireLibrary(CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.on('change', noteChanged);
}
$noteDetailCode.show();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content);
@@ -148,10 +180,10 @@ const noteEditor = (function() {
if (isNewNoteCreated) {
isNewNoteCreated = false;
noteTitleEl.focus().select();
$noteTitle.focus().select();
}
noteIdDisplayEl.html(noteId);
$noteIdDisplay.html(noteId);
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
@@ -163,26 +195,36 @@ const noteEditor = (function() {
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protected_session.ensureDialogIsClosed();
noteDetailWrapperEl.show();
$noteDetailWrapper.show();
noteChangeDisabled = true;
noteTitleEl.val(currentNote.detail.title);
$noteTitle.val(currentNote.detail.title);
noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime);
$noteDetail.hide();
$noteDetailCode.hide();
$noteDetailRender.html('').hide();
$noteDetailAttachment.hide();
if (currentNote.detail.type === 'render') {
noteDetailEl.hide();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').show();
$noteDetailRender.show();
const subTree = await server.get('script/subtree/' + getCurrentNoteId());
noteDetailRenderEl.html(subTree);
$noteDetailRender.html(subTree);
}
else if (currentNote.detail.type === 'file') {
$noteDetailAttachment.show();
$attachmentFileName.text(currentNote.attributes.original_file_name);
$attachmentFileSize.text(currentNote.attributes.file_size + " bytes");
$attachmentFileType.text(currentNote.detail.mime);
}
else {
setContent(currentNote.detail.content);
await setContent(currentNote.detail.content);
}
noteChangeDisabled = false;
@@ -191,7 +233,7 @@ const noteEditor = (function() {
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0);
$noteDetailWrapper.scrollTop(0);
loadAttributeList();
}
@@ -201,17 +243,17 @@ const noteEditor = (function() {
const attributes = await server.get('notes/' + noteId + '/attributes');
attributeListInnerEl.html('');
$attributeListInner.html('');
if (attributes.length > 0) {
for (const attr of attributes) {
attributeListInnerEl.append(formatAttribute(attr) + " ");
$attributeListInner.append(formatAttribute(attr) + " ");
}
attributeListEl.show();
$attributeList.show();
}
else {
attributeListEl.hide();
$attributeList.hide();
}
}
@@ -227,12 +269,12 @@ const noteEditor = (function() {
const note = getCurrentNote();
if (note.detail.type === 'text') {
noteDetailEl.focus();
$noteDetail.focus();
}
else if (note.detail.type === 'code') {
codeEditor.focus();
}
else if (note.detail.type === 'render') {
else if (note.detail.type === 'render' || note.detail.type === 'file') {
// do nothing
}
else {
@@ -257,45 +299,49 @@ const noteEditor = (function() {
}
}
$attachmentDownload.click(() => {
if (isElectron()) {
const remote = require('electron').remote;
remote.getCurrentWebContents().downloadURL(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
});
$attachmentOpen.click(() => {
if (isElectron()) {
const open = require("open");
open(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
});
function getAttachmentUrl() {
// electron needs absolute URL so we extract current host, port, protocol
const url = new URL(window.location.href);
const host = url.protocol + "//" + url.hostname + ":" + url.port;
const downloadUrl = "/api/attachments/download/" + getCurrentNoteId();
return host + downloadUrl;
}
$(document).ready(() => {
noteTitleEl.on('input', () => {
$noteTitle.on('input', () => {
noteChanged();
const title = noteTitleEl.val();
const title = $noteTitle.val();
noteTree.setNoteTitle(getCurrentNoteId(), title);
});
BalloonEditor
.create(document.querySelector('#note-detail'), {
})
.then(edit => {
editor = edit;
editor.document.on('change', noteChanged);
})
.catch(error => {
console.error(error);
});
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.on('change', noteChanged);
// so that tab jumps from note title (which has tabindex 1)
noteDetailEl.attr("tabindex", 2);
$noteDetail.attr("tabindex", 2);
});
$(document).bind('keydown', "ctrl+return", executeCurrentNote);

View File

@@ -1,9 +1,9 @@
"use strict";
const noteTree = (function() {
const treeEl = $("#tree");
const parentListEl = $("#parent-list");
const parentListListEl = $("#parent-list-inner");
const $tree = $("#tree");
const $parentList = $("#parent-list");
const $parentListList = $("#parent-list-inner");
let startNotePath = null;
let notesTreeMap = {};
@@ -52,7 +52,7 @@ const noteTree = (function() {
// note that if you want to access data like noteId or isProtected, you need to go into "data" property
function getCurrentNode() {
return treeEl.fancytree("getActiveNode");
return $tree.fancytree("getActiveNode");
}
function getCurrentNotePath() {
@@ -314,11 +314,11 @@ const noteTree = (function() {
}
if (parents.length <= 1) {
parentListEl.hide();
$parentList.hide();
}
else {
parentListEl.show();
parentListListEl.empty();
$parentList.show();
$parentListList.empty();
for (const parentNoteId of parents) {
const parentNotePath = getSomeNotePath(parentNoteId);
@@ -335,7 +335,7 @@ const noteTree = (function() {
item = link.createNoteLink(notePath, title);
}
parentListListEl.append($("<li/>").append(item));
$parentListList.append($("<li/>").append(item));
}
}
}
@@ -543,7 +543,7 @@ const noteTree = (function() {
}
};
treeEl.fancytree({
$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"],
@@ -624,11 +624,11 @@ const noteTree = (function() {
}
});
treeEl.contextmenu(contextMenu.contextMenuSettings);
$tree.contextmenu(contextMenu.contextMenuSettings);
}
function getTree() {
return treeEl.fancytree('getTree');
return $tree.fancytree('getTree');
}
async function reload() {
@@ -663,7 +663,7 @@ const noteTree = (function() {
function collapseTree(node = null) {
if (!node) {
node = treeEl.fancytree("getRootNode");
node = $tree.fancytree("getRootNode");
}
node.setExpanded(false);
@@ -744,7 +744,7 @@ const noteTree = (function() {
}
async function createNewTopLevelNote() {
const rootNode = treeEl.fancytree("getRootNode");
const rootNode = $tree.fancytree("getRootNode");
await createNote(rootNode, "root", "into");
}

View File

@@ -1,7 +1,7 @@
"use strict";
const noteType = (function() {
const executeScriptButton = $("#execute-script-button");
const $executeScriptButton = $("#execute-script-button");
const noteTypeModel = new NoteTypeModel();
function NoteTypeModel() {
@@ -65,11 +65,18 @@ const noteType = (function() {
else if (type === 'render') {
return 'Render HTML note';
}
else if (type === 'file') {
return 'Attachment';
}
else {
throwError('Unrecognized type: ' + type);
}
};
this.isDisabled = function() {
return self.type() === "file";
};
async function save() {
const note = noteEditor.getCurrentNote();
@@ -114,7 +121,7 @@ const noteType = (function() {
};
this.updateExecuteScriptButtonVisibility = function() {
executeScriptButton.toggle(self.mime() === 'application/javascript');
$executeScriptButton.toggle(self.mime() === 'application/javascript');
}
}

View File

@@ -1,10 +1,10 @@
"use strict";
const protected_session = (function() {
const dialogEl = $("#protected-session-password-dialog");
const passwordFormEl = $("#protected-session-password-form");
const passwordEl = $("#protected-session-password");
const noteDetailWrapperEl = $("#note-detail-wrapper");
const $dialog = $("#protected-session-password-dialog");
const $passwordForm = $("#protected-session-password-form");
const $password = $("#protected-session-password");
const $noteDetailWrapper = $("#note-detail-wrapper");
let protectedSessionDeferred = null;
let lastProtectedSessionOperationDate = null;
@@ -25,9 +25,9 @@ const protected_session = (function() {
if (requireProtectedSession && !isProtectedSessionAvailable()) {
protectedSessionDeferred = dfd;
noteDetailWrapperEl.hide();
$noteDetailWrapper.hide();
dialogEl.dialog({
$dialog.dialog({
modal: modal,
width: 400,
open: () => {
@@ -46,8 +46,8 @@ const protected_session = (function() {
}
async function setupProtectedSession() {
const password = passwordEl.val();
passwordEl.val("");
const password = $password.val();
$password.val("");
const response = await enterProtectedSession(password);
@@ -58,15 +58,15 @@ const protected_session = (function() {
protectedSessionId = response.protectedSessionId;
dialogEl.dialog("close");
$dialog.dialog("close");
noteEditor.reload();
noteTree.reload();
if (protectedSessionDeferred !== null) {
ensureDialogIsClosed(dialogEl, passwordEl);
ensureDialogIsClosed($dialog, $password);
noteDetailWrapperEl.show();
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
@@ -77,11 +77,11 @@ const protected_session = (function() {
function ensureDialogIsClosed() {
// this may fal if the dialog has not been previously opened
try {
dialogEl.dialog('close');
$dialog.dialog('close');
}
catch (e) {}
passwordEl.val('');
$password.val('');
}
async function enterProtectedSession(password) {
@@ -155,7 +155,7 @@ const protected_session = (function() {
noteEditor.reload();
}
passwordFormEl.submit(() => {
$passwordForm.submit(() => {
setupProtectedSession();
return false;

View File

@@ -1,40 +1,40 @@
"use strict";
const searchTree = (function() {
const treeEl = $("#tree");
const searchInputEl = $("input[name='search-text']");
const resetSearchButton = $("button#reset-search-button");
const searchBoxEl = $("#search-box");
const $tree = $("#tree");
const $searchInput = $("input[name='search-text']");
const $resetSearchButton = $("button#reset-search-button");
const $searchBox = $("#search-box");
resetSearchButton.click(resetSearch);
$resetSearchButton.click(resetSearch);
function toggleSearch() {
if (searchBoxEl.is(":hidden")) {
searchBoxEl.show();
searchInputEl.focus();
if ($searchBox.is(":hidden")) {
$searchBox.show();
$searchInput.focus();
}
else {
resetSearch();
searchBoxEl.hide();
$searchBox.hide();
}
}
function resetSearch() {
searchInputEl.val("");
$searchInput.val("");
getTree().clearFilter();
}
function getTree() {
return treeEl.fancytree('getTree');
return $tree.fancytree('getTree');
}
searchInputEl.keyup(async e => {
const searchText = searchInputEl.val();
$searchInput.keyup(async e => {
const searchText = $searchInput.val();
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
resetSearchButton.click();
$resetSearchButton.click();
return;
}

View File

@@ -31,12 +31,23 @@ const server = (function() {
return await call('DELETE', url);
}
function prepareParams(params) {
return params.map(p => {
if (typeof p === "function") {
return "!@#Function: " + p.toString();
}
else {
return p;
}
});
}
async function exec(params, script) {
if (typeof script === "function") {
script = script.toString();
}
const ret = await post('script/exec', { script: script, params: params });
const ret = await post('script/exec', { script: script, params: prepareParams(params) });
return ret.executionResult;
}
@@ -105,6 +116,7 @@ const server = (function() {
put,
remove,
exec,
ajax,
// don't remove, used from CKEditor image upload!
getHeaders
}

View File

@@ -1,14 +1,14 @@
"use strict";
const treeUtils = (function() {
const treeEl = $("#tree");
const $tree = $("#tree");
function getParentProtectedStatus(node) {
return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
}
function getNodeByKey(key) {
return treeEl.fancytree('getNodeByKey', key);
return $tree.fancytree('getNodeByKey', key);
}
function getNoteIdFromNotePath(notePath) {

View File

@@ -116,8 +116,7 @@ async function stopWatch(what, func) {
}
function executeScript(script) {
// last \r\n is necessary if script contains line comment on its last line
eval("(async function() {" + script + "\r\n})()");
eval(script);
}
function formatValueWithWhitespace(val) {
@@ -132,4 +131,57 @@ function formatAttribute(attr) {
}
return str;
}
const CKEDITOR = { "js": ["libraries/ckeditor/ckeditor.js"] };
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js"
],
css: [
"libraries/codemirror/codemirror.css"
]
};
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
}
if (library.js) {
for (const scriptUrl of library.js) {
await requireScript(scriptUrl);
}
}
}
async function requireScript(url) {
const scripts = Array
.from(document.querySelectorAll('script'))
.map(scr => scr.src);
if (!scripts.includes(url)) {
return $.ajax({
url: url,
dataType: "script",
cache: true
})
}
}
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(url)) {
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
}
}

View File

@@ -268,4 +268,9 @@ div.ui-tooltip {
#attribute-list button {
padding: 2px;
margin-right: 5px;
}
#attachment-table th, #attachment-table td {
padding: 10px;
font-size: large;
}

View File

@@ -0,0 +1,61 @@
"use strict";
const express = require('express');
const router = express.Router();
const sql = require('../../services/sql');
const auth = require('../../services/auth');
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const multer = require('multer')();
const wrap = require('express-promise-wrap').wrap;
router.post('/upload/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
const sourceId = req.headers.source_id;
const parentNoteId = req.params.parentNoteId;
const file = req.file;
const originalName = file.originalname;
const size = file.size;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
await sql.doInTransaction(async () => {
const noteId = (await notes.createNewNote(parentNoteId, {
title: originalName,
content: file.buffer,
target: 'into',
isProtected: false,
type: 'file',
mime: file.mimetype
}, req, sourceId)).noteId;
await attributes.createAttribute(noteId, "original_file_name", originalName, sourceId);
await attributes.createAttribute(noteId, "file_size", size, sourceId);
res.send({
noteId: noteId
});
});
}));
router.get('/download/:noteId', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
const attributeMap = await attributes.getNoteAttributeMap(noteId);
const fileName = attributeMap.original_file_name ? attributeMap.original_file_name : note.title;
res.setHeader('Content-Disposition', 'attachment; filename=' + fileName);
res.setHeader('Content-Type', note.mime);
res.send(note.content);
}));
module.exports = router;

View File

@@ -5,6 +5,7 @@ const router = express.Router();
const auth = require('../../services/auth');
const sql = require('../../services/sql');
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const log = require('../../services/log');
const utils = require('../../services/utils');
const protected_session = require('../../services/protected_session');
@@ -25,8 +26,19 @@ router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
protected_session.decryptNote(req, detail);
let attributeMap = null;
if (detail.type === 'file') {
// no need to transfer attachment payload for this request
detail.content = null;
// attributes contain important attachment metadata - filename and size
attributeMap = await attributes.getNoteAttributeMap(noteId);
}
res.send({
detail: detail
detail: detail,
attributes: attributeMap
});
}));

View File

@@ -31,26 +31,28 @@ router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
}));
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
const noteId = req.params.noteId;
const repository = new Repository(req);
const noteScript = (await repository.getNote(noteId)).content;
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
res.send(subTreeScripts + noteScript);
res.send(await getNoteWithSubtreeScript(noteId, repository));
}));
async function getNoteWithSubtreeScript(noteId, repository) {
const noteScript = (await repository.getNote(noteId)).content;
const note = await repository.getNote(noteId);
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
let noteScript = note.content;
if (note.isJavaScript()) {
// last \r\n is necessary if script contains line comment on its last line
noteScript = "(async function() {" + noteScript + "\r\n})()";
}
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository, note.isJavaScript());
return subTreeScripts + noteScript;
}
async function getSubTreeScripts(parentId, includedNoteIds, repository) {
async function getSubTreeScripts(parentId, includedNoteIds, repository, isJavaScript) {
const children = await repository.getEntities(`
SELECT notes.*
FROM notes JOIN note_tree USING(noteId)
@@ -69,7 +71,7 @@ async function getSubTreeScripts(parentId, includedNoteIds, repository) {
script += await getSubTreeScripts(child.noteId, includedNoteIds, repository);
if (child.mime === 'application/javascript') {
if (!isJavaScript && child.mime === 'application/javascript') {
child.content = '<script>' + child.content + '</script>';
}

View File

@@ -79,9 +79,12 @@ router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => {
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
sync.serializeNoteContentBuffer(entity);
res.send({
entity: await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId])
entity: entity
});
}));

View File

@@ -29,6 +29,7 @@ const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender');
const attachmentsRoute = require('./api/attachments');
function register(app) {
app.use('/', indexRoute);
@@ -61,6 +62,7 @@ function register(app) {
app.use('/api/images', imageRoute);
app.use('/api/script', scriptRoute);
app.use('/api/sender', senderRoute);
app.use('/api/attachments', attachmentsRoute);
}
module.exports = {

View File

@@ -1,4 +1,4 @@
<form id="weight-form" style="display: flex; width: 500px; justify-content: space-around; align-items: flex-end;">
<form id="weight-form" style="display: flex; width: 700px; justify-content: space-around; align-items: flex-end;">
<div>
<label for="weight-date">Date</label>
<input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" />
@@ -7,6 +7,10 @@
<label for="weight">Weight</label>
<input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" />
</div>
<div>
<label for="comment">Comment</label>
<input type="text" id="comment" class="form-control" style="width: 200px;" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
</form>
@@ -16,84 +20,127 @@
<canvas id="canvas"></canvas>
<script>
(async function() {
const dateEl = $("#weight-date");
const weightEl = $("#weight");
(async function() {
const $form = $("#weight-form");
const $date = $("#weight-date");
const $weight = $("#weight");
const $comment = $("#comment");
let chart;
dateEl.datepicker();
dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd');
dateEl.datepicker('setDate', new Date());
$date.datepicker();
$date.datepicker('option', 'dateFormat', 'yy-mm-dd');
$date.datepicker('setDate', new Date());
async function saveWeight() {
await server.exec([dateEl.val(), weightEl.val()], async (date, weight) => {
const dataNote = await this.getNoteWithAttribute('date_data', date);
async function saveWeight() {
await server.exec([$date.val(), parseFloat($weight.val()), $comment.val()], async (date, weight, comment) => {
const dataNote = await this.getNoteWithAttribute('date_data', date);
if (dataNote) {
dataNote.jsonContent.weight = weight;
if (dataNote) {
dataNote.jsonContent.weight = weight;
await this.updateEntity(dataNote);
}
else {
const parentNoteId = await this.getDateNoteId(date);
const jsonContent = { weight: weight };
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date,
hide_in_autocomplete: null
if (comment) {
dataNote.jsonContent.weight_comment = comment;
}
});
}
});
showMessage("Weight has been saved");
await this.updateEntity(dataNote);
}
else {
const parentNoteId = await this.getDateNoteId(date);
const jsonContent = { weight: weight };
if (comment) {
jsonContent.weight_comment = comment;
}
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date,
hide_in_autocomplete: null
}
});
}
});
showMessage("Weight has been saved");
chart.data = await getData();
chart.update();
}
async function drawChart() {
const data = await getData();
const ctx = $("#canvas")[0].getContext("2d");
chart = new Chart(ctx, {
type: 'line',
data: data,
options: {
tooltips: {
enabled: true,
mode: 'single',
callbacks: {
label: function (tooltipItem, data) {
const multistringText = [tooltipItem.yLabel];
const comment = data.comments[tooltipItem['index']];
if (comment) {
multistringText.push(comment);
}
return multistringText;
}
}
},
}
});
}
async function getData() {
const data = await server.exec([], async () => {
const notes = await this.getNotesWithAttribute('date_data');
const data = [];
for (const note of notes) {
const dateAttr = await note.getAttribute('date_data');
data.push({
date: dateAttr.value,
weight: note.jsonContent.weight,
comment: note.jsonContent.weight_comment
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
const datasets = [{
label: "Weight",
backgroundColor: 'red',
borderColor: 'red',
data: data.map(row => row.weight),
fill: false
}];
const labels = data.map(row => row.date);
const comments = data.map(row => row.comment);
return {
labels: labels,
datasets: datasets,
comments: comments
};
}
$form.submit(event => {
saveWeight();
event.preventDefault();
});
drawChart();
}
async function drawChart() {
const data = await server.exec([], async () => {
const notes = await this.getNotesWithAttribute('date_data');
const data = [];
for (const note of notes) {
const dateAttr = await note.getAttribute('date_data');
data.push({
date: dateAttr.value,
weight: note.jsonContent.weight
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
const ctx = $("#canvas")[0].getContext("2d");
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(row => row.date),
datasets: [{
label: "Weight",
backgroundColor: 'red',
borderColor: 'red',
data: data.map(row => row.weight),
fill: false
}]
}
});
}
$("#weight-form").submit(event => {
saveWeight();
event.preventDefault();
});
drawChart();
})();
})();
</script>

View File

@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 76;
const APP_DB_VERSION = 77;
module.exports = {
app_version: packageJson.version,

View File

@@ -13,7 +13,7 @@ const BUILTIN_ATTRIBUTES = [
];
async function getNoteAttributeMap(noteId) {
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]);
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ? AND isDeleted = 0`, [noteId]);
}
async function getNoteIdWithAttribute(name, value) {
@@ -52,7 +52,11 @@ async function getNoteIdsWithAttribute(name) {
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]);
}
async function createAttribute(noteId, name, value = null, sourceId = null) {
async function createAttribute(noteId, name, value = "", sourceId = null) {
if (value === null || value === undefined) {
value = "";
}
const now = utils.nowDate();
const attributeId = utils.newAttributeId();

View File

@@ -214,7 +214,7 @@ async function runAllChecks() {
FROM
notes
WHERE
type != 'text' AND type != 'code' AND type != 'render'`,
type != 'text' AND type != 'code' AND type != 'render' AND type != 'file'`,
"Note has invalid type", errorList);
await runSyncRowChecks("notes", "noteId", errorList);

View File

@@ -19,7 +19,14 @@ async function executeScript(dataKey, script, params) {
}
function getParams(params) {
return params.map(p => JSON.stringify(p)).join(",");
return params.map(p => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
}
else {
return JSON.stringify(p);
}
}).join(",");
}
module.exports = {

View File

@@ -204,6 +204,8 @@ async function pushEntity(sync, syncContext) {
if (sync.entityName === 'notes') {
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
serializeNoteContentBuffer(entity);
}
else if (sync.entityName === 'note_tree') {
entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]);
@@ -258,6 +260,12 @@ async function pushEntity(sync, syncContext) {
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
}
function serializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = note.content.toString("binary");
}
}
async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
@@ -350,5 +358,6 @@ sql.dbReady.then(() => {
});
module.exports = {
sync
sync,
serializeNoteContentBuffer
};

View File

@@ -1,10 +1,17 @@
const sql = require('./sql');
const log = require('./log');
const eventLog = require('./event_log');
const notes = require('./notes');
const sync_table = require('./sync_table');
function deserializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary');
}
}
async function updateNote(entity, sourceId) {
deserializeNoteContentBuffer(entity);
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
if (!origNote || origNote.dateModified <= entity.dateModified) {

View File

@@ -105,7 +105,7 @@
onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button>
<div class="dropdown" id="note-type">
<button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
<button data-bind="disable: isDisabled()" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span>
</button>
@@ -130,6 +130,7 @@
<li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li>
<li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li>
<li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li>
<li><a onclick="uploadAttachment();">Upload attachment</a></li>
</ul>
</div>
</div>
@@ -141,6 +142,32 @@
<div id="note-detail-code"></div>
<div id="note-detail-render"></div>
<div id="note-detail-attachment">
<table id="attachment-table">
<tr>
<th>File name:</th>
<td id="attachment-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="attachment-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="attachment-filesize"></td>
</tr>
<tr>
<td>
<button id="attachment-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="attachment-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="file-upload" style="display: none" />
</div>
<div id="attribute-list">
@@ -449,8 +476,6 @@
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all.min.js"></script>
<script src="libraries/ckeditor/ckeditor.js"></script>
<script src="libraries/jquery.hotkeys.js"></script>
<script src="libraries/jquery.fancytree.hotkeys.js"></script>
@@ -458,15 +483,6 @@
<script src="libraries/knockout.min.js"></script>
<script src="libraries/codemirror/codemirror.js"></script>
<link rel="stylesheet" href="libraries/codemirror/codemirror.css">
<script src="libraries/codemirror/addon/mode/loadmode.js"></script>
<script src="libraries/codemirror/addon/fold/xml-fold.js"></script>
<script src="libraries/codemirror/addon/edit/matchbrackets.js"></script>
<script src="libraries/codemirror/addon/edit/matchtags.js"></script>
<script src="libraries/codemirror/addon/search/match-highlighter.js"></script>
<script src="libraries/codemirror/mode/meta.js"></script>
<link href="stylesheets/style.css" rel="stylesheet">
<script src="javascripts/utils.js"></script>