Merge branch 'master' into dev

# Conflicts:
#	src/services/app_info.js
This commit is contained in:
zadam 2023-03-08 07:54:23 +01:00
commit 6c4df5110e
34 changed files with 350 additions and 118 deletions

View File

@ -10,6 +10,7 @@ Trilium Notes 是一个层次化的笔记应用程序,专注于建立大型个
Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/). Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/).
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/> <img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
<img src="https://signmyrocket.com//uploads/2b2a523cd0c0e76cdbba95a89a9636b2_1676971281.jpg" alt="Trilium Notes supports Ukraine!" width="600"/>
## 特性 ## 特性

View File

@ -11,6 +11,8 @@ Ukraine is currently defending itself from Russian aggression, please consider [
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/> <img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
<img src="https://signmyrocket.com//uploads/2b2a523cd0c0e76cdbba95a89a9636b2_1676971281.jpg" alt="Trilium Notes supports Ukraine!" width="600"/>
## Features ## Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://github.com/zadam/trilium/wiki/Cloning-notes)) * Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://github.com/zadam/trilium/wiki/Cloning-notes))
@ -33,6 +35,8 @@ Ukraine is currently defending itself from Russian aggression, please consider [
* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) and [Markdown import & export](https://github.com/zadam/trilium/wiki/Markdown) * [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) and [Markdown import & export](https://github.com/zadam/trilium/wiki/Markdown)
* [Web Clipper](https://github.com/zadam/trilium/wiki/Web-clipper) for easy saving of web content * [Web Clipper](https://github.com/zadam/trilium/wiki/Web-clipper) for easy saving of web content
Check out [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more.
## Builds ## Builds
Trilium is provided as either desktop application (Linux and Windows) or web application hosted on your server (Linux). Mac OS desktop build is available, but it is [unsupported](https://github.com/zadam/trilium/wiki/FAQ#mac-os-support). Trilium is provided as either desktop application (Linux and Windows) or web application hosted on your server (Linux). Mac OS desktop build is available, but it is [unsupported](https://github.com/zadam/trilium/wiki/FAQ#mac-os-support).

View File

@ -10,6 +10,7 @@ Trilium Notes это приложение для заметок с иера
Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/). Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/).
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/> <img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
<img src="https://signmyrocket.com//uploads/2b2a523cd0c0e76cdbba95a89a9636b2_1676971281.jpg" alt="Trilium Notes supports Ukraine!" width="600"/>
## Возможности ## Возможности

View File

@ -1,13 +1,162 @@
UPDATE etapi_tokens SET tokenHash = 'API token hash value'; UPDATE etapi_tokens SET tokenHash = 'API token hash value';
UPDATE notes SET title = 'title' WHERE title NOT IN ('root', '_hidden', '_share'); UPDATE notes SET title = 'title' WHERE noteId != 'root' AND noteId NOT LIKE '\_%' ESCAPE '\';
UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL; UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL;
UPDATE note_revisions SET title = 'title'; UPDATE note_revisions SET title = 'title';
UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL; UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL;
UPDATE note_ancillary_contents SET content = 'text' WHERE content IS NOT NULL; UPDATE note_ancillary_contents SET content = 'text' WHERE content IS NOT NULL;
UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'workspaceInbox', 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteContentChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon'); UPDATE attributes SET name = 'name', value = 'value'
UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN ('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'workspaceInbox', 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteContentChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon'); WHERE type = 'label'
AND name NOT IN ('inbox',
'disableVersioning',
'calendarRoot',
'archived',
'excludeFromExport',
'disableInclusion',
'appCss',
'appTheme',
'hidePromotedAttributes',
'readOnly',
'autoReadOnlyDisabled',
'cssClass',
'iconClass',
'keyboardShortcut',
'run',
'runOnInstance',
'runAtHour',
'customRequestHandler',
'customResourceProvider',
'widget',
'noteInfoWidgetDisabled',
'linkMapWidgetDisabled',
'noteRevisionsWidgetDisabled',
'whatLinksHereWidgetDisabled',
'similarNotesWidgetDisabled',
'workspace',
'workspaceIconClass',
'workspaceTabBackgroundColor',
'searchHome',
'workspaceInbox',
'workspaceSearchHome',
'sqlConsoleHome',
'datePattern',
'pageSize',
'viewType',
'mapRootNoteId',
'bookmarkFolder',
'sorted',
'top',
'fullContentWidth',
'shareHiddenFromTree',
'shareAlias',
'shareOmitDefaultCss',
'shareRoot',
'internalLink',
'imageLink',
'relationMapLink',
'includeMapLink',
'runOnNoteCreation',
'runOnNoteTitleChange',
'runOnNoteContentChange',
'runOnNoteChange',
'runOnChildNoteCreation',
'runOnAttributeCreation',
'runOnAttributeChange',
'template',
'inherit',
'widget',
'renderNote',
'shareCss',
'shareJs',
'shareFavicon',
'executeButton',
'keepCurrentHoisting',
'color',
'toc',
'excludeFromNoteMap',
'docName',
'launcherType',
'builtinWidget',
'baseSize',
'growthFactor'
);
UPDATE attributes SET name = 'name'
AND name NOT IN ('inbox',
'disableVersioning',
'calendarRoot',
'archived',
'excludeFromExport',
'disableInclusion',
'appCss',
'appTheme',
'hidePromotedAttributes',
'readOnly',
'autoReadOnlyDisabled',
'cssClass',
'iconClass',
'keyboardShortcut',
'run',
'runOnInstance',
'runAtHour',
'customRequestHandler',
'customResourceProvider',
'widget',
'noteInfoWidgetDisabled',
'linkMapWidgetDisabled',
'noteRevisionsWidgetDisabled',
'whatLinksHereWidgetDisabled',
'similarNotesWidgetDisabled',
'workspace',
'workspaceIconClass',
'workspaceTabBackgroundColor',
'searchHome',
'workspaceInbox',
'workspaceSearchHome',
'sqlConsoleHome',
'datePattern',
'pageSize',
'viewType',
'mapRootNoteId',
'bookmarkFolder',
'sorted',
'top',
'fullContentWidth',
'shareHiddenFromTree',
'shareAlias',
'shareOmitDefaultCss',
'shareRoot',
'internalLink',
'imageLink',
'relationMapLink',
'includeMapLink',
'runOnNoteCreation',
'runOnNoteTitleChange',
'runOnNoteContentChange',
'runOnNoteChange',
'runOnChildNoteCreation',
'runOnAttributeCreation',
'runOnAttributeChange',
'template',
'inherit',
'widget',
'renderNote',
'shareCss',
'shareJs',
'shareFavicon',
'executeButton',
'keepCurrentHoisting',
'color',
'toc',
'excludeFromNoteMap',
'docName',
'launcherType',
'builtinWidget',
'baseSize',
'growthFactor'
);
UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL AND prefix != 'recovered'; UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL AND prefix != 'recovered';
UPDATE options SET value = 'anonymized' WHERE name IN UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey', ('documentId', 'documentSecret', 'encryptedDataKey',

Binary file not shown.

View File

@ -0,0 +1,48 @@
module.exports = () => {
const beccaLoader = require("../../src/becca/becca_loader");
const becca = require("../../src/becca/becca");
const cls = require("../../src/services/cls");
const log = require("../../src/services/log");
cls.init(() => {
beccaLoader.load();
for (const note of Object.values(becca.notes)) {
try {
if (!note.isJavaScript()) {
continue;
}
if (!note.mime?.endsWith('env=frontend') && !note.mime?.endsWith('env=backend')) {
continue;
}
const origContent = note.getContent().toString();
const fixedContent = origContent
.replaceAll("runOnServer", "runOnBackend")
.replaceAll("api.refreshTree()", "")
.replaceAll("addTextToActiveTabEditor", "addTextToActiveContextEditor")
.replaceAll("getActiveTabNote", "getActiveContextNote")
.replaceAll("getActiveTabTextEditor", "getActiveContextTextEditor")
.replaceAll("getActiveTabNotePath", "getActiveContextNotePath")
.replaceAll("getDateNote", "getDayNote")
.replaceAll("utils.unescapeHtml", "unescapeHtml")
.replaceAll("sortNotesByTitle", "sortNotes")
.replaceAll("CollapsibleWidget", "RightPanelWidget")
.replaceAll("TabAwareWidget", "NoteContextAwareWidget")
.replaceAll("TabCachingWidget", "NoteContextAwareWidget")
.replaceAll("NoteContextCachingWidget", "NoteContextAwareWidget");
if (origContent !== fixedContent) {
log.info(`Replacing legacy API calls for note '${note.noteId}'`);
note.saveNoteRevision();
note.setContent(fixedContent);
}
}
catch (e) {
log.error(`Error during migration to 213 for note '${note.noteId}': ${e.message} ${e.stack}`);
}
}
});
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
package-lock.json generated
View File

@ -1,11 +1,12 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.58.8", "version": "0.59.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.58.8", "name": "trilium",
"version": "0.59.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {

View File

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

View File

@ -84,7 +84,7 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entity
return; return;
} }
if (["notes", "branches", "attributes", "etapi_tokens"].includes(entityName)) { if (["notes", "branches", "attributes", "etapi_tokens", "options"].includes(entityName)) {
const EntityClass = entityConstructor.getEntityFromEntityName(entityName); const EntityClass = entityConstructor.getEntityFromEntityName(entityName);
const primaryKeyName = EntityClass.primaryKeyName; const primaryKeyName = EntityClass.primaryKeyName;

View File

@ -124,35 +124,43 @@ function getNoteTitleForPath(notePathArray) {
* Archived (and hidden) notes are also returned, but non-archived paths are preferred if available * Archived (and hidden) notes are also returned, but non-archived paths are preferred if available
* - this means that archived paths is returned only if there's no non-archived path * - this means that archived paths is returned only if there's no non-archived path
* - you can check whether returned path is archived using isArchived * - you can check whether returned path is archived using isArchived
*
* @param {BNote} note
* @param {string[]} path
*/ */
function getSomePath(note, path = []) { function getSomePath(note, path = []) {
// first try to find note within hoisted note, otherwise take any existing note path // first try to find note within hoisted note, otherwise take any existing note path
// each branch needs a separate copy since it's mutable return getSomePathInner(note, path, true)
return getSomePathInner(note, [...path], true) || getSomePathInner(note, path, false);
|| getSomePathInner(note, [...path], false);
} }
/**
* @param {BNote} note
* @param {string[]} path
* @param {boolean}respectHoisting
* @returns {string[]|false}
*/
function getSomePathInner(note, path, respectHoisting) { function getSomePathInner(note, path, respectHoisting) {
if (note.isRoot()) { if (note.isRoot()) {
path.push(note.noteId); const foundPath = [...path, note.noteId];
path.reverse(); foundPath.reverse();
if (respectHoisting && !path.includes(cls.getHoistedNoteId())) { if (respectHoisting && !foundPath.includes(cls.getHoistedNoteId())) {
return false; return false;
} }
return path; return foundPath;
} }
const parents = note.parents; const parents = note.parents;
if (parents.length === 0) { if (parents.length === 0) {
console.log(`Note ${note.noteId} - "${note.title}" has no parents.`); console.log(`Note '${note.noteId}' - '${note.title}' has no parents.`);
return false; return false;
} }
for (const parentNote of parents) { for (const parentNote of parents) {
const retPath = getSomePathInner(parentNote, path.concat([note.noteId]), respectHoisting); const retPath = getSomePathInner(parentNote, [...path, note.noteId], respectHoisting);
if (retPath) { if (retPath) {
return retPath; return retPath;

View File

@ -1182,9 +1182,7 @@ class BNote extends AbstractBeccaEntity {
return false; return false;
} else if (parentNote.noteId === '_hidden') { } else if (parentNote.noteId === '_hidden') {
continue; continue;
} } else if (!parentNote.isHiddenCompletely()) {
if (!parentNote.isHiddenCompletely()) {
return false; return false;
} }
} }

View File

@ -166,12 +166,14 @@ class BNoteRevision extends AbstractBeccaEntity {
utcDateLastEdited: this.utcDateLastEdited, utcDateLastEdited: this.utcDateLastEdited,
utcDateCreated: this.utcDateCreated, utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified, utcDateModified: this.utcDateModified,
content: this.content, // used when retrieving full note revision to frontend
contentLength: this.contentLength contentLength: this.contentLength
}; };
} }
getPojoToSave() { getPojoToSave() {
const pojo = this.getPojo(); const pojo = this.getPojo();
delete pojo.content; // not getting persisted
delete pojo.contentLength; // not getting persisted delete pojo.contentLength; // not getting persisted
if (pojo.isProtected) { if (pojo.isProtected) {

View File

@ -16,6 +16,11 @@ class BOption extends AbstractBeccaEntity {
constructor(row) { constructor(row) {
super(); super();
this.updateFromRow(row);
this.becca.options[this.name] = this;
}
updateFromRow(row) {
/** @type {string} */ /** @type {string} */
this.name = row.name; this.name = row.name;
/** @type {string} */ /** @type {string} */
@ -24,8 +29,6 @@ class BOption extends AbstractBeccaEntity {
this.isSynced = !!row.isSynced; this.isSynced = !!row.isSynced;
/** @type {string} */ /** @type {string} */
this.utcDateModified = row.utcDateModified; this.utcDateModified = row.utcDateModified;
this.becca.options[this.name] = this;
} }
beforeSaving() { beforeSaving() {

View File

@ -73,7 +73,7 @@ class FNote {
this.mime = row.mime; this.mime = row.mime;
} }
addParent(parentNoteId, branchId) { addParent(parentNoteId, branchId, sort = true) {
if (parentNoteId === 'none') { if (parentNoteId === 'none') {
return; return;
} }
@ -83,6 +83,10 @@ class FNote {
} }
this.parentToBranch[parentNoteId] = branchId; this.parentToBranch[parentNoteId] = branchId;
if (sort) {
this.sortParents();
}
} }
addChild(childNoteId, branchId, sort = true) { addChild(childNoteId, branchId, sort = true) {
@ -189,7 +193,7 @@ class FNote {
// will sort the parents so that non-search & non-archived are first and archived at the end // will sort the parents so that non-search & non-archived are first and archived at the end
// this is done so that non-search & non-archived paths are always explored as first when looking for note path // this is done so that non-search & non-archived paths are always explored as first when looking for note path
resortParents() { sortParents() {
this.parents.sort((aNoteId, bNoteId) => { this.parents.sort((aNoteId, bNoteId) => {
const aBranchId = this.parentToBranch[aNoteId]; const aBranchId = this.parentToBranch[aNoteId];
@ -197,7 +201,7 @@ class FNote {
return 1; return 1;
} }
const aNote = this.froca.getNoteFromCache([aNoteId]); const aNote = this.froca.getNoteFromCache(aNoteId);
if (aNote.isArchived || aNote.isHiddenCompletely()) { if (aNote.isArchived || aNote.isHiddenCompletely()) {
return 1; return 1;

View File

@ -14,7 +14,8 @@ function createClassForColor(color) {
const className = `color-${normalizedColorName}`; const className = `color-${normalizedColorName}`;
if (!registeredClasses.has(className)) { if (!registeredClasses.has(className)) {
$("head").append(`<style>.${className} { color: ${color} !important; }</style>`); // make the active fancytree selector more specific than the normal color setting
$("head").append(`<style>.${className}, span.fancytree-active.${className} { color: ${color} !important; }</style>`);
registeredClasses.add(className); registeredClasses.add(className);
} }

View File

@ -115,7 +115,7 @@ class Froca {
const childNote = this.notes[branch.noteId]; const childNote = this.notes[branch.noteId];
if (childNote) { if (childNote) {
childNote.addParent(branch.parentNoteId, branch.branchId); childNote.addParent(branch.parentNoteId, branch.branchId, false);
} }
const parentNote = this.notes[branch.parentNoteId]; const parentNote = this.notes[branch.parentNoteId];
@ -152,6 +152,7 @@ class Froca {
// sort all of them at once, this avoids repeated sorts (#1480) // sort all of them at once, this avoids repeated sorts (#1480)
for (const noteId of noteIdsToSort) { for (const noteId of noteIdsToSort) {
this.notes[noteId].sortChildren(); this.notes[noteId].sortChildren();
this.notes[noteId].sortParents();
} }
} }

View File

@ -35,36 +35,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
/** @property {dayjs} day.js library for date manipulation. See {@link https://day.js.org} for documentation */ /** @property {dayjs} day.js library for date manipulation. See {@link https://day.js.org} for documentation */
this.dayjs = dayjs; this.dayjs = dayjs;
/**
* @property {RightPanelWidget}
* @deprecated use api.RightPanelWidget instead
*/
this.CollapsibleWidget = RightPanelWidget;
/** @property {RightPanelWidget} */ /** @property {RightPanelWidget} */
this.RightPanelWidget = RightPanelWidget; this.RightPanelWidget = RightPanelWidget;
/** @property {NoteContextAwareWidget} */ /** @property {NoteContextAwareWidget} */
this.NoteContextAwareWidget = NoteContextAwareWidget; this.NoteContextAwareWidget = NoteContextAwareWidget;
/**
* @property {NoteContextAwareWidget}
* @deprecated use NoteContextAwareWidget instead
*/
this.TabAwareWidget = NoteContextAwareWidget;
/**
* @property {NoteContextAwareWidget}
* @deprecated use NoteContextAwareWidget instead
*/
this.TabCachingWidget = NoteContextAwareWidget;
/**
* @property {NoteContextAwareWidget}
* @deprecated use NoteContextAwareWidget instead
*/
this.NoteContextCachingWidget = NoteContextAwareWidget;
/** @property {BasicWidget} */ /** @property {BasicWidget} */
this.BasicWidget = BasicWidget; this.BasicWidget = BasicWidget;

View File

@ -58,7 +58,7 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
return; return;
} }
child.resortParents(); child.sortParents();
const parents = child.getParentNotes(); const parents = child.getParentNotes();

View File

@ -172,11 +172,16 @@ export default class NoteRevisionsDialog extends BasicWidget {
const revisionItem = this.revisionItems.find(r => r.noteRevisionId === noteRevisionId); const revisionItem = this.revisionItems.find(r => r.noteRevisionId === noteRevisionId);
this.$titleButtons.empty();
this.$content.empty();
this.$title.html(revisionItem.title); this.$title.html(revisionItem.title);
this.renderContentButtons(revisionItem);
await this.renderContent(revisionItem);
}
renderContentButtons(revisionItem) {
this.$titleButtons.empty();
const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>'); const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>');
$restoreRevisionButton.on('click', async () => { $restoreRevisionButton.on('click', async () => {
@ -222,9 +227,15 @@ export default class NoteRevisionsDialog extends BasicWidget {
if (!revisionItem.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) { if (!revisionItem.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
this.$titleButtons.append($downloadButton); this.$titleButtons.append($downloadButton);
} }
}
async renderContent(revisionItem) {
this.$content.empty();
const fullNoteRevision = await server.get(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`); const fullNoteRevision = await server.get(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`);
console.log(fullNoteRevision);
if (revisionItem.type === 'text') { if (revisionItem.type === 'text') {
this.$content.html(fullNoteRevision.content); this.$content.html(fullNoteRevision.content);
@ -233,19 +244,16 @@ export default class NoteRevisionsDialog extends BasicWidget {
renderMathInElement($content[0], {trust: true}); renderMathInElement($content[0], {trust: true});
} }
} } else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') {
else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') {
this.$content.html($("<pre>").text(fullNoteRevision.content)); this.$content.html($("<pre>").text(fullNoteRevision.content));
} } else if (revisionItem.type === 'image') {
else if (revisionItem.type === 'image') {
this.$content.html($("<img>") this.$content.html($("<img>")
// reason why we put this inline as base64 is that we do not want to let user copy this // reason why we put this inline as base64 is that we do not want to let user copy this
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be an uploaded as a new note // as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be an uploaded as a new note
.attr("src", `data:${fullNoteRevision.mime};base64,${fullNoteRevision.content}`) .attr("src", `data:${fullNoteRevision.mime};base64,${fullNoteRevision.content}`)
.css("max-width", "100%") .css("max-width", "100%")
.css("max-height", "100%")); .css("max-height", "100%"));
} } else if (revisionItem.type === 'file') {
else if (revisionItem.type === 'file') {
const $table = $("<table cellpadding='10'>") const $table = $("<table cellpadding='10'>")
.append($("<tr>").append( .append($("<tr>").append(
$("<th>").text("MIME: "), $("<th>").text("MIME: "),
@ -267,8 +275,7 @@ export default class NoteRevisionsDialog extends BasicWidget {
} }
this.$content.html($table); this.$content.html($table);
} } else if (revisionItem.type === 'canvas') {
else if (revisionItem.type === 'canvas') {
/** /**
* FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com * FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com
* REMOVE external dependency!!!! This is defined in the svg in defs.style * REMOVE external dependency!!!! This is defined in the svg in defs.style
@ -285,12 +292,11 @@ export default class NoteRevisionsDialog extends BasicWidget {
*/ */
const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"}); const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"});
this.$content.html($('<div>').append($svgHtml)); this.$content.html($('<div>').append($svgHtml));
} catch(err) { } catch (err) {
console.error("error parsing fullNoteRevision.content as JSON", fullNoteRevision.content, err); console.error("error parsing fullNoteRevision.content as JSON", fullNoteRevision.content, err);
this.$content.html($("<div>").text("Error parsing content. Please check console.error() for more details.")); this.$content.html($("<div>").text("Error parsing content. Please check console.error() for more details."));
} }
} } else {
else {
this.$content.text("Preview isn't available for this note type."); this.$content.text("Preview isn't available for this note type.");
} }
} }

View File

@ -13,4 +13,11 @@
.relation-map-wrapper { .relation-map-wrapper {
height: 100vh !important; height: 100vh !important;
} }
.table thead th,
.table td, .table th {
/* Fix center vertical alignment of table cells */
vertical-align: middle;
}
} }

View File

@ -990,3 +990,9 @@ button.close:hover {
textarea { textarea {
cursor: auto; cursor: auto;
} }
.table thead th,
.table td, .table th {
/* Fix center vertical alignment of table cells */
vertical-align: middle;
}

View File

@ -75,7 +75,7 @@ function addClipping(req) {
} }
function createNote(req) { function createNote(req) {
let {title, content, pageUrl, images, clipType} = req.body; let {title, content, pageUrl, images, clipType, labels} = req.body;
if (!title || !title.trim()) { if (!title || !title.trim()) {
title = `Clipped note from ${pageUrl}`; title = `Clipped note from ${pageUrl}`;
@ -100,6 +100,13 @@ function createNote(req) {
note.setLabel('pageUrl', pageUrl); note.setLabel('pageUrl', pageUrl);
note.setLabel('iconClass', 'bx bx-globe'); note.setLabel('iconClass', 'bx bx-globe');
} }
if (labels) {
for (const labelName in labels) {
const labelValue = htmlSanitizer.sanitize(labels[labelName]);
note.setLabel(labelName, labelValue);
}
}
const rewrittenContent = processContent(images, note, content); const rewrittenContent = processContent(images, note, content);

View File

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

View File

@ -1 +1 @@
module.exports = { buildDate:"2023-02-13T21:50:54+01:00", buildRevision: "17085e5578d2a20a77a6ade058f74e6d5b798ecc" }; module.exports = { buildDate:"2023-02-28T23:39:34+01:00", buildRevision: "9eb3075f65af98bd8c4a8ca8cc241ae2ae05bfa9" };

View File

@ -250,12 +250,6 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
const {noteMeta} = getMeta(absUrl); const {noteMeta} = getMeta(absUrl);
if (!noteMeta) {
log.info(`Could not find note meta for URL '${absUrl}'.`);
return null;
}
const targetNoteId = getNoteId(noteMeta, absUrl); const targetNoteId = getNoteId(noteMeta, absUrl);
return targetNoteId; return targetNoteId;
} }
@ -344,6 +338,9 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
content = content.replace(new RegExp(link.value, "g"), getNewNoteId(link.value)); content = content.replace(new RegExp(link.value, "g"), getNewNoteId(link.value));
} }
} }
content = content.trim();
return content; return content;
} }

View File

@ -55,12 +55,22 @@ function deriveMime(type, mime) {
} }
function copyChildAttributes(parentNote, childNote) { function copyChildAttributes(parentNote, childNote) {
const hasAlreadyTemplate = childNote.hasRelation('template');
for (const attr of parentNote.getAttributes()) { for (const attr of parentNote.getAttributes()) {
if (attr.name.startsWith("child:")) { if (attr.name.startsWith("child:")) {
const name = attr.name.substr(6);
if (hasAlreadyTemplate && attr.type === 'relation' && name === 'template') {
// if the note already has a template, it means the template was chosen by the user explicitly
// in the menu. In that case we should override the default templates defined in the child: attrs
continue;
}
new BAttribute({ new BAttribute({
noteId: childNote.noteId, noteId: childNote.noteId,
type: attr.type, type: attr.type,
name: attr.name.substr(6), name: name,
value: attr.value, value: attr.value,
position: attr.position, position: attr.position,
isInheritable: attr.isInheritable isInheritable: attr.isInheritable
@ -194,21 +204,18 @@ function createNewNote(params) {
asyncPostProcessContent(note, params.content); asyncPostProcessContent(note, params.content);
copyChildAttributes(parentNote, note);
if (params.templateNoteId) { if (params.templateNoteId) {
if (!becca.getNote(params.templateNoteId)) { if (!becca.getNote(params.templateNoteId)) {
throw new Error(`Template note '${params.templateNoteId}' does not exist.`); throw new Error(`Template note '${params.templateNoteId}' does not exist.`);
} }
// could be already copied from the parent via `child:`, no need to have 2 note.addRelation('template', params.templateNoteId);
if (!note.hasOwnedRelation('template', params.templateNoteId)) {
note.addRelation('template', params.templateNoteId);
}
// no special handling for ~inherit since it doesn't matter if it's assigned with the note creation or later // no special handling for ~inherit since it doesn't matter if it's assigned with the note creation or later
} }
copyChildAttributes(parentNote, note);
triggerNoteTitleChanged(note); triggerNoteTitleChanged(note);
eventService.emit(eventService.ENTITY_CREATED, { eventService.emit(eventService.ENTITY_CREATED, {

View File

@ -3,8 +3,7 @@ const cls = require('./cls');
const sqlInit = require('./sql_init'); const sqlInit = require('./sql_init');
const config = require('./config'); const config = require('./config');
const log = require('./log'); const log = require('./log');
const sql = require("./sql"); const attributeService = require("../services/attributes");
const becca = require("../becca/becca");
const protectedSessionService = require("../services/protected_session"); const protectedSessionService = require("../services/protected_session");
const hiddenSubtreeService = require("./hidden_subtree"); const hiddenSubtreeService = require("./hidden_subtree");
const userGuideImportService = require("./user_guide_import"); const userGuideImportService = require("./user_guide_import");
@ -21,23 +20,9 @@ function getRunAtHours(note) {
} }
function runNotesWithLabel(runAttrValue) { function runNotesWithLabel(runAttrValue) {
// TODO: should be refactored into becca search
const noteIds = sql.getColumn(`
SELECT notes.noteId
FROM notes
JOIN attributes ON attributes.noteId = notes.noteId
AND attributes.isDeleted = 0
AND attributes.type = 'label'
AND attributes.name = 'run'
AND attributes.value = ?
WHERE
notes.type = 'code'
AND notes.isDeleted = 0`, [runAttrValue]);
const notes = becca.getNotes(noteIds);
const instanceName = config.General ? config.General.instanceName : null; const instanceName = config.General ? config.General.instanceName : null;
const currentHours = new Date().getHours(); const currentHours = new Date().getHours();
const notes = attributeService.getNotesWithLabel('run', runAttrValue);
for (const note of notes) { for (const note of notes) {
const runOnInstances = note.getLabelValues('runOnInstance'); const runOnInstances = note.getLabelValues('runOnInstance');

View File

@ -79,6 +79,10 @@ class NoteContentFulltextExp extends Expression {
} }
} }
if (!content) {
return;
}
content = this.preprocessContent(content, type, mime); content = this.preprocessContent(content, type, mime);
if (this.tokens.length === 1) { if (this.tokens.length === 1) {
@ -108,6 +112,7 @@ class NoteContentFulltextExp extends Expression {
resultNoteSet.add(becca.notes[noteId]); resultNoteSet.add(becca.notes[noteId]);
} }
} }
return content; return content;
} }

View File

@ -28,15 +28,20 @@ class NoteFlatTextExp extends Expression {
if (retPath) { if (retPath) {
const noteId = retPath[retPath.length - 1]; const noteId = retPath[retPath.length - 1];
executionContext.noteIdToNotePath[noteId] = retPath;
resultNoteSet.add(becca.notes[noteId]); if (!resultNoteSet.hasNoteId(noteId)) {
// we could get here from multiple paths, the first one wins because the paths
// are sorted by importance
executionContext.noteIdToNotePath[noteId] = retPath;
resultNoteSet.add(becca.notes[noteId]);
}
} }
return; return;
} }
if (!note.parents.length === 0 || note.noteId === 'root') { if (note.parents.length === 0 || note.noteId === 'root') {
return; return;
} }
@ -49,9 +54,11 @@ class NoteFlatTextExp extends Expression {
} }
for (const attribute of note.ownedAttributes) { for (const attribute of note.ownedAttributes) {
const normalizedName = utils.normalize(attribute.name);
const normalizedValue = utils.normalize(attribute.value);
for (const token of tokens) { for (const token of tokens) {
if (utils.normalize(attribute.name).includes(token) if (normalizedName.includes(token) || normalizedValue.includes(token)) {
|| utils.normalize(attribute.value).includes(token)) {
foundAttrTokens.push(token); foundAttrTokens.push(token);
} }
} }
@ -70,10 +77,10 @@ class NoteFlatTextExp extends Expression {
if (foundTokens.length > 0) { if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token)); const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
searchDownThePath(parentNote, remainingTokens, path.concat([note.noteId])); searchDownThePath(parentNote, remainingTokens, [...path, note.noteId]);
} }
else { else {
searchDownThePath(parentNote, tokens, path.concat([note.noteId])); searchDownThePath(parentNote, tokens, [...path, note.noteId]);
} }
} }
} }

View File

@ -155,11 +155,8 @@ function findResultsWithExpression(expression, searchContext) {
const noteSet = expression.execute(allNoteSet, executionContext, searchContext); const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
const searchResults = noteSet.notes const searchResults = noteSet.notes
.filter(note => !note.isDeleted)
.map(note => { .map(note => {
if (note.isDeleted) {
return null;
}
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || beccaService.getSomePath(note); const notePathArray = executionContext.noteIdToNotePath[note.noteId] || beccaService.getSomePath(note);
if (!notePathArray) { if (!notePathArray) {
@ -167,8 +164,7 @@ function findResultsWithExpression(expression, searchContext) {
} }
return new SearchResult(notePathArray); return new SearchResult(notePathArray);
}) });
.filter(note => !!note);
for (const res of searchResults) { for (const res of searchResults) {
res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens); res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens);

View File

@ -52,8 +52,7 @@
device = "mobile"; device = "mobile";
} }
else { else {
// mobile device detection based on https://stackoverflow.com/a/24600597/944162 device = isMobile() ? "mobile" : "desktop";
device = /Mobi/.test(navigator.userAgent) ? "mobile" : "desktop";
} }
console.log("Setting device cookie to:", device); console.log("Setting device cookie to:", device);
@ -66,6 +65,19 @@
document.cookie = name + "=" + (value || "") + expires + "; path=/"; document.cookie = name + "=" + (value || "") + expires + "; path=/";
} }
// https://stackoverflow.com/a/73731646/944162
function isMobile() {
if ('maxTouchPoints' in navigator) return navigator.maxTouchPoints > 0;
const mQ = matchMedia?.('(pointer:coarse)');
if (mQ?.media === '(pointer:coarse)') return !!mQ.matches;
if ('orientation' in window) return true;
return /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(navigator.userAgent) ||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(navigator.userAgent);
}
</script> </script>
<link href="<%= assetPath %>/libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="<%= assetPath %>/libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">

View File

@ -109,12 +109,12 @@ async function startTrilium() {
// handle specific listen errors with friendly messages // handle specific listen errors with friendly messages
switch (error.code) { switch (error.code) {
case 'EACCES': case 'EACCES':
console.error(`Port ${port} requires elevated privileges`); console.error(`Port ${port} requires elevated privileges. It's recommended to use port above 1024.`);
process.exit(1); process.exit(1);
break; break;
case 'EADDRINUSE': case 'EADDRINUSE':
console.error(`Port ${port} is already in use`); console.error(`Port ${port} is already in use. Most likely, another Trilium process is already running. You might try to find it, kill it, and try again.`);
process.exit(1); process.exit(1);
break; break;