Compare commits

...

13 Commits

Author SHA1 Message Date
azivner
b1ed022771 release 0.24.1-beta 2018-11-19 00:06:44 +01:00
azivner
ad6cb6ba34 mirror relation has to target source note 2018-11-19 00:06:04 +01:00
azivner
2e76de5f34 minor relation map fixes 2018-11-18 23:57:39 +01:00
azivner
4f23f2515a fix relation saving 2018-11-18 23:36:21 +01:00
azivner
8e16cc2326 fix prompt mutability (autocomplete on answer input field) 2018-11-18 23:31:31 +01:00
azivner
49bca04ebb fix doubling of notes after some actions 2018-11-18 23:01:48 +01:00
azivner
05e9669eaf fixed relation map sizing issue 2018-11-18 22:49:53 +01:00
azivner
62a250a7fc fixes for tooltip handling 2018-11-18 20:57:52 +01:00
azivner
8299524682 fixed cleanup of deleted notes for protected notes 2018-11-18 11:34:40 +01:00
azivner
7691a59977 imported notes can reference root 2018-11-18 11:19:50 +01:00
azivner
3db2f6784d fix DB migration and consistency issues 2018-11-18 10:20:06 +01:00
azivner
48684d0509 fixed app termination after unsuccessful migration for electron 2018-11-18 09:05:50 +01:00
azivner
1ee8d9fd93 sql console executes selected text if there's a selection instead of the whole content 2018-11-18 08:29:56 +01:00
23 changed files with 110 additions and 53 deletions

View File

@@ -1,4 +1,4 @@
FROM node:10.12.0 FROM node:10.13.0
RUN apt-get update && apt-get install -y nasm RUN apt-get update && apt-get install -y nasm

View File

@@ -7,7 +7,7 @@ fi
VERSION=$1 VERSION=$1
PKG_DIR=dist/trilium-linux-x64-server PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=10.12.0 NODE_VERSION=10.13.0
rm -r $PKG_DIR rm -r $PKG_DIR
mkdir $PKG_DIR mkdir $PKG_DIR

View File

@@ -1,3 +1,9 @@
-- first fix deleted status of existing images
UPDATE note_images SET isDeleted = 1 WHERE noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1);
-- we don't need set data to null because table is going to be dropped anyway and we want image size into attribute
UPDATE images SET isDeleted = 1 WHERE imageId NOT IN (SELECT imageId FROM note_images WHERE isDeleted = 0);
-- allow null for note content (for deleted notes) -- allow null for note content (for deleted notes)
CREATE TABLE IF NOT EXISTS "notes_mig" ( CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL, `noteId` TEXT NOT NULL,

View File

@@ -0,0 +1 @@
UPDATE attributes SET isDeleted = 1 WHERE noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1);

View File

@@ -0,0 +1 @@
UPDATE attributes SET isDeleted = 1 WHERE type = 'relation' AND value NOT IN (SELECT noteId FROM notes WHERE notes.isDeleted = 0);

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.23.1", "version": "0.24.0-beta",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,7 +1,7 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.24.0-beta", "version": "0.24.1-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {

View File

