mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 03:46:37 +01:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			v0.5.0-bet
			...
			v0.5.5-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					02e07ec03a | ||
| 
						 | 
					3d2dc8e699 | ||
| 
						 | 
					c84e15c9be | ||
| 
						 | 
					e18d0b9fd4 | ||
| 
						 | 
					52817504d1 | ||
| 
						 | 
					a3b31fab54 | ||
| 
						 | 
					bc4aa3e40a | ||
| 
						 | 
					873ea67e9c | ||
| 
						 | 
					2c5115003b | ||
| 
						 | 
					e8ed913374 | ||
| 
						 | 
					5bffba4e2f | ||
| 
						 | 
					05575913db | ||
| 
						 | 
					31c32ff42c | ||
| 
						 | 
					6a671a5c02 | ||
| 
						 | 
					e174aec299 | ||
| 
						 | 
					d1329f60c3 | ||
| 
						 | 
					56b59e59bc | ||
| 
						 | 
					0ef426dde3 | 
							
								
								
									
										190
									
								
								db/schema.sql
									
									
									
									
									
								
							
							
						
						
									
										190
									
								
								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`)
 | 
			
		||||
  `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 `event_log` (
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "event_log" (
 | 
			
		||||
  `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
    `note_id`	TEXT,
 | 
			
		||||
  `noteId`	TEXT,
 | 
			
		||||
  `comment`	TEXT,
 | 
			
		||||
    `date_added`	TEXT NOT NULL,
 | 
			
		||||
    FOREIGN KEY(note_id) REFERENCES notes(note_id)
 | 
			
		||||
  `dateAdded`	TEXT NOT NULL,
 | 
			
		||||
  FOREIGN KEY(noteId) REFERENCES notes(noteId)
 | 
			
		||||
);
 | 
			
		||||
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);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
const electron = require('electron');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const config = require('./services/config');
 | 
			
		||||
const config = require('./src/services/config');
 | 
			
		||||
const url = require("url");
 | 
			
		||||
 | 
			
		||||
const app = electron.app;
 | 
			
		||||
@@ -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';
 | 
			
		||||
@@ -69,4 +69,4 @@ app.on('ready', () => {
 | 
			
		||||
    mainWindow = createMainWindow();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
require('./www');
 | 
			
		||||
require('./src/www');
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "description": "Trilium Notes",
 | 
			
		||||
  "version": "0.5.0-beta",
 | 
			
		||||
  "version": "0.5.5-beta",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "main": "electron.js",
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
    "url": "https://github.com/zadam/trilium.git"
 | 
			
		||||
@@ -11,8 +12,8 @@
 | 
			
		||||
    "start": "node ./bin/www",
 | 
			
		||||
    "test-electron": "xo",
 | 
			
		||||
    "rebuild-electron": "electron-rebuild",
 | 
			
		||||
    "start-electron": "electron src/electron",
 | 
			
		||||
    "build-electron": "electron-packager src/electron --out=dist --asar --overwrite --all",
 | 
			
		||||
    "start-electron": "electron .",
 | 
			
		||||
    "build-electron": "electron-packager . --out=dist --asar --overwrite --all",
 | 
			
		||||
    "start-forge": "electron-forge start",
 | 
			
		||||
    "package-forge": "electron-forge package",
 | 
			
		||||
    "make-forge": "electron-forge make",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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,111 @@ 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-name:last").focus();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.addNewRow = function() {
 | 
			
		||||
            self.attributes.push({
 | 
			
		||||
        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 attributesToSave = self.attributes()
 | 
			
		||||
                .map(attr => attr())
 | 
			
		||||
                .filter(attr => attr.attributeId !== "" || attr.name !== "");
 | 
			
		||||
 | 
			
		||||
            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[attrs.length - 1]();
 | 
			
		||||
 | 
			
		||||
            if (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.save = async function() {
 | 
			
		||||
            const noteId = noteEditor.getCurrentNoteId();
 | 
			
		||||
        this.isNotUnique = function(index) {
 | 
			
		||||
            const cur = self.attributes()[index]();
 | 
			
		||||
 | 
			
		||||
            const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes());
 | 
			
		||||
            if (cur.name.trim() === "") {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self.attributes(attributes);
 | 
			
		||||
            for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
 | 
			
		||||
                const attr = attrs[i]();
 | 
			
		||||
 | 
			
		||||
            showMessage("Attributes have been saved.");
 | 
			
		||||
                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 +131,56 @@ 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('blur', '.attribute-name', function (e) { console.log("blur!"); });
 | 
			
		||||
 | 
			
		||||
    $(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
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -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 /\s/.test(val) ? '"' + val + '"' : val;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatAttribute(attr) {
 | 
			
		||||
    let str = "@" + formatValueWithWhitespace(attr.name);
 | 
			
		||||
 | 
			
		||||
    if (attr.value !== "") {
 | 
			
		||||
        str += "=" + formatValueWithWhitespace(attr.value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return str;
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,8 @@
 | 
			
		||||
    grid-template-areas: "header header"
 | 
			
		||||
                         "tree-actions title"
 | 
			
		||||
                         "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
 | 
			
		||||
@@ -108,7 +109,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;
 | 
			
		||||
}
 | 
			
		||||
@@ -190,11 +191,6 @@ div.ui-tooltip {
 | 
			
		||||
    float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#note-id-display {
 | 
			
		||||
    color: lightgrey;
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#note-source {
 | 
			
		||||
    height: 98%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
@@ -243,8 +239,9 @@ div.ui-tooltip {
 | 
			
		||||
#note-id-display {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 10px;
 | 
			
		||||
    bottom: 5px;
 | 
			
		||||
    bottom: 8px;
 | 
			
		||||
    z-index: 1000;
 | 
			
		||||
    color: lightgrey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#note-type-dropdown {
 | 
			
		||||
@@ -254,3 +251,15 @@ div.ui-tooltip {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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.replace(new RegExp(match[0], 'g'), '');
 | 
			
		||||
 | 
			
		||||
        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">
 | 
			
		||||
@@ -53,12 +58,11 @@
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
 | 
			
		||||
          <p>
 | 
			
		||||
          <div style="display: flex; align-items: center;">
 | 
			
		||||
            <label>Search:</label>
 | 
			
		||||
            <input name="search-text" autocomplete="off">
 | 
			
		||||
            <button id="reset-search-button">×</button>
 | 
			
		||||
            <span id="matches"></span>
 | 
			
		||||
          </p>
 | 
			
		||||
            <input name="search-text" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
 | 
			
		||||
            <button id="reset-search-button" class="btn btn-sm" title="Reset search">×</button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@@ -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-primary btn-large" id="save-attributes-button" type="submit">Save</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