Compare commits

..

1 Commits

Author SHA1 Message Date
zadam
baed93e749 algolia v1 upgrade 2021-12-14 22:44:54 +01:00
90 changed files with 1329 additions and 12697 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

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.1-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;
}

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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,8 +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.",
"widget": "target of this relation will be executed and rendered as a widget in the sidebar"
}
};
@@ -296,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.cloneNoteTo(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

@@ -14,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>
@@ -27,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>

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;
@@ -36,14 +32,14 @@ body {
#title {
margin: 0;
padding: 20px 20px 0 20px;
padding: 10px 20px 0 20px;
}
#content {
padding: 20px;
}
img {
.type-image img {
max-width: 100%;
}
@@ -52,59 +48,40 @@ pre {
word-wrap: anywhere;
}
iframe.pdf-view {
width: 100%;
height: 800px;
}
#menuButton {
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;
}
#menuButton::after {
position: relative;
top: -2px;
left: 1px;
}
@media (max-width: 48em) {
#layout.navMenu #menu {
display: block;
margin-top: 40px;
}
#menuButton {
#layout.active #menu {
display: block;
}
#layout.navMenu #main {
#layout.active #main {
display: none;
}
#title {
padding-left: 60px;
}
#layout.navMenu #menuButton::after {
#layout.active #menuLink::after {
content: "«";
}
#menuButton::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

@@ -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

