mirror of
https://github.com/zadam/trilium.git
synced 2025-11-18 03:00:41 +01:00
Merge branch 'feature/typescript_backend_3' into feature/typescript_backend_4
This commit is contained in:
@@ -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;
|
||||
@@ -20,7 +21,7 @@ interface AttachmentOpts {
|
||||
* Becca is a backend cache of all notes, branches, and attributes.
|
||||
* There's a similar frontend cache Froca, and share cache Shaca.
|
||||
*/
|
||||
class Becca {
|
||||
export default class Becca {
|
||||
loaded!: boolean;
|
||||
|
||||
notes!: Record<string, BNote>;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -276,4 +280,12 @@ class Becca {
|
||||
}
|
||||
}
|
||||
|
||||
export = Becca;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export interface ConstructorData<T extends AbstractBeccaEntity<T>> {
|
||||
primaryKeyName: string;
|
||||
entityName: string;
|
||||
hashedProperties: (keyof T)[];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
import Becca = require("./becca-interface");
|
||||
import Becca from "./becca-interface";
|
||||
|
||||
const becca = new Becca();
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import BEtapiToken = require('./entities/betapi_token');
|
||||
import cls = require('../services/cls');
|
||||
import entityConstructor = require('../becca/entity_constructor');
|
||||
import { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from './entities/rows';
|
||||
import AbstractBeccaEntity = require('./entities/abstract_becca_entity');
|
||||
|
||||
const beccaLoaded = new Promise<void>((res, rej) => {
|
||||
sqlInit.dbReady.then(() => {
|
||||
@@ -75,7 +76,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;
|
||||
}
|
||||
@@ -89,7 +90,7 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entity
|
||||
if (beccaEntity) {
|
||||
beccaEntity.updateFromRow(entityRow);
|
||||
} else {
|
||||
beccaEntity = new EntityClass();
|
||||
beccaEntity = new EntityClass() as AbstractBeccaEntity<AbstractBeccaEntity<any>>;
|
||||
beccaEntity.updateFromRow(entityRow);
|
||||
beccaEntity.init();
|
||||
}
|
||||
@@ -98,7 +99,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 +126,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;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import cls = require('../../services/cls');
|
||||
import log = require('../../services/log');
|
||||
import protectedSessionService = require('../../services/protected_session');
|
||||
import blobService = require('../../services/blob');
|
||||
import Becca = require('../becca-interface');
|
||||
import Becca, { ConstructorData } from '../becca-interface';
|
||||
|
||||
let becca: Becca | null = null;
|
||||
|
||||
@@ -18,26 +18,22 @@ interface ContentOpts {
|
||||
forceFrontendReload?: boolean;
|
||||
}
|
||||
|
||||
interface ConstructorData<T extends AbstractBeccaEntity<T>> {
|
||||
primaryKeyName: string;
|
||||
entityName: string;
|
||||
hashedProperties: (keyof 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>> {
|
||||
|
||||
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 +42,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
protected getUtcDateChanged() {
|
||||
getUtcDateChanged() {
|
||||
return this.utcDateModified || this.utcDateCreated;
|
||||
}
|
||||
|
||||
@@ -70,7 +66,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 = "";
|
||||
|
||||
@@ -96,6 +92,10 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
||||
|
||||
abstract getPojo(): {};
|
||||
|
||||
abstract init(): void;
|
||||
|
||||
abstract updateFromRow(row: unknown): void;
|
||||
|
||||
get isDeleted(): boolean {
|
||||
// TODO: Not sure why some entities don't implement it.
|
||||
return false;
|
||||
|
||||
@@ -11,7 +11,8 @@ import BNote = require('./bnote');
|
||||
import BBranch = require('./bbranch');
|
||||
|
||||
const attachmentRoleToNoteTypeMapping = {
|
||||
'image': 'image'
|
||||
'image': 'image',
|
||||
'file': 'file'
|
||||
};
|
||||
|
||||
interface ContentOpts {
|
||||
@@ -36,10 +37,10 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
|
||||
noteId?: number;
|
||||
attachmentId?: string;
|
||||
/** either noteId or revisionId to which this attachment belongs */
|
||||
ownerId: string;
|
||||
role: string;
|
||||
mime: string;
|
||||
title: string;
|
||||
ownerId!: string;
|
||||
role!: string;
|
||||
mime!: string;
|
||||
title!: string;
|
||||
type?: keyof typeof attachmentRoleToNoteTypeMapping;
|
||||
position?: number;
|
||||
blobId?: string;
|
||||
@@ -53,6 +54,11 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
|
||||
constructor(row: AttachmentRow) {
|
||||
super();
|
||||
|
||||
this.updateFromRow(row);
|
||||
this.decrypt();
|
||||
}
|
||||
|
||||
updateFromRow(row: AttachmentRow): void {
|
||||
if (!row.ownerId?.trim()) {
|
||||
throw new Error("'ownerId' must be given to initialize a Attachment entity");
|
||||
} else if (!row.role?.trim()) {
|
||||
@@ -75,8 +81,10 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince;
|
||||
this.contentLength = row.contentLength;
|
||||
}
|
||||
|
||||
this.decrypt();
|
||||
init(): void {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
copy(): BAttachment {
|
||||
@@ -130,7 +138,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import AbstractBeccaEntity = require("./abstract_becca_entity");
|
||||
import { BlobRow } from "./rows";
|
||||
|
||||
// TODO: Why this does not extend the abstract becca?
|
||||
class BBlob {
|
||||
class BBlob extends AbstractBeccaEntity<BBlob> {
|
||||
static get entityName() { return "blobs"; }
|
||||
static get primaryKeyName() { return "blobId"; }
|
||||
static get hashedProperties() { return ["blobId", "content"]; }
|
||||
|
||||
blobId: string;
|
||||
content: string | Buffer;
|
||||
contentLength: number;
|
||||
dateModified: string;
|
||||
utcDateModified: string;
|
||||
blobId!: string;
|
||||
content!: string | Buffer;
|
||||
contentLength!: number;
|
||||
dateModified!: string;
|
||||
utcDateModified!: string;
|
||||
|
||||
constructor(row: BlobRow) {
|
||||
super();
|
||||
this.updateFromRow(row);
|
||||
}
|
||||
|
||||
updateFromRow(row: BlobRow): void {
|
||||
this.blobId = row.blobId;
|
||||
this.content = row.content;
|
||||
this.contentLength = row.contentLength;
|
||||
@@ -20,6 +26,10 @@ class BBlob {
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
}
|
||||
|
||||
init() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
blobId: this.blobId,
|
||||
|
||||
@@ -1657,6 +1657,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
||||
position
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
throw new Error("Attempted to save an attachment with no content.");
|
||||
}
|
||||
|
||||
attachment.setContent(content, {forceSave: true});
|
||||
|
||||
return attachment;
|
||||
|
||||
@@ -32,6 +32,10 @@ class BOption extends AbstractBeccaEntity<BOption> {
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
|
||||
@@ -11,19 +11,28 @@ import AbstractBeccaEntity = require('./abstract_becca_entity');
|
||||
class BRecentNote extends AbstractBeccaEntity<BRecentNote> {
|
||||
static get entityName() { return "recent_notes"; }
|
||||
static get primaryKeyName() { return "noteId"; }
|
||||
static get hashedProperties() { return ["noteId", "notePath"]; }
|
||||
|
||||
noteId: string;
|
||||
notePath: string;
|
||||
utcDateCreated: string;
|
||||
noteId!: string;
|
||||
notePath!: string;
|
||||
utcDateCreated!: string;
|
||||
|
||||
constructor(row: RecentNoteRow) {
|
||||
super();
|
||||
|
||||
this.updateFromRow(row);
|
||||
}
|
||||
|
||||
updateFromRow(row: RecentNoteRow): void {
|
||||
this.noteId = row.noteId;
|
||||
this.notePath = row.notePath;
|
||||
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
init(): void {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
noteId: this.noteId,
|
||||
|
||||
@@ -29,22 +29,30 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
||||
"utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"]; }
|
||||
|
||||
revisionId?: string;
|
||||
noteId: string;
|
||||
type: string;
|
||||
mime: string;
|
||||
isProtected: boolean;
|
||||
title: string;
|
||||
noteId!: string;
|
||||
type!: string;
|
||||
mime!: string;
|
||||
isProtected!: boolean;
|
||||
title!: string;
|
||||
blobId?: string;
|
||||
dateLastEdited?: string;
|
||||
dateCreated: string;
|
||||
dateCreated!: string;
|
||||
utcDateLastEdited?: string;
|
||||
utcDateCreated: string;
|
||||
utcDateCreated!: string;
|
||||
contentLength?: number;
|
||||
content?: string;
|
||||
|
||||
constructor(row: RevisionRow, titleDecrypted = false) {
|
||||
super();
|
||||
|
||||
this.updateFromRow(row);
|
||||
if (this.isProtected && !titleDecrypted) {
|
||||
const decryptedTitle = protectedSessionService.isProtectedSessionAvailable() ? protectedSessionService.decryptString(this.title) : null;
|
||||
this.title = decryptedTitle || "[protected]";
|
||||
}
|
||||
}
|
||||
|
||||
updateFromRow(row: RevisionRow) {
|
||||
this.revisionId = row.revisionId;
|
||||
this.noteId = row.noteId;
|
||||
this.type = row.type;
|
||||
@@ -58,11 +66,10 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
||||
this.utcDateCreated = row.utcDateCreated;
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
this.contentLength = row.contentLength;
|
||||
}
|
||||
|
||||
if (this.isProtected && !titleDecrypted) {
|
||||
const decryptedTitle = protectedSessionService.isProtectedSessionAvailable() ? protectedSessionService.decryptString(this.title) : null;
|
||||
this.title = decryptedTitle || "[protected]";
|
||||
}
|
||||
init() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
getNote() {
|
||||
@@ -115,7 +122,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
||||
}
|
||||
}
|
||||
|
||||
setContent(content: any, opts: ContentOpts = {}) {
|
||||
setContent(content: string | Buffer, opts: ContentOpts = {}) {
|
||||
this._setContent(content, opts);
|
||||
}
|
||||
|
||||
@@ -158,6 +165,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();
|
||||
|
||||
|
||||
@@ -108,7 +108,3 @@ export interface NoteRow {
|
||||
utcDateModified: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface AttributeRow {
|
||||
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ConstructorData } from './becca-interface';
|
||||
import AbstractBeccaEntity = require('./entities/abstract_becca_entity');
|
||||
import BAttachment = require('./entities/battachment');
|
||||
import BAttribute = require('./entities/battribute');
|
||||
import BBlob = require('./entities/bblob');
|
||||
@@ -8,7 +10,9 @@ import BOption = require('./entities/boption');
|
||||
import BRecentNote = require('./entities/brecent_note');
|
||||
import BRevision = require('./entities/brevision');
|
||||
|
||||
const ENTITY_NAME_TO_ENTITY: Record<string, any> = {
|
||||
type EntityClass = new (row?: any) => AbstractBeccaEntity<any>;
|
||||
|
||||
const ENTITY_NAME_TO_ENTITY: Record<string, ConstructorData<any> & EntityClass> = {
|
||||
"attachments": BAttachment,
|
||||
"attributes": BAttribute,
|
||||
"blobs": BBlob,
|
||||
|
||||
@@ -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, "");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export = { buildDate:"2024-01-21T23:49:23+01:00", buildRevision: "4f8073daa7cff1b8b6737ae45792b2e87c2adf33" };
|
||||
export = { buildDate:"2024-03-28T07:11:39+01:00", buildRevision: "399458b52f250b22be22d980a78de0b3390d7521" };
|
||||
|
||||
@@ -20,6 +20,6 @@ export interface EntityRow {
|
||||
}
|
||||
|
||||
export interface EntityChangeRecord {
|
||||
entityChange: EntityChange;
|
||||
entity?: EntityRow;
|
||||
entityChange: EntityChange;
|
||||
entity?: EntityRow;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -128,11 +128,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(/ /g, ' ');
|
||||
@@ -140,6 +136,23 @@ class NoteContentFulltextExp extends Expression {
|
||||
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
stripTags(content: string) {
|
||||
// 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, "");
|
||||
}
|
||||
}
|
||||
|
||||
export = NoteContentFulltextExp;
|
||||
|
||||
@@ -25,7 +25,7 @@ class OrderByAndLimitExp extends Expression {
|
||||
constructor(orderDefinitions: Pick<OrderDefinition, "direction" | "valueExtractor">[], limit?: number) {
|
||||
super();
|
||||
|
||||
this.orderDefinitions = orderDefinitions as unknown as OrderDefinition[];
|
||||
this.orderDefinitions = orderDefinitions as OrderDefinition[];
|
||||
|
||||
for (const od of this.orderDefinitions) {
|
||||
od.smaller = od.direction === "asc" ? -1 : 1;
|
||||
|
||||
@@ -51,9 +51,7 @@ class SearchResult {
|
||||
addScoreForStrings(tokens: string[], str: string, factor: number) {
|
||||
const chunks = str.toLowerCase().split(" ");
|
||||
|
||||
if (!this.score) {
|
||||
this.score = 0;
|
||||
}
|
||||
this.score = 0;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
for (const token of tokens) {
|
||||
|
||||
@@ -113,7 +113,7 @@ class ValueExtractor {
|
||||
i++;
|
||||
|
||||
const attr = cursor.getAttributeCaseInsensitive('relation', cur());
|
||||
cursor = (attr ? attr.targetNote || null : null);
|
||||
cursor = attr?.targetNote || null;
|
||||
}
|
||||
else if (cur() === 'parents') {
|
||||
cursor = cursor.parents[0];
|
||||
|
||||
@@ -57,7 +57,7 @@ class TaskContext {
|
||||
type: 'taskProgressCount',
|
||||
taskId: this.taskId,
|
||||
taskType: this.taskType,
|
||||
data: this.data || undefined,
|
||||
data: this.data,
|
||||
progressCount: this.progressCount
|
||||
});
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class TaskContext {
|
||||
type: 'taskError',
|
||||
taskId: this.taskId,
|
||||
taskType: this.taskType,
|
||||
data: this.data || undefined,
|
||||
data: this.data,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
@@ -78,7 +78,7 @@ class TaskContext {
|
||||
type: 'taskSucceeded',
|
||||
taskId: this.taskId,
|
||||
taskType: this.taskType,
|
||||
data: this.data || undefined,
|
||||
data: this.data,
|
||||
result: result
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,11 +3,7 @@ import path = require('path');
|
||||
import windowService = require('./window');
|
||||
import optionService = require('./options');
|
||||
|
||||
const UPDATE_TRAY_EVENTS = [
|
||||
'minimize', 'maximize', 'show', 'hide'
|
||||
] as const;
|
||||
|
||||
let tray: Tray | null = null;
|
||||
let tray: Tray;
|
||||
// `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window
|
||||
// is minimized
|
||||
let isVisible = true;
|
||||
@@ -42,14 +38,15 @@ const registerVisibilityListener = () => {
|
||||
// They need to be registered before the tray updater is registered
|
||||
mainWindow.on('show', () => {
|
||||
isVisible = true;
|
||||
updateTrayMenu();
|
||||
});
|
||||
mainWindow.on('hide', () => {
|
||||
isVisible = false;
|
||||
updateTrayMenu();
|
||||
});
|
||||
|
||||
UPDATE_TRAY_EVENTS.forEach((eventName) => {
|
||||
mainWindow.on(eventName as any, updateTrayMenu)
|
||||
});
|
||||
mainWindow.on("minimize", updateTrayMenu);
|
||||
mainWindow.on("maximize", updateTrayMenu);
|
||||
}
|
||||
|
||||
const updateTrayMenu = () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ let setupWindow: BrowserWindow | null;
|
||||
async function createExtraWindow(extraWindowHash: string) {
|
||||
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
|
||||
|
||||
const {BrowserWindow} = require('electron');
|
||||
const { BrowserWindow } = require('electron');
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 1000,
|
||||
@@ -53,7 +53,7 @@ async function createMainWindow(app: App) {
|
||||
|
||||
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
|
||||
|
||||
const {BrowserWindow} = require('electron'); // should not be statically imported
|
||||
const { BrowserWindow } = require('electron'); // should not be statically imported
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
@@ -128,7 +128,7 @@ function getIcon() {
|
||||
}
|
||||
|
||||
async function createSetupWindow() {
|
||||
const {BrowserWindow} = require('electron'); // should not be statically imported
|
||||
const { BrowserWindow } = require('electron'); // should not be statically imported
|
||||
setupWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 800,
|
||||
@@ -152,7 +152,7 @@ function closeSetupWindow() {
|
||||
}
|
||||
|
||||
async function registerGlobalShortcuts() {
|
||||
const {globalShortcut} = require('electron');
|
||||
const { globalShortcut } = require('electron');
|
||||
|
||||
await sqlInit.dbReady;
|
||||
|
||||
|
||||
@@ -29,11 +29,11 @@ let lastSyncedPush: number | null = null;
|
||||
interface Message {
|
||||
type: string;
|
||||
data?: {
|
||||
lastSyncedPush?: number,
|
||||
lastSyncedPush?: number | null,
|
||||
entityChanges?: any[],
|
||||
safeImport?: boolean
|
||||
},
|
||||
lastSyncedPush?: number,
|
||||
lastSyncedPush?: number | null,
|
||||
|
||||
progressCount?: number;
|
||||
taskId?: string;
|
||||
@@ -143,7 +143,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 || "");
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,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;
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,7 @@ function sendPing(client: WebSocket, entityChangeIds = []) {
|
||||
sendMessage(client, {
|
||||
type: 'frontend-update',
|
||||
data: {
|
||||
lastSyncedPush: lastSyncedPush || undefined,
|
||||
lastSyncedPush,
|
||||
entityChanges
|
||||
}
|
||||
});
|
||||
@@ -238,19 +238,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) {
|
||||
|
||||
@@ -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'));
|
||||
|
||||
40
src/www.js
40
src/www.js
@@ -45,7 +45,7 @@ function startTrilium() {
|
||||
* instead of the new one. This is complicated by the fact that it is possible to run multiple instances of Trilium
|
||||
* if port and data dir are configured separately. This complication is the source of the following weird usage.
|
||||
*
|
||||
* The line below makes sure that the "second-instance" (process in window) is fired. Normally it returns a boolean
|
||||
* The line below makes sure that the "second-instance" (process in window.ts) is fired. Normally it returns a boolean
|
||||
* indicating whether another instance is running or not, but we ignore that and kill the app only based on the port conflict.
|
||||
*
|
||||
* A bit weird is that "second-instance" is triggered also on the valid usecases (different port/data dir) and
|
||||
@@ -126,26 +126,26 @@ function startHttpServer() {
|
||||
}
|
||||
|
||||
httpServer.on('error', error => {
|
||||
if (!listenOnTcp || error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(`Port ${port} requires elevated privileges. It's recommended to use port above 1024.`);
|
||||
process.exit(1);
|
||||
break;
|
||||
|
||||
case 'EADDRINUSE':
|
||||
console.error(`Port ${port} is already in use. Most likely, another Trilium process is already running. You might try to find it, kill it, and try again.`);
|
||||
process.exit(1);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
if (!listenOnTcp || error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(`Port ${port} requires elevated privileges. It's recommended to use port above 1024.`);
|
||||
process.exit(1);
|
||||
break;
|
||||
|
||||
case 'EADDRINUSE':
|
||||
console.error(`Port ${port} is already in use. Most likely, another Trilium process is already running. You might try to find it, kill it, and try again.`);
|
||||
process.exit(1);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
httpServer.on('listening', () => {
|
||||
|
||||
Reference in New Issue
Block a user