Compare commits

...

32 Commits

Author SHA1 Message Date
zadam
265401775b release 0.48.9 2021-12-22 22:39:24 +01:00
zadam
10a5773c66 added consistency check to reconcile erased status between entity_changes and data 2021-12-21 14:25:26 +01:00
zadam
1e8472266f small fixes 2021-12-21 13:22:13 +01:00
zadam
b30792a3da make sure that the info about periodically erased entities is sent to the frontend 2021-12-21 11:04:41 +01:00
zadam
26602e8226 focus "jump to note" in new tab when there are no workspaces, #2455 2021-12-20 22:10:21 +01:00
zadam
b8eeb0371c fixed bug in search where note with inherited attribute is not matched when the owning note is not matched, #2457 2021-12-20 21:35:12 +01:00
zadam
b1c4737e78 fixed search tests 2021-12-20 21:31:44 +01:00
zadam
e29aee1aae use lazy title loading in bookmarks buttons to have decrypted title upon protected session start, #2393 2021-12-19 10:36:48 +01:00
zadam
1aff42f453 fix setting exported file extension when truncated 2021-12-17 22:03:00 +01:00
zadam
a098630e09 add default JPG quality if value not in range 2021-12-16 22:10:51 +01:00
zadam
074eb1c02f importing of cloned notes should not depend on ZIP listing order, fixes #2440 2021-12-15 22:36:45 +01:00
zadam
a81ea3771f release 0.48.8 2021-12-13 11:12:31 +01:00
zadam
d9550dd59b fix "getNoteStartingWith" relic 2021-12-11 14:15:38 +01:00
zadam
a810c08c02 trigger note revisioning saving also on title changes #2426 2021-12-08 21:04:22 +01:00
zadam
263b7a84bb full text search should look into link URLs as well, closes #2412 2021-12-06 20:54:37 +01:00
zadam
9d18bebb13 "show in full search" closes the quick search dropdown 2021-12-06 20:43:50 +01:00
zadam
26bcfe5160 fix hidden notes appearing in note map, closes #2403 2021-12-04 13:45:15 +01:00
zadam
89c04e6b6b fix "note paths" ribbon widget for root note 2021-12-04 13:33:31 +01:00
zadam
40fb4ff56b fix for "Today page does not work for 2021-11-20", closes #2359 2021-11-30 21:21:16 +01:00
zadam
d64c14482b after removing last promoted attribute, create a blank one, fixes #2388 2021-11-26 23:39:08 +01:00
zadam
564366861e prevent browser from caching images/files, fixes #2378 2021-11-25 20:24:42 +01:00
zadam
8c11d022fb release 0.48.7 2021-11-23 21:53:32 +01:00
Myzel394
24210ef80c fixed settings menu (#2374)
(cherry picked from commit 3f40a52f65)
2021-11-23 21:38:13 +01:00
zadam
2135aa058e increase sync version to 22 2021-11-21 16:16:28 +01:00
zadam
67542f448d fix total height / scrolling on mobile chrome/safari, closes #2367 2021-11-21 13:39:47 +01:00
zadam
08e9b59696 hide hidden subtree notes from search results, closes #2361 2021-11-20 21:01:37 +01:00
zadam
fe605c012a fix setting monospace font from theme 2021-11-20 13:20:06 +01:00
zadam
4c7c53d8c8 retry for OpenNoteButtonWidget 2021-11-20 12:49:12 +01:00
zadam
d345b7ed56 added entity changes check after sync check failure, fixed sync 2021-11-17 22:57:09 +01:00
zadam
298af217e9 fix bug overwriting entity changes 2021-11-17 21:47:41 +01:00
zadam
7d64f6a7dd increment lastProcessedEntityChangeId correctly 2021-11-16 22:12:53 +01:00
zadam
bc8b6284a6 fix exporting root note, closes #2346
(cherry picked from commit 20a187fab9)
2021-11-15 21:28:12 +01:00
47 changed files with 11362 additions and 214 deletions

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

11080
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.48.6", "version": "0.48.9",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@@ -81,7 +81,7 @@
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "13.6.1", "electron": "13.6.3",
"electron-builder": "22.13.1", "electron-builder": "22.13.1",
"electron-packager": "15.4.0", "electron-packager": "15.4.0",
"electron-rebuild": "3.2.3", "electron-rebuild": "3.2.3",

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,12 +30,12 @@ const TPL = `
<div class="form-group"> <div class="form-group">
<label for="image-max-width-height">Max width / height of an image in pixels (image will be resized if it exceeds this setting).</label> <label for="image-max-width-height">Max width / height of an image in pixels (image will be resized if it exceeds this setting).</label>
<input class="form-control" id="image-max-width-height" type="number"> <input class="form-control" id="image-max-width-height" type="number" min="1">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="image-jpeg-quality">JPEG quality (0 - worst quality, 100 best quality, 50 - 85 is recommended)</label> <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="0" max="100" type="number"> <input class="form-control" id="image-jpeg-quality" min="10" max="100" type="number">
</div> </div>
</div> </div>
@@ -67,7 +67,7 @@ const TPL = `
<div class="form-group"> <div class="form-group">
<label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label> <label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
<input class="form-control" id="protected-session-timeout-in-seconds" type="number"> <input class="form-control" id="protected-session-timeout-in-seconds" type="number" min="60">
</div> </div>
</div> </div>
@@ -78,7 +78,7 @@ const TPL = `
<div class="form-group"> <div class="form-group">
<label for="note-revision-snapshot-time-interval-in-seconds">Note revision snapshot time interval (in seconds)</label> <label for="note-revision-snapshot-time-interval-in-seconds">Note revision snapshot time interval (in seconds)</label>
<input class="form-control" id="note-revision-snapshot-time-interval-in-seconds" type="number"> <input class="form-control" id="note-revision-snapshot-time-interval-in-seconds" type="number" min="10">
</div> </div>
</div> </div>
@@ -89,12 +89,12 @@ const TPL = `
<div class="form-group"> <div class="form-group">
<label for="auto-readonly-size-text">Automatic readonly size (text notes)</label> <label for="auto-readonly-size-text">Automatic readonly size (text notes)</label>
<input class="form-control" id="auto-readonly-size-text" type="number"> <input class="form-control" id="auto-readonly-size-text" type="number" min="0">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="auto-readonly-size-code">Automatic readonly size (code notes)</label> <label for="auto-readonly-size-code">Automatic readonly size (code notes)</label>
<input class="form-control" id="auto-readonly-size-code" type="number"> <input class="form-control" id="auto-readonly-size-code" type="number" min="0">
</div> </div>
</div>`; </div>`;

View File

@@ -88,7 +88,7 @@ export default class MobileLayout {
return new FlexContainer('row').cssBlock(MOBILE_CSS) return new FlexContainer('row').cssBlock(MOBILE_CSS)
.setParent(appContext) .setParent(appContext)
.id('root-widget') .id('root-widget')
.css('height', '100vh') .css('height', '100%')
.child(new ScreenContainer("tree", 'column') .child(new ScreenContainer("tree", 'column')
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-4 col-xl-4") .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-4 col-xl-4")
.css("max-height", "100%") .css("max-height", "100%")

View File

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

View File

@@ -83,6 +83,10 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
if (someNotePath) { // in case it's root the path may be empty if (someNotePath) { // in case it's root the path may be empty
const pathToRoot = someNotePath.split("/").reverse().slice(1); const pathToRoot = someNotePath.split("/").reverse().slice(1);
if (!pathToRoot.includes("root")) {
pathToRoot.push('root');
}
for (const noteId of pathToRoot) { for (const noteId of pathToRoot) {
effectivePathSegments.push(noteId); effectivePathSegments.push(noteId);
} }

View File

@@ -181,9 +181,9 @@ async function consumeFrontendUpdateData() {
for (const entityChange of nonProcessedEntityChanges) { for (const entityChange of nonProcessedEntityChanges) {
processedEntityChangeIds.add(entityChange.id); processedEntityChangeIds.add(entityChange.id);
}
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, allEntityChanges[allEntityChanges.length - 1].id); lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, entityChange.id);
}
} }
checkEntityChangeIdListeners(); checkEntityChangeIdListeners();

View File

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

View File

@@ -5,8 +5,25 @@ import froca from "../../services/froca.js";
export default class OpenNoteButtonWidget extends ButtonWidget { export default class OpenNoteButtonWidget extends ButtonWidget {
targetNote(noteId) { targetNote(noteId) {
froca.getNote(noteId).then(note => { froca.getNote(noteId).then(note => {
if (!note) {
console.log(`Note ${noteId} has not been found. This might happen on the first run before the target note is created.`);
if (!this.retried) {
this.retried = true;
setTimeout(() => this.targetNote(noteId), 15000); // should be higher than timeout for createMissingSpecialNotes
}
return;
}
this.icon(note.getIcon()); this.icon(note.getIcon());
this.title(note.title); this.title(() => {
const n = froca.getNoteFromCache(noteId);
// always fresh, always decoded (when protected session is available)
return n.title;
});
this.refreshIcon(); this.refreshIcon();
}); });

View File

@@ -7,7 +7,7 @@ export default class RootContainer extends FlexContainer {
super('row'); super('row');
this.id('root-widget'); this.id('root-widget');
this.css('height', '100vh'); this.css('height', '100%');
} }
refresh() { refresh() {

View File

@@ -150,6 +150,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$tree = this.$widget.find('.tree'); this.$tree = this.$widget.find('.tree');
this.$treeActions = this.$widget.find(".tree-actions");
this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist()); this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist());
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e)); this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
@@ -200,20 +201,16 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.$hideIncludedImages.prop("checked", this.hideIncludedImages); this.$hideIncludedImages.prop("checked", this.hideIncludedImages);
this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree); this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree);
let top = this.$treeSettingsButton[0].offsetTop; const top = this.$treeActions[0].offsetTop - (this.$treeSettingsPopup.outerHeight());
let left = this.$treeSettingsButton[0].offsetLeft; const left = Math.max(
top -= this.$treeSettingsPopup.outerHeight() + 10; 0,
left += this.$treeSettingsButton.outerWidth() - this.$treeSettingsPopup.outerWidth(); this.$treeActions[0].offsetLeft - this.$treeSettingsPopup.outerWidth() + this.$treeActions.outerWidth()
);
if (left < 0) {
left = 0;
}
this.$treeSettingsPopup.css({ this.$treeSettingsPopup.css({
display: "block", top,
top: top, left
left: left }).show();
}).addClass("show");
return false; return false;
}); });

