Compare commits

...

11 Commits

22 changed files with 11175 additions and 93 deletions

11072
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.8", "version": "0.48.9",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {

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

@@ -84,7 +84,7 @@ class Branch extends AbstractEntity {
/** @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

@@ -34,8 +34,8 @@ const TPL = `
</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>

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

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

@@ -18,7 +18,12 @@ export default class OpenNoteButtonWidget extends ButtonWidget {
} }
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

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

@@ -1 +1 @@
module.exports = { buildDate:"2021-12-13T11:12:31+01:00", buildRevision: "d9550dd59b9b0dff0b229c400cdf6585abcb226a" }; 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();

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
@@ -67,14 +73,13 @@ function exportToZip(taskContext, branch, format, res) {
else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
newExtension = null; newExtension = null;
} }
else {
if (note.mime?.toLowerCase()?.trim() === "image/jpg") {
newExtension = 'jpg';
}
else { else {
newExtension = mimeTypes.extension(note.mime) || "dat"; newExtension = mimeTypes.extension(note.mime) || "dat";
} }
let fileName = baseFileName;
if (fileName.length > 30) {
fileName = fileName.substr(0, 30);
} }
// if the note is already named with extension (e.g. "jquery"), then it's silly to append exact same extension again // 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,6 +732,8 @@ function eraseAttributes(attributeIdsToErase) {
} }
function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) { function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) {
// this is important also so that the erased entity changes are sent to the connected clients
sql.transactional(() => {
if (eraseEntitiesAfterTimeInSeconds === null) { if (eraseEntitiesAfterTimeInSeconds === null) {
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds'); eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds');
} }
@@ -749,6 +751,7 @@ function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) {
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) {

View File

@@ -23,7 +23,6 @@ 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());
} }
@@ -34,9 +33,8 @@ class AttributeExistsExp extends Expression {
resultNoteSet.add(note); resultNoteSet.add(note);
} }
} }
}
return resultNoteSet; return resultNoteSet.intersection(inputNoteSet);
} }
} }

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

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