Compare commits

...

18 Commits

Author SHA1 Message Date
zadam
e8a9e49e9e release 0.40.4 2020-02-24 22:59:22 +01:00
zadam
fb55cdaea6 release 0.40.4 2020-02-24 22:58:18 +01:00
zadam
b9b2cc8364 make sure $rendered is always jquery object 2020-02-24 22:55:12 +01:00
zadam
8dfdd090f5 use note's css class also in book and included note, closes #879 2020-02-24 22:46:27 +01:00
zadam
fe7705524a fix include note in mobile frontend, closes #878 2020-02-24 22:37:45 +01:00
zadam
2b1b7774f8 make images in text notes rendered responsively in a book, fixes #871 2020-02-19 22:24:31 +01:00
zadam
2d58019d6e allow to setup web version as a sync client 2020-02-19 22:10:40 +01:00
zadam
fe31f08c0d show setup window if DB is not initialized 2020-02-19 22:09:49 +01:00
zadam
ad7a55d305 always autofix note_contents.content = NULL sync issue after note erasion 2020-02-19 19:51:36 +01:00
zadam
4ce4ac9584 release 0.40.3 2020-02-09 10:48:23 +01:00
zadam
88bd65c679 external links are also parsed and label is created for them, closes #851 2020-02-09 10:45:07 +01:00
zadam
9eab3026bb display advanced item in the tree context menu in the middle to avoid being hidden, closes #853 2020-02-09 10:15:35 +01:00
zadam
7abaedbf31 add possibility to change clipper parent to a fixed note instead of day notes, fixes #854 2020-02-09 10:12:02 +01:00
zadam
402718d293 return focus to the previously focused element after closing the dialog, fixes #861 2020-02-09 10:00:13 +01:00
zadam
990a84c202 Merge remote-tracking branch 'origin/master' 2020-02-09 08:38:47 +01:00
zadam
d8e181a828 Merge branch 'stable'
# Conflicts:
#	package.json
#	src/services/build.js
2020-02-09 08:38:29 +01:00
jasontan056
adb8caa8a2 Fix corner case preventing notes from being created before ckeditor is initialized (#849)
* Pass deleteId to deleteBranch in ensureNoteIsAbsentFromParent

* Add checks for whether window.cutToNote is defined.

* check ckEditor initialized.
2020-02-02 09:28:19 +01:00
zadam
9a13edd490 release 0.39.6 2020-01-18 20:52:14 +01:00
44 changed files with 228 additions and 194 deletions

3
.idea/dataSources.xml generated
View File

@@ -6,9 +6,6 @@
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver> <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/../trilium-data/document.db</jdbc-url> <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/../trilium-data/document.db</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
</data-source> </data-source>
</component> </component>
</project> </project>

View File

@@ -26,9 +26,9 @@ app.on('ready', async () => {
await sqlInit.dbConnection; await sqlInit.dbConnection;
// if schema doesn't exist -> setup process // if db is not initialized -> setup process
// if schema exists, then we need to wait until the migration process is finished // if db is initialized, then we need to wait until the migration process is finished
if (await sqlInit.schemaExists()) { if (await sqlInit.isDbInitialized()) {
await sqlInit.dbReady; await sqlInit.dbReady;
await windowService.createMainWindow(); await windowService.createMainWindow();

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.40.1", "version": "0.40.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.40.2", "version": "0.40.4",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {

View File

@@ -811,8 +811,10 @@ class Note extends Entity {
FROM attributes FROM attributes
WHERE noteId = ? AND WHERE noteId = ? AND
isDeleted = 0 AND isDeleted = 0 AND
type = 'relation' AND ((type = 'relation' AND
name IN ('internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink')`, [this.noteId]); name IN ('internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'))
OR
(type = 'label' AND name = 'externalLink'))`, [this.noteId]);
} }
/** /**

View File

@@ -10,8 +10,6 @@ const $buildRevision = $("#build-revision");
const $dataDirectory = $("#data-directory"); const $dataDirectory = $("#data-directory");
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog();
const appInfo = await server.get('app-info'); const appInfo = await server.get('app-info');
$appVersion.text(appInfo.appVersion); $appVersion.text(appInfo.appVersion);
@@ -22,7 +20,5 @@ export async function showDialog() {
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision); $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
$dataDirectory.text(appInfo.dataDirectory); $dataDirectory.text(appInfo.dataDirectory);
glob.activeDialog = $dialog; utils.openDialog($dialog);
$dialog.modal();
} }

View File

@@ -11,13 +11,9 @@ const $linkTitle = $("#link-title");
const $addLinkTitleFormGroup = $("#add-link-title-form-group"); const $addLinkTitleFormGroup = $("#add-link-title-form-group");
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog();
$addLinkTitleFormGroup.toggle(!hasSelection()); $addLinkTitleFormGroup.toggle(!hasSelection());
glob.activeDialog = $dialog; utils.openDialog($dialog);
$dialog.modal();
$autoComplete.val('').trigger('focus'); $autoComplete.val('').trigger('focus');
$linkTitle.val(''); $linkTitle.val('');

View File

@@ -287,8 +287,6 @@ function initKoPlugins() {
} }
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog();
await libraryLoader.requireLibrary(libraryLoader.KNOCKOUT); await libraryLoader.requireLibrary(libraryLoader.KNOCKOUT);
// lazily apply bindings on first use // lazily apply bindings on first use
@@ -300,11 +298,9 @@ export async function showDialog() {
ko.applyBindings(attributesModel, $dialog[0]); ko.applyBindings(attributesModel, $dialog[0]);
} }
glob.activeDialog = $dialog;
await attributesModel.loadAttributes(); await attributesModel.loadAttributes();
$dialog.modal(); utils.openDialog($dialog);
} }
$dialog.on('focus', '.attribute-name', function (e) { $dialog.on('focus', '.attribute-name', function (e) {

View File

@@ -6,11 +6,7 @@ const $backendLogTextArea = $("#backend-log-textarea");
const $refreshBackendLog = $("#refresh-backend-log-button"); const $refreshBackendLog = $("#refresh-backend-log-button");
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog(); utils.openDialog($dialog);
glob.activeDialog = $dialog;
$dialog.modal();
load(); load();
} }

View File

@@ -13,10 +13,6 @@ const $noteTitle = $('#branch-prefix-note-title');
let branchId; let branchId;
export async function showDialog(node) { export async function showDialog(node) {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
branchId = node.data.branchId; branchId = node.data.branchId;
const branch = treeCache.getBranch(branchId); const branch = treeCache.getBranch(branchId);
@@ -30,7 +26,7 @@ export async function showDialog(node) {
return; return;
} }
$dialog.modal(); utils.openDialog($dialog);
$treePrefixInput.val(branch.prefix); $treePrefixInput.val(branch.prefix);

View File

@@ -22,11 +22,7 @@ export async function showDialog(noteIds) {
} }
} }
utils.closeActiveDialog(); utils.openDialog($dialog);
glob.activeDialog = $dialog;
$dialog.modal();
$noteAutoComplete.val('').trigger('focus'); $noteAutoComplete.val('').trigger('focus');

View File

@@ -17,8 +17,6 @@ let taskId = '';
let branchId = null; let branchId = null;
export async function showDialog(node, defaultType) { export async function showDialog(node, defaultType) {
utils.closeActiveDialog();
// each opening of the dialog resets the taskId so we don't associate it with previous exports anymore // each opening of the dialog resets the taskId so we don't associate it with previous exports anymore
taskId = ''; taskId = '';
$exportButton.removeAttr("disabled"); $exportButton.removeAttr("disabled");
@@ -38,9 +36,7 @@ export async function showDialog(node, defaultType) {
$("#opml-v2").prop("checked", true); // setting default $("#opml-v2").prop("checked", true); // setting default
glob.activeDialog = $dialog; utils.openDialog($dialog);
$dialog.modal();
branchId = node.data.branchId; branchId = node.data.branchId;

View File

@@ -3,9 +3,5 @@ import utils from "../services/utils.js";
const $dialog = $("#help-dialog"); const $dialog = $("#help-dialog");
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog(); utils.openDialog($dialog);
glob.activeDialog = $dialog;
$dialog.modal();
} }

View File

@@ -16,8 +16,6 @@ const $explodeArchivesCheckbox = $("#explode-archives-checkbox");
let parentNoteId = null; let parentNoteId = null;
export async function showDialog(node) { export async function showDialog(node) {
utils.closeActiveDialog();
$fileUploadInput.val('').trigger('change'); // to trigger Import button disabling listener below $fileUploadInput.val('').trigger('change'); // to trigger Import button disabling listener below
$safeImportCheckbox.prop("checked", true); $safeImportCheckbox.prop("checked", true);
@@ -26,13 +24,11 @@ export async function showDialog(node) {
$codeImportedAsCodeCheckbox.prop("checked", true); $codeImportedAsCodeCheckbox.prop("checked", true);
$explodeArchivesCheckbox.prop("checked", true); $explodeArchivesCheckbox.prop("checked", true);
glob.activeDialog = $dialog;
parentNoteId = node.data.noteId; parentNoteId = node.data.noteId;
$noteTitle.text(await treeUtils.getNoteTitle(parentNoteId)); $noteTitle.text(await treeUtils.getNoteTitle(parentNoteId));
$dialog.modal(); utils.openDialog($dialog);
} }
$form.on('submit', () => { $form.on('submit', () => {

View File

@@ -10,13 +10,9 @@ let callback = null;
export async function showDialog(cb) { export async function showDialog(cb) {
callback = cb; callback = cb;
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$autoComplete.val(''); $autoComplete.val('');
$dialog.modal(); utils.openDialog($dialog);
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true }); noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true });
noteAutocompleteService.showRecentNotes($autoComplete); noteAutocompleteService.showRecentNotes($autoComplete);

View File

@@ -10,13 +10,9 @@ let $originallyFocused; // element focused before the dialog was opened so we ca
export function info(message) { export function info(message) {
$originallyFocused = $(':focus'); $originallyFocused = $(':focus');
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$infoContent.text(message); $infoContent.text(message);
$dialog.modal(); utils.openDialog($dialog);
return new Promise((res, rej) => { resolve = res; }); return new Promise((res, rej) => { resolve = res; });
} }

View File

@@ -8,13 +8,9 @@ const $autoComplete = $("#jump-to-note-autocomplete");
const $showInFullTextButton = $("#show-in-full-text-button"); const $showInFullTextButton = $("#show-in-full-text-button");
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$autoComplete.val(''); $autoComplete.val('');
$dialog.modal(); utils.openDialog($dialog);
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true }) noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true })
.on('autocomplete:selected', function(event, suggestion, dataset) { .on('autocomplete:selected', function(event, suggestion, dataset) {

View File

@@ -16,10 +16,6 @@ function getOptions() {
} }
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
// set default settings // set default settings
$maxNotesInput.val(20); $maxNotesInput.val(20);
@@ -27,7 +23,7 @@ export async function showDialog() {
$linkMapContainer.empty(); $linkMapContainer.empty();
$dialog.modal(); utils.openDialog($dialog);
} }
$dialog.on('shown.bs.modal', () => { $dialog.on('shown.bs.modal', () => {

View File

@@ -37,9 +37,7 @@ export async function importMarkdownInline() {
convertMarkdownToHtml(text); convertMarkdownToHtml(text);
} }
else { else {
glob.activeDialog = $dialog; utils.openDialog($dialog);
$dialog.modal();
} }
} }

View File

@@ -1,7 +1,5 @@
import noteAutocompleteService from "../services/note_autocomplete.js"; import noteAutocompleteService from "../services/note_autocomplete.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import cloningService from "../services/cloning.js";
import treeUtils from "../services/tree_utils.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import treeCache from "../services/tree_cache.js"; import treeCache from "../services/tree_cache.js";
import treeChangesService from "../services/branches.js"; import treeChangesService from "../services/branches.js";
@@ -18,11 +16,7 @@ let movedNodes;
export async function showDialog(nodes) { export async function showDialog(nodes) {
movedNodes = nodes; movedNodes = nodes;
utils.closeActiveDialog(); utils.openDialog($dialog);
glob.activeDialog = $dialog;
$dialog.modal();
$noteAutoComplete.val('').trigger('focus'); $noteAutoComplete.val('').trigger('focus');

View File

@@ -10,11 +10,7 @@ const $mime = $("#note-info-mime");
const $okButton = $("#note-info-ok-button"); const $okButton = $("#note-info-ok-button");
export function showDialog() { export function showDialog() {
utils.closeActiveDialog(); utils.openDialog($dialog);
glob.activeDialog = $dialog;
$dialog.modal();
const activeNote = noteDetailService.getActiveTabNote(); const activeNote = noteDetailService.getActiveTabNote();

View File

@@ -29,11 +29,7 @@ export async function showCurrentNoteRevisions() {
} }
export async function showNoteRevisionsDialog(noteId, noteRevisionId) { export async function showNoteRevisionsDialog(noteId, noteRevisionId) {
utils.closeActiveDialog(); utils.openDialog($dialog);
glob.activeDialog = $dialog;
$dialog.modal();
await loadNoteRevisions(noteId, noteRevisionId); await loadNoteRevisions(noteId, noteRevisionId);
} }

View File

@@ -5,11 +5,7 @@ const $dialog = $("#note-source-dialog");
const $noteSource = $("#note-source"); const $noteSource = $("#note-source");
export function showDialog() { export function showDialog() {
utils.closeActiveDialog(); utils.openDialog($dialog);
glob.activeDialog = $dialog;
$dialog.modal();
const noteText = noteDetailService.getActiveTabNote().content; const noteText = noteDetailService.getActiveTabNote().content;

View File

@@ -6,13 +6,9 @@ import utils from "../services/utils.js";
const $dialog = $("#options-dialog"); const $dialog = $("#options-dialog");
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
const options = await server.get('options'); const options = await server.get('options');
$dialog.modal(); utils.openDialog($dialog);
(await Promise.all([ (await Promise.all([
import('./options/advanced.js'), import('./options/advanced.js'),

View File

@@ -12,10 +12,6 @@ let resolve;
let shownCb; let shownCb;
export function ask({ message, defaultValue, shown }) { export function ask({ message, defaultValue, shown }) {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
shownCb = shown; shownCb = shown;
$question = $("<label>") $question = $("<label>")
@@ -34,7 +30,7 @@ export function ask({ message, defaultValue, shown }) {
.append($question) .append($question)
.append($answer)); .append($answer));
$dialog.modal(); utils.openDialog($dialog);
return new Promise((res, rej) => { resolve = res; }); return new Promise((res, rej) => { resolve = res; });
} }

View File

@@ -1,11 +1,12 @@
import protectedSessionService from "../services/protected_session.js"; import protectedSessionService from "../services/protected_session.js";
import utils from "../services/utils.js";
const $dialog = $("#protected-session-password-dialog"); const $dialog = $("#protected-session-password-dialog");
const $passwordForm = $dialog.find(".protected-session-password-form"); const $passwordForm = $dialog.find(".protected-session-password-form");
const $passwordInput = $dialog.find(".protected-session-password"); const $passwordInput = $dialog.find(".protected-session-password");
export function show() { export function show() {
$dialog.modal(); utils.openDialog($dialog);
$passwordInput.trigger('focus'); $passwordInput.trigger('focus');
} }

View File

@@ -8,11 +8,7 @@ const $dialog = $("#recent-changes-dialog");
const $content = $("#recent-changes-content"); const $content = $("#recent-changes-content");
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog(); utils.openDialog($dialog);
glob.activeDialog = $dialog;
$dialog.modal();
const result = await server.get('recent-changes'); const result = await server.get('recent-changes');

View File

@@ -14,13 +14,9 @@ let codeEditor;
$dialog.on("shown.bs.modal", e => initEditor()); $dialog.on("shown.bs.modal", e => initEditor());
export async function showDialog() { export async function showDialog() {
utils.closeActiveDialog();
glob.activeDialog = $dialog;
await showTableSchemas(); await showTableSchemas();
$dialog.modal(); utils.openDialog($dialog);
} }
async function initEditor() { async function initEditor() {

View File

@@ -7,9 +7,26 @@ import contextMenuWidget from "./services/context_menu.js";
import treeChangesService from "./services/branches.js"; import treeChangesService from "./services/branches.js";
import utils from "./services/utils.js"; import utils from "./services/utils.js";
import treeUtils from "./services/tree_utils.js"; import treeUtils from "./services/tree_utils.js";
import linkService from "./services/link.js";
import noteContentRenderer from "./services/note_content_renderer.js";
window.glob.isDesktop = utils.isDesktop; window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile; window.glob.isMobile = utils.isMobile;
window.glob.showAddLinkDialog = () => import('./dialogs/add_link.js').then(d => d.showDialog());
window.glob.showIncludeNoteDialog = cb => import('./dialogs/include_note.js').then(d => d.showDialog(cb));
window.glob.loadIncludedNote = async (noteId, el) => {
const note = await treeCache.getNote(noteId);
if (note) {
$(el).empty().append($("<h3>").append(await linkService.createNoteLink(note.noteId, {
showTooltip: false
})));
const {renderedContent} = await noteContentRenderer.getRenderedContent(note);
$(el).append(renderedContent);
}
};
const $leftPane = $("#left-pane"); const $leftPane = $("#left-pane");
const $tree = $("#tree"); const $tree = $("#tree");

View File

@@ -7,31 +7,20 @@ import protectedSessionHolder from "./protected_session_holder.js";
async function getRenderedContent(note) { async function getRenderedContent(note) {
const type = getRenderingType(note); const type = getRenderingType(note);
let rendered; let $rendered;
if (type === 'text') { if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId); const fullNote = await server.get('notes/' + note.noteId);
const $content = $("<div>").html(fullNote.content); $rendered = $("<div>").html(fullNote.content);
if (utils.isHtmlEmpty(fullNote.content)) {
rendered = "";
}
else {
rendered = $content;
}
} }
else if (type === 'code') { else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId); const fullNote = await server.get('notes/' + note.noteId);
if (fullNote.content.trim() === "") { $rendered = $("<pre>").text(fullNote.content);
rendered = "";
}
rendered = $("<pre>").text(fullNote.content);
} }
else if (type === 'image') { else if (type === 'image') {
rendered = $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`); $rendered = $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`);
} }
else if (type === 'file') { else if (type === 'file') {
function getFileUrl() { function getFileUrl() {
@@ -56,33 +45,35 @@ async function getRenderedContent(note) {
// open doesn't work for protected notes since it works through browser which isn't in protected session // open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected); $openButton.toggle(!note.isProtected);
rendered = $('<div>') $rendered = $('<div>')
.append($downloadButton) .append($downloadButton)
.append(' &nbsp; ') .append(' &nbsp; ')
.append($openButton); .append($openButton);
} }
else if (type === 'render') { else if (type === 'render') {
const $el = $('<div>'); $rendered = $('<div>');
await renderService.render(note, $el, this.ctx); await renderService.render(note, $rendered, this.ctx);
rendered = $el;
} }
else if (type === 'protected-session') { else if (type === 'protected-session') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`) const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession); .on('click', protectedSessionService.enterProtectedSession);
rendered = $("<div>") $rendered = $("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>") .append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>") .append("<br/>")
.append($button); .append($button);
} }
else { else {
rendered = "<em>Content of this note cannot be displayed in the book format</em>"; $rendered = $("<em>Content of this note cannot be displayed in the book format</em>");
}
if (note.cssClass) {
$rendered.addClass(note.cssClass);
} }
return { return {
renderedContent: rendered, renderedContent: $rendered,
type type
}; };
} }

View File

@@ -2,6 +2,7 @@ import treeService from './tree.js';
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import server from './server.js'; import server from './server.js';
import toastService from "./toast.js"; import toastService from "./toast.js";
import utils from "./utils.js";
const $searchInput = $("input[name='search-text']"); const $searchInput = $("input[name='search-text']");
const $resetSearchButton = $("#reset-search-button"); const $resetSearchButton = $("#reset-search-button");
@@ -28,6 +29,8 @@ const helpText = `
</p>`; </p>`;
function showSearch() { function showSearch() {
utils.saveFocusedElement();
$searchBox.slideDown(); $searchBox.slideDown();
$searchBox.tooltip({ $searchBox.tooltip({
@@ -49,6 +52,8 @@ function hideSearch() {
$searchResults.hide(); $searchResults.hide();
$searchBox.slideUp(); $searchBox.slideUp();
utils.focusSavedElement();
} }
function toggleSearch() { function toggleSearch() {

View File

@@ -635,7 +635,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
extraOptions.saveSelection = false; extraOptions.saveSelection = false;
} }
if (extraOptions.saveSelection) { if (extraOptions.saveSelection && utils.isCKEditorInitialized()) {
[extraOptions.title, extraOptions.content] = parseSelectedHtml(window.cutToNote.getSelectedHtml()); [extraOptions.title, extraOptions.content] = parseSelectedHtml(window.cutToNote.getSelectedHtml());
} }
@@ -648,7 +648,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
type: extraOptions.type type: extraOptions.type
}); });
if (extraOptions.saveSelection) { if (extraOptions.saveSelection && utils.isCKEditorInitialized()) {
// we remove the selection only after it was saved to server to make sure we don't lose anything // we remove the selection only after it was saved to server to make sure we don't lose anything
window.cutToNote.removeSelection(); window.cutToNote.removeSelection();
} }
@@ -870,7 +870,7 @@ window.glob.cutIntoNote = () => createNoteInto(true);
keyboardActionService.setGlobalActionHandler('CutIntoNote', () => createNoteInto(true)); keyboardActionService.setGlobalActionHandler('CutIntoNote', () => createNoteInto(true));
keyboardActionService.setGlobalActionHandler('CreateNoteInto', createNoteInto); keyboardActionService.setGlobalActionHandler('CreateNoteInto', () => createNoteInto(true));
keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', scrollToActiveNote); keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', scrollToActiveNote);

View File

@@ -62,6 +62,11 @@ class TreeContextMenu {
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-kb-action="ToggleNoteHoisting"></kbd>', cmd: "unhoist", uiIcon: "arrow-up" }, !isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-kb-action="ToggleNoteHoisting"></kbd>', cmd: "unhoist", uiIcon: "arrow-up" },
{ title: 'Edit branch prefix <kbd data-kb-action="EditBranchPrefix"></kbd>', cmd: "editBranchPrefix", uiIcon: "empty", { title: 'Edit branch prefix <kbd data-kb-action="EditBranchPrefix"></kbd>', cmd: "editBranchPrefix", uiIcon: "empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes}, enabled: isNotRoot && parentNotSearch && noSelectedNotes},
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
{ title: 'Collapse subtree <kbd data-kb-action="CollapseSubtree"></kbd>', cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "recycle", enabled: noSelectedNotes },
{ title: 'Sort alphabetically <kbd data-kb-action="SortChildNotes"></kbd>', cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes && notSearch }
] },
{ title: "----" }, { title: "----" },
{ title: "Protect subtree", cmd: "protectSubtree", uiIcon: "check-shield", enabled: noSelectedNotes }, { title: "Protect subtree", cmd: "protectSubtree", uiIcon: "check-shield", enabled: noSelectedNotes },
{ title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield", enabled: noSelectedNotes }, { title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield", enabled: noSelectedNotes },
@@ -84,12 +89,7 @@ class TreeContextMenu {
{ title: "Export", cmd: "export", uiIcon: "empty", { title: "Export", cmd: "export", uiIcon: "empty",
enabled: notSearch && noSelectedNotes }, enabled: notSearch && noSelectedNotes },
{ title: "Import into note", cmd: "importIntoNote", uiIcon: "empty", { title: "Import into note", cmd: "importIntoNote", uiIcon: "empty",
enabled: notSearch && noSelectedNotes }, enabled: notSearch && noSelectedNotes }
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
{ title: 'Collapse subtree <kbd data-kb-action="CollapseSubtree"></kbd>', cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "recycle", enabled: noSelectedNotes },
{ title: 'Sort alphabetically <kbd data-kb-action="SortChildNotes"></kbd>', cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes && notSearch }
] },
].filter(row => row !== null); ].filter(row => row !== null);
} }

View File

@@ -209,9 +209,50 @@ function getMimeTypeClass(mime) {
function closeActiveDialog() { function closeActiveDialog() {
if (glob.activeDialog) { if (glob.activeDialog) {
glob.activeDialog.modal('hide'); glob.activeDialog.modal('hide');
glob.activeDialog = null;
} }
} }
let $lastFocusedElement = null;
function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
if ($lastFocusedElement.hasClass("ck")) {
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
import("./note_detail.js").then(noteDetail => {
noteDetail.default.getActiveEditor().editing.view.focus();
});
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}
function openDialog($dialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
saveFocusedElement();
$dialog.modal();
$dialog.on('hidden.bs.modal', () => {
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
}
function isHtmlEmpty(html) { function isHtmlEmpty(html) {
html = html.toLowerCase(); html = html.toLowerCase();
@@ -248,6 +289,10 @@ function copySelectionToClipboard() {
} }
} }
function isCKEditorInitialized() {
return !!(window && window.cutToNote);
}
export default { export default {
reloadApp, reloadApp,
parseDate, parseDate,
@@ -277,9 +322,13 @@ export default {
getNoteTypeClass, getNoteTypeClass,
getMimeTypeClass, getMimeTypeClass,
closeActiveDialog, closeActiveDialog,
openDialog,
saveFocusedElement,
focusSavedElement,
isHtmlEmpty, isHtmlEmpty,
clearBrowserCache, clearBrowserCache,
getUrlForDownload, getUrlForDownload,
normalizeShortcut, normalizeShortcut,
copySelectionToClipboard copySelectionToClipboard,
isCKEditorInitialized
}; };

View File

@@ -71,7 +71,19 @@ class AttributesWidget extends StandardWidget {
async renderAttributes(attributes, $container) { async renderAttributes(attributes, $container) {
for (const attribute of attributes) { for (const attribute of attributes) {
if (attribute.type === 'label') { if (attribute.type === 'label') {
$container.append(utils.formatLabel(attribute) + " "); if (attribute.name === 'externalLink') {
$container.append('@' + attribute.name + "=");
$container.append(
$('<a>')
.text(attribute.value)
.attr('href', attribute.value)
.addClass('external')
);
$container.append(" ");
}
else {
$container.append(utils.formatLabel(attribute) + " ");
}
} else if (attribute.type === 'relation') { } else if (attribute.type === 'relation') {
if (attribute.value) { if (attribute.value) {
$container.append('@' + attribute.name + "="); $container.append('@' + attribute.name + "=");

View File

@@ -897,7 +897,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
text-align: center; text-align: center;
} }
.note-book-card.type-image .note-book-content img { .note-book-card.type-image .note-book-content img, .note-book-card.type-text .note-book-content img {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
} }

View File

@@ -1,5 +1,6 @@
"use strict"; "use strict";
const attributeService = require("../../services/attributes");
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const dateNoteService = require('../../services/date_notes'); const dateNoteService = require('../../services/date_notes');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
@@ -23,16 +24,26 @@ async function findClippingNote(todayNote, pageUrl) {
return null; return null;
} }
async function getClipperInboxNote() {
let clipperInbox = await attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) {
clipperInbox = await dateNoteService.getDateNote(dateUtils.localNowDate());
}
return clipperInbox;
}
async function addClipping(req) { async function addClipping(req) {
const {title, content, pageUrl, images} = req.body; const {title, content, pageUrl, images} = req.body;
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate()); const clipperInbox = await getClipperInboxNote();
let clippingNote = await findClippingNote(todayNote, pageUrl); let clippingNote = await findClippingNote(clipperInbox, pageUrl);
if (!clippingNote) { if (!clippingNote) {
clippingNote = (await noteService.createNewNote({ clippingNote = (await noteService.createNewNote({
parentNoteId: todayNote.noteId, parentNoteId: clipperInbox.noteId,
title: title, title: title,
content: '', content: '',
type: 'text' type: 'text'
@@ -54,10 +65,10 @@ async function addClipping(req) {
async function createNote(req) { async function createNote(req) {
const {title, content, pageUrl, images, clipType} = req.body; const {title, content, pageUrl, images, clipType} = req.body;
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate()); const clipperInbox = await getClipperInboxNote();
const {note} = await noteService.createNewNote({ const {note} = await noteService.createNewNote({
parentNoteId: todayNote.noteId, parentNoteId: clipperInbox.noteId,
title, title,
content, content,
type: 'text' type: 'text'

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-02-01T10:17:51+01:00", buildRevision: "0f25c8a95f381d99b66735b9c0af3e319edb72ed" }; module.exports = { buildDate:"2020-02-24T22:59:22+01:00", buildRevision: "fb55cdaea6b1367129e11118b8b6fd2eadebad5f" };

View File

@@ -323,14 +323,25 @@ class ConsistencyChecks {
WHERE isErased = 1 WHERE isErased = 1
AND content IS NOT NULL`, AND content IS NOT NULL`,
async ({noteId}) => { async ({noteId}) => {
if (this.autoFix) {
await sql.execute(`UPDATE note_contents SET content = NULL WHERE noteId = ?`, [noteId]);
logFix(`Note ${noteId} content has been set to null since the note is erased`); // we always fix this issue because there does not seem to be a good way to prevent it.
} // Scenario in which this can happen:
else { // 1. user on instance A deletes the note (sync for notes is created, but not for note_contents) and is later erased
logError(`Note ${noteId} content is not null even though the note is erased`); // 2. instance B gets synced from instance A, note is updated because of sync row for notes,
} // but note_contents is not because erasion does not create sync rows
// 3. therefore note.isErased = true, but note_contents.content remains not updated and not erased.
//
// Considered solutions:
// - don't sync erased notes - this might prevent syncing also of the isDeleted flag and note would continue
// to exist on the other instance
// - create sync rows for erased event - this would be a problem for undeletion since erasion might happen
// on one instance after undelete and thus would win even though there's no user action behind it
//
// So instead we just fix such cases afterwards here.
await sql.execute(`UPDATE note_contents SET content = NULL WHERE noteId = ?`, [noteId]);
logFix(`Note ${noteId} content has been set to null since the note is erased`);
}); });
await this.findAndFixIssues(` await this.findAndFixIssues(`
@@ -547,23 +558,23 @@ class ConsistencyChecks {
}); });
await this.findAndFixIssues(` await this.findAndFixIssues(`
SELECT SELECT
id, entityId id, entityId
FROM FROM
sync sync
LEFT JOIN ${entityName} ON entityId = ${key} LEFT JOIN ${entityName} ON entityId = ${key}
WHERE WHERE
sync.entityName = '${entityName}' sync.entityName = '${entityName}'
AND ${key} IS NULL`, AND ${key} IS NULL`,
async ({id, entityId}) => { async ({id, entityId}) => {
if (this.autoFix) { if (this.autoFix) {
await sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]); await sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`); logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
} else { } else {
logError(`Unrecognized sync record id=${id}, entityName=${entityName}, entityId=${entityId}`); logError(`Unrecognized sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
} }
}); });
} }
async findSyncRowsIssues() { async findSyncRowsIssues() {

View File

@@ -152,6 +152,11 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
continue; continue;
} }
if (attr.type === 'label' && attr.name === 'externalLink') {
// also created automatically
continue;
}
if (attr.type === 'relation') { if (attr.type === 'relation') {
attr.value = getNewNoteId(attr.value); attr.value = getNewNoteId(attr.value);
} }

View File

@@ -242,6 +242,20 @@ function findInternalLinks(content, foundLinks) {
return content.replace(/href="[^"]*#root/g, 'href="#root'); return content.replace(/href="[^"]*#root/g, 'href="#root');
} }
function findExternalLinks(content, foundLinks) {
const re = /href="([a-zA-Z]+:\/\/[^"]*)"/g;
let match;
while (match = re.exec(content)) {
foundLinks.push({
name: 'externalLink',
value: match[1]
});
}
return content;
}
function findIncludeNoteLinks(content, foundLinks) { function findIncludeNoteLinks(content, foundLinks) {
const re = /<section class="include-note" data-note-id="([a-zA-Z0-9]+)">/g; const re = /<section class="include-note" data-note-id="([a-zA-Z0-9]+)">/g;
let match; let match;
@@ -281,6 +295,7 @@ async function saveLinks(note, content) {
if (note.type === 'text') { if (note.type === 'text') {
content = findImageLinks(content, foundLinks); content = findImageLinks(content, foundLinks);
content = findInternalLinks(content, foundLinks); content = findInternalLinks(content, foundLinks);
content = findExternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks); content = findIncludeNoteLinks(content, foundLinks);
} }
else if (note.type === 'relation-map') { else if (note.type === 'relation-map') {
@@ -293,9 +308,11 @@ async function saveLinks(note, content) {
const existingLinks = await note.getLinks(); const existingLinks = await note.getLinks();
for (const foundLink of foundLinks) { for (const foundLink of foundLinks) {
const targetNote = await repository.getNote(foundLink.value); if (foundLink.name !== 'externalLink') {
if (!targetNote || targetNote.isDeleted) { const targetNote = await repository.getNote(foundLink.value);
continue; if (!targetNote || targetNote.isDeleted) {
continue;
}
} }
const existingLink = existingLinks.find(existingLink => const existingLink = existingLinks.find(existingLink =>
@@ -305,7 +322,7 @@ async function saveLinks(note, content) {
if (!existingLink) { if (!existingLink) {
await new Attribute({ await new Attribute({
noteId: note.noteId, noteId: note.noteId,
type: 'relation', type: foundLink.name === 'externalLink' ? 'label' : 'relation',
name: foundLink.name, name: foundLink.name,
value: foundLink.value, value: foundLink.value,
}).save(); }).save();
@@ -582,6 +599,7 @@ async function eraseDeletedNotes() {
UPDATE notes UPDATE notes
SET title = '[deleted]', SET title = '[deleted]',
contentLength = 0, contentLength = 0,
isProtected = 0,
isErased = 1 isErased = 1
WHERE noteId IN (???)`, noteIdsToErase); WHERE noteId IN (???)`, noteIdsToErase);

View File

@@ -228,9 +228,10 @@ async function syncFinished(syncContext) {
async function checkContentHash(syncContext) { async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
const lastSyncedPullId = await getLastSyncedPull();
if (await getLastSyncedPull() < resp.maxSyncId) { if (lastSyncedPullId < resp.maxSyncId) {
log.info("There are some outstanding pulls, skipping content check."); log.info(`There are some outstanding pulls (${lastSyncedPullId} vs. ${resp.maxSyncId}), skipping content check.`);
return true; return true;
} }

View File

@@ -73,6 +73,8 @@
<% include details/relation_map.ejs %> <% include details/relation_map.ejs %>
<% include details/protected_session_password.ejs %> <% include details/protected_session_password.ejs %>
<% include details/book.ejs %>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,13 +19,13 @@
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType"> <label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
I'm a new user and I want to create new Trilium document for my notes</label> I'm a new user and I want to create new Trilium document for my notes</label>
</div> </div>
<div class="radio" data-bind="if: instanceType == 'server'" style="margin-bottom: 15px;"> <div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType"> <label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
I have desktop instance already and I want to setup sync with it</label> I have desktop instance already and I want to setup sync with it</label>
</div> </div>
<div class="radio" data-bind="if: instanceType == 'desktop'" style="margin-bottom: 15px;"> <div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType"> <label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
I have server instance up and I want to setup sync with it</label> I have server instance already and I want to setup sync with it</label>
</div> </div>
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button> <button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>