@@ -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);

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,8 +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: 'renderNote', isDangerous: true }
];
/** @returns {Note[]} */
@@ -99,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,
@@ -208,7 +186,6 @@ function sanitizeAttributeName(origName) {
module.exports = {
getNotesWithLabel,
getNotesWithLabelFast,
getNoteWithLabel,
createLabel,
createRelation,

View File

@@ -1 +1 @@
module.exports = { buildDate:"2021-12-24T23:05:10+01:00", buildRevision: "0217b1c85de9a2824e7f07d07a357064c5803383" };
module.exports = { buildDate:"2021-12-13T11:12:31+01:00", buildRevision: "d9550dd59b9b0dff0b229c400cdf6585abcb226a" };

View File

@@ -11,12 +11,6 @@ const becca = require("../becca/becca");
const beccaService = require("../becca/becca_service");
function cloneNoteToParent(noteId, parentBranchId, prefix) {
if (parentBranchId === 'share') {
const specialNotesService = require('./special_notes');
// share root note is created lazily
specialNotesService.getShareRoot();
}
const parentBranch = becca.getBranch(parentBranchId);
if (isNoteDeleted(noteId) || isNoteDeleted(parentBranch.noteId)) {

View File

@@ -258,8 +258,7 @@ class ConsistencyChecks {
FROM branches
WHERE noteId = ?
and parentNoteId = ?
and isDeleted = 0
ORDER BY utcDateCreated`, [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

@@ -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

@@ -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

@@ -351,19 +351,7 @@ 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);
}
else {
@@ -379,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),
@@ -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

@@ -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

@@ -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

@@ -59,7 +59,6 @@ async function createMainWindow() {
height: mainWindowState.height,
title: 'Trilium Notes',
webPreferences: {
enableRemoteModule: true,
nodeIntegration: true,
contextIsolation: false,
spellcheck: spellcheckEnabled

View File

@@ -1,98 +0,0 @@
const {JSDOM} = require("jsdom");
const NO_CONTENT = '<p>This note has no content.</p>';
const shaca = require("./shaca/shaca");
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("/");
const noteId = notePathSegments[notePathSegments.length - 1];
const linkedNote = shaca.getNote(noteId);
if (linkedNote) {
linkEl.setAttribute("href", linkedNote.shareId);
}
else {
linkEl.removeAttribute("href");
}
}
}
content = document.body.innerHTML;
}
}
else if (note.type === 'code' || note.type === 'mermaid') {
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') {
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') {
content = getChildrenList(note);
}
else {
content = '<p>This note type cannot be displayed.</p>' + getChildrenList(note);
}
return content;
}
module.exports = {
getContent
};

View File

@@ -1,46 +1,123 @@
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 content = contentRenderer.getContent(note);
const content = getContent(note);
const subRoot = getSharedSubTreeRoot(note);
const subRoot = getSubRoot(note);
res.render("share/page", {
res.render("share", {
note,
content,
subRoot
});
}
else {
res.status(404).render("share/404");
res.send("FFF");
}
});
@@ -48,10 +125,10 @@ function register(router) {
const image = shaca.getNote(req.params.noteId);
if (!image) {
return res.status(404).send("Not found");
return res.sendStatus(404);
}
else if (image.type !== 'image') {
return res.status(400).send("Requested note is not an image");
return res.sendStatus(400);
}
res.set('Content-Type', image.mime);
@@ -59,12 +136,12 @@ function register(router) {
res.send(image.getContent());
});
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
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(`Not found`);
return res.status(404).send(`Note ${noteId} doesn't exist.`);
}
const utils = require("../services/utils");
@@ -78,26 +155,6 @@ function register(router) {
res.send(note.getContent());
});
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(`Not found`);
}
const utils = require("../services/utils");
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
// res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
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() {

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,14 +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;
}
/**
* @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,44 +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 (
(type = 'label' AND name IN ('archived', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss'))
OR (type = 'relation' AND name IN ('imageLink', 'template', 'shareCss'))
(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

@@ -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 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>
<% } %>

87
src/views/share.ejs Normal file
View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="../favicon.ico">
<link href="../libraries/normalize.min.css" rel="stylesheet">
<link href="../stylesheets/share.css" rel="stylesheet">
<% if (note.type === 'text' || note.type === 'book') { %>
<link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
<% } %>
<title><%= note.title %></title>
</head>
<body>
<div id="layout">
<button id="menuLink"></button>
<div id="menu">
<%- include('share-tree-item', {note: subRoot, activeNote: note}) %>
</div>
<div id="main">
<h1 id="title"><%= note.title %></h1>
<div id="content">
<div class="ck-content"><%- content %></div>
</div>
</div>
</div>
<script>
(function (window, document) {
// we fetch the elements each time because docusaurus removes the previous
// element references on page navigation
function getElements() {
return {
layout: document.getElementById('layout'),
menu: document.getElementById('menu'),
menuLink: document.getElementById('menuLink')
};
}
function toggleClass(element, className) {
var classes = element.className.split(/\s+/);
var length = classes.length;
var i = 0;
for (; i < length; i++) {
if (classes[i] === className) {
classes.splice(i, 1);
break;
}
}
// The className is not found
if (length === classes.length) {
classes.push(className);
}
element.className = classes.join(' ');
}
function toggleAll() {
var active = 'active';
var elements = getElements();
toggleClass(elements.layout, active);
toggleClass(elements.menu, active);
toggleClass(elements.menuLink, active);
}
function handleEvent(e) {
var elements = getElements();
if (e.target.id === elements.menuLink.id) {
toggleAll();
e.preventDefault();
} else if (elements.menu.className.indexOf('active') !== -1) {
toggleAll();
}
}
document.addEventListener('click', handleEvent);
}(this, this.document));
</script>
</body>
</html>

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="../favicon.ico">
<title>Not found</title>
</head>
<body>
<h1>Not found</h1>
</body>
</html>

View File

@@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="../favicon.ico">
<% if (!note.hasLabel("shareOmitDefaultCss")) { %>
<link href="../libraries/normalize.min.css" rel="stylesheet">
<link href="../stylesheets/share.css" rel="stylesheet">
<% } %>
<% if (note.type === 'text' || note.type === 'book') { %>
<link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
<% } %>
<% for (const cssRelation of note.getRelations("shareCss")) { %>
<link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
<% } %>
<title><%= note.title %></title>
</head>
<body>
<div id="layout">
<div id="main">
<h1 id="title"><%= note.title %></h1>
<div id="content" class="note-<%= note.type %> <% if (note.type === 'text') { %>ck-content<% } %>">
<%- content %>
</div>
</div>
<% if (subRoot.hasChildren()) { %>
<button id="menuButton"></button>
<nav id="menu">
<%- include('tree_item', {note: subRoot, activeNote: note}) %>
</nav>
<% } %>
</div>
<script>
(function () {
const menuButton = document.getElementById('menuButton');
const layout = document.getElementById('layout');
menuButton.addEventListener('click', () => layout.classList.toggle('navMenu'));
}());
</script>
</body>
</html>

View File

@@ -10,6 +10,5 @@ module.exports = {
path: path.resolve(__dirname, 'src/public/app-dist'),
filename: 'desktop.js'
},
devtool: 'source-map',
target: 'electron-renderer'
};
devtool: 'source-map'
};

View File

@@ -10,6 +10,5 @@ module.exports = {
path: path.resolve(__dirname, 'src/public/app-dist'),
filename: 'mobile.js'
},
devtool: 'source-map',
target: 'electron-renderer'
};
devtool: 'source-map'
};

View File

@@ -10,6 +10,5 @@ module.exports = {
path: path.resolve(__dirname, 'src/public/app-dist'),
filename: 'setup.js'
},
devtool: 'source-map',
target: 'electron-renderer'
};
devtool: 'source-map'
};