Compare commits

..

1 Commits

Author SHA1 Message Date
zadam
baed93e749 algolia v1 upgrade 2021-12-14 22:44:54 +01:00
106 changed files with 1389 additions and 18730 deletions

View File

@@ -3,6 +3,13 @@ description: Report a bug
title: "(Bug report) "
labels: "Type: Bug"
body:
- type: checkboxes
attributes:
label: Preflight Checklist
description: Please ensure you've completed all of the following.
options:
- label: I have searched the [issue tracker](https://www.github.com/zadam/trilium/issues) for a bug report that matches the one I want to file, without success.
required: true
- type: input
attributes:
label: Trilium Version
@@ -23,7 +30,7 @@ body:
required: true
- type: dropdown
attributes:
label: What is your setup?
label: What is your setup?
description: https://github.com/zadam/trilium/wiki#choose-the-setup
options:
- Local (no sync)
@@ -40,7 +47,17 @@ body:
required: true
- type: textarea
attributes:
label: Description
description: A clear and concise description of the bug and any additional information.
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: A clear description of what actually happens.
validations:
required: true
- type: textarea
attributes:
label: Additional Information
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.

View File

@@ -1,8 +1,15 @@
name: Feature Request
description: Ask for a new feature to be added
description: Report a bug
title: "(Feature request) "
labels: "Type: Enhancement"
body:
- type: checkboxes
attributes:
label: Preflight Checklist
description: Please ensure you've completed all of the following.
options:
- label: I have searched the [issue tracker](https://www.github.com/zadam/trilium/issues) for a feature request that matches the one I want to file, without success.
required: true
- type: textarea
attributes:
label: Describe feature

View File

@@ -1,8 +0,0 @@
UPDATE branches SET branchId = 'hidden' where branchId = (
SELECT branchId FROM branches
WHERE parentNoteId = 'root'
AND noteId = 'hidden'
AND isDeleted = 0
ORDER BY utcDateModified
LIMIT 1
);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

11794
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.49.2-beta",
"version": "0.48.8",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -40,8 +40,7 @@
"electron-dl": "3.3.0",
"electron-find": "1.0.7",
"electron-window-state": "5.0.3",
"@electron/remote": "2.0.1",
"express": "4.17.2",
"express": "4.17.1",
"express-partial-content": "^1.0.2",
"express-rate-limit": "5.5.1",
"express-session": "1.17.2",
@@ -78,12 +77,13 @@
"tmp": "^0.2.1",
"turndown": "7.1.1",
"unescape": "1.0.1",
"ws": "8.4.0",
"ws": "8.3.0",
"yauzl": "2.10.0"
},
"devDependencies": {
"cross-env": "7.0.3",
"electron": "16.0.5",
"electron": "16.0.4",
"@electron/remote": "2.0.1",
"electron-builder": "22.14.5",
"electron-packager": "15.4.0",
"electron-rebuild": "3.2.5",

View File

@@ -42,7 +42,7 @@ class NoteBuilder {
}
child(childNoteBuilder, prefix = "") {
new Branch({
new Branch(becca, {
branchId: id(),
noteId: childNoteBuilder.note.noteId,
parentNoteId: this.note.noteId,

View File

@@ -37,7 +37,7 @@ describe("Parser", () => {
expect(rootExp.constructor.name).toEqual("AndExp");
expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp");
expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp");
expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("BeccaFlatTextExp");
expect(rootExp.subExpressions[1].subExpressions[0].tokens).toEqual(["hello", "hi"]);
});
@@ -55,7 +55,7 @@ describe("Parser", () => {
const subs = rootExp.subExpressions[1].subExpressions;
expect(subs[0].constructor.name).toEqual("NoteFlatTextExp");
expect(subs[0].constructor.name).toEqual("BeccaFlatTextExp");
expect(subs[0].tokens).toEqual(["hello", "hi"]);
expect(subs[1].constructor.name).toEqual("NoteContentProtectedFulltextExp");
@@ -182,7 +182,7 @@ describe("Parser", () => {
expect(firstSub.propertyName).toEqual('isArchived');
expect(secondSub.constructor.name).toEqual("OrExp");
expect(secondSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
expect(secondSub.subExpressions[0].constructor.name).toEqual("BeccaFlatTextExp");
expect(secondSub.subExpressions[0].tokens).toEqual(["hello"]);
expect(thirdSub.constructor.name).toEqual("LabelComparisonExp");

View File

@@ -13,7 +13,7 @@ describe("Search", () => {
becca.reset();
rootNote = new NoteBuilder(new Note({noteId: 'root', title: 'root', type: 'text'}));
new Branch({branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
new Branch(becca, {branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
});
it("simple path match", () => {
@@ -157,21 +157,6 @@ describe("Search", () => {
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("inherited label comparison", () => {
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria"))
.child(note("Czech Republic"))
);
const searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery('austria #country', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("numeric label comparison fallback to string comparison", () => {
// dates should not be coerced into numbers which would then give wrong numbers
@@ -184,7 +169,7 @@ describe("Search", () => {
.label('established', '1993-01-01'))
.child(note("Hungary")
.label('established', '1920-06-04'))
);
);
const searchContext = new SearchContext();
@@ -233,7 +218,7 @@ describe("Search", () => {
test("#month = month", 1);
test("#month = 'MONTH'", 0);
test("note.dateCreated =* month", 2);
test("note.dateCreated =* month", 1);
test("#date = TODAY", 1);
test("#date = today", 1);
@@ -352,11 +337,11 @@ describe("Search", () => {
const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Europe', searchContext);
let searchResults = searchService.findResultsWithQuery('#city AND note.getAncestors().title = Europe', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Asia', searchContext);
searchResults = searchService.findResultsWithQuery('#city AND note.getAncestors().title = Asia', searchContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Taipei")).toBeTruthy();
});

View File

@@ -58,10 +58,7 @@ class Branch extends AbstractEntity {
}
init() {
if (this.branchId) {
this.becca.branches[this.branchId] = this;
}
this.becca.branches[this.branchId] = this;
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
if (this.branchId === 'root') {
@@ -87,7 +84,7 @@ class Branch extends AbstractEntity {
/** @returns {Note} */
get childNote() {
if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync/import, create skeleton which will be filled later
// entities can come out of order in sync, create skeleton which will be filled later
this.becca.addNote(this.noteId, new Note({noteId: this.noteId}));
}
@@ -101,7 +98,7 @@ class Branch extends AbstractEntity {
/** @returns {Note} */
get parentNote() {
if (!(this.parentNoteId in this.becca.notes)) {
// entities can come out of order in sync/import, create skeleton which will be filled later
// entities can come out of order in sync, create skeleton which will be filled later
this.becca.addNote(this.parentNoteId, new Note({noteId: this.parentNoteId}));
}

View File

@@ -136,10 +136,7 @@ class Note extends AbstractEntity {
return this.parentBranches;
}
/**
* @returns {Branch[]}
* @deprecated use getParentBranches() instead
*/
/** @returns {Branch[]} */
getBranches() {
return this.parentBranches;
}
@@ -1114,7 +1111,7 @@ class Note extends AbstractEntity {
const branch = this.becca.getNote(parentNoteId).getParentBranches()[0];
return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
return cloningService.cloneNoteToParent(this.noteId, branch.branchId);
}
decrypt() {

View File

@@ -30,8 +30,6 @@ bundleService.getWidgetBundlesByParent().then(widgetBundles => {
noteTooltipService.setupGlobalTooltip();
noteAutocompleteService.init();
if (utils.isElectron()) {
const electron = utils.dynamicRequire('electron');

View File

@@ -41,7 +41,7 @@ export async function showDialog(widget, text = '') {
$linkTitle.val(noteTitle);
}
noteAutocompleteService.initNoteAutocomplete($autoComplete, {
const ac = noteAutocompleteService.initNoteAutocomplete($autoComplete, {
allowExternalLinks: true,
allowCreatingNotes: true
});
@@ -84,7 +84,7 @@ export async function showDialog(widget, text = '') {
});
if (text && text.trim()) {
noteAutocompleteService.setText($autoComplete, text);
noteAutocompleteService.setText(ac, text);
}
else {
noteAutocompleteService.showRecentNotes($autoComplete);

View File

@@ -48,7 +48,7 @@ async function cloneNotesTo(notePath) {
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
for (const cloneNoteId of clonedNoteIds) {
await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, $clonePrefix.val());
await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val());
const clonedNote = await froca.getNote(cloneNoteId);
const targetNote = await froca.getBranch(targetBranchId).getNote();

View File

@@ -12,7 +12,12 @@ const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
export async function showDialog() {
utils.openDialog($dialog);
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true })
const ac = noteAutocompleteService.initNoteAutocomplete($autoComplete, {
hideGoToSelectedNoteButton: true,
placeholder: "search for note by its name"
});
$autoComplete
// clear any event listener added in previous invocation of this function
.off('autocomplete:noteselected')
.on('autocomplete:noteselected', function(event, suggestion, dataset) {
@@ -28,15 +33,12 @@ export async function showDialog() {
// so we'll keep the content.
// if it's outside of this time limit then we assume it's a completely new search and show recent notes instead.
if (Date.now() - lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) {
noteAutocompleteService.showRecentNotes($autoComplete);
noteAutocompleteService.showRecentNotes(ac, $autoComplete);
}
else {
$autoComplete
// hack, the actual search value is stored in <pre> element next to the search input
// this is important because the search input value is replaced with the suggestion note's title
.autocomplete("val", $autoComplete.next().text())
.trigger('focus')
.trigger('select');
ac.setIsOpen(true);
ac.ext.focus();
ac.ext.select();
}
lastOpenedTs = Date.now();

View File

@@ -1,15 +1,7 @@
import mimeTypesService from "../../services/mime_types.js";
import options from "../../services/options.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
const TPL = `
<h4>Use vim keybindings in CodeNotes (no ex mode)</h4>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="vim-keymap-enabled">
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label>
</div>
<h4>Available MIME types in the dropdown</h4>
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>`;
@@ -18,18 +10,12 @@ export default class CodeNotesOptions {
constructor() {
$("#options-code-notes").html(TPL);
this.$vimKeymapEnabled = $("#vim-keymap-enabled");
this.$vimKeymapEnabled.on('change', () => {
const opts = { 'vimKeymapEnabled': this.$vimKeymapEnabled.is(":checked") ? "true" : "false" };
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
return false;
});
this.$mimeTypes = $("#options-mime-types");
}
async optionsLoaded(options) {
async optionsLoaded() {
this.$mimeTypes.empty();
this.$vimKeymapEnabled.prop("checked", options['vimKeymapEnabled'] === 'true');
let idCtr = 1;
for (const mimeType of await mimeTypesService.getMimeTypes()) {
@@ -59,4 +45,4 @@ export default class CodeNotesOptions {
mimeTypesService.loadMimeTypes();
}
}
}

View File

@@ -47,8 +47,8 @@ const TPL = `
</div>
<div class="form-group">
<label for="image-jpeg-quality">JPEG quality (10 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
<input class="form-control" id="image-jpeg-quality" min="10" max="100" type="number">
<label for="image-jpeg-quality">JPEG quality (0 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
<input class="form-control" id="image-jpeg-quality" min="0" max="100" type="number">
</div>
</div>
</div>

View File

@@ -130,38 +130,18 @@ class NoteShort {
}
}
/**
* @returns {string[]}
*/
getParentBranchIds() {
/** @returns {string[]} */
getBranchIds() {
return Object.values(this.parentToBranch);
}
/**
* @returns {string[]}
* @deprecated use getParentBranchIds() instead
*/
getBranchIds() {
return this.getParentBranchIds();
}
/**
* @returns {Branch[]}
*/
getParentBranches() {
/** @returns {Branch[]} */
getBranches() {
const branchIds = Object.values(this.parentToBranch);
return this.froca.getBranches(branchIds);
}
/**
* @returns {Branch[]}
* @deprecated use getParentBranches() instead
*/
getBranches() {
return this.getParentBranches();
}
/** @returns {boolean} */
hasChildren() {
return this.children.length > 0;
@@ -398,9 +378,6 @@ class NoteShort {
else if (this.noteId === 'root') {
return "bx bx-chevrons-right";
}
if (this.noteId === 'share') {
return "bx bx-share-alt";
}
else if (this.type === 'text') {
if (this.isFolder()) {
return "bx bx-folder";
@@ -643,8 +620,8 @@ class NoteShort {
});
}
hasAncestor(ancestorNoteId, visitedNoteIds = null) {
if (this.noteId === ancestorNoteId) {
hasAncestor(ancestorNote, visitedNoteIds = null) {
if (this.noteId === ancestorNote.noteId) {
return true;
}
@@ -658,13 +635,13 @@ class NoteShort {
visitedNoteIds.add(this.noteId);
for (const templateNote of this.getTemplateNotes()) {
if (templateNote.hasAncestor(ancestorNoteId, visitedNoteIds)) {
if (templateNote.hasAncestor(ancestorNote, visitedNoteIds)) {
return true;
}
}
for (const parentNote of this.getParentNotes()) {
if (parentNote.hasAncestor(ancestorNoteId, visitedNoteIds)) {
if (parentNote.hasAncestor(ancestorNote, visitedNoteIds)) {
return true;
}
}
@@ -781,26 +758,6 @@ class NoteShort {
throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`);
}
}
isShared() {
for (const parentNoteId of this.parents) {
if (parentNoteId === 'root' || parentNoteId === 'none') {
continue;
}
const parentNote = froca.notes[parentNoteId];
if (!parentNote) {
continue;
}
if (parentNote.noteId === 'share' || parentNote.isShared()) {
return true;
}
}
return false;
}
}
export default NoteShort;

View File

@@ -47,7 +47,6 @@ import MermaidWidget from "../widgets/mermaid.js";
import BookmarkButtons from "../widgets/bookmark_buttons.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import BacklinksWidget from "../widgets/backlinks.js";
import SharedInfoWidget from "../widgets/shared_info.js";
export default class DesktopLayout {
constructor(customWidgets) {
@@ -148,7 +147,6 @@ export default class DesktopLayout {
.titlePlacement("bottom"))
.button(new NoteActionsWidget())
)
.child(new SharedInfoWidget())
.child(new NoteUpdateStatusWidget())
.child(new BacklinksWidget())
.child(new MermaidWidget())

View File

@@ -50,7 +50,7 @@ function isAffecting(attrRow, affectedNote) {
if (this.isInheritable) {
for (const owningNote of owningNotes) {
if (owningNote.hasAncestor(attrNote.noteId)) {
if (owningNote.hasAncestor(attrNote)) {
return true;
}
}

View File

@@ -196,18 +196,8 @@ ws.subscribeToMessages(async message => {
}
});
async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) {
const resp = await server.put(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
prefix: prefix
});
if (!resp.success) {
alert(resp.message);
}
}
async function cloneNoteToNote(childNoteId, parentNoteId, prefix) {
const resp = await server.put(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
async function cloneNoteTo(childNoteId, parentBranchId, prefix) {
const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, {
prefix: prefix
});
@@ -232,6 +222,5 @@ export default {
deleteNotes,
moveNodeUpInHierarchy,
cloneNoteAfter,
cloneNoteToBranch,
cloneNoteToNote,
cloneNoteTo
};

View File

@@ -51,7 +51,7 @@ async function pasteInto(parentBranchId) {
for (const clipboardBranch of clipboardBranches) {
const clipboardNote = await clipboardBranch.getNote();
await branchService.cloneNoteToBranch(clipboardNote.noteId, parentBranchId);
await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId);
}
// copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places

View File

@@ -213,13 +213,9 @@ export default class Entrypoints extends Component {
} else if (note.mime.endsWith("env=backend")) {
await server.post('script/run/' + note.noteId);
} else if (note.mime === 'text/x-sqlite;schema=trilium') {
const resp = await server.post("sql/execute/" + note.noteId);
const {results} = await server.post("sql/execute/" + note.noteId);
if (!resp.success) {
alert("Error occurred while executing SQL query: " + resp.message);
}
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: results});
}
toastService.showMessage("Note executed");

View File

@@ -188,7 +188,7 @@ class Froca {
froca.notes[note.noteId].childToBranch = {};
}
const branches = [...note.getParentBranches(), ...note.getChildBranches()];
const branches = [...note.getBranches(), ...note.getChildBranches()];
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree

View File

@@ -10,7 +10,6 @@ const CODE_MIRROR = {
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
"libraries/codemirror/keymap/vim.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
],

View File

@@ -8,7 +8,75 @@ import froca from "./froca.js";
// this key needs to have this value so it's hit by the tooltip
const SELECTED_NOTE_PATH_KEY = "data-note-path";
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
const acMixin = {
selectedNotePath: "",
selectedExternalLink: "",
$el: "",
focus() {
this.$el.find('.aa-Input').focus();
},
select() {
this.$el.find('.aa-Input').select();
},
getQuery() {
return this?.lastState.query;
},
getSelectedNotePath() {
if (!this.getQuery()) {
return "";
} else {
return this.selectedNotePath;
}
},
getSelectedNoteId() {
const notePath = this.getSelectedNotePath();
const chunks = notePath.split('/');
return chunks.length >= 1 ? chunks[chunks.length - 1] : null;
},
setSelectedNotePath(notePath) {
notePath = notePath || "";
this.selectedNotePath = notePath;
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", !notePath.trim())
.attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed
},
getSelectedExternalLink() {
if (!$(this).val().trim()) {
return "";
} else {
return this.selectedExternalLink;
}
},
setSelectedExternalLink(externalLink) {
this.selectedExternalLink = externalLink;
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", true);
},
async setNote(noteId) {
const note = noteId ? await froca.getNote(noteId, true) : null;
$(this)
.val(note ? note.title : "")
.setSelectedNotePath(noteId);
}
}
async function autocompleteSourceForCKEditor(queryText) {
return await new Promise((res, rej) => {
@@ -30,7 +98,7 @@ async function autocompleteSourceForCKEditor(queryText) {
});
}
async function autocompleteSource(term, cb, options = {}) {
async function autocompleteSource(term, options = {}) {
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
let results = await server.get('autocomplete'
@@ -58,206 +126,176 @@ async function autocompleteSource(term, cb, options = {}) {
].concat(results);
}
cb(results);
return results;
}
function clearText($el) {
function clearText(acObj) {
if (utils.isMobile()) {
return;
}
$el.setSelectedNotePath("");
$el.autocomplete("val", "").trigger('change');
acObj.ext.setSelectedNotePath("");
acObj.setQuery("");
}
function setText($el, text) {
function setText(ac, text) {
if (utils.isMobile()) {
return;
}
$el.setSelectedNotePath("");
$el
.autocomplete("val", text.trim())
.autocomplete("open");
ac.ext.setSelectedNotePath("");
ac.setQuery(text.trim());
ac.setIsOpen(true);
}
function showRecentNotes($el) {
function showRecentNotes(ac) {
if (utils.isMobile()) {
return;
}
$el.setSelectedNotePath("");
$el.autocomplete("val", "");
$el.trigger('focus');
ac.ext.$el.find(".aa-Input").val("").change();
ac.setQuery("");
ac.setIsOpen(true);
ac.update();
ac.ext.setSelectedNotePath("");
ac.ext.focus();
console.log("BBB");
}
function initNoteAutocomplete($el, options) {
if ($el.hasClass("note-autocomplete-input") || utils.isMobile()) {
function initNoteAutocomplete($container, options) {
if ($container.hasClass("note-autocomplete-container") || utils.isMobile()) {
// clear any event listener added in previous invocation of this function
$el.off('autocomplete:noteselected');
$container.off('autocomplete:noteselected');
return $el;
return $container.prop("acObj");
}
options = options || {};
$el.addClass("note-autocomplete-input");
const $el = $('<div class="note-autocomplete-input">');
const $sideButtons = $('<div>');
const $clearTextButton = $("<a>")
.addClass("input-group-text input-clearer-button bx bx-x")
.prop("title", "Clear text field");
$container.addClass("note-autocomplete-container")
.append($el)
.append($sideButtons);
const $showRecentNotesButton = $("<a>")
.addClass("input-group-text show-recent-notes-button bx bx-time")
.addClass("show-recent-notes-button bx bx-time")
.prop("title", "Show recent notes");
const $goToSelectedNoteButton = $("<a>")
.addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right")
.addClass("go-to-selected-note-button bx bx-arrow-to-right")
.attr("data-action", "note");
const $sideButtons = $("<div>")
.addClass("input-group-append")
.append($clearTextButton)
.append($showRecentNotesButton);
$sideButtons.append($showRecentNotesButton);
if (!options.hideGoToSelectedNoteButton) {
$sideButtons.append($goToSelectedNoteButton);
}
$el.after($sideButtons);
$clearTextButton.on('click', () => clearText($el));
$showRecentNotesButton.on('click', e => {
showRecentNotes($el);
showRecentNotes(acObj);
// this will cause the click not give focus to the "show recent notes" button
// this is important because otherwise input will lose focus immediatelly and not show the results
return false;
});
$el.autocomplete({
appendTo: document.querySelector('body'),
hint: false,
autoselect: true,
const { autocomplete } = window['@algolia/autocomplete-js'];
let acObj = autocomplete({
container: $el[0],
defaultActiveItemId: 0,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
}, [
{
source: (term, cb) => autocompleteSource(term, cb, options),
displayKey: 'notePathTitle',
templates: {
suggestion: suggestion => suggestion.highlightedNotePathTitle
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
cache: false
tabAutocomplete: false,
placeholder: options.placeholder,
onStateChange({ state }) {
acObj.lastState = state;
},
async getSources({ query }) {
const items = await autocompleteSource(query, options);
return [
{
getItems() {
return items;
},
onSelect({item}) {
acObj.ext.$el.trigger("autocomplete:selected", [item]);
},
displayKey: 'notePathTitle',
templates: {
item({ item, createElement }) {
return createElement('div', {
dangerouslySetInnerHTML: {
__html: item.highlightedNotePathTitle,
},
});
}
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
cache: false
}
]
}
]);
});
$el.on('autocomplete:selected', async (event, suggestion) => {
if (suggestion.action === 'external-link') {
$el.setSelectedNotePath(null);
$el.setSelectedExternalLink(suggestion.externalLink);
acObj.ext = {...acMixin, $el, $container };
$el.autocomplete("val", suggestion.externalLink);
$container.prop("acObj", acObj);
$el.autocomplete("close");
$container.on('autocomplete:selected', async (event, item) => {
if (item.action === 'external-link') {
acObj.ext.setSelectedNotePath(null);
acObj.ext.setSelectedExternalLink(item.externalLink);
$el.trigger('autocomplete:externallinkselected', [suggestion]);
acObj.setQuery(item.externalLink);
$container.autocomplete("close");
$container.trigger('autocomplete:externallinkselected', [item]);
return;
}
if (suggestion.action === 'create-note') {
const {note} = await noteCreateService.createNote(suggestion.parentNoteId, {
title: suggestion.noteTitle,
if (item.action === 'create-note') {
const {note} = await noteCreateService.createNote(item.parentNoteId, {
title: item.noteTitle,
activate: false
});
suggestion.notePath = treeService.getSomeNotePath(note);
item.notePath = treeService.getSomeNotePath(note);
}
$el.setSelectedNotePath(suggestion.notePath);
$el.setSelectedExternalLink(null);
acObj.ext.setSelectedNotePath(item.notePath);
acObj.ext.setSelectedExternalLink(null);
$el.autocomplete("val", suggestion.noteTitle);
acObj.setQuery(item.noteTitle);
$el.autocomplete("close");
acObj.setIsOpen(false);
$el.trigger('autocomplete:noteselected', [suggestion]);
$container.trigger('autocomplete:noteselected', [item]);
});
$el.on('autocomplete:closed', () => {
if (!$el.val().trim()) {
clearText($el);
$container.on('autocomplete:closed', () => {
if (!$container.val().trim()) {
clearText($container);
}
});
$el.on('autocomplete:opened', () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
$container.on('autocomplete:opened', () => {
if ($container.attr("readonly")) {
$container.autocomplete('close');
}
});
// clear any event listener added in previous invocation of this function
$el.off('autocomplete:noteselected');
$container.off('autocomplete:noteselected');
return $el;
}
function init() {
$.fn.getSelectedNotePath = function () {
if (!$(this).val().trim()) {
return "";
} else {
return $(this).attr(SELECTED_NOTE_PATH_KEY);
}
};
$.fn.getSelectedNoteId = function () {
const notePath = $(this).getSelectedNotePath();
const chunks = notePath.split('/');
return chunks.length >= 1 ? chunks[chunks.length - 1] : null;
}
$.fn.setSelectedNotePath = function (notePath) {
notePath = notePath || "";
$(this).attr(SELECTED_NOTE_PATH_KEY, notePath);
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", !notePath.trim())
.attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed
};
$.fn.getSelectedExternalLink = function () {
if (!$(this).val().trim()) {
return "";
} else {
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
}
};
$.fn.setSelectedExternalLink = function (externalLink) {
$(this).attr(SELECTED_EXTERNAL_LINK_KEY, externalLink);
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", true);
}
$.fn.setNote = async function (noteId) {
const note = noteId ? await froca.getNote(noteId, true) : null;
$(this)
.val(note ? note.title : "")
.setSelectedNotePath(noteId);
}
return acObj;
}
export default {
@@ -265,6 +303,5 @@ export default {
autocompleteSourceForCKEditor,
initNoteAutocomplete,
showRecentNotes,
setText,
init
setText
}

View File

@@ -59,10 +59,10 @@ class NoteContext extends Component {
});
}
if (utils.isDesktop()) {
// close dangling autocompletes after closing the tab
$(".aa-input").autocomplete("close");
}
// if (utils.isDesktop()) {
// // close dangling autocompletes after closing the tab
// $(".aa-input").autocomplete("close");
// }
}
getSubContexts() {

View File

@@ -75,9 +75,7 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
if (logErrors) {
const parent = froca.getNoteFromCache(parentNoteId);
console.debug(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'})
for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}.
You can ignore this message as it is mostly harmless.`);
console.debug(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}. You can ignore this message as it is mostly harmless.`);
}
const someNotePath = getSomeNotePath(child, hoistedNoteId);
@@ -85,10 +83,6 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
if (someNotePath) { // in case it's root the path may be empty
const pathToRoot = someNotePath.split("/").reverse().slice(1);
if (!pathToRoot.includes("root")) {
pathToRoot.push('root');
}
for (const noteId of pathToRoot) {
effectivePathSegments.push(noteId);
}

View File

@@ -340,12 +340,10 @@ function initHelpDropdown($el) {
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"
function openHelp(e) {
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
}
function initHelpButtons($el) {
$el.on("click", "*[data-help-page]", e => openHelp(e));
$el.on("click", "*[data-help-page]", e => {
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
});
}
function filterAttributeName(name) {
@@ -399,7 +397,6 @@ export default {
timeLimit,
initHelpDropdown,
initHelpButtons,
openHelp,
filterAttributeName,
isValidAttributeName
};

View File

@@ -1,21 +0,0 @@
/**
* Fetch note with given ID from backend
*
* @param noteId of the given note to be fetched. If falsy, fetches current note.
*/
async function fetchNote(noteId = null) {
if (!noteId) {
noteId = document.body.getAttribute("data-note-id");
}
const resp = await fetch(`api/notes/${noteId}`);
return await resp.json();
}
document.addEventListener('DOMContentLoaded', () => {
const toggleMenuButton = document.getElementById('toggleMenuButton');
const layout = document.getElementById('layout');
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
}, false);

View File

@@ -210,10 +210,7 @@ const ATTR_HELP = {
"hoistedInbox": "default inbox location for new notes when hoisted to some ancestor of this note",
"sqlConsoleHome": "default location of SQL console notes",
"bookmarked": "note with this label will appear in bookmarks",
"bookmarkFolder": "note with this label will appear in bookmarks as folder (allowing access to its children)",
"shareHiddenFromTree": "this note is hidden from left navigation tree, but still accessible with its URL",
"shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]",
"shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.",
"bookmarkFolder": "note with this label will appear in bookmarks as folder (allowing access to its children)"
},
"relation": {
"runOnNoteCreation": "executes when note is created on backend",
@@ -224,10 +221,7 @@ const ATTR_HELP = {
"runOnAttributeChange": "executes when attribute is changed under this note",
"template": "attached note's attributes will be inherited even without parent-child relationship. See template for details.",
"renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered',
"widget": "target of this relation will be executed and rendered as a widget in the sidebar",
"shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.",
"shareJs": "JavaScript note which will be injected into the share page. JS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
"shareFavicon": "Favicon note to be set in the shared page. Typically you want to set it to share root and make it inheritable. Favicon note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
"widget": "target of this relation will be executed and rendered as a widget in the sidebar"
}
};
@@ -298,7 +292,9 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.$rowTargetNote = this.$widget.find('.attr-row-target-note');
this.$inputTargetNote = this.$widget.find('.attr-input-target-note');
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, {allowCreatingNotes: true})
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, {allowCreatingNotes: true});
this.$inputTargetNote
.on('autocomplete:noteselected', (event, suggestion, dataset) => {
if (!suggestion.notePath) {
return false;

View File

@@ -33,7 +33,11 @@ const TPL = `
.backlinks-close-ticker {
cursor: pointer;
}
.backlinks-ticker:hover {
opacity: 100%;
}
.backlinks-items {
z-index: 10;
position: absolute;
@@ -103,19 +107,17 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
async refreshWithNote(note) {
this.clearItems();
// can't use froca since that would count only relations from loaded notes
const resp = await server.get(`notes/${this.noteId}/backlink-count`);
if (!resp || !resp.count) {
const targetRelationCount = note.getTargetRelations().length;
if (targetRelationCount === 0) {
this.$ticker.hide();
return;
}
this.$ticker.show();
this.$count.text(
`${resp.count} backlink`
+ (resp.count === 1 ? '' : 's')
);
else {
this.$ticker.show();
this.$count.text(
`${targetRelationCount} backlink`
+ (targetRelationCount === 1 ? '' : 's')
);
}
}
clearItems() {
@@ -138,22 +140,18 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
await froca.getNotes(backlinks.map(bl => bl.noteId)); // prefetch all
for (const backlink of backlinks) {
const $item = $("<div>");
$item.append(await linkService.createNoteLink(backlink.noteId, {
this.$items.append(await linkService.createNoteLink(backlink.noteId, {
showNoteIcon: true,
showNotePath: true,
showTooltip: false
}));
if (backlink.relationName) {
$item.append($("<p>").text("relation: " + backlink.relationName));
this.$items.append($("<p>").text("relation: " + backlink.relationName));
}
else {
$item.append(...backlink.excerpts);
this.$items.append(...backlink.excerpts);
}
this.$items.append($item);
}
}
}

View File

@@ -1,32 +1,96 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import protectedSessionService from "../services/protected_session.js";
import attributeService from "../services/attributes.js";
import SwitchWidget from "./switch.js";
export default class BookmarkSwitchWidget extends SwitchWidget {
const TPL = `
<div class="bookmark-switch">
<style>
/* The switch - the box around the slider */
.bookmark-switch .switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
float: right;
}
/* The slider */
.bookmark-switch .slider {
border-radius: 24px;
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--more-accented-background-color);
transition: .4s;
}
.bookmark-switch .slider:before {
border-radius: 50%;
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: var(--main-background-color);
-webkit-transition: .4s;
transition: .4s;
}
.bookmark-switch .slider.checked {
background-color: var(--main-text-color);
}
.bookmark-switch .slider.checked:before {
transform: translateX(26px);
}
</style>
<div class="add-bookmark-button">
Bookmark
&nbsp;
<span title="Bookmark this note to the left side panel">
<label class="switch">
<span class="slider"></span>
</span>
</div>
<div class="remove-bookmark-button">
Bookmark
&nbsp;
<span title="Remove bookmark">
<label class="switch">
<span class="slider checked"></span>
</span>
</div>
</div>`;
export default class BookmarkSwitchWidget extends NoteContextAwareWidget {
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$switchOnName.text("Bookmark");
this.$switchOnButton.attr("title", "Bookmark this note to the left side panel");
this.$addBookmarkButton = this.$widget.find(".add-bookmark-button");
this.$addBookmarkButton.on('click', () => attributeService.setLabel(this.noteId, 'bookmarked'));
this.$switchOffName.text("Bookmark");
this.$switchOffButton.attr("title", "Remove bookmark");
}
async switchOff() {
for (const label of this.note.getLabels('bookmarked')) {
await attributeService.removeAttributeById(this.noteId, label.attributeId);
}
}
switchOn() {
return attributeService.setLabel(this.noteId, 'bookmarked');
this.$removeBookmarkButton = this.$widget.find(".remove-bookmark-button");
this.$removeBookmarkButton.on('click', async () => {
for (const label of this.note.getLabels('bookmarked')) {
await attributeService.removeAttributeById(this.noteId, label.attributeId);
}
});
}
refreshWithNote(note) {
const isBookmarked = note.hasLabel('bookmarked');
this.$switchOn.toggle(!isBookmarked);
this.$switchOff.toggle(isBookmarked);
this.$addBookmarkButton.toggle(!isBookmarked);
this.$removeBookmarkButton.toggle(isBookmarked);
}
entitiesReloadedEvent({loadResults}) {

View File

@@ -44,10 +44,7 @@ export default class ButtonWidget extends NoteContextAwareWidget {
this.$widget.tooltip({
html: true,
title: () => {
const title = typeof this.settings.title === "function"
? this.settings.title()
: this.settings.title;
const title = this.settings.title;
const action = actions.find(act => act.actionName === this.settings.command);
if (action && action.effectiveShortcuts.length > 0) {

View File

@@ -18,12 +18,7 @@ export default class OpenNoteButtonWidget extends ButtonWidget {
}
this.icon(note.getIcon());
this.title(() => {
const n = froca.getNoteFromCache(noteId);
// always fresh, always decoded (when protected session is available)
return n.title;
});
this.title(note.title);
this.refreshIcon();
});

View File

@@ -661,10 +661,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
extraClasses.push("protected");
}
if (note.isShared()) {
extraClasses.push("shared");
}
else if (note.getParentNoteIds().length > 1) {
if (note.getParentNoteIds().length > 1) {
const notSearchParents = note.getParentNoteIds()
.map(noteId => froca.notes[noteId])
.filter(note => !!note)
@@ -1017,14 +1014,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
for (const ecBranch of loadResults.getBranches()) {
if (ecBranch.parentNoteId === 'share') {
// all shared notes have a sign in the tree, even the descendants of shared notes
noteIdsToReload.add(ecBranch.noteId);
}
else {
// adding noteId itself to update all potential clones
noteIdsToUpdate.add(ecBranch.noteId);
}
// adding noteId itself to update all potential clones
noteIdsToUpdate.add(ecBranch.noteId);
for (const node of this.getNodesByBranch(ecBranch)) {
if (ecBranch.isDeleted) {

View File

@@ -1,28 +1,89 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import protectedSessionService from "../services/protected_session.js";
import SwitchWidget from "./switch.js";
export default class ProtectedNoteSwitchWidget extends SwitchWidget {
const TPL = `
<div class="protected-note-switch">
<style>
/* The switch - the box around the slider */
.protected-note-switch .switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
float: right;
}
/* The slider */
.protected-note-switch .slider {
border-radius: 24px;
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--more-accented-background-color);
transition: .4s;
}
.protected-note-switch .slider:before {
border-radius: 50%;
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: var(--main-background-color);
-webkit-transition: .4s;
transition: .4s;
}
.protected-note-switch .slider.checked {
background-color: var(--main-text-color);
}
.protected-note-switch .slider.checked:before {
transform: translateX(26px);
}
</style>
<div class="protect-button">
Protect the note
&nbsp;
<span title="Note is not protected, click to make it protected">
<label class="switch">
<span class="slider"></span>
</span>
</div>
<div class="unprotect-button">
Unprotect the note
&nbsp;
<span title="Note is protected, click to make it unprotected">
<label class="switch">
<span class="slider checked"></span>
</span>
</div>
</div>`;
export default class ProtectedNoteSwitchWidget extends NoteContextAwareWidget {
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$switchOnName.text("Protect the note");
this.$switchOnButton.attr("title", "Note is not protected, click to make it protected");
this.$protectButton = this.$widget.find(".protect-button");
this.$protectButton.on('click', () => protectedSessionService.protectNote(this.noteId, true, false));
this.$switchOffName.text("Unprotect the note");
this.$switchOffButton.attr("title", "Note is protected, click to make it unprotected");
}
switchOn() {
protectedSessionService.protectNote(this.noteId, true, false);
}
switchOff() {
protectedSessionService.protectNote(this.noteId, false, false)
this.$unprotectButton = this.$widget.find(".unprotect-button");
this.$unprotectButton.on('click', () => protectedSessionService.protectNote(this.noteId, false, false));
}
refreshWithNote(note) {
this.$switchOn.toggle(!note.isProtected);
this.$switchOff.toggle(!!note.isProtected);
this.$protectButton.toggle(!note.isProtected);
this.$unprotectButton.toggle(!!note.isProtected);
}
entitiesReloadedEvent({loadResults}) {

View File

@@ -3,7 +3,6 @@ import NoteTypeWidget from "../note_type.js";
import ProtectedNoteSwitchWidget from "../protected_note_switch.js";
import EditabilitySelectWidget from "../editability_select.js";
import BookmarkSwitchWidget from "../bookmark_switch.js";
import SharedSwitchWidget from "../shared_switch.js";
const TPL = `
<div class="basic-properties-widget">
@@ -37,8 +36,6 @@ const TPL = `
</div>
<div class="bookmark-switch-container"></div>
<div class="shared-switch-container"></div>
</div>`;
export default class BasicPropertiesWidget extends NoteContextAwareWidget {
@@ -49,14 +46,12 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
this.protectedNoteSwitchWidget = new ProtectedNoteSwitchWidget().contentSized();
this.editabilitySelectWidget = new EditabilitySelectWidget().contentSized();
this.bookmarkSwitchWidget = new BookmarkSwitchWidget().contentSized();
this.sharedSwitchWidget = new SharedSwitchWidget().contentSized();
this.child(
this.noteTypeWidget,
this.protectedNoteSwitchWidget,
this.editabilitySelectWidget,
this.bookmarkSwitchWidget,
this.sharedSwitchWidget
this.bookmarkSwitchWidget
);
}
@@ -88,7 +83,6 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
this.$widget.find(".protected-note-switch-container").append(this.protectedNoteSwitchWidget.render());
this.$widget.find(".editability-select-container").append(this.editabilitySelectWidget.render());
this.$widget.find(".bookmark-switch-container").append(this.bookmarkSwitchWidget.render());
this.$widget.find(".shared-switch-container").append(this.sharedSwitchWidget.render());
}
async refreshWithNote(note) {

View File

@@ -1,57 +0,0 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import options from "../services/options.js";
import attributeService from "../services/attributes.js";
const TPL = `
<div class="shared-info-widget alert alert-warning">
<style>
.shared-info-widget {
margin: 10px;
contain: none;
padding: 10px;
font-weight: bold;
}
</style>
<span class="share-text"></span> <a class="share-link external"></a>. For help visit <a href="https://github.com/zadam/trilium/wiki/Sharing">wiki</a>.
</div>`;
export default class SharedInfoWidget extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && this.noteId !== 'share' && this.note.hasAncestor('share');
}
doRender() {
this.$widget = $(TPL);
this.$shareLink = this.$widget.find(".share-link");
this.$shareText = this.$widget.find(".share-text");
this.contentSized();
}
async refreshWithNote(note) {
const syncServerHost = options.get("syncServerHost");
let link;
const shareId = note.getOwnedLabelValue('shareAlias') || note.noteId;
if (syncServerHost) {
link = syncServerHost + "/share/" + shareId;
this.$shareText.text("This note is shared publicly on");
}
else {
link = location.protocol + '//' + location.host + location.pathname + "share/" + shareId;
this.$shareText.text("This note is shared locally on");
}
this.$shareLink.attr("href", link).text(link);
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes().find(attr => attr.name.startsWith("share") && attributeService.isAffecting(attr, this.note))) {
this.refresh();
}
else if (loadResults.getBranches().find(branch => branch.noteId === this.noteId)) {
this.refresh();
}
}
}

View File

@@ -1,71 +0,0 @@
import SwitchWidget from "./switch.js";
import branchService from "../services/branches.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
export default class SharedSwitchWidget extends SwitchWidget {
isEnabled() {
return super.isEnabled() && this.noteId !== 'root' && this.noteId !== 'share';
}
doRender() {
super.doRender();
this.$switchOnName.text("Shared");
this.$switchOnButton.attr("title", "Share the note");
this.$switchOffName.text("Shared");
this.$switchOffButton.attr("title", "Unshare the note");
this.$helpButton.attr("data-help-page", "Sharing").show();
this.$helpButton.on('click', e => utils.openHelp(e));
}
switchOn() {
branchService.cloneNoteToNote(this.noteId, 'share');
}
async switchOff() {
const shareBranch = this.note.getParentBranches().find(b => b.parentNoteId === 'share');
if (!shareBranch) {
return;
}
if (this.note.getParentBranches().length === 1) {
const confirmDialog = await import('../dialogs/confirm.js');
const text = "This note exists only as a shared note, unsharing would delete it. Do you want to continue and thus delete this note?";
if (!await confirmDialog.confirm(text)) {
return;
}
}
await server.remove(`branches/${shareBranch.branchId}?taskId=no-progress-reporting`);
}
async refreshWithNote(note) {
const isShared = note.hasAncestor('share');
const canBeUnshared = isShared && note.getParentBranches().find(b => b.parentNoteId === 'share');
const switchDisabled = isShared && !canBeUnshared;
this.$switchOn.toggle(!isShared);
this.$switchOff.toggle(!!isShared);
if (switchDisabled) {
this.$widget.attr("title", "Note cannot be unshared here because it is shared through inheritance from an ancestor.");
this.$switchOff.addClass("switch-disabled");
}
else {
this.$widget.removeAttr("title");
this.$switchOff.removeClass("switch-disabled");
}
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getBranches().find(b => b.noteId === this.noteId)) {
this.refresh();
}
}
}

View File

@@ -1,117 +0,0 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = `
<div class="switch-widget">
<style>
.switch-widget {
display: flex;
align-items: center;
}
/* The switch - the box around the slider */
.switch-widget .switch {
position: relative;
display: block;
width: 50px;
height: 24px;
margin: 0;
}
.switch-on, .switch-off {
display: flex;
}
/* The slider */
.switch-widget .slider {
border-radius: 24px;
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--more-accented-background-color);
transition: .4s;
}
.switch-widget .slider:before {
border-radius: 50%;
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: var(--main-background-color);
-webkit-transition: .4s;
transition: .4s;
}
.switch-widget .slider.checked {
background-color: var(--main-text-color);
}
.switch-widget .slider.checked:before {
transform: translateX(26px);
}
.switch-widget .switch-disabled {
opacity: 70%;
pointer-events: none;
}
.switch-widget .switch-help-button {
font-weight: 900;
border: 0;
background: none;
cursor: pointer;
}
</style>
<div class="switch-on">
<span class="switch-on-name"></span>
&nbsp;
<span class="switch-on-button">
<label class="switch">
<span class="slider"></span>
</span>
</div>
<div class="switch-off">
<span class="switch-off-name"></span>
&nbsp;
<span class="switch-off-button">
<label class="switch">
<span class="slider checked"></span>
</span>
</div>
<button class="switch-help-button" type="button" data-help-page="" title="Open help page" style="display: none;">?</button>
</div>`;
export default class SwitchWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.$switchOn = this.$widget.find(".switch-on");
this.$switchOnName = this.$widget.find(".switch-on-name");
this.$switchOnButton = this.$widget.find(".switch-on-button");
this.$switchOnButton.on('click', () => this.switchOn());
this.$switchOff = this.$widget.find(".switch-off");
this.$switchOffName = this.$widget.find(".switch-off-name");
this.$switchOffButton = this.$widget.find(".switch-off-button");
this.$switchOffButton.on('click', () => this.switchOff());
this.$helpButton = this.$widget.find(".switch-help-button");
}
switchOff() {}
switchOn() {}
}

View File

@@ -6,7 +6,6 @@ import ws from "../../services/ws.js";
import appContext from "../../services/app_context.js";
import toastService from "../../services/toast.js";
import treeService from "../../services/tree.js";
import options from "../../services/options.js";
const TPL = `
<div class="note-detail-code note-detail-printable">
@@ -15,10 +14,20 @@ const TPL = `
position: relative;
}
.trilium-api-docs-button {
/*display: none;*/
position: absolute;
top: 10px;
right: 10px;
}
.note-detail-code-editor {
min-height: 50px;
}
</style>
<button class="btn bx bx-help-circle trilium-api-docs-button icon-button floating-button"
title="Open Trilium API docs"></button>
<div class="note-detail-code-editor"></div>
@@ -28,13 +37,6 @@ const TPL = `
Execute <kbd data-command="runActiveNote"></kbd>
</button>
<button class="no-print trilium-api-docs-button btn btn-sm"
title="Open Trilium API docs">
<span class="bx bx-help-circle"></span>
API docs
</button>
<button class="no-print save-to-note-button btn btn-sm">
<span class="bx bx-save"></span>
@@ -95,7 +97,6 @@ export default class EditableCodeTypeWidget extends TypeWidget {
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
matchTags: {bothTags: true},
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
lint: true,

View File

@@ -84,11 +84,5 @@ export default class EmptyTypeWidget extends TypeWidget {
.on('click', () => this.triggerCommand('hoistNote', {noteId: workspaceNote.noteId}))
);
}
if (workspaceNotes.length === 0) {
this.$autoComplete
.trigger('focus')
.trigger('select');
}
}
}

View File

@@ -1,16 +1,12 @@
body {
font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
}
#layout {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: row-reverse;
flex-direction: row;
}
#menu {
padding: 25px;
padding: 20px;
flex-basis: 0;
flex-grow: 1;
overflow: auto;
@@ -32,21 +28,18 @@ body {
#main {
flex-basis: 0;
flex-grow: 3;
overflow: auto;
padding: 10px 20px 20px 20px;
}
#parentLink {
float: right;
margin-top: 20px;
}
#title {
margin: 0;
padding-top: 10px;
padding: 10px 20px 0 20px;
}
img {
#content {
padding: 20px;
}
.type-image img {
max-width: 100%;
}
@@ -55,110 +48,40 @@ pre {
word-wrap: anywhere;
}
iframe.pdf-view {
width: 100%;
height: 800px;
}
#toggleMenuButton {
display: none;
#menuLink {
position: fixed;
top: 8px;
left: 5px;
display: block;
top: 0;
left: 0;
width: 1.4em;
border-radius: 5px;
border: 1px solid #aaa;
background: #000;
background: rgba(0,0,0,0.7);
font-size: 2rem;
z-index: 10;
height: auto;
color: black;
color: white;
border: none;
cursor: pointer;
}
#childLinks.grid ul {
list-style-type: none;
display: flex;
flex-wrap: wrap;
padding: 0;
}
#childLinks.grid ul li {
width: 180px;
height: 140px;
padding: 10px;
}
#childLinks.grid ul li a {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
border: 1px solid #ddd;
border-radius: 5px;
justify-content: center;
align-content: center;
text-align: center;
font-size: large;
}
#childLinks.grid ul li a:hover {
background: #eee;
}
#childLinks.list ul {
list-style-type: none;
display: inline-flex;
flex-wrap: wrap;
padding: 0;
margin-top: 5px;
}
#childLinks.list ul li {
margin-right: 20px;
}
#noteClippedFrom {
padding: 10px 0 10px 0;
margin: 20px 0 20px 0;
color: #666;
border: 1px solid #ddd;
border-left: 0;
border-right: 0;
}
#toggleMenuButton::after {
position: relative;
top: -2px;
left: 1px;
}
@media (max-width: 48em) {
#layout.showMenu #menu {
display: block;
margin-top: 40px;
}
#toggleMenuButton {
#layout.active #menu {
display: block;
}
#layout.showMenu #main {
#layout.active #main {
display: none;
}
#title {
padding-left: 60px;
}
#layout.showMenu #toggleMenuButton::after {
#layout.active #menuLink::after {
content: "«";
}
#toggleMenuButton::after {
content: "»";
}
#menu {
display: none;
}
#menuLink::after {
content: "»";
}
}

View File

@@ -358,9 +358,12 @@ pre:not(.CodeMirror-line) {
color: var(--button-disabled-background-color) !important;
}
.note-autocomplete-input {
/* this is for seamless integration of "input clearer" button */
border-right: 0;
.note-autocomplete-container {
display: flex;
}
.note-autocomplete-container .note-autocomplete-input {
flex-grow: 1;
}
table.promoted-attributes-in-tooltip {
@@ -432,43 +435,6 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
opacity: 1;
}
.algolia-autocomplete {
width: calc(100% - 30px);
z-index: 2000 !important;
}
.algolia-autocomplete .aa-input, .algolia-autocomplete .aa-hint {
width: 100%;
}
.algolia-autocomplete .aa-dropdown-menu {
width: 100%;
background-color: var(--main-background-color);
border: 1px solid var(--main-border-color);
border-top: none;
z-index: 2000 !important;
max-height: 500px;
overflow: auto;
padding: 0;
margin: 0;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion {
cursor: pointer;
padding: 5px;
margin: 0;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion p {
padding: 0;
margin: 0;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
color: var(--hover-item-text-color);
background-color: var(--hover-item-background-color);
}
.help-button {
float: right;
background: none;
@@ -958,3 +924,7 @@ input {
.note-split.full-content-width {
max-width: 999999px;
}
.aa-Panel {
z-index: 10000;
}

View File

@@ -135,13 +135,7 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
}
span.fancytree-node.multiple-parents .fancytree-title::after {
content: " *";
}
span.fancytree-node.shared .fancytree-title::after {
font-family: 'boxicons' !important;
font-size: smaller;
content: " \ec03"; /* lookup code for "share-alt" in boxicons.css */
content: " *"
}
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {

View File

@@ -2,18 +2,11 @@
const cloningService = require('../../services/cloning');
function cloneNoteToBranch(req) {
function cloneNoteToParent(req) {
const {noteId, parentBranchId} = req.params;
const {prefix} = req.body;
return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix);
}
function cloneNoteToNote(req) {
const {noteId, parentNoteId} = req.params;
const {prefix} = req.body;
return cloningService.cloneNoteToNote(noteId, parentNoteId, prefix);
return cloningService.cloneNoteToParent(noteId, parentBranchId, prefix);
}
function cloneNoteAfter(req) {
@@ -23,7 +16,6 @@ function cloneNoteAfter(req) {
}
module.exports = {
cloneNoteToBranch,
cloneNoteToNote,
cloneNoteToParent,
cloneNoteAfter
};

View File

@@ -1,22 +1,19 @@
"use strict";
const keyboardActions = require('../../services/keyboard_actions');
const becca = require('../../becca/becca');
const sql = require('../../services/sql');
function getKeyboardActions() {
return keyboardActions.getKeyboardActions();
}
function getShortcutsForNotes() {
const attrs = becca.findAttributes('label', 'keyboardShortcut');
const map = {};
for (const attr of attrs) {
map[attr.value] = attr.noteId;
}
return map;
return sql.getMap(`
SELECT value, noteId
FROM attributes
WHERE isDeleted = 0
AND type = 'label'
AND name = 'keyboardShortcut'`);
}
module.exports = {

View File

@@ -73,7 +73,7 @@ function deleteNote(req) {
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
for (const branch of note.getParentBranches()) {
for (const branch of note.getBranches()) {
noteService.deleteBranch(branch, deleteId, taskContext);
}
@@ -239,7 +239,7 @@ function getDeleteNotesPreview(req) {
const note = branch.getNote();
if (deleteAllClones || note.getParentBranches().length <= branchCountToDelete[branch.branchId]) {
if (deleteAllClones || note.getBranches().length <= branchCountToDelete[branch.branchId]) {
noteIdsToBeDeleted.add(note.noteId);
for (const childBranch of note.getChildBranches()) {
@@ -302,21 +302,6 @@ function uploadModifiedFile(req) {
note.setContent(fileContent);
}
function getBacklinkCount(req) {
const {noteId} = req.params;
const note = becca.getNote(noteId);
if (!note) {
return [404, "Not found"];
}
else {
return {
count: note.getTargetRelations().length
};
}
}
module.exports = {
getNote,
updateNote,
@@ -331,6 +316,5 @@ module.exports = {
duplicateSubtree,
eraseDeletedNotesNow,
getDeleteNotesPreview,
uploadModifiedFile,
getBacklinkCount
uploadModifiedFile
};

View File

@@ -33,7 +33,6 @@ const ALLOWED_OPTIONS = new Set([
'similarNotesWidget',
'editedNotesWidget',
'calendarWidget',
'vimKeymapEnabled',
'codeNotesMimeTypes',
'spellCheckEnabled',
'spellCheckLanguageCode',

View File

@@ -38,7 +38,7 @@ function run(req) {
}
function getBundlesWithLabel(label, value) {
const notes = attributeService.getNotesWithLabelFast(label, value);
const notes = attributeService.getNotesWithLabel(label, value);
const bundles = [];

View File

@@ -17,17 +17,7 @@ function uploadImage(req) {
const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
const {note, noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
const labelsStr = req.headers['x-labels'];
if (labelsStr?.trim()) {
const labels = JSON.parse(labelsStr);
for (const {name, value} of labels) {
note.setLabel(attributeService.sanitizeAttributeName(name), value);
}
}
const {noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
return {
noteId: noteId

View File

@@ -220,7 +220,6 @@ function register(app) {
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(GET, '/api/notes/:noteId/backlink-count', notesApiRoute.getBacklinkCount);
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
@@ -229,8 +228,7 @@ function register(app) {
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote);
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentBranchId', cloningApiRoute.cloneNoteToParent);
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);

View File

@@ -4,7 +4,7 @@ const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 188;
const APP_DB_VERSION = 187;
const SYNC_VERSION = 23;
const CLIPPER_PROTOCOL_VERSION = "1.0";

View File

@@ -52,9 +52,6 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'sorted' },
{ type: 'label', name: 'top' },
{ type: 'label', name: 'fullContentWidth' },
{ type: 'label', name: 'shareHiddenFromTree' },
{ type: 'label', name: 'shareAlias' },
{ type: 'label', name: 'shareOmitDefaultCss' },
// relation names
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
@@ -65,10 +62,7 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'relation', name: 'runOnAttributeChange', isDangerous: true },
{ type: 'relation', name: 'template' },
{ type: 'relation', name: 'widget', isDangerous: true },
{ type: 'relation', name: 'renderNote', isDangerous: true },
{ type: 'relation', name: 'shareCss', isDangerous: false },
{ type: 'relation', name: 'shareJs', isDangerous: false },
{ type: 'relation', name: 'shareFavicon', isDangerous: false },
{ type: 'relation', name: 'renderNote', isDangerous: true }
];
/** @returns {Note[]} */
@@ -101,24 +95,6 @@ function getNoteWithLabel(name, value) {
return null;
}
/**
* Does not take into account templates and inheritance
*/
function getNotesWithLabelFast(name, value) {
// optimized version (~20 times faster) without using normal search, useful for e.g. finding date notes
const attrs = becca.findAttributes('label', name);
if (value === undefined) {
return attrs.map(attr => attr.getNote());
}
value = value?.toLowerCase();
return attrs
.filter(attr => attr.value.toLowerCase() === value)
.map(attr => attr.getNote());
}
function createLabel(noteId, name, value = "") {
return createAttribute({
noteId: noteId,
@@ -210,7 +186,6 @@ function sanitizeAttributeName(origName) {
module.exports = {
getNotesWithLabel,
getNotesWithLabelFast,
getNoteWithLabel,
createLabel,
createRelation,

View File

@@ -1 +1 @@
module.exports = { buildDate:"2022-01-02T22:43:30+01:00", buildRevision: "feffd57f240438d107c1ed1c1772545611a97dee" };
module.exports = { buildDate:"2021-12-13T11:12:31+01:00", buildRevision: "d9550dd59b9b0dff0b229c400cdf6585abcb226a" };

View File

@@ -10,18 +10,14 @@ const utils = require('./utils');
const becca = require("../becca/becca");
const beccaService = require("../becca/becca_service");
function cloneNoteToNote(noteId, parentNoteId, prefix) {
if (parentNoteId === 'share') {
const specialNotesService = require('./special_notes');
// share root note is created lazily
specialNotesService.getShareRoot();
}
function cloneNoteToParent(noteId, parentBranchId, prefix) {
const parentBranch = becca.getBranch(parentBranchId);
if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
if (isNoteDeleted(noteId) || isNoteDeleted(parentBranch.noteId)) {
return { success: false, message: 'Note is deleted.' };
}
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
const validationResult = treeService.validateParentChild(parentBranch.noteId, noteId);
if (!validationResult.success) {
return validationResult;
@@ -29,31 +25,19 @@ function cloneNoteToNote(noteId, parentNoteId, prefix) {
const branch = new Branch({
noteId: noteId,
parentNoteId: parentNoteId,
parentNoteId: parentBranch.noteId,
prefix: prefix,
isExpanded: 0
}).save();
return {
success: true,
branchId: branch.branchId,
notePath: beccaService.getNotePath(parentNoteId).path + "/" + noteId
};
}
function cloneNoteToBranch(noteId, parentBranchId, prefix) {
const parentBranch = becca.getBranch(parentBranchId);
if (!parentBranch) {
return { success: false, message: `Parent branch ${parentBranchId} does not exist.` };
}
const ret = cloneNoteToNote(noteId, parentBranch.noteId, prefix);
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
parentBranch.save();
return ret;
return {
success: true,
branchId: branch.branchId,
notePath: beccaService.getNotePath(parentBranch.noteId).path + "/" + noteId
};
}
function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
@@ -131,8 +115,7 @@ function isNoteDeleted(noteId) {
}
module.exports = {
cloneNoteToBranch,
cloneNoteToNote,
cloneNoteToParent,
ensureNoteIsPresentInParent,
ensureNoteIsAbsentFromParent,
toggleNoteInParent,

View File

@@ -258,8 +258,7 @@ class ConsistencyChecks {
FROM branches
WHERE noteId = ?
and parentNoteId = ?
and isDeleted = 0
ORDER BY utcDateModified`, [noteId, parentNoteId]);
and isDeleted = 0`, [noteId, parentNoteId]);
const branches = branchIds.map(branchId => becca.getBranch(branchId));
@@ -538,27 +537,6 @@ class ConsistencyChecks {
logError(`Unrecognized entity change id=${id}, entityName=${entityName}, entityId=${entityId}`);
}
});
this.findAndFixIssues(`
SELECT
id, entityId
FROM
entity_changes
JOIN ${entityName} ON entityId = ${key}
WHERE
entity_changes.isErased = 1
AND entity_changes.entityName = '${entityName}'`,
({id, entityId}) => {
if (this.autoFix) {
sql.execute(`DELETE FROM ${entityName} WHERE ${key} = ?`, [entityId]);
this.reloadNeeded = true;
logFix(`Erasing entityName=${entityName}, entityId=${entityId} since entity change id=${id} has it as erased.`);
} else {
logError(`Entity change id=${id} has entityName=${entityName}, entityId=${entityId} as erased, but it's not.`);
}
});
}
findEntityChangeIssues() {
@@ -625,14 +603,14 @@ class ConsistencyChecks {
this.fixedIssues = false;
this.reloadNeeded = false;
this.findEntityChangeIssues();
this.findBrokenReferenceIssues();
this.findExistencyIssues();
this.findLogicIssues();
this.findEntityChangeIssues();
this.findWronglyNamedAttributes();
this.findSyncIssues();

View File

@@ -54,7 +54,7 @@ function getYearNote(dateStr, rootNote) {
rootNote = getRootCalendarNote();
}
const yearStr = dateStr.trim().substr(0, 4);
const yearStr = dateStr.substr(0, 4);
let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr);
@@ -138,8 +138,6 @@ function getDateNoteTitle(rootNote, dayNumber, dateObj) {
/** @returns {Note} */
function getDateNote(dateStr) {
dateStr = dateStr.trim().substr(0, 10);
let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
if (dateNote) {

View File

@@ -50,13 +50,7 @@ function exportToZip(taskContext, branch, format, res) {
}
function getDataFileName(note, baseFileName, existingFileNames) {
let fileName = baseFileName;
if (fileName.length > 30) {
fileName = fileName.substr(0, 30);
}
let existingExtension = path.extname(fileName).toLowerCase();
const existingExtension = path.extname(baseFileName).toLowerCase();
let newExtension;
// following two are handled specifically since we always want to have these extensions no matter the automatic detection
@@ -74,12 +68,13 @@ function exportToZip(taskContext, branch, format, res) {
newExtension = null;
}
else {
if (note.mime?.toLowerCase()?.trim() === "image/jpg") {
newExtension = 'jpg';
}
else {
newExtension = mimeTypes.extension(note.mime) || "dat";
}
newExtension = mimeTypes.extension(note.mime) || "dat";
}
let fileName = baseFileName;
if (fileName.length > 30) {
fileName = fileName.substr(0, 30);
}
// if the note is already named with extension (e.g. "jquery"), then it's silly to append exact same extension again

View File

@@ -3,10 +3,6 @@ const sanitizeHtml = require('sanitize-html');
// intended mainly as protection against XSS via import
// secondarily it (partly) protects against "CSS takeover"
function sanitize(dirtyHtml) {
if (!dirtyHtml) {
return dirtyHtml;
}
// avoid H1 per https://github.com/zadam/trilium/issues/1552
// demote H1, and if that conflicts with existing H2, demote that, etc
const transformTags = {};

View File

@@ -123,11 +123,7 @@ function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch,
}
async function shrinkImage(buffer, originalName) {
let jpegQuality = optionService.getOptionInt('imageJpegQuality');
if (jpegQuality < 10 || jpegQuality > 100) {
jpegQuality = 75;
}
const jpegQuality = optionService.getOptionInt('imageJpegQuality');
let finalImageBuffer;
try {

View File

@@ -51,7 +51,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
throw new Error("Unrecognized OPML version " + opmlVersion);
}
content = htmlSanitizer.sanitize(content || "");
content = htmlSanitizer.sanitize(content);
const {note} = noteService.createNewNote({
parentNoteId,

View File

@@ -240,15 +240,13 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
if (noteMeta && noteMeta.isClone) {
if (!becca.getBranchFromChildAndParent(noteId, parentNoteId)) {
new Branch({
noteId,
parentNoteId,
isExpanded: noteMeta.isExpanded,
prefix: noteMeta.prefix,
notePosition: noteMeta.notePosition
}).save();
}
new Branch({
noteId,
parentNoteId,
isExpanded: noteMeta.isExpanded,
prefix: noteMeta.prefix,
notePosition: noteMeta.notePosition
}).save();
return;
}
@@ -353,30 +351,8 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
let note = becca.getNote(noteId);
const isProtected = importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable();
if (note) {
// only skeleton was created because of altered order of cloned notes in ZIP, we need to update
// https://github.com/zadam/trilium/issues/2440
if (note.type === undefined) {
note.type = type;
note.mime = mime;
note.title = noteTitle;
note.isProtected = isProtected;
note.save();
}
note.setContent(content);
if (!becca.getBranchFromChildAndParent(noteId, parentNoteId)) {
new Branch({
noteId,
parentNoteId,
isExpanded: noteMeta.isExpanded,
prefix: noteMeta.prefix,
notePosition: noteMeta.notePosition
}).save();
}
}
else {
({note} = noteService.createNewNote({
@@ -391,7 +367,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
// root notePosition should be ignored since it relates to original document
// now import root should be placed after existing notes into new parent
notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined,
isProtected: isProtected,
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
}));
createdNoteIds[note.noteId] = true;

View File

@@ -118,7 +118,6 @@ function createNewNote(params) {
note.setContent(params.content);
const branch = new Branch({
branchId: params.branchId,
noteId: note.noteId,
parentNoteId: params.parentNoteId,
notePosition: params.notePosition !== undefined ? params.notePosition : getNewNotePosition(params.parentNoteId),
@@ -360,7 +359,7 @@ function downloadImages(noteId, content) {
// which upon the download of all the images will update the note if the links have not been fixed before
sql.transactional(() => {
const imageNotes = becca.getNotes(Object.values(imageUrlToNoteIdMapping), true);
const imageNotes = becca.getNotes(Object.values(imageUrlToNoteIdMapping));
const origNote = becca.getNote(noteId);
@@ -541,7 +540,7 @@ function deleteBranch(branch, deleteId, taskContext) {
branch.markAsDeleted(deleteId);
const note = branch.getNote();
const notDeletedBranches = note.getParentBranches();
const notDeletedBranches = note.getBranches();
if (notDeletedBranches.length === 0) {
for (const childBranch of note.getChildBranches()) {
@@ -733,26 +732,23 @@ function eraseAttributes(attributeIdsToErase) {
}
function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) {
// this is important also so that the erased entity changes are sent to the connected clients
sql.transactional(() => {
if (eraseEntitiesAfterTimeInSeconds === null) {
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds');
}
if (eraseEntitiesAfterTimeInSeconds === null) {
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds');
}
const cutoffDate = new Date(Date.now() - eraseEntitiesAfterTimeInSeconds * 1000);
const cutoffDate = new Date(Date.now() - eraseEntitiesAfterTimeInSeconds * 1000);
const noteIdsToErase = sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
const noteIdsToErase = sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
eraseNotes(noteIdsToErase);
eraseNotes(noteIdsToErase);
const branchIdsToErase = sql.getColumn("SELECT branchId FROM branches WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
const branchIdsToErase = sql.getColumn("SELECT branchId FROM branches WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
eraseBranches(branchIdsToErase);
eraseBranches(branchIdsToErase);
const attributeIdsToErase = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
const attributeIdsToErase = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
eraseAttributes(attributeIdsToErase);
});
eraseAttributes(attributeIdsToErase);
}
function eraseNotesWithDeleteId(deleteId) {
@@ -789,7 +785,7 @@ function duplicateSubtree(origNoteId, newParentNoteId) {
const origNote = becca.notes[origNoteId];
// might be null if orig note is not in the target newParentNoteId
const origBranch = origNote.getParentBranches().find(branch => branch.parentNoteId === newParentNoteId);
const origBranch = origNote.getBranches().find(branch => branch.parentNoteId === newParentNoteId);
const noteIdMapping = getNoteIdMapping(origNote);

View File

@@ -1,16 +1,7 @@
const becca = require('../becca/becca');
const sql = require("./sql.js");
function getOption(name) {
let option;
if (becca.loaded) {
option = becca.getOption(name);
}
else {
// e.g. in initial sync becca is not loaded because DB is not initialized
option = sql.getRow("SELECT * FROM options WHERE name = ?", name);
}
const option = require('../becca/becca').getOption(name);
if (!option) {
throw new Error(`Option "${name}" doesn't exist`);
@@ -48,12 +39,12 @@ function getOptionBool(name) {
}
function setOption(name, value) {
const option = becca.getOption(name);
if (value === true || value === false) {
value = value.toString();
}
const option = becca.getOption(name);
if (option) {
option.value = value;

View File

@@ -70,7 +70,6 @@ const defaultOptions = [
{ name: 'imageMaxWidthHeight', value: '2000', isSynced: true },
{ name: 'imageJpegQuality', value: '75', isSynced: true },
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
{ name: 'vimKeymapEnabled', value: 'false', 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-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 },

View File

@@ -23,18 +23,20 @@ class AttributeExistsExp extends Expression {
for (const attr of attrs) {
const note = attr.note;
if (attr.isInheritable) {
resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated());
}
else if (note.isTemplate()) {
resultNoteSet.addAll(note.getTemplatedNotes());
}
else {
resultNoteSet.add(note);
if (inputNoteSet.hasNoteId(note.noteId)) {
if (attr.isInheritable) {
resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated());
}
else if (note.isTemplate()) {
resultNoteSet.addAll(note.getTemplatedNotes());
}
else {
resultNoteSet.add(note);
}
}
}
return resultNoteSet.intersection(inputNoteSet);
return resultNoteSet;
}
}

View File

@@ -11,7 +11,7 @@ const RelationWhereExp = require('../expressions/relation_where');
const PropertyComparisonExp = require('../expressions/property_comparison');
const AttributeExistsExp = require('../expressions/attribute_exists');
const LabelComparisonExp = require('../expressions/label_comparison');
const NoteFlatTextExp = require('../expressions/note_flat_text.js');
const BeccaFlatTextExp = require('../expressions/note_cache_flat_text');
const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext');
const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit');
@@ -31,13 +31,13 @@ function getFulltext(tokens, searchContext) {
if (!searchContext.fastSearch) {
return new OrExp([
new NoteFlatTextExp(tokens),
new BeccaFlatTextExp(tokens),
new NoteContentProtectedFulltextExp('*=*', tokens),
new NoteContentUnprotectedFulltextExp('*=*', tokens)
]);
}
else {
return new NoteFlatTextExp(tokens);
return new BeccaFlatTextExp(tokens);
}
}

View File

@@ -34,7 +34,6 @@ function getHiddenRoot() {
if (!hidden) {
hidden = noteService.createNewNote({
branchId: 'hidden',
noteId: 'hidden',
title: 'hidden',
type: 'text',
@@ -207,12 +206,11 @@ function getShareRoot() {
if (!shareRoot) {
shareRoot = noteService.createNewNote({
branchId: 'share',
noteId: 'share',
title: 'Shared notes',
title: 'share',
type: 'text',
content: '',
parentNoteId: 'root'
parentNoteId: getHiddenRoot().noteId
}).note;
}
@@ -225,7 +223,7 @@ function createMissingSpecialNotes() {
getSinglesNoteRoot();
getSinglesNoteRoot();
getGlobalNoteMap();
// share root is not automatically created since it's visible in the tree and many won't need it/use it
getShareRoot();
const hidden = getHiddenRoot();
@@ -240,6 +238,5 @@ module.exports = {
saveSqlConsole,
createSearchNote,
saveSearchNote,
createMissingSpecialNotes,
getShareRoot
createMissingSpecialNotes
};

View File

@@ -2,8 +2,6 @@
/**
* @module sql
*
* TODO: some methods (like getValue()) could use raw rows
*/
const log = require('./log');
@@ -91,7 +89,13 @@ function getRowOrNull(query, params = []) {
}
function getValue(query, params = []) {
return wrap(query, s => s.pluck().get(params));
const row = getRowOrNull(query, params);
if (!row) {
return null;
}
return row[Object.keys(row)[0]];
}
// smaller values can result in better performance due to better usage of statement cache
@@ -140,17 +144,32 @@ function iterateRows(query, params = []) {
function getMap(query, params = []) {
const map = {};
const results = getRawRows(query, params);
const results = getRows(query, params);
for (const row of results) {
map[row[0]] = row[1];
const keys = Object.keys(row);
map[row[keys[0]]] = row[keys[1]];
}
return map;
}
function getColumn(query, params = []) {
return wrap(query, s => s.pluck().all(params));
const list = [];
const result = getRows(query, params);
if (result.length === 0) {
return list;
}
const key = Object.keys(result[0])[0];
for (const row of result) {
list.push(row[key]);
}
return list;
}
function execute(query, params = []) {

View File

@@ -94,7 +94,7 @@ async function createInitialDatabase(username, password, theme) {
log.info("Importing demo content ...");
const dummyTaskContext = new TaskContext("no-progress-reporting", 'import', false);
const dummyTaskContext = new TaskContext("initial-demo-import", 'import', false);
const zipImportService = require("./import/zip");
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);

View File

@@ -371,10 +371,7 @@ function getLastSyncedPull() {
function setLastSyncedPull(entityChangeId) {
const lastSyncedPullOption = becca.getOption('lastSyncedPull');
if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded
lastSyncedPullOption.value = entityChangeId + '';
}
lastSyncedPullOption.value = entityChangeId + '';
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPull']);
@@ -392,10 +389,7 @@ function setLastSyncedPush(entityChangeId) {
ws.setLastSyncedPush(entityChangeId);
const lastSyncedPushOption = becca.getOption('lastSyncedPush');
if (lastSyncedPushOption) { // might be null in initial sync when becca is not loaded
lastSyncedPushOption.value = entityChangeId + '';
}
lastSyncedPushOption.value = entityChangeId + '';
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPush']);

View File

@@ -34,7 +34,7 @@ class TaskContext {
increaseProgressCount() {
this.progressCount++;
if (Date.now() - this.lastSentCountTs >= 300 && this.taskId !== 'no-progress-reporting') {
if (Date.now() - this.lastSentCountTs >= 300 && this.taskId !== 'initial-demo-import') {
this.lastSentCountTs = Date.now();
ws.sendMessageToAllClients({

View File

@@ -41,15 +41,10 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
const existing = getExistingBranch(parentNoteId, childNoteId);
console.log("BBBB", existing);
if (existing && (branchId === null || existing.branchId !== branchId)) {
const parentNote = becca.getNote(parentNoteId);
const childNote = becca.getNote(childNoteId);
return {
success: false,
message: `Note "${childNote.title}" note already exists in the "${parentNote.title}".`
message: 'This note already exists in the target.'
};
}
@@ -64,12 +59,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
}
function getExistingBranch(parentNoteId, childNoteId) {
const branchId = sql.getValue(`
SELECT branchId
FROM branches
WHERE noteId = ?
AND parentNoteId = ?
AND isDeleted = 0`, [childNoteId, parentNoteId]);
const branchId = sql.getValue('SELECT branchId FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0', [childNoteId, parentNoteId]);
return becca.getBranch(branchId);
}
@@ -183,7 +173,7 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
let position = 10;
for (const note of notes) {
const branch = note.getParentBranches().find(b => b.parentNoteId === parentNoteId);
const branch = note.getBranches().find(b => b.parentNoteId === parentNoteId);
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
[position, branch.branchId]);

View File

@@ -16,10 +16,7 @@ let mainWindow;
let setupWindow;
async function createExtraWindow(notePath, hoistedNoteId = 'root') {
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
const {BrowserWindow} = require('electron');
const win = new BrowserWindow({
width: 1000,
height: 800,
@@ -28,7 +25,7 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
enableRemoteModule: true,
nodeIntegration: true,
contextIsolation: false,
spellcheck: spellcheckEnabled
spellcheck: optionService.getOptionBool('spellCheckEnabled')
},
frame: optionService.getOptionBool('nativeTitleBarVisible'),
icon: getIcon()
@@ -36,8 +33,6 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
win.setMenuBarVisibility(false);
win.loadURL('http://127.0.0.1:' + await port + '/?extra=1&extraHoistedNoteId=' + hoistedNoteId + '#' + notePath);
configureWebContents(win.webContents, spellcheckEnabled);
}
ipcMain.on('create-extra-window', (event, arg) => {
@@ -64,7 +59,6 @@ async function createMainWindow() {
height: mainWindowState.height,
title: 'Trilium Notes',
webPreferences: {
enableRemoteModule: true,
nodeIntegration: true,
contextIsolation: false,
spellcheck: spellcheckEnabled
@@ -79,10 +73,8 @@ async function createMainWindow() {
mainWindow.loadURL('http://127.0.0.1:' + await port);
mainWindow.on('closed', () => mainWindow = null);
configureWebContents(mainWindow.webContents, spellcheckEnabled);
}
const {webContents} = mainWindow;
function configureWebContents(webContents, spellcheckEnabled) {
require("@electron/remote/main").enable(webContents);
webContents.on('new-window', (e, url) => {

View File

@@ -1,105 +0,0 @@
const {JSDOM} = require("jsdom");
const shaca = require("./shaca/shaca");
function getContent(note) {
let content = note.getContent();
let header = '';
let isEmpty = false;
if (note.type === 'text') {
const document = new JSDOM(content || "").window.document;
isEmpty = document.body.textContent.trim().length === 0
&& document.querySelectorAll("img").length === 0;
if (!isEmpty) {
for (const linkEl of document.querySelectorAll("a")) {
const href = linkEl.getAttribute("href");
if (href?.startsWith("#")) {
const notePathSegments = href.split("/");
const noteId = notePathSegments[notePathSegments.length - 1];
const linkedNote = shaca.getNote(noteId);
if (linkedNote) {
linkEl.setAttribute("href", linkedNote.shareId);
linkEl.classList.add("type-" + linkedNote.type);
}
else {
linkEl.removeAttribute("href");
}
}
}
content = document.body.innerHTML;
if (content.includes(`<span class="math-tex">`)) {
header += `
<script src="../../libraries/katex/katex.min.js"></script>
<link rel="stylesheet" href="../../libraries/katex/katex.min.css">
<script src="../../libraries/katex/auto-render.min.js"></script>
<script src="../../libraries/katex/mhchem.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.getElementById('content'));
});
</script>`;
}
}
}
else if (note.type === 'code') {
if (!content?.trim()) {
isEmpty = true;
}
else {
const document = new JSDOM().window.document;
const preEl = document.createElement('pre');
preEl.appendChild(document.createTextNode(content));
content = preEl.outerHTML;
}
}
else if (note.type === 'mermaid') {
content = `
<div class="mermaid">${content}</div>
<hr>
<details>
<summary>Chart source</summary>
<pre>${content}</pre>
</details>`
header += `<script src="../../libraries/mermaid.min.js"></script>`;
}
else if (note.type === 'image') {
content = `<img src="api/images/${note.noteId}/${note.title}?${note.utcDateModified}">`;
}
else if (note.type === 'file') {
if (note.mime === 'application/pdf') {
content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`
}
else {
content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
}
}
else if (note.type === 'book') {
isEmpty = true;
}
else {
content = '<p>This note type cannot be displayed.</p>';
}
return {
header,
content,
isEmpty
};
}
module.exports = {
getContent
};

View File

@@ -1,68 +1,147 @@
const shaca = require("./shaca/shaca");
const shacaLoader = require("./shaca/shaca_loader");
const shareRoot = require("./share_root");
const contentRenderer = require("./content_renderer.js");
const {JSDOM} = require("jsdom");
function getSharedSubTreeRoot(note) {
function getSubRoot(note) {
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
// share root itself is not shared
return null;
}
// every path leads to share root, but which one to choose?
// for sake of simplicity URLs are not note paths
const parentNote = note.getParentNotes()[0];
if (parentNote.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
return note;
}
return getSharedSubTreeRoot(parentNote);
return getSubRoot(parentNote);
}
const NO_CONTENT = '<p>This note has no content.</p>';
function getChildrenList(note) {
if (note.hasChildren()) {
const document = new JSDOM().window.document;
const ulEl = document.createElement("ul");
for (const childNote of note.getChildNotes()) {
const li = document.createElement("li");
const link = document.createElement("a");
link.appendChild(document.createTextNode(childNote.title));
link.setAttribute("href", childNote.noteId);
li.appendChild(link);
ulEl.appendChild(li);
}
return '<p>Child notes:</p>' + ulEl.outerHTML;
}
else {
return '';
}
}
function getContent(note) {
let content = note.getContent();
if (note.type === 'text') {
const document = new JSDOM(content || "").window.document;
const isEmpty = document.body.textContent.trim().length === 0
&& document.querySelectorAll("img").length === 0;
if (isEmpty) {
content = NO_CONTENT + getChildrenList(note);
}
else {
for (const linkEl of document.querySelectorAll("a")) {
const href = linkEl.getAttribute("href");
if (href?.startsWith("#")) {
const notePathSegments = href.split("/");
linkEl.setAttribute("href", notePathSegments[notePathSegments.length - 1]);
}
}
content = document.body.innerHTML;
}
}
else if (note.type === 'code') {
if (!content?.trim()) {
content = NO_CONTENT + getChildrenList(note);
}
else {
const document = new JSDOM().window.document;
const preEl = document.createElement('pre');
preEl.appendChild(document.createTextNode(content));
content = preEl.outerHTML;
}
}
else if (note.type === 'image') {
content = `<img src="api/images/${note.noteId}/${note.title}?${note.utcDateModified}">`;
}
else if (note.type === 'file') {
content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
}
else if (note.type === 'book') {
content = getChildrenList(note);
}
else {
content = '<p>This note type cannot be displayed.</p>' + getChildrenList(note);
}
return `<div class="type-${note.type}">${content}</content>`;
}
function register(router) {
router.get('/share/:shareId', (req, res, next) => {
const {shareId} = req.params;
router.get('/share/:noteId', (req, res, next) => {
const {noteId} = req.params;
shacaLoader.ensureLoad();
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
if (noteId in shaca.notes) {
const note = shaca.notes[noteId];
if (note) {
const {header, content, isEmpty} = contentRenderer.getContent(note);
const content = getContent(note);
const subRoot = getSharedSubTreeRoot(note);
const subRoot = getSubRoot(note);
res.render("share/page", {
res.render("share", {
note,
header,
content,
isEmpty,
subRoot
});
}
else {
res.status(404).render("share/404");
res.send("FFF");
}
});
router.get('/share/api/notes/:noteId', (req, res, next) => {
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
const image = shaca.getNote(req.params.noteId);
if (!image) {
return res.sendStatus(404);
}
else if (image.type !== 'image') {
return res.sendStatus(400);
}
res.set('Content-Type', image.mime);
res.send(image.getContent());
});
router.get('/share/api/notes/:noteId/:download', (req, res, next) => {
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
return res.status(404).send(`Note ${noteId} not found`);
}
res.json(note.getPojoWithAttributes());
});
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
return res.status(404).send(`Note ${noteId} not found`);
return res.status(404).send(`Note ${noteId} doesn't exist.`);
}
const utils = require("../services/utils");
@@ -76,36 +155,6 @@ function register(router) {
res.send(note.getContent());
});
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
const image = shaca.getNote(req.params.noteId);
if (!image) {
return res.status(404).send(`Note ${noteId} not found`);
}
else if (image.type !== 'image') {
return res.status(400).send("Requested note is not an image");
}
res.set('Content-Type', image.mime);
res.send(image.getContent());
});
// used for PDF viewing
router.get('/share/api/notes/:noteId/view', (req, res, next) => {
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
return res.status(404).send(`Note ${noteId} not found`);
}
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);
res.send(note.getContent());
});
}
module.exports = {

View File

@@ -39,10 +39,6 @@ class Attribute extends AbstractEntity {
linkedChildNote.parents = linkedChildNote.parents.filter(parentNote => parentNote.noteId !== this.noteId);
}
}
if (this.type === 'label' && this.name === 'shareAlias' && this.value.trim()) {
this.shaca.aliasToNote[this.value.trim()] = this.note;
}
}
get isAffectingSubtree() {
@@ -89,18 +85,6 @@ class Attribute extends AbstractEntity {
return this.shaca.getNote(this.value);
}
getPojo() {
return {
attributeId: this.attributeId,
noteId: this.noteId,
type: this.type,
name: this.name,
position: this.position,
value: this.value,
isInheritable: this.isInheritable
};
}
}
module.exports = Attribute;

View File

@@ -1,6 +1,7 @@
"use strict";
const AbstractEntity = require('./abstract_entity');
const shareRoot = require("../../share_root");
class Branch extends AbstractEntity {
constructor([branchId, noteId, parentNoteId, prefix, isExpanded]) {
@@ -17,6 +18,10 @@ class Branch extends AbstractEntity {
/** @param {boolean} */
this.isExpanded = !!isExpanded;
if (this.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
return;
}
const childNote = this.childNote;
const parentNote = this.parentNote;

View File

@@ -40,6 +40,9 @@ class Note extends AbstractEntity {
this.targetRelations = [];
this.shaca.notes[this.noteId] = this;
/** @param {Note[]|null} */
this.ancestorCache = null;
}
getParentBranches() {
@@ -90,6 +93,36 @@ class Note extends AbstractEntity {
}
}
/** @returns {*} */
getJsonContent() {
const content = this.getContent();
if (!content || !content.trim()) {
return null;
}
return JSON.parse(content);
}
/** @returns {boolean} true if this note is of application/json content type */
isJson() {
return this.mime === "application/json";
}
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
isJavaScript() {
return (this.type === "code" || this.type === "file")
&& (this.mime.startsWith("application/javascript")
|| this.mime === "application/x-javascript"
|| this.mime === "text/javascript");
}
/** @returns {boolean} true if this note is HTML */
isHtml() {
return ["code", "file", "render"].includes(this.type)
&& this.mime === "text/html";
}
/** @returns {boolean} true if the note has string content (not binary) */
isStringNote() {
return utils.isStringNote(this.type, this.mime);
@@ -185,6 +218,16 @@ class Note extends AbstractEntity {
return !!this.getAttributes().find(attr => attr.type === type && attr.name === name);
}
getAttributeCaseInsensitive(type, name, value) {
name = name.toLowerCase();
value = value ? value.toLowerCase() : null;
return this.getAttributes().find(
attr => attr.type === type
&& attr.name.toLowerCase() === name
&& (!value || attr.value.toLowerCase() === value));
}
getRelationTarget(name) {
const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name);
@@ -401,27 +444,110 @@ class Note extends AbstractEntity {
return !!this.targetRelations.find(rel => rel.name === 'template');
}
/** @return {Note[]} */
getSubtreeNotesIncludingTemplated() {
const arr = [[this]];
for (const childNote of this.children) {
arr.push(childNote.getSubtreeNotesIncludingTemplated());
}
for (const targetRelation of this.targetRelations) {
if (targetRelation.name === 'template') {
const note = targetRelation.note;
if (note) {
arr.push(note.getSubtreeNotesIncludingTemplated());
}
}
}
return arr.flat();
}
/** @return {Note[]} */
getSubtreeNotes(includeArchived = true) {
const noteSet = new Set();
function addSubtreeNotesInner(note) {
if (!includeArchived && note.isArchived) {
return;
}
noteSet.add(note);
for (const childNote of note.children) {
addSubtreeNotesInner(childNote);
}
}
addSubtreeNotesInner(this);
return Array.from(noteSet);
}
/** @return {String[]} */
getSubtreeNoteIds() {
return this.getSubtreeNotes().map(note => note.noteId);
}
getDescendantNoteIds() {
return this.getSubtreeNoteIds();
}
getAncestors() {
if (!this.ancestorCache) {
const noteIds = new Set();
this.ancestorCache = [];
for (const parent of this.parents) {
if (!noteIds.has(parent.noteId)) {
this.ancestorCache.push(parent);
noteIds.add(parent.noteId);
}
for (const ancestorNote of parent.getAncestors()) {
if (!noteIds.has(ancestorNote.noteId)) {
this.ancestorCache.push(ancestorNote);
noteIds.add(ancestorNote.noteId);
}
}
}
}
return this.ancestorCache;
}
getTargetRelations() {
return this.targetRelations;
}
get shareId() {
const sharedAlias = this.getOwnedLabelValue("shareAlias");
/** @return {Note[]} - returns only notes which are templated, does not include their subtrees
* in effect returns notes which are influenced by note's non-inheritable attributes */
getTemplatedNotes() {
const arr = [this];
return sharedAlias || this.noteId;
for (const targetRelation of this.targetRelations) {
if (targetRelation.name === 'template') {
const note = targetRelation.note;
if (note) {
arr.push(note);
}
}
}
return arr;
}
getPojoWithAttributes() {
return {
noteId: this.noteId,
title: this.title,
type: this.type,
mime: this.mime,
utcDateModified: this.utcDateModified,
attributes: this.getAttributes().map(attr => attr.getPojo()),
parentNoteIds: this.parents.map(parentNote => parentNote.noteId),
childNoteIds: this.children.map(child => child.noteId)
};
/**
* @param ancestorNoteId
* @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
*/
isDescendantOfNote(ancestorNoteId) {
const notePaths = this.getAllNotePaths();
return notePaths.some(path => path.includes(ancestorNoteId));
}
}

View File

@@ -14,8 +14,6 @@ class Shaca {
this.childParentToBranch = {};
/** @type {Object.<String, Attribute>} */
this.attributes = {};
/** @type {Object.<String, String>} */
this.aliasToNote = {};
this.loaded = false;
}
@@ -24,10 +22,6 @@ class Shaca {
return this.notes[noteId];
}
hasNote(noteId) {
return noteId in this.notes;
}
getNotes(noteIds, ignoreMissing = false) {
const filteredNotes = [];

View File

@@ -34,40 +34,31 @@ function load() {
const noteIdStr = noteIds.map(noteId => `'${noteId}'`).join(",");
const rawNoteRows = sql.getRawRows(`
SELECT noteId, title, type, mime, utcDateModified
FROM notes
WHERE isDeleted = 0
AND noteId IN (${noteIdStr})`);
for (const row of rawNoteRows) {
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, utcDateModified FROM notes WHERE isDeleted = 0 AND noteId IN (${noteIdStr})`)) {
new Note(row);
}
const rawBranchRows = sql.getRawRows(`
SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified
FROM branches
WHERE isDeleted = 0
AND parentNoteId IN (${noteIdStr})
ORDER BY notePosition`);
for (const row of rawBranchRows) {
for (const row of sql.getRawRows(`SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0 AND noteId IN (${noteIdStr}) ORDER BY notePosition`)) {
new Branch(row);
}
const rawAttributeRows = sql.getRawRows(`
const attributes = sql.getRawRows(`
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
FROM attributes
WHERE isDeleted = 0
AND noteId IN (${noteIdStr})`);
AND noteId IN (${noteIdStr})
AND (
(type = 'label' AND name IN ('archived'))
OR (type = 'relation' AND name IN ('imageLink', 'template'))
)`, []);
for (const row of rawAttributeRows) {
for (const row of attributes) {
new Attribute(row);
}
shaca.loaded = true;
log.info(`Shaca loaded ${rawNoteRows.length} notes, ${rawBranchRows.length} branches, ${rawAttributeRows.length} attributes took ${Date.now() - start}ms`);
log.info(`Shaca load took ${Date.now() - start}ms`);
}
function ensureLoad() {

View File

@@ -1,5 +1,6 @@
"use strict";
const log = require('../services/log');
const Database = require('better-sqlite3');
const dataDir = require('../services/data_dir');
@@ -15,20 +16,152 @@ const dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true });
});
});
function getRawRows(query, params = []) {
return dbConnection.prepare(query).raw().all(params);
const statementCache = {};
function stmt(sql) {
if (!(sql in statementCache)) {
statementCache[sql] = dbConnection.prepare(sql);
}
return statementCache[sql];
}
function getRow(query, params = []) {
return dbConnection.prepare(query).get(params);
return wrap(query, s => s.get(params));
}
function getRowOrNull(query, params = []) {
const all = getRows(query, params);
return all.length > 0 ? all[0] : null;
}
function getValue(query, params = []) {
const row = getRowOrNull(query, params);
if (!row) {
return null;
}
return row[Object.keys(row)[0]];
}
// smaller values can result in better performance due to better usage of statement cache
const PARAM_LIMIT = 100;
function getManyRows(query, params) {
let results = [];
while (params.length > 0) {
const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
params = params.slice(curParams.length);
const curParamsObj = {};
let j = 1;
for (const param of curParams) {
curParamsObj['param' + j++] = param;
}
let i = 1;
const questionMarks = curParams.map(() => ":param" + i++).join(",");
const curQuery = query.replace(/\?\?\?/g, questionMarks);
const statement = curParams.length === PARAM_LIMIT
? stmt(curQuery)
: dbConnection.prepare(curQuery);
const subResults = statement.all(curParamsObj);
results = results.concat(subResults);
}
return results;
}
function getRows(query, params = []) {
return wrap(query, s => s.all(params));
}
function getRawRows(query, params = []) {
return wrap(query, s => s.raw().all(params));
}
function iterateRows(query, params = []) {
return stmt(query).iterate(params);
}
function getMap(query, params = []) {
const map = {};
const results = getRows(query, params);
for (const row of results) {
const keys = Object.keys(row);
map[row[keys[0]]] = row[keys[1]];
}
return map;
}
function getColumn(query, params = []) {
return dbConnection.prepare(query).pluck().all(params);
const list = [];
const result = getRows(query, params);
if (result.length === 0) {
return list;
}
const key = Object.keys(result[0])[0];
for (const row of result) {
list.push(row[key]);
}
return list;
}
function wrap(query, func) {
const startTimestamp = Date.now();
let result;
try {
result = func(stmt(query));
}
catch (e) {
if (e.message.includes("The database connection is not open")) {
// this often happens on killing the app which puts these alerts in front of user
// in these cases error should be simply ignored.
console.log(e.message);
return null
}
throw e;
}
const milliseconds = Date.now() - startTimestamp;
if (milliseconds >= 20) {
if (query.includes("WITH RECURSIVE")) {
log.info(`Slow recursive query took ${milliseconds}ms.`);
}
else {
log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
}
}
return result;
}
module.exports = {
getRawRows,
dbConnection,
getValue,
getRow,
getRowOrNull,
getRows,
getRawRows,
iterateRows,
getManyRows,
getMap,
getColumn
};

View File

@@ -61,7 +61,7 @@ async function start() {
const parentNoteId = getRandomNoteId();
const prefix = Math.random() > 0.8 ? "prefix" : null;
const result = await cloningService.cloneNoteToBranch(noteIdToClone, parentNoteId, prefix);
const result = await cloningService.cloneNoteToParent(noteIdToClone, parentNoteId, prefix);
console.log(`Cloning ${i}:`, result.success ? "succeeded" : "FAILED");
}

View File

@@ -41,8 +41,6 @@
<%- include('dialogs/delete_notes.ejs') %>
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.baseApiUrl = 'api/';
window.device = "desktop";
window.glob = {
@@ -81,7 +79,8 @@
<script src="libraries/jquery.hotkeys.js"></script>
<script src="libraries/autocomplete.jquery.min.js"></script>
<link href="libraries/autocomplete-theme-classic.css" rel="stylesheet">
<script src="libraries/autocomplete.js"></script>
<script src="libraries/dayjs.min.js"></script>

View File

@@ -10,9 +10,8 @@
<div class="modal-body">
<div class="form-group">
<label for="jump-to-note-autocomplete">Note</label>
<div class="input-group">
<input id="jump-to-note-autocomplete" class="form-control" placeholder="search for note by its name">
</div>
<div id="jump-to-note-autocomplete"></div>
</div>
</div>
<div class="modal-footer">

View File

@@ -105,8 +105,6 @@
<%- include('dialogs/confirm.ejs') %>
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.baseApiUrl = 'api/';
window.device = "mobile";
window.glob = {

View File

@@ -189,8 +189,6 @@
</div>
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
sourceId: ''
};

View File

@@ -2,18 +2,16 @@
<% if (activeNote.noteId === note.noteId) { %>
<strong><%= note.title %></strong>
<% } else { %>
<a class="type-<%= note.type %>" href="./<%= note.shareId %>"><%= note.title %></a>
<a href="./<%= note.noteId %>"><%= note.title %></a>
<% } %>
</p>
<% if (note.hasChildren()) { %>
<ul>
<% note.getChildNotes().forEach(function (childNote) { %>
<% if (!childNote.hasLabel("shareHiddenFromTree")) { %>
<li>
<%- include('tree_item', {note: childNote}) %>
<%- include('share-tree-item', {note: childNote}) %>
</li>
<% } %>
<% }) %>
</ul>
<% } %>

Some files were not shown because too many files have changed in this diff Show More