View File

@@ -134,6 +134,8 @@ export default class QuickSearchWidget extends BasicWidget {
} }
async showInFullSearch() { async showInFullSearch() {
this.$dropdownToggle.dropdown("hide");
const searchNote = await dateNotesService.createSearchNote({searchString: this.$searchString.val()}); const searchNote = await dateNotesService.createSearchNote({searchString: this.$searchString.val()});
await froca.loadSearchNote(searchNote.noteId); await froca.loadSearchNote(searchNote.noteId);

View File

@@ -69,7 +69,10 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
this.$notePathList.empty(); this.$notePathList.empty();
if (this.noteId === 'root') { if (this.noteId === 'root') {
await this.getRenderedPath('root'); this.$notePathList.empty().append(
await this.getRenderedPath('root')
);
return; return;
} }
@@ -94,7 +97,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
this.$notePathList.empty().append(...renderedPaths); this.$notePathList.empty().append(...renderedPaths);
} }
async getRenderedPath(notePath, notePathRecord) { async getRenderedPath(notePath, notePathRecord = null) {
const title = await treeService.getNotePathTitle(notePath); const title = await treeService.getNotePathTitle(notePath);
const $noteLink = await linkService.createNoteLink(notePath, {title}); const $noteLink = await linkService.createNoteLink(notePath, {title});
@@ -109,20 +112,20 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
$noteLink.addClass("path-current"); $noteLink.addClass("path-current");
} }
if (notePathRecord.isInHoistedSubTree) { if (!notePathRecord || notePathRecord.isInHoistedSubTree) {
$noteLink.addClass("path-in-hoisted-subtree"); $noteLink.addClass("path-in-hoisted-subtree");
} }
else { else {
icons.push(`<span class="bx bx-trending-up" title="This path is outside of hoisted note and you would have to unhoist."></span>`); icons.push(`<span class="bx bx-trending-up" title="This path is outside of hoisted note and you would have to unhoist."></span>`);
} }
if (notePathRecord.isArchived) { if (notePathRecord?.isArchived) {
$noteLink.addClass("path-archived"); $noteLink.addClass("path-archived");
icons.push(`<span class="bx bx-archive" title="Archived"></span>`); icons.push(`<span class="bx bx-archive" title="Archived"></span>`);
} }
if (notePathRecord.isSearch) { if (notePathRecord?.isSearch) {
$noteLink.addClass("path-search"); $noteLink.addClass("path-search");
icons.push(`<span class="bx bx-search" title="Search"></span>`); icons.push(`<span class="bx bx-search" title="Search"></span>`);

View File

@@ -115,9 +115,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
const $input = $("<input>") const $input = $("<input>")
.prop("tabindex", 200 + definitionAttr.position) .prop("tabindex", 200 + definitionAttr.position)
.prop("attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one .attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.prop("attribute-type", valueAttr.type) .attr("data-attribute-type", valueAttr.type)
.prop("attribute-name", valueAttr.name) .attr("data-attribute-name", valueAttr.name)
.prop("value", valueAttr.value) .prop("value", valueAttr.value)
.addClass("form-control") .addClass("form-control")
.addClass("promoted-attribute-input") .addClass("promoted-attribute-input")
@@ -230,7 +230,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
} }
if (definition.multiplicity === "multi") { if (definition.multiplicity === "multi") {
const addButton = $("<span>") const $addButton = $("<span>")
.addClass("bx bx-plus pointer") .addClass("bx bx-plus pointer")
.prop("title", "Add new attribute") .prop("title", "Add new attribute")
.on('click', async () => { .on('click', async () => {
@@ -246,12 +246,28 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
$new.find('input').trigger('focus'); $new.find('input').trigger('focus');
}); });
const removeButton = $("<span>") const $removeButton = $("<span>")
.addClass("bx bx-trash pointer") .addClass("bx bx-trash pointer")
.prop("title", "Remove this attribute") .prop("title", "Remove this attribute")
.on('click', async () => { .on('click', async () => {
if (valueAttr.attributeId) { const attributeId = $input.attr("data-attribute-id");
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId, this.componentId);
if (attributeId) {
await server.remove("notes/" + this.noteId + "/attributes/" + attributeId, this.componentId);
}
// if it's the last one the create new empty form immediately
const sameAttrSelector = `input[data-attribute-type='${valueAttr.type}'][data-attribute-name='${valueName}']`;
if (this.$widget.find(sameAttrSelector).length <= 1) {
const $new = await this.createPromotedAttributeCell(definitionAttr, {
attributeId: "",
type: valueAttr.type,
name: valueName,
value: ""
}, valueName);
$wrapper.after($new);
} }
$wrapper.remove(); $wrapper.remove();
@@ -259,9 +275,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
$multiplicityCell $multiplicityCell
.append(" &nbsp;") .append(" &nbsp;")
.append(addButton) .append($addButton)
.append(" &nbsp;") .append(" &nbsp;")
.append(removeButton); .append($removeButton);
} }
return $wrapper; return $wrapper;
@@ -275,7 +291,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
if ($attr.prop("type") === "checkbox") { if ($attr.prop("type") === "checkbox") {
value = $attr.is(':checked') ? "true" : "false"; value = $attr.is(':checked') ? "true" : "false";
} }
else if ($attr.prop("attribute-type") === "relation") { else if ($attr.attr("data-attribute-type") === "relation") {
const selectedPath = $attr.getSelectedNotePath(); const selectedPath = $attr.getSelectedNotePath();
value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : ""; value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : "";
@@ -285,13 +301,13 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
} }
const result = await server.put(`notes/${this.noteId}/attribute`, { const result = await server.put(`notes/${this.noteId}/attribute`, {
attributeId: $attr.prop("attribute-id"), attributeId: $attr.attr("data-attribute-id"),
type: $attr.prop("attribute-type"), type: $attr.attr("data-attribute-type"),
name: $attr.prop("attribute-name"), name: $attr.attr("data-attribute-name"),
value: value value: value
}, this.componentId); }, this.componentId);
$attr.prop("attribute-id", result.attributeId); $attr.attr("data-attribute-id", result.attributeId);
} }
entitiesReloadedEvent({loadResults}) { entitiesReloadedEvent({loadResults}) {

View File

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

View File

@@ -57,9 +57,7 @@ class ImageTypeWidget extends TypeWidget {
} }
async doRefresh(note) { async doRefresh(note) {
const imageHash = utils.randomString(10); this.$imageView.prop("src", `api/images/${note.noteId}/${note.title}`);
this.$imageView.prop("src", `api/images/${note.noteId}/${note.title}?${imageHash}`);
} }
copyImageToClipboardEvent({ntxId}) { copyImageToClipboardEvent({ntxId}) {

View File

@@ -18,9 +18,11 @@ body {
on the last line of the editor. */ on the last line of the editor. */
position: fixed; position: fixed;
width: 100%; width: 100%;
height: 100%;
background-color: var(--main-background-color); background-color: var(--main-background-color);
color: var(--main-text-color); color: var(--main-text-color);
font-family: var(--main-font-family); font-family: var(--main-font-family);
font-size: var(--main-font-size);
} }
a, a:visited, a:hover { a, a:visited, a:hover {
@@ -58,7 +60,7 @@ table td, table th {
} }
code, kbd, pre, samp { code, kbd, pre, samp {
font-family: var(--monospace-font-family); font-family: var(--monospace-font-family) !important;
} }
.input-group-text { .input-group-text {
@@ -714,10 +716,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
border-color: var(--main-border-color) !important; border-color: var(--main-border-color) !important;
} }
body {
font-size: var(--main-font-size);
}
.gutter { .gutter {
background: linear-gradient(to bottom, transparent, var(--accented-background-color), transparent); background: linear-gradient(to bottom, transparent, var(--accented-background-color), transparent);
} }

View File

@@ -60,6 +60,7 @@ function downloadNoteFile(noteId, res, contentDisposition = true) {
res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
} }
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime); res.setHeader('Content-Type', note.mime);
res.send(note.getContent()); res.send(note.getContent());

View File

@@ -20,6 +20,7 @@ function returnImage(req, res) {
} }
res.set('Content-Type', image.mime); res.set('Content-Type', image.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(image.getContent()); res.send(image.getContent());
} }

View File

@@ -42,6 +42,11 @@ function getNeighbors(note, depth) {
} }
const targetNote = relation.getTargetNote(); const targetNote = relation.getTargetNote();
if (targetNote.hasLabel('excludeFromNoteMap')) {
continue;
}
retNoteIds.push(targetNote.noteId); retNoteIds.push(targetNote.noteId);
for (const noteId of getNeighbors(targetNote, depth - 1)) { for (const noteId of getNeighbors(targetNote, depth - 1)) {
@@ -56,6 +61,11 @@ function getNeighbors(note, depth) {
} }
const sourceNote = relation.getNote(); const sourceNote = relation.getNote();
if (sourceNote.hasLabel('excludeFromNoteMap')) {
continue;
}
retNoteIds.push(sourceNote.noteId); retNoteIds.push(sourceNote.noteId);
for (const noteId of getNeighbors(sourceNote, depth - 1)) { for (const noteId of getNeighbors(sourceNote, depth - 1)) {

View File

@@ -203,6 +203,10 @@ function changeTitle(req) {
const noteTitleChanged = note.title !== title; const noteTitleChanged = note.title !== title;
if (noteTitleChanged) {
noteService.saveNoteRevision(note);
}
note.title = title; note.title = title;
note.save(); note.save();

View File

@@ -204,6 +204,11 @@ function queueSector(req) {
entityChangesService.addEntityChangesForSector(entityName, sector); entityChangesService.addEntityChangesForSector(entityName, sector);
} }
function checkEntityChanges() {
const consistencyChecks = require("../../services/consistency_checks");
consistencyChecks.runEntityChangesChecks();
}
module.exports = { module.exports = {
testSync, testSync,
checkSync, checkSync,
@@ -215,5 +220,6 @@ module.exports = {
update, update,
getStats, getStats,
syncFinished, syncFinished,
queueSector queueSector,
checkEntityChanges
}; };

View File

@@ -294,6 +294,7 @@ function register(app) {
route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler); route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler);
route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler); route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler);
route(POST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler); route(POST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler);
route(POST, '/api/sync/check-entity-changes', [auth.checkApiAuth], syncApiRoute.checkEntityChanges, apiResultHandler);
route(POST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler); route(POST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler);
route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler); route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler);

