mirror of
https://github.com/zadam/trilium.git
synced 2025-10-30 01:36:24 +01:00
Compare commits
18 Commits
v0.5.6
...
v0.6.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72df0d8861 | ||
|
|
9910aebf45 | ||
|
|
f9f8ecb2b1 | ||
|
|
438f7c5b0b | ||
|
|
4b1d1aba74 | ||
|
|
6dea73cfe2 | ||
|
|
58f5d0cf6e | ||
|
|
7b77e40514 | ||
|
|
660908c54b | ||
|
|
e970564036 | ||
|
|
b3038487f8 | ||
|
|
cac98392a6 | ||
|
|
dbd28377e3 | ||
|
|
c76e4faf5d | ||
|
|
e011b9ae63 | ||
|
|
7c74c77a2c | ||
|
|
c2a2f195aa | ||
|
|
4e70cebf70 |
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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,11 @@ 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 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,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.5.6",
|
"version": "0.6.0-beta",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "electron.js",
|
"main": "electron.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class Note extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAttributes() {
|
async getAttributes() {
|
||||||
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ?", [this.noteId]);
|
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAttribute(name) {
|
async getAttribute(name) {
|
||||||
|
|||||||
@@ -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,10 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const attributesDialog = (function() {
|
const attributesDialog = (function() {
|
||||||
const dialogEl = $("#attributes-dialog");
|
const $dialog = $("#attributes-dialog");
|
||||||
const saveAttributesButton = $("#save-attributes-button");
|
const $saveAttributesButton = $("#save-attributes-button");
|
||||||
|
const $attributesBody = $('#attributes-table tbody');
|
||||||
|
|
||||||
const attributesModel = new AttributesModel();
|
const attributesModel = new AttributesModel();
|
||||||
let attributeNames = [];
|
let attributeNames = [];
|
||||||
|
|
||||||
@@ -24,11 +26,40 @@ const attributesDialog = (function() {
|
|||||||
|
|
||||||
// attribute might not be rendered immediatelly so could not focus
|
// attribute might not be rendered immediatelly so could not focus
|
||||||
setTimeout(() => $(".attribute-name:last").focus(), 100);
|
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() {
|
function isValid() {
|
||||||
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
|
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
|
||||||
if (self.isEmptyName(i) || self.isNotUnique(i)) {
|
if (self.isEmptyName(i)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +71,7 @@ const attributesDialog = (function() {
|
|||||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
// 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
|
// 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.
|
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||||
saveAttributesButton.focus();
|
$saveAttributesButton.focus();
|
||||||
|
|
||||||
if (!isValid()) {
|
if (!isValid()) {
|
||||||
alert("Please fix all validation errors and try saving again.");
|
alert("Please fix all validation errors and try saving again.");
|
||||||
@@ -65,26 +96,26 @@ const attributesDialog = (function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function addLastEmptyRow() {
|
function addLastEmptyRow() {
|
||||||
const attrs = self.attributes();
|
const attrs = self.attributes().filter(attr => attr().isDeleted === 0);
|
||||||
const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
|
const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
|
||||||
|
|
||||||
if (!last || last.name.trim() !== "" || last.value !== "") {
|
if (!last || last.name.trim() !== "" || last.value !== "") {
|
||||||
self.attributes.push(ko.observable({
|
self.attributes.push(ko.observable({
|
||||||
attributeId: '',
|
attributeId: '',
|
||||||
name: '',
|
name: '',
|
||||||
value: ''
|
value: '',
|
||||||
|
isDeleted: 0,
|
||||||
|
position: 0
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.attributeChanged = function (row) {
|
this.attributeChanged = function (data, event) {
|
||||||
addLastEmptyRow();
|
addLastEmptyRow();
|
||||||
|
|
||||||
for (const attr of self.attributes()) {
|
const attr = self.getTargetAttribute(event.target);
|
||||||
if (row.attributeId === attr().attributeId) {
|
|
||||||
attr.valueHasMutated();
|
attr.valueHasMutated();
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isNotUnique = function(index) {
|
this.isNotUnique = function(index) {
|
||||||
@@ -109,15 +140,22 @@ const attributesDialog = (function() {
|
|||||||
const cur = self.attributes()[index]();
|
const cur = self.attributes()[index]();
|
||||||
|
|
||||||
return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== "");
|
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;
|
||||||
|
|
||||||
await attributesModel.loadAttributes();
|
await attributesModel.loadAttributes();
|
||||||
|
|
||||||
dialogEl.dialog({
|
$dialog.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 500
|
height: 500
|
||||||
|
|||||||
@@ -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,66 @@ 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: 400
|
||||||
});
|
});
|
||||||
|
|
||||||
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 +93,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,44 @@
|
|||||||
"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() {
|
||||||
|
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
|
||||||
|
CodeMirror.keyMap.default["Tab"] = "indentMore";
|
||||||
|
|
||||||
|
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
|
||||||
|
|
||||||
|
codeEditor = CodeMirror($query[0], {
|
||||||
|
value: "",
|
||||||
|
viewportMargin: Infinity,
|
||||||
|
indentUnit: 4,
|
||||||
|
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
codeEditor.setOption("mode", "text/x-sqlite");
|
||||||
|
CodeMirror.autoLoadMode(codeEditor, "sql");
|
||||||
|
|
||||||
|
codeEditor.focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function execute() {
|
async function execute() {
|
||||||
const sqlQuery = queryEl.val();
|
const sqlQuery = codeEditor.getValue();
|
||||||
|
|
||||||
const result = await server.post("sql/execute", {
|
const result = await server.post("sql/execute", {
|
||||||
query: sqlQuery
|
query: sqlQuery
|
||||||
@@ -34,8 +54,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 +65,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 +75,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
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ const server = (function() {
|
|||||||
post,
|
post,
|
||||||
put,
|
put,
|
||||||
remove,
|
remove,
|
||||||
exec
|
exec,
|
||||||
|
// don't remove, used from CKEditor image upload!
|
||||||
|
getHeaders
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
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
@@ -269,3 +269,7 @@ div.ui-tooltip {
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recent-notes-autocomplete {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ const attributes = require('../../services/attributes');
|
|||||||
router.get('/notes/: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('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
@@ -23,19 +23,26 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res,
|
|||||||
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +50,11 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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) => {
|
router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const names = await sql.getColumn("SELECT DISTINCT name FROM attributes");
|
const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0");
|
||||||
|
|
||||||
for (const attr of attributes.BUILTIN_ATTRIBUTES) {
|
for (const attr of attributes.BUILTIN_ATTRIBUTES) {
|
||||||
if (!names.includes(attr)) {
|
if (!names.includes(attr)) {
|
||||||
@@ -63,7 +70,7 @@ router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) =
|
|||||||
router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => {
|
router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const attributeName = req.params.attributeName;
|
const attributeName = req.params.attributeName;
|
||||||
|
|
||||||
const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE name = ? AND value != '' ORDER BY value", [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);
|
res.send(values);
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -66,7 +66,7 @@ async function importNotes(dir, parentNoteId) {
|
|||||||
const noteText = fs.readFileSync(path, "utf8");
|
const noteText = fs.readFileSync(path, "utf8");
|
||||||
|
|
||||||
const noteId = utils.newNoteId();
|
const noteId = utils.newNoteId();
|
||||||
const noteTreeId = utils.newnoteRevisionId();
|
const noteTreeId = utils.newNoteRevisionId();
|
||||||
|
|
||||||
const now = utils.nowDate();
|
const now = utils.nowDate();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
|||||||
|
|
||||||
const {query, params} = getSearchQuery(attrFilters, searchText);
|
const {query, params} = getSearchQuery(attrFilters, searchText);
|
||||||
|
|
||||||
|
console.log(query, params);
|
||||||
|
|
||||||
const noteIds = await sql.getColumn(query, params);
|
const noteIds = await sql.getColumn(query, params);
|
||||||
|
|
||||||
res.send(noteIds);
|
res.send(noteIds);
|
||||||
@@ -152,7 +154,7 @@ function getSearchQuery(attrFilters, searchText) {
|
|||||||
searchParams.push(searchText); // two occurences in searchCondition
|
searchParams.push(searchText); // two occurences in searchCondition
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = `SELECT notes.noteId FROM notes
|
const query = `SELECT DISTINCT notes.noteId FROM notes
|
||||||
${joins.join('\r\n')}
|
${joins.join('\r\n')}
|
||||||
WHERE
|
WHERE
|
||||||
notes.isDeleted = 0
|
notes.isDeleted = 0
|
||||||
|
|||||||
@@ -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;
|
||||||
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;
|
||||||
@@ -147,6 +147,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 +207,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;
|
||||||
@@ -28,6 +28,7 @@ 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');
|
||||||
|
|
||||||
function register(app) {
|
function register(app) {
|
||||||
app.use('/', indexRoute);
|
app.use('/', indexRoute);
|
||||||
@@ -59,6 +60,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -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 = 75;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
app_version: packageJson.version,
|
app_version: packageJson.version,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const utils = require('./utils');
|
|||||||
const sync_table = require('./sync_table');
|
const sync_table = require('./sync_table');
|
||||||
const Repository = require('./repository');
|
const Repository = require('./repository');
|
||||||
|
|
||||||
const BUILTIN_ATTRIBUTES = [ 'run_on_startup', 'disable_versioning' ];
|
const BUILTIN_ATTRIBUTES = [ 'run_on_startup', 'disable_versioning', 'calendar_root' ];
|
||||||
|
|
||||||
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 = ?`, [noteId]);
|
||||||
@@ -13,7 +13,10 @@ async function getNoteAttributeMap(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(dataKey, name, value) {
|
||||||
@@ -23,11 +26,11 @@ async function getNotesWithAttribute(dataKey, name, value) {
|
|||||||
|
|
||||||
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;
|
||||||
@@ -41,7 +44,7 @@ async function getNoteWithAttribute(dataKey, name, value) {
|
|||||||
|
|
||||||
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 = null, sourceId = null) {
|
||||||
@@ -54,7 +57,8 @@ async function createAttribute(noteId, name, value = null, sourceId = null) {
|
|||||||
name: name,
|
name: name,
|
||||||
value: value,
|
value: value,
|
||||||
dateModified: now,
|
dateModified: now,
|
||||||
dateCreated: now
|
dateCreated: now,
|
||||||
|
isDeleted: false
|
||||||
});
|
});
|
||||||
|
|
||||||
await sync_table.addAttributeSync(attributeId, sourceId);
|
await sync_table.addAttributeSync(attributeId, sourceId);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
|||||||
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
|
||||||
|
};
|
||||||
@@ -154,10 +154,10 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
|
|||||||
note.isProtected = false;
|
note.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 +167,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) {
|
||||||
@@ -235,7 +235,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
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
@@ -233,6 +236,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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -137,6 +137,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 +160,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);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function newNoteTreeId() {
|
|||||||
return randomString(12);
|
return randomString(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
function newnoteRevisionId() {
|
function newNoteRevisionId() {
|
||||||
return randomString(12);
|
return randomString(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +27,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);
|
||||||
}
|
}
|
||||||
@@ -47,7 +51,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 +60,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');
|
||||||
}
|
}
|
||||||
@@ -117,12 +127,14 @@ module.exports = {
|
|||||||
nowDate,
|
nowDate,
|
||||||
dateStr,
|
dateStr,
|
||||||
parseDate,
|
parseDate,
|
||||||
|
parseDateTime,
|
||||||
newNoteId,
|
newNoteId,
|
||||||
newNoteTreeId,
|
newNoteTreeId,
|
||||||
newnoteRevisionId,
|
newNoteRevisionId,
|
||||||
newImageId,
|
newImageId,
|
||||||
newNoteImageId,
|
newNoteImageId,
|
||||||
newAttributeId,
|
newAttributeId,
|
||||||
|
newApiTokenId,
|
||||||
toBase64,
|
toBase64,
|
||||||
fromBase64,
|
fromBase64,
|
||||||
hmac,
|
hmac,
|
||||||
|
|||||||
@@ -151,20 +151,7 @@
|
|||||||
</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;">
|
||||||
@@ -373,8 +360,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>
|
||||||
@@ -389,31 +379,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="text-align: center">
|
<div style="text-align: center">
|
||||||
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save <kbd>enter</kbd></button>
|
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></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>
|
||||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
<!-- 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 }"/>
|
<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.isNotUnique($index())">Attribute name must be unique per note.</div>
|
|
||||||
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</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" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
|
<input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
|
||||||
</td>
|
</td>
|
||||||
|
<td title="Delete" style="padding: 13px;">
|
||||||
|
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user