mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 08:16:40 +01:00
Compare commits
118 Commits
v0.5.4-bet
...
v0.9.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
840af15dae | ||
|
|
0fdb6af98a | ||
|
|
f6c7f6a0f2 | ||
|
|
354999f37a | ||
|
|
348c622845 | ||
|
|
44bcdedaba | ||
|
|
755c0f3ce2 | ||
|
|
895bda41b5 | ||
|
|
b2df622cb6 | ||
|
|
9ba6e6d0f5 | ||
|
|
a5c9180533 | ||
|
|
e86f1e0d05 | ||
|
|
b6277049f3 | ||
|
|
c831221cc4 | ||
|
|
577a168714 | ||
|
|
b0bd27321a | ||
|
|
90c5348ca7 | ||
|
|
8e95b080da | ||
|
|
766a567a32 | ||
|
|
6d0218cb36 | ||
|
|
d26170762b | ||
|
|
b3209a9bbf | ||
|
|
61c2456cf6 | ||
|
|
1c6fc9029f | ||
|
|
5c91e38dfe | ||
|
|
07bf075894 | ||
|
|
ddce5c959e | ||
|
|
3b9d1df05c | ||
|
|
d239ef2956 | ||
|
|
7a865a9081 | ||
|
|
83d6c2970f | ||
|
|
8c7d159012 | ||
|
|
d169f67901 | ||
|
|
982b723647 | ||
|
|
31d5ac05ff | ||
|
|
72d91d1571 | ||
|
|
f4b57f4c57 | ||
|
|
ee0833390a | ||
|
|
2acff07368 | ||
|
|
bea1d24f07 | ||
|
|
adc270c59f | ||
|
|
66064f7a94 | ||
|
|
1501fa8dbf | ||
|
|
60bba46d80 | ||
|
|
12c06ae97e | ||
|
|
f0bea9cf71 | ||
|
|
a555b6319c | ||
|
|
5dd93e4cdc | ||
|
|
3b4509d833 | ||
|
|
19308bbfbd | ||
|
|
4acc5432c3 | ||
|
|
08b8141fdf | ||
|
|
e1200aa308 | ||
|
|
89666eb078 | ||
|
|
d5605aa64d | ||
|
|
2582b016f9 | ||
|
|
e8c52e25f0 | ||
|
|
a149c6a105 | ||
|
|
131af9ab12 | ||
|
|
aa2bbc6575 | ||
|
|
78e8c15786 | ||
|
|
fda4146150 | ||
|
|
ddc885066e | ||
|
|
08bc2afb49 | ||
|
|
1d0220b03d | ||
|
|
3033f7cc08 | ||
|
|
6b9ff47c88 | ||
|
|
fd02c6102d | ||
|
|
30c712a6be | ||
|
|
3928c96640 | ||
|
|
d86f655658 | ||
|
|
abdad1c3ae | ||
|
|
9e5f1a0a87 | ||
|
|
cdde6a4d8e | ||
|
|
8028b09351 | ||
|
|
ebe66eaed9 | ||
|
|
5bce9a5f94 | ||
|
|
dfd9927310 | ||
|
|
9bf1735bde | ||
|
|
2e8eeda5ab | ||
|
|
1cef0ce5f9 | ||
|
|
1efac99828 | ||
|
|
0e9473119e | ||
|
|
7bbfef7af3 | ||
|
|
5cb93509c1 | ||
|
|
89e89e04d8 | ||
|
|
72df0d8861 | ||
|
|
9910aebf45 | ||
|
|
f9f8ecb2b1 | ||
|
|
438f7c5b0b | ||
|
|
4b1d1aba74 | ||
|
|
6dea73cfe2 | ||
|
|
58f5d0cf6e | ||
|
|
7b77e40514 | ||
|
|
660908c54b | ||
|
|
e970564036 | ||
|
|
b3038487f8 | ||
|
|
cac98392a6 | ||
|
|
dbd28377e3 | ||
|
|
c76e4faf5d | ||
|
|
e011b9ae63 | ||
|
|
7c74c77a2c | ||
|
|
c2a2f195aa | ||
|
|
85d32c66f2 | ||
|
|
4e70cebf70 | ||
|
|
214d2e7659 | ||
|
|
f380bb7f65 | ||
|
|
0a9a032daa | ||
|
|
23a2b58b24 | ||
|
|
aee64b2522 | ||
|
|
02e07ec03a | ||
|
|
3d2dc8e699 | ||
|
|
c84e15c9be | ||
|
|
e18d0b9fd4 | ||
|
|
52817504d1 | ||
|
|
a3b31fab54 | ||
|
|
bc4aa3e40a | ||
|
|
873ea67e9c |
@@ -10,6 +10,7 @@ Trilium Notes is a hierarchical note taking application. Picture tells a thousan
|
|||||||
* WYSIWYG (What You See Is What You Get) editing
|
* WYSIWYG (What You See Is What You Get) editing
|
||||||
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
|
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
|
||||||
* Seamless note versioning
|
* Seamless note versioning
|
||||||
|
* Note attributes can be used to tag/label notes as an alternative note organization and querying
|
||||||
* Can be deployed as web application and / or desktop application with offline access (electron based)
|
* Can be deployed as web application and / or desktop application with offline access (electron based)
|
||||||
* [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server
|
* [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server
|
||||||
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes)
|
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes)
|
||||||
@@ -34,6 +35,7 @@ List of documentation pages:
|
|||||||
* [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
|
* [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
|
||||||
* [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation)
|
* [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation)
|
||||||
* [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation)
|
* [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation)
|
||||||
|
* [Attributes](https://github.com/zadam/trilium/wiki/Attributes)
|
||||||
* [Links](https://github.com/zadam/trilium/wiki/Links)
|
* [Links](https://github.com/zadam/trilium/wiki/Links)
|
||||||
* [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes)
|
* [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes)
|
||||||
* [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes)
|
* [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
[General]
|
||||||
|
# Instance name can be used to distinguish between different instances
|
||||||
|
instanceName=
|
||||||
|
|
||||||
[Network]
|
[Network]
|
||||||
port=8080
|
port=8080
|
||||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
|
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX IDX_attributes_noteId_name;
|
||||||
1
db/migrations/0073__add_isDeleted_to_attributes.sql
Normal file
1
db/migrations/0073__add_isDeleted_to_attributes.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE attributes ADD COLUMN isDeleted INT NOT NULL DEFAULT 0;
|
||||||
1
db/migrations/0074__add_position_to_attribute.sql
Normal file
1
db/migrations/0074__add_position_to_attribute.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE attributes ADD COLUMN position INT NOT NULL DEFAULT 0;
|
||||||
7
db/migrations/0075__add_api_token.sql
Normal file
7
db/migrations/0075__add_api_token.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "api_tokens"
|
||||||
|
(
|
||||||
|
apiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
dateCreated TEXT NOT NULL,
|
||||||
|
isDeleted INT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
1
db/migrations/0076__add_attribute_name_index.sql
Normal file
1
db/migrations/0076__add_attribute_name_index.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);
|
||||||
23
db/migrations/0077__non_null_attribute_value.sql
Normal file
23
db/migrations/0077__non_null_attribute_value.sql
Normal 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);
|
||||||
1
db/migrations/0078__javascript_type.sql
Normal file
1
db/migrations/0078__javascript_type.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript';
|
||||||
@@ -85,8 +85,10 @@ CREATE TABLE IF NOT EXISTS "attributes"
|
|||||||
noteId TEXT NOT NULL,
|
noteId TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
value TEXT,
|
value TEXT,
|
||||||
|
position INT NOT NULL DEFAULT 0,
|
||||||
dateCreated TEXT NOT NULL,
|
dateCreated TEXT NOT NULL,
|
||||||
dateModified TEXT NOT NULL
|
dateModified TEXT NOT NULL,
|
||||||
|
isDeleted INT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
|
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
|
||||||
`entityName`,
|
`entityName`,
|
||||||
@@ -118,4 +120,12 @@ CREATE INDEX IDX_note_images_noteId ON note_images (noteId);
|
|||||||
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
|
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
|
||||||
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
|
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
|
||||||
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
|
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
|
||||||
CREATE UNIQUE INDEX IDX_attributes_noteId_name ON attributes (noteId, name);
|
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "api_tokens"
|
||||||
|
(
|
||||||
|
apiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
dateCreated TEXT NOT NULL,
|
||||||
|
isDeleted INT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
24
electron.js
24
electron.js
@@ -3,9 +3,11 @@
|
|||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const config = require('./src/services/config');
|
const config = require('./src/services/config');
|
||||||
|
const log = require('./src/services/log');
|
||||||
const url = require("url");
|
const url = require("url");
|
||||||
|
|
||||||
const app = electron.app;
|
const app = electron.app;
|
||||||
|
const globalShortcut = electron.globalShortcut;
|
||||||
|
|
||||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||||
require('electron-debug')();
|
require('electron-debug')();
|
||||||
@@ -13,6 +15,8 @@ require('electron-debug')();
|
|||||||
// Prevent window being garbage collected
|
// Prevent window being garbage collected
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
|
|
||||||
|
require('electron-dl')({ saveAs: true });
|
||||||
|
|
||||||
function onClosed() {
|
function onClosed() {
|
||||||
// Dereference the window
|
// Dereference the window
|
||||||
// For multiple windows store them in an array
|
// For multiple windows store them in an array
|
||||||
@@ -67,6 +71,26 @@ app.on('activate', () => {
|
|||||||
|
|
||||||
app.on('ready', () => {
|
app.on('ready', () => {
|
||||||
mainWindow = createMainWindow();
|
mainWindow = createMainWindow();
|
||||||
|
|
||||||
|
const result = globalShortcut.register('CommandOrControl+Alt+P', async () => {
|
||||||
|
const date_notes = require('./src/services/date_notes');
|
||||||
|
const utils = require('./src/services/utils');
|
||||||
|
|
||||||
|
const parentNoteId = await date_notes.getDateNoteId(utils.nowDate());
|
||||||
|
|
||||||
|
// window may be hidden / not in focus
|
||||||
|
mainWindow.focus();
|
||||||
|
|
||||||
|
mainWindow.webContents.send('create-day-sub-note', parentNoteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
log.error("Could not register global shortcut CTRL+ALT+P");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('will-quit', () => {
|
||||||
|
globalShortcut.unregisterAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
require('./src/www');
|
require('./src/www');
|
||||||
|
|||||||
148
package-lock.json
generated
148
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"version": "0.4.1",
|
"version": "0.7.0-beta",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3061,19 +3061,19 @@
|
|||||||
"integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo="
|
"integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo="
|
||||||
},
|
},
|
||||||
"electron": {
|
"electron": {
|
||||||
"version": "1.8.2-beta.4",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.4.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2.tgz",
|
||||||
"integrity": "sha1-GDayBO6s6dx3Bi7Ugg/bxsvZoZU=",
|
"integrity": "sha512-0TV5Hy92g8ACnPn+PVol6a/2uk+khzmRtWxhah/FcKs6StCytm5hD14QqOdZxEdJN8HljXIVCayN/wJX+0wDiQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "8.5.9",
|
"@types/node": "8.9.4",
|
||||||
"electron-download": "3.3.0",
|
"electron-download": "3.3.0",
|
||||||
"extract-zip": "1.6.5"
|
"extract-zip": "1.6.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "8.5.9",
|
"version": "8.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz",
|
||||||
"integrity": "sha512-s+c3AjymyAccTI4hcgNFK4mToH8l+hyPDhu4LIkn71lRy56FLijGu00fyLgldjM/846Pmk9N4KFUs2P8GDs0pA=="
|
"integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3206,6 +3206,16 @@
|
|||||||
"electron-localshortcut": "3.1.0"
|
"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": {
|
"electron-download": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz",
|
||||||
@@ -3325,9 +3335,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"electron-packager": {
|
"electron-packager": {
|
||||||
"version": "10.1.1",
|
"version": "11.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-10.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-11.0.1.tgz",
|
||||||
"integrity": "sha1-MWp/ossf/CYz9YBcn8IJE8vAnZQ=",
|
"integrity": "sha1-wtH/nsqBEL6evIGCbiqSHATRIA4=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"asar": "0.14.0",
|
"asar": "0.14.0",
|
||||||
@@ -3343,13 +3353,19 @@
|
|||||||
"pify": "3.0.0",
|
"pify": "3.0.0",
|
||||||
"plist": "2.1.0",
|
"plist": "2.1.0",
|
||||||
"pruner": "0.0.7",
|
"pruner": "0.0.7",
|
||||||
"rcedit": "0.9.0",
|
"rcedit": "1.0.0",
|
||||||
"resolve": "1.4.0",
|
"resolve": "1.4.0",
|
||||||
"sanitize-filename": "1.6.1",
|
"sanitize-filename": "1.6.1",
|
||||||
"semver": "5.4.1",
|
"semver": "5.4.1",
|
||||||
"yargs-parser": "8.1.0"
|
"yargs-parser": "9.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"camelcase": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
|
||||||
|
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"electron-download": {
|
"electron-download": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz",
|
||||||
@@ -3437,6 +3453,12 @@
|
|||||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
|
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"rcedit": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-W7DNa34x/3OgWyDHsI172AG/Lr/lZ+PkavFkHj0QhhkBRcV9QTmRJE1tDKrWkx8XHPSBsmZkNv9OKue6pncLFQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"sumchecker": {
|
"sumchecker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz",
|
||||||
@@ -3456,20 +3478,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"yargs-parser": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz",
|
||||||
|
"integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"camelcase": "4.1.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"electron-prebuilt-compile": {
|
"electron-prebuilt-compile": {
|
||||||
"version": "1.8.2-beta.4",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2-beta.4.tgz",
|
"resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2.tgz",
|
||||||
"integrity": "sha512-whVdRgFEDovWSFrAsbMXIiush6RQ8IV3XhYdL59zShck4U1eXGmdkaBCy+2tlkGmUGr0fRu+S4FpUx2ebBkRhQ==",
|
"integrity": "sha512-wiDVjy8S0PA/K/TUM0lw5gzZ+SmyVVGQ0qt9iFYXHJc6t8TzDXFY3DsoK37H3A7nWnkvXvoPdpJ5/h9KbTMoAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"babel-plugin-array-includes": "2.0.3",
|
"babel-plugin-array-includes": "2.0.3",
|
||||||
"babel-plugin-transform-async-to-generator": "6.24.1",
|
"babel-plugin-transform-async-to-generator": "6.24.1",
|
||||||
"babel-preset-es2016-node5": "1.1.2",
|
"babel-preset-es2016-node5": "1.1.2",
|
||||||
"babel-preset-react": "6.24.1",
|
"babel-preset-react": "6.24.1",
|
||||||
"electron": "1.8.2-beta.4",
|
"electron": "1.8.2",
|
||||||
"electron-compile": "6.4.2",
|
"electron-compile": "6.4.2",
|
||||||
"electron-compilers": "5.9.0",
|
"electron-compilers": "5.9.0",
|
||||||
"yargs": "6.6.0"
|
"yargs": "6.6.0"
|
||||||
@@ -4353,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": {
|
"extend": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||||
@@ -5901,8 +5949,7 @@
|
|||||||
"is-plain-obj": {
|
"is-plain-obj": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
|
||||||
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
|
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"is-png": {
|
"is-png": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -7131,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": {
|
"moment": {
|
||||||
"version": "2.20.1",
|
"version": "2.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
|
||||||
@@ -7543,6 +7595,11 @@
|
|||||||
"mimic-fn": "1.1.0"
|
"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": {
|
"optimist": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
|
||||||
@@ -8370,6 +8427,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
"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": {
|
"q": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||||
@@ -8472,12 +8534,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rcedit": {
|
|
||||||
"version": "0.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-0.9.0.tgz",
|
|
||||||
"integrity": "sha1-ORDfVzRTmeKwMl9KUZAH+J5V7xw=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"read-all-stream": {
|
"read-all-stream": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
|
||||||
@@ -9171,11 +9227,18 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
|
||||||
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
|
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-plain-obj": "1.1.0"
|
"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": {
|
"source-map": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
@@ -10948,6 +11011,22 @@
|
|||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
|
"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": {
|
"unzip-response": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
|
||||||
@@ -11694,23 +11773,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"yargs-parser": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"camelcase": "4.1.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"camelcase": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
|
|
||||||
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"yauzl": {
|
"yauzl": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.5.4-beta",
|
"version": "0.9.2",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "electron.js",
|
"main": "electron.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -27,8 +27,9 @@
|
|||||||
"debug": "~3.1.0",
|
"debug": "~3.1.0",
|
||||||
"devtron": "^1.4.0",
|
"devtron": "^1.4.0",
|
||||||
"ejs": "~2.5.7",
|
"ejs": "~2.5.7",
|
||||||
"electron": "^1.8.2-beta.4",
|
"electron": "^1.8.2",
|
||||||
"electron-debug": "^1.5.0",
|
"electron-debug": "^1.5.0",
|
||||||
|
"electron-dl": "^1.11.0",
|
||||||
"electron-in-page-search": "^1.2.4",
|
"electron-in-page-search": "^1.2.4",
|
||||||
"express": "~4.16.2",
|
"express": "~4.16.2",
|
||||||
"express-promise-wrap": "^0.2.2",
|
"express-promise-wrap": "^0.2.2",
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
"jimp": "^0.2.28",
|
"jimp": "^0.2.28",
|
||||||
"moment": "^2.20.1",
|
"moment": "^2.20.1",
|
||||||
"multer": "^1.3.0",
|
"multer": "^1.3.0",
|
||||||
|
"open": "0.0.5",
|
||||||
"rand-token": "^0.4.0",
|
"rand-token": "^0.4.0",
|
||||||
"request": "^2.83.0",
|
"request": "^2.83.0",
|
||||||
"request-promise": "^4.2.2",
|
"request-promise": "^4.2.2",
|
||||||
@@ -55,13 +57,14 @@
|
|||||||
"session-file-store": "^1.1.2",
|
"session-file-store": "^1.1.2",
|
||||||
"simple-node-logger": "^0.93.30",
|
"simple-node-logger": "^0.93.30",
|
||||||
"sqlite": "^2.9.0",
|
"sqlite": "^2.9.0",
|
||||||
|
"tar-stream": "^1.5.5",
|
||||||
"unescape": "^1.0.1",
|
"unescape": "^1.0.1",
|
||||||
"ws": "^3.3.2"
|
"ws": "^3.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron-compile": "^6.4.2",
|
"electron-compile": "^6.4.2",
|
||||||
"electron-packager": "^10.1.1",
|
"electron-packager": "^11.0.1",
|
||||||
"electron-prebuilt-compile": "1.8.2-beta.4",
|
"electron-prebuilt-compile": "1.8.2",
|
||||||
"electron-rebuild": "^1.7.3",
|
"electron-rebuild": "^1.7.3",
|
||||||
"tape": "^4.8.0",
|
"tape": "^4.8.0",
|
||||||
"xo": "^0.18.0"
|
"xo": "^0.18.0"
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ require('./services/backup');
|
|||||||
// trigger consistency checks timer
|
// trigger consistency checks timer
|
||||||
require('./services/consistency_checks');
|
require('./services/consistency_checks');
|
||||||
|
|
||||||
require('./plugins/reddit');
|
require('./services/scheduler');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
app,
|
app,
|
||||||
|
|||||||
@@ -23,10 +23,53 @@ class Note extends Entity {
|
|||||||
return this.type === "code" && this.mime === "application/json";
|
return this.type === "code" && this.mime === "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAttributes() {
|
isJavaScript() {
|
||||||
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ?", [this.noteId]);
|
return (this.type === "code" || this.type === "file")
|
||||||
|
&& (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isHtml() {
|
||||||
|
return (this.type === "code" || this.type === "file") && this.mime === "text/html";
|
||||||
|
}
|
||||||
|
|
||||||
|
getScriptEnv() {
|
||||||
|
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
|
||||||
|
return "frontend";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === 'render') {
|
||||||
|
return "frontend";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
|
||||||
|
return "backend";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAttributes() {
|
||||||
|
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: this doesn't take into account the possibility to have multi-valued attributes!
|
||||||
|
async getAttributeMap() {
|
||||||
|
const map = {};
|
||||||
|
|
||||||
|
for (const attr of await this.getAttributes()) {
|
||||||
|
map[attr.name] = attr.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasAttribute(name) {
|
||||||
|
const map = await this.getAttributeMap();
|
||||||
|
|
||||||
|
return map.hasOwnProperty(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: this doesn't take into account the possibility to have multi-valued attributes!
|
||||||
async getAttribute(name) {
|
async getAttribute(name) {
|
||||||
return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]);
|
return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]);
|
||||||
}
|
}
|
||||||
@@ -39,6 +82,49 @@ class Note extends Entity {
|
|||||||
return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
|
return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChild(name) {
|
||||||
|
return this.repository.getEntity(`
|
||||||
|
SELECT notes.*
|
||||||
|
FROM note_tree
|
||||||
|
JOIN notes USING(noteId)
|
||||||
|
WHERE notes.isDeleted = 0
|
||||||
|
AND note_tree.isDeleted = 0
|
||||||
|
AND note_tree.parentNoteId = ?
|
||||||
|
AND notes.title = ?`, [this.noteId, name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChildren() {
|
||||||
|
return this.repository.getEntities(`
|
||||||
|
SELECT notes.*
|
||||||
|
FROM note_tree
|
||||||
|
JOIN notes USING(noteId)
|
||||||
|
WHERE notes.isDeleted = 0
|
||||||
|
AND note_tree.isDeleted = 0
|
||||||
|
AND note_tree.parentNoteId = ?
|
||||||
|
ORDER BY note_tree.notePosition`, [this.noteId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getParents() {
|
||||||
|
return this.repository.getEntities(`
|
||||||
|
SELECT parent_notes.*
|
||||||
|
FROM
|
||||||
|
note_tree AS child_tree
|
||||||
|
JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId
|
||||||
|
WHERE child_tree.noteId = ?
|
||||||
|
AND child_tree.isDeleted = 0
|
||||||
|
AND parent_notes.isDeleted = 0`, [this.noteId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNoteTree() {
|
||||||
|
return this.repository.getEntities(`
|
||||||
|
SELECT note_tree.*
|
||||||
|
FROM note_tree
|
||||||
|
JOIN notes USING(noteId)
|
||||||
|
WHERE notes.isDeleted = 0
|
||||||
|
AND note_tree.isDeleted = 0
|
||||||
|
AND note_tree.noteId = ?`, [this.noteId]);
|
||||||
|
}
|
||||||
|
|
||||||
beforeSaving() {
|
beforeSaving() {
|
||||||
this.content = JSON.stringify(this.jsonContent, null, '\t');
|
this.content = JSON.stringify(this.jsonContent, null, '\t');
|
||||||
|
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const sql = require('../services/sql');
|
|
||||||
const notes = require('../services/notes');
|
|
||||||
const axios = require('axios');
|
|
||||||
const log = require('../services/log');
|
|
||||||
const utils = require('../services/utils');
|
|
||||||
const unescape = require('unescape');
|
|
||||||
const attributes = require('../services/attributes');
|
|
||||||
const sync_mutex = require('../services/sync_mutex');
|
|
||||||
const config = require('../services/config');
|
|
||||||
const date_notes = require('../services/date_notes');
|
|
||||||
|
|
||||||
// "reddit" date note is subnote of date note which contains all reddit comments from that date
|
|
||||||
const REDDIT_DATE_ATTRIBUTE = 'reddit_date_note';
|
|
||||||
|
|
||||||
async function createNote(parentNoteId, noteTitle, noteText) {
|
|
||||||
return (await notes.createNewNote(parentNoteId, {
|
|
||||||
title: noteTitle,
|
|
||||||
content: noteText,
|
|
||||||
target: 'into',
|
|
||||||
isProtected: false
|
|
||||||
})).noteId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function redditId(kind, id) {
|
|
||||||
return kind + "_" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) {
|
|
||||||
const dateStr = dateTimeStr.substr(0, 10);
|
|
||||||
|
|
||||||
let redditDateNoteId = await attributes.getNoteIdWithAttribute(REDDIT_DATE_ATTRIBUTE, dateStr);
|
|
||||||
|
|
||||||
if (!redditDateNoteId) {
|
|
||||||
const dateNoteId = await date_notes.getDateNoteId(dateTimeStr, rootNoteId);
|
|
||||||
|
|
||||||
redditDateNoteId = await createNote(dateNoteId, "Reddit");
|
|
||||||
|
|
||||||
await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redditDateNoteId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importComments(rootNoteId, accountName, afterId = null) {
|
|
||||||
let url = `https://www.reddit.com/user/${accountName}.json`;
|
|
||||||
|
|
||||||
if (afterId) {
|
|
||||||
url += "?after=" + afterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.get(url);
|
|
||||||
const listing = response.data;
|
|
||||||
|
|
||||||
if (listing.kind !== 'Listing') {
|
|
||||||
log.info(`Reddit: Unknown object kind ${listing.kind}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const children = listing.data.children;
|
|
||||||
|
|
||||||
let importedComments = 0;
|
|
||||||
|
|
||||||
for (const child of children) {
|
|
||||||
const comment = child.data;
|
|
||||||
|
|
||||||
let commentNoteId = await attributes.getNoteIdWithAttribute('reddit_id', redditId(child.kind, comment.id));
|
|
||||||
|
|
||||||
if (commentNoteId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateTimeStr = utils.dateStr(new Date(comment.created_utc * 1000));
|
|
||||||
|
|
||||||
const permaLink = 'https://reddit.com' + comment.permalink;
|
|
||||||
|
|
||||||
const noteText =
|
|
||||||
`<p><a href="${permaLink}">${permaLink}</a></p>
|
|
||||||
<p>author: <a href="https://reddit.com/u/${comment.author}">${comment.author}</a>,
|
|
||||||
subreddit: <a href="https://reddit.com/r/${comment.subreddit}">${comment.subreddit}</a>,
|
|
||||||
karma: ${comment.score}, created at ${dateTimeStr}</p><p></p>`
|
|
||||||
+ unescape(comment.body_html);
|
|
||||||
|
|
||||||
let parentNoteId = await getDateNoteIdForReddit(dateTimeStr, rootNoteId);
|
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
|
||||||
commentNoteId = await createNote(parentNoteId, comment.link_title, noteText);
|
|
||||||
|
|
||||||
log.info("Reddit: Imported comment to note " + commentNoteId);
|
|
||||||
importedComments++;
|
|
||||||
|
|
||||||
await attributes.createAttribute(commentNoteId, "reddit_kind", child.kind);
|
|
||||||
await attributes.createAttribute(commentNoteId, "reddit_id", redditId(child.kind, comment.id));
|
|
||||||
await attributes.createAttribute(commentNoteId, "reddit_created_utc", comment.created_utc);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there have been no imported comments on this page, there shouldn't be any to import
|
|
||||||
// on the next page since those are older
|
|
||||||
if (listing.data.after && importedComments > 0) {
|
|
||||||
importedComments += await importComments(rootNoteId, accountName, listing.data.after);
|
|
||||||
}
|
|
||||||
|
|
||||||
return importedComments;
|
|
||||||
}
|
|
||||||
|
|
||||||
let redditAccounts = [];
|
|
||||||
|
|
||||||
async function runImport() {
|
|
||||||
const rootNoteId = await date_notes.getRootNoteId();
|
|
||||||
|
|
||||||
// technically mutex shouldn't be necessary but we want to avoid doing potentially expensive import
|
|
||||||
// concurrently with sync
|
|
||||||
await sync_mutex.doExclusively(async () => {
|
|
||||||
let importedComments = 0;
|
|
||||||
|
|
||||||
for (const account of redditAccounts) {
|
|
||||||
importedComments += await importComments(rootNoteId, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(`Reddit: Imported ${importedComments} comments.`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sql.dbReady.then(async () => {
|
|
||||||
if (!config['Reddit'] || config['Reddit']['enabled'] !== true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const redditAccountsStr = config['Reddit']['accounts'];
|
|
||||||
|
|
||||||
if (!redditAccountsStr) {
|
|
||||||
log.info("Reddit: No reddit accounts defined in option 'reddit_accounts'");
|
|
||||||
}
|
|
||||||
|
|
||||||
redditAccounts = redditAccountsStr.split(",").map(s => s.trim());
|
|
||||||
|
|
||||||
const pollingIntervalInSeconds = config['Reddit']['pollingIntervalInSeconds'] || (4 * 3600);
|
|
||||||
|
|
||||||
setInterval(runImport, pollingIntervalInSeconds * 1000);
|
|
||||||
setTimeout(runImport, 10000); // 10 seconds after startup - intentionally after initial sync
|
|
||||||
});
|
|
||||||
BIN
src/public/images/icons/paperclip.png
Normal file
BIN
src/public/images/icons/paperclip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 B |
BIN
src/public/images/icons/play.png
Normal file
BIN
src/public/images/icons/play.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 252 B |
@@ -1,5 +1,27 @@
|
|||||||
const api = (function() {
|
function ScriptContext(startNote, allNotes) {
|
||||||
const pluginButtonsEl = $("#plugin-buttons");
|
const modules = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
modules: modules,
|
||||||
|
notes: toObject(allNotes, note => [note.noteId, note]),
|
||||||
|
apis: toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]),
|
||||||
|
require: moduleNoteIds => {
|
||||||
|
return moduleName => {
|
||||||
|
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||||
|
const note = candidates.find(c => c.title === moduleName);
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
throw new Error("Could not find module note " + moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modules[note.noteId].exports;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScriptApi(startNote, currentNote) {
|
||||||
|
const $pluginButtons = $("#plugin-buttons");
|
||||||
|
|
||||||
async function activateNote(notePath) {
|
async function activateNote(notePath) {
|
||||||
await noteTree.activateNode(notePath);
|
await noteTree.activateNode(notePath);
|
||||||
@@ -10,12 +32,45 @@ const api = (function() {
|
|||||||
|
|
||||||
button.attr('id', buttonId);
|
button.attr('id', buttonId);
|
||||||
|
|
||||||
pluginButtonsEl.append(button);
|
$pluginButtons.append(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareParams(params) {
|
||||||
|
if (!params) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.map(p => {
|
||||||
|
if (typeof p === "function") {
|
||||||
|
return "!@#Function: " + p.toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runOnServer(script, params = []) {
|
||||||
|
if (typeof script === "function") {
|
||||||
|
script = script.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = await server.post('script/exec', {
|
||||||
|
script: script,
|
||||||
|
params: prepareParams(params),
|
||||||
|
startNoteId: startNote.noteId,
|
||||||
|
currentNoteId: currentNote.noteId
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret.executionResult;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
startNote: startNote,
|
||||||
|
currentNote: currentNote,
|
||||||
addButtonToToolbar,
|
addButtonToToolbar,
|
||||||
activateNote
|
activateNote,
|
||||||
|
getInstanceName: noteTree.getInstanceName,
|
||||||
|
runOnServer
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const contextMenu = (function() {
|
const contextMenu = (function() {
|
||||||
const treeEl = $("#tree");
|
const $tree = $("#tree");
|
||||||
|
|
||||||
let clipboardIds = [];
|
let clipboardIds = [];
|
||||||
let clipboardMode = null;
|
let clipboardMode = null;
|
||||||
@@ -85,16 +85,19 @@ const contextMenu = (function() {
|
|||||||
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
|
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
|
||||||
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
|
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
|
||||||
{title: "----"},
|
{title: "----"},
|
||||||
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"},
|
{title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"},
|
||||||
{title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"},
|
{title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"},
|
||||||
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
|
{title: "----"},
|
||||||
|
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapseSubTree", uiIcon: "ui-icon-minus"},
|
||||||
|
{title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
|
||||||
|
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
|
||||||
|
|
||||||
],
|
],
|
||||||
beforeOpen: (event, ui) => {
|
beforeOpen: (event, ui) => {
|
||||||
const node = $.ui.fancytree.getNode(ui.target);
|
const node = $.ui.fancytree.getNode(ui.target);
|
||||||
// Modify menu entries depending on node status
|
// Modify menu entries depending on node status
|
||||||
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
|
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
|
||||||
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
|
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
|
||||||
|
|
||||||
// Activate node on right-click
|
// Activate node on right-click
|
||||||
node.setActive();
|
node.setActive();
|
||||||
@@ -139,13 +142,19 @@ const contextMenu = (function() {
|
|||||||
else if (ui.cmd === "delete") {
|
else if (ui.cmd === "delete") {
|
||||||
treeChanges.deleteNodes(noteTree.getSelectedNodes(true));
|
treeChanges.deleteNodes(noteTree.getSelectedNodes(true));
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "collapse-sub-tree") {
|
else if (ui.cmd === "exportSubTree") {
|
||||||
|
exportSubTree(node.data.noteId);
|
||||||
|
}
|
||||||
|
else if (ui.cmd === "importSubTree") {
|
||||||
|
importSubTree(node.data.noteId);
|
||||||
|
}
|
||||||
|
else if (ui.cmd === "collapseSubTree") {
|
||||||
noteTree.collapseTree(node);
|
noteTree.collapseTree(node);
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "force-note-sync") {
|
else if (ui.cmd === "forceNoteSync") {
|
||||||
forceNoteSync(node.data.noteId);
|
forceNoteSync(node.data.noteId);
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "sort-alphabetically") {
|
else if (ui.cmd === "sortAlphabetically") {
|
||||||
noteTree.sortAlphabetically(node.data.noteId);
|
noteTree.sortAlphabetically(node.data.noteId);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const addLink = (function() {
|
const addLink = (function() {
|
||||||
const dialogEl = $("#add-link-dialog");
|
const $dialog = $("#add-link-dialog");
|
||||||
const formEl = $("#add-link-form");
|
const $form = $("#add-link-form");
|
||||||
const autoCompleteEl = $("#note-autocomplete");
|
const $autoComplete = $("#note-autocomplete");
|
||||||
const linkTitleEl = $("#link-title");
|
const $linkTitle = $("#link-title");
|
||||||
const clonePrefixEl = $("#clone-prefix");
|
const $clonePrefix = $("#clone-prefix");
|
||||||
const linkTitleFormGroup = $("#add-link-title-form-group");
|
const $linkTitleFormGroup = $("#add-link-title-form-group");
|
||||||
const prefixFormGroup = $("#add-link-prefix-form-group");
|
const $prefixFormGroup = $("#add-link-prefix-form-group");
|
||||||
const linkTypeEls = $("input[name='add-link-type']");
|
const $linkTypes = $("input[name='add-link-type']");
|
||||||
const linkTypeHtmlEl = linkTypeEls.filter('input[value="html"]');
|
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
|
||||||
|
|
||||||
function setLinkType(linkType) {
|
function setLinkType(linkType) {
|
||||||
linkTypeEls.each(function () {
|
$linkTypes.each(function () {
|
||||||
$(this).prop('checked', $(this).val() === linkType);
|
$(this).prop('checked', $(this).val() === linkType);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,39 +20,39 @@ const addLink = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showDialog() {
|
function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
if (noteEditor.getCurrentNoteType() === 'text') {
|
if (noteEditor.getCurrentNoteType() === 'text') {
|
||||||
linkTypeHtmlEl.prop('disabled', false);
|
$linkTypeHtml.prop('disabled', false);
|
||||||
|
|
||||||
setLinkType('html');
|
setLinkType('html');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
linkTypeHtmlEl.prop('disabled', true);
|
$linkTypeHtml.prop('disabled', true);
|
||||||
|
|
||||||
setLinkType('selected-to-current');
|
setLinkType('selected-to-current');
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 700
|
width: 700
|
||||||
});
|
});
|
||||||
|
|
||||||
autoCompleteEl.val('').focus();
|
$autoComplete.val('').focus();
|
||||||
clonePrefixEl.val('');
|
$clonePrefix.val('');
|
||||||
linkTitleEl.val('');
|
$linkTitle.val('');
|
||||||
|
|
||||||
function setDefaultLinkTitle(noteId) {
|
function setDefaultLinkTitle(noteId) {
|
||||||
const noteTitle = noteTree.getNoteTitle(noteId);
|
const noteTitle = noteTree.getNoteTitle(noteId);
|
||||||
|
|
||||||
linkTitleEl.val(noteTitle);
|
$linkTitle.val(noteTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
autoCompleteEl.autocomplete({
|
$autoComplete.autocomplete({
|
||||||
source: noteTree.getAutocompleteItems(),
|
source: noteTree.getAutocompleteItems(),
|
||||||
minLength: 0,
|
minLength: 0,
|
||||||
change: () => {
|
change: () => {
|
||||||
const val = autoCompleteEl.val();
|
const val = $autoComplete.val();
|
||||||
const notePath = link.getNodePathFromLabel(val);
|
const notePath = link.getNodePathFromLabel(val);
|
||||||
if (!notePath) {
|
if (!notePath) {
|
||||||
return;
|
return;
|
||||||
@@ -75,8 +75,8 @@ const addLink = (function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formEl.submit(() => {
|
$form.submit(() => {
|
||||||
const value = autoCompleteEl.val();
|
const value = $autoComplete.val();
|
||||||
|
|
||||||
const notePath = link.getNodePathFromLabel(value);
|
const notePath = link.getNodePathFromLabel(value);
|
||||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||||
@@ -85,25 +85,25 @@ const addLink = (function() {
|
|||||||
const linkType = $("input[name='add-link-type']:checked").val();
|
const linkType = $("input[name='add-link-type']:checked").val();
|
||||||
|
|
||||||
if (linkType === 'html') {
|
if (linkType === 'html') {
|
||||||
const linkTitle = linkTitleEl.val();
|
const linkTitle = $linkTitle.val();
|
||||||
|
|
||||||
dialogEl.dialog("close");
|
$dialog.dialog("close");
|
||||||
|
|
||||||
link.addLinkToEditor(linkTitle, '#' + notePath);
|
link.addLinkToEditor(linkTitle, '#' + notePath);
|
||||||
}
|
}
|
||||||
else if (linkType === 'selected-to-current') {
|
else if (linkType === 'selected-to-current') {
|
||||||
const prefix = clonePrefixEl.val();
|
const prefix = $clonePrefix.val();
|
||||||
|
|
||||||
cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
|
cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
|
||||||
|
|
||||||
dialogEl.dialog("close");
|
$dialog.dialog("close");
|
||||||
}
|
}
|
||||||
else if (linkType === 'current-to-selected') {
|
else if (linkType === 'current-to-selected') {
|
||||||
const prefix = clonePrefixEl.val();
|
const prefix = $clonePrefix.val();
|
||||||
|
|
||||||
cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
|
cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
|
||||||
|
|
||||||
dialogEl.dialog("close");
|
$dialog.dialog("close");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,19 +111,19 @@ const addLink = (function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function linkTypeChanged() {
|
function linkTypeChanged() {
|
||||||
const value = linkTypeEls.filter(":checked").val();
|
const value = $linkTypes.filter(":checked").val();
|
||||||
|
|
||||||
if (value === 'html') {
|
if (value === 'html') {
|
||||||
linkTitleFormGroup.show();
|
$linkTitleFormGroup.show();
|
||||||
prefixFormGroup.hide();
|
$prefixFormGroup.hide();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
linkTitleFormGroup.hide();
|
$linkTitleFormGroup.hide();
|
||||||
prefixFormGroup.show();
|
$prefixFormGroup.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
linkTypeEls.change(linkTypeChanged);
|
$linkTypes.change(linkTypeChanged);
|
||||||
|
|
||||||
$(document).bind('keydown', 'ctrl+l', e => {
|
$(document).bind('keydown', 'ctrl+l', e => {
|
||||||
showDialog();
|
showDialog();
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const attributesDialog = (function() {
|
const attributesDialog = (function() {
|
||||||
const dialogEl = $("#attributes-dialog");
|
const $dialog = $("#attributes-dialog");
|
||||||
|
const $saveAttributesButton = $("#save-attributes-button");
|
||||||
|
const $attributesBody = $('#attributes-table tbody');
|
||||||
|
|
||||||
const attributesModel = new AttributesModel();
|
const attributesModel = new AttributesModel();
|
||||||
|
let attributeNames = [];
|
||||||
|
|
||||||
function AttributesModel() {
|
function AttributesModel() {
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -14,38 +18,148 @@ const attributesDialog = (function() {
|
|||||||
|
|
||||||
const attributes = await server.get('notes/' + noteId + '/attributes');
|
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||||
|
|
||||||
this.attributes(attributes);
|
self.attributes(attributes.map(ko.observable));
|
||||||
};
|
|
||||||
|
|
||||||
this.addNewRow = function() {
|
addLastEmptyRow();
|
||||||
self.attributes.push({
|
|
||||||
attributeId: '',
|
attributeNames = await server.get('attributes/names');
|
||||||
name: '',
|
|
||||||
value: ''
|
// attribute might not be rendered immediatelly so could not focus
|
||||||
|
setTimeout(() => $(".attribute-name:last").focus(), 100);
|
||||||
|
|
||||||
|
$attributesBody.sortable({
|
||||||
|
handle: '.handle',
|
||||||
|
containment: $attributesBody,
|
||||||
|
update: function() {
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
// we need to update positions by searching in the DOM, because order of the
|
||||||
|
// attributes in the viewmodel (self.attributes()) stays the same
|
||||||
|
$attributesBody.find('input[name="position"]').each(function() {
|
||||||
|
const attr = self.getTargetAttribute(this);
|
||||||
|
|
||||||
|
attr().position = position++;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.deleteAttribute = function(data, event) {
|
||||||
|
const attr = self.getTargetAttribute(event.target);
|
||||||
|
const attrData = attr();
|
||||||
|
|
||||||
|
if (attrData) {
|
||||||
|
attrData.isDeleted = 1;
|
||||||
|
|
||||||
|
attr(attrData);
|
||||||
|
|
||||||
|
addLastEmptyRow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function isValid() {
|
||||||
|
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
|
||||||
|
if (self.isEmptyName(i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
this.save = async function() {
|
this.save = async function() {
|
||||||
|
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||||
|
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||||
|
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||||
|
$saveAttributesButton.focus();
|
||||||
|
|
||||||
|
if (!isValid()) {
|
||||||
|
alert("Please fix all validation errors and try saving again.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const noteId = noteEditor.getCurrentNoteId();
|
const noteId = noteEditor.getCurrentNoteId();
|
||||||
|
|
||||||
const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes());
|
const attributesToSave = self.attributes()
|
||||||
|
.map(attr => attr())
|
||||||
|
.filter(attr => attr.attributeId !== "" || attr.name !== "");
|
||||||
|
|
||||||
self.attributes(attributes);
|
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
|
||||||
|
|
||||||
|
self.attributes(attributes.map(ko.observable));
|
||||||
|
|
||||||
|
addLastEmptyRow();
|
||||||
|
|
||||||
showMessage("Attributes have been saved.");
|
showMessage("Attributes have been saved.");
|
||||||
|
|
||||||
|
noteEditor.loadAttributeList();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function addLastEmptyRow() {
|
||||||
|
const attrs = self.attributes().filter(attr => attr().isDeleted === 0);
|
||||||
|
const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
|
||||||
|
|
||||||
|
if (!last || last.name.trim() !== "" || last.value !== "") {
|
||||||
|
self.attributes.push(ko.observable({
|
||||||
|
attributeId: '',
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
isDeleted: 0,
|
||||||
|
position: 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attributeChanged = function (data, event) {
|
||||||
|
addLastEmptyRow();
|
||||||
|
|
||||||
|
const attr = self.getTargetAttribute(event.target);
|
||||||
|
|
||||||
|
attr.valueHasMutated();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isNotUnique = function(index) {
|
||||||
|
const cur = self.attributes()[index]();
|
||||||
|
|
||||||
|
if (cur.name.trim() === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
|
||||||
|
const attr = attrs[i]();
|
||||||
|
|
||||||
|
if (index !== i && cur.name === attr.name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isEmptyName = function(index) {
|
||||||
|
const cur = self.attributes()[index]();
|
||||||
|
|
||||||
|
return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== "");
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getTargetAttribute = function(target) {
|
||||||
|
const context = ko.contextFor(target);
|
||||||
|
const index = context.$index();
|
||||||
|
|
||||||
|
return self.attributes()[index];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showDialog() {
|
async function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
dialogEl.dialog({
|
await attributesModel.loadAttributes();
|
||||||
|
|
||||||
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 500
|
height: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
attributesModel.loadAttributes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).bind('keydown', 'alt+a', e => {
|
$(document).bind('keydown', 'alt+a', e => {
|
||||||
@@ -56,6 +170,54 @@ const attributesDialog = (function() {
|
|||||||
|
|
||||||
ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
|
ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
|
||||||
|
|
||||||
|
$(document).on('focus', '.attribute-name', function (e) {
|
||||||
|
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||||
|
$(this).autocomplete({
|
||||||
|
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||||
|
// because we have overriden filter() function in init.js
|
||||||
|
source: attributeNames.map(attr => {
|
||||||
|
return {
|
||||||
|
label: attr,
|
||||||
|
value: attr
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
minLength: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).autocomplete("search", $(this).val());
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('focus', '.attribute-value', async function (e) {
|
||||||
|
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||||
|
const attributeName = $(this).parent().parent().find('.attribute-name').val();
|
||||||
|
|
||||||
|
if (attributeName.trim() === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
|
||||||
|
|
||||||
|
if (attributeValues.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).autocomplete({
|
||||||
|
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||||
|
// because we have overriden filter() function in init.js
|
||||||
|
source: attributeValues.map(attr => {
|
||||||
|
return {
|
||||||
|
label: attr,
|
||||||
|
value: attr
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
minLength: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).autocomplete("search", $(this).val());
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showDialog
|
showDialog
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const editTreePrefix = (function() {
|
const editTreePrefix = (function() {
|
||||||
const dialogEl = $("#edit-tree-prefix-dialog");
|
const $dialog = $("#edit-tree-prefix-dialog");
|
||||||
const formEl = $("#edit-tree-prefix-form");
|
const $form = $("#edit-tree-prefix-form");
|
||||||
const treePrefixInputEl = $("#tree-prefix-input");
|
const $treePrefixInput = $("#tree-prefix-input");
|
||||||
const noteTitleEl = $('#tree-prefix-note-title');
|
const $noteTitle = $('#tree-prefix-note-title');
|
||||||
|
|
||||||
let noteTreeId;
|
let noteTreeId;
|
||||||
|
|
||||||
async function showDialog() {
|
async function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
await dialogEl.dialog({
|
await $dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 500
|
width: 500
|
||||||
});
|
});
|
||||||
@@ -20,21 +20,21 @@ const editTreePrefix = (function() {
|
|||||||
|
|
||||||
noteTreeId = currentNode.data.noteTreeId;
|
noteTreeId = currentNode.data.noteTreeId;
|
||||||
|
|
||||||
treePrefixInputEl.val(currentNode.data.prefix).focus();
|
$treePrefixInput.val(currentNode.data.prefix).focus();
|
||||||
|
|
||||||
const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId);
|
const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId);
|
||||||
|
|
||||||
noteTitleEl.html(noteTitle);
|
$noteTitle.html(noteTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
formEl.submit(() => {
|
$form.submit(() => {
|
||||||
const prefix = treePrefixInputEl.val();
|
const prefix = $treePrefixInput.val();
|
||||||
|
|
||||||
server.put('tree/' + noteTreeId + '/set-prefix', {
|
server.put('tree/' + noteTreeId + '/set-prefix', {
|
||||||
prefix: prefix
|
prefix: prefix
|
||||||
}).then(() => noteTree.setPrefix(noteTreeId, prefix));
|
}).then(() => noteTree.setPrefix(noteTreeId, prefix));
|
||||||
|
|
||||||
dialogEl.dialog("close");
|
$dialog.dialog("close");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const eventLog = (function() {
|
const eventLog = (function() {
|
||||||
const dialogEl = $("#event-log-dialog");
|
const $dialog = $("#event-log-dialog");
|
||||||
const listEl = $("#event-log-list");
|
const $list = $("#event-log-list");
|
||||||
|
|
||||||
async function showDialog() {
|
async function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 700
|
height: 700
|
||||||
@@ -15,7 +15,7 @@ const eventLog = (function() {
|
|||||||
|
|
||||||
const result = await server.get('event-log');
|
const result = await server.get('event-log');
|
||||||
|
|
||||||
listEl.html('');
|
$list.html('');
|
||||||
|
|
||||||
for (const event of result) {
|
for (const event of result) {
|
||||||
const dateTime = formatDateTime(parseDate(event.dateAdded));
|
const dateTime = formatDateTime(parseDate(event.dateAdded));
|
||||||
@@ -28,7 +28,7 @@ const eventLog = (function() {
|
|||||||
|
|
||||||
const eventEl = $('<li>').html(dateTime + " - " + event.comment);
|
const eventEl = $('<li>').html(dateTime + " - " + event.comment);
|
||||||
|
|
||||||
listEl.append(eventEl);
|
$list.append(eventEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const jumpToNote = (function() {
|
const jumpToNote = (function() {
|
||||||
const dialogEl = $("#jump-to-note-dialog");
|
const $dialog = $("#jump-to-note-dialog");
|
||||||
const autoCompleteEl = $("#jump-to-note-autocomplete");
|
const $autoComplete = $("#jump-to-note-autocomplete");
|
||||||
const formEl = $("#jump-to-note-form");
|
const $form = $("#jump-to-note-form");
|
||||||
|
|
||||||
async function showDialog() {
|
async function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
autoCompleteEl.val('');
|
$autoComplete.val('');
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800
|
width: 800
|
||||||
});
|
});
|
||||||
|
|
||||||
await autoCompleteEl.autocomplete({
|
await $autoComplete.autocomplete({
|
||||||
source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems),
|
source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems),
|
||||||
minLength: 0
|
minLength: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedNotePath() {
|
function getSelectedNotePath() {
|
||||||
const val = autoCompleteEl.val();
|
const val = $autoComplete.val();
|
||||||
return link.getNodePathFromLabel(val);
|
return link.getNodePathFromLabel(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ const jumpToNote = (function() {
|
|||||||
if (notePath) {
|
if (notePath) {
|
||||||
noteTree.activateNode(notePath);
|
noteTree.activateNode(notePath);
|
||||||
|
|
||||||
dialogEl.dialog('close');
|
$dialog.dialog('close');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ const jumpToNote = (function() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
formEl.submit(() => {
|
$form.submit(() => {
|
||||||
const action = dialogEl.find("button:focus").val();
|
const action = $dialog.find("button:focus").val();
|
||||||
|
|
||||||
goToNote();
|
goToNote();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const noteHistory = (function() {
|
const noteHistory = (function() {
|
||||||
const dialogEl = $("#note-history-dialog");
|
const $dialog = $("#note-history-dialog");
|
||||||
const listEl = $("#note-history-list");
|
const $list = $("#note-history-list");
|
||||||
const contentEl = $("#note-history-content");
|
const $content = $("#note-history-content");
|
||||||
const titleEl = $("#note-history-title");
|
const $title = $("#note-history-title");
|
||||||
|
|
||||||
let historyItems = [];
|
let historyItems = [];
|
||||||
|
|
||||||
@@ -13,23 +13,23 @@ const noteHistory = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function showNoteHistoryDialog(noteId, noteRevisionId) {
|
async function showNoteHistoryDialog(noteId, noteRevisionId) {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 700
|
height: 700
|
||||||
});
|
});
|
||||||
|
|
||||||
listEl.empty();
|
$list.empty();
|
||||||
contentEl.empty();
|
$content.empty();
|
||||||
|
|
||||||
historyItems = await server.get('notes-history/' + noteId);
|
historyItems = await server.get('notes-history/' + noteId);
|
||||||
|
|
||||||
for (const item of historyItems) {
|
for (const item of historyItems) {
|
||||||
const dateModified = parseDate(item.dateModifiedFrom);
|
const dateModified = parseDate(item.dateModifiedFrom);
|
||||||
|
|
||||||
listEl.append($('<option>', {
|
$list.append($('<option>', {
|
||||||
value: item.noteRevisionId,
|
value: item.noteRevisionId,
|
||||||
text: formatDateTime(dateModified)
|
text: formatDateTime(dateModified)
|
||||||
}));
|
}));
|
||||||
@@ -37,13 +37,13 @@ const noteHistory = (function() {
|
|||||||
|
|
||||||
if (historyItems.length > 0) {
|
if (historyItems.length > 0) {
|
||||||
if (!noteRevisionId) {
|
if (!noteRevisionId) {
|
||||||
noteRevisionId = listEl.find("option:first").val();
|
noteRevisionId = $list.find("option:first").val();
|
||||||
}
|
}
|
||||||
|
|
||||||
listEl.val(noteRevisionId).trigger('change');
|
$list.val(noteRevisionId).trigger('change');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
titleEl.text("No history for this note yet...");
|
$title.text("No history for this note yet...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +53,13 @@ const noteHistory = (function() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
listEl.on('change', () => {
|
$list.on('change', () => {
|
||||||
const optVal = listEl.find(":selected").val();
|
const optVal = $list.find(":selected").val();
|
||||||
|
|
||||||
const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
|
const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
|
||||||
|
|
||||||
titleEl.html(historyItem.title);
|
$title.html(historyItem.title);
|
||||||
contentEl.html(historyItem.content);
|
$content.html(historyItem.content);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on('click', "a[action='note-history']", event => {
|
$(document).on('click', "a[action='note-history']", event => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const noteSource = (function() {
|
const noteSource = (function() {
|
||||||
const dialogEl = $("#note-source-dialog");
|
const $dialog = $("#note-source-dialog");
|
||||||
const noteSourceEl = $("#note-source");
|
const $noteSource = $("#note-source");
|
||||||
|
|
||||||
function showDialog() {
|
function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 500
|
height: 500
|
||||||
@@ -15,7 +15,7 @@ const noteSource = (function() {
|
|||||||
|
|
||||||
const noteText = noteEditor.getCurrentNote().detail.content;
|
const noteText = noteEditor.getCurrentNote().detail.content;
|
||||||
|
|
||||||
noteSourceEl.text(formatHtml(noteText));
|
$noteSource.text(formatHtml(noteText));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHtml(str) {
|
function formatHtml(str) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const recentChanges = (function() {
|
const recentChanges = (function() {
|
||||||
const dialogEl = $("#recent-changes-dialog");
|
const $dialog = $("#recent-changes-dialog");
|
||||||
|
|
||||||
async function showDialog() {
|
async function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 700
|
height: 700
|
||||||
@@ -14,7 +14,7 @@ const recentChanges = (function() {
|
|||||||
|
|
||||||
const result = await server.get('recent-changes/');
|
const result = await server.get('recent-changes/');
|
||||||
|
|
||||||
dialogEl.html('');
|
$dialog.html('');
|
||||||
|
|
||||||
const groupedByDate = groupByDate(result);
|
const groupedByDate = groupByDate(result);
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ const recentChanges = (function() {
|
|||||||
.append(' (').append(revLink).append(')'));
|
.append(' (').append(revLink).append(')'));
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogEl.append(dayEl);
|
$dialog.append(dayEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const recentNotes = (function() {
|
const recentNotes = (function() {
|
||||||
const dialogEl = $("#recent-notes-dialog");
|
const $dialog = $("#recent-notes-dialog");
|
||||||
const selectBoxEl = $('#recent-notes-select-box');
|
const $searchInput = $('#recent-notes-search-input');
|
||||||
const jumpToButtonEl = $('#recent-notes-jump-to');
|
|
||||||
const addLinkButtonEl = $('#recent-notes-add-link');
|
|
||||||
const addCurrentAsChildEl = $("#recent-notes-add-current-as-child");
|
|
||||||
const addRecentAsChildEl = $("#recent-notes-add-recent-as-child");
|
|
||||||
const noteDetailEl = $('#note-detail');
|
|
||||||
// list of recent note paths
|
// list of recent note paths
|
||||||
let list = [];
|
let list = [];
|
||||||
|
|
||||||
@@ -29,98 +25,67 @@ const recentNotes = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showDialog() {
|
function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800
|
width: 800,
|
||||||
|
height: 100,
|
||||||
|
position: { my: "center top+100", at: "top", of: window }
|
||||||
});
|
});
|
||||||
|
|
||||||
selectBoxEl.find('option').remove();
|
$searchInput.val('');
|
||||||
|
|
||||||
// remove the current note
|
// remove the current note
|
||||||
const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath());
|
const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath());
|
||||||
|
|
||||||
$.each(recNotes, (key, valueNotePath) => {
|
$searchInput.autocomplete({
|
||||||
const noteTitle = noteTree.getNotePathTitle(valueNotePath);
|
source: recNotes.map(notePath => {
|
||||||
|
let noteTitle;
|
||||||
|
|
||||||
const option = $("<option></option>")
|
try {
|
||||||
.attr("value", valueNotePath)
|
noteTitle = noteTree.getNotePathTitle(notePath);
|
||||||
.text(noteTitle);
|
}
|
||||||
|
catch (e) {
|
||||||
|
noteTitle = "[error - can't find note title]";
|
||||||
|
|
||||||
// select the first one (most recent one) by default
|
messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
|
||||||
if (key === 0) {
|
}
|
||||||
option.attr("selected", "selected");
|
|
||||||
|
return {
|
||||||
|
label: noteTitle,
|
||||||
|
value: notePath
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
minLength: 0,
|
||||||
|
autoFocus: true,
|
||||||
|
select: function (event, ui) {
|
||||||
|
noteTree.activateNode(ui.item.value);
|
||||||
|
|
||||||
|
$searchInput.autocomplete('destroy');
|
||||||
|
$dialog.dialog('close');
|
||||||
|
},
|
||||||
|
focus: function (event, ui) {
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
close: function (event, ui) {
|
||||||
|
if (event.keyCode === 27) { // escape closes dialog
|
||||||
|
$searchInput.autocomplete('destroy');
|
||||||
|
$dialog.dialog('close');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// keep autocomplete open
|
||||||
|
// we're kind of abusing autocomplete to work in a way which it's not designed for
|
||||||
|
$searchInput.autocomplete("search", "");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: () => $searchInput.autocomplete("search", ""),
|
||||||
|
classes: {
|
||||||
|
"ui-autocomplete": "recent-notes-autocomplete"
|
||||||
}
|
}
|
||||||
|
|
||||||
selectBoxEl.append(option);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedNotePath() {
|
|
||||||
return selectBoxEl.find("option:selected").val();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedNoteId() {
|
|
||||||
const notePath = getSelectedNotePath();
|
|
||||||
return treeUtils.getNoteIdFromNotePath(notePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActiveNoteBasedOnRecentNotes() {
|
|
||||||
const notePath = getSelectedNotePath();
|
|
||||||
|
|
||||||
noteTree.activateNode(notePath);
|
|
||||||
|
|
||||||
dialogEl.dialog('close');
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLinkBasedOnRecentNotes() {
|
|
||||||
const notePath = getSelectedNotePath();
|
|
||||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
|
||||||
|
|
||||||
const linkTitle = noteTree.getNoteTitle(noteId);
|
|
||||||
|
|
||||||
dialogEl.dialog("close");
|
|
||||||
|
|
||||||
link.addLinkToEditor(linkTitle, '#' + notePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addCurrentAsChild() {
|
|
||||||
await cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId());
|
|
||||||
|
|
||||||
dialogEl.dialog("close");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addRecentAsChild() {
|
|
||||||
await cloning.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId());
|
|
||||||
|
|
||||||
dialogEl.dialog("close");
|
|
||||||
}
|
|
||||||
|
|
||||||
selectBoxEl.keydown(e => {
|
|
||||||
const key = e.which;
|
|
||||||
|
|
||||||
// to get keycodes use http://keycode.info/
|
|
||||||
if (key === 13)// the enter key code
|
|
||||||
{
|
|
||||||
setActiveNoteBasedOnRecentNotes();
|
|
||||||
}
|
|
||||||
else if (key === 76 /* l */) {
|
|
||||||
addLinkBasedOnRecentNotes();
|
|
||||||
}
|
|
||||||
else if (key === 67 /* c */) {
|
|
||||||
addCurrentAsChild();
|
|
||||||
}
|
|
||||||
else if (key === 82 /* r */) {
|
|
||||||
addRecentAsChild()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return; // avoid prevent default
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
|
|
||||||
$(document).bind('keydown', 'ctrl+e', e => {
|
$(document).bind('keydown', 'ctrl+e', e => {
|
||||||
@@ -129,15 +94,6 @@ const recentNotes = (function() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
selectBoxEl.dblclick(e => {
|
|
||||||
setActiveNoteBasedOnRecentNotes();
|
|
||||||
});
|
|
||||||
|
|
||||||
jumpToButtonEl.click(setActiveNoteBasedOnRecentNotes);
|
|
||||||
addLinkButtonEl.click(addLinkBasedOnRecentNotes);
|
|
||||||
addCurrentAsChildEl.click(addCurrentAsChild);
|
|
||||||
addRecentAsChildEl.click(addRecentAsChild);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showDialog,
|
showDialog,
|
||||||
addRecentNote,
|
addRecentNote,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const settings = (function() {
|
const settings = (function() {
|
||||||
const dialogEl = $("#settings-dialog");
|
const $dialog = $("#settings-dialog");
|
||||||
const tabsEl = $("#settings-tabs");
|
const $tabs = $("#settings-tabs");
|
||||||
|
|
||||||
const settingModules = [];
|
const settingModules = [];
|
||||||
|
|
||||||
@@ -11,16 +11,16 @@ const settings = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function showDialog() {
|
async function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
const settings = await server.get('settings');
|
const settings = await server.get('settings');
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 900
|
width: 900
|
||||||
});
|
});
|
||||||
|
|
||||||
tabsEl.tabs();
|
$tabs.tabs();
|
||||||
|
|
||||||
for (const module of settingModules) {
|
for (const module of settingModules) {
|
||||||
if (module.settingsLoaded) {
|
if (module.settingsLoaded) {
|
||||||
@@ -46,22 +46,22 @@ const settings = (function() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
settings.addModule((function() {
|
settings.addModule((function() {
|
||||||
const formEl = $("#change-password-form");
|
const $form = $("#change-password-form");
|
||||||
const oldPasswordEl = $("#old-password");
|
const $oldPassword = $("#old-password");
|
||||||
const newPassword1El = $("#new-password1");
|
const $newPassword1 = $("#new-password1");
|
||||||
const newPassword2El = $("#new-password2");
|
const $newPassword2 = $("#new-password2");
|
||||||
|
|
||||||
function settingsLoaded(settings) {
|
function settingsLoaded(settings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
formEl.submit(() => {
|
$form.submit(() => {
|
||||||
const oldPassword = oldPasswordEl.val();
|
const oldPassword = $oldPassword.val();
|
||||||
const newPassword1 = newPassword1El.val();
|
const newPassword1 = $newPassword1.val();
|
||||||
const newPassword2 = newPassword2El.val();
|
const newPassword2 = $newPassword2.val();
|
||||||
|
|
||||||
oldPasswordEl.val('');
|
$oldPassword.val('');
|
||||||
newPassword1El.val('');
|
$newPassword1.val('');
|
||||||
newPassword2El.val('');
|
$newPassword2.val('');
|
||||||
|
|
||||||
if (newPassword1 !== newPassword2) {
|
if (newPassword1 !== newPassword2) {
|
||||||
alert("New passwords are not the same.");
|
alert("New passwords are not the same.");
|
||||||
@@ -92,16 +92,16 @@ settings.addModule((function() {
|
|||||||
})());
|
})());
|
||||||
|
|
||||||
settings.addModule((function() {
|
settings.addModule((function() {
|
||||||
const formEl = $("#protected-session-timeout-form");
|
const $form = $("#protected-session-timeout-form");
|
||||||
const protectedSessionTimeoutEl = $("#protected-session-timeout-in-seconds");
|
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
|
||||||
const settingName = 'protected_session_timeout';
|
const settingName = 'protected_session_timeout';
|
||||||
|
|
||||||
function settingsLoaded(settings) {
|
function settingsLoaded(settings) {
|
||||||
protectedSessionTimeoutEl.val(settings[settingName]);
|
$protectedSessionTimeout.val(settings[settingName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
formEl.submit(() => {
|
$form.submit(() => {
|
||||||
const protectedSessionTimeout = protectedSessionTimeoutEl.val();
|
const protectedSessionTimeout = $protectedSessionTimeout.val();
|
||||||
|
|
||||||
settings.saveSettings(settingName, protectedSessionTimeout).then(() => {
|
settings.saveSettings(settingName, protectedSessionTimeout).then(() => {
|
||||||
protected_session.setProtectedSessionTimeout(protectedSessionTimeout);
|
protected_session.setProtectedSessionTimeout(protectedSessionTimeout);
|
||||||
@@ -116,16 +116,16 @@ settings.addModule((function() {
|
|||||||
})());
|
})());
|
||||||
|
|
||||||
settings.addModule((function () {
|
settings.addModule((function () {
|
||||||
const formEl = $("#history-snapshot-time-interval-form");
|
const $form = $("#history-snapshot-time-interval-form");
|
||||||
const timeIntervalEl = $("#history-snapshot-time-interval-in-seconds");
|
const $timeInterval = $("#history-snapshot-time-interval-in-seconds");
|
||||||
const settingName = 'history_snapshot_time_interval';
|
const settingName = 'history_snapshot_time_interval';
|
||||||
|
|
||||||
function settingsLoaded(settings) {
|
function settingsLoaded(settings) {
|
||||||
timeIntervalEl.val(settings[settingName]);
|
$timeInterval.val(settings[settingName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
formEl.submit(() => {
|
$form.submit(() => {
|
||||||
settings.saveSettings(settingName, timeIntervalEl.val());
|
settings.saveSettings(settingName, $timeInterval.val());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -136,50 +136,50 @@ settings.addModule((function () {
|
|||||||
})());
|
})());
|
||||||
|
|
||||||
settings.addModule((async function () {
|
settings.addModule((async function () {
|
||||||
const appVersionEl = $("#app-version");
|
const $appVersion = $("#app-version");
|
||||||
const dbVersionEl = $("#db-version");
|
const $dbVersion = $("#db-version");
|
||||||
const buildDateEl = $("#build-date");
|
const $buildDate = $("#build-date");
|
||||||
const buildRevisionEl = $("#build-revision");
|
const $buildRevision = $("#build-revision");
|
||||||
|
|
||||||
const appInfo = await server.get('app-info');
|
const appInfo = await server.get('app-info');
|
||||||
|
|
||||||
appVersionEl.html(appInfo.app_version);
|
$appVersion.html(appInfo.app_version);
|
||||||
dbVersionEl.html(appInfo.db_version);
|
$dbVersion.html(appInfo.db_version);
|
||||||
buildDateEl.html(appInfo.build_date);
|
$buildDate.html(appInfo.build_date);
|
||||||
buildRevisionEl.html(appInfo.build_revision);
|
$buildRevision.html(appInfo.build_revision);
|
||||||
buildRevisionEl.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
|
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
})());
|
})());
|
||||||
|
|
||||||
settings.addModule((async function () {
|
settings.addModule((async function () {
|
||||||
const forceFullSyncButton = $("#force-full-sync-button");
|
const $forceFullSyncButton = $("#force-full-sync-button");
|
||||||
const fillSyncRowsButton = $("#fill-sync-rows-button");
|
const $fillSyncRowsButton = $("#fill-sync-rows-button");
|
||||||
const anonymizeButton = $("#anonymize-button");
|
const $anonymizeButton = $("#anonymize-button");
|
||||||
const cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
|
const $cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
|
||||||
const cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
|
const $cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
|
||||||
const vacuumDatabaseButton = $("#vacuum-database-button");
|
const $vacuumDatabaseButton = $("#vacuum-database-button");
|
||||||
|
|
||||||
forceFullSyncButton.click(async () => {
|
$forceFullSyncButton.click(async () => {
|
||||||
await server.post('sync/force-full-sync');
|
await server.post('sync/force-full-sync');
|
||||||
|
|
||||||
showMessage("Full sync triggered");
|
showMessage("Full sync triggered");
|
||||||
});
|
});
|
||||||
|
|
||||||
fillSyncRowsButton.click(async () => {
|
$fillSyncRowsButton.click(async () => {
|
||||||
await server.post('sync/fill-sync-rows');
|
await server.post('sync/fill-sync-rows');
|
||||||
|
|
||||||
showMessage("Sync rows filled successfully");
|
showMessage("Sync rows filled successfully");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
anonymizeButton.click(async () => {
|
$anonymizeButton.click(async () => {
|
||||||
await server.post('anonymization/anonymize');
|
await server.post('anonymization/anonymize');
|
||||||
|
|
||||||
showMessage("Created anonymized database");
|
showMessage("Created anonymized database");
|
||||||
});
|
});
|
||||||
|
|
||||||
cleanupSoftDeletedButton.click(async () => {
|
$cleanupSoftDeletedButton.click(async () => {
|
||||||
if (confirm("Do you really want to clean up soft-deleted items?")) {
|
if (confirm("Do you really want to clean up soft-deleted items?")) {
|
||||||
await server.post('cleanup/cleanup-soft-deleted-items');
|
await server.post('cleanup/cleanup-soft-deleted-items');
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ settings.addModule((async function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cleanupUnusedImagesButton.click(async () => {
|
$cleanupUnusedImagesButton.click(async () => {
|
||||||
if (confirm("Do you really want to clean up unused images?")) {
|
if (confirm("Do you really want to clean up unused images?")) {
|
||||||
await server.post('cleanup/cleanup-unused-images');
|
await server.post('cleanup/cleanup-unused-images');
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ settings.addModule((async function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
vacuumDatabaseButton.click(async () => {
|
$vacuumDatabaseButton.click(async () => {
|
||||||
await server.post('cleanup/vacuum-database');
|
await server.post('cleanup/vacuum-database');
|
||||||
|
|
||||||
showMessage("Database has been vacuumed");
|
showMessage("Database has been vacuumed");
|
||||||
|
|||||||
@@ -1,24 +1,59 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const sqlConsole = (function() {
|
const sqlConsole = (function() {
|
||||||
const dialogEl = $("#sql-console-dialog");
|
const $dialog = $("#sql-console-dialog");
|
||||||
const queryEl = $('#sql-console-query');
|
const $query = $('#sql-console-query');
|
||||||
const executeButton = $('#sql-console-execute');
|
const $executeButton = $('#sql-console-execute');
|
||||||
const resultHeadEl = $('#sql-console-results thead');
|
const $resultHead = $('#sql-console-results thead');
|
||||||
const resultBodyEl = $('#sql-console-results tbody');
|
const $resultBody = $('#sql-console-results tbody');
|
||||||
|
|
||||||
|
let codeEditor;
|
||||||
|
|
||||||
function showDialog() {
|
function showDialog() {
|
||||||
glob.activeDialog = dialogEl;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: $(window).width(),
|
width: $(window).width(),
|
||||||
height: $(window).height()
|
height: $(window).height(),
|
||||||
|
open: function() {
|
||||||
|
initEditor();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function execute() {
|
async function initEditor() {
|
||||||
const sqlQuery = queryEl.val();
|
if (!codeEditor) {
|
||||||
|
await requireLibrary(CODE_MIRROR);
|
||||||
|
|
||||||
|
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
|
||||||
|
CodeMirror.keyMap.default["Tab"] = "indentMore";
|
||||||
|
|
||||||
|
// removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
|
||||||
|
delete CodeMirror.keyMap.basic["Esc"];
|
||||||
|
|
||||||
|
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(e) {
|
||||||
|
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const sqlQuery = codeEditor.getValue();
|
||||||
|
|
||||||
const result = await server.post("sql/execute", {
|
const result = await server.post("sql/execute", {
|
||||||
query: sqlQuery
|
query: sqlQuery
|
||||||
@@ -34,8 +69,8 @@ const sqlConsole = (function() {
|
|||||||
|
|
||||||
const rows = result.rows;
|
const rows = result.rows;
|
||||||
|
|
||||||
resultHeadEl.empty();
|
$resultHead.empty();
|
||||||
resultBodyEl.empty();
|
$resultBody.empty();
|
||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const result = rows[0];
|
const result = rows[0];
|
||||||
@@ -45,7 +80,7 @@ const sqlConsole = (function() {
|
|||||||
rowEl.append($("<th>").html(key));
|
rowEl.append($("<th>").html(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
resultHeadEl.append(rowEl);
|
$resultHead.append(rowEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const result of rows) {
|
for (const result of rows) {
|
||||||
@@ -55,15 +90,15 @@ const sqlConsole = (function() {
|
|||||||
rowEl.append($("<td>").html(result[key]));
|
rowEl.append($("<td>").html(result[key]));
|
||||||
}
|
}
|
||||||
|
|
||||||
resultBodyEl.append(rowEl);
|
$resultBody.append(rowEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).bind('keydown', 'alt+o', showDialog);
|
$(document).bind('keydown', 'alt+o', showDialog);
|
||||||
|
|
||||||
queryEl.bind('keydown', 'ctrl+return', execute);
|
$query.bind('keydown', 'ctrl+return', execute);
|
||||||
|
|
||||||
executeButton.click(execute);
|
$executeButton.click(execute);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showDialog
|
showDialog
|
||||||
|
|||||||
32
src/public/javascripts/export.js
Normal file
32
src/public/javascripts/export.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
function exportSubTree(noteId) {
|
||||||
|
const url = getHost() + "/api/export/" + noteId + "?protectedSessionId="
|
||||||
|
+ encodeURIComponent(protected_session.getProtectedSessionId());
|
||||||
|
|
||||||
|
download(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let importNoteId;
|
||||||
|
|
||||||
|
function importSubTree(noteId) {
|
||||||
|
importNoteId = noteId;
|
||||||
|
|
||||||
|
$("#import-upload").trigger('click');
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#import-upload").change(async function() {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('upload', this.files[0]);
|
||||||
|
|
||||||
|
await $.ajax({
|
||||||
|
url: baseApiUrl + 'import/' + importNoteId,
|
||||||
|
headers: server.getHeaders(),
|
||||||
|
data: formData,
|
||||||
|
type: 'POST',
|
||||||
|
contentType: false, // NEEDED, DON'T OMIT THIS
|
||||||
|
processData: false, // NEEDED, DON'T OMIT THIS
|
||||||
|
});
|
||||||
|
|
||||||
|
await noteTree.reload();
|
||||||
|
});
|
||||||
@@ -54,24 +54,6 @@ $(document).bind('keydown', 'ctrl+f', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).bind('keydown', "ctrl+shift+left", () => {
|
|
||||||
const node = noteTree.getCurrentNode();
|
|
||||||
node.navigate($.ui.keyCode.LEFT, true);
|
|
||||||
|
|
||||||
$("#note-detail").focus();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).bind('keydown', "ctrl+shift+right", () => {
|
|
||||||
const node = noteTree.getCurrentNode();
|
|
||||||
node.navigate($.ui.keyCode.RIGHT, true);
|
|
||||||
|
|
||||||
$("#note-detail").focus();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).bind('keydown', "ctrl+shift+up", () => {
|
$(document).bind('keydown', "ctrl+shift+up", () => {
|
||||||
const node = noteTree.getCurrentNode();
|
const node = noteTree.getCurrentNode();
|
||||||
node.navigate($.ui.keyCode.UP, true);
|
node.navigate($.ui.keyCode.UP, true);
|
||||||
@@ -123,7 +105,7 @@ $(window).on('beforeunload', () => {
|
|||||||
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
|
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
|
||||||
$.ui.autocomplete.filter = (array, terms) => {
|
$.ui.autocomplete.filter = (array, terms) => {
|
||||||
if (!terms) {
|
if (!terms) {
|
||||||
return [];
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
@@ -132,18 +114,32 @@ $.ui.autocomplete.filter = (array, terms) => {
|
|||||||
const tokens = terms.toLowerCase().split(" ");
|
const tokens = terms.toLowerCase().split(" ");
|
||||||
|
|
||||||
for (const item of array) {
|
for (const item of array) {
|
||||||
let found = true;
|
|
||||||
const lcLabel = item.label.toLowerCase();
|
const lcLabel = item.label.toLowerCase();
|
||||||
|
|
||||||
for (const token of tokens) {
|
const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
|
||||||
if (lcLabel.indexOf(token) === -1) {
|
if (!found) {
|
||||||
found = false;
|
continue;
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
// this is not completely correct and might cause minor problems with note with names containing this " / "
|
||||||
|
const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
|
||||||
|
|
||||||
|
if (lastSegmentIndex !== -1) {
|
||||||
|
const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
|
||||||
|
|
||||||
|
// at least some token needs to be in the last segment (leaf note), otherwise this
|
||||||
|
// particular note is not that interesting (query is satisfied by parent note)
|
||||||
|
const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
|
||||||
|
|
||||||
|
if (!foundInLastSegment) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found) {
|
results.push(item);
|
||||||
results.push(item);
|
|
||||||
|
if (results.length > 100) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,9 +201,48 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
|
|||||||
$("#logout-button").toggle(!isElectron());
|
$("#logout-button").toggle(!isElectron());
|
||||||
|
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
server.get("script/startup").then(scripts => {
|
server.get("script/startup").then(scriptBundles => {
|
||||||
for (const script of scripts) {
|
for (const bundle of scriptBundles) {
|
||||||
executeScript(script);
|
executeBundle(bundle);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isElectron()) {
|
||||||
|
require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
|
||||||
|
// this might occur when day note had to be created
|
||||||
|
if (!noteTree.noteExists(parentNoteId)) {
|
||||||
|
await noteTree.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
await noteTree.activateNode(parentNoteId);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
|
||||||
|
noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadAttachment() {
|
||||||
|
$("#attachment-upload").trigger('click');
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#attachment-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);
|
||||||
});
|
});
|
||||||
@@ -41,11 +41,11 @@ const link = (function() {
|
|||||||
function goToLink(e) {
|
function goToLink(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const linkEl = $(e.target);
|
const $link = $(e.target);
|
||||||
let notePath = linkEl.attr("note-path");
|
let notePath = $link.attr("note-path");
|
||||||
|
|
||||||
if (!notePath) {
|
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) {
|
if (!address) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const messaging = (function() {
|
const messaging = (function() {
|
||||||
const changesToPushCountEl = $("#changes-to-push-count");
|
const $changesToPushCount = $("#changes-to-push-count");
|
||||||
|
|
||||||
function logError(message) {
|
function logError(message) {
|
||||||
console.log(now(), message); // needs to be separate from .trace()
|
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
|
// we don't detect image changes here since images themselves are immutable and references should be
|
||||||
// updated in note detail as well
|
// updated in note detail as well
|
||||||
|
|
||||||
changesToPushCountEl.html(message.changesToPushCount);
|
$changesToPushCount.html(message.changesToPushCount);
|
||||||
}
|
}
|
||||||
else if (message.type === 'sync-hash-check-failed') {
|
else if (message.type === 'sync-hash-check-failed') {
|
||||||
showError("Sync check failed!", 60000);
|
showError("Sync check failed!", 60000);
|
||||||
@@ -84,7 +84,7 @@ const messaging = (function() {
|
|||||||
let connectionBrokenNotification = null;
|
let connectionBrokenNotification = null;
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
if (new Date().getTime() - lastPingTs > 5000) {
|
if (new Date().getTime() - lastPingTs > 30000) {
|
||||||
if (!connectionBrokenNotification) {
|
if (!connectionBrokenNotification) {
|
||||||
connectionBrokenNotification = $.notify({
|
connectionBrokenNotification = $.notify({
|
||||||
// options
|
// options
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const noteEditor = (function() {
|
const noteEditor = (function() {
|
||||||
const noteTitleEl = $("#note-title");
|
const $noteTitle = $("#note-title");
|
||||||
const noteDetailEl = $('#note-detail');
|
|
||||||
const noteDetailCodeEl = $('#note-detail-code');
|
const $noteDetail = $('#note-detail');
|
||||||
const noteDetailRenderEl = $('#note-detail-render');
|
const $noteDetailCode = $('#note-detail-code');
|
||||||
const protectButton = $("#protect-button");
|
const $noteDetailRender = $('#note-detail-render');
|
||||||
const unprotectButton = $("#unprotect-button");
|
const $noteDetailAttachment = $('#note-detail-attachment');
|
||||||
const noteDetailWrapperEl = $("#note-detail-wrapper");
|
|
||||||
const noteIdDisplayEl = $("#note-id-display");
|
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 editor = null;
|
||||||
let codeEditor = null;
|
let codeEditor = null;
|
||||||
@@ -67,25 +77,27 @@ const noteEditor = (function() {
|
|||||||
|
|
||||||
function updateNoteFromInputs(note) {
|
function updateNoteFromInputs(note) {
|
||||||
if (note.detail.type === 'text') {
|
if (note.detail.type === 'text') {
|
||||||
note.detail.content = editor.getData();
|
let content = editor.getData();
|
||||||
|
|
||||||
// if content is only tags/whitespace (typically <p> </p>), then just make it empty
|
// if content is only tags/whitespace (typically <p> </p>), then just make it empty
|
||||||
// this is important when setting new note to code
|
// this is important when setting new note to code
|
||||||
if (jQuery(note.detail.content).text().trim() === '') {
|
if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
|
||||||
note.detail.content = ''
|
content = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
note.detail.content = content;
|
||||||
}
|
}
|
||||||
else if (note.detail.type === 'code') {
|
else if (note.detail.type === 'code') {
|
||||||
note.detail.content = codeEditor.getValue();
|
note.detail.content = codeEditor.getValue();
|
||||||
}
|
}
|
||||||
else if (note.detail.type === 'render') {
|
else if (note.detail.type === 'render' || note.detail.type === 'file') {
|
||||||
// nothing
|
// nothing
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throwError("Unrecognized type: " + note.detail.type);
|
throwError("Unrecognized type: " + note.detail.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = noteTitleEl.val();
|
const title = $noteTitle.val();
|
||||||
|
|
||||||
note.detail.title = title;
|
note.detail.title = title;
|
||||||
|
|
||||||
@@ -103,9 +115,9 @@ const noteEditor = (function() {
|
|||||||
function setNoteBackgroundIfProtected(note) {
|
function setNoteBackgroundIfProtected(note) {
|
||||||
const isProtected = !!note.detail.isProtected;
|
const isProtected = !!note.detail.isProtected;
|
||||||
|
|
||||||
noteDetailWrapperEl.toggleClass("protected", isProtected);
|
$noteDetailWrapper.toggleClass("protected", isProtected);
|
||||||
protectButton.toggle(!isProtected);
|
$protectButton.toggle(!isProtected);
|
||||||
unprotectButton.toggle(isProtected);
|
$unprotectButton.toggle(isProtected);
|
||||||
}
|
}
|
||||||
|
|
||||||
let isNewNoteCreated = false;
|
let isNewNoteCreated = false;
|
||||||
@@ -114,16 +126,71 @@ const noteEditor = (function() {
|
|||||||
isNewNoteCreated = true;
|
isNewNoteCreated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>");
|
||||||
|
|
||||||
|
$noteDetail.show();
|
||||||
|
}
|
||||||
|
else if (currentNote.detail.type === 'code') {
|
||||||
|
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 },
|
||||||
|
lint: true,
|
||||||
|
gutters: ["CodeMirror-lint-markers"],
|
||||||
|
lineNumbers: true
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
codeEditor.setOption("mode", info.mime);
|
||||||
|
CodeMirror.autoLoadMode(codeEditor, info.mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
codeEditor.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadNoteToEditor(noteId) {
|
async function loadNoteToEditor(noteId) {
|
||||||
currentNote = await loadNote(noteId);
|
currentNote = await loadNote(noteId);
|
||||||
|
|
||||||
if (isNewNoteCreated) {
|
if (isNewNoteCreated) {
|
||||||
isNewNoteCreated = false;
|
isNewNoteCreated = false;
|
||||||
|
|
||||||
noteTitleEl.focus().select();
|
$noteTitle.focus().select();
|
||||||
}
|
}
|
||||||
|
|
||||||
noteIdDisplayEl.html(noteId);
|
$noteIdDisplay.html(noteId);
|
||||||
|
|
||||||
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
|
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
|
||||||
|
|
||||||
@@ -135,49 +202,38 @@ 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.
|
// 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();
|
protected_session.ensureDialogIsClosed();
|
||||||
|
|
||||||
noteDetailWrapperEl.show();
|
$noteDetailWrapper.show();
|
||||||
|
|
||||||
noteChangeDisabled = true;
|
noteChangeDisabled = true;
|
||||||
|
|
||||||
noteTitleEl.val(currentNote.detail.title);
|
$noteTitle.val(currentNote.detail.title);
|
||||||
|
|
||||||
noteType.setNoteType(currentNote.detail.type);
|
noteType.setNoteType(currentNote.detail.type);
|
||||||
noteType.setNoteMime(currentNote.detail.mime);
|
noteType.setNoteMime(currentNote.detail.mime);
|
||||||
|
|
||||||
if (currentNote.detail.type === 'text') {
|
$noteDetail.hide();
|
||||||
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
|
$noteDetailCode.hide();
|
||||||
editor.setData(currentNote.detail.content ? currentNote.detail.content : "<p></p>");
|
$noteDetailRender.html('').hide();
|
||||||
|
$noteDetailAttachment.hide();
|
||||||
|
|
||||||
noteDetailEl.show();
|
if (currentNote.detail.type === 'render') {
|
||||||
noteDetailCodeEl.hide();
|
$noteDetailRender.show();
|
||||||
noteDetailRenderEl.html('').hide();
|
|
||||||
|
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
|
||||||
|
|
||||||
|
$noteDetailRender.html(bundle.html);
|
||||||
|
|
||||||
|
executeBundle(bundle);
|
||||||
}
|
}
|
||||||
else if (currentNote.detail.type === 'code') {
|
else if (currentNote.detail.type === 'file') {
|
||||||
noteDetailEl.hide();
|
$noteDetailAttachment.show();
|
||||||
noteDetailCodeEl.show();
|
|
||||||
noteDetailRenderEl.html('').hide();
|
|
||||||
|
|
||||||
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
|
$attachmentFileName.text(currentNote.attributes.original_file_name);
|
||||||
codeEditor.setValue(currentNote.detail.content);
|
$attachmentFileSize.text(currentNote.attributes.file_size + " bytes");
|
||||||
|
$attachmentFileType.text(currentNote.detail.mime);
|
||||||
const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
|
|
||||||
|
|
||||||
if (info) {
|
|
||||||
codeEditor.setOption("mode", info.mime);
|
|
||||||
CodeMirror.autoLoadMode(codeEditor, info.mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (currentNote.detail.type === 'render') {
|
|
||||||
noteDetailEl.hide();
|
|
||||||
noteDetailCodeEl.hide();
|
|
||||||
noteDetailRenderEl.html('').show();
|
|
||||||
|
|
||||||
const subTree = await server.get('script/subtree/' + getCurrentNoteId());
|
|
||||||
|
|
||||||
noteDetailRenderEl.html(subTree);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throwError("Unrecognized type " + currentNote.detail.type);
|
await setContent(currentNote.detail.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
noteChangeDisabled = false;
|
noteChangeDisabled = false;
|
||||||
@@ -186,7 +242,28 @@ const noteEditor = (function() {
|
|||||||
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
|
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
|
||||||
|
|
||||||
// after loading new note make sure editor is scrolled to the top
|
// after loading new note make sure editor is scrolled to the top
|
||||||
noteDetailWrapperEl.scrollTop(0);
|
$noteDetailWrapper.scrollTop(0);
|
||||||
|
|
||||||
|
loadAttributeList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAttributeList() {
|
||||||
|
const noteId = getCurrentNoteId();
|
||||||
|
|
||||||
|
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||||
|
|
||||||
|
$attributeListInner.html('');
|
||||||
|
|
||||||
|
if (attributes.length > 0) {
|
||||||
|
for (const attr of attributes) {
|
||||||
|
$attributeListInner.append(formatAttribute(attr) + " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributeList.show();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$attributeList.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadNote(noteId) {
|
async function loadNote(noteId) {
|
||||||
@@ -201,12 +278,12 @@ const noteEditor = (function() {
|
|||||||
const note = getCurrentNote();
|
const note = getCurrentNote();
|
||||||
|
|
||||||
if (note.detail.type === 'text') {
|
if (note.detail.type === 'text') {
|
||||||
noteDetailEl.focus();
|
$noteDetail.focus();
|
||||||
}
|
}
|
||||||
else if (note.detail.type === 'code') {
|
else if (note.detail.type === 'code') {
|
||||||
codeEditor.focus();
|
codeEditor.focus();
|
||||||
}
|
}
|
||||||
else if (note.detail.type === 'render') {
|
else if (note.detail.type === 'render' || note.detail.type === 'file') {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -225,51 +302,50 @@ const noteEditor = (function() {
|
|||||||
// make sure note is saved so we load latest changes
|
// make sure note is saved so we load latest changes
|
||||||
await saveNoteIfChanged();
|
await saveNoteIfChanged();
|
||||||
|
|
||||||
const script = await server.get('script/subtree/' + getCurrentNoteId());
|
if (currentNote.detail.mime.endsWith("env=frontend")) {
|
||||||
|
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
|
||||||
|
|
||||||
executeScript(script);
|
executeBundle(bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentNote.detail.mime.endsWith("env=backend")) {
|
||||||
|
await server.post('script/run/' + getCurrentNoteId());
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage("Note executed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$attachmentDownload.click(() => download(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
|
||||||
|
return getHost() + "/api/attachments/download/" + getCurrentNoteId()
|
||||||
|
+ "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
noteTitleEl.on('input', () => {
|
$noteTitle.on('input', () => {
|
||||||
noteChanged();
|
noteChanged();
|
||||||
|
|
||||||
const title = noteTitleEl.val();
|
const title = $noteTitle.val();
|
||||||
|
|
||||||
noteTree.setNoteTitle(getCurrentNoteId(), title);
|
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)
|
// 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);
|
$(document).bind('keydown', "ctrl+return", executeCurrentNote);
|
||||||
@@ -290,6 +366,8 @@ const noteEditor = (function() {
|
|||||||
newNoteCreated,
|
newNoteCreated,
|
||||||
getEditor,
|
getEditor,
|
||||||
focus,
|
focus,
|
||||||
executeCurrentNote
|
executeCurrentNote,
|
||||||
|
loadAttributeList,
|
||||||
|
setContent
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const noteTree = (function() {
|
const noteTree = (function() {
|
||||||
const treeEl = $("#tree");
|
const $tree = $("#tree");
|
||||||
const parentListEl = $("#parent-list");
|
const $parentList = $("#parent-list");
|
||||||
const parentListListEl = $("#parent-list-list");
|
const $parentListList = $("#parent-list-inner");
|
||||||
|
|
||||||
|
let instanceName = null; // should have better place
|
||||||
|
|
||||||
let startNotePath = null;
|
let startNotePath = null;
|
||||||
let notesTreeMap = {};
|
let notesTreeMap = {};
|
||||||
@@ -14,6 +16,8 @@ const noteTree = (function() {
|
|||||||
let parentChildToNoteTreeId = {};
|
let parentChildToNoteTreeId = {};
|
||||||
let noteIdToTitle = {};
|
let noteIdToTitle = {};
|
||||||
|
|
||||||
|
let hiddenInAutocomplete = {};
|
||||||
|
|
||||||
function getNoteTreeId(parentNoteId, childNoteId) {
|
function getNoteTreeId(parentNoteId, childNoteId) {
|
||||||
assertArguments(parentNoteId, childNoteId);
|
assertArguments(parentNoteId, childNoteId);
|
||||||
|
|
||||||
@@ -50,7 +54,7 @@ const noteTree = (function() {
|
|||||||
|
|
||||||
// note that if you want to access data like noteId or isProtected, you need to go into "data" property
|
// note that if you want to access data like noteId or isProtected, you need to go into "data" property
|
||||||
function getCurrentNode() {
|
function getCurrentNode() {
|
||||||
return treeEl.fancytree("getActiveNode");
|
return $tree.fancytree("getActiveNode");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentNotePath() {
|
function getCurrentNotePath() {
|
||||||
@@ -153,6 +157,12 @@ const noteTree = (function() {
|
|||||||
if (note.type === 'code') {
|
if (note.type === 'code') {
|
||||||
extraClasses.push("code");
|
extraClasses.push("code");
|
||||||
}
|
}
|
||||||
|
else if (note.type === 'render') {
|
||||||
|
extraClasses.push('render');
|
||||||
|
}
|
||||||
|
else if (note.type === 'file') {
|
||||||
|
extraClasses.push('attachment');
|
||||||
|
}
|
||||||
|
|
||||||
return extraClasses.join(" ");
|
return extraClasses.join(" ");
|
||||||
}
|
}
|
||||||
@@ -312,11 +322,11 @@ const noteTree = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parents.length <= 1) {
|
if (parents.length <= 1) {
|
||||||
parentListEl.hide();
|
$parentList.hide();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
parentListEl.show();
|
$parentList.show();
|
||||||
parentListListEl.empty();
|
$parentListList.empty();
|
||||||
|
|
||||||
for (const parentNoteId of parents) {
|
for (const parentNoteId of parents) {
|
||||||
const parentNotePath = getSomeNotePath(parentNoteId);
|
const parentNotePath = getSomeNotePath(parentNoteId);
|
||||||
@@ -333,7 +343,7 @@ const noteTree = (function() {
|
|||||||
item = link.createNoteLink(notePath, title);
|
item = link.createNoteLink(notePath, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
parentListListEl.append($("<li/>").append(item));
|
$parentListList.append($("<li/>").append(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,7 +551,7 @@ const noteTree = (function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
treeEl.fancytree({
|
$tree.fancytree({
|
||||||
autoScroll: true,
|
autoScroll: true,
|
||||||
keyboard: false, // we takover keyboard handling in the hotkeys plugin
|
keyboard: false, // we takover keyboard handling in the hotkeys plugin
|
||||||
extensions: ["hotkeys", "filter", "dnd", "clones"],
|
extensions: ["hotkeys", "filter", "dnd", "clones"],
|
||||||
@@ -622,11 +632,11 @@ const noteTree = (function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
treeEl.contextmenu(contextMenu.contextMenuSettings);
|
$tree.contextmenu(contextMenu.contextMenuSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTree() {
|
function getTree() {
|
||||||
return treeEl.fancytree('getTree');
|
return $tree.fancytree('getTree');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reload() {
|
async function reload() {
|
||||||
@@ -640,23 +650,29 @@ const noteTree = (function() {
|
|||||||
return document.location.hash.substr(1); // strip initial #
|
return document.location.hash.substr(1); // strip initial #
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTree() {
|
async function loadTree() {
|
||||||
return server.get('tree').then(resp => {
|
const resp = await server.get('tree');
|
||||||
startNotePath = resp.start_note_path;
|
startNotePath = resp.start_note_path;
|
||||||
|
instanceName = resp.instanceName;
|
||||||
|
|
||||||
if (document.location.hash) {
|
if (document.location.hash) {
|
||||||
startNotePath = getNotePathFromAddress();
|
startNotePath = getNotePathFromAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
return prepareNoteTree(resp.notes);
|
hiddenInAutocomplete = {};
|
||||||
});
|
|
||||||
|
for (const noteId of resp.hiddenInAutocomplete) {
|
||||||
|
hiddenInAutocomplete[noteId] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepareNoteTree(resp.notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(() => loadTree().then(noteTree => initFancyTree(noteTree)));
|
$(() => loadTree().then(noteTree => initFancyTree(noteTree)));
|
||||||
|
|
||||||
function collapseTree(node = null) {
|
function collapseTree(node = null) {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
node = treeEl.fancytree("getRootNode");
|
node = $tree.fancytree("getRootNode");
|
||||||
}
|
}
|
||||||
|
|
||||||
node.setExpanded(false);
|
node.setExpanded(false);
|
||||||
@@ -703,9 +719,16 @@ const noteTree = (function() {
|
|||||||
titlePath = '';
|
titlePath = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/zadam/trilium/issues/46
|
||||||
|
// unfortunately not easy to implement because we don't have an easy access to note's isProtected property
|
||||||
|
|
||||||
const autocompleteItems = [];
|
const autocompleteItems = [];
|
||||||
|
|
||||||
for (const childNoteId of parentToChildren[parentNoteId]) {
|
for (const childNoteId of parentToChildren[parentNoteId]) {
|
||||||
|
if (hiddenInAutocomplete[childNoteId]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId;
|
const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId;
|
||||||
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId);
|
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId);
|
||||||
|
|
||||||
@@ -733,7 +756,7 @@ const noteTree = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createNewTopLevelNote() {
|
async function createNewTopLevelNote() {
|
||||||
const rootNode = treeEl.fancytree("getRootNode");
|
const rootNode = $tree.fancytree("getRootNode");
|
||||||
|
|
||||||
await createNote(rootNode, "root", "into");
|
await createNote(rootNode, "root", "into");
|
||||||
}
|
}
|
||||||
@@ -775,7 +798,7 @@ const noteTree = (function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (target === 'after') {
|
if (target === 'after') {
|
||||||
node.appendSibling(newNode).setActive(true);
|
await node.appendSibling(newNode).setActive(true);
|
||||||
}
|
}
|
||||||
else if (target === 'into') {
|
else if (target === 'into') {
|
||||||
if (!node.getChildren() && node.isFolder()) {
|
if (!node.getChildren() && node.isFolder()) {
|
||||||
@@ -785,7 +808,7 @@ const noteTree = (function() {
|
|||||||
node.addChildren(newNode);
|
node.addChildren(newNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
node.getLastChild().setActive(true);
|
await node.getLastChild().setActive(true);
|
||||||
|
|
||||||
node.folder = true;
|
node.folder = true;
|
||||||
node.renderTitle();
|
node.renderTitle();
|
||||||
@@ -794,6 +817,8 @@ const noteTree = (function() {
|
|||||||
throwError("Unrecognized target: " + target);
|
throwError("Unrecognized target: " + target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearSelectedNodes(); // to unmark previously active node
|
||||||
|
|
||||||
showMessage("Created!");
|
showMessage("Created!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,6 +828,14 @@ const noteTree = (function() {
|
|||||||
await reload();
|
await reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function noteExists(noteId) {
|
||||||
|
return !!childToParents[noteId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstanceName() {
|
||||||
|
return instanceName;
|
||||||
|
}
|
||||||
|
|
||||||
$(document).bind('keydown', 'ctrl+o', e => {
|
$(document).bind('keydown', 'ctrl+o', e => {
|
||||||
const node = getCurrentNode();
|
const node = getCurrentNode();
|
||||||
const parentNoteId = node.data.parentNoteId;
|
const parentNoteId = node.data.parentNoteId;
|
||||||
@@ -876,6 +909,8 @@ const noteTree = (function() {
|
|||||||
removeParentChildRelation,
|
removeParentChildRelation,
|
||||||
setParentChildRelation,
|
setParentChildRelation,
|
||||||
getSelectedNodes,
|
getSelectedNodes,
|
||||||
sortAlphabetically
|
sortAlphabetically,
|
||||||
|
noteExists,
|
||||||
|
getInstanceName
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const noteType = (function() {
|
const noteType = (function() {
|
||||||
const executeScriptButton = $("#execute-script-button");
|
const $executeScriptButton = $("#execute-script-button");
|
||||||
const noteTypeModel = new NoteTypeModel();
|
const noteTypeModel = new NoteTypeModel();
|
||||||
|
|
||||||
function NoteTypeModel() {
|
function NoteTypeModel() {
|
||||||
@@ -25,7 +25,8 @@ const noteType = (function() {
|
|||||||
{ mime: 'text/html', title: 'HTML' },
|
{ mime: 'text/html', title: 'HTML' },
|
||||||
{ mime: 'message/http', title: 'HTTP' },
|
{ mime: 'message/http', title: 'HTTP' },
|
||||||
{ mime: 'text/x-java', title: 'Java' },
|
{ mime: 'text/x-java', title: 'Java' },
|
||||||
{ mime: 'application/javascript', title: 'JavaScript' },
|
{ mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
|
||||||
|
{ mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
|
||||||
{ mime: 'application/json', title: 'JSON' },
|
{ mime: 'application/json', title: 'JSON' },
|
||||||
{ mime: 'text/x-kotlin', title: 'Kotlin' },
|
{ mime: 'text/x-kotlin', title: 'Kotlin' },
|
||||||
{ mime: 'text/x-lua', title: 'Lua' },
|
{ mime: 'text/x-lua', title: 'Lua' },
|
||||||
@@ -65,11 +66,18 @@ const noteType = (function() {
|
|||||||
else if (type === 'render') {
|
else if (type === 'render') {
|
||||||
return 'Render HTML note';
|
return 'Render HTML note';
|
||||||
}
|
}
|
||||||
|
else if (type === 'file') {
|
||||||
|
return 'Attachment';
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
throwError('Unrecognized type: ' + type);
|
throwError('Unrecognized type: ' + type);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.isDisabled = function() {
|
||||||
|
return self.type() === "file";
|
||||||
|
};
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
const note = noteEditor.getCurrentNote();
|
const note = noteEditor.getCurrentNote();
|
||||||
|
|
||||||
@@ -114,7 +122,7 @@ const noteType = (function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.updateExecuteScriptButtonVisibility = function() {
|
this.updateExecuteScriptButtonVisibility = function() {
|
||||||
executeScriptButton.toggle(self.mime() === 'application/javascript');
|
$executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const protected_session = (function() {
|
const protected_session = (function() {
|
||||||
const dialogEl = $("#protected-session-password-dialog");
|
const $dialog = $("#protected-session-password-dialog");
|
||||||
const passwordFormEl = $("#protected-session-password-form");
|
const $passwordForm = $("#protected-session-password-form");
|
||||||
const passwordEl = $("#protected-session-password");
|
const $password = $("#protected-session-password");
|
||||||
const noteDetailWrapperEl = $("#note-detail-wrapper");
|
const $noteDetailWrapper = $("#note-detail-wrapper");
|
||||||
|
|
||||||
let protectedSessionDeferred = null;
|
let protectedSessionDeferred = null;
|
||||||
let lastProtectedSessionOperationDate = null;
|
let lastProtectedSessionOperationDate = null;
|
||||||
@@ -25,9 +25,11 @@ const protected_session = (function() {
|
|||||||
if (requireProtectedSession && !isProtectedSessionAvailable()) {
|
if (requireProtectedSession && !isProtectedSessionAvailable()) {
|
||||||
protectedSessionDeferred = dfd;
|
protectedSessionDeferred = dfd;
|
||||||
|
|
||||||
noteDetailWrapperEl.hide();
|
if (noteTree.getCurrentNode().data.isProtected) {
|
||||||
|
$noteDetailWrapper.hide();
|
||||||
|
}
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: modal,
|
modal: modal,
|
||||||
width: 400,
|
width: 400,
|
||||||
open: () => {
|
open: () => {
|
||||||
@@ -46,8 +48,8 @@ const protected_session = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupProtectedSession() {
|
async function setupProtectedSession() {
|
||||||
const password = passwordEl.val();
|
const password = $password.val();
|
||||||
passwordEl.val("");
|
$password.val("");
|
||||||
|
|
||||||
const response = await enterProtectedSession(password);
|
const response = await enterProtectedSession(password);
|
||||||
|
|
||||||
@@ -58,15 +60,15 @@ const protected_session = (function() {
|
|||||||
|
|
||||||
protectedSessionId = response.protectedSessionId;
|
protectedSessionId = response.protectedSessionId;
|
||||||
|
|
||||||
dialogEl.dialog("close");
|
$dialog.dialog("close");
|
||||||
|
|
||||||
noteEditor.reload();
|
noteEditor.reload();
|
||||||
noteTree.reload();
|
noteTree.reload();
|
||||||
|
|
||||||
if (protectedSessionDeferred !== null) {
|
if (protectedSessionDeferred !== null) {
|
||||||
ensureDialogIsClosed(dialogEl, passwordEl);
|
ensureDialogIsClosed($dialog, $password);
|
||||||
|
|
||||||
noteDetailWrapperEl.show();
|
$noteDetailWrapper.show();
|
||||||
|
|
||||||
protectedSessionDeferred.resolve();
|
protectedSessionDeferred.resolve();
|
||||||
|
|
||||||
@@ -77,11 +79,11 @@ const protected_session = (function() {
|
|||||||
function ensureDialogIsClosed() {
|
function ensureDialogIsClosed() {
|
||||||
// this may fal if the dialog has not been previously opened
|
// this may fal if the dialog has not been previously opened
|
||||||
try {
|
try {
|
||||||
dialogEl.dialog('close');
|
$dialog.dialog('close');
|
||||||
}
|
}
|
||||||
catch (e) {}
|
catch (e) {}
|
||||||
|
|
||||||
passwordEl.val('');
|
$password.val('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enterProtectedSession(password) {
|
async function enterProtectedSession(password) {
|
||||||
@@ -155,7 +157,7 @@ const protected_session = (function() {
|
|||||||
noteEditor.reload();
|
noteEditor.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
passwordFormEl.submit(() => {
|
$passwordForm.submit(() => {
|
||||||
setupProtectedSession();
|
setupProtectedSession();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const searchTree = (function() {
|
const searchTree = (function() {
|
||||||
const treeEl = $("#tree");
|
const $tree = $("#tree");
|
||||||
const searchInputEl = $("input[name='search-text']");
|
const $searchInput = $("input[name='search-text']");
|
||||||
const resetSearchButton = $("button#reset-search-button");
|
const $resetSearchButton = $("button#reset-search-button");
|
||||||
const searchBoxEl = $("#search-box");
|
const $searchBox = $("#search-box");
|
||||||
|
|
||||||
resetSearchButton.click(resetSearch);
|
$resetSearchButton.click(resetSearch);
|
||||||
|
|
||||||
function toggleSearch() {
|
function toggleSearch() {
|
||||||
if (searchBoxEl.is(":hidden")) {
|
if ($searchBox.is(":hidden")) {
|
||||||
searchBoxEl.show();
|
$searchBox.show();
|
||||||
searchInputEl.focus();
|
$searchInput.focus();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
resetSearch();
|
resetSearch();
|
||||||
|
|
||||||
searchBoxEl.hide();
|
$searchBox.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSearch() {
|
function resetSearch() {
|
||||||
searchInputEl.val("");
|
$searchInput.val("");
|
||||||
|
|
||||||
getTree().clearFilter();
|
getTree().clearFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTree() {
|
function getTree() {
|
||||||
return treeEl.fancytree('getTree');
|
return $tree.fancytree('getTree');
|
||||||
}
|
}
|
||||||
|
|
||||||
searchInputEl.keyup(async e => {
|
$searchInput.keyup(async e => {
|
||||||
const searchText = searchInputEl.val();
|
const searchText = $searchInput.val();
|
||||||
|
|
||||||
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
|
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
|
||||||
resetSearchButton.click();
|
$resetSearchButton.click();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,16 +31,6 @@ const server = (function() {
|
|||||||
return await call('DELETE', url);
|
return await call('DELETE', url);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exec(params, script) {
|
|
||||||
if (typeof script === "function") {
|
|
||||||
script = script.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = await post('script/exec', { script: script, params: params });
|
|
||||||
|
|
||||||
return ret.executionResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
const reqResolves = {};
|
const reqResolves = {};
|
||||||
|
|
||||||
@@ -104,6 +94,8 @@ const server = (function() {
|
|||||||
post,
|
post,
|
||||||
put,
|
put,
|
||||||
remove,
|
remove,
|
||||||
exec
|
ajax,
|
||||||
|
// don't remove, used from CKEditor image upload!
|
||||||
|
getHeaders
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const treeUtils = (function() {
|
const treeUtils = (function() {
|
||||||
const treeEl = $("#tree");
|
const $tree = $("#tree");
|
||||||
|
|
||||||
function getParentProtectedStatus(node) {
|
function getParentProtectedStatus(node) {
|
||||||
return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
|
return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeByKey(key) {
|
function getNodeByKey(key) {
|
||||||
return treeEl.fancytree('getNodeByKey', key);
|
return $tree.fancytree('getNodeByKey', key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteIdFromNotePath(notePath) {
|
function getNoteIdFromNotePath(notePath) {
|
||||||
|
|||||||
@@ -115,7 +115,108 @@ async function stopWatch(what, func) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeScript(script) {
|
async function executeBundle(bundle) {
|
||||||
// last \r\n is necessary if script contains line comment on its last line
|
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
|
||||||
eval("(async function() {" + script + "\r\n})()");
|
|
||||||
|
return await (function() { return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); }.call(apiContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValueWithWhitespace(val) {
|
||||||
|
return /[^\w_-]/.test(val) ? '"' + val + '"' : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAttribute(attr) {
|
||||||
|
let str = "@" + formatValueWithWhitespace(attr.name);
|
||||||
|
|
||||||
|
if (attr.value !== "") {
|
||||||
|
str += "=" + formatValueWithWhitespace(attr.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
"libraries/codemirror/addon/lint/lint.js",
|
||||||
|
"libraries/codemirror/addon/lint/eslint.js"
|
||||||
|
],
|
||||||
|
css: [
|
||||||
|
"libraries/codemirror/codemirror.css",
|
||||||
|
"libraries/codemirror/addon/lint/lint.css"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const ESLINT = { js: [ "libraries/eslint.js" ] };
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicallyLoadedScripts = [];
|
||||||
|
|
||||||
|
async function requireScript(url) {
|
||||||
|
if (!dynamicallyLoadedScripts.includes(url)) {
|
||||||
|
dynamicallyLoadedScripts.push(url);
|
||||||
|
|
||||||
|
return await $.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHost() {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
return url.protocol + "//" + url.hostname + ":" + url.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
function download(url) {
|
||||||
|
if (isElectron()) {
|
||||||
|
const remote = require('electron').remote;
|
||||||
|
|
||||||
|
remote.getCurrentWebContents().downloadURL(url);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toObject(array, fn) {
|
||||||
|
const obj = {};
|
||||||
|
|
||||||
|
for (const item of array) {
|
||||||
|
const ret = fn(item);
|
||||||
|
|
||||||
|
obj[ret[0]] = ret[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
2
src/public/libraries/ckeditor/ckeditor.js
vendored
2
src/public/libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
@@ -102,18 +102,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentlyHighlighted = null;
|
|
||||||
function doMatchBrackets(cm) {
|
function doMatchBrackets(cm) {
|
||||||
cm.operation(function() {
|
cm.operation(function() {
|
||||||
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
|
if (cm.state.matchBrackets.currentlyHighlighted) {
|
||||||
currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
|
cm.state.matchBrackets.currentlyHighlighted();
|
||||||
|
cm.state.matchBrackets.currentlyHighlighted = null;
|
||||||
|
}
|
||||||
|
cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
|
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
|
||||||
if (old && old != CodeMirror.Init) {
|
if (old && old != CodeMirror.Init) {
|
||||||
cm.off("cursorActivity", doMatchBrackets);
|
cm.off("cursorActivity", doMatchBrackets);
|
||||||
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
|
if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
|
||||||
|
cm.state.matchBrackets.currentlyHighlighted();
|
||||||
|
cm.state.matchBrackets.currentlyHighlighted = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (val) {
|
if (val) {
|
||||||
cm.state.matchBrackets = typeof val == "object" ? val : {};
|
cm.state.matchBrackets = typeof val == "object" ? val : {};
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
var iter = new Iter(cm, start.line, 0);
|
var iter = new Iter(cm, start.line, 0);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
var openTag = toNextTag(iter), end;
|
var openTag = toNextTag(iter), end;
|
||||||
if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return;
|
if (!openTag || !(end = toTagEnd(iter)) || iter.line != start.line) return;
|
||||||
if (!openTag[1] && end != "selfClose") {
|
if (!openTag[1] && end != "selfClose") {
|
||||||
var startPos = Pos(iter.line, iter.ch);
|
var startPos = Pos(iter.line, iter.ch);
|
||||||
var endPos = findMatchingClose(iter, openTag[2]);
|
var endPos = findMatchingClose(iter, openTag[2]);
|
||||||
|
|||||||
92
src/public/libraries/codemirror/addon/lint/eslint.js
vendored
Normal file
92
src/public/libraries/codemirror/addon/lint/eslint.js
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
async function validatorHtml(text, options) {
|
||||||
|
const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text);
|
||||||
|
|
||||||
|
if (result !== null) {
|
||||||
|
// preceding code is copied over but any (non-newline) character is replaced with space
|
||||||
|
// this will preserve line numbers etc.
|
||||||
|
const prefix = text.substr(0, result.index).replace(/./g, " ");
|
||||||
|
|
||||||
|
const js = prefix + result[1];
|
||||||
|
|
||||||
|
return await validatorJavaScript(js, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validatorJavaScript(text, options) {
|
||||||
|
if (noteEditor.getCurrentNote().detail.mime === 'application/json') {
|
||||||
|
// eslint doesn't seem to validate pure JSON well
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
await requireLibrary(ESLINT);
|
||||||
|
|
||||||
|
if (text.length > 20000) {
|
||||||
|
console.log("Skipping linting because of large size: ", text.length);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = new eslint().verify(text, {
|
||||||
|
root: true,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2017
|
||||||
|
},
|
||||||
|
extends: ['eslint:recommended', 'airbnb-base'],
|
||||||
|
env: {
|
||||||
|
'node': true
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'func-names': 'off',
|
||||||
|
'comma-dangle': ['warn'],
|
||||||
|
'padded-blocks': 'off',
|
||||||
|
'linebreak-style': 'off',
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
'no-unused-vars': ['warn', { vars: 'local', args: 'after-used' }],
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
|
'no-underscore-dangle': ['error', {'allow': ['_super', '_lookupFactory']}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
if (errors) {
|
||||||
|
parseErrors(errors, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.registerHelper("lint", "javascript", validatorJavaScript);
|
||||||
|
CodeMirror.registerHelper("lint", "html", validatorHtml);
|
||||||
|
|
||||||
|
function parseErrors(errors, output) {
|
||||||
|
for (const error of errors) {
|
||||||
|
const startLine = error.line - 1;
|
||||||
|
const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine;
|
||||||
|
const startCol = error.column - 1;
|
||||||
|
const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1;
|
||||||
|
|
||||||
|
output.push({
|
||||||
|
message: error.message,
|
||||||
|
severity: error.severity === 1 ? "warning" : "error",
|
||||||
|
from: CodeMirror.Pos(startLine, startCol),
|
||||||
|
to: CodeMirror.Pos(endLine, endCol)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
73
src/public/libraries/codemirror/addon/lint/lint.css
vendored
Normal file
73
src/public/libraries/codemirror/addon/lint/lint.css
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/* The lint marker gutter */
|
||||||
|
.CodeMirror-lint-markers {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-tooltip {
|
||||||
|
background-color: #ffd;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
color: black;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10pt;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 2px 5px;
|
||||||
|
position: fixed;
|
||||||
|
white-space: pre;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
z-index: 100;
|
||||||
|
max-width: 600px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .4s;
|
||||||
|
-moz-transition: opacity .4s;
|
||||||
|
-webkit-transition: opacity .4s;
|
||||||
|
-o-transition: opacity .4s;
|
||||||
|
-ms-transition: opacity .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
|
||||||
|
background-position: left bottom;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark-error {
|
||||||
|
background-image:
|
||||||
|
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark-warning {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
|
||||||
|
padding-left: 18px;
|
||||||
|
background-position: top left;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-multiple {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right bottom;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
}
|
||||||
252
src/public/libraries/codemirror/addon/lint/lint.js
vendored
Normal file
252
src/public/libraries/codemirror/addon/lint/lint.js
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
var GUTTER_ID = "CodeMirror-lint-markers";
|
||||||
|
|
||||||
|
function showTooltip(e, content) {
|
||||||
|
var tt = document.createElement("div");
|
||||||
|
tt.className = "CodeMirror-lint-tooltip";
|
||||||
|
tt.appendChild(content.cloneNode(true));
|
||||||
|
document.body.appendChild(tt);
|
||||||
|
|
||||||
|
function position(e) {
|
||||||
|
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
|
||||||
|
tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px";
|
||||||
|
tt.style.left = (e.clientX + 5) + "px";
|
||||||
|
}
|
||||||
|
CodeMirror.on(document, "mousemove", position);
|
||||||
|
position(e);
|
||||||
|
if (tt.style.opacity != null) tt.style.opacity = 1;
|
||||||
|
return tt;
|
||||||
|
}
|
||||||
|
function rm(elt) {
|
||||||
|
if (elt.parentNode) elt.parentNode.removeChild(elt);
|
||||||
|
}
|
||||||
|
function hideTooltip(tt) {
|
||||||
|
if (!tt.parentNode) return;
|
||||||
|
if (tt.style.opacity == null) rm(tt);
|
||||||
|
tt.style.opacity = 0;
|
||||||
|
setTimeout(function() { rm(tt); }, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTooltipFor(e, content, node) {
|
||||||
|
var tooltip = showTooltip(e, content);
|
||||||
|
function hide() {
|
||||||
|
CodeMirror.off(node, "mouseout", hide);
|
||||||
|
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
|
||||||
|
}
|
||||||
|
var poll = setInterval(function() {
|
||||||
|
if (tooltip) for (var n = node;; n = n.parentNode) {
|
||||||
|
if (n && n.nodeType == 11) n = n.host;
|
||||||
|
if (n == document.body) return;
|
||||||
|
if (!n) { hide(); break; }
|
||||||
|
}
|
||||||
|
if (!tooltip) return clearInterval(poll);
|
||||||
|
}, 400);
|
||||||
|
CodeMirror.on(node, "mouseout", hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LintState(cm, options, hasGutter) {
|
||||||
|
this.marked = [];
|
||||||
|
this.options = options;
|
||||||
|
this.timeout = null;
|
||||||
|
this.hasGutter = hasGutter;
|
||||||
|
this.onMouseOver = function(e) { onMouseOver(cm, e); };
|
||||||
|
this.waitingFor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOptions(_cm, options) {
|
||||||
|
if (options instanceof Function) return {getAnnotations: options};
|
||||||
|
if (!options || options === true) options = {};
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMarks(cm) {
|
||||||
|
var state = cm.state.lint;
|
||||||
|
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
|
||||||
|
for (var i = 0; i < state.marked.length; ++i)
|
||||||
|
state.marked[i].clear();
|
||||||
|
state.marked.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMarker(labels, severity, multiple, tooltips) {
|
||||||
|
var marker = document.createElement("div"), inner = marker;
|
||||||
|
marker.className = "CodeMirror-lint-marker-" + severity;
|
||||||
|
if (multiple) {
|
||||||
|
inner = marker.appendChild(document.createElement("div"));
|
||||||
|
inner.className = "CodeMirror-lint-marker-multiple";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
|
||||||
|
showTooltipFor(e, labels, inner);
|
||||||
|
});
|
||||||
|
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxSeverity(a, b) {
|
||||||
|
if (a == "error") return a;
|
||||||
|
else return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupByLine(annotations) {
|
||||||
|
var lines = [];
|
||||||
|
for (var i = 0; i < annotations.length; ++i) {
|
||||||
|
var ann = annotations[i], line = ann.from.line;
|
||||||
|
(lines[line] || (lines[line] = [])).push(ann);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function annotationTooltip(ann) {
|
||||||
|
var severity = ann.severity;
|
||||||
|
if (!severity) severity = "error";
|
||||||
|
var tip = document.createElement("div");
|
||||||
|
tip.className = "CodeMirror-lint-message-" + severity;
|
||||||
|
if (typeof ann.messageHTML != 'undefined') {
|
||||||
|
tip.innerHTML = ann.messageHTML;
|
||||||
|
} else {
|
||||||
|
tip.appendChild(document.createTextNode(ann.message));
|
||||||
|
}
|
||||||
|
return tip;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lintAsync(cm, getAnnotations, passOptions) {
|
||||||
|
var state = cm.state.lint
|
||||||
|
var id = ++state.waitingFor
|
||||||
|
function abort() {
|
||||||
|
id = -1
|
||||||
|
cm.off("change", abort)
|
||||||
|
}
|
||||||
|
cm.on("change", abort)
|
||||||
|
getAnnotations(cm.getValue(), function(annotations, arg2) {
|
||||||
|
cm.off("change", abort)
|
||||||
|
if (state.waitingFor != id) return
|
||||||
|
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
|
||||||
|
cm.operation(function() {updateLinting(cm, annotations)})
|
||||||
|
}, passOptions, cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLinting(cm) {
|
||||||
|
var state = cm.state.lint, options = state.options;
|
||||||
|
/*
|
||||||
|
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
|
||||||
|
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
|
||||||
|
*/
|
||||||
|
var passOptions = options.options || options;
|
||||||
|
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
|
||||||
|
if (!getAnnotations) return;
|
||||||
|
if (options.async || getAnnotations.async) {
|
||||||
|
lintAsync(cm, getAnnotations, passOptions)
|
||||||
|
} else {
|
||||||
|
var annotations = getAnnotations(cm.getValue(), passOptions, cm);
|
||||||
|
if (!annotations) return;
|
||||||
|
if (annotations.then) annotations.then(function(issues) {
|
||||||
|
cm.operation(function() {updateLinting(cm, issues)})
|
||||||
|
});
|
||||||
|
else cm.operation(function() {updateLinting(cm, annotations)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLinting(cm, annotationsNotSorted) {
|
||||||
|
clearMarks(cm);
|
||||||
|
var state = cm.state.lint, options = state.options;
|
||||||
|
|
||||||
|
var annotations = groupByLine(annotationsNotSorted);
|
||||||
|
|
||||||
|
for (var line = 0; line < annotations.length; ++line) {
|
||||||
|
var anns = annotations[line];
|
||||||
|
if (!anns) continue;
|
||||||
|
|
||||||
|
var maxSeverity = null;
|
||||||
|
var tipLabel = state.hasGutter && document.createDocumentFragment();
|
||||||
|
|
||||||
|
for (var i = 0; i < anns.length; ++i) {
|
||||||
|
var ann = anns[i];
|
||||||
|
var severity = ann.severity;
|
||||||
|
if (!severity) severity = "error";
|
||||||
|
maxSeverity = getMaxSeverity(maxSeverity, severity);
|
||||||
|
|
||||||
|
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
|
||||||
|
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
|
||||||
|
|
||||||
|
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
|
||||||
|
className: "CodeMirror-lint-mark-" + severity,
|
||||||
|
__annotation: ann
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.hasGutter)
|
||||||
|
cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1,
|
||||||
|
state.options.tooltips));
|
||||||
|
}
|
||||||
|
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(cm) {
|
||||||
|
var state = cm.state.lint;
|
||||||
|
if (!state) return;
|
||||||
|
clearTimeout(state.timeout);
|
||||||
|
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function popupTooltips(annotations, e) {
|
||||||
|
var target = e.target || e.srcElement;
|
||||||
|
var tooltip = document.createDocumentFragment();
|
||||||
|
for (var i = 0; i < annotations.length; i++) {
|
||||||
|
var ann = annotations[i];
|
||||||
|
tooltip.appendChild(annotationTooltip(ann));
|
||||||
|
}
|
||||||
|
showTooltipFor(e, tooltip, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseOver(cm, e) {
|
||||||
|
var target = e.target || e.srcElement;
|
||||||
|
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
|
||||||
|
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
|
||||||
|
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
|
||||||
|
|
||||||
|
var annotations = [];
|
||||||
|
for (var i = 0; i < spans.length; ++i) {
|
||||||
|
var ann = spans[i].__annotation;
|
||||||
|
if (ann) annotations.push(ann);
|
||||||
|
}
|
||||||
|
if (annotations.length) popupTooltips(annotations, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.defineOption("lint", false, function(cm, val, old) {
|
||||||
|
if (old && old != CodeMirror.Init) {
|
||||||
|
clearMarks(cm);
|
||||||
|
if (cm.state.lint.options.lintOnChange !== false)
|
||||||
|
cm.off("change", onChange);
|
||||||
|
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
|
||||||
|
clearTimeout(cm.state.lint.timeout);
|
||||||
|
delete cm.state.lint;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val) {
|
||||||
|
var gutters = cm.getOption("gutters"), hasLintGutter = false;
|
||||||
|
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
|
||||||
|
var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter);
|
||||||
|
if (state.options.lintOnChange !== false)
|
||||||
|
cm.on("change", onChange);
|
||||||
|
if (state.options.tooltips != false && state.options.tooltips != "gutter")
|
||||||
|
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
|
||||||
|
|
||||||
|
startLinting(cm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineExtension("performLint", function() {
|
||||||
|
if (this.state.lint) startLinting(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
var state = cm.state.matchHighlighter;
|
var state = cm.state.matchHighlighter;
|
||||||
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
|
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
|
||||||
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
|
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
|
||||||
var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
|
var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[+*?(){|^$]/g, "\\$&") + "\\b") : query;
|
||||||
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
|
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
|
||||||
{className: "CodeMirror-selection-highlight-scrollbar"});
|
{className: "CodeMirror-selection-highlight-scrollbar"});
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -846,6 +846,8 @@ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
|
|||||||
CodeMirror.defineMIME("text/javascript", "javascript");
|
CodeMirror.defineMIME("text/javascript", "javascript");
|
||||||
CodeMirror.defineMIME("text/ecmascript", "javascript");
|
CodeMirror.defineMIME("text/ecmascript", "javascript");
|
||||||
CodeMirror.defineMIME("application/javascript", "javascript");
|
CodeMirror.defineMIME("application/javascript", "javascript");
|
||||||
|
CodeMirror.defineMIME("application/javascript;env=frontend", "javascript");
|
||||||
|
CodeMirror.defineMIME("application/javascript;env=backend", "javascript");
|
||||||
CodeMirror.defineMIME("application/x-javascript", "javascript");
|
CodeMirror.defineMIME("application/x-javascript", "javascript");
|
||||||
CodeMirror.defineMIME("application/ecmascript", "javascript");
|
CodeMirror.defineMIME("application/ecmascript", "javascript");
|
||||||
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
|
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
|
||||||
|
|||||||
2
src/public/libraries/codemirror/mode/meta.js
vendored
2
src/public/libraries/codemirror/mode/meta.js
vendored
@@ -70,7 +70,7 @@
|
|||||||
{name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]},
|
{name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]},
|
||||||
{name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]},
|
{name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]},
|
||||||
{name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]},
|
{name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]},
|
||||||
{name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"],
|
{name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/javascript;env=frontend", "application/javascript;env=backend", "application/x-javascript", "application/ecmascript"],
|
||||||
mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]},
|
mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]},
|
||||||
{name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]},
|
{name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]},
|
||||||
{name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]},
|
{name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]},
|
||||||
|
|||||||
101349
src/public/libraries/eslint.js
Normal file
101349
src/public/libraries/eslint.js
Normal file
File diff suppressed because one or more lines are too long
@@ -5,12 +5,18 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header header"
|
grid-template-areas: "header header"
|
||||||
"tree-actions title"
|
"tree-actions title"
|
||||||
|
"search note-content"
|
||||||
"tree note-content"
|
"tree note-content"
|
||||||
"parent-list note-content";
|
"parent-list note-content"
|
||||||
|
"parent-list attribute-list";
|
||||||
grid-template-columns: 2fr 5fr;
|
grid-template-columns: 2fr 5fr;
|
||||||
grid-template-rows: auto
|
grid-template-rows: auto
|
||||||
auto
|
auto
|
||||||
1fr;
|
auto
|
||||||
|
1fr
|
||||||
|
auto
|
||||||
|
auto;
|
||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
}
|
}
|
||||||
@@ -66,6 +72,16 @@ span.fancytree-node.fancytree-folder.code > span.fancytree-icon {
|
|||||||
background-image: url("../images/icons/code-folder.png");
|
background-image: url("../images/icons/code-folder.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.fancytree-node.attachment > span.fancytree-icon {
|
||||||
|
background-position: 0 0;
|
||||||
|
background-image: url("../images/icons/paperclip.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
span.fancytree-node.render > span.fancytree-icon {
|
||||||
|
background-position: 0 0;
|
||||||
|
background-image: url("../images/icons/play.png");
|
||||||
|
}
|
||||||
|
|
||||||
span.fancytree-node.protected > span.fancytree-icon {
|
span.fancytree-node.protected > span.fancytree-icon {
|
||||||
filter: drop-shadow(2px 2px 2px black);
|
filter: drop-shadow(2px 2px 2px black);
|
||||||
}
|
}
|
||||||
@@ -97,6 +113,9 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
|||||||
|
|
||||||
.icon-action {
|
.icon-action {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#protect-button, #unprotect-button {
|
#protect-button, #unprotect-button {
|
||||||
@@ -134,6 +153,7 @@ div.ui-tooltip {
|
|||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
border-top: 2px solid #eee;
|
border-top: 2px solid #eee;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
|
grid-area: parent-list;
|
||||||
}
|
}
|
||||||
|
|
||||||
#parent-list ul {
|
#parent-list ul {
|
||||||
@@ -238,7 +258,7 @@ div.ui-tooltip {
|
|||||||
#note-id-display {
|
#note-id-display {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
bottom: 5px;
|
bottom: 8px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
color: lightgrey;
|
color: lightgrey;
|
||||||
}
|
}
|
||||||
@@ -249,4 +269,21 @@ div.ui-tooltip {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-matchhighlight {background-color: #eeeeee}
|
.cm-matchhighlight {background-color: #eeeeee}
|
||||||
|
|
||||||
|
#attribute-list {
|
||||||
|
grid-area: attribute-list;
|
||||||
|
color: #777777;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 5px; display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#attribute-list button {
|
||||||
|
padding: 2px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#attachment-table th, #attachment-table td {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
74
src/routes/api/attachments.js
Normal file
74
src/routes/api/attachments.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"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 protected_session = require('../../services/protected_session');
|
||||||
|
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]);
|
||||||
|
const protectedSessionId = req.query.protectedSessionId;
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
return res.status(404).send(`Note ${noteId} doesn't exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.isProtected) {
|
||||||
|
const dataKey = protected_session.getDataKeyForProtectedSessionId(protectedSessionId);
|
||||||
|
|
||||||
|
if (!dataKey) {
|
||||||
|
res.status(401).send("Protected session not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected_session.decryptNote(dataKey, note);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -7,14 +7,15 @@ const auth = require('../../services/auth');
|
|||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const wrap = require('express-promise-wrap').wrap;
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
const attributes = require('../../services/attributes');
|
||||||
|
|
||||||
router.get('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
|
res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const attributes = req.body;
|
const attributes = req.body;
|
||||||
const now = utils.nowDate();
|
const now = utils.nowDate();
|
||||||
@@ -22,19 +23,26 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next)
|
|||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
for (const attr of attributes) {
|
for (const attr of attributes) {
|
||||||
if (attr.attributeId) {
|
if (attr.attributeId) {
|
||||||
await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ? WHERE attributeId = ?",
|
await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ?, isDeleted = ?, position = ? WHERE attributeId = ?",
|
||||||
[attr.name, attr.value, now, attr.attributeId]);
|
[attr.name, attr.value, now, attr.isDeleted, attr.position, attr.attributeId]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
// if it was "created" and then immediatelly deleted, we just don't create it at all
|
||||||
|
if (attr.isDeleted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
attr.attributeId = utils.newAttributeId();
|
attr.attributeId = utils.newAttributeId();
|
||||||
|
|
||||||
await sql.insert("attributes", {
|
await sql.insert("attributes", {
|
||||||
attributeId: attr.attributeId,
|
attributeId: attr.attributeId,
|
||||||
noteId: noteId,
|
noteId: noteId,
|
||||||
name: attr.name,
|
name: attr.name,
|
||||||
value: attr.value,
|
value: attr.value,
|
||||||
dateCreated: now,
|
position: attr.position,
|
||||||
dateModified: now
|
dateCreated: now,
|
||||||
|
dateModified: now,
|
||||||
|
isDeleted: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +50,29 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
|
res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]));
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0");
|
||||||
|
|
||||||
|
for (const attr of attributes.BUILTIN_ATTRIBUTES) {
|
||||||
|
if (!names.includes(attr)) {
|
||||||
|
names.push(attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
res.send(names);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const attributeName = req.params.attributeName;
|
||||||
|
|
||||||
|
const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [attributeName]);
|
||||||
|
|
||||||
|
res.send(values);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -2,56 +2,79 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const rimraf = require('rimraf');
|
|
||||||
const fs = require('fs');
|
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const data_dir = require('../../services/data_dir');
|
|
||||||
const html = require('html');
|
const html = require('html');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
const wrap = require('express-promise-wrap').wrap;
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
const tar = require('tar-stream');
|
||||||
|
const sanitize = require("sanitize-filename");
|
||||||
|
const Repository = require("../../services/repository");
|
||||||
|
|
||||||
router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.get('/:noteId/', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
const repo = new Repository(req);
|
||||||
|
|
||||||
if (!fs.existsSync(data_dir.EXPORT_DIR)) {
|
|
||||||
fs.mkdirSync(data_dir.EXPORT_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
const completeExportDir = data_dir.EXPORT_DIR + '/' + directory;
|
|
||||||
|
|
||||||
if (fs.existsSync(completeExportDir)) {
|
|
||||||
rimraf.sync(completeExportDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.mkdirSync(completeExportDir);
|
|
||||||
|
|
||||||
const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]);
|
const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]);
|
||||||
|
|
||||||
await exportNote(noteTreeId, completeExportDir);
|
const pack = tar.pack();
|
||||||
|
|
||||||
res.send({});
|
const name = await exportNote(noteTreeId, '', pack, repo);
|
||||||
|
|
||||||
|
pack.finalize();
|
||||||
|
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="' + name + '.tar"');
|
||||||
|
res.setHeader('Content-Type', 'application/tar');
|
||||||
|
|
||||||
|
pack.pipe(res);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function exportNote(noteTreeId, dir) {
|
async function exportNote(noteTreeId, directory, pack, repo) {
|
||||||
const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]);
|
const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]);
|
||||||
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]);
|
const note = await repo.getEntity("SELECT notes.* FROM notes WHERE noteId = ?", [noteTree.noteId]);
|
||||||
|
|
||||||
const pos = (noteTree.notePosition + '').padStart(4, '0');
|
if (note.isProtected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync(dir + '/' + pos + '-' + note.title + '.html', html.prettyPrint(note.content, {indent_size: 2}));
|
const metadata = await getMetadata(note);
|
||||||
|
|
||||||
|
if (metadata.attributes.find(attr => attr.name === 'exclude_from_export')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataJson = JSON.stringify(metadata, null, '\t');
|
||||||
|
const childFileName = directory + sanitize(note.title);
|
||||||
|
|
||||||
|
pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson);
|
||||||
|
|
||||||
|
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
|
||||||
|
|
||||||
|
pack.entry({ name: childFileName + ".dat", size: content.length }, content);
|
||||||
|
|
||||||
const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]);
|
const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]);
|
||||||
|
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
const childrenDir = dir + '/' + pos + '-' + note.title;
|
|
||||||
|
|
||||||
fs.mkdirSync(childrenDir);
|
|
||||||
|
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
await exportNote(child.noteTreeId, childrenDir);
|
await exportNote(child.noteTreeId, childFileName + "/", pack, repo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return childFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMetadata(note) {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
title: note.title,
|
||||||
|
type: note.type,
|
||||||
|
mime: note.mime,
|
||||||
|
attributes: (await note.getAttributes()).map(attr => {
|
||||||
|
return {
|
||||||
|
name: attr.name,
|
||||||
|
value: attr.value
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,16 +4,8 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
const utils = require('../../services/utils');
|
const image = require('../../services/image');
|
||||||
const sync_table = require('../../services/sync_table');
|
|
||||||
const multer = require('multer')();
|
const multer = require('multer')();
|
||||||
const imagemin = require('imagemin');
|
|
||||||
const imageminMozJpeg = require('imagemin-mozjpeg');
|
|
||||||
const imageminPngQuant = require('imagemin-pngquant');
|
|
||||||
const imageminGifLossy = require('imagemin-giflossy');
|
|
||||||
const jimp = require('jimp');
|
|
||||||
const imageType = require('image-type');
|
|
||||||
const sanitizeFilename = require('sanitize-filename');
|
|
||||||
const wrap = require('express-promise-wrap').wrap;
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
|
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -49,45 +41,7 @@ router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async
|
|||||||
return res.status(400).send("Unknown image type: " + file.mimetype);
|
return res.status(400).send("Unknown image type: " + file.mimetype);
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = utils.nowDate();
|
const {fileName, imageId} = await image.saveImage(file, sourceId, noteId);
|
||||||
|
|
||||||
const resizedImage = await resize(file.buffer);
|
|
||||||
const optimizedImage = await optimize(resizedImage);
|
|
||||||
|
|
||||||
const imageFormat = imageType(optimizedImage);
|
|
||||||
|
|
||||||
const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, "");
|
|
||||||
const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext);
|
|
||||||
|
|
||||||
const imageId = utils.newImageId();
|
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
|
||||||
await sql.insert("images", {
|
|
||||||
imageId: imageId,
|
|
||||||
format: imageFormat.ext,
|
|
||||||
name: fileName,
|
|
||||||
checksum: utils.hash(optimizedImage),
|
|
||||||
data: optimizedImage,
|
|
||||||
isDeleted: 0,
|
|
||||||
dateModified: now,
|
|
||||||
dateCreated: now
|
|
||||||
});
|
|
||||||
|
|
||||||
await sync_table.addImageSync(imageId, sourceId);
|
|
||||||
|
|
||||||
const noteImageId = utils.newNoteImageId();
|
|
||||||
|
|
||||||
await sql.insert("note_images", {
|
|
||||||
noteImageId: noteImageId,
|
|
||||||
noteId: noteId,
|
|
||||||
imageId: imageId,
|
|
||||||
isDeleted: 0,
|
|
||||||
dateModified: now,
|
|
||||||
dateCreated: now
|
|
||||||
});
|
|
||||||
|
|
||||||
await sync_table.addNoteImageSync(noteImageId, sourceId);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
uploaded: true,
|
uploaded: true,
|
||||||
@@ -95,54 +49,4 @@ router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const MAX_SIZE = 1000;
|
|
||||||
const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs
|
|
||||||
|
|
||||||
async function resize(buffer) {
|
|
||||||
const image = await jimp.read(buffer);
|
|
||||||
|
|
||||||
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) {
|
|
||||||
image.resize(MAX_SIZE, jimp.AUTO);
|
|
||||||
}
|
|
||||||
else if (image.bitmap.height > MAX_SIZE) {
|
|
||||||
image.resize(jimp.AUTO, MAX_SIZE);
|
|
||||||
}
|
|
||||||
else if (buffer.byteLength <= MAX_BYTE_SIZE) {
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we do resizing with max quality which will be trimmed during optimization step next
|
|
||||||
image.quality(100);
|
|
||||||
|
|
||||||
// when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
|
|
||||||
image.background(0xFFFFFFFF);
|
|
||||||
|
|
||||||
// getBuffer doesn't support promises so this workaround
|
|
||||||
return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function optimize(buffer) {
|
|
||||||
return await imagemin.buffer(buffer, {
|
|
||||||
plugins: [
|
|
||||||
imageminMozJpeg({
|
|
||||||
quality: 50
|
|
||||||
}),
|
|
||||||
imageminPngQuant({
|
|
||||||
quality: "0-70"
|
|
||||||
}),
|
|
||||||
imageminGifLossy({
|
|
||||||
lossy: 80,
|
|
||||||
optimize: '3' // needs to be string
|
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -2,104 +2,136 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const fs = require('fs');
|
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const data_dir = require('../../services/data_dir');
|
|
||||||
const utils = require('../../services/utils');
|
|
||||||
const sync_table = require('../../services/sync_table');
|
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const attributes = require('../../services/attributes');
|
||||||
|
const notes = require('../../services/notes');
|
||||||
const wrap = require('express-promise-wrap').wrap;
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
const tar = require('tar-stream');
|
||||||
|
const multer = require('multer')();
|
||||||
|
const stream = require('stream');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
function getFileName(name) {
|
||||||
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
let key;
|
||||||
|
|
||||||
|
if (name.endsWith(".dat")) {
|
||||||
|
key = "data";
|
||||||
|
name = name.substr(0, name.length - 4);
|
||||||
|
}
|
||||||
|
else if (name.endsWith((".meta"))) {
|
||||||
|
key = "meta";
|
||||||
|
name = name.substr(0, name.length - 5);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("Unknown file type in import archive: " + name);
|
||||||
|
}
|
||||||
|
return {name, key};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseImportFile(file) {
|
||||||
|
const fileMap = {};
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
const extract = tar.extract();
|
||||||
|
|
||||||
|
extract.on('entry', function(header, stream, next) {
|
||||||
|
let {name, key} = getFileName(header.name);
|
||||||
|
|
||||||
|
let file = fileMap[name];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
file = fileMap[name] = {
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
let parentFileName = path.dirname(header.name);
|
||||||
|
|
||||||
|
if (parentFileName && parentFileName !== '.') {
|
||||||
|
fileMap[parentFileName].children.push(file);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
stream.on("data", function (chunk) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
// header is the tar header
|
||||||
|
// stream is the content body (might be an empty stream)
|
||||||
|
// call next when you are done with this entry
|
||||||
|
|
||||||
|
stream.on('end', function() {
|
||||||
|
file[key] = Buffer.concat(chunks);
|
||||||
|
|
||||||
|
if (key === "meta") {
|
||||||
|
file[key] = JSON.parse(file[key].toString("UTF-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
next(); // ready for next entry
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.resume(); // just auto drain the stream
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
extract.on('finish', function() {
|
||||||
|
resolve(files);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bufferStream = new stream.PassThrough();
|
||||||
|
bufferStream.end(file.buffer);
|
||||||
|
|
||||||
|
bufferStream.pipe(extract);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
|
||||||
|
const sourceId = req.headers.source_id;
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
const dir = data_dir.EXPORT_DIR + '/' + directory;
|
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
|
||||||
|
|
||||||
await sql.doInTransaction(async () => await importNotes(dir, parentNoteId));
|
if (!note) {
|
||||||
|
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await parseImportFile(file);
|
||||||
|
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await importNotes(files, parentNoteId, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function importNotes(dir, parentNoteId) {
|
async function importNotes(files, parentNoteId, sourceId) {
|
||||||
const parent = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
|
for (const file of files) {
|
||||||
|
if (file.meta.version !== 1) {
|
||||||
if (!parent) {
|
throw new Error("Can't read meta data version " + file.meta.version);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileList = fs.readdirSync(dir);
|
|
||||||
|
|
||||||
for (const file of fileList) {
|
|
||||||
const path = dir + '/' + file;
|
|
||||||
|
|
||||||
if (fs.lstatSync(path).isDirectory()) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file.endsWith('.html')) {
|
if (file.meta.type !== 'file') {
|
||||||
continue;
|
file.data = file.data.toString("UTF-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileNameWithoutExt = file.substr(0, file.length - 5);
|
const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, {
|
||||||
|
type: file.meta.type,
|
||||||
let noteTitle;
|
mime: file.meta.mime,
|
||||||
let notePos;
|
sourceId: sourceId
|
||||||
|
|
||||||
const match = fileNameWithoutExt.match(/^([0-9]{4})-(.*)$/);
|
|
||||||
if (match) {
|
|
||||||
notePos = parseInt(match[1]);
|
|
||||||
noteTitle = match[2];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let maxPos = await sql.getValue("SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]);
|
|
||||||
if (maxPos) {
|
|
||||||
notePos = maxPos + 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
notePos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
noteTitle = fileNameWithoutExt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const noteText = fs.readFileSync(path, "utf8");
|
|
||||||
|
|
||||||
const noteId = utils.newNoteId();
|
|
||||||
const noteTreeId = utils.newnoteRevisionId();
|
|
||||||
|
|
||||||
const now = utils.nowDate();
|
|
||||||
|
|
||||||
await sql.insert('note_tree', {
|
|
||||||
noteTreeId: noteTreeId,
|
|
||||||
noteId: noteId,
|
|
||||||
parentNoteId: parentNoteId,
|
|
||||||
notePosition: notePos,
|
|
||||||
isExpanded: 0,
|
|
||||||
isDeleted: 0,
|
|
||||||
dateModified: now
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await sync_table.addNoteTreeSync(noteTreeId);
|
for (const attr of file.meta.attributes) {
|
||||||
|
await attributes.createAttribute(noteId, attr.name, attr.value);
|
||||||
|
}
|
||||||
|
|
||||||
await sql.insert('notes', {
|
if (file.children.length > 0) {
|
||||||
noteId: noteId,
|
await importNotes(file.children, noteId, sourceId);
|
||||||
title: noteTitle,
|
|
||||||
content: noteText,
|
|
||||||
isDeleted: 0,
|
|
||||||
isProtected: 0,
|
|
||||||
type: 'text',
|
|
||||||
mime: 'text/html',
|
|
||||||
dateCreated: now,
|
|
||||||
dateModified: now
|
|
||||||
});
|
|
||||||
|
|
||||||
await sync_table.addNoteSync(noteId);
|
|
||||||
|
|
||||||
const noteDir = dir + '/' + fileNameWithoutExt;
|
|
||||||
|
|
||||||
if (fs.existsSync(noteDir) && fs.lstatSync(noteDir).isDirectory()) {
|
|
||||||
await importNotes(noteDir, noteId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const wrap = require('express-promise-wrap').wrap;
|
|||||||
router.post('/sync', wrap(async (req, res, next) => {
|
router.post('/sync', wrap(async (req, res, next) => {
|
||||||
const timestampStr = req.body.timestamp;
|
const timestampStr = req.body.timestamp;
|
||||||
|
|
||||||
const timestamp = utils.parseDate(timestampStr);
|
const timestamp = utils.parseDateTime(timestampStr);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const router = express.Router();
|
|||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const notes = require('../../services/notes');
|
const notes = require('../../services/notes');
|
||||||
|
const attributes = require('../../services/attributes');
|
||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const protected_session = require('../../services/protected_session');
|
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);
|
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({
|
res.send({
|
||||||
detail: detail
|
detail: detail,
|
||||||
|
attributes: attributeMap
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -58,15 +70,114 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const search = '%' + utils.sanitizeSql(req.query.search) + '%';
|
let {attrFilters, searchText} = parseFilters(req.query.search);
|
||||||
|
|
||||||
// searching in protected notes is pointless because of encryption
|
const {query, params} = getSearchQuery(attrFilters, searchText);
|
||||||
const noteIds = await sql.getColumn(`SELECT noteId FROM notes
|
|
||||||
WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]);
|
console.log(query, params);
|
||||||
|
|
||||||
|
const noteIds = await sql.getColumn(query, params);
|
||||||
|
|
||||||
res.send(noteIds);
|
res.send(noteIds);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function parseFilters(searchText) {
|
||||||
|
const attrFilters = [];
|
||||||
|
|
||||||
|
const attrRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i;
|
||||||
|
|
||||||
|
let match = attrRegex.exec(searchText);
|
||||||
|
|
||||||
|
function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; }
|
||||||
|
|
||||||
|
while (match != null) {
|
||||||
|
const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and';
|
||||||
|
const operator = match[3] === '!' ? 'not-exists' : 'exists';
|
||||||
|
|
||||||
|
attrFilters.push({
|
||||||
|
relation: relation,
|
||||||
|
name: trimQuotes(match[4]),
|
||||||
|
operator: match[6] !== undefined ? match[6] : operator,
|
||||||
|
value: match[7] !== undefined ? trimQuotes(match[7]) : null
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove attributes from further fulltext search
|
||||||
|
searchText = searchText.split(match[0]).join('');
|
||||||
|
|
||||||
|
match = attrRegex.exec(searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {attrFilters, searchText};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchQuery(attrFilters, searchText) {
|
||||||
|
const joins = [];
|
||||||
|
const joinParams = [];
|
||||||
|
let where = '1';
|
||||||
|
const whereParams = [];
|
||||||
|
|
||||||
|
let i = 1;
|
||||||
|
|
||||||
|
for (const filter of attrFilters) {
|
||||||
|
joins.push(`LEFT JOIN attributes AS attr${i} ON attr${i}.noteId = notes.noteId AND attr${i}.name = ?`);
|
||||||
|
joinParams.push(filter.name);
|
||||||
|
|
||||||
|
where += " " + filter.relation + " ";
|
||||||
|
|
||||||
|
if (filter.operator === 'exists') {
|
||||||
|
where += `attr${i}.attributeId IS NOT NULL`;
|
||||||
|
}
|
||||||
|
else if (filter.operator === 'not-exists') {
|
||||||
|
where += `attr${i}.attributeId IS NULL`;
|
||||||
|
}
|
||||||
|
else if (filter.operator === '=' || filter.operator === '!=') {
|
||||||
|
where += `attr${i}.value ${filter.operator} ?`;
|
||||||
|
whereParams.push(filter.value);
|
||||||
|
}
|
||||||
|
else if ([">", ">=", "<", "<="].includes(filter.operator)) {
|
||||||
|
const floatParam = parseFloat(filter.value);
|
||||||
|
|
||||||
|
if (isNaN(floatParam)) {
|
||||||
|
where += `attr${i}.value ${filter.operator} ?`;
|
||||||
|
whereParams.push(filter.value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
where += `CAST(attr${i}.value AS DECIMAL) ${filter.operator} ?`;
|
||||||
|
whereParams.push(floatParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("Unknown operator " + filter.operator);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchCondition = '';
|
||||||
|
const searchParams = [];
|
||||||
|
|
||||||
|
if (searchText.trim() !== '') {
|
||||||
|
// searching in protected notes is pointless because of encryption
|
||||||
|
searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))';
|
||||||
|
|
||||||
|
searchText = '%' + searchText.trim() + '%';
|
||||||
|
|
||||||
|
searchParams.push(searchText);
|
||||||
|
searchParams.push(searchText); // two occurences in searchCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `SELECT DISTINCT notes.noteId FROM notes
|
||||||
|
${joins.join('\r\n')}
|
||||||
|
WHERE
|
||||||
|
notes.isDeleted = 0
|
||||||
|
AND (${where})
|
||||||
|
${searchCondition}`;
|
||||||
|
|
||||||
|
const params = joinParams.concat(whereParams).concat(searchParams);
|
||||||
|
|
||||||
|
return { query, params };
|
||||||
|
}
|
||||||
|
|
||||||
router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ async function getRecentNotes() {
|
|||||||
recent_notes.isDeleted = 0
|
recent_notes.isDeleted = 0
|
||||||
AND note_tree.isDeleted = 0
|
AND note_tree.isDeleted = 0
|
||||||
ORDER BY
|
ORDER BY
|
||||||
dateAccessed DESC`);
|
dateAccessed DESC
|
||||||
|
LIMIT 200`);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,13 +4,23 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
const wrap = require('express-promise-wrap').wrap;
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
const notes = require('../../services/notes');
|
|
||||||
const attributes = require('../../services/attributes');
|
const attributes = require('../../services/attributes');
|
||||||
const script = require('../../services/script');
|
const script = require('../../services/script');
|
||||||
const Repository = require('../../services/repository');
|
const Repository = require('../../services/repository');
|
||||||
|
|
||||||
router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const ret = await script.executeScript(req, req.body.script, req.body.params);
|
const ret = await script.executeScript(req, req.body.script, req.body.params, req.body.startNoteId, req.body.currentNoteId);
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
executionResult: ret
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/run/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const repository = new Repository(req);
|
||||||
|
const note = await repository.getNote(req.params.noteId);
|
||||||
|
|
||||||
|
const ret = await script.executeNote(req, note);
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
executionResult: ret
|
executionResult: ret
|
||||||
@@ -18,65 +28,28 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
|
|
||||||
const repository = new Repository(req);
|
const repository = new Repository(req);
|
||||||
|
const notes = await attributes.getNotesWithAttribute(repository, "run", "frontend_startup");
|
||||||
|
|
||||||
const scripts = [];
|
const scripts = [];
|
||||||
|
|
||||||
for (const noteId of noteIds) {
|
for (const note of notes) {
|
||||||
scripts.push(await getNoteWithSubtreeScript(noteId, repository));
|
const bundle = await script.getScriptBundle(note);
|
||||||
|
|
||||||
|
if (bundle) {
|
||||||
|
scripts.push(bundle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(scripts);
|
res.send(scripts);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.get('/bundle/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
|
||||||
|
|
||||||
const repository = new Repository(req);
|
const repository = new Repository(req);
|
||||||
|
const note = await repository.getNote(req.params.noteId);
|
||||||
|
const bundle = await script.getScriptBundle(note);
|
||||||
|
|
||||||
const noteScript = (await repository.getNote(noteId)).content;
|
res.send(bundle);
|
||||||
|
|
||||||
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
|
|
||||||
|
|
||||||
res.send(subTreeScripts + noteScript);
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function getNoteWithSubtreeScript(noteId, repository) {
|
|
||||||
const noteScript = (await repository.getNote(noteId)).content;
|
|
||||||
|
|
||||||
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
|
|
||||||
|
|
||||||
return subTreeScripts + noteScript;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSubTreeScripts(parentId, includedNoteIds, repository) {
|
|
||||||
const children = await repository.getEntities(`
|
|
||||||
SELECT notes.*
|
|
||||||
FROM notes JOIN note_tree USING(noteId)
|
|
||||||
WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0
|
|
||||||
AND note_tree.parentNoteId = ? AND notes.type = 'code'
|
|
||||||
AND (notes.mime = 'application/javascript' OR notes.mime = 'text/html')`, [parentId]);
|
|
||||||
|
|
||||||
let script = "\r\n";
|
|
||||||
|
|
||||||
for (const child of children) {
|
|
||||||
if (includedNoteIds.includes(child.noteId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
includedNoteIds.push(child.noteId);
|
|
||||||
|
|
||||||
script += await getSubTreeScripts(child.noteId, includedNoteIds, repository);
|
|
||||||
|
|
||||||
if (child.mime === 'application/javascript') {
|
|
||||||
child.content = '<script>' + child.content + '</script>';
|
|
||||||
}
|
|
||||||
|
|
||||||
script += child.content + "\r\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return script;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
106
src/routes/api/sender.js
Normal file
106
src/routes/api/sender.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const image = require('../../services/image');
|
||||||
|
const utils = require('../../services/utils');
|
||||||
|
const date_notes = require('../../services/date_notes');
|
||||||
|
const sql = require('../../services/sql');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
const notes = require('../../services/notes');
|
||||||
|
const multer = require('multer')();
|
||||||
|
const password_encryption = require('../../services/password_encryption');
|
||||||
|
const options = require('../../services/options');
|
||||||
|
const sync_table = require('../../services/sync_table');
|
||||||
|
|
||||||
|
router.post('/login', wrap(async (req, res, next) => {
|
||||||
|
const username = req.body.username;
|
||||||
|
const password = req.body.password;
|
||||||
|
|
||||||
|
const isUsernameValid = username === await options.getOption('username');
|
||||||
|
const isPasswordValid = await password_encryption.verifyPassword(password);
|
||||||
|
|
||||||
|
if (!isUsernameValid || !isPasswordValid) {
|
||||||
|
res.status(401).send("Incorrect username/password");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const token = utils.randomSecureToken();
|
||||||
|
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
const apiTokenId = utils.newApiTokenId();
|
||||||
|
|
||||||
|
await sql.insert("api_tokens", {
|
||||||
|
apiTokenId: apiTokenId,
|
||||||
|
token: token,
|
||||||
|
dateCreated: utils.nowDate(),
|
||||||
|
isDeleted: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addApiTokenSync(apiTokenId);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
token: token
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function checkSenderToken(req, res, next) {
|
||||||
|
const token = req.headers.authorization;
|
||||||
|
|
||||||
|
if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
|
||||||
|
res.status(401).send("Not authorized");
|
||||||
|
}
|
||||||
|
else if (await sql.isDbUpToDate()) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.status(409).send("Mismatched app versions"); // need better response than that
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/image', checkSenderToken, multer.single('upload'), wrap(async (req, res, next) => {
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
|
if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) {
|
||||||
|
return res.status(400).send("Unknown image type: " + file.mimetype);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentNoteId = await date_notes.getDateNoteId(req.headers['x-local-date']);
|
||||||
|
|
||||||
|
const noteId = (await notes.createNewNote(parentNoteId, {
|
||||||
|
title: "Sender image",
|
||||||
|
content: "",
|
||||||
|
target: 'into',
|
||||||
|
isProtected: false,
|
||||||
|
type: 'text',
|
||||||
|
mime: 'text/html'
|
||||||
|
})).noteId;
|
||||||
|
|
||||||
|
const {fileName, imageId} = await image.saveImage(file, null, noteId);
|
||||||
|
|
||||||
|
const url = `/api/images/${imageId}/${fileName}`;
|
||||||
|
|
||||||
|
const content = `<img src="${url}"/>`;
|
||||||
|
|
||||||
|
await sql.execute("UPDATE notes SET content = ? WHERE noteId = ?", [content, noteId]);
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/note', checkSenderToken, wrap(async (req, res, next) => {
|
||||||
|
const parentNoteId = await date_notes.getDateNoteId(req.headers['x-local-date']);
|
||||||
|
|
||||||
|
await notes.createNewNote(parentNoteId, {
|
||||||
|
title: req.body.title,
|
||||||
|
content: req.body.content,
|
||||||
|
target: 'into',
|
||||||
|
isProtected: false,
|
||||||
|
type: 'text',
|
||||||
|
mime: 'text/html'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
}));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -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) => {
|
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
||||||
|
|
||||||
|
sync.serializeNoteContentBuffer(entity);
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
entity: await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId])
|
entity: entity
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -147,6 +150,12 @@ router.get('/attributes/:attributeId', auth.checkApiAuth, wrap(async (req, res,
|
|||||||
res.send(await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]));
|
res.send(await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
router.get('/api_tokens/:apiTokenId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const apiTokenId = req.params.apiTokenId;
|
||||||
|
|
||||||
|
res.send(await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [apiTokenId]));
|
||||||
|
}));
|
||||||
|
|
||||||
router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
@@ -201,4 +210,10 @@ router.put('/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
|||||||
res.send({});
|
res.send({});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
router.put('/api_tokens', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
await syncUpdate.updateApiToken(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -6,6 +6,7 @@ const sql = require('../../services/sql');
|
|||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const config = require('../../services/config');
|
||||||
const protected_session = require('../../services/protected_session');
|
const protected_session = require('../../services/protected_session');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
const wrap = require('express-promise-wrap').wrap;
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
@@ -29,8 +30,21 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
|||||||
|
|
||||||
protected_session.decryptNotes(req, notes);
|
protected_session.decryptNotes(req, notes);
|
||||||
|
|
||||||
|
const hiddenInAutocomplete = await sql.getColumn(`
|
||||||
|
SELECT
|
||||||
|
DISTINCT noteId
|
||||||
|
FROM
|
||||||
|
attributes
|
||||||
|
JOIN notes USING(noteId)
|
||||||
|
WHERE
|
||||||
|
attributes.name = 'hide_in_autocomplete'
|
||||||
|
AND attributes.isDeleted = 0
|
||||||
|
AND notes.isDeleted = 0`);
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
|
instanceName: config.General ? config.General.instanceName : null,
|
||||||
notes: notes,
|
notes: notes,
|
||||||
|
hiddenInAutocomplete: hiddenInAutocomplete,
|
||||||
start_note_path: await options.getOption('start_note_path')
|
start_note_path: await options.getOption('start_note_path')
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -61,10 +61,8 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap
|
|||||||
|
|
||||||
await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId);
|
await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId);
|
||||||
|
|
||||||
const now = utils.nowDate();
|
|
||||||
|
|
||||||
await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?",
|
await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?",
|
||||||
[beforeNote.parentNoteId, beforeNote.notePosition, now, noteTreeId]);
|
[beforeNote.parentNoteId, beforeNote.notePosition, utils.nowDate(), noteTreeId]);
|
||||||
|
|
||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,13 +5,32 @@ const router = express.Router();
|
|||||||
const auth = require('../services/auth');
|
const auth = require('../services/auth');
|
||||||
const source_id = require('../services/source_id');
|
const source_id = require('../services/source_id');
|
||||||
const sql = require('../services/sql');
|
const sql = require('../services/sql');
|
||||||
|
const Repository = require('../services/repository');
|
||||||
|
const attributes = require('../services/attributes');
|
||||||
const wrap = require('express-promise-wrap').wrap;
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkAuth, wrap(async (req, res, next) => {
|
router.get('', auth.checkAuth, wrap(async (req, res, next) => {
|
||||||
|
const repository = new Repository(req);
|
||||||
|
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
sourceId: await source_id.generateSourceId(),
|
sourceId: await source_id.generateSourceId(),
|
||||||
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync")
|
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
|
||||||
|
appCss: await getAppCss(repository)
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
async function getAppCss(repository) {
|
||||||
|
let css = '';
|
||||||
|
const notes = attributes.getNotesWithAttribute(repository, 'app_css');
|
||||||
|
|
||||||
|
for (const note of await notes) {
|
||||||
|
css += `/* ${note.noteId} */
|
||||||
|
${note.content}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return css;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const cleanupRoute = require('./api/cleanup');
|
|||||||
const imageRoute = require('./api/image');
|
const imageRoute = require('./api/image');
|
||||||
const attributesRoute = require('./api/attributes');
|
const attributesRoute = require('./api/attributes');
|
||||||
const scriptRoute = require('./api/script');
|
const scriptRoute = require('./api/script');
|
||||||
|
const senderRoute = require('./api/sender');
|
||||||
|
const attachmentsRoute = require('./api/attachments');
|
||||||
|
|
||||||
function register(app) {
|
function register(app) {
|
||||||
app.use('/', indexRoute);
|
app.use('/', indexRoute);
|
||||||
@@ -40,7 +42,7 @@ function register(app) {
|
|||||||
app.use('/api/notes', notesApiRoute);
|
app.use('/api/notes', notesApiRoute);
|
||||||
app.use('/api/tree', treeChangesApiRoute);
|
app.use('/api/tree', treeChangesApiRoute);
|
||||||
app.use('/api/notes', cloningApiRoute);
|
app.use('/api/notes', cloningApiRoute);
|
||||||
app.use('/api/notes', attributesRoute);
|
app.use('/api', attributesRoute);
|
||||||
app.use('/api/notes-history', noteHistoryApiRoute);
|
app.use('/api/notes-history', noteHistoryApiRoute);
|
||||||
app.use('/api/recent-changes', recentChangesApiRoute);
|
app.use('/api/recent-changes', recentChangesApiRoute);
|
||||||
app.use('/api/settings', settingsApiRoute);
|
app.use('/api/settings', settingsApiRoute);
|
||||||
@@ -59,6 +61,8 @@ function register(app) {
|
|||||||
app.use('/api/cleanup', cleanupRoute);
|
app.use('/api/cleanup', cleanupRoute);
|
||||||
app.use('/api/images', imageRoute);
|
app.use('/api/images', imageRoute);
|
||||||
app.use('/api/script', scriptRoute);
|
app.use('/api/script', scriptRoute);
|
||||||
|
app.use('/api/sender', senderRoute);
|
||||||
|
app.use('/api/attachments', attachmentsRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
BIN
src/scripts/Reddit Importer.tar
Normal file
BIN
src/scripts/Reddit Importer.tar
Normal file
Binary file not shown.
BIN
src/scripts/Today.tar
Normal file
BIN
src/scripts/Today.tar
Normal file
Binary file not shown.
BIN
src/scripts/Weight Tracker.tar
Normal file
BIN
src/scripts/Weight Tracker.tar
Normal file
Binary file not shown.
@@ -1,11 +0,0 @@
|
|||||||
api.addButtonToToolbar('go-today', $('<button class="btn btn-xs" onclick="goToday();"><span class="ui-icon ui-icon-calendar"></span> Today</button>'));
|
|
||||||
|
|
||||||
window.goToday = async function() {
|
|
||||||
const todayDateStr = formatDateISO(new Date());
|
|
||||||
|
|
||||||
const todayNoteId = await server.exec([todayDateStr], async todayDateStr => {
|
|
||||||
return await this.getDateNoteId(todayDateStr);
|
|
||||||
});
|
|
||||||
|
|
||||||
api.activateNote(todayNoteId);
|
|
||||||
};
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<form id="weight-form" style="display: flex; width: 500px; 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;" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Add</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<canvas id="canvas"></canvas>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(async function() {
|
|
||||||
const dateEl = $("#weight-date");
|
|
||||||
const weightEl = $("#weight");
|
|
||||||
|
|
||||||
dateEl.datepicker();
|
|
||||||
dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd');
|
|
||||||
dateEl.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);
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
showMessage("Weight has been saved");
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: data.map(row => row.date),
|
|
||||||
datasets: [{
|
|
||||||
label: "Weight",
|
|
||||||
backgroundColor: 'red',
|
|
||||||
borderColor: 'red',
|
|
||||||
data: data.map(row => row.weight),
|
|
||||||
fill: false
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var ctx = $("#canvas")[0].getContext("2d");
|
|
||||||
new Chart(ctx, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#weight-form").submit(event => {
|
|
||||||
saveWeight();
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
drawChart();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
const build = require('./build');
|
const build = require('./build');
|
||||||
const packageJson = require('../../package');
|
const packageJson = require('../../package');
|
||||||
|
|
||||||
const APP_DB_VERSION = 71;
|
const APP_DB_VERSION = 78;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
app_version: packageJson.version,
|
app_version: packageJson.version,
|
||||||
|
|||||||
@@ -3,56 +3,76 @@
|
|||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const sync_table = require('./sync_table');
|
const sync_table = require('./sync_table');
|
||||||
const Repository = require('./repository');
|
|
||||||
|
const BUILTIN_ATTRIBUTES = [
|
||||||
|
'frontend_startup',
|
||||||
|
'backend_startup',
|
||||||
|
'disable_versioning',
|
||||||
|
'calendar_root',
|
||||||
|
'hide_in_autocomplete',
|
||||||
|
'exclude_from_export',
|
||||||
|
'run',
|
||||||
|
'manual_transaction_handling',
|
||||||
|
'disable_inclusion',
|
||||||
|
'app_css'
|
||||||
|
];
|
||||||
|
|
||||||
async function getNoteAttributeMap(noteId) {
|
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) {
|
async function getNoteIdWithAttribute(name, value) {
|
||||||
return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)
|
return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)
|
||||||
WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
|
WHERE notes.isDeleted = 0
|
||||||
|
AND attributes.isDeleted = 0
|
||||||
|
AND attributes.name = ?
|
||||||
|
AND attributes.value = ?`, [name, value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNotesWithAttribute(dataKey, name, value) {
|
async function getNotesWithAttribute(repository, name, value) {
|
||||||
const repository = new Repository(dataKey);
|
|
||||||
|
|
||||||
let notes;
|
let notes;
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
|
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
|
||||||
WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
|
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
|
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
|
||||||
WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]);
|
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return notes;
|
return notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNoteWithAttribute(dataKey, name, value) {
|
async function getNoteWithAttribute(repository, name, value) {
|
||||||
const notes = getNotesWithAttribute(dataKey, name, value);
|
const notes = getNotesWithAttribute(repository, name, value);
|
||||||
|
|
||||||
return notes.length > 0 ? notes[0] : null;
|
return notes.length > 0 ? notes[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNoteIdsWithAttribute(name) {
|
async function getNoteIdsWithAttribute(name) {
|
||||||
return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId)
|
return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId)
|
||||||
WHERE notes.isDeleted = 0 AND attributes.name = ?`, [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 now = utils.nowDate();
|
||||||
const attributeId = utils.newAttributeId();
|
const attributeId = utils.newAttributeId();
|
||||||
|
const position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [noteId]);
|
||||||
|
|
||||||
await sql.insert("attributes", {
|
await sql.insert("attributes", {
|
||||||
attributeId: attributeId,
|
attributeId: attributeId,
|
||||||
noteId: noteId,
|
noteId: noteId,
|
||||||
name: name,
|
name: name,
|
||||||
value: value,
|
value: value,
|
||||||
|
position: position,
|
||||||
dateModified: now,
|
dateModified: now,
|
||||||
dateCreated: now
|
dateCreated: now,
|
||||||
|
isDeleted: false
|
||||||
});
|
});
|
||||||
|
|
||||||
await sync_table.addAttributeSync(attributeId, sourceId);
|
await sync_table.addAttributeSync(attributeId, sourceId);
|
||||||
@@ -64,5 +84,6 @@ module.exports = {
|
|||||||
getNotesWithAttribute,
|
getNotesWithAttribute,
|
||||||
getNoteWithAttribute,
|
getNoteWithAttribute,
|
||||||
getNoteIdsWithAttribute,
|
getNoteIdsWithAttribute,
|
||||||
createAttribute
|
createAttribute,
|
||||||
|
BUILTIN_ATTRIBUTES
|
||||||
};
|
};
|
||||||
@@ -10,7 +10,7 @@ const sync_mutex = require('./sync_mutex');
|
|||||||
|
|
||||||
async function regularBackup() {
|
async function regularBackup() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const lastBackupDate = utils.parseDate(await options.getOption('last_backup_date'));
|
const lastBackupDate = utils.parseDateTime(await options.getOption('last_backup_date'));
|
||||||
|
|
||||||
console.log(lastBackupDate);
|
console.log(lastBackupDate);
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ async function runAllChecks() {
|
|||||||
FROM
|
FROM
|
||||||
notes
|
notes
|
||||||
WHERE
|
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);
|
"Note has invalid type", errorList);
|
||||||
|
|
||||||
await runSyncRowChecks("notes", "noteId", errorList);
|
await runSyncRowChecks("notes", "noteId", errorList);
|
||||||
@@ -223,6 +223,8 @@ async function runAllChecks() {
|
|||||||
await runSyncRowChecks("recent_notes", "noteTreeId", errorList);
|
await runSyncRowChecks("recent_notes", "noteTreeId", errorList);
|
||||||
await runSyncRowChecks("images", "imageId", errorList);
|
await runSyncRowChecks("images", "imageId", errorList);
|
||||||
await runSyncRowChecks("note_images", "noteImageId", errorList);
|
await runSyncRowChecks("note_images", "noteImageId", errorList);
|
||||||
|
await runSyncRowChecks("attributes", "attributeId", errorList);
|
||||||
|
await runSyncRowChecks("api_tokens", "apiTokenId", errorList);
|
||||||
|
|
||||||
if (errorList.length === 0) {
|
if (errorList.length === 0) {
|
||||||
// we run this only if basic checks passed since this assumes basic data consistency
|
// we run this only if basic checks passed since this assumes basic data consistency
|
||||||
|
|||||||
@@ -20,6 +20,5 @@ module.exports = {
|
|||||||
DOCUMENT_PATH,
|
DOCUMENT_PATH,
|
||||||
BACKUP_DIR,
|
BACKUP_DIR,
|
||||||
LOG_DIR,
|
LOG_DIR,
|
||||||
EXPORT_DIR,
|
|
||||||
ANONYMIZED_DB_DIR
|
ANONYMIZED_DB_DIR
|
||||||
};
|
};
|
||||||
@@ -88,7 +88,7 @@ function noteTitleIv(iv) {
|
|||||||
return "0" + iv;
|
return "0" + iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteTextIv(iv) {
|
function noteContentIv(iv) {
|
||||||
return "1" + iv;
|
return "1" + iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,5 +97,5 @@ module.exports = {
|
|||||||
decrypt,
|
decrypt,
|
||||||
decryptString,
|
decryptString,
|
||||||
noteTitleIv,
|
noteTitleIv,
|
||||||
noteTextIv
|
noteContentIv
|
||||||
};
|
};
|
||||||
@@ -3,12 +3,16 @@
|
|||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const notes = require('./notes');
|
const notes = require('./notes');
|
||||||
const attributes = require('./attributes');
|
const attributes = require('./attributes');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
const CALENDAR_ROOT_ATTRIBUTE = 'calendar_root';
|
const CALENDAR_ROOT_ATTRIBUTE = 'calendar_root';
|
||||||
const YEAR_ATTRIBUTE = 'year_note';
|
const YEAR_ATTRIBUTE = 'year_note';
|
||||||
const MONTH_ATTRIBUTE = 'month_note';
|
const MONTH_ATTRIBUTE = 'month_note';
|
||||||
const DATE_ATTRIBUTE = 'date_note';
|
const DATE_ATTRIBUTE = 'date_note';
|
||||||
|
|
||||||
|
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
||||||
|
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
||||||
|
|
||||||
async function createNote(parentNoteId, noteTitle, noteText) {
|
async function createNote(parentNoteId, noteTitle, noteText) {
|
||||||
return (await notes.createNewNote(parentNoteId, {
|
return (await notes.createNewNote(parentNoteId, {
|
||||||
title: noteTitle,
|
title: noteTitle,
|
||||||
@@ -25,7 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
|
|||||||
AND note_tree.isDeleted = 0`, [parentNoteId]);
|
AND note_tree.isDeleted = 0`, [parentNoteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRootNoteId() {
|
async function getRootCalendarNoteId() {
|
||||||
let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)
|
let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)
|
||||||
WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`);
|
WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`);
|
||||||
|
|
||||||
@@ -72,7 +76,11 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) {
|
|||||||
monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber);
|
monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber);
|
||||||
|
|
||||||
if (!monthNoteId) {
|
if (!monthNoteId) {
|
||||||
monthNoteId = await createNote(yearNoteId, monthNumber);
|
const dateObj = utils.parseDate(dateTimeStr);
|
||||||
|
|
||||||
|
const noteTitle = monthNumber + " - " + MONTHS[dateObj.getMonth()];
|
||||||
|
|
||||||
|
monthNoteId = await createNote(yearNoteId, noteTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
await attributes.createAttribute(monthNoteId, MONTH_ATTRIBUTE, monthStr);
|
await attributes.createAttribute(monthNoteId, MONTH_ATTRIBUTE, monthStr);
|
||||||
@@ -83,7 +91,7 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) {
|
|||||||
|
|
||||||
async function getDateNoteId(dateTimeStr, rootNoteId = null) {
|
async function getDateNoteId(dateTimeStr, rootNoteId = null) {
|
||||||
if (!rootNoteId) {
|
if (!rootNoteId) {
|
||||||
rootNoteId = await getRootNoteId();
|
rootNoteId = await getRootCalendarNoteId();
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateStr = dateTimeStr.substr(0, 10);
|
const dateStr = dateTimeStr.substr(0, 10);
|
||||||
@@ -97,7 +105,11 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) {
|
|||||||
dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber);
|
dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber);
|
||||||
|
|
||||||
if (!dateNoteId) {
|
if (!dateNoteId) {
|
||||||
dateNoteId = await createNote(monthNoteId, dayNumber);
|
const dateObj = utils.parseDate(dateTimeStr);
|
||||||
|
|
||||||
|
const noteTitle = dayNumber + " - " + DAYS[dateObj.getDay()];
|
||||||
|
|
||||||
|
dateNoteId = await createNote(monthNoteId, noteTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
await attributes.createAttribute(dateNoteId, DATE_ATTRIBUTE, dateStr);
|
await attributes.createAttribute(dateNoteId, DATE_ATTRIBUTE, dateStr);
|
||||||
@@ -107,7 +119,7 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getRootNoteId,
|
getRootCalendarNoteId,
|
||||||
getYearNoteId,
|
getYearNoteId,
|
||||||
getMonthNoteId,
|
getMonthNoteId,
|
||||||
getDateNoteId
|
getDateNoteId
|
||||||
|
|||||||
108
src/services/image.js
Normal file
108
src/services/image.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const utils = require('./utils');
|
||||||
|
const sql = require('./sql');
|
||||||
|
const sync_table = require('./sync_table');
|
||||||
|
const imagemin = require('imagemin');
|
||||||
|
const imageminMozJpeg = require('imagemin-mozjpeg');
|
||||||
|
const imageminPngQuant = require('imagemin-pngquant');
|
||||||
|
const imageminGifLossy = require('imagemin-giflossy');
|
||||||
|
const jimp = require('jimp');
|
||||||
|
const imageType = require('image-type');
|
||||||
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
|
|
||||||
|
async function saveImage(file, sourceId, noteId) {
|
||||||
|
const resizedImage = await resize(file.buffer);
|
||||||
|
const optimizedImage = await optimize(resizedImage);
|
||||||
|
|
||||||
|
const imageFormat = imageType(optimizedImage);
|
||||||
|
|
||||||
|
const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, "");
|
||||||
|
const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext);
|
||||||
|
|
||||||
|
const imageId = utils.newImageId();
|
||||||
|
const now = utils.nowDate();
|
||||||
|
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sql.insert("images", {
|
||||||
|
imageId: imageId,
|
||||||
|
format: imageFormat.ext,
|
||||||
|
name: fileName,
|
||||||
|
checksum: utils.hash(optimizedImage),
|
||||||
|
data: optimizedImage,
|
||||||
|
isDeleted: 0,
|
||||||
|
dateModified: now,
|
||||||
|
dateCreated: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addImageSync(imageId, sourceId);
|
||||||
|
|
||||||
|
const noteImageId = utils.newNoteImageId();
|
||||||
|
|
||||||
|
await sql.insert("note_images", {
|
||||||
|
noteImageId: noteImageId,
|
||||||
|
noteId: noteId,
|
||||||
|
imageId: imageId,
|
||||||
|
isDeleted: 0,
|
||||||
|
dateModified: now,
|
||||||
|
dateCreated: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(noteImageId, sourceId);
|
||||||
|
});
|
||||||
|
return {fileName, imageId};
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SIZE = 1000;
|
||||||
|
const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs
|
||||||
|
|
||||||
|
async function resize(buffer) {
|
||||||
|
const image = await jimp.read(buffer);
|
||||||
|
|
||||||
|
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) {
|
||||||
|
image.resize(MAX_SIZE, jimp.AUTO);
|
||||||
|
}
|
||||||
|
else if (image.bitmap.height > MAX_SIZE) {
|
||||||
|
image.resize(jimp.AUTO, MAX_SIZE);
|
||||||
|
}
|
||||||
|
else if (buffer.byteLength <= MAX_BYTE_SIZE) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do resizing with max quality which will be trimmed during optimization step next
|
||||||
|
image.quality(100);
|
||||||
|
|
||||||
|
// when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
|
||||||
|
image.background(0xFFFFFFFF);
|
||||||
|
|
||||||
|
// getBuffer doesn't support promises so this workaround
|
||||||
|
return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function optimize(buffer) {
|
||||||
|
return await imagemin.buffer(buffer, {
|
||||||
|
plugins: [
|
||||||
|
imageminMozJpeg({
|
||||||
|
quality: 50
|
||||||
|
}),
|
||||||
|
imageminPngQuant({
|
||||||
|
quality: "0-70"
|
||||||
|
}),
|
||||||
|
imageminGifLossy({
|
||||||
|
lossy: 80,
|
||||||
|
optimize: '3' // needs to be string
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
saveImage
|
||||||
|
};
|
||||||
@@ -22,7 +22,7 @@ function info(message) {
|
|||||||
|
|
||||||
function error(message) {
|
function error(message) {
|
||||||
// we're using .info() instead of .error() because simple-node-logger emits weird error for showError()
|
// we're using .info() instead of .error() because simple-node-logger emits weird error for showError()
|
||||||
info(message);
|
info("ERROR: " + message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ];
|
const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ];
|
||||||
|
|||||||
@@ -83,6 +83,40 @@ async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createNote(parentNoteId, title, content = "", extraOptions = {}) {
|
||||||
|
if (!parentNoteId) throw new Error("Empty parentNoteId");
|
||||||
|
if (!title) throw new Error("Empty title");
|
||||||
|
|
||||||
|
const note = {
|
||||||
|
title: title,
|
||||||
|
content: extraOptions.json ? JSON.stringify(content, null, '\t') : content,
|
||||||
|
target: 'into',
|
||||||
|
isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false,
|
||||||
|
type: extraOptions.type,
|
||||||
|
mime: extraOptions.mime
|
||||||
|
};
|
||||||
|
|
||||||
|
if (extraOptions.json) {
|
||||||
|
note.type = "code";
|
||||||
|
note.mime = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!note.type) {
|
||||||
|
note.type = "text";
|
||||||
|
note.mime = "text/html";
|
||||||
|
}
|
||||||
|
|
||||||
|
const {noteId} = await createNewNote(parentNoteId, note, extraOptions.dataKey, extraOptions.sourceId);
|
||||||
|
|
||||||
|
if (extraOptions.attributes) {
|
||||||
|
for (const attrName in extraOptions.attributes) {
|
||||||
|
await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return noteId;
|
||||||
|
}
|
||||||
|
|
||||||
async function protectNoteRecursively(noteId, dataKey, protect, sourceId) {
|
async function protectNoteRecursively(noteId, dataKey, protect, sourceId) {
|
||||||
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
||||||
|
|
||||||
@@ -148,16 +182,20 @@ async function protectNoteHistory(noteId, dataKey, protect, sourceId) {
|
|||||||
async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
|
async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
|
||||||
const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
||||||
|
|
||||||
|
if (oldNote.type === 'file') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (oldNote.isProtected) {
|
if (oldNote.isProtected) {
|
||||||
protected_session.decryptNote(dataKey, oldNote);
|
protected_session.decryptNote(dataKey, oldNote);
|
||||||
|
|
||||||
note.isProtected = false;
|
oldNote.isProtected = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newnoteRevisionId = utils.newnoteRevisionId();
|
const newNoteRevisionId = utils.newNoteRevisionId();
|
||||||
|
|
||||||
await sql.insert('note_revisions', {
|
await sql.insert('note_revisions', {
|
||||||
noteRevisionId: newnoteRevisionId,
|
noteRevisionId: newNoteRevisionId,
|
||||||
noteId: noteId,
|
noteId: noteId,
|
||||||
// title and text should be decrypted now
|
// title and text should be decrypted now
|
||||||
title: oldNote.title,
|
title: oldNote.title,
|
||||||
@@ -167,7 +205,7 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
|
|||||||
dateModifiedTo: nowStr
|
dateModifiedTo: nowStr
|
||||||
});
|
});
|
||||||
|
|
||||||
await sync_table.addNoteHistorySync(newnoteRevisionId, sourceId);
|
await sync_table.addNoteHistorySync(newNoteRevisionId, sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveNoteImages(noteId, noteText, sourceId) {
|
async function saveNoteImages(noteId, noteText, sourceId) {
|
||||||
@@ -217,7 +255,21 @@ async function saveNoteImages(noteId, noteText, sourceId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadFile(noteId, newNote, dataKey) {
|
||||||
|
const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
||||||
|
|
||||||
|
if (oldNote.isProtected) {
|
||||||
|
await protected_session.decryptNote(dataKey, oldNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
newNote.detail.content = oldNote.content;
|
||||||
|
}
|
||||||
|
|
||||||
async function updateNote(noteId, newNote, dataKey, sourceId) {
|
async function updateNote(noteId, newNote, dataKey, sourceId) {
|
||||||
|
if (newNote.detail.type === 'file') {
|
||||||
|
await loadFile(noteId, newNote, dataKey);
|
||||||
|
}
|
||||||
|
|
||||||
if (newNote.detail.isProtected) {
|
if (newNote.detail.isProtected) {
|
||||||
await protected_session.encryptNote(dataKey, newNote.detail);
|
await protected_session.encryptNote(dataKey, newNote.detail);
|
||||||
}
|
}
|
||||||
@@ -235,7 +287,7 @@ async function updateNote(noteId, newNote, dataKey, sourceId) {
|
|||||||
"SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?", [noteId, historyCutoff]);
|
"SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?", [noteId, historyCutoff]);
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
const msSinceDateCreated = now.getTime() - utils.parseDate(newNote.detail.dateCreated).getTime();
|
const msSinceDateCreated = now.getTime() - utils.parseDateTime(newNote.detail.dateCreated).getTime();
|
||||||
|
|
||||||
if (attributesMap.disable_versioning !== 'true'
|
if (attributesMap.disable_versioning !== 'true'
|
||||||
&& !existingnoteRevisionId
|
&& !existingnoteRevisionId
|
||||||
@@ -289,6 +341,7 @@ async function deleteNote(noteTreeId, sourceId) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createNewNote,
|
createNewNote,
|
||||||
|
createNote,
|
||||||
updateNote,
|
updateNote,
|
||||||
deleteNote,
|
deleteNote,
|
||||||
protectNoteRecursively
|
protectNoteRecursively
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ function getDataKey(obj) {
|
|||||||
|
|
||||||
const protectedSessionId = getProtectedSessionId(obj);
|
const protectedSessionId = getProtectedSessionId(obj);
|
||||||
|
|
||||||
|
return getDataKeyForProtectedSessionId(protectedSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataKeyForProtectedSessionId(protectedSessionId) {
|
||||||
if (protectedSessionId && session.protectedSessionId === protectedSessionId) {
|
if (protectedSessionId && session.protectedSessionId === protectedSessionId) {
|
||||||
return session.decryptedDataKey;
|
return session.decryptedDataKey;
|
||||||
}
|
}
|
||||||
@@ -52,7 +56,14 @@ function decryptNote(dataKey, note) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (note.content) {
|
if (note.content) {
|
||||||
note.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(note.noteId), note.content);
|
const contentIv = data_encryption.noteContentIv(note.noteId);
|
||||||
|
|
||||||
|
if (note.type === 'file') {
|
||||||
|
note.content = data_encryption.decrypt(dataKey, contentIv, note.content);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
note.content = data_encryption.decryptString(dataKey, contentIv, note.content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +87,7 @@ function decryptNoteHistoryRow(dataKey, hist) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hist.content) {
|
if (hist.content) {
|
||||||
hist.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(hist.noteRevisionId), hist.content);
|
hist.content = data_encryption.decryptString(dataKey, data_encryption.noteContentIv(hist.noteRevisionId), hist.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,19 +103,20 @@ function encryptNote(dataKey, note) {
|
|||||||
dataKey = getDataKey(dataKey);
|
dataKey = getDataKey(dataKey);
|
||||||
|
|
||||||
note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title);
|
note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title);
|
||||||
note.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(note.noteId), note.content);
|
note.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(note.noteId), note.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function encryptNoteHistoryRow(dataKey, history) {
|
function encryptNoteHistoryRow(dataKey, history) {
|
||||||
dataKey = getDataKey(dataKey);
|
dataKey = getDataKey(dataKey);
|
||||||
|
|
||||||
history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title);
|
history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title);
|
||||||
history.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(history.noteRevisionId), history.content);
|
history.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(history.noteRevisionId), history.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setDataKey,
|
setDataKey,
|
||||||
getDataKey,
|
getDataKey,
|
||||||
|
getDataKeyForProtectedSessionId,
|
||||||
isProtectedSessionAvailable,
|
isProtectedSessionAvailable,
|
||||||
decryptNote,
|
decryptNote,
|
||||||
decryptNotes,
|
decryptNotes,
|
||||||
|
|||||||
27
src/services/scheduler.js
Normal file
27
src/services/scheduler.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const script = require('./script');
|
||||||
|
const Repository = require('./repository');
|
||||||
|
|
||||||
|
const repo = new Repository();
|
||||||
|
|
||||||
|
async function runNotesWithAttribute(runAttrValue) {
|
||||||
|
const notes = await repo.getEntities(`
|
||||||
|
SELECT notes.*
|
||||||
|
FROM notes
|
||||||
|
JOIN attributes ON attributes.noteId = notes.noteId
|
||||||
|
AND attributes.isDeleted = 0
|
||||||
|
AND attributes.name = 'run'
|
||||||
|
AND attributes.value = ?
|
||||||
|
WHERE
|
||||||
|
notes.type = 'code'
|
||||||
|
AND notes.isDeleted = 0`, [runAttrValue]);
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
script.executeNote(null, note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => runNotesWithAttribute('backend_startup'), 10 * 1000);
|
||||||
|
|
||||||
|
setInterval(() => runNotesWithAttribute('hourly'), 3600 * 1000);
|
||||||
|
|
||||||
|
setInterval(() => runNotesWithAttribute('daily'), 24 * 3600 * 1000);
|
||||||
@@ -1,27 +1,141 @@
|
|||||||
const log = require('./log');
|
|
||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const ScriptContext = require('./script_context');
|
const ScriptContext = require('./script_context');
|
||||||
|
const Repository = require('./repository');
|
||||||
|
|
||||||
async function executeScript(dataKey, script, params) {
|
async function executeNote(dataKey, note) {
|
||||||
log.info('Executing script: ' + script);
|
if (!note.isJavaScript()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = new ScriptContext(dataKey);
|
const bundle = await getScriptBundle(note);
|
||||||
|
|
||||||
const paramsStr = getParams(params);
|
await executeBundle(dataKey, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
let ret;
|
async function executeBundle(dataKey, bundle, startNote) {
|
||||||
|
if (!startNote) {
|
||||||
|
// this is the default case, the only exception is when we want to preserve frontend startNote
|
||||||
|
startNote = bundle.note;
|
||||||
|
}
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
// last \r\n is necessary if script contains line comment on its last line
|
||||||
ret = await (function() { return eval(`(${script})(${paramsStr})`); }.call(ctx));
|
const script = "async function() {\r\n" + bundle.script + "\r\n}";
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
const ctx = new ScriptContext(dataKey, startNote, bundle.allNotes);
|
||||||
|
|
||||||
|
if (await bundle.note.hasAttribute('manual_transaction_handling')) {
|
||||||
|
return await execute(ctx, script, '');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return await sql.doInTransaction(async () => execute(ctx, script, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method preserves frontend startNode - that's why we start execution from currentNote and override
|
||||||
|
* bundle's startNote.
|
||||||
|
*/
|
||||||
|
async function executeScript(dataKey, script, params, startNoteId, currentNoteId) {
|
||||||
|
const repository = new Repository(dataKey);
|
||||||
|
const startNote = await repository.getNote(startNoteId);
|
||||||
|
const currentNote = await repository.getNote(currentNoteId);
|
||||||
|
|
||||||
|
currentNote.content = `return await (${script}\r\n)(${getParams(params)})`;
|
||||||
|
currentNote.type = 'code';
|
||||||
|
currentNote.mime = 'application/javascript;env=backend';
|
||||||
|
|
||||||
|
const bundle = await getScriptBundle(currentNote);
|
||||||
|
|
||||||
|
return await executeBundle(dataKey, bundle, startNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execute(ctx, script, paramsStr) {
|
||||||
|
return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getParams(params) {
|
function getParams(params) {
|
||||||
return params.map(p => JSON.stringify(p)).join(",");
|
if (!params) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.map(p => {
|
||||||
|
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
|
||||||
|
return p.substr(13);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return JSON.stringify(p);
|
||||||
|
}
|
||||||
|
}).join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
|
||||||
|
if (!note.isJavaScript() && !note.isHtml() && note.type !== 'render') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root && await note.hasAttribute('disable_inclusion')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
scriptEnv = note.getScriptEnv();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.type !== 'file' && scriptEnv !== note.getScriptEnv()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundle = {
|
||||||
|
note: note,
|
||||||
|
script: '',
|
||||||
|
html: '',
|
||||||
|
allNotes: [note]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includedNoteIds.includes(note.noteId)) {
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
includedNoteIds.push(note.noteId);
|
||||||
|
|
||||||
|
const modules = [];
|
||||||
|
|
||||||
|
for (const child of await note.getChildren()) {
|
||||||
|
const childBundle = await getScriptBundle(child, false, scriptEnv, includedNoteIds);
|
||||||
|
|
||||||
|
if (childBundle) {
|
||||||
|
modules.push(childBundle.note);
|
||||||
|
bundle.script += childBundle.script;
|
||||||
|
bundle.html += childBundle.html;
|
||||||
|
bundle.allNotes = bundle.allNotes.concat(childBundle.allNotes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleNoteIds = modules.map(mod => mod.noteId);
|
||||||
|
|
||||||
|
if (note.isJavaScript()) {
|
||||||
|
bundle.script += `
|
||||||
|
apiContext.modules['${note.noteId}'] = {};
|
||||||
|
${root ? 'return ' : ''}await (async function(exports, module, require, api` + (modules.length > 0 ? ', ' : '') +
|
||||||
|
modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) {
|
||||||
|
${note.content}
|
||||||
|
})({}, apiContext.modules['${note.noteId}'], apiContext.require(${JSON.stringify(moduleNoteIds)}), apiContext.apis['${note.noteId}']` + (modules.length > 0 ? ', ' : '') +
|
||||||
|
modules.map(mod => `apiContext.modules['${mod.noteId}'].exports`).join(', ') + `);
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
else if (note.isHtml()) {
|
||||||
|
bundle.html += note.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeVariableName(str) {
|
||||||
|
return str.replace(/[^a-z0-9_]/gim, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
executeScript
|
executeNote,
|
||||||
|
executeScript,
|
||||||
|
getScriptBundle
|
||||||
};
|
};
|
||||||
@@ -1,20 +1,55 @@
|
|||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const protected_session = require('./protected_session');
|
const protected_session = require('./protected_session');
|
||||||
const notes = require('./notes');
|
const notes = require('./notes');
|
||||||
|
const sql = require('./sql');
|
||||||
|
const utils = require('./utils');
|
||||||
const attributes = require('./attributes');
|
const attributes = require('./attributes');
|
||||||
const date_notes = require('./date_notes');
|
const date_notes = require('./date_notes');
|
||||||
|
const config = require('./config');
|
||||||
const Repository = require('./repository');
|
const Repository = require('./repository');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
function ScriptContext(noteId, dataKey) {
|
function ScriptContext(dataKey, startNote, allNotes) {
|
||||||
this.dataKey = protected_session.getDataKey(dataKey);
|
dataKey = protected_session.getDataKey(dataKey);
|
||||||
this.repository = new Repository(dataKey);
|
|
||||||
|
this.modules = {};
|
||||||
|
this.notes = utils.toObject(allNotes, note => [note.noteId, note]);
|
||||||
|
this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(dataKey, startNote, note)]);
|
||||||
|
this.require = moduleNoteIds => {
|
||||||
|
return moduleName => {
|
||||||
|
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||||
|
const note = candidates.find(c => c.title === moduleName);
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
throw new Error("Could not find module note " + moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.modules[note.noteId].exports;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScriptApi(dataKey, startNote, currentNote) {
|
||||||
|
const repository = new Repository(dataKey);
|
||||||
|
this.startNote = startNote;
|
||||||
|
this.currentNote = currentNote;
|
||||||
|
|
||||||
|
this.axios = axios;
|
||||||
|
|
||||||
|
this.utils = {
|
||||||
|
unescapeHtml: utils.unescapeHtml,
|
||||||
|
isoDateTimeStr: utils.dateStr,
|
||||||
|
isoDateStr: date => utils.dateStr(date).substr(0, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getInstanceName = () => config.General ? config.General.instanceName : null;
|
||||||
|
|
||||||
this.getNoteById = async function(noteId) {
|
this.getNoteById = async function(noteId) {
|
||||||
return this.repository.getNote(noteId);
|
return repository.getNote(noteId);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getNotesWithAttribute = async function (attrName, attrValue) {
|
this.getNotesWithAttribute = async function (attrName, attrValue) {
|
||||||
return await attributes.getNotesWithAttribute(this.dataKey, attrName, attrValue);
|
return await attributes.getNotesWithAttribute(repository, attrName, attrValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getNoteWithAttribute = async function (attrName, attrValue) {
|
this.getNoteWithAttribute = async function (attrName, attrValue) {
|
||||||
@@ -23,44 +58,22 @@ function ScriptContext(noteId, dataKey) {
|
|||||||
return notes.length > 0 ? notes[0] : null;
|
return notes.length > 0 ? notes[0] : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.createNote = async function (parentNoteId, name, jsonContent, extraOptions = {}) {
|
this.createNote = async function(parentNoteId, title, content = "", extraOptions = {}) {
|
||||||
const note = {
|
extraOptions.dataKey = dataKey;
|
||||||
title: name,
|
|
||||||
content: extraOptions.json ? JSON.stringify(jsonContent, null, '\t') : jsonContent,
|
|
||||||
target: 'into',
|
|
||||||
isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false,
|
|
||||||
type: extraOptions.type,
|
|
||||||
mime: extraOptions.mime
|
|
||||||
};
|
|
||||||
|
|
||||||
if (extraOptions.json) {
|
return await notes.createNote(parentNoteId, title, content, extraOptions);
|
||||||
note.type = "code";
|
|
||||||
note.mime = "application/json";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!note.type) {
|
|
||||||
note.type = "text";
|
|
||||||
note.mime = "text/html";
|
|
||||||
}
|
|
||||||
|
|
||||||
const noteId = (await notes.createNewNote(parentNoteId, note, this.dataKey)).noteId;
|
|
||||||
|
|
||||||
if (extraOptions.attributes) {
|
|
||||||
for (const attrName in extraOptions.attributes) {
|
|
||||||
await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return noteId;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateEntity = this.repository.updateEntity;
|
this.createAttribute = attributes.createAttribute;
|
||||||
|
|
||||||
this.log = function(message) {
|
this.updateEntity = repository.updateEntity;
|
||||||
log.info(`Script: ${message}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`);
|
||||||
|
|
||||||
|
this.getRootCalendarNoteId = date_notes.getRootCalendarNoteId;
|
||||||
this.getDateNoteId = date_notes.getDateNoteId;
|
this.getDateNoteId = date_notes.getDateNoteId;
|
||||||
|
|
||||||
|
this.transaction = sql.doInTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ScriptContext;
|
module.exports = ScriptContext;
|
||||||
@@ -195,6 +195,7 @@ async function doInTransaction(func) {
|
|||||||
await transactionPromise;
|
await transactionPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ret = null;
|
||||||
const error = new Error(); // to capture correct stack trace in case of exception
|
const error = new Error(); // to capture correct stack trace in case of exception
|
||||||
|
|
||||||
transactionActive = true;
|
transactionActive = true;
|
||||||
@@ -202,7 +203,7 @@ async function doInTransaction(func) {
|
|||||||
try {
|
try {
|
||||||
await beginTransaction();
|
await beginTransaction();
|
||||||
|
|
||||||
await func();
|
ret = await func();
|
||||||
|
|
||||||
await commit();
|
await commit();
|
||||||
|
|
||||||
@@ -223,6 +224,8 @@ async function doInTransaction(func) {
|
|||||||
if (transactionActive) {
|
if (transactionActive) {
|
||||||
await transactionPromise;
|
await transactionPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isDbUpToDate() {
|
async function isDbUpToDate() {
|
||||||
|
|||||||
@@ -149,6 +149,9 @@ async function pullSync(syncContext) {
|
|||||||
else if (sync.entityName === 'attributes') {
|
else if (sync.entityName === 'attributes') {
|
||||||
await syncUpdate.updateAttribute(resp, syncContext.sourceId);
|
await syncUpdate.updateAttribute(resp, syncContext.sourceId);
|
||||||
}
|
}
|
||||||
|
else if (sync.entityName === 'api_tokens') {
|
||||||
|
await syncUpdate.updateApiToken(resp, syncContext.sourceId);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
|
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
|
||||||
}
|
}
|
||||||
@@ -201,6 +204,8 @@ async function pushEntity(sync, syncContext) {
|
|||||||
|
|
||||||
if (sync.entityName === 'notes') {
|
if (sync.entityName === 'notes') {
|
||||||
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
|
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
|
||||||
|
|
||||||
|
serializeNoteContentBuffer(entity);
|
||||||
}
|
}
|
||||||
else if (sync.entityName === 'note_tree') {
|
else if (sync.entityName === 'note_tree') {
|
||||||
entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]);
|
entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]);
|
||||||
@@ -233,6 +238,9 @@ async function pushEntity(sync, syncContext) {
|
|||||||
else if (sync.entityName === 'attributes') {
|
else if (sync.entityName === 'attributes') {
|
||||||
entity = await sql.getRow('SELECT * FROM attributes WHERE attributeId = ?', [sync.entityId]);
|
entity = await sql.getRow('SELECT * FROM attributes WHERE attributeId = ?', [sync.entityId]);
|
||||||
}
|
}
|
||||||
|
else if (sync.entityName === 'api_tokens') {
|
||||||
|
entity = await sql.getRow('SELECT * FROM api_tokens WHERE apiTokenId = ?', [sync.entityId]);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
|
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
|
||||||
}
|
}
|
||||||
@@ -252,6 +260,12 @@ async function pushEntity(sync, syncContext) {
|
|||||||
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
|
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) {
|
async function checkContentHash(syncContext) {
|
||||||
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
|
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
|
||||||
|
|
||||||
@@ -344,5 +358,6 @@ sql.dbReady.then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sync
|
sync,
|
||||||
|
serializeNoteContentBuffer
|
||||||
};
|
};
|
||||||
@@ -40,6 +40,10 @@ async function addAttributeSync(attributeId, sourceId) {
|
|||||||
await addEntitySync("attributes", attributeId, sourceId);
|
await addEntitySync("attributes", attributeId, sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addApiTokenSync(apiTokenId, sourceId) {
|
||||||
|
await addEntitySync("api_tokens", apiTokenId, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
async function addEntitySync(entityName, entityId, sourceId) {
|
async function addEntitySync(entityName, entityId, sourceId) {
|
||||||
await sql.replace("sync", {
|
await sql.replace("sync", {
|
||||||
entityName: entityName,
|
entityName: entityName,
|
||||||
@@ -93,6 +97,7 @@ async function fillAllSyncRows() {
|
|||||||
await fillSyncRows("images", "imageId");
|
await fillSyncRows("images", "imageId");
|
||||||
await fillSyncRows("note_images", "noteImageId");
|
await fillSyncRows("note_images", "noteImageId");
|
||||||
await fillSyncRows("attributes", "attributeId");
|
await fillSyncRows("attributes", "attributeId");
|
||||||
|
await fillSyncRows("api_tokens", "apiTokenId");
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -105,6 +110,7 @@ module.exports = {
|
|||||||
addImageSync,
|
addImageSync,
|
||||||
addNoteImageSync,
|
addNoteImageSync,
|
||||||
addAttributeSync,
|
addAttributeSync,
|
||||||
|
addApiTokenSync,
|
||||||
addEntitySync,
|
addEntitySync,
|
||||||
cleanupSyncRowsForMissingEntities,
|
cleanupSyncRowsForMissingEntities,
|
||||||
fillAllSyncRows
|
fillAllSyncRows
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const eventLog = require('./event_log');
|
const eventLog = require('./event_log');
|
||||||
const notes = require('./notes');
|
|
||||||
const sync_table = require('./sync_table');
|
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) {
|
async function updateNote(entity, sourceId) {
|
||||||
|
deserializeNoteContentBuffer(entity);
|
||||||
|
|
||||||
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
|
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
|
||||||
|
|
||||||
if (!origNote || origNote.dateModified <= entity.dateModified) {
|
if (!origNote || origNote.dateModified <= entity.dateModified) {
|
||||||
@@ -137,6 +144,20 @@ async function updateAttribute(entity, sourceId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateApiToken(entity, sourceId) {
|
||||||
|
const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]);
|
||||||
|
|
||||||
|
if (!apiTokenId) {
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sql.replace("api_tokens", entity);
|
||||||
|
|
||||||
|
await sync_table.addApiTokenSync(entity.apiTokenId, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Update/sync API token " + entity.apiTokenId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
updateNote,
|
updateNote,
|
||||||
updateNoteTree,
|
updateNoteTree,
|
||||||
@@ -146,5 +167,6 @@ module.exports = {
|
|||||||
updateRecentNotes,
|
updateRecentNotes,
|
||||||
updateImage,
|
updateImage,
|
||||||
updateNoteImage,
|
updateNoteImage,
|
||||||
updateAttribute
|
updateAttribute,
|
||||||
|
updateApiToken
|
||||||
};
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const sync_table = require('./sync_table');
|
const sync_table = require('./sync_table');
|
||||||
|
const protected_session = require('./protected_session');
|
||||||
|
|
||||||
async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) {
|
async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) {
|
||||||
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const randtoken = require('rand-token').generator({source: 'crypto'});
|
const randtoken = require('rand-token').generator({source: 'crypto'});
|
||||||
|
const unescape = require('unescape');
|
||||||
|
|
||||||
function newNoteId() {
|
function newNoteId() {
|
||||||
return randomString(12);
|
return randomString(12);
|
||||||
@@ -11,7 +12,7 @@ function newNoteTreeId() {
|
|||||||
return randomString(12);
|
return randomString(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
function newnoteRevisionId() {
|
function newNoteRevisionId() {
|
||||||
return randomString(12);
|
return randomString(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +28,10 @@ function newAttributeId() {
|
|||||||
return randomString(12);
|
return randomString(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newApiTokenId() {
|
||||||
|
return randomString(12);
|
||||||
|
}
|
||||||
|
|
||||||
function randomString(length) {
|
function randomString(length) {
|
||||||
return randtoken.generate(length);
|
return randtoken.generate(length);
|
||||||
}
|
}
|
||||||
@@ -39,6 +44,14 @@ function nowDate() {
|
|||||||
return dateStr(new Date());
|
return dateStr(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function localDate() {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
return date.getFullYear() + "-"
|
||||||
|
+ (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1) + "-"
|
||||||
|
+ (date.getDate() < 10 ? "0" : "") + date.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
function dateStr(date) {
|
function dateStr(date) {
|
||||||
return date.toISOString();
|
return date.toISOString();
|
||||||
}
|
}
|
||||||
@@ -47,7 +60,7 @@ function dateStr(date) {
|
|||||||
* @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr().
|
* @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr().
|
||||||
* also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time
|
* also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time
|
||||||
*/
|
*/
|
||||||
function parseDate(str) {
|
function parseDateTime(str) {
|
||||||
try {
|
try {
|
||||||
return new Date(Date.parse(str));
|
return new Date(Date.parse(str));
|
||||||
}
|
}
|
||||||
@@ -56,6 +69,12 @@ function parseDate(str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDate(str) {
|
||||||
|
const datePart = str.substr(0, 10);
|
||||||
|
|
||||||
|
return parseDateTime(datePart + "T12:00:00.000Z");
|
||||||
|
}
|
||||||
|
|
||||||
function toBase64(plainText) {
|
function toBase64(plainText) {
|
||||||
return Buffer.from(plainText).toString('base64');
|
return Buffer.from(plainText).toString('base64');
|
||||||
}
|
}
|
||||||
@@ -111,18 +130,37 @@ async function stopWatch(what, func) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unescapeHtml(str) {
|
||||||
|
return unescape(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toObject(array, fn) {
|
||||||
|
const obj = {};
|
||||||
|
|
||||||
|
for (const item of array) {
|
||||||
|
const ret = fn(item);
|
||||||
|
|
||||||
|
obj[ret[0]] = ret[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
randomSecureToken,
|
randomSecureToken,
|
||||||
randomString,
|
randomString,
|
||||||
nowDate,
|
nowDate,
|
||||||
|
localDate,
|
||||||
dateStr,
|
dateStr,
|
||||||
parseDate,
|
parseDate,
|
||||||
|
parseDateTime,
|
||||||
newNoteId,
|
newNoteId,
|
||||||
newNoteTreeId,
|
newNoteTreeId,
|
||||||
newnoteRevisionId,
|
newNoteRevisionId,
|
||||||
newImageId,
|
newImageId,
|
||||||
newNoteImageId,
|
newNoteImageId,
|
||||||
newAttributeId,
|
newAttributeId,
|
||||||
|
newApiTokenId,
|
||||||
toBase64,
|
toBase64,
|
||||||
fromBase64,
|
fromBase64,
|
||||||
hmac,
|
hmac,
|
||||||
@@ -132,5 +170,7 @@ module.exports = {
|
|||||||
getDateTimeForFile,
|
getDateTimeForFile,
|
||||||
sanitizeSql,
|
sanitizeSql,
|
||||||
assertArguments,
|
assertArguments,
|
||||||
stopWatch
|
stopWatch,
|
||||||
|
unescapeHtml,
|
||||||
|
toObject
|
||||||
};
|
};
|
||||||
@@ -40,30 +40,27 @@
|
|||||||
|
|
||||||
<div class="hide-toggle" style="grid-area: tree-actions;">
|
<div class="hide-toggle" style="grid-area: tree-actions;">
|
||||||
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
|
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
|
||||||
<a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action">
|
<a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action"
|
||||||
<img src="images/icons/file-plus.png" alt="Create new top level note"/>
|
style="background: url('images/icons/file-plus.png')"></a>
|
||||||
</a>
|
|
||||||
|
|
||||||
<a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action">
|
<a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action"
|
||||||
<img src="images/icons/list.png" alt="Collapse note tree"/>
|
style="background: url('images/icons/list.png')"></a>
|
||||||
</a>
|
|
||||||
|
|
||||||
<a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action">
|
<a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action"
|
||||||
<img src="images/icons/crosshair.png" alt="Scroll to current note"/>
|
style="background: url('images/icons/crosshair.png')"></a>
|
||||||
</a>
|
|
||||||
|
|
||||||
<a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action">
|
<a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action"
|
||||||
<img src="images/icons/search.png" alt="Search in notes"/>
|
style="background: url('images/icons/search.png')"></a>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
|
<input type="file" id="import-upload" style="display: none" />
|
||||||
<p>
|
</div>
|
||||||
<label>Search:</label>
|
|
||||||
<input name="search-text" autocomplete="off">
|
<div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;">
|
||||||
<button id="reset-search-button">×</button>
|
<div style="display: flex; align-items: center;">
|
||||||
<span id="matches"></span>
|
<label>Search:</label>
|
||||||
</p>
|
<input name="search-text" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
|
||||||
|
<button id="reset-search-button" class="btn btn-sm" title="Reset search">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,7 +70,7 @@
|
|||||||
<div id="parent-list" class="hide-toggle">
|
<div id="parent-list" class="hide-toggle">
|
||||||
<p><strong>Note locations:</strong></p>
|
<p><strong>Note locations:</strong></p>
|
||||||
|
|
||||||
<ul id="parent-list-list"></ul>
|
<ul id="parent-list-inner"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hide-toggle" style="grid-area: title;">
|
<div class="hide-toggle" style="grid-area: title;">
|
||||||
@@ -82,17 +79,13 @@
|
|||||||
title="Protect the note so that password will be required to view the note"
|
title="Protect the note so that password will be required to view the note"
|
||||||
class="icon-action"
|
class="icon-action"
|
||||||
id="protect-button"
|
id="protect-button"
|
||||||
style="display: none;">
|
style="display: none; background: url('images/icons/lock.png')"></a>
|
||||||
<img src="images/icons/lock.png" alt="Protect note"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a onclick="protected_session.unprotectNoteAndSendToServer()"
|
<a onclick="protected_session.unprotectNoteAndSendToServer()"
|
||||||
title="Unprotect note so that password will not be required to access this note in the future"
|
title="Unprotect note so that password will not be required to access this note in the future"
|
||||||
class="icon-action"
|
class="icon-action"
|
||||||
id="unprotect-button"
|
id="unprotect-button"
|
||||||
style="display: none;">
|
style="display: none; background: url('images/icons/unlock.png')"></a>
|
||||||
<img src="images/icons/unlock.png" alt="Unprotect note"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -106,7 +99,7 @@
|
|||||||
onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button>
|
onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button>
|
||||||
|
|
||||||
<div class="dropdown" id="note-type">
|
<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>
|
Type: <span data-bind="text: typeString()"></span>
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -131,6 +124,7 @@
|
|||||||
<li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li>
|
<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="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="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li>
|
||||||
|
<li><a onclick="uploadAttachment();">Upload attachment</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,24 +136,43 @@
|
|||||||
<div id="note-detail-code"></div>
|
<div id="note-detail-code"></div>
|
||||||
|
|
||||||
<div id="note-detail-render"></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>
|
||||||
|
|
||||||
|
<button id="attachment-open" class="btn btn-primary" type="button">Open</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="file" id="attachment-upload" style="display: none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="attribute-list">
|
||||||
|
<button class="btn btn-sm" onclick="attributesDialog.showDialog();">Attributes:</button>
|
||||||
|
|
||||||
|
<span id="attribute-list-inner"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="recent-notes-dialog" title="Recent notes" style="display: none;">
|
<div id="recent-notes-dialog" title="Recent notes" style="display: none;">
|
||||||
<select id="recent-notes-select-box" size="20" style="width: 100%">
|
<input id="recent-notes-search-input" class="form-control"/>
|
||||||
</select>
|
|
||||||
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<button class="btn btn-sm" id="recent-notes-jump-to">Jump to <kbd>enter</kbd></button>
|
|
||||||
|
|
||||||
<button class="btn btn-sm" id="recent-notes-add-link">Add link <kbd>l</kbd></button>
|
|
||||||
|
|
||||||
<button class="btn btn-sm" id="recent-notes-add-current-as-child">Add current as child <kbd>c</kbd></button>
|
|
||||||
|
|
||||||
<button class="btn btn-sm" id="recent-notes-add-recent-as-child">Add recent as child <kbd>r</kbd></button>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="add-link-dialog" title="Add link" style="display: none;">
|
<div id="add-link-dialog" title="Add link" style="display: none;">
|
||||||
@@ -368,8 +381,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
|
<div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
|
||||||
<textarea style="width: 100%; height: 100px" id="sql-console-query"></textarea>
|
<div style="height: 150px; width: 100%; border: 1px solid #ccc; margin-bottom: 10px;" id="sql-console-query"></div>
|
||||||
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
|
|
||||||
|
<div style="text-align: center">
|
||||||
|
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
|
<table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
|
||||||
<thead></thead>
|
<thead></thead>
|
||||||
@@ -383,29 +399,40 @@
|
|||||||
|
|
||||||
<div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
|
<div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
|
||||||
<form data-bind="submit: save">
|
<form data-bind="submit: save">
|
||||||
<div style="display: flex; justify-content: space-between; padding: 15px; padding-top: 0;">
|
<div style="text-align: center">
|
||||||
<button class="btn-default" type="button" data-bind="click: addNewRow">Add new attribute</button>
|
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button>
|
||||||
|
|
||||||
<button class="btn-primary" type="submit">Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="height: 97%; overflow: auto">
|
<div style="height: 97%; overflow: auto">
|
||||||
<table id="attributes-table" class="table">
|
<table id="attributes-table" class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th></th>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Value</th>
|
<th>Value</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody data-bind="foreach: attributes">
|
<tbody data-bind="foreach: attributes">
|
||||||
<tr>
|
<tr data-bind="if: isDeleted == 0">
|
||||||
<td data-bind="text: attributeId"></td>
|
<td class="handle">
|
||||||
|
<span class="glyphicon glyphicon-resize-vertical"></span>
|
||||||
|
<input type="hidden" name="position" data-bind="value: position"/>
|
||||||
|
</td>
|
||||||
|
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
|
||||||
|
<td data-bind="text: attributeId" style="width: 150px;"></td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" data-bind="value: name"/>
|
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||||
|
<input type="text" class="attribute-name" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"/>
|
||||||
|
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate attribute.</div>
|
||||||
|
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" data-bind="value: value" style="width: 300px"/>
|
<input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
|
||||||
|
</td>
|
||||||
|
<td title="Delete" style="padding: 13px;">
|
||||||
|
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -443,8 +470,6 @@
|
|||||||
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
|
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
|
||||||
<script src="libraries/fancytree/jquery.fancytree-all.min.js"></script>
|
<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.hotkeys.js"></script>
|
||||||
<script src="libraries/jquery.fancytree.hotkeys.js"></script>
|
<script src="libraries/jquery.fancytree.hotkeys.js"></script>
|
||||||
|
|
||||||
@@ -452,15 +477,6 @@
|
|||||||
|
|
||||||
<script src="libraries/knockout.min.js"></script>
|
<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">
|
<link href="stylesheets/style.css" rel="stylesheet">
|
||||||
|
|
||||||
<script src="javascripts/utils.js"></script>
|
<script src="javascripts/utils.js"></script>
|
||||||
@@ -475,6 +491,7 @@
|
|||||||
<script src="javascripts/drag_and_drop.js"></script>
|
<script src="javascripts/drag_and_drop.js"></script>
|
||||||
<script src="javascripts/context_menu.js"></script>
|
<script src="javascripts/context_menu.js"></script>
|
||||||
<script src="javascripts/search_tree.js"></script>
|
<script src="javascripts/search_tree.js"></script>
|
||||||
|
<script src="javascripts/export.js"></script>
|
||||||
|
|
||||||
<!-- Note detail -->
|
<!-- Note detail -->
|
||||||
<script src="javascripts/note_editor.js"></script>
|
<script src="javascripts/note_editor.js"></script>
|
||||||
@@ -504,5 +521,9 @@
|
|||||||
// final form which is pretty ugly.
|
// final form which is pretty ugly.
|
||||||
$("#container").show();
|
$("#container").show();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
<%= appCss %>
|
||||||
|
</style>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user