View File

@@ -5,7 +5,7 @@ const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir'); const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 185; const APP_DB_VERSION = 185;
const SYNC_VERSION = 21; const SYNC_VERSION = 22;
const CLIPPER_PROTOCOL_VERSION = "1.0"; const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = { module.exports = {

View File

@@ -62,6 +62,7 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'relation', name: 'renderNote', isDangerous: true } { type: 'relation', name: 'renderNote', isDangerous: true }
]; ];
/** @returns {Note[]} */
function getNotesWithLabel(name, value) { function getNotesWithLabel(name, value) {
const query = formatAttrForSearch({type: 'label', name, value}, true); const query = formatAttrForSearch({type: 'label', name, value}, true);
return searchService.searchNotes(query, { return searchService.searchNotes(query, {
@@ -71,6 +72,7 @@ function getNotesWithLabel(name, value) {
} }
// TODO: should be in search service // TODO: should be in search service
/** @returns {Note|null} */
function getNoteWithLabel(name, value) { function getNoteWithLabel(name, value) {
// optimized version (~20 times faster) without using normal search, useful for e.g. finding date notes // optimized version (~20 times faster) without using normal search, useful for e.g. finding date notes
const attrs = becca.findAttributes('label', name); const attrs = becca.findAttributes('label', name);

View File

@@ -1 +1 @@
module.exports = { buildDate:"2021-11-13T22:49:58+01:00", buildRevision: "c94603010630cfafe64575ab378c482bb39fb083" }; module.exports = { buildDate:"2021-12-22T22:39:24+01:00", buildRevision: "10a5773c66e678c6d2bd3bd9dc9952d5cd76b795" };

View File

@@ -258,7 +258,8 @@ class ConsistencyChecks {
FROM branches FROM branches
WHERE noteId = ? WHERE noteId = ?
and parentNoteId = ? and parentNoteId = ?
and isDeleted = 0`, [noteId, parentNoteId]); and isDeleted = 0
ORDER BY utcDateCreated`, [noteId, parentNoteId]);
const branches = branchIds.map(branchId => becca.getBranch(branchId)); const branches = branchIds.map(branchId => becca.getBranch(branchId));
@@ -537,6 +538,27 @@ class ConsistencyChecks {
logError(`Unrecognized entity change id=${id}, entityName=${entityName}, entityId=${entityId}`); 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() { findEntityChangeIssues() {
@@ -603,14 +625,14 @@ class ConsistencyChecks {
this.fixedIssues = false; this.fixedIssues = false;
this.reloadNeeded = false; this.reloadNeeded = false;
this.findEntityChangeIssues();
this.findBrokenReferenceIssues(); this.findBrokenReferenceIssues();
this.findExistencyIssues(); this.findExistencyIssues();
this.findLogicIssues(); this.findLogicIssues();
this.findEntityChangeIssues();
this.findWronglyNamedAttributes(); this.findWronglyNamedAttributes();
this.findSyncIssues(); this.findSyncIssues();
@@ -701,6 +723,11 @@ function runOnDemandChecks(autoFix) {
consistencyChecks.runChecks(); consistencyChecks.runChecks();
} }
function runEntityChangesChecks() {
const consistencyChecks = new ConsistencyChecks(true);
consistencyChecks.findEntityChangeIssues();
}
sqlInit.dbReady.then(() => { sqlInit.dbReady.then(() => {
setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000); setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000);
@@ -709,5 +736,6 @@ sqlInit.dbReady.then(() => {
}); });
module.exports = { module.exports = {
runOnDemandChecks runOnDemandChecks,
runEntityChangesChecks
}; };

View File

@@ -25,15 +25,6 @@ function createNote(parentNote, noteTitle) {
}).note; }).note;
} }
function getNoteStartingWith(parentNoteId, startsWith) {
const noteId = sql.getValue(`SELECT notes.noteId FROM notes JOIN branches USING(noteId)
WHERE parentNoteId = ? AND title LIKE '${startsWith}%'
AND notes.isDeleted = 0 AND isProtected = 0
AND branches.isDeleted = 0`, [parentNoteId]);
return becca.getNote(noteId);
}
/** @returns {Note} */ /** @returns {Note} */
function getRootCalendarNote() { function getRootCalendarNote() {
let rootNote = attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL); let rootNote = attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
@@ -65,8 +56,7 @@ function getYearNote(dateStr, rootNote) {
const yearStr = dateStr.substr(0, 4); const yearStr = dateStr.substr(0, 4);
let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr) let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr);
|| getNoteStartingWith(rootNote.noteId, yearStr);
if (yearNote) { if (yearNote) {
return yearNote; return yearNote;
@@ -112,18 +102,12 @@ function getMonthNote(dateStr, rootNote) {
return monthNote; return monthNote;
} }
const yearNote = getYearNote(dateStr, rootNote);
monthNote = getNoteStartingWith(yearNote.noteId, monthNumber);
if (monthNote) {
return monthNote;
}
const dateObj = dateUtils.parseLocalDate(dateStr); const dateObj = dateUtils.parseLocalDate(dateStr);
const noteTitle = getMonthNoteTitle(rootNote, monthNumber, dateObj); const noteTitle = getMonthNoteTitle(rootNote, monthNumber, dateObj);
const yearNote = getYearNote(dateStr, rootNote);
sql.transactional(() => { sql.transactional(() => {
monthNote = createNote(yearNote, noteTitle); monthNote = createNote(yearNote, noteTitle);
@@ -164,12 +148,6 @@ function getDateNote(dateStr) {
const monthNote = getMonthNote(dateStr, rootNote); const monthNote = getMonthNote(dateStr, rootNote);
const dayNumber = dateStr.substr(8, 2); const dayNumber = dateStr.substr(8, 2);
dateNote = getNoteStartingWith(monthNote.noteId, dayNumber);
if (dateNote) {
return dateNote;
}
const dateObj = dateUtils.parseLocalDate(dateStr); const dateObj = dateUtils.parseLocalDate(dateStr);
const noteTitle = getDateNoteTitle(rootNote, dayNumber, dateObj); const noteTitle = getDateNoteTitle(rootNote, dayNumber, dateObj);

View File

@@ -7,12 +7,10 @@ const becca = require("../becca/becca");
let maxEntityChangeId = 0; let maxEntityChangeId = 0;
function addEntityChange(origEntityChange, keepOriginalId = false) { function addEntityChange(origEntityChange) {
const ec = {...origEntityChange}; const ec = {...origEntityChange};
if (!keepOriginalId) { delete ec.id;
delete ec.id;
}
ec.sourceId = ec.sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId(); ec.sourceId = ec.sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId();
ec.isSynced = ec.isSynced ? 1 : 0; ec.isSynced = ec.isSynced ? 1 : 0;

View File

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

View File

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

View File

@@ -351,7 +351,19 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
let note = becca.getNote(noteId); let note = becca.getNote(noteId);
const isProtected = importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable();
if (note) { 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); note.setContent(content);
} }
else { else {
@@ -367,7 +379,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
// root notePosition should be ignored since it relates to original document // root notePosition should be ignored since it relates to original document
// now import root should be placed after existing notes into new parent // now import root should be placed after existing notes into new parent
notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined, notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined,
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), isProtected: isProtected,
})); }));
createdNoteIds[note.noteId] = true; createdNoteIds[note.noteId] = true;

View File

@@ -732,23 +732,26 @@ function eraseAttributes(attributeIdsToErase) {
} }
function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) { function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) {
if (eraseEntitiesAfterTimeInSeconds === null) { // this is important also so that the erased entity changes are sent to the connected clients
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds'); sql.transactional(() => {
} 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) { function eraseNotesWithDeleteId(deleteId) {
@@ -918,5 +921,6 @@ module.exports = {
getUndeletedParentBranchIds, getUndeletedParentBranchIds,
triggerNoteTitleChanged, triggerNoteTitleChanged,
eraseDeletedNotesNow, eraseDeletedNotesNow,
eraseNotesWithDeleteId eraseNotesWithDeleteId,
saveNoteRevision
}; };

View File

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

View File

@@ -8,6 +8,7 @@ const protectedSessionService = require('../../protected_session');
const striptags = require('striptags'); const striptags = require('striptags');
const utils = require("../../utils"); const utils = require("../../utils");
// FIXME: create common subclass with NoteContentUnprotectedFulltextExp to avoid duplication
class NoteContentProtectedFulltextExp extends Expression { class NoteContentProtectedFulltextExp extends Expression {
constructor(operator, tokens, raw) { constructor(operator, tokens, raw) {
super(); super();
@@ -46,15 +47,7 @@ class NoteContentProtectedFulltextExp extends Expression {
continue; continue;
} }
content = utils.normalize(content); content = this.preprocessContent(content, type, mime);
if (type === 'text' && mime === 'text/html') {
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
content = striptags(content);
}
content = content.replace(/&nbsp;/g, ' ');
}
if (!this.tokens.find(token => !content.includes(token))) { if (!this.tokens.find(token => !content.includes(token))) {
resultNoteSet.add(becca.notes[noteId]); resultNoteSet.add(becca.notes[noteId]);
@@ -63,6 +56,23 @@ class NoteContentProtectedFulltextExp extends Expression {
return resultNoteSet; return resultNoteSet;
} }
preprocessContent(content, type, mime) {
content = utils.normalize(content.toString());
if (type === 'text' && mime === 'text/html') {
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
// allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
content = striptags(content, ['a']);
// at least the closing tag can be easily stripped
content = content.replace(/<\/a>/ig, "");
}
content = content.replace(/&nbsp;/g, ' ');
}
return content;
}
} }
module.exports = NoteContentProtectedFulltextExp; module.exports = NoteContentProtectedFulltextExp;

View File

@@ -6,6 +6,7 @@ const becca = require('../../../becca/becca');
const striptags = require('striptags'); const striptags = require('striptags');
const utils = require("../../utils"); const utils = require("../../utils");
// FIXME: create common subclass with NoteContentProtectedFulltextExp to avoid duplication
class NoteContentUnprotectedFulltextExp extends Expression { class NoteContentUnprotectedFulltextExp extends Expression {
constructor(operator, tokens, raw) { constructor(operator, tokens, raw) {
super(); super();
@@ -32,15 +33,7 @@ class NoteContentUnprotectedFulltextExp extends Expression {
continue; continue;
} }
content = utils.normalize(content.toString()); content = this.preprocessContent(content, type, mime);
if (type === 'text' && mime === 'text/html') {
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
content = striptags(content);
}
content = content.replace(/&nbsp;/g, ' ');
}
if (!this.tokens.find(token => !content.includes(token))) { if (!this.tokens.find(token => !content.includes(token))) {
resultNoteSet.add(becca.notes[noteId]); resultNoteSet.add(becca.notes[noteId]);
@@ -49,6 +42,23 @@ class NoteContentUnprotectedFulltextExp extends Expression {
return resultNoteSet; return resultNoteSet;
} }
preprocessContent(content, type, mime) {
content = utils.normalize(content.toString());
if (type === 'text' && mime === 'text/html') {
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
// allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
content = striptags(content, ['a']);
// at least the closing tag can be easily stripped
content = content.replace(/<\/a>/ig, "");
}
content = content.replace(/&nbsp;/g, ' ');
}
return content;
}
} }
module.exports = NoteContentUnprotectedFulltextExp; module.exports = NoteContentUnprotectedFulltextExp;

View File

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

View File

@@ -83,8 +83,12 @@ function findResultsWithExpression(expression, searchContext) {
throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`); throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);
} }
if (notePathArray.includes("hidden")) {
return null;
}
return new SearchResult(notePathArray); return new SearchResult(notePathArray);
}); }).filter(Boolean);
for (const res of searchResults) { for (const res of searchResults) {
res.computeScore(searchContext.highlightedTokens); res.computeScore(searchContext.highlightedTokens);

View File

@@ -34,6 +34,7 @@ function getHiddenRoot() {
if (!hidden) { if (!hidden) {
hidden = noteService.createNewNote({ hidden = noteService.createNewNote({
branchId: 'hidden',
noteId: 'hidden', noteId: 'hidden',
title: 'hidden', title: 'hidden',
type: 'text', type: 'text',
@@ -44,6 +45,7 @@ function getHiddenRoot() {
// isInheritable: false means that this notePath is automatically not preffered but at the same time // isInheritable: false means that this notePath is automatically not preffered but at the same time
// the flag is not inherited to the children // the flag is not inherited to the children
hidden.addLabel('archived', "", false); hidden.addLabel('archived', "", false);
hidden.addLabel('excludeFromNoteMap', "", true);
} }
return hidden; return hidden;
@@ -206,6 +208,12 @@ function createMissingSpecialNotes() {
getSinglesNoteRoot(); getSinglesNoteRoot();
getSinglesNoteRoot(); getSinglesNoteRoot();
getGlobalNoteMap(); getGlobalNoteMap();
const hidden = getHiddenRoot();
if (!hidden.hasOwnedLabel('excludeFromNoteMap')) {
hidden.addLabel('excludeFromNoteMap', "", true);
}
} }
module.exports = { module.exports = {

View File

@@ -149,7 +149,10 @@ async function pullChanges(syncContext) {
sql.transactional(() => { sql.transactional(() => {
for (const {entityChange, entity} of entityChanges) { for (const {entityChange, entity} of entityChanges) {
if (!sourceIdService.isLocalSourceId(entityChange.sourceId)) { // FIXME: temporary fix
const existsAlready = !!sql.getValue("SELECT id FROM entity_changes WHERE entityName = ? AND entityId = ? AND utcDateChanged = ? AND hash = ?", [entityChange.entityName, entityChange.entityId, entityChange.utcDateChanged, entityChange.hash]);
if (!existsAlready && !sourceIdService.isLocalSourceId(entityChange.sourceId)) {
if (!atLeastOnePullApplied) { // send only for first if (!atLeastOnePullApplied) { // send only for first
ws.syncPullInProgress(); ws.syncPullInProgress();
@@ -249,6 +252,14 @@ async function checkContentHash(syncContext) {
const failedChecks = contentHashService.checkContentHashes(resp.entityHashes); const failedChecks = contentHashService.checkContentHashes(resp.entityHashes);
if (failedChecks.length > 0) {
// before requeuing sectors make sure the entity changes are correct
const consistencyChecks = require("./consistency_checks");
consistencyChecks.runEntityChangesChecks();
await syncRequest(syncContext, 'POST', `/api/sync/check-entity-changes`);
}
for (const {entityName, sector} of failedChecks) { for (const {entityName, sector} of failedChecks) {
entityChangesService.addEntityChangesForSector(entityName, sector); entityChangesService.addEntityChangesForSector(entityName, sector);

View File

@@ -54,7 +54,7 @@ function updateNormalEntity(remoteEntityChange, entity) {
sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId); sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId);
entityChangesService.addEntityChange(remoteEntityChange, true); entityChangesService.addEntityChange(remoteEntityChange);
}); });
return true; return true;
@@ -71,7 +71,7 @@ function updateNormalEntity(remoteEntityChange, entity) {
sql.transactional(() => { sql.transactional(() => {
sql.replace(remoteEntityChange.entityName, entity); sql.replace(remoteEntityChange.entityName, entity);
entityChangesService.addEntityChange(remoteEntityChange, true); entityChangesService.addEntityChange(remoteEntityChange);
}); });
return true; return true;
@@ -86,7 +86,7 @@ function updateNoteReordering(entityChange, entity) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]); sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
} }
entityChangesService.addEntityChange(entityChange, true); entityChangesService.addEntityChange(entityChange);
}); });
return true; return true;