Compare commits

...

18 Commits

Author SHA1 Message Date
azivner
72df0d8861 release 0.6.0-beta 2018-02-11 22:06:57 -05:00
azivner
9910aebf45 fix schema.sql 2018-02-11 22:06:12 -05:00
azivner
f9f8ecb2b1 recent notes doesn't fail totally when we can't find title for some note 2018-02-11 15:33:10 -05:00
azivner
438f7c5b0b escape should close the recent notes dialog 2018-02-11 11:53:43 -05:00
azivner
4b1d1aba74 add sender API to send text notes 2018-02-11 10:54:56 -05:00
azivner
6dea73cfe2 sender API now accepts local time header so we don't have problems with UTC 2018-02-11 09:14:21 -05:00
azivner
58f5d0cf6e recent notes are not closed when I click on e.g. dialog title bar 2018-02-11 08:57:12 -05:00
azivner
7b77e40514 added support for trilium-sender 2018-02-11 00:18:59 -05:00
azivner
660908c54b fix sorting notes 2018-02-10 13:55:06 -05:00
azivner
e970564036 create months and days with associated english names, closes #37 2018-02-10 13:53:35 -05:00
azivner
b3038487f8 fix image support broken in recent refactorings 2018-02-10 10:00:40 -05:00
azivner
cac98392a6 code mirror in SQL console, closes #24 2018-02-10 09:14:18 -05:00
azivner
dbd28377e3 change in naming conventions for element variables from *El to $name 2018-02-10 08:44:34 -05:00
azivner
c76e4faf5d added attributes sorting 2018-02-10 08:37:14 -05:00
azivner
e011b9ae63 deleting attributes, closes #34 2018-02-06 23:09:19 -05:00
azivner
7c74c77a2c allow duplicated attribute per note (in effect attributes can be multi-valued). Closes #33 2018-02-06 21:18:09 -05:00
azivner
c2a2f195aa Merge branch 'stable' 2018-02-06 21:04:27 -05:00
azivner
4e70cebf70 recent notes now use autocomplete instead of select box, closes #36 2018-02-05 23:50:25 -05:00
44 changed files with 656 additions and 417 deletions

View File

@@ -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)

View File

@@ -0,0 +1 @@
DROP INDEX IDX_attributes_noteId_name;

View File

@@ -0,0 +1 @@
ALTER TABLE attributes ADD COLUMN isDeleted INT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1 @@
ALTER TABLE attributes ADD COLUMN position INT NOT NULL DEFAULT 0;

View 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
);

View File

@@ -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
);

View File

@@ -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": {

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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

View File

@@ -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;
}); });

View File

@@ -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);
} }
} }

View File

@@ -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();

View File

@@ -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 => {

View File

@@ -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) {

View File

@@ -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);
} }
} }

View File

@@ -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,

View File

@@ -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");

View File

@@ -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

View File

@@ -104,6 +104,8 @@ const server = (function() {
post, post,
put, put,
remove, remove,
exec exec,
// don't remove, used from CKEditor image upload!
getHeaders
} }
})(); })();

File diff suppressed because one or more lines are too long

View File

@@ -269,3 +269,7 @@ div.ui-tooltip {
padding: 2px; padding: 2px;
margin-right: 5px; margin-right: 5px;
} }
.recent-notes-autocomplete {
border: 0 !important;
}

View File

@@ -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);
})); }));

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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

View File

@@ -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
View 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;

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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
View 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
};

View File

@@ -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

View File

@@ -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}`);
} }

View File

@@ -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

View File

@@ -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
}; };

View File

@@ -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);

View File

@@ -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,

View File

@@ -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>
&nbsp;
<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>