mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
20 Commits
v0.5.2-bet
...
v0.5.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85d32c66f2 | ||
|
|
214d2e7659 | ||
|
|
f380bb7f65 | ||
|
|
0a9a032daa | ||
|
|
23a2b58b24 | ||
|
|
aee64b2522 | ||
|
|
02e07ec03a | ||
|
|
3d2dc8e699 | ||
|
|
c84e15c9be | ||
|
|
e18d0b9fd4 | ||
|
|
52817504d1 | ||
|
|
a3b31fab54 | ||
|
|
bc4aa3e40a | ||
|
|
873ea67e9c | ||
|
|
2c5115003b | ||
|
|
e8ed913374 | ||
|
|
5bffba4e2f | ||
|
|
05575913db | ||
|
|
31c32ff42c | ||
|
|
6a671a5c02 |
196
db/schema.sql
196
db/schema.sql
@@ -1,119 +1,121 @@
|
||||
CREATE TABLE IF NOT EXISTS "options" (
|
||||
`opt_name` TEXT NOT NULL PRIMARY KEY,
|
||||
`opt_value` TEXT,
|
||||
`date_modified` INT
|
||||
, is_synced INTEGER NOT NULL DEFAULT 0);
|
||||
`name` TEXT NOT NULL PRIMARY KEY,
|
||||
`value` TEXT,
|
||||
`dateModified` INT,
|
||||
isSynced INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS "sync" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entity_name` TEXT NOT NULL,
|
||||
`entity_id` TEXT NOT NULL,
|
||||
`source_id` TEXT NOT NULL,
|
||||
`sync_date` TEXT NOT NULL);
|
||||
CREATE UNIQUE INDEX `IDX_sync_entity_name_id` ON `sync` (
|
||||
`entity_name`,
|
||||
`entity_id`
|
||||
);
|
||||
CREATE INDEX `IDX_sync_sync_date` ON `sync` (
|
||||
`sync_date`
|
||||
);
|
||||
CREATE TABLE `source_ids` (
|
||||
`source_id` TEXT NOT NULL,
|
||||
`date_created` TEXT NOT NULL,
|
||||
PRIMARY KEY(`source_id`)
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entityName` TEXT NOT NULL,
|
||||
`entityId` TEXT NOT NULL,
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`syncDate` TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "source_ids" (
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
PRIMARY KEY(`sourceId`)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`note_id` TEXT NOT NULL,
|
||||
`note_title` TEXT,
|
||||
`note_text` TEXT,
|
||||
`is_protected` INT NOT NULL DEFAULT 0,
|
||||
`is_deleted` INT NOT NULL DEFAULT 0,
|
||||
`date_created` TEXT NOT NULL,
|
||||
`date_modified` TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'text', mime TEXT NOT NULL DEFAULT 'text/html',
|
||||
PRIMARY KEY(`note_id`)
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT,
|
||||
`content` TEXT,
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`isDeleted` INT NOT NULL DEFAULT 0,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'text',
|
||||
mime TEXT NOT NULL DEFAULT 'text/html',
|
||||
PRIMARY KEY(`noteId`)
|
||||
);
|
||||
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
||||
`is_deleted`
|
||||
CREATE TABLE IF NOT EXISTS "event_log" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`noteId` TEXT,
|
||||
`comment` TEXT,
|
||||
`dateAdded` TEXT NOT NULL,
|
||||
FOREIGN KEY(noteId) REFERENCES notes(noteId)
|
||||
);
|
||||
CREATE TABLE `event_log` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`note_id` TEXT,
|
||||
`comment` TEXT,
|
||||
`date_added` TEXT NOT NULL,
|
||||
FOREIGN KEY(note_id) REFERENCES notes(note_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "notes_tree" (
|
||||
`note_tree_id` TEXT NOT NULL,
|
||||
`note_id` TEXT NOT NULL,
|
||||
`parent_note_id` TEXT NOT NULL,
|
||||
`note_position` INTEGER NOT NULL,
|
||||
CREATE TABLE IF NOT EXISTS "note_tree" (
|
||||
`noteTreeId` TEXT NOT NULL,
|
||||
`noteId` TEXT NOT NULL,
|
||||
`parentNoteId` TEXT NOT NULL,
|
||||
`notePosition` INTEGER NOT NULL,
|
||||
`prefix` TEXT,
|
||||
`is_expanded` BOOLEAN,
|
||||
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`date_modified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`note_tree_id`)
|
||||
`isExpanded` BOOLEAN,
|
||||
`isDeleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteTreeId`)
|
||||
);
|
||||
CREATE INDEX `IDX_notes_tree_note_id` ON `notes_tree` (
|
||||
`note_id`
|
||||
CREATE TABLE IF NOT EXISTS "note_revisions" (
|
||||
`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT,
|
||||
`content` TEXT,
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`dateModifiedFrom` TEXT NOT NULL,
|
||||
`dateModifiedTo` TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "notes_history" (
|
||||
`note_history_id` TEXT NOT NULL PRIMARY KEY,
|
||||
`note_id` TEXT NOT NULL,
|
||||
`note_title` TEXT,
|
||||
`note_text` TEXT,
|
||||
`is_protected` INT NOT NULL DEFAULT 0,
|
||||
`date_modified_from` TEXT NOT NULL,
|
||||
`date_modified_to` TEXT NOT NULL
|
||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
|
||||
`noteTreeId` TEXT NOT NULL PRIMARY KEY,
|
||||
`notePath` TEXT NOT NULL,
|
||||
`dateAccessed` TEXT NOT NULL,
|
||||
isDeleted INT
|
||||
);
|
||||
CREATE INDEX `IDX_notes_history_note_id` ON `notes_history` (
|
||||
`note_id`
|
||||
);
|
||||
CREATE INDEX `IDX_notes_history_note_date_modified_from` ON `notes_history` (
|
||||
`date_modified_from`
|
||||
);
|
||||
CREATE INDEX `IDX_notes_history_note_date_modified_to` ON `notes_history` (
|
||||
`date_modified_to`
|
||||
);
|
||||
CREATE TABLE `recent_notes` (
|
||||
`note_tree_id` TEXT NOT NULL PRIMARY KEY,
|
||||
`note_path` TEXT NOT NULL,
|
||||
`date_accessed` TEXT NOT NULL,
|
||||
is_deleted INT
|
||||
);
|
||||
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||
`note_id`,
|
||||
`parent_note_id`
|
||||
);
|
||||
CREATE TABLE images
|
||||
CREATE TABLE IF NOT EXISTS "images"
|
||||
(
|
||||
image_id TEXT PRIMARY KEY NOT NULL,
|
||||
imageId TEXT PRIMARY KEY NOT NULL,
|
||||
format TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
data BLOB,
|
||||
is_deleted INT NOT NULL DEFAULT 0,
|
||||
date_modified TEXT NOT NULL,
|
||||
date_created TEXT NOT NULL
|
||||
isDeleted INT NOT NULL DEFAULT 0,
|
||||
dateModified TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE notes_image
|
||||
CREATE TABLE note_images
|
||||
(
|
||||
note_image_id TEXT PRIMARY KEY NOT NULL,
|
||||
note_id TEXT NOT NULL,
|
||||
image_id TEXT NOT NULL,
|
||||
is_deleted INT NOT NULL DEFAULT 0,
|
||||
date_modified TEXT NOT NULL,
|
||||
date_created TEXT NOT NULL
|
||||
noteImageId TEXT PRIMARY KEY NOT NULL,
|
||||
noteId TEXT NOT NULL,
|
||||
imageId TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0,
|
||||
dateModified TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX notes_image_note_id_index ON notes_image (note_id);
|
||||
CREATE INDEX notes_image_image_id_index ON notes_image (image_id);
|
||||
CREATE INDEX notes_image_note_id_image_id_index ON notes_image (note_id, image_id);
|
||||
CREATE TABLE attributes
|
||||
CREATE TABLE IF NOT EXISTS "attributes"
|
||||
(
|
||||
attribute_id TEXT PRIMARY KEY NOT NULL,
|
||||
note_id TEXT NOT NULL,
|
||||
attributeId TEXT PRIMARY KEY NOT NULL,
|
||||
noteId TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT,
|
||||
date_created TEXT NOT NULL,
|
||||
date_modified TEXT NOT NULL
|
||||
dateCreated TEXT NOT NULL,
|
||||
dateModified TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX attributes_note_id_index ON attributes (note_id);
|
||||
CREATE UNIQUE INDEX attributes_note_id_name_index ON attributes (note_id, name);
|
||||
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
|
||||
`entityName`,
|
||||
`entityId`
|
||||
);
|
||||
CREATE INDEX `IDX_sync_syncDate` ON `sync` (
|
||||
`syncDate`
|
||||
);
|
||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
|
||||
`isDeleted`
|
||||
);
|
||||
CREATE INDEX `IDX_note_tree_noteId` ON `note_tree` (
|
||||
`noteId`
|
||||
);
|
||||
CREATE INDEX `IDX_note_tree_noteId_parentNoteId` ON `note_tree` (
|
||||
`noteId`,
|
||||
`parentNoteId`
|
||||
);
|
||||
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
|
||||
`noteId`
|
||||
);
|
||||
CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` (
|
||||
`dateModifiedFrom`
|
||||
);
|
||||
CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` (
|
||||
`dateModifiedTo`
|
||||
);
|
||||
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_noteId_imageId ON note_images (noteId, imageId);
|
||||
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
|
||||
CREATE UNIQUE INDEX IDX_attributes_noteId_name ON attributes (noteId, name);
|
||||
|
||||
@@ -24,7 +24,7 @@ function createMainWindow() {
|
||||
width: 1200,
|
||||
height: 900,
|
||||
title: 'Trilium Notes',
|
||||
icon: path.join(__dirname, 'public/images/app-icons/png/256x256.png')
|
||||
icon: path.join(__dirname, 'src/public/images/app-icons/png/256x256.png')
|
||||
});
|
||||
|
||||
const port = config['Network']['port'] || '3000';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.5.2-beta",
|
||||
"version": "0.5.6",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"repository": {
|
||||
@@ -12,7 +12,7 @@
|
||||
"start": "node ./bin/www",
|
||||
"test-electron": "xo",
|
||||
"rebuild-electron": "electron-rebuild",
|
||||
"start-electron": "electron src/electron",
|
||||
"start-electron": "electron .",
|
||||
"build-electron": "electron-packager . --out=dist --asar --overwrite --all",
|
||||
"start-forge": "electron-forge start",
|
||||
"package-forge": "electron-forge package",
|
||||
|
||||
21
src/public/javascripts/api.js
Normal file
21
src/public/javascripts/api.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const api = (function() {
|
||||
const pluginButtonsEl = $("#plugin-buttons");
|
||||
|
||||
async function activateNote(notePath) {
|
||||
await noteTree.activateNode(notePath);
|
||||
}
|
||||
|
||||
function addButtonToToolbar(buttonId, button) {
|
||||
$("#" + buttonId).remove();
|
||||
|
||||
button.attr('id', buttonId);
|
||||
|
||||
pluginButtonsEl.append(button);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
addButtonToToolbar,
|
||||
activateNote
|
||||
}
|
||||
})();
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
const attributesDialog = (function() {
|
||||
const dialogEl = $("#attributes-dialog");
|
||||
const saveAttributesButton = $("#save-attributes-button");
|
||||
const attributesModel = new AttributesModel();
|
||||
let attributeNames = [];
|
||||
|
||||
function AttributesModel() {
|
||||
const self = this;
|
||||
@@ -14,38 +16,112 @@ const attributesDialog = (function() {
|
||||
|
||||
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||
|
||||
this.attributes(attributes);
|
||||
self.attributes(attributes.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
attributeNames = await server.get('attributes/names');
|
||||
|
||||
// attribute might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".attribute-name:last").focus(), 100);
|
||||
};
|
||||
|
||||
this.addNewRow = function() {
|
||||
self.attributes.push({
|
||||
attributeId: '',
|
||||
name: '',
|
||||
value: ''
|
||||
});
|
||||
};
|
||||
function isValid() {
|
||||
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
|
||||
if (self.isEmptyName(i) || self.isNotUnique(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
saveAttributesButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
const noteId = noteEditor.getCurrentNoteId();
|
||||
|
||||
const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes());
|
||||
const attributesToSave = self.attributes()
|
||||
.map(attr => attr())
|
||||
.filter(attr => attr.attributeId !== "" || attr.name !== "");
|
||||
|
||||
self.attributes(attributes);
|
||||
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
|
||||
|
||||
self.attributes(attributes.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
showMessage("Attributes have been saved.");
|
||||
|
||||
noteEditor.loadAttributeList();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
const attrs = self.attributes();
|
||||
const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
|
||||
|
||||
if (!last || last.name.trim() !== "" || last.value !== "") {
|
||||
self.attributes.push(ko.observable({
|
||||
attributeId: '',
|
||||
name: '',
|
||||
value: ''
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.attributeChanged = function (row) {
|
||||
addLastEmptyRow();
|
||||
|
||||
for (const attr of self.attributes()) {
|
||||
if (row.attributeId === attr().attributeId) {
|
||||
attr.valueHasMutated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.isNotUnique = function(index) {
|
||||
const cur = self.attributes()[index]();
|
||||
|
||||
if (cur.name.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
|
||||
const attr = attrs[i]();
|
||||
|
||||
if (index !== i && cur.name === attr.name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.attributes()[index]();
|
||||
|
||||
return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== "");
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
await attributesModel.loadAttributes();
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 700
|
||||
height: 500
|
||||
});
|
||||
|
||||
attributesModel.loadAttributes();
|
||||
}
|
||||
|
||||
$(document).bind('keydown', 'alt+a', e => {
|
||||
@@ -56,6 +132,54 @@ const attributesDialog = (function() {
|
||||
|
||||
ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
|
||||
|
||||
$(document).on('focus', '.attribute-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in init.js
|
||||
source: attributeNames.map(attr => {
|
||||
return {
|
||||
label: attr,
|
||||
value: attr
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
$(document).on('focus', '.attribute-value', async function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
const attributeName = $(this).parent().parent().find('.attribute-name').val();
|
||||
|
||||
if (attributeName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
|
||||
|
||||
if (attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in init.js
|
||||
source: attributeValues.map(attr => {
|
||||
return {
|
||||
label: attr,
|
||||
value: attr
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
|
||||
@@ -54,24 +54,6 @@ $(document).bind('keydown', 'ctrl+f', () => {
|
||||
}
|
||||
});
|
||||
|
||||
$(document).bind('keydown', "ctrl+shift+left", () => {
|
||||
const node = noteTree.getCurrentNode();
|
||||
node.navigate($.ui.keyCode.LEFT, true);
|
||||
|
||||
$("#note-detail").focus();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).bind('keydown', "ctrl+shift+right", () => {
|
||||
const node = noteTree.getCurrentNode();
|
||||
node.navigate($.ui.keyCode.RIGHT, true);
|
||||
|
||||
$("#note-detail").focus();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).bind('keydown', "ctrl+shift+up", () => {
|
||||
const node = noteTree.getCurrentNode();
|
||||
node.navigate($.ui.keyCode.UP, true);
|
||||
@@ -123,7 +105,7 @@ $(window).on('beforeunload', () => {
|
||||
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
|
||||
$.ui.autocomplete.filter = (array, terms) => {
|
||||
if (!terms) {
|
||||
return [];
|
||||
return array;
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
|
||||
@@ -9,6 +9,8 @@ const noteEditor = (function() {
|
||||
const unprotectButton = $("#unprotect-button");
|
||||
const noteDetailWrapperEl = $("#note-detail-wrapper");
|
||||
const noteIdDisplayEl = $("#note-id-display");
|
||||
const attributeListEl = $("#attribute-list");
|
||||
const attributeListInnerEl = $("#attribute-list-inner");
|
||||
|
||||
let editor = null;
|
||||
let codeEditor = null;
|
||||
@@ -187,6 +189,27 @@ const noteEditor = (function() {
|
||||
|
||||
// after loading new note make sure editor is scrolled to the top
|
||||
noteDetailWrapperEl.scrollTop(0);
|
||||
|
||||
loadAttributeList();
|
||||
}
|
||||
|
||||
async function loadAttributeList() {
|
||||
const noteId = getCurrentNoteId();
|
||||
|
||||
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||
|
||||
attributeListInnerEl.html('');
|
||||
|
||||
if (attributes.length > 0) {
|
||||
for (const attr of attributes) {
|
||||
attributeListInnerEl.append(formatAttribute(attr) + " ");
|
||||
}
|
||||
|
||||
attributeListEl.show();
|
||||
}
|
||||
else {
|
||||
attributeListEl.hide();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNote(noteId) {
|
||||
@@ -290,6 +313,7 @@ const noteEditor = (function() {
|
||||
newNoteCreated,
|
||||
getEditor,
|
||||
focus,
|
||||
executeCurrentNote
|
||||
executeCurrentNote,
|
||||
loadAttributeList
|
||||
};
|
||||
})();
|
||||
@@ -3,7 +3,7 @@
|
||||
const noteTree = (function() {
|
||||
const treeEl = $("#tree");
|
||||
const parentListEl = $("#parent-list");
|
||||
const parentListListEl = $("#parent-list-list");
|
||||
const parentListListEl = $("#parent-list-inner");
|
||||
|
||||
let startNotePath = null;
|
||||
let notesTreeMap = {};
|
||||
|
||||
@@ -116,5 +116,20 @@ async function stopWatch(what, func) {
|
||||
}
|
||||
|
||||
function executeScript(script) {
|
||||
eval("(async function() {" + script + "})()");
|
||||
// last \r\n is necessary if script contains line comment on its last line
|
||||
eval("(async function() {" + script + "\r\n})()");
|
||||
}
|
||||
|
||||
function formatValueWithWhitespace(val) {
|
||||
return /[^\w_-]/.test(val) ? '"' + val + '"' : val;
|
||||
}
|
||||
|
||||
function formatAttribute(attr) {
|
||||
let str = "@" + formatValueWithWhitespace(attr.name);
|
||||
|
||||
if (attr.value !== "") {
|
||||
str += "=" + formatValueWithWhitespace(attr.value);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
@@ -5,12 +5,18 @@
|
||||
display: grid;
|
||||
grid-template-areas: "header header"
|
||||
"tree-actions title"
|
||||
"search note-content"
|
||||
"tree note-content"
|
||||
"parent-list note-content";
|
||||
"parent-list note-content"
|
||||
"parent-list attribute-list";
|
||||
grid-template-columns: 2fr 5fr;
|
||||
grid-template-rows: auto
|
||||
auto
|
||||
1fr;
|
||||
auto
|
||||
1fr
|
||||
auto
|
||||
auto;
|
||||
|
||||
justify-content: center;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
@@ -108,7 +114,7 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
}
|
||||
|
||||
#header-title {
|
||||
padding: 5px 50px 5px 10px;
|
||||
padding: 5px 20px 5px 10px;
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -134,6 +140,7 @@ div.ui-tooltip {
|
||||
margin-left: 20px;
|
||||
border-top: 2px solid #eee;
|
||||
padding-top: 10px;
|
||||
grid-area: parent-list;
|
||||
}
|
||||
|
||||
#parent-list ul {
|
||||
@@ -190,11 +197,6 @@ div.ui-tooltip {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#note-id-display {
|
||||
color: lightgrey;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#note-source {
|
||||
height: 98%;
|
||||
width: 100%;
|
||||
@@ -243,8 +245,9 @@ div.ui-tooltip {
|
||||
#note-id-display {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 5px;
|
||||
bottom: 8px;
|
||||
z-index: 1000;
|
||||
color: lightgrey;
|
||||
}
|
||||
|
||||
#note-type-dropdown {
|
||||
@@ -253,4 +256,16 @@ div.ui-tooltip {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.cm-matchhighlight {background-color: #eeeeee}
|
||||
.cm-matchhighlight {background-color: #eeeeee}
|
||||
|
||||
#attribute-list {
|
||||
grid-area: attribute-list;
|
||||
color: #777777;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 5px; display: none;
|
||||
}
|
||||
|
||||
#attribute-list button {
|
||||
padding: 2px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -7,14 +7,15 @@ const auth = require('../../services/auth');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const utils = require('../../services/utils');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
const attributes = require('../../services/attributes');
|
||||
|
||||
router.get('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
|
||||
}));
|
||||
|
||||
router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
const attributes = req.body;
|
||||
const now = utils.nowDate();
|
||||
@@ -45,4 +46,26 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next)
|
||||
res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
|
||||
}));
|
||||
|
||||
router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const names = await sql.getColumn("SELECT DISTINCT name FROM attributes");
|
||||
|
||||
for (const attr of attributes.BUILTIN_ATTRIBUTES) {
|
||||
if (!names.includes(attr)) {
|
||||
names.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
names.sort();
|
||||
|
||||
res.send(names);
|
||||
}));
|
||||
|
||||
router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const attributeName = req.params.attributeName;
|
||||
|
||||
const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE name = ? AND value != '' ORDER BY value", [attributeName]);
|
||||
|
||||
res.send(values);
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -58,15 +58,112 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
}));
|
||||
|
||||
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const search = '%' + utils.sanitizeSql(req.query.search) + '%';
|
||||
let {attrFilters, searchText} = parseFilters(req.query.search);
|
||||
|
||||
// searching in protected notes is pointless because of encryption
|
||||
const noteIds = await sql.getColumn(`SELECT noteId FROM notes
|
||||
WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]);
|
||||
const {query, params} = getSearchQuery(attrFilters, searchText);
|
||||
|
||||
const noteIds = await sql.getColumn(query, params);
|
||||
|
||||
res.send(noteIds);
|
||||
}));
|
||||
|
||||
function parseFilters(searchText) {
|
||||
const attrFilters = [];
|
||||
|
||||
const attrRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i;
|
||||
|
||||
let match = attrRegex.exec(searchText);
|
||||
|
||||
function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; }
|
||||
|
||||
while (match != null) {
|
||||
const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and';
|
||||
const operator = match[3] === '!' ? 'not-exists' : 'exists';
|
||||
|
||||
attrFilters.push({
|
||||
relation: relation,
|
||||
name: trimQuotes(match[4]),
|
||||
operator: match[6] !== undefined ? match[6] : operator,
|
||||
value: match[7] !== undefined ? trimQuotes(match[7]) : null
|
||||
});
|
||||
|
||||
// remove attributes from further fulltext search
|
||||
searchText = searchText.split(match[0]).join('');
|
||||
|
||||
match = attrRegex.exec(searchText);
|
||||
}
|
||||
|
||||
return {attrFilters, searchText};
|
||||
}
|
||||
|
||||
function getSearchQuery(attrFilters, searchText) {
|
||||
const joins = [];
|
||||
const joinParams = [];
|
||||
let where = '1';
|
||||
const whereParams = [];
|
||||
|
||||
let i = 1;
|
||||
|
||||
for (const filter of attrFilters) {
|
||||
joins.push(`LEFT JOIN attributes AS attr${i} ON attr${i}.noteId = notes.noteId AND attr${i}.name = ?`);
|
||||
joinParams.push(filter.name);
|
||||
|
||||
where += " " + filter.relation + " ";
|
||||
|
||||
if (filter.operator === 'exists') {
|
||||
where += `attr${i}.attributeId IS NOT NULL`;
|
||||
}
|
||||
else if (filter.operator === 'not-exists') {
|
||||
where += `attr${i}.attributeId IS NULL`;
|
||||
}
|
||||
else if (filter.operator === '=' || filter.operator === '!=') {
|
||||
where += `attr${i}.value ${filter.operator} ?`;
|
||||
whereParams.push(filter.value);
|
||||
}
|
||||
else if ([">", ">=", "<", "<="].includes(filter.operator)) {
|
||||
const floatParam = parseFloat(filter.value);
|
||||
|
||||
if (isNaN(floatParam)) {
|
||||
where += `attr${i}.value ${filter.operator} ?`;
|
||||
whereParams.push(filter.value);
|
||||
}
|
||||
else {
|
||||
where += `CAST(attr${i}.value AS DECIMAL) ${filter.operator} ?`;
|
||||
whereParams.push(floatParam);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error("Unknown operator " + filter.operator);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
let searchCondition = '';
|
||||
const searchParams = [];
|
||||
|
||||
if (searchText.trim() !== '') {
|
||||
// searching in protected notes is pointless because of encryption
|
||||
searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))';
|
||||
|
||||
searchText = '%' + searchText.trim() + '%';
|
||||
|
||||
searchParams.push(searchText);
|
||||
searchParams.push(searchText); // two occurences in searchCondition
|
||||
}
|
||||
|
||||
const query = `SELECT notes.noteId FROM notes
|
||||
${joins.join('\r\n')}
|
||||
WHERE
|
||||
notes.isDeleted = 0
|
||||
AND (${where})
|
||||
${searchCondition}`;
|
||||
|
||||
const params = joinParams.concat(whereParams).concat(searchParams);
|
||||
|
||||
return { query, params };
|
||||
}
|
||||
|
||||
router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
@@ -19,11 +19,12 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
|
||||
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
|
||||
const repository = new Repository(req);
|
||||
|
||||
const scripts = [];
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
scripts.push(await getNoteWithSubtreeScript(noteId, req));
|
||||
scripts.push(await getNoteWithSubtreeScript(noteId, repository));
|
||||
}
|
||||
|
||||
res.send(scripts);
|
||||
@@ -41,10 +42,10 @@ router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) =>
|
||||
res.send(subTreeScripts + noteScript);
|
||||
}));
|
||||
|
||||
async function getNoteWithSubtreeScript(noteId, req) {
|
||||
const noteScript = (await notes.getNoteById(noteId, req)).content;
|
||||
async function getNoteWithSubtreeScript(noteId, repository) {
|
||||
const noteScript = (await repository.getNote(noteId)).content;
|
||||
|
||||
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], req);
|
||||
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
|
||||
|
||||
return subTreeScripts + noteScript;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ function register(app) {
|
||||
app.use('/api/notes', notesApiRoute);
|
||||
app.use('/api/tree', treeChangesApiRoute);
|
||||
app.use('/api/notes', cloningApiRoute);
|
||||
app.use('/api/notes', attributesRoute);
|
||||
app.use('/api', attributesRoute);
|
||||
app.use('/api/notes-history', noteHistoryApiRoute);
|
||||
app.use('/api/recent-changes', recentChangesApiRoute);
|
||||
app.use('/api/settings', settingsApiRoute);
|
||||
|
||||
11
src/scripts/today.js
Normal file
11
src/scripts/today.js
Normal file
@@ -0,0 +1,11 @@
|
||||
api.addButtonToToolbar('go-today', $('<button class="btn btn-xs" onclick="goToday();"><span class="ui-icon ui-icon-calendar"></span> Today</button>'));
|
||||
|
||||
window.goToday = async function() {
|
||||
const todayDateStr = formatDateISO(new Date());
|
||||
|
||||
const todayNoteId = await server.exec([todayDateStr], async todayDateStr => {
|
||||
return await this.getDateNoteId(todayDateStr);
|
||||
});
|
||||
|
||||
api.activateNote(todayNoteId);
|
||||
};
|
||||
@@ -65,6 +65,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
data.sort((a, b) => a.date < b.date ? -1 : +1);
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ const utils = require('./utils');
|
||||
const sync_table = require('./sync_table');
|
||||
const Repository = require('./repository');
|
||||
|
||||
const BUILTIN_ATTRIBUTES = [ 'run_on_startup', 'disable_versioning' ];
|
||||
|
||||
async function getNoteAttributeMap(noteId) {
|
||||
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]);
|
||||
}
|
||||
@@ -64,5 +66,6 @@ module.exports = {
|
||||
getNotesWithAttribute,
|
||||
getNoteWithAttribute,
|
||||
getNoteIdsWithAttribute,
|
||||
createAttribute
|
||||
createAttribute,
|
||||
BUILTIN_ATTRIBUTES
|
||||
};
|
||||
@@ -5,32 +5,6 @@ const sync_table = require('./sync_table');
|
||||
const attributes = require('./attributes');
|
||||
const protected_session = require('./protected_session');
|
||||
|
||||
async function updateJsonNote(noteId, data) {
|
||||
const ret = await createNewNote(noteId, {
|
||||
title: name,
|
||||
content: JSON.stringify(data),
|
||||
target: 'into',
|
||||
isProtected: false,
|
||||
type: 'code',
|
||||
mime: 'application/json'
|
||||
});
|
||||
|
||||
return ret.noteId;
|
||||
}
|
||||
|
||||
async function createNewJsonNote(parentNoteId, name, payload) {
|
||||
const ret = await createNewNote(parentNoteId, {
|
||||
title: name,
|
||||
content: JSON.stringify(payload),
|
||||
target: 'into',
|
||||
isProtected: false,
|
||||
type: 'code',
|
||||
mime: 'application/json'
|
||||
});
|
||||
|
||||
return ret.noteId;
|
||||
}
|
||||
|
||||
async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) {
|
||||
const noteId = utils.newNoteId();
|
||||
const noteTreeId = utils.newNoteTreeId();
|
||||
|
||||
@@ -17,15 +17,20 @@
|
||||
<button class="btn btn-xs" onclick="jumpToNote.showDialog();" title="CTRL+J">Jump to note</button>
|
||||
<button class="btn btn-xs" onclick="recentNotes.showDialog();" title="CTRL+E">Recent notes</button>
|
||||
<button class="btn btn-xs" onclick="recentChanges.showDialog();">Recent changes</button>
|
||||
<button class="btn btn-xs" onclick="eventLog.showDialog();">Event log</button>
|
||||
</div>
|
||||
|
||||
<div id="plugin-buttons">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-xs" onclick="syncNow();" title="Number of outstanding changes to be pushed to server">
|
||||
<span class="ui-icon ui-icon-refresh"></span>
|
||||
|
||||
Sync now (<span id="changes-to-push-count">0</span>)
|
||||
</button>
|
||||
|
||||
<button class="btn btn-xs" onclick="settings.showDialog();">Settings</button>
|
||||
<button class="btn btn-xs" onclick="settings.showDialog();">
|
||||
<span class="ui-icon ui-icon-gear"></span> Settings</button>
|
||||
|
||||
<form action="logout" id="logout-button" method="POST" style="display: inline;">
|
||||
<input type="submit" class="btn btn-xs" value="Logout">
|
||||
@@ -51,14 +56,13 @@
|
||||
<img src="images/icons/search.png" alt="Search in notes"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
|
||||
<p>
|
||||
<label>Search:</label>
|
||||
<input name="search-text" autocomplete="off">
|
||||
<button id="reset-search-button">×</button>
|
||||
<span id="matches"></span>
|
||||
</p>
|
||||
<div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<label>Search:</label>
|
||||
<input name="search-text" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
|
||||
<button id="reset-search-button" class="btn btn-sm" title="Reset search">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +72,7 @@
|
||||
<div id="parent-list" class="hide-toggle">
|
||||
<p><strong>Note locations:</strong></p>
|
||||
|
||||
<ul id="parent-list-list"></ul>
|
||||
<ul id="parent-list-inner"></ul>
|
||||
</div>
|
||||
|
||||
<div class="hide-toggle" style="grid-area: title;">
|
||||
@@ -138,6 +142,12 @@
|
||||
|
||||
<div id="note-detail-render"></div>
|
||||
</div>
|
||||
|
||||
<div id="attribute-list">
|
||||
<button class="btn btn-sm" onclick="attributesDialog.showDialog();">Attributes:</button>
|
||||
|
||||
<span id="attribute-list-inner"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="recent-notes-dialog" title="Recent notes" style="display: none;">
|
||||
@@ -378,10 +388,8 @@
|
||||
|
||||
<div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
|
||||
<form data-bind="submit: save">
|
||||
<div style="display: flex; justify-content: space-between; padding: 15px; padding-top: 0;">
|
||||
<button class="btn-default" type="button" data-bind="click: addNewRow">Add new attribute</button>
|
||||
|
||||
<button class="btn-primary" type="submit">Save</button>
|
||||
<div style="text-align: center">
|
||||
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save <kbd>enter</kbd></button>
|
||||
</div>
|
||||
|
||||
<div style="height: 97%; overflow: auto">
|
||||
@@ -397,10 +405,14 @@
|
||||
<tr>
|
||||
<td data-bind="text: attributeId"></td>
|
||||
<td>
|
||||
<input type="text" data-bind="value: name"/>
|
||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||
<input type="text" class="attribute-name" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"/>
|
||||
|
||||
<div style="color: 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>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" data-bind="value: value" style="width: 300px"/>
|
||||
<input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -492,7 +504,7 @@
|
||||
<script src="javascripts/link.js"></script>
|
||||
<script src="javascripts/sync.js"></script>
|
||||
<script src="javascripts/messaging.js"></script>
|
||||
|
||||
<script src="javascripts/api.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
// we hide container initally because otherwise it is rendered first without CSS and then flickers into
|
||||
|
||||
Reference in New Issue
Block a user