mirror of
https://github.com/zadam/trilium.git
synced 2025-11-02 03:16:11 +01:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a2cacb5b | ||
|
|
17085e5578 | ||
|
|
fc7da015fe | ||
|
|
adf222b5e8 | ||
|
|
ade22ea825 | ||
|
|
2c8fb90ecb | ||
|
|
c67644a2e3 | ||
|
|
346f6edd7e | ||
|
|
92f586486f | ||
|
|
e7c6d912a4 | ||
|
|
290f7e2101 | ||
|
|
5e0fbea3b3 | ||
|
|
7b2c3afe4c | ||
|
|
2e181d0fb1 | ||
|
|
68a03211ce | ||
|
|
f2a19c56b1 | ||
|
|
a98fd509c6 | ||
|
|
a3149aecf4 | ||
|
|
ef825371cf | ||
|
|
b567775129 | ||
|
|
58d71bf8e3 | ||
|
|
9fd0b85ff2 | ||
|
|
98620887d4 | ||
|
|
9eff08144c | ||
|
|
4ba72bc8d3 | ||
|
|
743979266a | ||
|
|
4f98c960ec | ||
|
|
424b624ea9 | ||
|
|
a004a8b092 | ||
|
|
df0f52aff9 | ||
|
|
f7bd72ba2f | ||
|
|
038c0e78a6 | ||
|
|
2a2cbcd68b | ||
|
|
14da697a4e | ||
|
|
2525857c20 | ||
|
|
8eaef3e1d8 | ||
|
|
082caf98e8 | ||
|
|
60602a2264 | ||
|
|
9479f1c1a1 | ||
|
|
88bc7402a2 | ||
|
|
e7b3c3239b | ||
|
|
4f4c3ef3f3 | ||
|
|
decfeb366c |
BIN
db/demo.zip
BIN
db/demo.zip
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
UPDATE branches SET branchId = '_hidden__search' WHERE parentNoteId = 'hidden' AND noteId = 'search';
|
||||
UPDATE branches SET branchId = 'root__globalNoteMap' WHERE parentNoteId = 'singles' AND noteId = 'globalnotemap';
|
||||
UPDATE branches SET branchId = '_hidden__sqlConsole' WHERE parentNoteId = 'hidden' AND noteId = 'sqlconsole';
|
||||
UPDATE branches SET branchId = 'root__hidden' WHERE parentNoteId = 'root' AND noteId = 'hidden';
|
||||
UPDATE branches SET branchId = '_hidden__bulkAction' WHERE parentNoteId = 'hidden' AND noteId = 'bulkaction';
|
||||
UPDATE branches SET branchId = '_hidden__share' WHERE parentNoteId = 'root' AND noteId = 'share';
|
||||
UPDATE branches SET branchId = '_hidden__search' WHERE parentNoteId = 'hidden' AND noteId = 'search' AND isDeleted = 0;
|
||||
UPDATE branches SET branchId = 'root__globalNoteMap' WHERE parentNoteId = 'singles' AND noteId = 'globalnotemap' AND isDeleted = 0;
|
||||
UPDATE branches SET branchId = '_hidden__sqlConsole' WHERE parentNoteId = 'hidden' AND noteId = 'sqlconsole' AND isDeleted = 0;
|
||||
UPDATE branches SET branchId = 'root__hidden' WHERE parentNoteId = 'root' AND noteId = 'hidden' AND isDeleted = 0;
|
||||
UPDATE branches SET branchId = '_hidden__bulkAction' WHERE parentNoteId = 'hidden' AND noteId = 'bulkaction' AND isDeleted = 0;
|
||||
UPDATE branches SET branchId = '_hidden__share' WHERE parentNoteId = 'root' AND noteId = 'share' AND isDeleted = 0;
|
||||
|
||||
@@ -7,6 +7,6 @@ module.exports = () => {
|
||||
beccaLoader.load();
|
||||
// make sure the hidden subtree exists since the subsequent migrations we will move some existing notes into it (share...)
|
||||
// in previous releases hidden subtree was created lazily
|
||||
hiddenSubtreeService.checkHiddenSubtree();
|
||||
hiddenSubtreeService.checkHiddenSubtree(true);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,5 +12,10 @@ module.exports = () => {
|
||||
|
||||
attr.markAsDeleted("0204__migrate_bookmarks_to_clones");
|
||||
}
|
||||
|
||||
// bookmarkFolder used to work in 0.57 without the bookmarked label
|
||||
for (const attr of becca.findAttributes('label','bookmarkFolder')) {
|
||||
cloningService.toggleNoteInParent(true, attr.noteId, '_lbBookmarks');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.58.3-beta",
|
||||
"version": "0.58.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.58.3-beta",
|
||||
"version": "0.58.7",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
@@ -29,6 +29,7 @@
|
||||
"electron-debug": "3.2.0",
|
||||
"electron-dl": "3.5.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
"express": "4.18.2",
|
||||
"express-partial-content": "1.0.2",
|
||||
"express-rate-limit": "6.7.0",
|
||||
@@ -4774,7 +4775,7 @@
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
@@ -14267,7 +14268,7 @@
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.58.4",
|
||||
"version": "0.58.8",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -49,6 +49,7 @@
|
||||
"electron-debug": "3.2.0",
|
||||
"electron-dl": "3.5.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
"express": "4.18.2",
|
||||
"express-partial-content": "1.0.2",
|
||||
"express-rate-limit": "6.7.0",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const backupService = require('./services/backup');
|
||||
const anonymizationService = require('./services/anonymization');
|
||||
const sqlInit = require('./services/sql_init');
|
||||
require('./entities/entity_constructor');
|
||||
require('./becca/entity_constructor');
|
||||
|
||||
sqlInit.dbReady.then(async () => {
|
||||
try {
|
||||
console.log("Starting anonymization...");
|
||||
|
||||
const resp = await backupService.anonymize();
|
||||
const resp = await anonymizationService.createAnonymizedCopy('full');
|
||||
|
||||
if (resp.success) {
|
||||
console.log(`Anonymized file has been saved to: ${resp.anonymizedFilePath}`);
|
||||
|
||||
@@ -152,6 +152,10 @@ class Becca {
|
||||
.replace('_', '')
|
||||
);
|
||||
|
||||
if (!(camelCaseEntityName in this)) {
|
||||
throw new Error(`Unknown entity name '${camelCaseEntityName}' (original argument '${entityName}')`);
|
||||
}
|
||||
|
||||
return this[camelCaseEntityName][entityId];
|
||||
}
|
||||
|
||||
|
||||
@@ -83,10 +83,8 @@ function getNoteTitleArrayForPath(notePathArray) {
|
||||
throw new Error(`${notePathArray} is not an array.`);
|
||||
}
|
||||
|
||||
const hoistedNoteId = cls.getHoistedNoteId();
|
||||
|
||||
if (notePathArray.length === 1 && notePathArray[0] === hoistedNoteId) {
|
||||
return [getNoteTitle(hoistedNoteId)];
|
||||
if (notePathArray.length === 1) {
|
||||
return [getNoteTitle(notePathArray[0])];
|
||||
}
|
||||
|
||||
const titles = [];
|
||||
@@ -95,6 +93,7 @@ function getNoteTitleArrayForPath(notePathArray) {
|
||||
let hoistedNotePassed = false;
|
||||
|
||||
// this is a notePath from outside of hoisted subtree so full title path needs to be returned
|
||||
const hoistedNoteId = cls.getHoistedNoteId();
|
||||
const outsideOfHoistedSubtree = !notePathArray.includes(hoistedNoteId);
|
||||
|
||||
for (const noteId of notePathArray) {
|
||||
|
||||
@@ -80,14 +80,14 @@ class AbstractEntity {
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
save() {
|
||||
save(opts = {}) {
|
||||
const entityName = this.constructor.entityName;
|
||||
const primaryKeyName = this.constructor.primaryKeyName;
|
||||
|
||||
const isNewEntity = !this[primaryKeyName];
|
||||
|
||||
if (this.beforeSaving) {
|
||||
this.beforeSaving();
|
||||
this.beforeSaving(opts);
|
||||
}
|
||||
|
||||
const pojo = this.getPojoToSave();
|
||||
|
||||
@@ -176,8 +176,10 @@ class Attribute extends AbstractEntity {
|
||||
return !(this.attributeId in this.becca.attributes);
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
this.validate();
|
||||
beforeSaving(opts = {}) {
|
||||
if (!opts.skipValidation) {
|
||||
this.validate();
|
||||
}
|
||||
|
||||
this.name = sanitizeAttributeName(this.name);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ const NoteRevision = require("./note_revision");
|
||||
const TaskContext = require("../../services/task_context");
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require('dayjs/plugin/utc');
|
||||
const eventService = require("../../services/events");
|
||||
dayjs.extend(utc)
|
||||
|
||||
const LABEL = 'label';
|
||||
@@ -314,6 +315,11 @@ class Note extends AbstractEntity {
|
||||
utcDateChanged: pojo.utcDateModified,
|
||||
isSynced: true
|
||||
});
|
||||
|
||||
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||
entityName: 'note_contents',
|
||||
entity: this
|
||||
});
|
||||
}
|
||||
|
||||
setJsonContent(content) {
|
||||
@@ -1107,6 +1113,13 @@ class Note extends AbstractEntity {
|
||||
return notePaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
|
||||
*/
|
||||
isHiddenCompletely() {
|
||||
return !this.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ancestorNoteId
|
||||
* @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
|
||||
@@ -1351,7 +1364,7 @@ class Note extends AbstractEntity {
|
||||
}
|
||||
|
||||
isOptions() {
|
||||
return this.noteId.startsWith("options");
|
||||
return this.noteId.startsWith("_options");
|
||||
}
|
||||
|
||||
get isDeleted() {
|
||||
|
||||
@@ -79,11 +79,12 @@ export default class TabManager extends Component {
|
||||
filteredTabs = filteredTabs.filter(tab => tab.active);
|
||||
}
|
||||
|
||||
if (filteredTabs.length === 0) {
|
||||
const [notePath] = treeService.getHashValueFromAddress();
|
||||
// resolve before opened tabs can change this
|
||||
const [notePathInUrl, ntxIdInUrl] = treeService.getHashValueFromAddress();
|
||||
|
||||
if (filteredTabs.length === 0) {
|
||||
filteredTabs.push({
|
||||
notePath: notePath || 'root',
|
||||
notePath: notePathInUrl || 'root',
|
||||
active: true,
|
||||
hoistedNoteId: glob.extraHoistedNoteId || 'root'
|
||||
});
|
||||
@@ -95,17 +96,14 @@ export default class TabManager extends Component {
|
||||
|
||||
await this.tabsUpdate.allowUpdateWithoutChange(async () => {
|
||||
for (const tab of filteredTabs) {
|
||||
|
||||
await this.openContextWithNote(tab.notePath, tab.active, tab.ntxId, tab.hoistedNoteId, tab.mainNtxId);
|
||||
}
|
||||
});
|
||||
|
||||
// if there's notePath in the URL, make sure it's open and active
|
||||
// (useful, for e.g. opening clipped notes from clipper or opening link in an extra window)
|
||||
if (treeService.isNotePathInAddress()) {
|
||||
const [notePath, ntxId] = treeService.getHashValueFromAddress();
|
||||
|
||||
await appContext.tabManager.switchToNoteContext(ntxId, notePath);
|
||||
if (notePathInUrl) {
|
||||
await appContext.tabManager.switchToNoteContext(ntxIdInUrl, notePathInUrl);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
@@ -31,7 +31,7 @@ class ZoomComponent extends Component {
|
||||
|
||||
async setZoomFactorAndSave(zoomFactor) {
|
||||
if (zoomFactor >= MIN_ZOOM && zoomFactor <= MAX_ZOOM) {
|
||||
zoomFactor = Math.round(zoomFactor * 1000) / 1000;
|
||||
zoomFactor = Math.round(zoomFactor * 10) / 10;
|
||||
|
||||
this.setZoomFactor(zoomFactor);
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>Back and Forward buttons allow you to move in the navigation history.</p>
|
||||
|
||||
<p>These launchers are active only in the desktop build and will be ignored in the server edition where you can use the native browser navigation buttons instead.</p>
|
||||
@@ -364,6 +364,13 @@ class NoteShort {
|
||||
return notePaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
|
||||
*/
|
||||
isHiddenCompletely() {
|
||||
return !this.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden'));
|
||||
}
|
||||
|
||||
__filterAttrs(attributes, type, name) {
|
||||
this.__validateTypeName(type, name);
|
||||
|
||||
@@ -681,7 +688,7 @@ class NoteShort {
|
||||
return promotedAttrs;
|
||||
}
|
||||
|
||||
hasAncestor(ancestorNoteId, visitedNoteIds = null) {
|
||||
hasAncestor(ancestorNoteId, followTemplates = false, visitedNoteIds = null) {
|
||||
if (this.noteId === ancestorNoteId) {
|
||||
return true;
|
||||
}
|
||||
@@ -695,14 +702,16 @@ class NoteShort {
|
||||
|
||||
visitedNoteIds.add(this.noteId);
|
||||
|
||||
for (const templateNote of this.getTemplateNotes()) {
|
||||
if (templateNote.hasAncestor(ancestorNoteId, visitedNoteIds)) {
|
||||
return true;
|
||||
if (followTemplates) {
|
||||
for (const templateNote of this.getTemplateNotes()) {
|
||||
if (templateNote.hasAncestor(ancestorNoteId, followTemplates, visitedNoteIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const parentNote of this.getParentNotes()) {
|
||||
if (parentNote.hasAncestor(ancestorNoteId, visitedNoteIds)) {
|
||||
if (parentNote.hasAncestor(ancestorNoteId, followTemplates, visitedNoteIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -853,7 +862,7 @@ class NoteShort {
|
||||
}
|
||||
|
||||
isOptions() {
|
||||
return this.noteId.startsWith("options");
|
||||
return this.noteId.startsWith("_options");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.noteId, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,10 @@ function init() {
|
||||
|
||||
$.fn.getSelectedNoteId = function () {
|
||||
const notePath = $(this).getSelectedNotePath();
|
||||
if (!notePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chunks = notePath.split('/');
|
||||
|
||||
return chunks.length >= 1 ? chunks[chunks.length - 1] : null;
|
||||
|
||||
@@ -242,12 +242,13 @@ const ATTR_HELP = {
|
||||
"keepCurrentHoisting": "Opening this link won't change hoisting even if the note is not displayable in the current hoisted subtree.",
|
||||
"executeButton": "Title of the button which will execute the current code note",
|
||||
"executeDescription": "Longer description of the current code note displayed together with the execute button",
|
||||
"excludeFromNoteMap": "Notes with this label will be hidden from the Note Map"
|
||||
},
|
||||
"relation": {
|
||||
"runOnNoteCreation": "executes when note is created on backend. Use this relation if you want to run the script for all notes created under a specific subtree. In that case, create it on the subtree root note and make it inheritable. A new note created within the subtree (any depth) will trigger the script.",
|
||||
"runOnChildNoteCreation": "executes when new note is created under the note where this relation is defined",
|
||||
"runOnNoteTitleChange": "executes when note title is changed (includes note creation as well)",
|
||||
"runOnNoteContentChange": "executes when note content is changed (includes note creation as well).",
|
||||
"runOnNoteContentChange": "executes when note content is changed (includes note creation as well).",
|
||||
"runOnNoteChange": "executes when note is changed (includes note creation as well). Does not include content changes",
|
||||
"runOnNoteDeletion": "executes when note is being deleted",
|
||||
"runOnBranchCreation": "executes when a branch is created. Branch is a link between parent note and child note and is created e.g. when cloning or moving note.",
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class CommandButtonWidget extends AbstractButtonWidget {
|
||||
|
||||
/**
|
||||
* @param {function|string} command
|
||||
* @returns {CommandButtonWidget}
|
||||
* @returns {this}
|
||||
*/
|
||||
command(command) {
|
||||
this.settings.command = command;
|
||||
|
||||
@@ -303,7 +303,7 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
const resp = await fetch(RELEASES_API_URL);
|
||||
const data = await resp.json();
|
||||
|
||||
return data.tag_name.substring(1);
|
||||
return data?.tag_name?.substring(1);
|
||||
}
|
||||
|
||||
downloadLatestVersionCommand() {
|
||||
|
||||
@@ -23,6 +23,10 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget {
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
if (!utils.isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
|
||||
|
||||
// without this the history is preserved across frontend reloads
|
||||
|
||||
@@ -4,7 +4,6 @@ const TPL = `
|
||||
<div class="dropdown right-dropdown-widget dropright">
|
||||
<style>
|
||||
.right-dropdown-widget {
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import options from "../../services/options.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
|
||||
export default class LeftPaneContainer extends FlexContainer {
|
||||
constructor() {
|
||||
@@ -16,7 +17,15 @@ export default class LeftPaneContainer extends FlexContainer {
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.isOptionReloaded("leftPaneVisible")) {
|
||||
this.toggleInt(this.isEnabled());
|
||||
const visible = this.isEnabled();
|
||||
this.toggleInt(visible);
|
||||
|
||||
if (visible) {
|
||||
this.triggerEvent('focusTree');
|
||||
} else {
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
this.triggerEvent('focusOnDetail', {ntxId: activeNoteContext.ntxId});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,14 +234,14 @@ export default class NoteRevisionsDialog extends BasicWidget {
|
||||
renderMathInElement($content[0], {trust: true});
|
||||
}
|
||||
}
|
||||
else if (revisionItem.type === 'code') {
|
||||
else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') {
|
||||
this.$content.html($("<pre>").text(fullNoteRevision.content));
|
||||
}
|
||||
else if (revisionItem.type === 'image') {
|
||||
this.$content.html($("<img>")
|
||||
// reason why we put this inline as base64 is that we do not want to let user to copy this
|
||||
// as a URL to be used in a note. Instead if they copy and paste it into a note, it will be a uploaded as a new note
|
||||
.attr("src", `data:${note.mime};base64,${fullNoteRevision.content}`)
|
||||
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be a uploaded as a new note
|
||||
.attr("src", `data:${fullNoteRevision.mime};base64,${fullNoteRevision.content}`)
|
||||
.css("max-width", "100%")
|
||||
.css("max-height", "100%"));
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$saveToNoteButton.toggle(
|
||||
note.mime === 'text/x-sqlite;schema=trilium'
|
||||
&& !note.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden'))
|
||||
&& note.isHiddenCompletely()
|
||||
);
|
||||
|
||||
this.$openTriliumApiDocsButton.toggle(note.mime.startsWith('application/javascript;env='));
|
||||
|
||||
@@ -832,6 +832,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
});
|
||||
|
||||
await this.filterHoistedBranch();
|
||||
|
||||
const activeNode = await this.getNodeFromPath(appContext.tabManager.getActiveContextNotePath());
|
||||
|
||||
if (activeNode) {
|
||||
@@ -887,6 +889,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
async focusTreeEvent() {
|
||||
this.tree.$container.focus();
|
||||
this.tree.setFocus(true);
|
||||
}
|
||||
|
||||
/** @returns {FancytreeNode} */
|
||||
async getNodeFromPath(notePath, expand = false, logErrors = true) {
|
||||
utils.assertArguments(notePath);
|
||||
@@ -1074,6 +1081,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
|
||||
this.filterHoistedBranch();
|
||||
}, 600 * 1000);
|
||||
}
|
||||
|
||||
@@ -1309,6 +1318,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
await this.tree.reload([rootNode]);
|
||||
});
|
||||
|
||||
await this.filterHoistedBranch();
|
||||
|
||||
if (activeNotePath) {
|
||||
const node = await this.getNodeFromPath(activeNotePath, true);
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
this.$component.show();
|
||||
|
||||
this.$saveToNoteButton.toggle(!note.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden')));
|
||||
this.$saveToNoteButton.toggle(note.isHiddenCompletely());
|
||||
|
||||
this.$searchOptions.empty();
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class ImageTypeWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
async doRefresh(note) {
|
||||
this.$imageView.prop("src", `api/images/${note.noteId}/${note.title}`);
|
||||
this.$imageView.prop("src", `api/images/${note.noteId}/${encodeURIComponent(note.title)}`);
|
||||
}
|
||||
|
||||
copyImageReferenceToClipboardEvent({ntxId}) {
|
||||
|
||||
@@ -871,12 +871,11 @@ body {
|
||||
#launcher-pane .launcher-button {
|
||||
font-size: 150%;
|
||||
display: inline-block;
|
||||
padding: 15px 15px;
|
||||
padding: 13px 13px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
color: var(--launcher-pane-text-color);
|
||||
background-color: var(--launcher-pane-background-color);
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ function update(name, value) {
|
||||
}
|
||||
|
||||
function getUserThemes() {
|
||||
const notes = searchService.searchNotes("#appTheme");
|
||||
const notes = searchService.searchNotes("#appTheme", {ignoreHoistedNote: true});
|
||||
const ret = [];
|
||||
|
||||
for (const note of notes) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2023-01-11T23:44:33+01:00", buildRevision: "bdfdc0402ddb23e9af002580f368bc52e4268b3a" };
|
||||
module.exports = { buildDate:"2023-02-13T21:50:54+01:00", buildRevision: "17085e5578d2a20a77a6ade058f74e6d5b798ecc" };
|
||||
|
||||
@@ -71,6 +71,7 @@ module.exports = [
|
||||
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnNoteTitleChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnNoteChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnNoteContentChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnNoteDeletion', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnBranchCreation', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnBranchDeletion', isDangerous: true },
|
||||
|
||||
@@ -148,13 +148,28 @@ class ConsistencyChecks {
|
||||
AND notes.noteId IS NULL`,
|
||||
({branchId, parentNoteId}) => {
|
||||
if (this.autoFix) {
|
||||
const branch = becca.getBranch(branchId);
|
||||
branch.parentNoteId = 'root';
|
||||
branch.save();
|
||||
// Delete the old branch and recreate it with root as parent.
|
||||
const oldBranch = becca.getBranch(branchId);
|
||||
const noteId = oldBranch.noteId;
|
||||
oldBranch.markAsDeleted("missing-parent");
|
||||
|
||||
let message = `Branch '${branchId}' was was missing parent note '${parentNoteId}', so it was deleted. `;
|
||||
|
||||
if (becca.getNote(noteId).getParentBranches().length === 0) {
|
||||
const newBranch = new Branch({
|
||||
parentNoteId: 'root',
|
||||
noteId: noteId,
|
||||
prefix: 'recovered'
|
||||
}).save();
|
||||
|
||||
message += `${newBranch.branchId} was created in the root instead.`;
|
||||
} else {
|
||||
message += `There is one or more valid branches, so no new one will be created as a replacement.`;
|
||||
}
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Branch '${branchId}' was set to root parent since it was referencing missing parent note '${parentNoteId}'`);
|
||||
logFix(message);
|
||||
} else {
|
||||
logError(`Branch '${branchId}' references missing parent note '${parentNoteId}'`);
|
||||
}
|
||||
@@ -428,10 +443,17 @@ class ConsistencyChecks {
|
||||
const branches = branchIds.map(branchId => becca.getBranch(branchId));
|
||||
|
||||
for (const branch of branches) {
|
||||
branch.parentNoteId = 'root';
|
||||
branch.save();
|
||||
// delete the old wrong branch
|
||||
branch.markAsDeleted("parent-is-search");
|
||||
|
||||
logFix(`Child branch '${branch.branchId}' has been moved to root since it was a child of a search note '${parentNoteId}'`)
|
||||
// create a replacement branch in root parent
|
||||
new Branch({
|
||||
parentNoteId: 'root',
|
||||
noteId: branch.noteId,
|
||||
prefix: 'recovered'
|
||||
}).save();
|
||||
|
||||
logFix(`Note '${branch.noteId}' has been moved to root since it was a child of a search note '${parentNoteId}'`)
|
||||
}
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
@@ -100,15 +100,38 @@ function fillEntityChanges(entityName, entityPrimaryKey, condition = '') {
|
||||
if (existingRows === 0) {
|
||||
createdCount++;
|
||||
|
||||
const entity = becca.getEntity(entityName, entityId);
|
||||
let hash;
|
||||
let utcDateChanged;
|
||||
let isSynced;
|
||||
|
||||
if (entityName.endsWith("_contents")) {
|
||||
// FIXME: hacky, not sure if it might cause some problems
|
||||
hash = "fake value";
|
||||
utcDateChanged = dateUtils.utcNowDateTime();
|
||||
isSynced = true; // contents are always synced
|
||||
} else {
|
||||
const entity = becca.getEntity(entityName, entityId);
|
||||
|
||||
if (entity) {
|
||||
hash = entity?.generateHash() || "|deleted";
|
||||
utcDateChanged = entity?.getUtcDateChanged() || dateUtils.utcNowDateTime();
|
||||
isSynced = entityName !== 'options' || !!entity?.isSynced;
|
||||
} else {
|
||||
// entity might be null (not present in becca) when it's deleted
|
||||
// FIXME: hacky, not sure if it might cause some problems
|
||||
hash = "deleted";
|
||||
utcDateChanged = dateUtils.utcNowDateTime();
|
||||
isSynced = true; // deletable (the ones with isDeleted) entities are synced
|
||||
}
|
||||
}
|
||||
|
||||
addEntityChange({
|
||||
entityName,
|
||||
entityId,
|
||||
hash: entity.generateHash(),
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: entity.getUtcDateChanged(),
|
||||
isSynced: entityName !== 'options' || !!entity.isSynced
|
||||
utcDateChanged: utcDateChanged,
|
||||
isSynced: isSynced
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ const treeService = require('./tree');
|
||||
const noteService = require('./notes');
|
||||
const becca = require('../becca/becca');
|
||||
const Attribute = require('../becca/entities/attribute');
|
||||
const hiddenSubtreeService = require("./hidden_subtree");
|
||||
const oneTimeTimer = require("./one_time_timer");
|
||||
|
||||
function runAttachedRelations(note, relationName, originEntity) {
|
||||
if (!note) {
|
||||
@@ -206,6 +208,16 @@ eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) =>
|
||||
if (entityName === 'branches') {
|
||||
runAttachedRelations(entity.getNote(), 'runOnBranchDeletion', entity);
|
||||
}
|
||||
|
||||
if (entityName === 'notes' && entity.noteId.startsWith("_")) {
|
||||
// "named" note has been deleted, we will probably need to rebuild the hidden subtree
|
||||
// scheduling so that bulk deletes won't trigger so many checks
|
||||
oneTimeTimer.scheduleExecution('hidden-subtree-check', 1000, () => {
|
||||
console.log("Checking hidden subtree");
|
||||
|
||||
hiddenSubtreeService.checkHiddenSubtree();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const becca = require("../becca/becca");
|
||||
const noteService = require("./notes");
|
||||
const Attribute = require("../becca/entities/attribute.js");
|
||||
const log = require("./log");
|
||||
const migrationService = require("./migration");
|
||||
|
||||
const LBTPL_ROOT = "_lbTplRoot";
|
||||
const LBTPL_BASE = "_lbTplBase";
|
||||
@@ -173,8 +175,10 @@ const HIDDEN_SUBTREE_DEFINITION = {
|
||||
isExpanded: true,
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
||||
children: [
|
||||
{ id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square' },
|
||||
{ id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square' },
|
||||
{ id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square',
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
|
||||
{ id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square',
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
|
||||
{ id: '_lbBackendLog', title: 'Backend Log', type: 'launcher', targetNoteId: '_backendLog', icon: 'bx bx-terminal' },
|
||||
]
|
||||
},
|
||||
@@ -230,7 +234,13 @@ const HIDDEN_SUBTREE_DEFINITION = {
|
||||
]
|
||||
};
|
||||
|
||||
function checkHiddenSubtree() {
|
||||
function checkHiddenSubtree(force = false) {
|
||||
if (!force && !migrationService.isDbUpToDate()) {
|
||||
// on-delete hook might get triggered during some future migration and cause havoc
|
||||
log.info("Will not check hidden subtree until migration is finished.");
|
||||
return;
|
||||
}
|
||||
|
||||
checkHiddenSubtreeRecursively('root', HIDDEN_SUBTREE_DEFINITION);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@ function importEnex(taskContext, file, parentNote) {
|
||||
content = content.replace(/<\/ol>\s*<li>/g, "</ol></li><li>");
|
||||
|
||||
// Replace en-todo with unicode ballot box
|
||||
content = content.replace(/<en-todo\s+checked="true"\/>/g, "\u2611 ");
|
||||
content = content.replace(/<en-todo(\s+checked="false")?\/>/g, "\u2610 ");
|
||||
content = content.replace(/<en-todo\s+checked="true"\s*\/>/g, "\u2611 ");
|
||||
content = content.replace(/<en-todo(\s+checked="false")?\s*\/>/g, "\u2610 ");
|
||||
|
||||
// Replace OneNote converted checkboxes with unicode ballot box based
|
||||
// on known hash of checkboxes for regular, p1, and p2 checkboxes
|
||||
|
||||
@@ -119,5 +119,6 @@ async function migrateIfNecessary() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
migrateIfNecessary
|
||||
migrateIfNecessary,
|
||||
isDbUpToDate
|
||||
};
|
||||
|
||||
@@ -108,8 +108,13 @@ function getAndValidateParent(params) {
|
||||
throw new ValidationError(`Only 'launcher' notes can be created in parent '${params.parentNoteId}'`);
|
||||
}
|
||||
|
||||
if (!params.ignoreForbiddenParents && (['_lbRoot', '_hidden'].includes(parentNote.noteId) || parentNote.isOptions())) {
|
||||
throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`);
|
||||
if (!params.ignoreForbiddenParents) {
|
||||
if (['_lbRoot', '_hidden'].includes(parentNote.noteId)
|
||||
|| parentNote.noteId.startsWith("_lbTpl")
|
||||
|| parentNote.isOptions()) {
|
||||
|
||||
throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`);
|
||||
}
|
||||
}
|
||||
|
||||
return parentNote;
|
||||
@@ -281,8 +286,12 @@ function protectNote(note, protect) {
|
||||
|
||||
note.isProtected = protect;
|
||||
|
||||
// this will force de/encryption
|
||||
note.setContent(content);
|
||||
// see https://github.com/zadam/trilium/issues/3523
|
||||
// IIRC a zero-sized buffer can be returned as null from the database
|
||||
if (content !== null) {
|
||||
// this will force de/encryption
|
||||
note.setContent(content);
|
||||
}
|
||||
|
||||
note.save();
|
||||
}
|
||||
@@ -590,11 +599,6 @@ function updateNoteContent(noteId, content) {
|
||||
content = saveLinks(note, content);
|
||||
|
||||
note.setContent(content);
|
||||
|
||||
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||
entityName: 'note_contents',
|
||||
entity: note
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,7 +661,8 @@ function undeleteBranch(branchId, deleteId, taskContext) {
|
||||
OR (type = 'relation' AND value = ?))`, [deleteId, note.noteId, note.noteId]);
|
||||
|
||||
for (const attribute of attributes) {
|
||||
new Attribute(attribute).save();
|
||||
// relation might point to a note which hasn't been undeleted yet and would thus throw up
|
||||
new Attribute(attribute).save({skipValidation: true});
|
||||
}
|
||||
|
||||
const childBranchIds = sql.getColumn(`
|
||||
|
||||
25
src/services/one_time_timer.js
Normal file
25
src/services/one_time_timer.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const scheduledExecutions = {};
|
||||
|
||||
/**
|
||||
* Subsequent calls will not move the timer to future. The first caller determines the time of execution.
|
||||
*
|
||||
* The good thing about synchronous better-sqlite3 is that this cannot interrupt transaction. The execution will be called
|
||||
* only outside of a transaction.
|
||||
*/
|
||||
function scheduleExecution(name, milliseconds, cb) {
|
||||
if (name in scheduledExecutions) {
|
||||
return;
|
||||
}
|
||||
|
||||
scheduledExecutions[name] = true;
|
||||
|
||||
setTimeout(() => {
|
||||
delete scheduledExecutions[name];
|
||||
|
||||
cb();
|
||||
}, milliseconds);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scheduleExecution
|
||||
};
|
||||
23
src/services/search/expressions/is_hidden.js
Normal file
23
src/services/search/expressions/is_hidden.js
Normal file
@@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
|
||||
const Expression = require('./expression');
|
||||
const NoteSet = require('../note_set');
|
||||
|
||||
/**
|
||||
* Note is hidden when all its note paths start in hidden subtree (i.e. the note is not cloned into visible tree)
|
||||
*/
|
||||
class IsHiddenExp extends Expression {
|
||||
execute(inputNoteSet, executionContext, searchContext) {
|
||||
const resultNoteSet = new NoteSet();
|
||||
|
||||
for (const note of inputNoteSet.notes) {
|
||||
if (note.isHiddenCompletely()) {
|
||||
resultNoteSet.add(note);
|
||||
}
|
||||
}
|
||||
|
||||
return resultNoteSet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IsHiddenExp;
|
||||
@@ -6,10 +6,11 @@ class SearchContext {
|
||||
constructor(params = {}) {
|
||||
this.fastSearch = !!params.fastSearch;
|
||||
this.includeArchivedNotes = !!params.includeArchivedNotes;
|
||||
this.includeHiddenNotes = !!params.includeHiddenNotes;
|
||||
this.ignoreHoistedNote = !!params.ignoreHoistedNote;
|
||||
this.ancestorNoteId = params.ancestorNoteId;
|
||||
|
||||
if (!this.ancestorNoteId && !this.ignoreHoistedNote && !hoistedNoteService.isHoistedInHiddenSubtree()) {
|
||||
if (!this.ancestorNoteId && !this.ignoreHoistedNote) {
|
||||
// hoisting in hidden subtree should not limit autocomplete
|
||||
// since we want to link (create relations) to the normal non-hidden notes
|
||||
this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
@@ -18,7 +18,8 @@ const AncestorExp = require("../expressions/ancestor");
|
||||
const buildComparator = require('./build_comparator');
|
||||
const ValueExtractor = require('../value_extractor');
|
||||
const utils = require("../../utils");
|
||||
const TrueExp = require("../expressions/true.js");
|
||||
const TrueExp = require("../expressions/true");
|
||||
const IsHiddenExp = require("../expressions/is_hidden");
|
||||
|
||||
function getFulltext(tokens, searchContext) {
|
||||
tokens = tokens.map(t => utils.removeDiacritic(t.token));
|
||||
@@ -429,7 +430,7 @@ function parse({fulltextTokens, expressionTokens, searchContext}) {
|
||||
|
||||
let exp = AndExp.of([
|
||||
searchContext.includeArchivedNotes ? null : new PropertyComparisonExp(searchContext, "isarchived", "=", "false"),
|
||||
(searchContext.ancestorNoteId && searchContext.ancestorNoteId !== 'root') ? new AncestorExp(searchContext.ancestorNoteId, searchContext.ancestorDepth) : null,
|
||||
getAncestorExp(searchContext),
|
||||
getFulltext(fulltextTokens, searchContext),
|
||||
expression
|
||||
]);
|
||||
@@ -448,4 +449,14 @@ function parse({fulltextTokens, expressionTokens, searchContext}) {
|
||||
return exp;
|
||||
}
|
||||
|
||||
function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}) {
|
||||
if (ancestorNoteId && ancestorNoteId !== 'root') {
|
||||
return new AncestorExp(ancestorNoteId, ancestorDepth);
|
||||
} else if (!includeHiddenNotes) {
|
||||
return new NotExp(new IsHiddenExp());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = parse;
|
||||
|
||||
@@ -11,6 +11,7 @@ const beccaService = require('../../../becca/becca_service');
|
||||
const utils = require('../../utils');
|
||||
const log = require('../../log');
|
||||
const scriptService = require("../../script");
|
||||
const hoistedNoteService = require("../../hoisted_note");
|
||||
|
||||
function searchFromNote(note) {
|
||||
let searchResultNoteIds, highlightedTokens;
|
||||
@@ -271,7 +272,11 @@ function searchNotesForAutocomplete(query) {
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: true,
|
||||
includeArchivedNotes: false,
|
||||
fuzzyAttributeSearch: true
|
||||
includeHiddenNotes: true,
|
||||
fuzzyAttributeSearch: true,
|
||||
ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree()
|
||||
? 'root'
|
||||
: hoistedNoteService.getHoistedNoteId()
|
||||
});
|
||||
|
||||
const allSearchResults = findResultsWithQuery(query, searchContext);
|
||||
|
||||
@@ -218,7 +218,7 @@ function resetLauncher(noteId) {
|
||||
log.info(`Note ${noteId} is not a resettable launcher note.`);
|
||||
}
|
||||
|
||||
hiddenSubtreeService.checkHiddenSubtree();
|
||||
// the re-building deleted launchers will be done in handlers
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ const {JSDOM} = require("jsdom");
|
||||
const shaca = require("./shaca/shaca");
|
||||
const assetPath = require("../services/asset_path");
|
||||
const shareRoot = require('./share_root');
|
||||
const escapeHtml = require('escape-html');
|
||||
|
||||
function getContent(note) {
|
||||
if (note.isProtected) {
|
||||
@@ -112,17 +113,17 @@ function renderCode(result) {
|
||||
|
||||
function renderMermaid(result) {
|
||||
result.content = `
|
||||
<div class="mermaid">${result.content}</div>
|
||||
<div class="mermaid">${escapeHtml(result.content)}</div>
|
||||
<hr>
|
||||
<details>
|
||||
<summary>Chart source</summary>
|
||||
<pre>${result.content}</pre>
|
||||
<pre>${escapeHtml(result.content)}</pre>
|
||||
</details>`
|
||||
result.header += `<script src="../../${assetPath}/libraries/mermaid.min.js"></script>`;
|
||||
}
|
||||
|
||||
function renderImage(result, note) {
|
||||
result.content = `<img src="api/images/${note.noteId}/${note.title}?${note.utcDateModified}">`;
|
||||
result.content = `<img src="api/images/${note.noteId}/${note.escapedTitle}?${note.utcDateModified}">`;
|
||||
}
|
||||
|
||||
function renderFile(note, result) {
|
||||
|
||||
Reference in New Issue
Block a user