Merge branch 'feature/typescript_backend' into feature/typescript_backend_2

This commit is contained in:
Elian Doran
2024-03-30 10:54:06 +02:00
34 changed files with 2239 additions and 5736 deletions

View File

@@ -11,6 +11,7 @@ import BAttachment = require('./entities/battachment');
import { AttachmentRow, RevisionRow } from './entities/rows';
import BBlob = require('./entities/bblob');
import BRecentNote = require('./entities/brecent_note');
import AbstractBeccaEntity = require('./entities/abstract_becca_entity');
interface AttachmentOpts {
includeContentLength?: boolean;
@@ -95,7 +96,7 @@ class Becca {
return this.notes[noteId];
}
getNoteOrThrow(noteId: string): BNote | null {
getNoteOrThrow(noteId: string): BNote {
const note = this.notes[noteId];
if (!note) {
throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
@@ -190,7 +191,11 @@ class Becca {
.map(row => new BAttachment(row));
}
getBlob(entity: { blobId: string }): BBlob | null {
getBlob(entity: { blobId?: string }): BBlob | null {
if (!entity.blobId) {
return null;
}
const row = sql.getRow("SELECT *, LENGTH(content) AS contentLength FROM blobs WHERE blobId = ?", [entity.blobId]);
const BBlob = require('./entities/bblob'); // avoiding circular dependency problems
@@ -209,8 +214,7 @@ class Becca {
return this.etapiTokens[etapiTokenId];
}
/** @returns {AbstractBeccaEntity|null} */
getEntity(entityName: string, entityId: string) {
getEntity<T extends AbstractBeccaEntity<T>>(entityName: string, entityId: string): AbstractBeccaEntity<T> | null {
if (!entityName || !entityId) {
return null;
}

View File

@@ -19,7 +19,7 @@ const beccaLoaded = new Promise<void>((res, rej) => {
cls.init(() => {
load();
require('../services/options_init').initStartupOptions();
require('../services/options_init.js').initStartupOptions();
res();
});
@@ -75,7 +75,7 @@ function reload(reason: string) {
require('../services/ws').reloadFrontend(reason || "becca reloaded");
}
eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entityName, entityRow}) => {
eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({ entityName, entityRow }) => {
if (!becca.loaded) {
return;
}
@@ -98,7 +98,7 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entity
postProcessEntityUpdate(entityName, entityRow);
});
eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED, ({entityName, entity}) => {
eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED, ({ entityName, entity }) => {
if (!becca.loaded) {
return;
}
@@ -125,7 +125,7 @@ function postProcessEntityUpdate(entityName: string, entityRow: any) {
}
}
eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED], ({entityName, entityId}) => {
eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED], ({ entityName, entityId }) => {
if (!becca.loaded) {
return;
}

View File

@@ -18,6 +18,10 @@ interface ContentOpts {
forceFrontendReload?: boolean;
}
/**
* This interface contains the data that is shared across all the objects of a given derived class of {@link AbstractBeccaEntity}.
* For example, all BAttributes will share their content, but all BBranches will have another set of this data.
*/
interface ConstructorData<T extends AbstractBeccaEntity<T>> {
primaryKeyName: string;
entityName: string;
@@ -26,18 +30,20 @@ interface ConstructorData<T extends AbstractBeccaEntity<T>> {
/**
* Base class for all backend entities.
*
* @type T the same entity type needed for self-reference in {@link ConstructorData}.
*/
abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
protected utcDateModified?: string;
protected dateCreated?: string;
protected dateModified?: string;
protected isSynced?: boolean;
protected blobId?: string;
utcDateCreated!: string;
isProtected?: boolean;
isSynced?: boolean;
blobId?: string;
protected beforeSaving() {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
@@ -46,7 +52,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}
}
protected getUtcDateChanged() {
getUtcDateChanged() {
return this.utcDateModified || this.utcDateCreated;
}
@@ -70,7 +76,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
});
}
protected generateHash(isDeleted: boolean): string {
generateHash(isDeleted?: boolean): string {
const constructorData = (this.constructor as unknown as ConstructorData<T>);
let contentToHash = "";

View File

@@ -11,7 +11,8 @@ import BNote = require('./bnote');
import BBranch = require('./bbranch');
const attachmentRoleToNoteTypeMapping = {
'image': 'image'
'image': 'image',
'file': 'file'
};
interface ContentOpts {
@@ -130,7 +131,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
return this._getContent() as Buffer;
}
setContent(content: any, opts: ContentOpts) {
setContent(content: string | Buffer, opts: ContentOpts) {
this._setContent(content, opts);
}

View File

@@ -115,7 +115,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
}
}
setContent(content: any, opts: ContentOpts = {}) {
setContent(content: string | Buffer, opts: ContentOpts = {}) {
this._setContent(content, opts);
}
@@ -158,6 +158,13 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
return this.getAttachments().filter(attachment => attachment.title === title)[0];
}
/**
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/
eraseRevision() {
require("../../services/erase.js").eraseRevisions([this.revisionId]);
}
beforeSaving() {
super.beforeSaving();

View File

@@ -43,7 +43,7 @@ interface DateLimits {
function filterUrlValue(value: string) {
return value
.replace(/https?:\/\//ig, "")
.replace(/www\./ig, "")
.replace(/www.js\./ig, "")
.replace(/(\.net|\.com|\.org|\.info|\.edu)/ig, "");
}

View File

@@ -4,6 +4,7 @@ import froca from "./froca.js";
import attributeRenderer from "./attribute_renderer.js";
import libraryLoader from "./library_loader.js";
import treeService from "./tree.js";
import utils from "./utils.js";
const TPL = `
<div class="note-list">
@@ -215,7 +216,11 @@ class NoteListRenderer {
if (highlightedTokens.length > 0) {
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
this.highlightRegex = new RegExp(highlightedTokens.join("|"), 'gi');
const regex = highlightedTokens
.map(token => utils.escapeRegExp(token))
.join("|");
this.highlightRegex = new RegExp(regex, 'gi');
} else {
this.highlightRegex = null;
}

View File

@@ -487,12 +487,14 @@ function areObjectsEqual () {
}
function copyHtmlToClipboard(content) {
const clipboardItem = new ClipboardItem({
'text/html': new Blob([content], {type: 'text/html'}),
'text/plain': new Blob([content], {type: 'text/plain'})
});
navigator.clipboard.write([clipboardItem]);
function listener(e) {
e.clipboardData.setData("text/html", content);
e.clipboardData.setData("text/plain", content);
e.preventDefault();
}
document.addEventListener("copy", listener);
document.execCommand("copy");
document.removeEventListener("copy", listener);
}
/**

View File

@@ -1,10 +1,8 @@
import libraryLoader from "../../services/library_loader.js";
import TypeWidget from "./type_widget.js";
import libraryLoader from '../../services/library_loader.js';
import TypeWidget from './type_widget.js';
import utils from '../../services/utils.js';
import linkService from '../../services/link.js';
import debounce from "../../services/debounce.js";
const {sleep} = utils;
import debounce from '../../services/debounce.js';
const TPL = `
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
@@ -115,7 +113,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.currentSceneVersion = this.SCENE_VERSION_INITIAL;
// will be overwritten
this.excalidrawRef;
this.$render;
this.$widget;
this.reactHandlers; // used to control react state
@@ -155,7 +152,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const renderElement = this.$render.get(0);
ReactDOM.unmountComponentAtNode(renderElement);
ReactDOM.render(React.createElement(this.createExcalidrawReactApp), renderElement);
const root = ReactDOM.createRoot(renderElement);
root.render(React.createElement(this.createExcalidrawReactApp));
});
return this.$widget;
@@ -179,9 +177,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const blob = await note.getBlob();
// before we load content into excalidraw, make sure excalidraw has loaded
while (!this.excalidrawRef?.current) {
console.log("excalidrawRef not yet loaded, sleep 200ms...");
await sleep(200);
while (!this.excalidrawApi) {
console.log("excalidrawApi not yet loaded, sleep 200ms...");
await utils.sleep(200);
}
/**
@@ -199,7 +197,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
collaborators: []
};
this.excalidrawRef.current.updateScene(sceneData);
this.excalidrawApi.updateScene(sceneData);
}
else if (blob.content) {
// load saved content into excalidraw canvas
@@ -246,9 +244,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
fileArray.push(file);
}
this.excalidrawRef.current.updateScene(sceneData);
this.excalidrawRef.current.addFiles(fileArray);
this.excalidrawRef.current.history.clear();
this.excalidrawApi.updateScene(sceneData);
this.excalidrawApi.addFiles(fileArray);
this.excalidrawApi.history.clear();
}
Promise.all(
@@ -261,7 +259,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}
const libraryItems = blobs.map(blob => blob.getJsonContentSafely()).filter(item => !!item);
this.excalidrawRef.current.updateLibrary({libraryItems, merge: false});
this.excalidrawApi.updateLibrary({libraryItems, merge: false});
});
// set initial scene version
@@ -275,17 +273,17 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
* this is automatically called after this.saveData();
*/
async getData() {
const elements = this.excalidrawRef.current.getSceneElements();
const appState = this.excalidrawRef.current.getAppState();
const elements = this.excalidrawApi.getSceneElements();
const appState = this.excalidrawApi.getAppState();
/**
* A file is not deleted, even though removed from canvas. Therefore, we only keep
* files that are referenced by an element. Maybe this will change with a new excalidraw version?
*/
const files = this.excalidrawRef.current.getFiles();
const files = this.excalidrawApi.getFiles();
// parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share
const svg = await window.ExcalidrawLib.exportToSvg({
const svg = await ExcalidrawLib.exportToSvg({
elements,
appState,
exportPadding: 5, // 5 px padding
@@ -321,7 +319,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
// this.libraryChanged is unset in dataSaved()
// there's no separate method to get library items, so have to abuse this one
const libraryItems = await this.excalidrawRef.current.updateLibrary({merge: true});
const libraryItems = await this.excalidrawApi.updateLibrary({merge: true});
let position = 10;
@@ -379,9 +377,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
createExcalidrawReactApp() {
const React = window.React;
const { Excalidraw } = window.ExcalidrawLib;
const excalidrawRef = React.useRef(null);
this.excalidrawRef = excalidrawRef;
const excalidrawWrapperRef = React.useRef(null);
this.excalidrawWrapperRef = excalidrawWrapperRef;
const [dimensions, setDimensions] = React.useState({
@@ -439,7 +434,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
React.createElement(Excalidraw, {
// this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu
theme: this.themeStyle,
ref: excalidrawRef,
excalidrawAPI: api => { this.excalidrawApi = api; },
width: dimensions.width,
height: dimensions.height,
onPaste: (data, event) => {
@@ -483,8 +478,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}
getSceneVersion() {
if (this.excalidrawRef) {
const elements = this.excalidrawRef.current.getSceneElements();
if (this.excalidrawApi) {
const elements = this.excalidrawApi.getSceneElements();
return window.ExcalidrawLib.getSceneVersion(elements);
} else {
return this.SCENE_VERSION_ERROR;

View File

@@ -88,3 +88,7 @@ body .CodeMirror {
.excalidraw.theme--dark {
--theme-filter: invert(80%) hue-rotate(180deg) !important;
}
body .todo-list input[type="checkbox"]:not(:checked):before {
border-color: var(--muted-text-color) !important;
}

View File

@@ -20,7 +20,7 @@ function getBlobPojo(entityName: string, entityId: string) {
if (!entity.hasStringContent()) {
pojo.content = null;
} else {
pojo.content = processContent(pojo.content, entity.isProtected, true);
pojo.content = processContent(pojo.content, !!entity.isProtected, true);
}
return pojo;

View File

@@ -1 +1 @@
export = { buildDate:"2024-01-21T23:49:23+01:00", buildRevision: "4f8073daa7cff1b8b6737ae45792b2e87c2adf33" };
export = { buildDate:"2024-03-28T07:11:39+01:00", buildRevision: "399458b52f250b22be22d980a78de0b3390d7521" };

View File

@@ -1,15 +1,15 @@
interface DefinitionObject {
isPromoted: boolean;
labelType: string;
multiplicity: string;
numberPrecision: number;
promotedAlias: string;
inverseRelation: string;
isPromoted?: boolean;
labelType?: string;
multiplicity?: string;
numberPrecision?: number;
promotedAlias?: string;
inverseRelation?: string;
}
function parse(value: string): DefinitionObject {
const tokens = value.split(',').map(t => t.trim());
const defObj: Partial<DefinitionObject> = {};
const defObj: DefinitionObject = {};
for (const token of tokens) {
if (token === 'promoted') {
@@ -41,7 +41,7 @@ function parse(value: string): DefinitionObject {
}
}
return defObj as DefinitionObject;
return defObj;
}
export = {

View File

@@ -111,11 +111,7 @@ class NoteContentFulltextExp extends Expression {
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 = this.stripTags(content);
}
content = content.replace(/&nbsp;/g, ' ');
@@ -123,6 +119,23 @@ class NoteContentFulltextExp extends Expression {
return content.trim();
}
stripTags(content) {
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
// we want to insert space in place of block tags (because they imply text separation)
// but we don't want to insert text for typical formatting inline tags which can occur within one word
const linkTag = 'a';
const inlineFormattingTags = ['b', 'strong', 'em', 'i', 'span', 'big', 'small', 'font', 'sub', 'sup'];
// replace tags which imply text separation with a space
content = striptags(content, [linkTag, ...inlineFormattingTags], ' ');
// replace the inline formatting tags (but not links) without a space
content = striptags(content, [linkTag], '');
// at least the closing link tag can be easily stripped
return content.replace(/<\/a>/ig, "");
}
}
module.exports = NoteContentFulltextExp;

View File

@@ -49,7 +49,7 @@ class TaskContext {
type: 'taskProgressCount',
taskId: this.taskId,
taskType: this.taskType,
data: this.data || undefined,
data: this.data,
progressCount: this.progressCount
});
}
@@ -60,7 +60,7 @@ class TaskContext {
type: 'taskError',
taskId: this.taskId,
taskType: this.taskType,
data: this.data || undefined,
data: this.data,
message: message
});
}
@@ -70,7 +70,7 @@ class TaskContext {
type: 'taskSucceeded',
taskId: this.taskId,
taskType: this.taskType,
data: this.data || undefined,
data: this.data,
result: result
});
}

View File

@@ -29,10 +29,10 @@ let lastSyncedPush: number | null = null;
interface Message {
type: string;
data?: {
lastSyncedPush?: number,
lastSyncedPush?: number | null,
entityChanges?: any[]
},
lastSyncedPush?: number,
} | null,
lastSyncedPush?: number | null,
progressCount?: number;
taskId?: string;
@@ -142,7 +142,7 @@ function fillInAdditionalProperties(entityChange: EntityChange) {
if (!entityChange.entity) {
entityChange.entity = sql.getRow(`SELECT * FROM notes WHERE noteId = ?`, [entityChange.entityId]);
if (entityChange.entity && entityChange.entity.isProtected) {
if (entityChange.entity?.isProtected) {
entityChange.entity.title = protectedSessionService.decryptString(entityChange.entity.title || "");
}
}
@@ -157,7 +157,7 @@ function fillInAdditionalProperties(entityChange: EntityChange) {
if (parentNote) {
for (const childBranch of parentNote.getChildBranches()) {
if (childBranch && childBranch.branchId) {
if (childBranch?.branchId) {
entityChange.positions[childBranch.branchId] = childBranch.notePosition;
}
}
@@ -222,7 +222,7 @@ function sendPing(client: WebSocket, entityChangeIds = []) {
sendMessage(client, {
type: 'frontend-update',
data: {
lastSyncedPush: lastSyncedPush || undefined,
lastSyncedPush,
entityChanges
}
});
@@ -237,19 +237,19 @@ function sendTransactionEntityChangesToAllClients() {
}
function syncPullInProgress() {
sendMessageToAllClients({ type: 'sync-pull-in-progress', lastSyncedPush: lastSyncedPush || undefined });
sendMessageToAllClients({ type: 'sync-pull-in-progress', lastSyncedPush });
}
function syncPushInProgress() {
sendMessageToAllClients({ type: 'sync-push-in-progress', lastSyncedPush: lastSyncedPush || undefined });
sendMessageToAllClients({ type: 'sync-push-in-progress', lastSyncedPush });
}
function syncFinished() {
sendMessageToAllClients({ type: 'sync-finished', lastSyncedPush: lastSyncedPush || undefined });
sendMessageToAllClients({ type: 'sync-finished', lastSyncedPush });
}
function syncFailed() {
sendMessageToAllClients({ type: 'sync-failed', lastSyncedPush: lastSyncedPush || undefined });
sendMessageToAllClients({ type: 'sync-failed', lastSyncedPush });
}
function reloadFrontend(reason: string) {

View File

@@ -105,10 +105,10 @@ function renderText(result, note) {
if (result.content.includes(`<span class="math-tex">`)) {
result.header += `
<script src="../../${assetPath}/node_modules/katex/dist/katex.min.js"></script>
<link rel="stylesheet" href="../../${assetPath}/node_modules/katex/dist/katex.min.css">
<script src="../../${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
<script src="../../${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
<script src="../${assetPath}/node_modules/katex/dist/katex.min.js"></script>
<link rel="stylesheet" href="../${assetPath}/node_modules/katex/dist/katex.min.css">
<script src="../${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
<script src="../${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.getElementById('content'));