Merge remote-tracking branch 'origin/beta' into beta

This commit is contained in:
zadam 2023-10-02 22:02:31 +02:00
commit 055bb39e4d
6 changed files with 93 additions and 33 deletions

View File

@ -1613,16 +1613,12 @@ class BNote extends AbstractBeccaEntity {
revision.save(); // to generate revisionId, which is then used to save attachments revision.save(); // to generate revisionId, which is then used to save attachments
if (this.type === 'text') {
for (const noteAttachment of this.getAttachments()) { for (const noteAttachment of this.getAttachments()) {
if (noteAttachment.utcDateScheduledForErasureSince) {
continue;
}
const revisionAttachment = noteAttachment.copy(); const revisionAttachment = noteAttachment.copy();
revisionAttachment.ownerId = revision.revisionId; revisionAttachment.ownerId = revision.revisionId;
revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true}); revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true});
if (this.type === 'text') {
// content is rewritten to point to the revision attachments // content is rewritten to point to the revision attachments
noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`,
`attachments/${revisionAttachment.attachmentId}`); `attachments/${revisionAttachment.attachmentId}`);

View File

@ -86,6 +86,29 @@ class BRevision extends AbstractBeccaEntity {
return this._getContent(); return this._getContent();
} }
/**
* @returns {*}
* @throws Error in case of invalid JSON */
getJsonContent() {
const content = this.getContent();
if (!content || !content.trim()) {
return null;
}
return JSON.parse(content);
}
/** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
getJsonContentSafely() {
try {
return this.getJsonContent();
}
catch (e) {
return null;
}
}
/** /**
* @param content * @param content
* @param {object} [opts] * @param {object} [opts]
@ -105,6 +128,45 @@ class BRevision extends AbstractBeccaEntity {
.map(row => new BAttachment(row)); .map(row => new BAttachment(row));
} }
/** @returns {BAttachment|null} */
getAttachmentById(attachmentId, opts = {}) {
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows(query, [this.revisionId, attachmentId])
.map(row => new BAttachment(row))[0];
}
/** @returns {BAttachment[]} */
getAttachmentsByRole(role) {
return sql.getRows(`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND role = ?
AND isDeleted = 0
ORDER BY position`, [this.revisionId, role])
.map(row => new BAttachment(row));
}
/** @returns {BAttachment} */
getAttachmentByTitle(title) {
return sql.getRows(`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND title = ?
AND isDeleted = 0
ORDER BY position`, [this.revisionId, title])
.map(row => new BAttachment(row))[0];
}
beforeSaving() { beforeSaving() {
super.beforeSaving(); super.beforeSaving();

View File

@ -45,6 +45,7 @@ const TPL = `
<a class="dropdown-item export-note-button">Export note</a> <a class="dropdown-item export-note-button">Export note</a>
<a class="dropdown-item delete-note-button">Delete note</a> <a class="dropdown-item delete-note-button">Delete note</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a> <a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a>
<a data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button"><kbd data-command="forceSaveRevision"></kbd> Save revision</a>
</div> </div>
</div>`; </div>`;

View File

@ -274,26 +274,11 @@ export default class RevisionsDialog extends BasicWidget {
this.$content.html($table); this.$content.html($table);
} else if (revisionItem.type === 'canvas') { } else if (revisionItem.type === 'canvas') {
/** const sanitizedTitle = revisionItem.title.replace(/[^a-z0-9-.]/gi, "");
* FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com
* REMOVE external dependency!!!! This is defined in the svg in defs.style
*/
const content = fullRevision.content;
try { this.$content.html($("<img>")
const data = JSON.parse(content) .attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`)
const svg = data.svg || "no svg present." .css("max-width", "100%"));
/**
* maxWidth: 100% use full width of container but do not enlarge!
* height:auto to ensure that height scales with width
*/
const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"});
this.$content.html($('<div>').append($svgHtml));
} catch (err) {
console.error("error parsing fullRevision.content as JSON", fullRevision.content, err);
this.$content.html($("<div>").text("Error parsing content. Please check console.error() for more details."));
}
} else { } else {
this.$content.text("Preview isn't available for this note type."); this.$content.text("Preview isn't available for this note type.");
} }

View File

@ -5,14 +5,27 @@ const becca = require('../../becca/becca');
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const fs = require('fs'); const fs = require('fs');
function returnImage(req, res) { function returnImageFromNote(req, res) {
const image = becca.getNote(req.params.noteId); const image = becca.getNote(req.params.noteId);
return returnImageInt(image, res);
}
function returnImageFromRevision(req, res) {
const image = becca.getRevision(req.params.revisionId);
return returnImageInt(image, res);
}
/**
* @param {BNote|BRevision} image
* @param res
*/
function returnImageInt(image, res) {
if (!image) { if (!image) {
res.set('Content-Type', 'image/png'); res.set('Content-Type', 'image/png');
return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`)); return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
} } else if (!["image", "canvas"].includes(image.type)) {
else if (!["image", "canvas"].includes(image.type)){
return res.sendStatus(400); return res.sendStatus(400);
} }
@ -84,7 +97,8 @@ function updateImage(req) {
} }
module.exports = { module.exports = {
returnImage, returnImageFromNote,
returnImageFromRevision,
returnAttachedImage, returnAttachedImage,
updateImage updateImage
}; };

View File

@ -181,6 +181,8 @@ function register(app) {
apiRoute(GET, '/api/revisions/:revisionId/blob', revisionsApiRoute.getRevisionBlob); apiRoute(GET, '/api/revisions/:revisionId/blob', revisionsApiRoute.getRevisionBlob);
apiRoute(DEL, '/api/revisions/:revisionId', revisionsApiRoute.eraseRevision); apiRoute(DEL, '/api/revisions/:revisionId', revisionsApiRoute.eraseRevision);
apiRoute(PST, '/api/revisions/:revisionId/restore', revisionsApiRoute.restoreRevision); apiRoute(PST, '/api/revisions/:revisionId/restore', revisionsApiRoute.restoreRevision);
route(GET, '/api/revisions/:revisionId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImageFromRevision);
route(GET, '/api/revisions/:revisionId/download', [auth.checkApiAuthOrElectron], revisionsApiRoute.downloadRevision); route(GET, '/api/revisions/:revisionId/download', [auth.checkApiAuthOrElectron], revisionsApiRoute.downloadRevision);
@ -200,7 +202,7 @@ function register(app) {
apiRoute(GET, '/api/attribute-values/:attributeName', attributesRoute.getValuesForAttribute); apiRoute(GET, '/api/attribute-values/:attributeName', attributesRoute.getValuesForAttribute);
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage); route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImageFromNote);
route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler); route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler);
apiRoute(GET, '/api/options', optionsApiRoute.getOptions); apiRoute(GET, '/api/options', optionsApiRoute.getOptions);