Merge branch 'master' into m43

This commit is contained in:
zadam
2020-05-09 20:17:26 +02:00
49 changed files with 485 additions and 1090 deletions

View File

@@ -37,15 +37,18 @@ export async function showNoteRevisionsDialog(noteId, noteRevisionId) {
async function loadNoteRevisions(noteId, noteRevId) {
$list.empty();
$content.empty();
$titleButtons.empty();
note = appContext.tabManager.getActiveTabNote();
revisionItems = await server.get(`notes/${noteId}/revisions`);
for (const item of revisionItems) {
$list.append($('<a class="dropdown-item" tabindex="0">')
.text(item.dateLastEdited.substr(0, 16) + ` (${item.contentLength} bytes)`)
.attr('data-note-revision-id', item.noteRevisionId))
.attr('title', 'This revision was last edited on ' + item.dateLastEdited);
$list.append(
$('<a class="dropdown-item" tabindex="0">')
.text(item.dateLastEdited.substr(0, 16) + ` (${item.contentLength} bytes)`)
.attr('data-note-revision-id', item.noteRevisionId)
.attr('title', 'This revision was last edited on ' + item.dateLastEdited)
);
}
$listDropdown.dropdown('show');
@@ -60,6 +63,8 @@ async function loadNoteRevisions(noteId, noteRevId) {
$title.text("No revisions for this note yet...");
noteRevisionId = null;
}
$eraseAllRevisionsButton.toggle(revisionItems.length > 0);
}
$dialog.on('shown.bs.modal', () => {
@@ -77,6 +82,21 @@ async function setContentPane() {
$title.html(revisionItem.title);
const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>');
$restoreRevisionButton.on('click', async () => {
const confirmDialog = await import('../dialogs/confirm.js');
const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.';
if (await confirmDialog.confirm(text)) {
await server.put(`notes/${revisionItem.noteId}/restore-revision/${revisionItem.noteRevisionId}`);
$dialog.modal('hide');
toastService.showMessage('Note revision has been restored.');
}
});
const $eraseRevisionButton = $('<button class="btn btn-sm" type="button">Delete this revision</button>');
$eraseRevisionButton.on('click', async () => {
@@ -93,6 +113,8 @@ async function setContentPane() {
});
$titleButtons
.append($restoreRevisionButton)
.append(' &nbsp; ')
.append($eraseRevisionButton)
.append(' &nbsp; ');

View File

@@ -68,7 +68,7 @@ export async function showDialog(ancestorNoteId) {
}
else {
const note = await treeCache.getNote(change.noteId);
const notePath = await treeService.getSomeNotePath(note);
const notePath = treeService.getSomeNotePath(note);
if (notePath) {
$noteLink = await linkService.createNoteLink(notePath, {

View File

@@ -1,138 +0,0 @@
import libraryLoader from '../services/library_loader.js';
import server from '../services/server.js';
import toastService from "../services/toast.js";
import utils from "../services/utils.js";
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $tableSchemas = $("#sql-console-table-schemas");
const $resultContainer = $("#result-container");
let codeEditor;
$dialog.on("shown.bs.modal", e => initEditor());
export async function showDialog() {
await showTableSchemas();
utils.openDialog($dialog);
}
async function initEditor() {
if (!codeEditor) {
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
// removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
delete CodeMirror.keyMap.basic["Esc"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.setValue(`SELECT title, isProtected, type, mime FROM notes WHERE noteId = 'root';
---
SELECT noteId, parentNoteId, notePosition, prefix FROM branches WHERE branchId = 'root';`);
}
codeEditor.focus();
}
async function execute() {
// 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", {
query: sqlQuery
});
if (!result.success) {
toastService.showError(result.error);
return;
}
else {
toastService.showMessage("Query was executed successfully.");
}
const results = result.results;
$resultContainer.empty();
for (const rows of results) {
if (rows.length === 0) {
continue;
}
const $table = $('<table class="table table-striped">');
$resultContainer.append($table);
const result = rows[0];
const $row = $("<tr>");
for (const key in result) {
$row.append($("<th>").html(key));
}
$table.append($row);
for (const result of rows) {
const $row = $("<tr>");
for (const key in result) {
$row.append($("<td>").html(result[key]));
}
$table.append($row);
}
}
}
async function showTableSchemas() {
const tables = await server.get('sql/schema');
$tableSchemas.empty();
for (const table of tables) {
const $tableLink = $('<button class="btn">').text(table.name);
const $columns = $("<ul>");
for (const column of table.columns) {
$columns.append(
$("<li>")
.append($("<span>").text(column.name))
.append($("<span>").text(column.type))
);
}
$tableSchemas.append($tableLink).append(" ");
$tableLink
.tooltip({
html: true,
placement: 'bottom',
boundary: 'window',
title: $columns[0].outerHTML
})
.on('click', () => codeEditor.setValue("SELECT * FROM " + table.name + " LIMIT 100"));
}
}
utils.bindElShortcut($query, 'ctrl+return', execute);
$executeButton.on('click', execute);

View File

@@ -45,10 +45,12 @@ class AppContext extends Component {
$("body").append($renderedWidget);
$renderedWidget.on('click', "[data-trigger-command]", e => {
const commandName = $(e.target).attr('data-trigger-command');
$renderedWidget.on('click', "[data-trigger-command]", function() {
const commandName = $(this).attr('data-trigger-command');
const $component = $(this).closest(".component");
const component = $component.prop("component");
this.triggerCommand(commandName);
component.triggerCommand(commandName, {$el: $(this)});
});
this.tabManager = new TabManager();
@@ -92,6 +94,8 @@ class AppContext extends Component {
}
}
// this might hint at error but sometimes this is used by components which are at different places
// in the component tree to communicate with each other
console.debug(`Unhandled command ${name}, converting to event.`);
return this.triggerEvent(name, data);

View File

@@ -27,9 +27,17 @@ async function getYearNote(year) {
return await treeCache.getNote(note.noteId);
}
/** @return {NoteShort} */
async function createSqlConsole() {
const note = await server.post('sql-console');
return await treeCache.getNote(note.noteId);
}
export default {
getTodayNote,
getDateNote,
getMonthNote,
getYearNote
getYearNote,
createSqlConsole
}

View File

@@ -1,5 +1,7 @@
import Component from "../widgets/component.js";
import appContext from "./app_context.js";
import dateNoteService from "../services/date_notes.js";
import noteCreateService from "../services/note_create.js";
export default class DialogCommandExecutor extends Component {
jumpToNoteCommand() {
@@ -54,18 +56,22 @@ export default class DialogCommandExecutor extends Component {
}
showOptionsCommand() {
import("../dialogs/options.js").then(d => d.showDialog())
import("../dialogs/options.js").then(d => d.showDialog());
}
showHelpCommand() {
import("../dialogs/help.js").then(d => d.showDialog())
import("../dialogs/help.js").then(d => d.showDialog());
}
showSQLConsoleCommand() {
import("../dialogs/sql_console.js").then(d => d.showDialog())
async showSQLConsoleCommand() {
const sqlConsoleNote = await dateNoteService.createSqlConsole();
const tabContext = await appContext.tabManager.openTabWithNote(sqlConsoleNote.noteId, true);
appContext.triggerCommand('focusOnDetail', {tabId: tabContext.tabId});
}
showBackendLogCommand() {
import("../dialogs/backend_log.js").then(d => d.showDialog())
import("../dialogs/backend_log.js").then(d => d.showDialog());
}
}

View File

@@ -7,6 +7,7 @@ import Component from "../widgets/component.js";
import toastService from "./toast.js";
import noteCreateService from "./note_create.js";
import ws from "./ws.js";
import bundleService from "./bundle.js";
export default class Entrypoints extends Component {
constructor() {
@@ -199,4 +200,23 @@ export default class Entrypoints extends Component {
async openNewWindowCommand() {
this.openInWindowCommand({notePath: ''});
}
async runActiveNoteCommand() {
const note = appContext.tabManager.getActiveTabNote();
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
if (!note || note.type !== 'code') {
return;
}
if (note.mime.endsWith("env=frontend")) {
await bundleService.getAndExecuteBundle(note.noteId);
}
if (note.mime.endsWith("env=backend")) {
await server.post('script/run/' + note.noteId);
}
toastService.showMessage("Note executed");
}
}

View File

@@ -93,8 +93,8 @@ function updateDisplayedShortcuts($container) {
}
});
$container.find('button[data-command],a.icon-action[data-command],.kb-in-title').each(async (i, el) => {
const actionName = $(el).attr('data-command');
$container.find('[data-trigger-command]').each(async (i, el) => {
const actionName = $(el).attr('data-trigger-command');
const action = await getAction(actionName, true);
if (action) {

View File

@@ -38,12 +38,6 @@ export default class MainTreeExecutors extends Component {
isProtected: activeNote.isProtected,
saveSelection: false
});
await ws.waitForMaxKnownSyncId();
appContext.tabManager.getActiveTabContext().setNote(note.noteId);
appContext.triggerCommand('focusAndSelectTitle');
}
async createNoteAfterCommand() {
@@ -55,17 +49,11 @@ export default class MainTreeExecutors extends Component {
return;
}
const {note} = await noteCreateService.createNote(parentNoteId, {
await noteCreateService.createNote(parentNoteId, {
target: 'after',
targetBranchId: node.data.branchId,
isProtected: isProtected,
saveSelection: false
});
await ws.waitForMaxKnownSyncId();
appContext.tabManager.getActiveTabContext().setNote(note.noteId);
appContext.triggerCommand('focusAndSelectTitle');
}
}

View File

@@ -123,6 +123,7 @@ const MIME_TYPES_DICT = [
{ title: "Spreadsheet", mime: "text/x-spreadsheet" },
{ default: true, title: "SQL", mime: "text/x-sql" },
{ title: "SQLite", mime: "text/x-sqlite" },
{ default: true, title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium" },
{ title: "Squirrel", mime: "text/x-squirrel" },
{ title: "Stylus", mime: "text/x-styl" },
{ default: true, title: "Swift", mime: "text/x-swift" },

View File

@@ -16,6 +16,7 @@ async function createNewTopLevelNote() {
async function createNote(parentNoteId, options = {}) {
options = Object.assign({
activate: true,
focus: 'title',
target: 'into'
}, options);
@@ -39,7 +40,8 @@ async function createNote(parentNoteId, options = {}) {
title: newNoteName,
content: options.content || "",
isProtected: options.isProtected,
type: options.type
type: options.type,
mime: options.mime
});
if (options.saveSelection && utils.isCKEditorInitialized()) {
@@ -49,10 +51,23 @@ async function createNote(parentNoteId, options = {}) {
if (options.activate) {
const activeTabContext = appContext.tabManager.getActiveTabContext();
activeTabContext.setNote(note.noteId);
await activeTabContext.setNote(note.noteId);
if (options.focus === 'title') {
appContext.triggerCommand('focusAndSelectTitle');
}
else if (options.focus === 'content') {
appContext.triggerCommand('focusOnDetail', {tabId: this.tabId});
}
}
return {note, branch};
const noteEntity = await treeCache.getNote(note.noteId);
const branchEntity = treeCache.getBranchId(branch.branchId);
return {
note: noteEntity,
branch: branchEntity
};
}
/* If first element is heading, parse it out and use it as a new heading. */
@@ -86,4 +101,4 @@ export default {
createNote,
createNewTopLevelNote,
duplicateNote
};
};

View File

@@ -208,6 +208,8 @@ export default class TabManager extends Component {
notePath: tabContext.notePath // resolved note path
});
}
return tabContext;
}
async activateOrOpenNote(noteId) {

View File

@@ -71,7 +71,7 @@ async function getRunPath(notePath) {
if (parents.length > 0) {
console.debug(utils.now(), "Available parents:", parents);
const someNotePath = await getSomeNotePath(parents[0]);
const someNotePath = getSomeNotePath(parents[0]);
if (someNotePath) { // in case it's root the path may be empty
const pathToRoot = someNotePath.split("/").reverse();
@@ -103,7 +103,7 @@ async function getRunPath(notePath) {
return effectivePath.reverse();
}
async function getSomeNotePath(note) {
function getSomeNotePath(note) {
utils.assertArguments(note);
const path = [];
@@ -286,4 +286,4 @@ export default {
getNotePathTitle,
getHashValueFromAddress,
parseNotePath
};
};

View File

@@ -4,7 +4,6 @@ const TPL = `
<table class="note-info-widget-table">
<style>
.note-info-widget-table {
table-layout: fixed;
width: 100%;
}
@@ -21,23 +20,25 @@ const TPL = `
</style>
<tr>
<th nowrap>Note ID:</th>
<td nowrap colspan="3" class="note-info-note-id"></td>
<th>Note ID:</th>
<td class="note-info-note-id"></td>
</tr>
<tr>
<th nowrap>Created:</th>
<td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-created"></td>
<th>Created:</th>
<td class="note-info-date-created"></td>
</tr>
<tr>
<th nowrap>Modified:</th>
<td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-modified"></td>
<th>Modified:</th>
<td class="note-info-date-modified"></td>
</tr>
<tr>
<th>Type:</th>
<td class="note-info-type"></td>
<th>MIME:</th>
<td class="note-info-mime"></td>
<td>
<span class="note-info-type"></span>,
MIME:
<span class="note-info-mime"></span>
</td>
</tr>
</table>
`;
@@ -60,11 +61,11 @@ export default class NoteInfoWidget extends CollapsibleWidget {
this.$noteId.text(note.noteId);
this.$dateCreated
.text(noteComplement.dateCreated)
.text(noteComplement.dateCreated.substr(0, 16))
.attr("title", noteComplement.dateCreated);
this.$dateModified
.text(noteComplement.dateModified)
.text(noteComplement.dateModified.substr(0, 16))
.attr("title", noteComplement.dateCreated);
this.$type.text(note.type);
@@ -79,4 +80,4 @@ export default class NoteInfoWidget extends CollapsibleWidget {
this.refresh();
}
}
}
}

View File

@@ -18,17 +18,14 @@ const WIDGET_TPL = `
<a data-trigger-command="collapseTree"
title="Collapse note tree"
data-command="collapseTree"
class="icon-action bx bx-layer-minus"></a>
<a data-trigger-command="scrollToActiveNote"
title="Scroll to active note"
data-command="scrollToActiveNote"
title="Scroll to active note"
class="icon-action bx bx-crosshair"></a>
<a data-trigger-command="searchNotes"
title="Search in notes"
data-command="searchNotes"
class="icon-action bx bx-search"></a>
</div>
`;

View File

@@ -186,7 +186,9 @@ export default class NoteDetailWidget extends TabAwareWidget {
const noteComplement = await this.tabContext.getNoteComplement();
if (note.hasLabel('readOnly') ||
(noteComplement.content && noteComplement.content.length > 10000)) {
(noteComplement.content
&& noteComplement.content.length > 10000)
&& !note.hasLabel('autoReadOnlyDisabled')) {
type = 'read-only-text';
}
}
@@ -195,7 +197,9 @@ export default class NoteDetailWidget extends TabAwareWidget {
const noteComplement = await this.tabContext.getNoteComplement();
if (note.hasLabel('readOnly') ||
(noteComplement.content && noteComplement.content.length > 30000)) {
(noteComplement.content
&& noteComplement.content.length > 30000)
&& !note.hasLabel('autoReadOnlyDisabled')) {
type = 'read-only-code';
}
}

View File

@@ -115,7 +115,7 @@ export default class NotePathsWidget extends TabAwareWidget {
const activeNoteParentNoteId = pathSegments[pathSegments.length - 2]; // we know this is not root so there must be a parent
for (const parentNote of this.note.getParentNotes()) {
const parentNotePath = await treeService.getSomeNotePath(parentNote);
const parentNotePath = treeService.getSomeNotePath(parentNote);
// this is to avoid having root notes leading '/'
const notePath = parentNotePath ? (parentNotePath + '/' + this.noteId) : this.noteId;
const isCurrent = activeNoteParentNoteId === parentNote.noteId;
@@ -158,4 +158,4 @@ export default class NotePathsWidget extends TabAwareWidget {
this.refresh();
}
}
}
}

View File

@@ -441,6 +441,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
return "bx bx-note";
}
}
else if (note.type === 'code' && note.mime.startsWith('text/x-sql')) {
return "bx bx-data";
}
else {
return NOTE_TYPE_ICONS[note.type];
}

View File

@@ -3,10 +3,12 @@ import TabAwareWidget from "./tab_aware_widget.js";
const TPL = `
<div style="display: inline-flex;">
<button class="btn btn-sm icon-button bx bx-play-circle render-button"
data-trigger-command="renderActiveNote"
title="Render"></button>
<button class="btn btn-sm icon-button bx bx-play-circle execute-script-button"
title="Execute (Ctrl+Enter)"></button>
data-trigger-command="runActiveNote"
title="Execute"></button>
</div>`;
export default class RunScriptButtonsWidget extends TabAwareWidget {
@@ -21,6 +23,12 @@ export default class RunScriptButtonsWidget extends TabAwareWidget {
refreshWithNote(note) {
this.$renderButton.toggle(note.type === 'render');
this.$executeScriptButton.toggle(note.mime.startsWith('application/javascript'));
this.$executeScriptButton.toggle(note.type === 'code' && note.mime.startsWith('application/javascript'));
}
async entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}
}
}

View File

@@ -29,11 +29,11 @@ const TAB_TPL = `
<div class="note-tab-wrapper">
<div class="note-tab-title"></div>
<div class="note-tab-drag-handle"></div>
<div class="note-tab-close kb-in-title" title="Close tab" data-command="closeActiveTab"><span>×</span></div>
<div class="note-tab-close" title="Close tab" data-trigger-command="closeActiveTab"><span>×</span></div>
</div>
</div>`;
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab kb-in-title" data-command="openNewTab" title="Add new tab">+</div>`;
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab" data-trigger-command="openNewTab" title="Add new tab">+</div>`;
const FILLER_TPL = `<div class="tab-row-filler">
<div class="tab-row-border"></div>
</div>`;
@@ -394,10 +394,13 @@ export default class TabRowWidget extends BasicWidget {
this.setupDraggabilly();
}
setTabCloseEvent($tab) {
$tab.find('.note-tab-close')
.on('click', _ => appContext.tabManager.removeTab($tab.attr('data-tab-id')));
closeActiveTabCommand({$el}) {
const tabId = $el.closest(".note-tab").attr('data-tab-id');
appContext.tabManager.removeTab(tabId);
}
setTabCloseEvent($tab) {
$tab.on('mousedown', e => {
if (e.which === 2) {
appContext.tabManager.removeTab($tab.attr('data-tab-id'));
@@ -558,8 +561,6 @@ export default class TabRowWidget extends BasicWidget {
this.$newTab = $(NEW_TAB_BUTTON_TPL);
this.$tabContainer.append(this.$newTab);
this.$newTab.on('click', _ => this.triggerCommand('openNewTab'));
}
setupFiller() {

View File

@@ -1,9 +1,9 @@
import libraryLoader from "../../services/library_loader.js";
import bundleService from "../../services/bundle.js";
import toastService from "../../services/toast.js";
import server from "../../services/server.js";
import keyboardActionService from "../../services/keyboard_actions.js";
import TypeWidget from "./type_widget.js";
import keyboardActionService from "../../services/keyboard_actions.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
const TPL = `
<div class="note-detail-code note-detail-printable">
@@ -11,27 +11,75 @@ const TPL = `
.note-detail-code {
overflow: auto;
height: 100%;
display: flex;
flex-direction: column;
}
.note-detail-code-editor {
min-height: 500px;
flex-basis: 200px;
min-height: 200px;
flex-grow: 1;
overflow: auto;
}
.sql-console-table-schemas button {
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
line-height: 0.5;
border-radius: 0.2rem;
}
.sql-console-result-wrapper {
flex-grow: 100;
display: flex;
flex-direction: column;
min-height: 0;
}
.sql-console-result-container {
width: 100%;
font-size: smaller;
margin-top: 10px;
flex-grow: 1;
overflow: auto;
min-height: 0;
}
</style>
<div class="sql-console-area">
Tables:
<span class="sql-console-table-schemas"></span>
</div>
<div class="note-detail-code-editor"></div>
<div class="sql-console-area sql-console-result-wrapper">
<div style="text-align: center">
<button class="btn btn-danger sql-console-execute">Execute query <kbd>Ctrl+Enter</kbd></button>
</div>
<div class="sql-console-result-container"></div>
</div>
</div>`;
let TABLE_SCHEMA;
export default class EditableCodeTypeWidget extends TypeWidget {
static getType() { return "editable-code"; }
doRender() {
this.$widget = $(TPL);
this.$editor = this.$widget.find('.note-detail-code-editor');
this.$executeScriptButton = this.$widget.find(".execute-script-button");
this.$sqlConsoleArea = this.$widget.find('.sql-console-area');
this.$sqlConsoleTableSchemas = this.$widget.find('.sql-console-table-schemas');
this.$sqlConsoleExecuteButton = this.$widget.find('.sql-console-execute');
this.$sqlConsoleResultContainer = this.$widget.find('.sql-console-result-container');
keyboardActionService.setElementActionHandler(this.$widget, 'runActiveNote', () => this.executeCurrentNote());
keyboardActionService.setupActionsForElement('code-detail', this.$widget, this);
this.$executeScriptButton.on('click', () => this.executeCurrentNote());
utils.bindElShortcut(this.$editor, 'ctrl+return', () => this.execute());
this.$sqlConsoleExecuteButton.on('click', () => this.execute());
this.initialized = this.initEditor();
@@ -87,9 +135,103 @@ export default class EditableCodeTypeWidget extends TypeWidget {
}
});
const isSqlConsole = note.mime === 'text/x-sqlite;schema=trilium';
this.$sqlConsoleArea.toggle(isSqlConsole);
if (isSqlConsole) {
await this.showTableSchemas();
}
this.show();
}
async showTableSchemas() {
if (!TABLE_SCHEMA) {
TABLE_SCHEMA = await server.get('sql/schema');
}
this.$sqlConsoleTableSchemas.empty();
for (const table of TABLE_SCHEMA) {
const $tableLink = $('<button class="btn">').text(table.name);
const $columns = $("<ul>");
for (const column of table.columns) {
$columns.append(
$("<li>")
.append($("<span>").text(column.name))
.append($("<span>").text(column.type))
);
}
this.$sqlConsoleTableSchemas.append($tableLink).append(" ");
$tableLink
.tooltip({
html: true,
placement: 'bottom',
boundary: 'window',
title: $columns[0].outerHTML
})
.on('click', () => this.codeEditor.setValue("SELECT * FROM " + table.name + " LIMIT 100"));
}
}
async execute() {
// execute the selected text or the whole content if there's no selection
let sqlQuery = this.codeEditor.getSelection();
if (!sqlQuery) {
sqlQuery = this.codeEditor.getValue();
}
const result = await server.post("sql/execute", {
query: sqlQuery
});
if (!result.success) {
toastService.showError(result.error);
return;
}
else {
toastService.showMessage("Query was executed successfully.");
}
const results = result.results;
this.$sqlConsoleResultContainer.empty();
for (const rows of results) {
if (rows.length === 0) {
continue;
}
const $table = $('<table class="table table-striped">');
this.$sqlConsoleResultContainer.append($table);
const result = rows[0];
const $row = $("<tr>");
for (const key in result) {
$row.append($("<th>").html(key));
}
$table.append($row);
for (const result of rows) {
const $row = $("<tr>");
for (const key in result) {
$row.append($("<td>").html(result[key]));
}
$table.append($row);
}
}
}
show() {
this.$widget.show();
@@ -106,26 +248,6 @@ export default class EditableCodeTypeWidget extends TypeWidget {
this.codeEditor.focus();
}
async executeCurrentNote() {
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
if (this.note.type !== 'code') {
return;
}
// make sure note is saved so we load latest changes
await this.spacedUpdate.updateNowIfNecessary();
if (this.note.mime.endsWith("env=frontend")) {
await bundleService.getAndExecuteBundle(this.noteId);
}
if (this.note.mime.endsWith("env=backend")) {
await server.post('script/run/' + this.noteId);
}
toastService.showMessage("Note executed");
}
cleanup() {
if (this.codeEditor) {
this.spacedUpdate.allowUpdateWithoutChange(() => {

View File

@@ -4,6 +4,8 @@ import mimeTypesService from '../../services/mime_types.js';
import utils from "../../services/utils.js";
import keyboardActionService from "../../services/keyboard_actions.js";
import treeCache from "../../services/tree_cache.js";
import treeService from "../../services/tree.js";
import noteCreateService from "../../services/note_create.js";
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
const ENABLE_INSPECTOR = false;
@@ -15,7 +17,7 @@ const mentionSetup = {
feed: queryText => {
return new Promise((res, rej) => {
noteAutocompleteService.autocompleteSource(queryText, rows => {
if (rows.length === 1 && rows[0].title === 'No results') {
if (rows.length === 1 && rows[0].pathTitle === 'No results') {
rows = [];
}
@@ -25,6 +27,17 @@ const mentionSetup = {
row.link = '#' + row.path;
}
if (queryText.trim().length > 0) {
rows = [
{
highlightedTitle: `Create and link note "<strong>${queryText}</strong>"`,
id: 'create',
title: queryText
},
...rows
];
}
res(rows);
});
});
@@ -256,4 +269,20 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.textEditor.model.insertContent(imageElement, this.textEditor.model.document.selection);
} );
}
}
async createNoteForReferenceLink(title) {
const {parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(this.notePath);
const {note} = await noteCreateService.createNote(parentNoteId, {
activate: false,
title: title,
target: 'after',
targetBranchId: await treeCache.getBranchId(parentNoteId, this.noteId),
type: 'text'
});
const notePath = treeService.getSomeNotePath(note);
return notePath;
}
}

View File

@@ -25,9 +25,6 @@ export default class RenderTypeWidget extends TypeWidget {
this.$widget = $(TPL);
this.$noteDetailRenderHelp = this.$widget.find('.note-detail-render-help');
this.$noteDetailRenderContent = this.$widget.find('.note-detail-render-content');
this.$renderButton = this.$widget.find('.render-button');
this.$renderButton.on('click', () => this.refresh());
return this.$widget;
}
@@ -46,4 +43,10 @@ export default class RenderTypeWidget extends TypeWidget {
cleanup() {
this.$noteDetailRenderContent.empty();
}
renderActiveNoteEvent() {
if (this.tabContext.isActive()) {
this.refresh();
}
}
}

View File

@@ -312,14 +312,10 @@ div.ui-tooltip {
.CodeMirror {
font-family: "Liberation Mono", "Lucida Console", monospace;
height: auto;
height: 100%;
background: inherit;
}
.CodeMirror-scroll {
min-height: 500px;
}
.CodeMirror-gutters {
background-color: inherit !important;
border-right: none;
@@ -661,12 +657,7 @@ div[data-notify="container"] {
padding: 2px;
}
#sql-console-table-schemas button {
padding: 0.25rem 0.4rem;
font-size: 0.875rem;
line-height: 0.5;
border-radius: 0.2rem;
}
a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href^="https://"]:not(.no-arrow):after {
font-size: smaller;

View File

@@ -101,7 +101,7 @@ async function updateNoteAttributes(req) {
|| (attribute.type === 'relation' && attribute.value !== attributeEntity.value)) {
if (attribute.type !== 'relation' || !!attribute.value.trim()) {
const newAttribute = attribute.createClone(attribute.type, attribute.name, attribute.value);
const newAttribute = attributeEntity.createClone(attribute.type, attribute.name, attribute.value);
await newAttribute.save();
}
@@ -139,6 +139,7 @@ async function updateNoteAttributes(req) {
}
const note = await repository.getNote(noteId);
note.invalidateAttributeCache();
return await note.getAttributes();
}

View File

@@ -2,6 +2,8 @@
const dateNoteService = require('../../services/date_notes');
const sql = require('../../services/sql');
const dateUtils = require('../../services/date_utils');
const noteService = require('../../services/notes');
async function getDateNote(req) {
return await dateNoteService.getDateNote(req.params.date);
@@ -31,9 +33,28 @@ async function getDateNotesForMonth(req) {
AND attr.value LIKE '${month}%'`);
}
async function createSqlConsole() {
const today = dateUtils.localNowDate();
const todayNote = await dateNoteService.getDateNote(today);
const {note} = await noteService.createNewNote({
parentNoteId: todayNote.noteId,
title: 'SQL Console',
content: "SELECT title, isDeleted, isProtected FROM notes WHERE noteId = ''\n\n\n\n",
type: 'code',
mime: 'text/x-sqlite;schema=trilium'
});
await note.setLabel("sqlConsole", today);
return note;
}
module.exports = {
getDateNote,
getMonthNote,
getYearNote,
getDateNotesForMonth
getDateNotesForMonth,
createSqlConsole
};

View File

@@ -3,6 +3,7 @@
const repository = require('../../services/repository');
const noteCacheService = require('../../services/note_cache');
const protectedSessionService = require('../../services/protected_session');
const noteRevisionService = require('../../services/note_revisions');
const utils = require('../../services/utils');
const path = require('path');
@@ -109,6 +110,20 @@ async function eraseNoteRevision(req) {
}
}
async function restoreNoteRevision(req) {
const noteRevision = await repository.getNoteRevision(req.params.noteRevisionId);
if (noteRevision && !noteRevision.isErased) {
const note = await noteRevision.getNote();
await noteRevisionService.createNoteRevision(note);
note.title = noteRevision.title;
await note.setContent(await noteRevision.getContent());
await note.save();
}
}
async function getEditedNotesOnDate(req) {
const date = utils.sanitizeSql(req.params.date);
@@ -141,5 +156,6 @@ module.exports = {
downloadNoteRevision,
getEditedNotesOnDate,
eraseAllNoteRevisions,
eraseNoteRevision
eraseNoteRevision,
restoreNoteRevision
};

View File

@@ -145,6 +145,7 @@ function register(app) {
apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision);
apiRoute(DELETE, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision);
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateNote);
@@ -180,6 +181,7 @@ function register(app) {
apiRoute(GET, '/api/date-notes/month/:month', dateNotesRoute.getMonthNote);
apiRoute(GET, '/api/date-notes/year/:year', dateNotesRoute.getYearNote);
apiRoute(GET, '/api/date-notes/notes-for-month/:month', dateNotesRoute.getDateNotesForMonth);
apiRoute(POST, '/api/sql-console', dateNotesRoute.createSqlConsole);
route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware], imageRoute.uploadImage, apiResultHandler);

View File

@@ -6,9 +6,8 @@ const utils = require('../services/utils');
async function setupPage(req, res) {
if (await sqlInit.isDbInitialized()) {
const windowService = require('../services/window');
if (utils.isElectron()) {
const windowService = require('../services/window');
await windowService.createMainWindow();
windowService.closeSetupWindow();
}

View File

@@ -19,6 +19,7 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'appTheme' },
{ type: 'label', name: 'hidePromotedAttributes' },
{ type: 'label', name: 'readOnly' },
{ type: 'label', name: 'autoReadOnlyDisabled' },
{ type: 'label', name: 'cssClass' },
{ type: 'label', name: 'iconClass' },
{ type: 'label', name: 'keyboardShortcut' },

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-05-04T21:59:14+02:00", buildRevision: "cafcb67a8a3a1943acac829590b34ff729b57e09" };
module.exports = { buildDate:"2020-05-06T23:24:13+02:00", buildRevision: "54ecd2ee75d1177cedadf9fee10319687feee5f0" };

View File

@@ -293,15 +293,11 @@ async function downloadImages(noteId, content) {
if (!url.includes('api/images/')
// this is and exception for the web clipper's "imageId"
&& (url.length !== 20 || url.toLowerCase().startsWith('http'))) {
if (url in downloadImagePromises) {
// download is already in progress
continue;
}
if (url in imageUrlToNoteIdMapping) {
const imageNote = await repository.getNote(imageUrlToNoteIdMapping[url]);
if (imageNote || imageNote.isDeleted) {
if (!imageNote || imageNote.isDeleted) {
delete imageUrlToNoteIdMapping[url];
}
else {
@@ -322,6 +318,11 @@ async function downloadImages(noteId, content) {
continue;
}
if (url in downloadImagePromises) {
// download is already in progress
continue;
}
// this is done asynchronously, it would be too slow to wait for the download
// given that save can be triggered very often
downloadImagePromises[url] = downloadImage(noteId, url);
@@ -338,28 +339,30 @@ async function downloadImages(noteId, content) {
// are downloaded and the IMG references are not updated. For this occassion we have this code
// which upon the download of all the images will update the note if the links have not been fixed before
const imageNotes = await repository.getNotes(Object.values(imageUrlToNoteIdMapping));
await sql.transactional(async () => {
const imageNotes = await repository.getNotes(Object.values(imageUrlToNoteIdMapping));
const origNote = await repository.getNote(noteId);
const origContent = await origNote.getContent();
let updatedContent = origContent;
const origNote = await repository.getNote(noteId);
const origContent = await origNote.getContent();
let updatedContent = origContent;
for (const url in imageUrlToNoteIdMapping) {
const imageNote = imageNotes.find(note => note.noteId === imageUrlToNoteIdMapping[url]);
for (const url in imageUrlToNoteIdMapping) {
const imageNote = imageNotes.find(note => note.noteId === imageUrlToNoteIdMapping[url]);
if (imageNote && !imageNote.isDeleted) {
updatedContent = replaceUrl(updatedContent, url, imageNote);
if (imageNote && !imageNote.isDeleted) {
updatedContent = replaceUrl(updatedContent, url, imageNote);
}
}
}
// update only if the links have not been already fixed.
if (updatedContent !== origContent) {
await origNote.setContent(updatedContent);
// update only if the links have not been already fixed.
if (updatedContent !== origContent) {
await origNote.setContent(updatedContent);
await scanForLinks(origNote);
await scanForLinks(origNote);
console.log(`Fixed the image links for note ${noteId} to the offline saved.`);
}
console.log(`Fixed the image links for note ${noteId} to the offline saved.`);
}
});
}, 5000);
});

View File

@@ -76,7 +76,7 @@ const defaultOptions = [
{ name: 'imageMaxWidthHeight', value: '1200', isSynced: true },
{ name: 'imageJpegQuality', value: '75', isSynced: true },
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-swift","text/xml","text/x-yaml"]', isSynced: true },
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml"]', isSynced: true },
{ name: 'leftPaneWidth', value: '25', isSynced: false },
{ name: 'leftPaneVisible', value: 'true', isSynced: false },
{ name: 'rightPaneWidth', value: '25', isSynced: false },

View File

@@ -165,6 +165,5 @@ module.exports = {
createMainWindow,
createSetupWindow,
closeSetupWindow,
createExtraWindow,
registerGlobalShortcuts
};

View File

@@ -25,7 +25,6 @@
<%- include('dialogs/options.ejs') %>
<%- include('dialogs/protected_session_password.ejs') %>
<%- include('dialogs/recent_changes.ejs') %>
<%- include('dialogs/sql_console.ejs') %>
<%- include('dialogs/info.ejs') %>
<%- include('dialogs/prompt.ejs') %>
<%- include('dialogs/confirm.ejs') %>

View File

@@ -1,27 +0,0 @@
<div id="sql-console-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">SQL console</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
Tables:
<span id="sql-console-table-schemas"></span>
</div>
<div id="sql-console-query"></div>
<div style="text-align: center">
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
</div>
<div id="result-container" style="width: 100%; font-size: smaller; margin-top: 10px;">
</div>
</div>
</div>
</div>
</div>

View File

@@ -210,7 +210,7 @@
<script src="libraries/knockout.min.js"></script>
<script src="app-dist/setup.js" crossorigin type="module"></script>
<script src="app/setup.js" crossorigin type="module"></script>
<link href="stylesheets/themes.css" rel="stylesheet">
</body>
</html>