@@ -1,6 +1,9 @@
const $dialog = $("#prompt-dialog"); const $dialog = $("#prompt-dialog");
const $question = $("#prompt-dialog-question"); const $dialogBody = $dialog.find(".modal-body");
const $answer = $("#prompt-dialog-answer");
let $question;
let $answer;
const $form = $("#prompt-dialog-form"); const $form = $("#prompt-dialog-form");
let resolve; let resolve;
@@ -11,8 +14,21 @@ function ask({ message, defaultValue, shown }) {
shownCb = shown; shownCb = shown;
$question.text(message); $question = $("<label>")
$answer.val(defaultValue || ""); .prop("for", "prompt-dialog-answer")
.text(message);
$answer = $("<input>")
.prop("type", "text")
.prop("id", "prompt-dialog-answer")
.addClass("form-control")
.val(defaultValue || "");
$dialogBody.empty().append(
$("<div>")
.addClass("form-group")
.append($question)
.append($answer));
$dialog.modal(); $dialog.modal();

View File

@@ -50,7 +50,12 @@ async function execute(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const sqlQuery = codeEditor.getValue(); // execute the selected text or the whole content if there's no selection
let sqlQuery = codeEditor.getSelection();
if (!sqlQuery) {
sqlQuery = codeEditor.getValue();
}
const result = await server.post("sql/execute", { const result = await server.post("sql/execute", {
query: sqlQuery query: sqlQuery

View File

@@ -117,6 +117,15 @@ async function show() {
} }
function clearMap() {
// delete all endpoints and connections
// this is done at this point (after async operations) to reduce flicker to the minimum
jsPlumbInstance.deleteEveryEndpoint();
// without this we still end up with note boxes remaining in the canvas
$relationMapContainer.empty();
}
async function loadNotesAndRelations() { async function loadNotesAndRelations() {
const noteIds = mapData.notes.map(note => note.noteId); const noteIds = mapData.notes.map(note => note.noteId);
const data = await server.post("notes/relation-map", {noteIds}); const data = await server.post("notes/relation-map", {noteIds});
@@ -142,11 +151,9 @@ async function loadNotesAndRelations() {
mapData.notes = mapData.notes.filter(note => note.noteId in data.noteTitles); mapData.notes = mapData.notes.filter(note => note.noteId in data.noteTitles);
// delete all endpoints and connections
// this is done at this point (after async operations) to reduce flicker to the minimum
jsPlumbInstance.deleteEveryEndpoint();
jsPlumbInstance.batch(async function () { jsPlumbInstance.batch(async function () {
clearMap();
for (const note of mapData.notes) { for (const note of mapData.notes) {
const title = data.noteTitles[note.noteId]; const title = data.noteTitles[note.noteId];
@@ -208,10 +215,17 @@ function initPanZoom() {
mapData.notes.push({ noteId: clipboard.noteId, x, y }); mapData.notes.push({ noteId: clipboard.noteId, x, y });
saveData();
clipboard = null; clipboard = null;
} }
return true; return true;
},
filterKey: function(e, dx, dy, dz) {
// if ALT is pressed then panzoom should bubble the event up
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working
return e.altKey;
} }
}); });
@@ -244,11 +258,7 @@ function saveCurrentTransform() {
function cleanup() { function cleanup() {
if (jsPlumbInstance) { if (jsPlumbInstance) {
// delete all endpoints and connections clearMap();
jsPlumbInstance.deleteEveryEndpoint();
// without this we still end up with note boxes remaining in the canvas
$relationMapContainer.empty();
} }
if (pzInstance) { if (pzInstance) {
@@ -312,8 +322,6 @@ function connectionContextMenuHandler(connection, event) {
async function connectionCreatedHandler(info, originalEvent) { async function connectionCreatedHandler(info, originalEvent) {
const connection = info.connection; const connection = info.connection;
const isRelation = relations.some(rel => rel.attributeId === connection.id);
connection.bind("contextmenu", (obj, event) => { connection.bind("contextmenu", (obj, event) => {
if (connection.getType().includes("link")) { if (connection.getType().includes("link")) {
// don't create context menu if it's a link since there's nothing to do with link from relation map // don't create context menu if it's a link since there's nothing to do with link from relation map
@@ -362,9 +370,7 @@ async function connectionCreatedHandler(info, originalEvent) {
return; return;
} }
const attribute = await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`); await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`);
relations.push({ attributeId: attribute.attributeId , targetNoteId, sourceNoteId, name });
await refresh(); await refresh();
} }

View File

@@ -41,6 +41,10 @@ function setupTooltip() {
if ($(this).is(":hover")) { if ($(this).is(":hover")) {
$(this).tooltip({ $(this).tooltip({
delay: {"show": 300, "hide": 100}, delay: {"show": 300, "hide": 100},
container: 'body',
placement: 'auto',
trigger: 'manual',
boundariesElement: 'window',
title: html, title: html,
html: true html: true
}); });
@@ -50,7 +54,7 @@ function setupTooltip() {
}); });
$(document).on("mouseleave", "a", function() { $(document).on("mouseleave", "a", function() {
$(this).tooltip('hide'); $(this).tooltip('dispose');
}); });
// close any tooltip after click, this fixes the problem that sometimes tooltips remained on the screen // close any tooltip after click, this fixes the problem that sometimes tooltips remained on the screen

View File

@@ -5,9 +5,7 @@
#relation-map-wrapper { #relation-map-wrapper {
position: relative; position: relative;
overflow: hidden !important; height: 100%;
height: 4000px; /* we need to set fixed dimentions. This number is probably enough to cover any screen */
width: 4000px;
outline: none; /* remove dotted outline on click */ outline: none; /* remove dotted outline on click */
} }

View File

@@ -76,6 +76,7 @@ body {
position: relative; position: relative;
overflow: auto; overflow: auto;
flex-basis: content; flex-basis: content;
height: 100%;
} }
.note-detail-component { .note-detail-component {

View File

@@ -95,7 +95,7 @@ async function updateNoteAttributes(req) {
attributeEntity.isInheritable = attribute.isInheritable; attributeEntity.isInheritable = attribute.isInheritable;
attributeEntity.isDeleted = attribute.isDeleted; attributeEntity.isDeleted = attribute.isDeleted;
if (attributeEntity.type === 'relation' && !attributeEntity.value.trim()) { if (attributeEntity.type === 'relation' && !attribute.value.trim()) {
// relation should never have empty target // relation should never have empty target
attributeEntity.isDeleted = true; attributeEntity.isDeleted = true;
} }

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 = 116; const APP_DB_VERSION = 118;
const SYNC_VERSION = 2; const SYNC_VERSION = 2;
module.exports = { module.exports = {

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-11-16T23:30:52+01:00", buildRevision: "90eb1b53fbe915c4658617772aea4347a107a722" }; module.exports = { buildDate:"2018-11-19T00:06:44+01:00", buildRevision: "ad6cb6ba347f0396cbf79b76ab62ee3e4a4e8566" };

View File

@@ -275,8 +275,9 @@ async function runAllChecks() {
LEFT JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId AND sourceNote.isDeleted = 0 LEFT JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId AND sourceNote.isDeleted = 0
LEFT JOIN notes AS targetNote ON targetNote.noteId = links.noteId AND targetNote.isDeleted = 0 LEFT JOIN notes AS targetNote ON targetNote.noteId = links.noteId AND targetNote.isDeleted = 0
WHERE WHERE
sourceNote.noteId IS NULL links.isDeleted = 0
OR targetNote.noteId IS NULL`, AND (sourceNote.noteId IS NULL
OR targetNote.noteId IS NULL)`,
"Link to source/target note link is broken", errorList); "Link to source/target note link is broken", errorList);
await runSyncRowChecks("notes", "noteId", errorList); await runSyncRowChecks("notes", "noteId", errorList);

View File

@@ -79,7 +79,11 @@ async function processMirrorRelations(entityName, entity, handler) {
eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => { eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => {
await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => { await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => {
// we need to make sure that also target's mirror attribute exists and if note, then create it // we need to make sure that also target's mirror attribute exists and if note, then create it
if (!await targetNote.hasRelation(definition.mirrorRelation)) { // mirror attribute has to target our note as well
const hasMirrorAttribute = (await targetNote.getRelations(definition.mirrorRelation))
.some(attr => attr.value === note.noteId);
if (!hasMirrorAttribute) {
await new Attribute({ await new Attribute({
noteId: targetNote.noteId, noteId: targetNote.noteId,
type: 'relation', type: 'relation',
@@ -97,13 +101,18 @@ eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity
await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => { await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => {
// if one mirror attribute is deleted then the other should be deleted as well // if one mirror attribute is deleted then the other should be deleted as well
const relations = await targetNote.getRelations(definition.mirrorRelation); const relations = await targetNote.getRelations(definition.mirrorRelation);
let deletedSomething = false;
for (const relation of relations) { for (const relation of relations) {
relation.isDeleted = true; if (relation.value === note.noteId) {
await relation.save(); relation.isDeleted = true;
await relation.save();
deletedSomething = true;
}
} }
if (relations.length > 0) { if (deletedSomething) {
targetNote.invalidateAttributeCache(); targetNote.invalidateAttributeCache();
} }
}); });

View File

@@ -31,6 +31,11 @@ async function importTar(fileBuffer, parentNote) {
return ""; return "";
} }
// we allow references to root and they don't need translation
if (origNoteId === 'root') {
return origNoteId;
}
if (!ctx.noteIdMap[origNoteId]) { if (!ctx.noteIdMap[origNoteId]) {
ctx.noteIdMap[origNoteId] = utils.newEntityId(); ctx.noteIdMap[origNoteId] = utils.newEntityId();
} }

View File

@@ -4,6 +4,7 @@ const sqlInit = require('./sql_init');
const optionService = require('./options'); const optionService = require('./options');
const fs = require('fs-extra'); const fs = require('fs-extra');
const log = require('./log'); const log = require('./log');
const utils = require('./utils');
const resourceDir = require('./resource_dir'); const resourceDir = require('./resource_dir');
async function migrate() { async function migrate() {
@@ -72,7 +73,7 @@ async function migrate() {
log.error("error during migration to version " + mig.dbVersion + ": " + e.stack); log.error("error during migration to version " + mig.dbVersion + ": " + e.stack);
log.error("migration failed, crashing hard"); // this is not very user friendly :-/ log.error("migration failed, crashing hard"); // this is not very user friendly :-/
process.exit(1); utils.crash();
} }
finally { finally {
// make sure foreign keys are enabled even if migration script disables them // make sure foreign keys are enabled even if migration script disables them

View File

@@ -68,6 +68,10 @@ async function createNewNote(parentNoteId, noteData) {
noteData.type = noteData.type || parentNote.type; noteData.type = noteData.type || parentNote.type;
noteData.mime = noteData.mime || parentNote.mime; noteData.mime = noteData.mime || parentNote.mime;
if (noteData.type === 'text' || noteData.type === 'code') {
noteData.content = noteData.content || "";
}
const note = await new Note({ const note = await new Note({
noteId: noteData.noteId, // optionally can force specific noteId noteId: noteData.noteId, // optionally can force specific noteId
title: noteData.title, title: noteData.title,
@@ -374,19 +378,12 @@ async function deleteNote(branch) {
async function cleanupDeletedNotes() { async function cleanupDeletedNotes() {
const cutoffDate = new Date(new Date().getTime() - 48 * 3600 * 1000); const cutoffDate = new Date(new Date().getTime() - 48 * 3600 * 1000);
const notesForCleanup = await repository.getEntities("SELECT * FROM notes WHERE isDeleted = 1 AND content != '' AND dateModified <= ?", [dateUtils.dateStr(cutoffDate)]); // it's better to not use repository for this because it will complain about saving protected notes
// out of protected session
for (const note of notesForCleanup) { await sql.execute("UPDATE notes SET content = NULL WHERE isDeleted = 1 AND content IS NOT NULL AND dateModified <= ?", [dateUtils.dateStr(cutoffDate)]);
note.content = null;
await note.save();
}
const notesRevisionsForCleanup = await repository.getEntities("SELECT note_revisions.* FROM notes JOIN note_revisions USING(noteId) WHERE notes.isDeleted = 1 AND note_revisions.content != '' AND notes.dateModified <= ?", [dateUtils.dateStr(cutoffDate)]); await sql.execute("UPDATE note_revisions SET content = NULL WHERE note_revisions.content IS NOT NULL AND noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1 AND notes.dateModified <= ?)", [dateUtils.dateStr(cutoffDate)]);
for (const noteRevision of notesRevisionsForCleanup) {
noteRevision.content = null;
await noteRevision.save();
}
} }
// first cleanup kickoff 5 minutes after startup // first cleanup kickoff 5 minutes after startup

View File

@@ -118,6 +118,15 @@ function escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
} }
function crash() {
if (isElectron()) {
require('electron').app.exit(1);
}
else {
process.exit(1);
}
}
module.exports = { module.exports = {
randomSecureToken, randomSecureToken,
randomString, randomString,
@@ -137,5 +146,6 @@ module.exports = {
stripTags, stripTags,
intersection, intersection,
union, union,
escapeRegExp escapeRegExp,
crash
}; };

View File

@@ -10,10 +10,6 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group">
<label for="prompt-dialog-answer" id="prompt-dialog-question"></label>
<input type="text" class="form-control" id="prompt-dialog-answer" placeholder="">
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-primary btn-sm" id="prompt-dialog-ok-button">OK <kbd>enter</kbd></button> <button class="btn btn-primary btn-sm" id="prompt-dialog-ok-button">OK <kbd>enter</kbd></button>