mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-10 18:39:22 +08:00
Merge pull request #273 from TriliumNext/feature/client_typescript_port1
Port frontend to TypeScript (0% -> 36.7%)
This commit is contained in:
commit
b920fb24ba
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
**/package-lock.json linguist-generated=true
|
@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd src/public
|
||||
echo Summary
|
||||
cloc HEAD \
|
||||
--git --md \
|
||||
--include-lang=javascript,typescript
|
||||
|
||||
echo By file
|
||||
cloc HEAD \
|
||||
--git --md \
|
||||
--include-lang=javascript,typescript \
|
||||
--found=filelist.txt \
|
||||
--exclude-dir=public,libraries,views,docs
|
||||
|
||||
grep -R \.js$ filelist.txt
|
||||
rm filelist.txt
|
||||
--by-file
|
108
package-lock.json
generated
108
package-lock.json
generated
@ -93,6 +93,7 @@
|
||||
"striptags": "3.2.0",
|
||||
"tmp": "0.2.3",
|
||||
"tree-kill": "1.2.2",
|
||||
"ts-loader": "9.5.1",
|
||||
"turndown": "7.2.0",
|
||||
"unescape": "1.0.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4",
|
||||
@ -113,6 +114,7 @@
|
||||
"@playwright/test": "1.49.1",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/cheerio": "0.22.35",
|
||||
"@types/cls-hooked": "4.3.9",
|
||||
"@types/compression": "1.7.5",
|
||||
@ -124,9 +126,11 @@
|
||||
"@types/escape-html": "1.0.4",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/express-session": "1.18.1",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/html": "1.0.4",
|
||||
"@types/ini": "4.1.1",
|
||||
"@types/jasmine": "5.1.5",
|
||||
"@types/jquery": "3.5.32",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/multer": "1.4.12",
|
||||
@ -3611,6 +3615,17 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "1.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.2.tgz",
|
||||
@ -3767,6 +3782,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bootstrap": {
|
||||
"version": "5.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
|
||||
"integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
@ -4186,13 +4211,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/fs-extra": {
|
||||
"version": "9.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
|
||||
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/jsonfile": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
@ -4259,6 +4284,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz",
|
||||
"integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jsdom": {
|
||||
"version": "21.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
|
||||
@ -4277,6 +4312,16 @@
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonfile": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
|
||||
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/keyv": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||
@ -4466,6 +4511,13 @@
|
||||
"@types/express-session": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz",
|
||||
"integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/source-map-support": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz",
|
||||
@ -5771,7 +5823,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@ -8142,6 +8193,17 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-installer-common/node_modules/@types/fs-extra": {
|
||||
"version": "9.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-installer-common/node_modules/fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
@ -8974,7 +9036,6 @@
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@ -9957,7 +10018,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@ -11803,7 +11863,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@ -13187,7 +13246,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@ -14541,7 +14599,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@ -17116,7 +17173,6 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -17506,7 +17562,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@ -17642,6 +17697,35 @@
|
||||
"node": ">=6.10"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
|
||||
"integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"enhanced-resolve": "^5.0.0",
|
||||
"micromatch": "^4.0.0",
|
||||
"semver": "^7.3.4",
|
||||
"source-map": "^0.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*",
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
|
@ -136,6 +136,7 @@
|
||||
"striptags": "3.2.0",
|
||||
"tmp": "0.2.3",
|
||||
"tree-kill": "1.2.2",
|
||||
"ts-loader": "9.5.1",
|
||||
"turndown": "7.2.0",
|
||||
"unescape": "1.0.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4",
|
||||
@ -153,6 +154,7 @@
|
||||
"@playwright/test": "1.49.1",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/cheerio": "0.22.35",
|
||||
"@types/cls-hooked": "4.3.9",
|
||||
"@types/compression": "1.7.5",
|
||||
@ -164,9 +166,11 @@
|
||||
"@types/escape-html": "1.0.4",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/express-session": "1.18.1",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/html": "1.0.4",
|
||||
"@types/ini": "4.1.1",
|
||||
"@types/jasmine": "5.1.5",
|
||||
"@types/jquery": "3.5.32",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/multer": "1.4.12",
|
||||
|
@ -15,8 +15,34 @@ import toast from "../services/toast.js";
|
||||
import ShortcutComponent from "./shortcut_component.js";
|
||||
import { t, initLocale } from "../services/i18n.js";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||
}
|
||||
|
||||
interface RootWidget extends Component {
|
||||
render: () => JQuery<HTMLElement>;
|
||||
}
|
||||
|
||||
interface BeforeUploadListener extends Component {
|
||||
beforeUnloadEvent(): boolean;
|
||||
}
|
||||
|
||||
interface TriggerData {
|
||||
noteId?: string;
|
||||
noteIds?: string[];
|
||||
messages?: unknown[];
|
||||
callback?: () => void;
|
||||
}
|
||||
|
||||
class AppContext extends Component {
|
||||
constructor(isMainWindow) {
|
||||
|
||||
isMainWindow: boolean;
|
||||
components: Component[];
|
||||
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
||||
tabManager!: TabManager;
|
||||
layout?: Layout;
|
||||
|
||||
constructor(isMainWindow: boolean) {
|
||||
super();
|
||||
|
||||
this.isMainWindow = isMainWindow;
|
||||
@ -33,7 +59,7 @@ class AppContext extends Component {
|
||||
await initLocale();
|
||||
}
|
||||
|
||||
setLayout(layout) {
|
||||
setLayout(layout: Layout) {
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
@ -73,6 +99,10 @@ class AppContext extends Component {
|
||||
}
|
||||
|
||||
renderWidgets() {
|
||||
if (!this.layout) {
|
||||
throw new Error("Missing layout.");
|
||||
}
|
||||
|
||||
const rootWidget = this.layout.getRootWidget(this);
|
||||
const $renderedWidget = rootWidget.render();
|
||||
|
||||
@ -97,15 +127,13 @@ class AppContext extends Component {
|
||||
this.triggerEvent('initialRenderComplete');
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
triggerEvent(name, data = {}) {
|
||||
triggerEvent(name: string, data: TriggerData = {}) {
|
||||
return this.handleEvent(name, data);
|
||||
}
|
||||
|
||||
/** @returns {Promise<*>} */
|
||||
triggerCommand(name, data = {}) {
|
||||
triggerCommand(name: string, data: TriggerData = {}) {
|
||||
for (const executor of this.components) {
|
||||
const fun = executor[`${name}Command`];
|
||||
const fun = (executor as any)[`${name}Command`];
|
||||
|
||||
if (fun) {
|
||||
return executor.callMethod(fun, data);
|
||||
@ -119,17 +147,17 @@ class AppContext extends Component {
|
||||
return this.triggerEvent(name, data);
|
||||
}
|
||||
|
||||
getComponentByEl(el) {
|
||||
getComponentByEl(el: HTMLElement) {
|
||||
return $(el).closest(".component").prop('component');
|
||||
}
|
||||
|
||||
addBeforeUnloadListener(obj) {
|
||||
addBeforeUnloadListener(obj: BeforeUploadListener) {
|
||||
if (typeof WeakRef !== "function") {
|
||||
// older browsers don't support WeakRef
|
||||
return;
|
||||
}
|
||||
|
||||
this.beforeUnloadListeners.push(new WeakRef(obj));
|
||||
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,13 @@ import utils from '../services/utils.js';
|
||||
* event / command is executed in all components - by simply awaiting the `triggerEvent()`.
|
||||
*/
|
||||
export default class Component {
|
||||
componentId: string;
|
||||
children: Component[];
|
||||
initialized: Promise<void> | null;
|
||||
parent?: Component;
|
||||
|
||||
constructor() {
|
||||
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
|
||||
/** @type Component[] */
|
||||
this.children = [];
|
||||
this.initialized = null;
|
||||
}
|
||||
@ -24,13 +28,12 @@ export default class Component {
|
||||
return this.constructor.name.replace(/[^A-Z0-9]/ig, "_");
|
||||
}
|
||||
|
||||
setParent(parent) {
|
||||
/** @type Component */
|
||||
setParent(parent: Component) {
|
||||
this.parent = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
child(...components) {
|
||||
child(...components: Component[]) {
|
||||
for (const component of components) {
|
||||
component.setParent(this);
|
||||
|
||||
@ -40,12 +43,11 @@ export default class Component {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
handleEvent(name, data) {
|
||||
handleEvent(name: string, data: unknown): Promise<unknown> | null {
|
||||
try {
|
||||
const callMethodPromise = this.initialized
|
||||
? this.initialized.then(() => this.callMethod(this[`${name}Event`], data))
|
||||
: this.callMethod(this[`${name}Event`], data);
|
||||
? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data))
|
||||
: this.callMethod((this as any)[`${name}Event`], data);
|
||||
|
||||
const childrenPromise = this.handleEventInChildren(name, data);
|
||||
|
||||
@ -54,20 +56,18 @@ export default class Component {
|
||||
? Promise.all([callMethodPromise, childrenPromise])
|
||||
: (callMethodPromise || childrenPromise);
|
||||
}
|
||||
catch (e) {
|
||||
catch (e: any) {
|
||||
console.error(`Handling of event '${name}' failed in ${this.constructor.name} with error ${e.message} ${e.stack}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
triggerEvent(name, data = {}) {
|
||||
return this.parent.triggerEvent(name, data);
|
||||
triggerEvent(name: string, data = {}): Promise<unknown> | undefined | null {
|
||||
return this.parent?.triggerEvent(name, data);
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
handleEventInChildren(name, data = {}) {
|
||||
handleEventInChildren(name: string, data: unknown = {}) {
|
||||
const promises = [];
|
||||
|
||||
for (const child of this.children) {
|
||||
@ -82,9 +82,8 @@ export default class Component {
|
||||
return promises.length > 0 ? Promise.all(promises) : null;
|
||||
}
|
||||
|
||||
/** @returns {Promise<*>} */
|
||||
triggerCommand(name, data = {}) {
|
||||
const fun = this[`${name}Command`];
|
||||
triggerCommand(name: string, data = {}): Promise<unknown> | undefined | null {
|
||||
const fun = (this as any)[`${name}Command`];
|
||||
|
||||
if (fun) {
|
||||
return this.callMethod(fun, data);
|
||||
@ -97,7 +96,7 @@ export default class Component {
|
||||
}
|
||||
}
|
||||
|
||||
callMethod(fun, data) {
|
||||
callMethod(fun: (arg: unknown) => Promise<unknown>, data: unknown) {
|
||||
if (typeof fun !== 'function') {
|
||||
return;
|
||||
}
|
@ -11,7 +11,10 @@ class ZoomComponent extends Component {
|
||||
|
||||
if (utils.isElectron()) {
|
||||
options.initializedPromise.then(() => {
|
||||
this.setZoomFactor(options.getFloat('zoomFactor'));
|
||||
const zoomFactor = options.getFloat('zoomFactor');
|
||||
if (zoomFactor) {
|
||||
this.setZoomFactor(zoomFactor);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("wheel", event => {
|
||||
@ -22,14 +25,13 @@ class ZoomComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
setZoomFactor(zoomFactor) {
|
||||
zoomFactor = parseFloat(zoomFactor);
|
||||
|
||||
setZoomFactor(zoomFactor: string | number) {
|
||||
const parsedZoomFactor = (typeof zoomFactor !== "number" ? parseFloat(zoomFactor) : zoomFactor);
|
||||
const webFrame = utils.dynamicRequire('electron').webFrame;
|
||||
webFrame.setZoomFactor(zoomFactor);
|
||||
webFrame.setZoomFactor(parsedZoomFactor);
|
||||
}
|
||||
|
||||
async setZoomFactorAndSave(zoomFactor) {
|
||||
async setZoomFactorAndSave(zoomFactor: number) {
|
||||
if (zoomFactor >= MIN_ZOOM && zoomFactor <= MAX_ZOOM) {
|
||||
zoomFactor = Math.round(zoomFactor * 10) / 10;
|
||||
|
||||
@ -57,7 +59,7 @@ class ZoomComponent extends Component {
|
||||
this.setZoomFactorAndSave(1);
|
||||
}
|
||||
|
||||
setZoomFactorAndSaveEvent({zoomFactor}) {
|
||||
setZoomFactorAndSaveEvent({ zoomFactor }: { zoomFactor: number }) {
|
||||
this.setZoomFactorAndSave(zoomFactor);
|
||||
}
|
||||
}
|
@ -1,48 +1,61 @@
|
||||
import { Froca } from "../services/froca-interface.js";
|
||||
|
||||
export interface FAttachmentRow {
|
||||
attachmentId: string;
|
||||
ownerId: string;
|
||||
role: string;
|
||||
mime: string;
|
||||
title: string;
|
||||
dateModified: string;
|
||||
utcDateModified: string;
|
||||
utcDateScheduledForErasureSince: string;
|
||||
contentLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachment is a file directly tied into a note without
|
||||
* being a hidden child.
|
||||
*/
|
||||
class FAttachment {
|
||||
constructor(froca, row) {
|
||||
private froca: Froca;
|
||||
attachmentId!: string;
|
||||
private ownerId!: string;
|
||||
role!: string;
|
||||
private mime!: string;
|
||||
private title!: string;
|
||||
private dateModified!: string;
|
||||
private utcDateModified!: string;
|
||||
private utcDateScheduledForErasureSince!: string;
|
||||
/**
|
||||
* optionally added to the entity
|
||||
*/
|
||||
private contentLength!: number;
|
||||
|
||||
constructor(froca: Froca, row: FAttachmentRow) {
|
||||
/** @type {Froca} */
|
||||
this.froca = froca;
|
||||
|
||||
this.update(row);
|
||||
}
|
||||
|
||||
update(row) {
|
||||
/** @type {string} */
|
||||
update(row: FAttachmentRow) {
|
||||
this.attachmentId = row.attachmentId;
|
||||
/** @type {string} */
|
||||
this.ownerId = row.ownerId;
|
||||
/** @type {string} */
|
||||
this.role = row.role;
|
||||
/** @type {string} */
|
||||
this.mime = row.mime;
|
||||
/** @type {string} */
|
||||
this.title = row.title;
|
||||
/** @type {string} */
|
||||
this.dateModified = row.dateModified;
|
||||
/** @type {string} */
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
/** @type {string} */
|
||||
this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince;
|
||||
|
||||
/**
|
||||
* optionally added to the entity
|
||||
* @type {int}
|
||||
*/
|
||||
this.contentLength = row.contentLength;
|
||||
|
||||
this.froca.attachments[this.attachmentId] = this;
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
getNote() {
|
||||
return this.froca.notes[this.ownerId];
|
||||
}
|
||||
|
||||
/** @return {FBlob} */
|
||||
async getBlob() {
|
||||
return await this.froca.getBlob('attachments', this.attachmentId);
|
||||
}
|
@ -1,45 +1,56 @@
|
||||
import { Froca } from '../services/froca-interface.js';
|
||||
import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js';
|
||||
|
||||
/**
|
||||
* There are currently only two types of attributes, labels or relations.
|
||||
* @typedef {"label" | "relation"} AttributeType
|
||||
*/
|
||||
export type AttributeType = "label" | "relation";
|
||||
|
||||
export interface FAttributeRow {
|
||||
attributeId: string;
|
||||
noteId: string;
|
||||
type: AttributeType;
|
||||
name: string;
|
||||
value: string;
|
||||
position: number;
|
||||
isInheritable: boolean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attribute is an abstract concept which has two real uses - label (key - value pair)
|
||||
* and relation (representing named relationship between source and target note)
|
||||
*/
|
||||
class FAttribute {
|
||||
constructor(froca, row) {
|
||||
/** @type {Froca} */
|
||||
private froca: Froca;
|
||||
attributeId!: string;
|
||||
noteId!: string;
|
||||
type!: AttributeType;
|
||||
name!: string;
|
||||
value!: string;
|
||||
position!: number;
|
||||
isInheritable!: boolean;
|
||||
|
||||
constructor(froca: Froca, row: FAttributeRow) {
|
||||
this.froca = froca;
|
||||
|
||||
this.update(row);
|
||||
}
|
||||
|
||||
update(row) {
|
||||
/** @type {string} */
|
||||
update(row: FAttributeRow) {
|
||||
this.attributeId = row.attributeId;
|
||||
/** @type {string} */
|
||||
this.noteId = row.noteId;
|
||||
/** @type {AttributeType} */
|
||||
this.type = row.type;
|
||||
/** @type {string} */
|
||||
this.name = row.name;
|
||||
/** @type {string} */
|
||||
this.value = row.value;
|
||||
/** @type {int} */
|
||||
this.position = row.position;
|
||||
/** @type {boolean} */
|
||||
this.isInheritable = !!row.isInheritable;
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
getNote() {
|
||||
return this.froca.notes[this.noteId];
|
||||
}
|
||||
|
||||
/** @returns {Promise<FNote>} */
|
||||
async getTargetNote() {
|
||||
const targetNoteId = this.targetNoteId;
|
||||
|
||||
@ -70,12 +81,12 @@ class FAttribute {
|
||||
return promotedAttributeDefinitionParser.parse(this.value);
|
||||
}
|
||||
|
||||
isDefinitionFor(attr) {
|
||||
isDefinitionFor(attr: FAttribute) {
|
||||
return this.type === 'label' && this.name === `${attr.type}:${attr.name}`;
|
||||
}
|
||||
|
||||
get dto() {
|
||||
const dto = Object.assign({}, this);
|
||||
get dto(): Omit<FAttribute, "froca"> {
|
||||
const dto: any = Object.assign({}, this);
|
||||
delete dto.froca;
|
||||
|
||||
return dto;
|
@ -1,39 +0,0 @@
|
||||
export default class FBlob {
|
||||
constructor(row) {
|
||||
/** @type {string} */
|
||||
this.blobId = row.blobId;
|
||||
|
||||
/**
|
||||
* can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
|
||||
* @type {string}
|
||||
*/
|
||||
this.content = row.content;
|
||||
this.contentLength = row.contentLength;
|
||||
|
||||
/** @type {string} */
|
||||
this.dateModified = row.dateModified;
|
||||
/** @type {string} */
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {*}
|
||||
* @throws Error in case of invalid JSON */
|
||||
getJsonContent() {
|
||||
if (!this.content || !this.content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(this.content);
|
||||
}
|
||||
|
||||
/** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
|
||||
getJsonContentSafely() {
|
||||
try {
|
||||
return this.getJsonContent();
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
48
src/public/app/entities/fblob.ts
Normal file
48
src/public/app/entities/fblob.ts
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
export interface FBlobRow {
|
||||
blobId: string;
|
||||
content: string;
|
||||
contentLength: number;
|
||||
dateModified: string;
|
||||
utcDateModified: string;
|
||||
}
|
||||
|
||||
export default class FBlob {
|
||||
|
||||
blobId: string;
|
||||
/**
|
||||
* can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
|
||||
*/
|
||||
content: string;
|
||||
contentLength: number;
|
||||
dateModified: string;
|
||||
utcDateModified: string;
|
||||
|
||||
constructor(row: FBlobRow) {
|
||||
this.blobId = row.blobId;
|
||||
this.content = row.content;
|
||||
this.contentLength = row.contentLength;
|
||||
this.dateModified = row.dateModified;
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error in case of invalid JSON
|
||||
*/
|
||||
getJsonContent(): unknown {
|
||||
if (!this.content || !this.content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(this.content);
|
||||
}
|
||||
|
||||
getJsonContentSafely(): unknown | null {
|
||||
try {
|
||||
return this.getJsonContent();
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +1,65 @@
|
||||
import { Froca } from "../services/froca-interface.js";
|
||||
|
||||
export interface FBranchRow {
|
||||
branchId: string;
|
||||
noteId: string;
|
||||
parentNoteId: string;
|
||||
notePosition: number;
|
||||
prefix?: string;
|
||||
isExpanded?: boolean;
|
||||
fromSearchNote: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
|
||||
* parents.
|
||||
*/
|
||||
class FBranch {
|
||||
constructor(froca, row) {
|
||||
/** @type {Froca} */
|
||||
private froca: Froca;
|
||||
|
||||
/**
|
||||
* primary key
|
||||
*/
|
||||
branchId!: string;
|
||||
noteId!: string;
|
||||
parentNoteId!: string;
|
||||
notePosition!: number;
|
||||
prefix?: string;
|
||||
isExpanded?: boolean;
|
||||
fromSearchNote!: boolean;
|
||||
|
||||
constructor(froca: Froca, row: FBranchRow) {
|
||||
this.froca = froca;
|
||||
|
||||
this.update(row);
|
||||
}
|
||||
|
||||
update(row) {
|
||||
update(row: FBranchRow) {
|
||||
/**
|
||||
* primary key
|
||||
* @type {string}
|
||||
*/
|
||||
this.branchId = row.branchId;
|
||||
/** @type {string} */
|
||||
this.noteId = row.noteId;
|
||||
/** @type {string} */
|
||||
this.parentNoteId = row.parentNoteId;
|
||||
/** @type {int} */
|
||||
this.notePosition = row.notePosition;
|
||||
/** @type {string} */
|
||||
this.prefix = row.prefix;
|
||||
/** @type {boolean} */
|
||||
this.isExpanded = !!row.isExpanded;
|
||||
/** @type {boolean} */
|
||||
this.fromSearchNote = !!row.fromSearchNote;
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async getNote() {
|
||||
return this.froca.getNote(this.noteId);
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
getNoteFromCache() {
|
||||
return this.froca.getNoteFromCache(this.noteId);
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async getParentNote() {
|
||||
return this.froca.getNote(this.parentNoteId);
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if it's top level, meaning its parent is the root note */
|
||||
/** @returns true if it's top level, meaning its parent is the root note */
|
||||
isTopLevel() {
|
||||
return this.parentNoteId === 'root';
|
||||
}
|
||||
@ -54,8 +68,8 @@ class FBranch {
|
||||
return `FBranch(branchId=${this.branchId})`;
|
||||
}
|
||||
|
||||
get pojo() {
|
||||
const pojo = {...this};
|
||||
get pojo(): Omit<FBranch, "froca"> {
|
||||
const pojo = {...this} as any;
|
||||
delete pojo.froca;
|
||||
return pojo;
|
||||
}
|
@ -4,6 +4,9 @@ import ws from "../services/ws.js";
|
||||
import froca from "../services/froca.js";
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import cssClassManager from "../services/css_class_manager.js";
|
||||
import { Froca } from '../services/froca-interface.js';
|
||||
import FAttachment from './fattachment.js';
|
||||
import FAttribute, { AttributeType } from './fattribute.js';
|
||||
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
@ -30,76 +33,91 @@ const NOTE_TYPE_ICONS = {
|
||||
* There are many different Note types, some of which are entirely opaque to the
|
||||
* end user. Those types should be used only for checking against, they are
|
||||
* not for direct use.
|
||||
* @typedef {"file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code"} NoteType
|
||||
*/
|
||||
type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code";
|
||||
|
||||
/**
|
||||
* @typedef {Object} NotePathRecord
|
||||
* @property {boolean} isArchived
|
||||
* @property {boolean} isInHoistedSubTree
|
||||
* @property {boolean} isSearch
|
||||
* @property {Array<string>} notePath
|
||||
* @property {boolean} isHidden
|
||||
*/
|
||||
interface NotePathRecord {
|
||||
isArchived: boolean;
|
||||
isInHoistedSubTree: boolean;
|
||||
isSearch: boolean;
|
||||
notePath: string[];
|
||||
isHidden: boolean;
|
||||
}
|
||||
|
||||
export interface FNoteRow {
|
||||
noteId: string;
|
||||
title: string;
|
||||
isProtected: boolean;
|
||||
type: NoteType;
|
||||
mime: string;
|
||||
blobId: string;
|
||||
}
|
||||
|
||||
export interface NoteMetaData {
|
||||
dateCreated: string;
|
||||
utcDateCreated: string;
|
||||
dateModified: string;
|
||||
utcDateModified: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note is the main node and concept in Trilium.
|
||||
*/
|
||||
class FNote {
|
||||
|
||||
private froca: Froca;
|
||||
|
||||
noteId!: string;
|
||||
title!: string;
|
||||
isProtected!: boolean;
|
||||
type!: NoteType;
|
||||
/**
|
||||
* @param {Froca} froca
|
||||
* @param {Object.<string, Object>} row
|
||||
* content-type, e.g. "application/json"
|
||||
*/
|
||||
constructor(froca, row) {
|
||||
/** @type {Froca} */
|
||||
mime!: string;
|
||||
// the main use case to keep this is to detect content change which should trigger refresh
|
||||
blobId!: string;
|
||||
|
||||
attributes: string[];
|
||||
targetRelations: string[];
|
||||
parents: string[];
|
||||
children: string[];
|
||||
|
||||
parentToBranch: Record<string, string>;
|
||||
childToBranch: Record<string, string>;
|
||||
attachments: FAttachment[] | null;
|
||||
|
||||
// Managed by Froca.
|
||||
searchResultsLoaded?: boolean;
|
||||
highlightedTokens?: unknown;
|
||||
|
||||
constructor(froca: Froca, row: FNoteRow) {
|
||||
this.froca = froca;
|
||||
|
||||
/** @type {string[]} */
|
||||
this.attributes = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
this.targetRelations = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
this.parents = [];
|
||||
/** @type {string[]} */
|
||||
this.children = [];
|
||||
|
||||
/** @type {Object.<string, string>} */
|
||||
this.parentToBranch = {};
|
||||
|
||||
/** @type {Object.<string, string>} */
|
||||
this.childToBranch = {};
|
||||
|
||||
/** @type {FAttachment[]|null} */
|
||||
this.attachments = null; // lazy loaded
|
||||
|
||||
this.update(row);
|
||||
}
|
||||
|
||||
update(row) {
|
||||
/** @type {string} */
|
||||
update(row: FNoteRow) {
|
||||
this.noteId = row.noteId;
|
||||
/** @type {string} */
|
||||
this.title = row.title;
|
||||
/** @type {boolean} */
|
||||
this.isProtected = !!row.isProtected;
|
||||
/**
|
||||
* See {@see NoteType} for info on values.
|
||||
* @type {NoteType}
|
||||
*/
|
||||
this.type = row.type;
|
||||
/**
|
||||
* content-type, e.g. "application/json"
|
||||
* @type {string}
|
||||
*/
|
||||
|
||||
this.mime = row.mime;
|
||||
|
||||
// the main use case to keep this is to detect content change which should trigger refresh
|
||||
this.blobId = row.blobId;
|
||||
}
|
||||
|
||||
addParent(parentNoteId, branchId, sort = true) {
|
||||
addParent(parentNoteId: string, branchId: string, sort = true) {
|
||||
if (parentNoteId === 'none') {
|
||||
return;
|
||||
}
|
||||
@ -115,7 +133,7 @@ class FNote {
|
||||
}
|
||||
}
|
||||
|
||||
addChild(childNoteId, branchId, sort = true) {
|
||||
addChild(childNoteId: string, branchId: string, sort = true) {
|
||||
if (!(childNoteId in this.childToBranch)) {
|
||||
this.children.push(childNoteId);
|
||||
}
|
||||
@ -128,16 +146,18 @@ class FNote {
|
||||
}
|
||||
|
||||
sortChildren() {
|
||||
const branchIdPos = {};
|
||||
const branchIdPos: Record<string, number> = {};
|
||||
|
||||
for (const branchId of Object.values(this.childToBranch)) {
|
||||
branchIdPos[branchId] = this.froca.getBranch(branchId).notePosition;
|
||||
const notePosition = this.froca.getBranch(branchId)?.notePosition;
|
||||
if (notePosition) {
|
||||
branchIdPos[branchId] = notePosition;
|
||||
}
|
||||
}
|
||||
|
||||
this.children.sort((a, b) => branchIdPos[this.childToBranch[a]] - branchIdPos[this.childToBranch[b]]);
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
isJson() {
|
||||
return this.mime === "application/json";
|
||||
}
|
||||
@ -151,34 +171,32 @@ class FNote {
|
||||
async getJsonContent() {
|
||||
const content = await this.getContent();
|
||||
|
||||
if (typeof content !== "string") {
|
||||
console.log(`Unknown note content for '${this.noteId}'.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
catch (e) {
|
||||
catch (e: any) {
|
||||
console.log(`Cannot parse content of note '${this.noteId}': `, e.message);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getParentBranchIds() {
|
||||
return Object.values(this.parentToBranch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
* @deprecated use getParentBranchIds() instead
|
||||
*/
|
||||
getBranchIds() {
|
||||
return this.getParentBranchIds();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {FBranch[]}
|
||||
*/
|
||||
getParentBranches() {
|
||||
const branchIds = Object.values(this.parentToBranch);
|
||||
|
||||
@ -186,19 +204,16 @@ class FNote {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {FBranch[]}
|
||||
* @deprecated use getParentBranches() instead
|
||||
*/
|
||||
getBranches() {
|
||||
return this.getParentBranches();
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
hasChildren() {
|
||||
return this.children.length > 0;
|
||||
}
|
||||
|
||||
/** @returns {FBranch[]} */
|
||||
getChildBranches() {
|
||||
// don't use Object.values() to guarantee order
|
||||
const branchIds = this.children.map(childNoteId => this.childToBranch[childNoteId]);
|
||||
@ -206,12 +221,10 @@ class FNote {
|
||||
return this.froca.getBranches(branchIds);
|
||||
}
|
||||
|
||||
/** @returns {string[]} */
|
||||
getParentNoteIds() {
|
||||
return this.parents;
|
||||
}
|
||||
|
||||
/** @returns {FNote[]} */
|
||||
getParentNotes() {
|
||||
return this.froca.getNotesFromCache(this.parents);
|
||||
}
|
||||
@ -240,17 +253,14 @@ class FNote {
|
||||
return this.hasAttribute('label', 'archived');
|
||||
}
|
||||
|
||||
/** @returns {string[]} */
|
||||
getChildNoteIds() {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
/** @returns {Promise<FNote[]>} */
|
||||
async getChildNotes() {
|
||||
return await this.froca.getNotes(this.children);
|
||||
}
|
||||
|
||||
/** @returns {Promise<FAttachment[]>} */
|
||||
async getAttachments() {
|
||||
if (!this.attachments) {
|
||||
this.attachments = await this.froca.getAttachmentsForNote(this.noteId);
|
||||
@ -259,14 +269,12 @@ class FNote {
|
||||
return this.attachments;
|
||||
}
|
||||
|
||||
/** @returns {Promise<FAttachment[]>} */
|
||||
async getAttachmentsByRole(role) {
|
||||
async getAttachmentsByRole(role: string) {
|
||||
return (await this.getAttachments())
|
||||
.filter(attachment => attachment.role === role);
|
||||
}
|
||||
|
||||
/** @returns {Promise<FAttachment>} */
|
||||
async getAttachmentById(attachmentId) {
|
||||
async getAttachmentById(attachmentId: string) {
|
||||
const attachments = await this.getAttachments();
|
||||
|
||||
return attachments.find(att => att.attachmentId === attachmentId);
|
||||
@ -296,11 +304,11 @@ class FNote {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [type] - (optional) attribute type to filter
|
||||
* @param {string} [name] - (optional) attribute name to filter
|
||||
* @returns {FAttribute[]} all note's attributes, including inherited ones
|
||||
* @param [type] - attribute type to filter
|
||||
* @param [name] - attribute name to filter
|
||||
* @returns all note's attributes, including inherited ones
|
||||
*/
|
||||
getOwnedAttributes(type, name) {
|
||||
getOwnedAttributes(type?: AttributeType, name?: string) {
|
||||
const attrs = this.attributes
|
||||
.map(attributeId => this.froca.attributes[attributeId])
|
||||
.filter(Boolean); // filter out nulls;
|
||||
@ -309,20 +317,18 @@ class FNote {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [type] - (optional) attribute type to filter
|
||||
* @param {string} [name] - (optional) attribute name to filter
|
||||
* @returns {FAttribute[]} all note's attributes, including inherited ones
|
||||
* @param [type] - attribute type to filter
|
||||
* @param [name] - attribute name to filter
|
||||
* @returns all note's attributes, including inherited ones
|
||||
*/
|
||||
getAttributes(type, name) {
|
||||
getAttributes(type?: AttributeType, name?: string) {
|
||||
return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} path
|
||||
* @return {FAttribute[]}
|
||||
* @private
|
||||
*/
|
||||
__getCachedAttributes(path) {
|
||||
__getCachedAttributes(path: string[]): FAttribute[] {
|
||||
// notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
|
||||
// when template instance is a parent of template itself
|
||||
if (path.includes(this.noteId)) {
|
||||
@ -377,9 +383,9 @@ class FNote {
|
||||
/**
|
||||
* Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
|
||||
*
|
||||
* @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
|
||||
* @returns array of notePaths (each represented by array of noteIds constituting the particular note path)
|
||||
*/
|
||||
getAllNotePaths() {
|
||||
getAllNotePaths(): string[][] {
|
||||
if (this.noteId === 'root') {
|
||||
return [['root']];
|
||||
}
|
||||
@ -397,10 +403,6 @@ class FNote {
|
||||
return notePaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hoistedNoteId='root']
|
||||
* @return {Array<NotePathRecord>}
|
||||
*/
|
||||
getSortedNotePathRecords(hoistedNoteId = 'root') {
|
||||
const isHoistedRoot = hoistedNoteId === 'root';
|
||||
|
||||
@ -477,13 +479,9 @@ class FNote {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FAttribute[]} attributes
|
||||
* @param {AttributeType} type
|
||||
* @param {string} name
|
||||
* @return {FAttribute[]}
|
||||
* @private
|
||||
*/
|
||||
__filterAttrs(attributes, type, name) {
|
||||
__filterAttrs(attributes: FAttribute[], type?: AttributeType, name?: string): FAttribute[] {
|
||||
this.__validateTypeName(type, name);
|
||||
|
||||
if (!type && !name) {
|
||||
@ -495,15 +493,17 @@ class FNote {
|
||||
} else if (name) {
|
||||
return attributes.filter(attr => attr.name === name);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
__getInheritableAttributes(path) {
|
||||
__getInheritableAttributes(path: string[]) {
|
||||
const attrs = this.__getCachedAttributes(path);
|
||||
|
||||
return attrs.filter(attr => attr.isInheritable);
|
||||
}
|
||||
|
||||
__validateTypeName(type, name) {
|
||||
__validateTypeName(type?: string, name?: string) {
|
||||
if (type && type !== 'label' && type !== 'relation') {
|
||||
throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`);
|
||||
}
|
||||
@ -517,18 +517,18 @@ class FNote {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - label name to filter
|
||||
* @returns {FAttribute[]} all note's labels (attributes with type label), including inherited ones
|
||||
* @param [name] - label name to filter
|
||||
* @returns all note's labels (attributes with type label), including inherited ones
|
||||
*/
|
||||
getOwnedLabels(name) {
|
||||
getOwnedLabels(name: string) {
|
||||
return this.getOwnedAttributes(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - label name to filter
|
||||
* @returns {FAttribute[]} all note's labels (attributes with type label), including inherited ones
|
||||
* @param [name] - label name to filter
|
||||
* @returns all note's labels (attributes with type label), including inherited ones
|
||||
*/
|
||||
getLabels(name) {
|
||||
getLabels(name: string) {
|
||||
return this.getAttributes(LABEL, name);
|
||||
}
|
||||
|
||||
@ -536,7 +536,7 @@ class FNote {
|
||||
const iconClassLabels = this.getLabels('iconClass');
|
||||
const workspaceIconClass = this.getWorkspaceIconClass();
|
||||
|
||||
if (iconClassLabels.length > 0) {
|
||||
if (iconClassLabels && iconClassLabels.length > 0) {
|
||||
return iconClassLabels[0].value;
|
||||
}
|
||||
else if (workspaceIconClass) {
|
||||
@ -579,7 +579,7 @@ class FNote {
|
||||
|
||||
if (!childBranches) {
|
||||
ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`);
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
|
||||
@ -591,102 +591,104 @@ class FNote {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - relation name to filter
|
||||
* @returns {FAttribute[]} all note's relations (attributes with type relation), including inherited ones
|
||||
* @param [name] - relation name to filter
|
||||
* @returns all note's relations (attributes with type relation), including inherited ones
|
||||
*/
|
||||
getOwnedRelations(name) {
|
||||
getOwnedRelations(name: string) {
|
||||
return this.getOwnedAttributes(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - relation name to filter
|
||||
* @returns {FAttribute[]} all note's relations (attributes with type relation), including inherited ones
|
||||
* @param [name] - relation name to filter
|
||||
* @returns all note's relations (attributes with type relation), including inherited ones
|
||||
*/
|
||||
getRelations(name) {
|
||||
getRelations(name: string) {
|
||||
return this.getAttributes(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributeType} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {boolean} true if note has an attribute with given type and name (including inherited)
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns true if note has an attribute with given type and name (including inherited)
|
||||
*/
|
||||
hasAttribute(type, name) {
|
||||
hasAttribute(type: AttributeType, name: string) {
|
||||
const attributes = this.getAttributes();
|
||||
|
||||
return attributes.some(attr => attr.name === name && attr.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributeType} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {boolean} true if note has an attribute with given type and name (including inherited)
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns true if note has an attribute with given type and name (including inherited)
|
||||
*/
|
||||
hasOwnedAttribute(type, name) {
|
||||
hasOwnedAttribute(type: AttributeType, name: string) {
|
||||
return !!this.getOwnedAttribute(type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributeType} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {FAttribute} attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
|
||||
*/
|
||||
getOwnedAttribute(type, name) {
|
||||
getOwnedAttribute(type: AttributeType, name: string) {
|
||||
const attributes = this.getOwnedAttributes();
|
||||
|
||||
return attributes.find(attr => attr.name === name && attr.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributeType} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {FAttribute} attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
|
||||
*/
|
||||
getAttribute(type, name) {
|
||||
getAttribute(type: AttributeType, name: string) {
|
||||
const attributes = this.getAttributes();
|
||||
|
||||
return attributes.find(attr => attr.name === name && attr.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributeType} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {string} attribute value of the given type and name or null if no such attribute exists.
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns attribute value of the given type and name or null if no such attribute exists.
|
||||
*/
|
||||
getOwnedAttributeValue(type, name) {
|
||||
getOwnedAttributeValue(type: AttributeType, name: string) {
|
||||
const attr = this.getOwnedAttribute(type, name);
|
||||
|
||||
return attr ? attr.value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributeType} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {string} attribute value of the given type and name or null if no such attribute exists.
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns attribute value of the given type and name or null if no such attribute exists.
|
||||
*/
|
||||
getAttributeValue(type, name) {
|
||||
getAttributeValue(type: AttributeType, name: string) {
|
||||
const attr = this.getAttribute(type, name);
|
||||
|
||||
return attr ? attr.value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {boolean} true if label exists (excluding inherited)
|
||||
* @param name - label name
|
||||
* @returns true if label exists (excluding inherited)
|
||||
*/
|
||||
hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
|
||||
hasOwnedLabel(name: string) {
|
||||
return this.hasOwnedAttribute(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {boolean} true if label exists (including inherited)
|
||||
* @param name - label name
|
||||
* @returns true if label exists (including inherited)
|
||||
*/
|
||||
hasLabel(name) { return this.hasAttribute(LABEL, name); }
|
||||
hasLabel(name: string) { return this.hasAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {boolean} true if label exists (including inherited) and does not have "false" value.
|
||||
* @param name - label name
|
||||
* @returns true if label exists (including inherited) and does not have "false" value.
|
||||
*/
|
||||
isLabelTruthy(name) {
|
||||
isLabelTruthy(name: string) {
|
||||
const label = this.getLabel(name);
|
||||
|
||||
if (!label) {
|
||||
@ -697,80 +699,79 @@ class FNote {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {boolean} true if relation exists (excluding inherited)
|
||||
* @param name - relation name
|
||||
* @returns true if relation exists (excluding inherited)
|
||||
*/
|
||||
hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
|
||||
hasOwnedRelation(name: string) { return this.hasOwnedAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {boolean} true if relation exists (including inherited)
|
||||
* @param name - relation name
|
||||
* @returns true if relation exists (including inherited)
|
||||
*/
|
||||
hasRelation(name) { return this.hasAttribute(RELATION, name); }
|
||||
hasRelation(name: string) { return this.hasAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {FAttribute} label if it exists, null otherwise
|
||||
* @param name - label name
|
||||
* @returns label if it exists, null otherwise
|
||||
*/
|
||||
getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
|
||||
getOwnedLabel(name: string) { return this.getOwnedAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {FAttribute} label if it exists, null otherwise
|
||||
* @param name - label name
|
||||
* @returns label if it exists, null otherwise
|
||||
*/
|
||||
getLabel(name) { return this.getAttribute(LABEL, name); }
|
||||
getLabel(name: string) { return this.getAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {FAttribute} relation if it exists, null otherwise
|
||||
* @param name - relation name
|
||||
* @returns relation if it exists, null otherwise
|
||||
*/
|
||||
getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
|
||||
getOwnedRelation(name: string) { return this.getOwnedAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {FAttribute} relation if it exists, null otherwise
|
||||
* @param name - relation name
|
||||
* @returns relation if it exists, null otherwise
|
||||
*/
|
||||
getRelation(name) { return this.getAttribute(RELATION, name); }
|
||||
getRelation(name: string) { return this.getAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {string} label value if label exists, null otherwise
|
||||
* @param name - label name
|
||||
* @returns label value if label exists, null otherwise
|
||||
*/
|
||||
getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
|
||||
getOwnedLabelValue(name: string) { return this.getOwnedAttributeValue(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {string} label value if label exists, null otherwise
|
||||
* @param name - label name
|
||||
* @returns label value if label exists, null otherwise
|
||||
*/
|
||||
getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
|
||||
getLabelValue(name: string) { return this.getAttributeValue(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {string} relation value if relation exists, null otherwise
|
||||
* @param name - relation name
|
||||
* @returns relation value if relation exists, null otherwise
|
||||
*/
|
||||
getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
|
||||
getOwnedRelationValue(name: string) { return this.getOwnedAttributeValue(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {string} relation value if relation exists, null otherwise
|
||||
* @param name - relation name
|
||||
* @returns relation value if relation exists, null otherwise
|
||||
*/
|
||||
getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
|
||||
getRelationValue(name: string) { return this.getAttributeValue(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {Promise<FNote>|null} target note of the relation or null (if target is empty or note was not found)
|
||||
* @param name
|
||||
* @returns target note of the relation or null (if target is empty or note was not found)
|
||||
*/
|
||||
async getRelationTarget(name) {
|
||||
async getRelationTarget(name: string) {
|
||||
const targets = await this.getRelationTargets(name);
|
||||
|
||||
return targets.length > 0 ? targets[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - relation name to filter
|
||||
* @returns {Promise<FNote[]>}
|
||||
* @param [name] - relation name to filter
|
||||
*/
|
||||
async getRelationTargets(name) {
|
||||
async getRelationTargets(name: string) {
|
||||
const relations = this.getRelations(name);
|
||||
const targets = [];
|
||||
|
||||
@ -781,9 +782,6 @@ class FNote {
|
||||
return targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {FNote[]}
|
||||
*/
|
||||
getNotesToInheritAttributesFrom() {
|
||||
const relations = [
|
||||
...this.getRelations('template'),
|
||||
@ -819,7 +817,7 @@ class FNote {
|
||||
return promotedAttrs;
|
||||
}
|
||||
|
||||
hasAncestor(ancestorNoteId, followTemplates = false, visitedNoteIds = null) {
|
||||
hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set<string> | null = null) {
|
||||
if (this.noteId === ancestorNoteId) {
|
||||
return true;
|
||||
}
|
||||
@ -861,8 +859,6 @@ class FNote {
|
||||
|
||||
/**
|
||||
* Get relations which target this note
|
||||
*
|
||||
* @returns {FAttribute[]}
|
||||
*/
|
||||
getTargetRelations() {
|
||||
return this.targetRelations
|
||||
@ -871,8 +867,6 @@ class FNote {
|
||||
|
||||
/**
|
||||
* Get relations which target this note
|
||||
*
|
||||
* @returns {Promise<FNote[]>}
|
||||
*/
|
||||
async getTargetRelationSourceNotes() {
|
||||
const targetRelations = this.getTargetRelations();
|
||||
@ -882,13 +876,11 @@ class FNote {
|
||||
|
||||
/**
|
||||
* @deprecated use getBlob() instead
|
||||
* @return {Promise<FBlob>}
|
||||
*/
|
||||
async getNoteComplement() {
|
||||
return this.getBlob();
|
||||
}
|
||||
|
||||
/** @return {Promise<FBlob>} */
|
||||
async getBlob() {
|
||||
return await this.froca.getBlob('notes', this.noteId);
|
||||
}
|
||||
@ -897,8 +889,8 @@ class FNote {
|
||||
return `Note(noteId=${this.noteId}, title=${this.title})`;
|
||||
}
|
||||
|
||||
get dto() {
|
||||
const dto = Object.assign({}, this);
|
||||
get dto(): Omit<FNote, "froca"> {
|
||||
const dto = Object.assign({}, this) as any;
|
||||
delete dto.froca;
|
||||
|
||||
return dto;
|
||||
@ -919,7 +911,7 @@ class FNote {
|
||||
return labels.length > 0 ? labels[0].value : "";
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is JavaScript (code or file) */
|
||||
/** @returns true if this note is JavaScript (code or file) */
|
||||
isJavaScript() {
|
||||
return (this.type === "code" || this.type === "file" || this.type === 'launcher')
|
||||
&& (this.mime.startsWith("application/javascript")
|
||||
@ -927,12 +919,12 @@ class FNote {
|
||||
|| this.mime === "text/javascript");
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is HTML */
|
||||
/** @returns true if this note is HTML */
|
||||
isHtml() {
|
||||
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
|
||||
}
|
||||
|
||||
/** @returns {string|null} JS script environment - either "frontend" or "backend" */
|
||||
/** @returns JS script environment - either "frontend" or "backend" */
|
||||
getScriptEnv() {
|
||||
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
|
||||
return "frontend";
|
||||
@ -959,11 +951,9 @@ class FNote {
|
||||
if (env === "frontend") {
|
||||
const bundleService = (await import("../services/bundle.js")).default;
|
||||
return await bundleService.getAndExecuteBundle(this.noteId);
|
||||
}
|
||||
else if (env === "backend") {
|
||||
const resp = await server.post(`script/run/${this.noteId}`);
|
||||
}
|
||||
else {
|
||||
} else if (env === "backend") {
|
||||
await server.post(`script/run/${this.noteId}`);
|
||||
} else {
|
||||
throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`);
|
||||
}
|
||||
}
|
||||
@ -1002,11 +992,9 @@ class FNote {
|
||||
|
||||
/**
|
||||
* Provides note's date metadata.
|
||||
*
|
||||
* @returns {Promise<{dateCreated: string, utcDateCreated: string, dateModified: string, utcDateModified: string}>}
|
||||
*/
|
||||
async getMetadata() {
|
||||
return await server.get(`notes/${this.noteId}/metadata`);
|
||||
return await server.get<NoteMetaData>(`notes/${this.noteId}/metadata`);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
const registeredClasses = new Set();
|
||||
const registeredClasses = new Set<string>();
|
||||
|
||||
function createClassForColor(color) {
|
||||
function createClassForColor(color: string | null) {
|
||||
if (!color?.trim()) {
|
||||
return "";
|
||||
}
|
@ -1,67 +1,61 @@
|
||||
import dayjs from "dayjs";
|
||||
import { FNoteRow } from "../entities/fnote.js";
|
||||
import froca from "./froca.js";
|
||||
import server from "./server.js";
|
||||
import ws from "./ws.js";
|
||||
|
||||
/** @returns {FNote} */
|
||||
async function getInboxNote() {
|
||||
const note = await server.get(`special-notes/inbox/${dayjs().format("YYYY-MM-DD")}`, "date-note");
|
||||
const note = await server.get<FNoteRow>(`special-notes/inbox/${dayjs().format("YYYY-MM-DD")}`, "date-note");
|
||||
|
||||
return await froca.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async function getTodayNote() {
|
||||
return await getDayNote(dayjs().format("YYYY-MM-DD"));
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async function getDayNote(date) {
|
||||
const note = await server.get(`special-notes/days/${date}`, "date-note");
|
||||
async function getDayNote(date: string) {
|
||||
const note = await server.get<FNoteRow>(`special-notes/days/${date}`, "date-note");
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
return await froca.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async function getWeekNote(date) {
|
||||
const note = await server.get(`special-notes/weeks/${date}`, "date-note");
|
||||
async function getWeekNote(date: string) {
|
||||
const note = await server.get<FNoteRow>(`special-notes/weeks/${date}`, "date-note");
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
return await froca.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async function getMonthNote(month) {
|
||||
const note = await server.get(`special-notes/months/${month}`, "date-note");
|
||||
async function getMonthNote(month: string) {
|
||||
const note = await server.get<FNoteRow>(`special-notes/months/${month}`, "date-note");
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
return await froca.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async function getYearNote(year) {
|
||||
const note = await server.get(`special-notes/years/${year}`, "date-note");
|
||||
async function getYearNote(year: string) {
|
||||
const note = await server.get<FNoteRow>(`special-notes/years/${year}`, "date-note");
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
return await froca.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async function createSqlConsole() {
|
||||
const note = await server.post('special-notes/sql-console');
|
||||
const note = await server.post<FNoteRow>('special-notes/sql-console');
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
return await froca.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/** @returns {FNote} */
|
||||
async function createSearchNote(opts = {}) {
|
||||
const note = await server.post('special-notes/search-note', opts);
|
||||
const note = await server.post<FNoteRow>('special-notes/search-note', opts);
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
24
src/public/app/services/froca-interface.ts
Normal file
24
src/public/app/services/froca-interface.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import FAttribute from "../entities/fattribute.js";
|
||||
import FBlob from "../entities/fblob.js";
|
||||
import FBranch from "../entities/fbranch.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
|
||||
export interface Froca {
|
||||
notes: Record<string, FNote>;
|
||||
branches: Record<string, FBranch>;
|
||||
attributes: Record<string, FAttribute>;
|
||||
attachments: Record<string, FAttachment>;
|
||||
blobPromises: Record<string, Promise<void | FBlob> | null>;
|
||||
|
||||
getBlob(entityType: string, entityId: string): Promise<void | FBlob | null>;
|
||||
getNote(noteId: string, silentNotFoundError?: boolean): Promise<FNote | null>;
|
||||
getNoteFromCache(noteId: string): FNote;
|
||||
getNotesFromCache(noteIds: string[], silentNotFoundError?: boolean): FNote[];
|
||||
getNotes(noteIds: string[], silentNotFoundError?: boolean): Promise<FNote[]>;
|
||||
|
||||
getBranch(branchId: string, silentNotFoundError?: boolean): FBranch | undefined;
|
||||
getBranches(branchIds: string[], silentNotFoundError?: boolean): FBranch[];
|
||||
|
||||
getAttachmentsForNote(noteId: string): Promise<FAttachment[]>;
|
||||
}
|
@ -1,10 +1,24 @@
|
||||
import FBranch from "../entities/fbranch.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import FAttribute from "../entities/fattribute.js";
|
||||
import FBranch, { FBranchRow } from "../entities/fbranch.js";
|
||||
import FNote, { FNoteRow } from "../entities/fnote.js";
|
||||
import FAttribute, { FAttributeRow } from "../entities/fattribute.js";
|
||||
import server from "./server.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import FBlob from "../entities/fblob.js";
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import FBlob, { FBlobRow } from "../entities/fblob.js";
|
||||
import FAttachment, { FAttachmentRow } from "../entities/fattachment.js";
|
||||
import { Froca } from "./froca-interface.js";
|
||||
|
||||
|
||||
interface SubtreeResponse {
|
||||
notes: FNoteRow[];
|
||||
branches: FBranchRow[];
|
||||
attributes: FAttributeRow[];
|
||||
}
|
||||
|
||||
interface SearchNoteResponse {
|
||||
searchResultNoteIds: string[];
|
||||
highlightedTokens: string[];
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Froca (FROntend CAche) keeps a read only cache of note tree structure in frontend's memory.
|
||||
@ -16,48 +30,47 @@ import FAttachment from "../entities/fattachment.js";
|
||||
*
|
||||
* Backend has a similar cache called Becca
|
||||
*/
|
||||
class Froca {
|
||||
class FrocaImpl implements Froca {
|
||||
initializedPromise: Promise<void>;
|
||||
|
||||
notes!: Record<string, FNote>;
|
||||
branches!: Record<string, FBranch>;
|
||||
attributes!: Record<string, FAttribute>;
|
||||
attachments!: Record<string, FAttachment>;
|
||||
blobPromises!: Record<string, Promise<void | FBlob> | null>;
|
||||
|
||||
constructor() {
|
||||
this.initializedPromise = this.loadInitialTree();
|
||||
}
|
||||
|
||||
async loadInitialTree() {
|
||||
const resp = await server.get('tree');
|
||||
const resp = await server.get<SubtreeResponse>('tree');
|
||||
|
||||
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
|
||||
|
||||
/** @type {Object.<string, FNote>} */
|
||||
this.notes = {};
|
||||
|
||||
/** @type {Object.<string, FBranch>} */
|
||||
this.branches = {};
|
||||
|
||||
/** @type {Object.<string, FAttribute>} */
|
||||
this.attributes = {};
|
||||
|
||||
/** @type {Object.<string, FAttachment>} */
|
||||
this.attachments = {};
|
||||
|
||||
/** @type {Object.<string, Promise<FBlob>>} */
|
||||
this.blobPromises = {};
|
||||
|
||||
this.addResp(resp);
|
||||
}
|
||||
|
||||
async loadSubTree(subTreeNoteId) {
|
||||
const resp = await server.get(`tree?subTreeNoteId=${subTreeNoteId}`);
|
||||
async loadSubTree(subTreeNoteId: string) {
|
||||
const resp = await server.get<SubtreeResponse>(`tree?subTreeNoteId=${subTreeNoteId}`);
|
||||
|
||||
this.addResp(resp);
|
||||
|
||||
return this.notes[subTreeNoteId];
|
||||
}
|
||||
|
||||
addResp(resp) {
|
||||
addResp(resp: SubtreeResponse) {
|
||||
const noteRows = resp.notes;
|
||||
const branchRows = resp.branches;
|
||||
const attributeRows = resp.attributes;
|
||||
|
||||
const noteIdsToSort = new Set();
|
||||
const noteIdsToSort = new Set<string>();
|
||||
|
||||
for (const noteRow of noteRows) {
|
||||
const {noteId} = noteRow;
|
||||
@ -160,28 +173,28 @@ class Froca {
|
||||
}
|
||||
}
|
||||
|
||||
async reloadNotes(noteIds) {
|
||||
async reloadNotes(noteIds: string[]) {
|
||||
if (noteIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
noteIds = Array.from(new Set(noteIds)); // make noteIds unique
|
||||
|
||||
const resp = await server.post('tree/load', { noteIds });
|
||||
const resp = await server.post<SubtreeResponse>('tree/load', { noteIds });
|
||||
|
||||
this.addResp(resp);
|
||||
|
||||
appContext.triggerEvent('notesReloaded', {noteIds});
|
||||
}
|
||||
|
||||
async loadSearchNote(noteId) {
|
||||
async loadSearchNote(noteId: string) {
|
||||
const note = await this.getNote(noteId);
|
||||
|
||||
if (!note || note.type !== 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
const {searchResultNoteIds, highlightedTokens, error} = await server.get(`search-note/${note.noteId}`);
|
||||
const {searchResultNoteIds, highlightedTokens, error} = await server.get<SearchNoteResponse>(`search-note/${note.noteId}`);
|
||||
|
||||
if (!Array.isArray(searchResultNoteIds)) {
|
||||
throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`);
|
||||
@ -193,7 +206,7 @@ class Froca {
|
||||
froca.notes[note.noteId].childToBranch = {};
|
||||
}
|
||||
|
||||
const branches = [...note.getParentBranches(), ...note.getChildBranches()];
|
||||
const branches: FBranchRow[] = [...note.getParentBranches(), ...note.getChildBranches()];
|
||||
|
||||
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
|
||||
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
|
||||
@ -217,8 +230,7 @@ class Froca {
|
||||
return {error};
|
||||
}
|
||||
|
||||
/** @returns {FNote[]} */
|
||||
getNotesFromCache(noteIds, silentNotFoundError = false) {
|
||||
getNotesFromCache(noteIds: string[], silentNotFoundError = false): FNote[] {
|
||||
return noteIds.map(noteId => {
|
||||
if (!this.notes[noteId] && !silentNotFoundError) {
|
||||
console.trace(`Can't find note '${noteId}'`);
|
||||
@ -228,11 +240,10 @@ class Froca {
|
||||
else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
}).filter(note => !!note);
|
||||
}).filter(note => !!note) as FNote[];
|
||||
}
|
||||
|
||||
/** @returns {Promise<FNote[]>} */
|
||||
async getNotes(noteIds, silentNotFoundError = false) {
|
||||
async getNotes(noteIds: string[], silentNotFoundError = false): Promise<FNote[]> {
|
||||
noteIds = Array.from(new Set(noteIds)); // make unique
|
||||
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
|
||||
|
||||
@ -246,18 +257,16 @@ class Froca {
|
||||
} else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
}).filter(note => !!note);
|
||||
}).filter(note => !!note) as FNote[];
|
||||
}
|
||||
|
||||
/** @returns {Promise<boolean>} */
|
||||
async noteExists(noteId) {
|
||||
async noteExists(noteId: string): Promise<boolean> {
|
||||
const notes = await this.getNotes([noteId], true);
|
||||
|
||||
return notes.length === 1;
|
||||
}
|
||||
|
||||
/** @returns {Promise<FNote>} */
|
||||
async getNote(noteId, silentNotFoundError = false) {
|
||||
async getNote(noteId: string, silentNotFoundError = false): Promise<FNote | null> {
|
||||
if (noteId === 'none') {
|
||||
console.trace(`No 'none' note.`);
|
||||
return null;
|
||||
@ -270,8 +279,7 @@ class Froca {
|
||||
return (await this.getNotes([noteId], silentNotFoundError))[0];
|
||||
}
|
||||
|
||||
/** @returns {FNote|null} */
|
||||
getNoteFromCache(noteId) {
|
||||
getNoteFromCache(noteId: string) {
|
||||
if (!noteId) {
|
||||
throw new Error("Empty noteId");
|
||||
}
|
||||
@ -279,15 +287,13 @@ class Froca {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
/** @returns {FBranch[]} */
|
||||
getBranches(branchIds, silentNotFoundError = false) {
|
||||
getBranches(branchIds: string[], silentNotFoundError = false): FBranch[] {
|
||||
return branchIds
|
||||
.map(branchId => this.getBranch(branchId, silentNotFoundError))
|
||||
.filter(b => !!b);
|
||||
.filter(b => !!b) as FBranch[];
|
||||
}
|
||||
|
||||
/** @returns {FBranch} */
|
||||
getBranch(branchId, silentNotFoundError = false) {
|
||||
getBranch(branchId: string, silentNotFoundError = false) {
|
||||
if (!(branchId in this.branches)) {
|
||||
if (!silentNotFoundError) {
|
||||
logError(`Not existing branch '${branchId}'`);
|
||||
@ -298,7 +304,7 @@ class Froca {
|
||||
}
|
||||
}
|
||||
|
||||
async getBranchId(parentNoteId, childNoteId) {
|
||||
async getBranchId(parentNoteId: string, childNoteId: string) {
|
||||
if (childNoteId === 'root') {
|
||||
return 'none_root';
|
||||
}
|
||||
@ -314,8 +320,7 @@ class Froca {
|
||||
return child.parentToBranch[parentNoteId];
|
||||
}
|
||||
|
||||
/** @returns {Promise<FAttachment>} */
|
||||
async getAttachment(attachmentId, silentNotFoundError = false) {
|
||||
async getAttachment(attachmentId: string, silentNotFoundError = false) {
|
||||
const attachment = this.attachments[attachmentId];
|
||||
if (attachment) {
|
||||
return attachment;
|
||||
@ -324,9 +329,8 @@ class Froca {
|
||||
// load all attachments for the given note even if one is requested, don't load one by one
|
||||
let attachmentRows;
|
||||
try {
|
||||
attachmentRows = await server.getWithSilentNotFound(`attachments/${attachmentId}/all`);
|
||||
}
|
||||
catch (e) {
|
||||
attachmentRows = await server.getWithSilentNotFound<FAttachmentRow[]>(`attachments/${attachmentId}/all`);
|
||||
} catch (e: any) {
|
||||
if (silentNotFoundError) {
|
||||
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ` + e.message);
|
||||
return null;
|
||||
@ -344,14 +348,12 @@ class Froca {
|
||||
return this.attachments[attachmentId];
|
||||
}
|
||||
|
||||
/** @returns {Promise<FAttachment[]>} */
|
||||
async getAttachmentsForNote(noteId) {
|
||||
const attachmentRows = await server.get(`notes/${noteId}/attachments`);
|
||||
async getAttachmentsForNote(noteId: string) {
|
||||
const attachmentRows = await server.get<FAttachmentRow[]>(`notes/${noteId}/attachments`);
|
||||
return this.processAttachmentRows(attachmentRows);
|
||||
}
|
||||
|
||||
/** @returns {FAttachment[]} */
|
||||
processAttachmentRows(attachmentRows) {
|
||||
processAttachmentRows(attachmentRows: FAttachmentRow[]): FAttachment[] {
|
||||
return attachmentRows.map(attachmentRow => {
|
||||
let attachment;
|
||||
|
||||
@ -367,22 +369,21 @@ class Froca {
|
||||
});
|
||||
}
|
||||
|
||||
/** @returns {Promise<FBlob>} */
|
||||
async getBlob(entityType, entityId) {
|
||||
async getBlob(entityType: string, entityId: string) {
|
||||
// I'm not sure why we're not using blobIds directly, it would save us this composite key ...
|
||||
// perhaps one benefit is that we're always requesting the latest blob, not relying on perhaps faulty/slow
|
||||
// websocket update?
|
||||
const key = `${entityType}-${entityId}`;
|
||||
|
||||
if (!this.blobPromises[key]) {
|
||||
this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob`)
|
||||
this.blobPromises[key] = server.get<FBlobRow>(`${entityType}/${entityId}/blob`)
|
||||
.then(row => new FBlob(row))
|
||||
.catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`, e));
|
||||
|
||||
// we don't want to keep large payloads forever in memory, so we clean that up quite quickly
|
||||
// this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components)
|
||||
// if the blob is updated within the cache lifetime, it should be invalidated by froca_updater
|
||||
this.blobPromises[key].then(
|
||||
this.blobPromises[key]?.then(
|
||||
() => setTimeout(() => this.blobPromises[key] = null, 1000)
|
||||
);
|
||||
}
|
||||
@ -391,6 +392,6 @@ class Froca {
|
||||
}
|
||||
}
|
||||
|
||||
const froca = new Froca();
|
||||
const froca = new FrocaImpl();
|
||||
|
||||
export default froca;
|
@ -3,11 +3,13 @@ import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import options from "./options.js";
|
||||
import noteAttributeCache from "./note_attribute_cache.js";
|
||||
import FBranch from "../entities/fbranch.js";
|
||||
import FAttribute from "../entities/fattribute.js";
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import FBranch, { FBranchRow } from "../entities/fbranch.js";
|
||||
import FAttribute, { FAttributeRow } from "../entities/fattribute.js";
|
||||
import FAttachment, { FAttachmentRow } from "../entities/fattachment.js";
|
||||
import FNote, { FNoteRow } from "../entities/fnote.js";
|
||||
import { EntityChange } from "../../../services/entity_changes_interface.js";
|
||||
|
||||
async function processEntityChanges(entityChanges) {
|
||||
async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
const loadResults = new LoadResults(entityChanges);
|
||||
|
||||
for (const ec of entityChanges) {
|
||||
@ -23,13 +25,14 @@ async function processEntityChanges(entityChanges) {
|
||||
} else if (ec.entityName === 'revisions') {
|
||||
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
|
||||
} else if (ec.entityName === 'options') {
|
||||
if (ec.entity.name === 'openNoteContexts') {
|
||||
const attributeEntity = ec.entity as FAttributeRow;
|
||||
if (attributeEntity.name === 'openNoteContexts') {
|
||||
continue; // only noise
|
||||
}
|
||||
|
||||
options.set(ec.entity.name, ec.entity.value);
|
||||
options.set(attributeEntity.name, attributeEntity.value);
|
||||
|
||||
loadResults.addOption(ec.entity.name);
|
||||
loadResults.addOption(attributeEntity.name);
|
||||
} else if (ec.entityName === 'attachments') {
|
||||
processAttachment(loadResults, ec);
|
||||
} else if (ec.entityName === 'blobs' || ec.entityName === 'etapi_tokens') {
|
||||
@ -39,7 +42,7 @@ async function processEntityChanges(entityChanges) {
|
||||
throw new Error(`Unknown entityName '${ec.entityName}'`);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
catch (e: any) {
|
||||
throw new Error(`Can't process entity ${JSON.stringify(ec)} with error ${e.message} ${e.stack}`);
|
||||
}
|
||||
}
|
||||
@ -56,15 +59,16 @@ async function processEntityChanges(entityChanges) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entityName === 'branches' && !(entity.parentNoteId in froca.notes)) {
|
||||
missingNoteIds.push(entity.parentNoteId);
|
||||
if (entityName === 'branches' && !((entity as FBranchRow).parentNoteId in froca.notes)) {
|
||||
missingNoteIds.push((entity as FBranchRow).parentNoteId);
|
||||
}
|
||||
else if (entityName === 'attributes'
|
||||
&& entity.type === 'relation'
|
||||
&& (entity.name === 'template' || entity.name === 'inherit')
|
||||
&& !(entity.value in froca.notes)) {
|
||||
|
||||
missingNoteIds.push(entity.value);
|
||||
else if (entityName === 'attributes') {
|
||||
let attributeEntity = entity as FAttributeRow;
|
||||
if (attributeEntity.type === 'relation'
|
||||
&& (attributeEntity.name === 'template' || attributeEntity.name === 'inherit')
|
||||
&& !(attributeEntity.value in froca.notes)) {
|
||||
missingNoteIds.push(attributeEntity.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,12 +81,14 @@ async function processEntityChanges(entityChanges) {
|
||||
noteAttributeCache.invalidate();
|
||||
}
|
||||
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
// TODO: Remove after porting the file
|
||||
// @ts-ignore
|
||||
const appContext = (await import("../components/app_context.js")).default as any;
|
||||
await appContext.triggerEvent('entitiesReloaded', {loadResults});
|
||||
}
|
||||
}
|
||||
|
||||
function processNoteChange(loadResults, ec) {
|
||||
function processNoteChange(loadResults: LoadResults, ec: EntityChange) {
|
||||
const note = froca.notes[ec.entityId];
|
||||
|
||||
if (!note) {
|
||||
@ -102,21 +108,23 @@ function processNoteChange(loadResults, ec) {
|
||||
delete froca.notes[ec.entityId];
|
||||
}
|
||||
else {
|
||||
if (note.blobId !== ec.entity.blobId) {
|
||||
if (note.blobId !== (ec.entity as FNoteRow).blobId) {
|
||||
for (const key of Object.keys(froca.blobPromises)) {
|
||||
if (key.includes(note.noteId)) {
|
||||
delete froca.blobPromises[key];
|
||||
}
|
||||
}
|
||||
|
||||
loadResults.addNoteContent(note.noteId, ec.componentId);
|
||||
if (ec.componentId) {
|
||||
loadResults.addNoteContent(note.noteId, ec.componentId);
|
||||
}
|
||||
}
|
||||
|
||||
note.update(ec.entity);
|
||||
note.update(ec.entity as FNoteRow);
|
||||
}
|
||||
}
|
||||
|
||||
async function processBranchChange(loadResults, ec) {
|
||||
async function processBranchChange(loadResults: LoadResults, ec: EntityChange) {
|
||||
if (ec.isErased && ec.entityId in froca.branches) {
|
||||
utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
|
||||
return;
|
||||
@ -139,7 +147,9 @@ async function processBranchChange(loadResults, ec) {
|
||||
delete parentNote.childToBranch[branch.noteId];
|
||||
}
|
||||
|
||||
loadResults.addBranch(ec.entityId, ec.componentId);
|
||||
if (ec.componentId) {
|
||||
loadResults.addBranch(ec.entityId, ec.componentId);
|
||||
}
|
||||
|
||||
delete froca.branches[ec.entityId];
|
||||
}
|
||||
@ -147,24 +157,27 @@ async function processBranchChange(loadResults, ec) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadResults.addBranch(ec.entityId, ec.componentId);
|
||||
if (ec.componentId) {
|
||||
loadResults.addBranch(ec.entityId, ec.componentId);
|
||||
}
|
||||
|
||||
const childNote = froca.notes[ec.entity.noteId];
|
||||
let parentNote = froca.notes[ec.entity.parentNoteId];
|
||||
const branchEntity = ec.entity as FBranchRow;
|
||||
const childNote = froca.notes[branchEntity.noteId];
|
||||
let parentNote: FNote | null = froca.notes[branchEntity.parentNoteId];
|
||||
|
||||
if (childNote && !childNote.isRoot() && !parentNote) {
|
||||
// a branch cannot exist without the parent
|
||||
// a note loaded into froca has to also contain all its ancestors,
|
||||
// this problem happened, e.g., in sharing where _share was hidden and thus not loaded
|
||||
// sharing meant cloning into _share, which crashed because _share was not loaded
|
||||
parentNote = await froca.getNote(ec.entity.parentNoteId);
|
||||
parentNote = await froca.getNote(branchEntity.parentNoteId);
|
||||
}
|
||||
|
||||
if (branch) {
|
||||
branch.update(ec.entity);
|
||||
branch.update(ec.entity as FBranch);
|
||||
}
|
||||
else if (childNote || parentNote) {
|
||||
froca.branches[ec.entityId] = branch = new FBranch(froca, ec.entity);
|
||||
froca.branches[ec.entityId] = branch = new FBranch(froca, branchEntity);
|
||||
}
|
||||
|
||||
if (childNote) {
|
||||
@ -176,8 +189,8 @@ async function processBranchChange(loadResults, ec) {
|
||||
}
|
||||
}
|
||||
|
||||
function processNoteReordering(loadResults, ec) {
|
||||
const parentNoteIdsToSort = new Set();
|
||||
function processNoteReordering(loadResults: LoadResults, ec: EntityChange) {
|
||||
const parentNoteIdsToSort = new Set<string>();
|
||||
|
||||
for (const branchId in ec.positions) {
|
||||
const branch = froca.branches[branchId];
|
||||
@ -197,10 +210,12 @@ function processNoteReordering(loadResults, ec) {
|
||||
}
|
||||
}
|
||||
|
||||
loadResults.addNoteReordering(ec.entityId, ec.componentId);
|
||||
if (ec.componentId) {
|
||||
loadResults.addNoteReordering(ec.entityId, ec.componentId);
|
||||
}
|
||||
}
|
||||
|
||||
function processAttributeChange(loadResults, ec) {
|
||||
function processAttributeChange(loadResults: LoadResults, ec: EntityChange) {
|
||||
let attribute = froca.attributes[ec.entityId];
|
||||
|
||||
if (ec.isErased && ec.entityId in froca.attributes) {
|
||||
@ -221,7 +236,9 @@ function processAttributeChange(loadResults, ec) {
|
||||
targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.attributeId);
|
||||
}
|
||||
|
||||
loadResults.addAttribute(ec.entityId, ec.componentId);
|
||||
if (ec.componentId) {
|
||||
loadResults.addAttribute(ec.entityId, ec.componentId);
|
||||
}
|
||||
|
||||
delete froca.attributes[ec.entityId];
|
||||
}
|
||||
@ -229,15 +246,18 @@ function processAttributeChange(loadResults, ec) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadResults.addAttribute(ec.entityId, ec.componentId);
|
||||
if (ec.componentId) {
|
||||
loadResults.addAttribute(ec.entityId, ec.componentId);
|
||||
}
|
||||
|
||||
const sourceNote = froca.notes[ec.entity.noteId];
|
||||
const targetNote = ec.entity.type === 'relation' && froca.notes[ec.entity.value];
|
||||
const attributeEntity = ec.entity as FAttributeRow;
|
||||
const sourceNote = froca.notes[attributeEntity.noteId];
|
||||
const targetNote = attributeEntity.type === 'relation' && froca.notes[attributeEntity.value];
|
||||
|
||||
if (attribute) {
|
||||
attribute.update(ec.entity);
|
||||
attribute.update(ec.entity as FAttributeRow);
|
||||
} else if (sourceNote || targetNote) {
|
||||
attribute = new FAttribute(froca, ec.entity);
|
||||
attribute = new FAttribute(froca, ec.entity as FAttributeRow);
|
||||
|
||||
froca.attributes[attribute.attributeId] = attribute;
|
||||
|
||||
@ -251,15 +271,16 @@ function processAttributeChange(loadResults, ec) {
|
||||
}
|
||||
}
|
||||
|
||||
function processAttachment(loadResults, ec) {
|
||||
function processAttachment(loadResults: LoadResults, ec: EntityChange) {
|
||||
if (ec.isErased && ec.entityId in froca.attachments) {
|
||||
utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = froca.attachments[ec.entityId];
|
||||
const attachmentEntity = ec.entity as FAttachmentRow;
|
||||
|
||||
if (ec.isErased || ec.entity?.isDeleted) {
|
||||
if (ec.isErased || (ec.entity as any)?.isDeleted) {
|
||||
if (attachment) {
|
||||
const note = attachment.getNote();
|
||||
|
||||
@ -267,7 +288,7 @@ function processAttachment(loadResults, ec) {
|
||||
note.attachments = note.attachments.filter(att => att.attachmentId !== attachment.attachmentId);
|
||||
}
|
||||
|
||||
loadResults.addAttachmentRow(ec.entity);
|
||||
loadResults.addAttachmentRow(attachmentEntity);
|
||||
|
||||
delete froca.attachments[ec.entityId];
|
||||
}
|
||||
@ -276,16 +297,17 @@ function processAttachment(loadResults, ec) {
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
attachment.update(ec.entity);
|
||||
attachment.update(ec.entity as FAttachmentRow);
|
||||
} else {
|
||||
const note = froca.notes[ec.entity.ownerId];
|
||||
const attachmentRow = ec.entity as FAttachmentRow;
|
||||
const note = froca.notes[attachmentRow.ownerId];
|
||||
|
||||
if (note?.attachments) {
|
||||
note.attachments.push(new FAttachment(froca, ec.entity));
|
||||
note.attachments.push(new FAttachment(froca, attachmentRow));
|
||||
}
|
||||
}
|
||||
|
||||
loadResults.addAttachmentRow(ec.entity);
|
||||
loadResults.addAttachmentRow(attachmentEntity);
|
||||
}
|
||||
|
||||
export default {
|
@ -1,7 +1,8 @@
|
||||
import appContext from "../components/app_context.js";
|
||||
import treeService from "./tree.js";
|
||||
import treeService, { Node } from "./tree.js";
|
||||
import dialogService from "./dialog.js";
|
||||
import froca from "./froca.js";
|
||||
import NoteContext from "../components/note_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
function getHoistedNoteId() {
|
||||
@ -18,11 +19,11 @@ async function unhoist() {
|
||||
}
|
||||
}
|
||||
|
||||
function isTopLevelNode(node) {
|
||||
function isTopLevelNode(node: Node) {
|
||||
return isHoistedNode(node.getParent());
|
||||
}
|
||||
|
||||
function isHoistedNode(node) {
|
||||
function isHoistedNode(node: Node) {
|
||||
// even though check for 'root' should not be necessary, we keep it just in case
|
||||
return node.data.noteId === "root"
|
||||
|| node.data.noteId === getHoistedNoteId();
|
||||
@ -36,10 +37,10 @@ async function isHoistedInHiddenSubtree() {
|
||||
}
|
||||
|
||||
const hoistedNote = await froca.getNote(hoistedNoteId);
|
||||
return hoistedNote.isHiddenCompletely();
|
||||
return hoistedNote?.isHiddenCompletely();
|
||||
}
|
||||
|
||||
async function checkNoteAccess(notePath, noteContext) {
|
||||
async function checkNoteAccess(notePath: string, noteContext: NoteContext) {
|
||||
const resolvedNotePath = await treeService.resolveNotePath(notePath, noteContext.hoistedNoteId);
|
||||
|
||||
if (!resolvedNotePath) {
|
||||
@ -50,11 +51,15 @@ async function checkNoteAccess(notePath, noteContext) {
|
||||
const hoistedNoteId = noteContext.hoistedNoteId;
|
||||
|
||||
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes('_hidden') || resolvedNotePath.includes('_lbBookmarks'))) {
|
||||
const requestedNote = await froca.getNote(treeService.getNoteIdFromUrl(resolvedNotePath));
|
||||
const noteId = treeService.getNoteIdFromUrl(resolvedNotePath);
|
||||
if (!noteId) {
|
||||
return false;
|
||||
}
|
||||
const requestedNote = await froca.getNote(noteId);
|
||||
const hoistedNote = await froca.getNote(hoistedNoteId);
|
||||
|
||||
if ((!hoistedNote.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
|
||||
&& !await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote.title, hoistedNote: hoistedNote.title }))) {
|
||||
if ((!hoistedNote?.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
|
||||
&& !await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote?.title, hoistedNote: hoistedNote?.title }))) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1,5 +1,46 @@
|
||||
import { EntityChange } from "../../../services/entity_changes_interface.js";
|
||||
|
||||
interface BranchRow {
|
||||
branchId: string;
|
||||
componentId: string;
|
||||
}
|
||||
|
||||
interface AttributeRow {
|
||||
attributeId: string;
|
||||
componentId: string;
|
||||
}
|
||||
|
||||
interface RevisionRow {
|
||||
revisionId: string;
|
||||
noteId?: string;
|
||||
componentId?: string | null;
|
||||
}
|
||||
|
||||
interface ContentNoteIdToComponentIdRow {
|
||||
noteId: string;
|
||||
componentId: string;
|
||||
}
|
||||
|
||||
interface AttachmentRow {}
|
||||
|
||||
interface ContentNoteIdToComponentIdRow {
|
||||
noteId: string;
|
||||
componentId: string;
|
||||
}
|
||||
|
||||
export default class LoadResults {
|
||||
constructor(entityChanges) {
|
||||
private entities: Record<string, Record<string, unknown>>;
|
||||
private noteIdToComponentId: Record<string, string[]>;
|
||||
private componentIdToNoteIds: Record<string, string[]>;
|
||||
private branchRows: BranchRow[];
|
||||
private attributeRows: AttributeRow[];
|
||||
private revisionRows: RevisionRow[];
|
||||
private noteReorderings: string[];
|
||||
private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[];
|
||||
private optionNames: string[];
|
||||
private attachmentRows: AttachmentRow[];
|
||||
|
||||
constructor(entityChanges: EntityChange[]) {
|
||||
this.entities = {};
|
||||
|
||||
for (const {entityId, entityName, entity} of entityChanges) {
|
||||
@ -27,25 +68,27 @@ export default class LoadResults {
|
||||
this.attachmentRows = [];
|
||||
}
|
||||
|
||||
getEntityRow(entityName, entityId) {
|
||||
getEntityRow(entityName: string, entityId: string) {
|
||||
return this.entities[entityName]?.[entityId];
|
||||
}
|
||||
|
||||
addNote(noteId, componentId) {
|
||||
addNote(noteId: string, componentId?: string | null) {
|
||||
this.noteIdToComponentId[noteId] = this.noteIdToComponentId[noteId] || [];
|
||||
|
||||
if (!this.noteIdToComponentId[noteId].includes(componentId)) {
|
||||
this.noteIdToComponentId[noteId].push(componentId);
|
||||
}
|
||||
if (componentId) {
|
||||
if (!this.noteIdToComponentId[noteId].includes(componentId)) {
|
||||
this.noteIdToComponentId[noteId].push(componentId);
|
||||
}
|
||||
|
||||
this.componentIdToNoteIds[componentId] = this.componentIdToNoteIds[componentId] || [];
|
||||
this.componentIdToNoteIds[componentId] = this.componentIdToNoteIds[componentId] || [];
|
||||
|
||||
if (!this.componentIdToNoteIds[componentId]) {
|
||||
this.componentIdToNoteIds[componentId].push(noteId);
|
||||
if (this.componentIdToNoteIds[componentId]) {
|
||||
this.componentIdToNoteIds[componentId].push(noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addBranch(branchId, componentId) {
|
||||
addBranch(branchId: string, componentId: string) {
|
||||
this.branchRows.push({branchId, componentId});
|
||||
}
|
||||
|
||||
@ -55,7 +98,7 @@ export default class LoadResults {
|
||||
.filter(branch => !!branch);
|
||||
}
|
||||
|
||||
addNoteReordering(parentNoteId, componentId) {
|
||||
addNoteReordering(parentNoteId: string, componentId: string) {
|
||||
this.noteReorderings.push(parentNoteId);
|
||||
}
|
||||
|
||||
@ -63,7 +106,7 @@ export default class LoadResults {
|
||||
return this.noteReorderings;
|
||||
}
|
||||
|
||||
addAttribute(attributeId, componentId) {
|
||||
addAttribute(attributeId: string, componentId: string) {
|
||||
this.attributeRows.push({attributeId, componentId});
|
||||
}
|
||||
|
||||
@ -74,11 +117,11 @@ export default class LoadResults {
|
||||
.filter(attr => !!attr);
|
||||
}
|
||||
|
||||
addRevision(revisionId, noteId, componentId) {
|
||||
addRevision(revisionId: string, noteId?: string, componentId?: string | null) {
|
||||
this.revisionRows.push({revisionId, noteId, componentId});
|
||||
}
|
||||
|
||||
hasRevisionForNote(noteId) {
|
||||
hasRevisionForNote(noteId: string) {
|
||||
return !!this.revisionRows.find(row => row.noteId === noteId);
|
||||
}
|
||||
|
||||
@ -86,7 +129,7 @@ export default class LoadResults {
|
||||
return Object.keys(this.noteIdToComponentId);
|
||||
}
|
||||
|
||||
isNoteReloaded(noteId, componentId = null) {
|
||||
isNoteReloaded(noteId: string, componentId = null) {
|
||||
if (!noteId) {
|
||||
return false;
|
||||
}
|
||||
@ -95,11 +138,11 @@ export default class LoadResults {
|
||||
return componentIds && componentIds.find(sId => sId !== componentId) !== undefined;
|
||||
}
|
||||
|
||||
addNoteContent(noteId, componentId) {
|
||||
addNoteContent(noteId: string, componentId: string) {
|
||||
this.contentNoteIdToComponentId.push({noteId, componentId});
|
||||
}
|
||||
|
||||
isNoteContentReloaded(noteId, componentId) {
|
||||
isNoteContentReloaded(noteId: string, componentId: string) {
|
||||
if (!noteId) {
|
||||
return false;
|
||||
}
|
||||
@ -107,11 +150,11 @@ export default class LoadResults {
|
||||
return this.contentNoteIdToComponentId.find(l => l.noteId === noteId && l.componentId !== componentId);
|
||||
}
|
||||
|
||||
addOption(name) {
|
||||
addOption(name: string) {
|
||||
this.optionNames.push(name);
|
||||
}
|
||||
|
||||
isOptionReloaded(name) {
|
||||
isOptionReloaded(name: string) {
|
||||
return this.optionNames.includes(name);
|
||||
}
|
||||
|
||||
@ -119,7 +162,7 @@ export default class LoadResults {
|
||||
return this.optionNames;
|
||||
}
|
||||
|
||||
addAttachmentRow(attachment) {
|
||||
addAttachmentRow(attachment: AttachmentRow) {
|
||||
this.attachmentRows.push(attachment);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import FAttribute from "../entities/fattribute.js";
|
||||
|
||||
/**
|
||||
* The purpose of this class is to cache the list of attributes for notes.
|
||||
*
|
||||
@ -6,8 +8,9 @@
|
||||
* as loading the tree which uses attributes heavily.
|
||||
*/
|
||||
class NoteAttributeCache {
|
||||
attributes: Record<string, FAttribute[]>;
|
||||
|
||||
constructor() {
|
||||
/** @property {Object.<string, BAttribute[]>} */
|
||||
this.attributes = {};
|
||||
}
|
||||
|
@ -1,25 +1,31 @@
|
||||
import utils from "./utils.js";
|
||||
import server from "./server.js";
|
||||
|
||||
function checkType(type) {
|
||||
type ExecFunction = (command: string, cb: ((err: string, stdout: string, stderror: string) => void)) => void;
|
||||
|
||||
interface TmpResponse {
|
||||
tmpFilePath: string;
|
||||
}
|
||||
|
||||
function checkType(type: string) {
|
||||
if (type !== 'notes' && type !== 'attachments') {
|
||||
throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileUrl(type, noteId) {
|
||||
function getFileUrl(type: string, noteId?: string) {
|
||||
checkType(type);
|
||||
|
||||
return getUrlForDownload(`api/${type}/${noteId}/download`);
|
||||
}
|
||||
|
||||
function getOpenFileUrl(type, noteId) {
|
||||
function getOpenFileUrl(type: string, noteId: string) {
|
||||
checkType(type);
|
||||
|
||||
return getUrlForDownload(`api/${type}/${noteId}/open`);
|
||||
}
|
||||
|
||||
function download(url) {
|
||||
function download(url: string) {
|
||||
if (utils.isElectron()) {
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
|
||||
@ -29,33 +35,33 @@ function download(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFileNote(noteId) {
|
||||
function downloadFileNote(noteId: string) {
|
||||
const url = `${getFileUrl('notes', noteId)}?${Date.now()}`; // don't use cache
|
||||
|
||||
download(url);
|
||||
}
|
||||
|
||||
function downloadAttachment(attachmentId) {
|
||||
function downloadAttachment(attachmentId: string) {
|
||||
const url = `${getFileUrl('attachments', attachmentId)}?${Date.now()}`; // don't use cache
|
||||
|
||||
download(url);
|
||||
}
|
||||
|
||||
async function openCustom(type, entityId, mime) {
|
||||
async function openCustom(type: string, entityId: string, mime: string) {
|
||||
checkType(type);
|
||||
if (!utils.isElectron() || utils.isMac()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await server.post(`${type}/${entityId}/save-to-tmp-dir`);
|
||||
const resp = await server.post<TmpResponse>(`${type}/${entityId}/save-to-tmp-dir`);
|
||||
let filePath = resp.tmpFilePath;
|
||||
const {exec} = utils.dynamicRequire('child_process');
|
||||
const exec = utils.dynamicRequire('child_process').exec as ExecFunction;
|
||||
const platform = process.platform;
|
||||
|
||||
if (platform === 'linux') {
|
||||
// we don't know which terminal is available, try in succession
|
||||
const terminals = ['x-terminal-emulator', 'gnome-terminal', 'konsole', 'xterm', 'xfce4-terminal', 'mate-terminal', 'rxvt', 'terminator', 'terminology'];
|
||||
const openFileWithTerminal = (terminal) => {
|
||||
const openFileWithTerminal = (terminal: string) => {
|
||||
const command = `${terminal} -e 'mimeopen -d "${filePath}"'`;
|
||||
console.log(`Open Note custom: ${command} `);
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
@ -68,11 +74,12 @@ async function openCustom(type, entityId, mime) {
|
||||
});
|
||||
};
|
||||
|
||||
const searchTerminal = (index) => {
|
||||
const searchTerminal = (index: number) => {
|
||||
const terminal = terminals[index];
|
||||
if (!terminal) {
|
||||
console.error('Open Note custom: No terminal found!');
|
||||
open(getFileUrl(type, entityId), {url: true});
|
||||
// TODO: Remove {url: true} if not needed.
|
||||
(open as any)(getFileUrl(type, entityId), {url: true});
|
||||
return;
|
||||
}
|
||||
exec(`which ${terminal}`, (error, stdout, stderr) => {
|
||||
@ -93,21 +100,27 @@ async function openCustom(type, entityId, mime) {
|
||||
exec(command, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error("Open Note custom: ", err);
|
||||
open(getFileUrl(entityId), {url: true});
|
||||
// TODO: This appears to be broken, since getFileUrl expects two arguments, with the first one being the type.
|
||||
// Also don't know why {url: true} is passed.
|
||||
(open as any)(getFileUrl(entityId), {url: true});
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('Currently "Open Note custom" only supports linux and windows systems');
|
||||
open(getFileUrl(entityId), {url: true});
|
||||
// TODO: This appears to be broken, since getFileUrl expects two arguments, with the first one being the type.
|
||||
// Also don't know why {url: true} is passed.
|
||||
(open as any)(getFileUrl(entityId), {url: true});
|
||||
}
|
||||
}
|
||||
|
||||
const openNoteCustom = async (noteId, mime) => await openCustom('notes', noteId, mime);
|
||||
const openAttachmentCustom = async (attachmentId, mime) => await openCustom('attachments', attachmentId, mime);
|
||||
const openNoteCustom =
|
||||
async (noteId: string, mime: string) => await openCustom('notes', noteId, mime);
|
||||
const openAttachmentCustom =
|
||||
async (attachmentId: string, mime: string) => await openCustom('attachments', attachmentId, mime);
|
||||
|
||||
|
||||
function downloadRevision(noteId, revisionId) {
|
||||
function downloadRevision(noteId: string, revisionId: string) {
|
||||
const url = getUrlForDownload(`api/revisions/${revisionId}/download`);
|
||||
|
||||
download(url);
|
||||
@ -116,7 +129,7 @@ function downloadRevision(noteId, revisionId) {
|
||||
/**
|
||||
* @param url - should be without initial slash!!!
|
||||
*/
|
||||
function getUrlForDownload(url) {
|
||||
function getUrlForDownload(url: string) {
|
||||
if (utils.isElectron()) {
|
||||
// electron needs absolute URL, so we extract current host, port, protocol
|
||||
return `${getHost()}/${url}`;
|
||||
@ -127,18 +140,18 @@ function getUrlForDownload(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function canOpenInBrowser(mime) {
|
||||
function canOpenInBrowser(mime: string) {
|
||||
return mime === "application/pdf"
|
||||
|| mime.startsWith("image")
|
||||
|| mime.startsWith("audio")
|
||||
|| mime.startsWith("video");
|
||||
}
|
||||
|
||||
async function openExternally(type, entityId, mime) {
|
||||
async function openExternally(type: string, entityId: string, mime: string) {
|
||||
checkType(type);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const resp = await server.post(`${type}/${entityId}/save-to-tmp-dir`);
|
||||
const resp = await server.post<TmpResponse>(`${type}/${entityId}/save-to-tmp-dir`);
|
||||
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
const res = await electron.shell.openPath(resp.tmpFilePath);
|
||||
@ -158,15 +171,17 @@ async function openExternally(type, entityId, mime) {
|
||||
}
|
||||
}
|
||||
|
||||
const openNoteExternally = async (noteId, mime) => await openExternally('notes', noteId, mime);
|
||||
const openAttachmentExternally = async (attachmentId, mime) => await openExternally('attachments', attachmentId, mime);
|
||||
const openNoteExternally =
|
||||
async (noteId: string, mime: string) => await openExternally('notes', noteId, mime);
|
||||
const openAttachmentExternally =
|
||||
async (attachmentId: string, mime: string) => await openExternally('attachments', attachmentId, mime);
|
||||
|
||||
function getHost() {
|
||||
const url = new URL(window.location.href);
|
||||
return `${url.protocol}//${url.hostname}:${url.port}`;
|
||||
}
|
||||
|
||||
async function openDirectory(directory) {
|
||||
async function openDirectory(directory: string) {
|
||||
try {
|
||||
if (utils.isElectron()) {
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
@ -177,7 +192,7 @@ async function openDirectory(directory) {
|
||||
} else {
|
||||
console.error('Not running in an Electron environment.');
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
// Handle file system errors (e.g. path does not exist or is inaccessible)
|
||||
console.error('Error:', err.message);
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import server from "./server.js";
|
||||
|
||||
class Options {
|
||||
constructor() {
|
||||
this.initializedPromise = server.get('options').then(data => this.load(data));
|
||||
}
|
||||
|
||||
load(arr) {
|
||||
this.arr = arr;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.arr[key];
|
||||
}
|
||||
|
||||
getNames() {
|
||||
return Object.keys(this.arr);
|
||||
}
|
||||
|
||||
getJson(key) {
|
||||
try {
|
||||
return JSON.parse(this.arr[key]);
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getInt(key) {
|
||||
return parseInt(this.arr[key]);
|
||||
}
|
||||
|
||||
getFloat(key) {
|
||||
return parseFloat(this.arr[key]);
|
||||
}
|
||||
|
||||
is(key) {
|
||||
return this.arr[key] === 'true';
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this.arr[key] = value;
|
||||
}
|
||||
|
||||
async save(key, value) {
|
||||
this.set(key, value);
|
||||
|
||||
const payload = {};
|
||||
payload[key] = value;
|
||||
|
||||
await server.put(`options`, payload);
|
||||
}
|
||||
|
||||
async toggle(key) {
|
||||
await this.save(key, (!this.is(key)).toString());
|
||||
}
|
||||
}
|
||||
|
||||
const options = new Options();
|
||||
|
||||
export default options;
|
79
src/public/app/services/options.ts
Normal file
79
src/public/app/services/options.ts
Normal file
@ -0,0 +1,79 @@
|
||||
|
||||
import server from "./server.js";
|
||||
|
||||
type OptionValue = string | number;
|
||||
|
||||
class Options {
|
||||
initializedPromise: Promise<void>;
|
||||
private arr!: Record<string, OptionValue>;
|
||||
|
||||
constructor() {
|
||||
this.initializedPromise = server.get<Record<string, OptionValue>>('options').then(data => this.load(data));
|
||||
}
|
||||
|
||||
load(arr: Record<string, OptionValue>) {
|
||||
this.arr = arr;
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return this.arr?.[key];
|
||||
}
|
||||
|
||||
getNames() {
|
||||
return Object.keys(this.arr || []);
|
||||
}
|
||||
|
||||
getJson(key: string) {
|
||||
const value = this.arr?.[key];
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getInt(key: string) {
|
||||
const value = this.arr?.[key];
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
return parseInt(value);
|
||||
}
|
||||
|
||||
getFloat(key: string) {
|
||||
const value = this.arr?.[key];
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
is(key: string) {
|
||||
return this.arr[key] === 'true';
|
||||
}
|
||||
|
||||
set(key: string, value: OptionValue) {
|
||||
this.arr[key] = value;
|
||||
}
|
||||
|
||||
async save(key: string, value: OptionValue) {
|
||||
this.set(key, value);
|
||||
|
||||
const payload: Record<string, OptionValue> = {};
|
||||
payload[key] = value;
|
||||
|
||||
await server.put(`options`, payload);
|
||||
}
|
||||
|
||||
async toggle(key: string) {
|
||||
await this.save(key, (!this.is(key)).toString());
|
||||
}
|
||||
}
|
||||
|
||||
const options = new Options();
|
||||
|
||||
export default options;
|
@ -1,16 +1,28 @@
|
||||
function parse(value) {
|
||||
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "url";
|
||||
type Multiplicity = "single" | "multi";
|
||||
|
||||
interface DefinitionObject {
|
||||
isPromoted?: boolean;
|
||||
labelType?: LabelType;
|
||||
multiplicity?: Multiplicity;
|
||||
numberPrecision?: number;
|
||||
promotedAlias?: string;
|
||||
inverseRelation?: string;
|
||||
}
|
||||
|
||||
function parse(value: string) {
|
||||
const tokens = value.split(',').map(t => t.trim());
|
||||
const defObj = {};
|
||||
const defObj: DefinitionObject = {};
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token === 'promoted') {
|
||||
defObj.isPromoted = true;
|
||||
}
|
||||
else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) {
|
||||
defObj.labelType = token;
|
||||
defObj.labelType = token as LabelType;
|
||||
}
|
||||
else if (['single', 'multi'].includes(token)) {
|
||||
defObj.multiplicity = token;
|
||||
defObj.multiplicity = token as Multiplicity;
|
||||
}
|
||||
else if (token.startsWith('precision')) {
|
||||
const chunks = token.split('=');
|
@ -1,3 +1,4 @@
|
||||
import FNote from "../entities/fnote.js";
|
||||
import server from "./server.js";
|
||||
|
||||
function enableProtectedSession() {
|
||||
@ -20,7 +21,7 @@ async function touchProtectedSession() {
|
||||
}
|
||||
}
|
||||
|
||||
function touchProtectedSessionIfNecessary(note) {
|
||||
function touchProtectedSessionIfNecessary(note: FNote) {
|
||||
if (note && note.isProtected && isProtectedSessionAvailable()) {
|
||||
touchProtectedSession();
|
||||
}
|
@ -1,13 +1,39 @@
|
||||
import utils from './utils.js';
|
||||
import ValidationError from "./validation_error.js";
|
||||
|
||||
async function getHeaders(headers) {
|
||||
type Headers = Record<string, string | null | undefined>;
|
||||
|
||||
type Method = string;
|
||||
|
||||
interface Response {
|
||||
headers: Headers;
|
||||
body: unknown;
|
||||
}
|
||||
|
||||
interface Arg extends Response {
|
||||
statusCode: number;
|
||||
method: Method;
|
||||
url: string;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
interface RequestData {
|
||||
resolve: (value: unknown) => any;
|
||||
reject: (reason: unknown) => any;
|
||||
silentNotFound: boolean;
|
||||
}
|
||||
|
||||
export interface StandardResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
async function getHeaders(headers?: Headers) {
|
||||
const appContext = (await import('../components/app_context.js')).default;
|
||||
const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
|
||||
|
||||
// headers need to be lowercase because node.js automatically converts them to lower case
|
||||
// also avoiding using underscores instead of dashes since nginx filters them out by default
|
||||
const allHeaders = {
|
||||
const allHeaders: Headers = {
|
||||
'trilium-component-id': glob.componentId,
|
||||
'trilium-local-now-datetime': utils.localNowDateTime(),
|
||||
'trilium-hoisted-note-id': activeNoteContext ? activeNoteContext.hoistedNoteId : null,
|
||||
@ -28,31 +54,31 @@ async function getHeaders(headers) {
|
||||
return allHeaders;
|
||||
}
|
||||
|
||||
async function getWithSilentNotFound(url, componentId) {
|
||||
return await call('GET', url, componentId, { silentNotFound: true });
|
||||
async function getWithSilentNotFound<T>(url: string, componentId?: string) {
|
||||
return await call<T>('GET', url, componentId, { silentNotFound: true });
|
||||
}
|
||||
|
||||
async function get(url, componentId) {
|
||||
return await call('GET', url, componentId);
|
||||
async function get<T>(url: string, componentId?: string) {
|
||||
return await call<T>('GET', url, componentId);
|
||||
}
|
||||
|
||||
async function post(url, data, componentId) {
|
||||
return await call('POST', url, componentId, { data });
|
||||
async function post<T>(url: string, data?: unknown, componentId?: string) {
|
||||
return await call<T>('POST', url, componentId, { data });
|
||||
}
|
||||
|
||||
async function put(url, data, componentId) {
|
||||
return await call('PUT', url, componentId, { data });
|
||||
async function put<T>(url: string, data?: unknown, componentId?: string) {
|
||||
return await call<T>('PUT', url, componentId, { data });
|
||||
}
|
||||
|
||||
async function patch(url, data, componentId) {
|
||||
return await call('PATCH', url, componentId, { data });
|
||||
async function patch<T>(url: string, data: unknown, componentId?: string) {
|
||||
return await call<T>('PATCH', url, componentId, { data });
|
||||
}
|
||||
|
||||
async function remove(url, componentId) {
|
||||
return await call('DELETE', url, componentId);
|
||||
async function remove<T>(url: string, componentId?: string) {
|
||||
return await call<T>('DELETE', url, componentId);
|
||||
}
|
||||
|
||||
async function upload(url, fileToUpload) {
|
||||
async function upload(url: string, fileToUpload: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('upload', fileToUpload);
|
||||
|
||||
@ -68,11 +94,17 @@ async function upload(url, fileToUpload) {
|
||||
}
|
||||
|
||||
let idCounter = 1;
|
||||
const idToRequestMap = {};
|
||||
|
||||
const idToRequestMap: Record<string, RequestData> = {};
|
||||
|
||||
let maxKnownEntityChangeId = 0;
|
||||
|
||||
async function call(method, url, componentId, options = {}) {
|
||||
interface CallOptions {
|
||||
data?: unknown;
|
||||
silentNotFound?: boolean;
|
||||
}
|
||||
|
||||
async function call<T>(method: string, url: string, componentId?: string, options: CallOptions = {}) {
|
||||
let resp;
|
||||
|
||||
const headers = await getHeaders({
|
||||
@ -98,7 +130,7 @@ async function call(method, url, componentId, options = {}) {
|
||||
url: `/${window.glob.baseApiUrl}${url}`,
|
||||
data: data
|
||||
});
|
||||
});
|
||||
}) as any;
|
||||
}
|
||||
else {
|
||||
resp = await ajax(url, method, data, headers, !!options.silentNotFound);
|
||||
@ -110,23 +142,25 @@ async function call(method, url, componentId, options = {}) {
|
||||
maxKnownEntityChangeId = Math.max(maxKnownEntityChangeId, parseInt(maxEntityChangeIdStr));
|
||||
}
|
||||
|
||||
return resp.body;
|
||||
return resp.body as T;
|
||||
}
|
||||
|
||||
function ajax(url, method, data, headers, silentNotFound) {
|
||||
function ajax(url: string, method: string, data: unknown, headers: Headers, silentNotFound: boolean): Promise<Response> {
|
||||
return new Promise((res, rej) => {
|
||||
const options = {
|
||||
const options: JQueryAjaxSettings = {
|
||||
url: window.glob.baseApiUrl + url,
|
||||
type: method,
|
||||
headers: headers,
|
||||
timeout: 60000,
|
||||
success: (body, textStatus, jqXhr) => {
|
||||
const respHeaders = {};
|
||||
const respHeaders: Headers = {};
|
||||
|
||||
jqXhr.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach(line => {
|
||||
const parts = line.split(': ');
|
||||
const header = parts.shift();
|
||||
respHeaders[header] = parts.join(': ');
|
||||
if (header) {
|
||||
respHeaders[header] = parts.join(': ');
|
||||
}
|
||||
});
|
||||
|
||||
res({
|
||||
@ -165,7 +199,7 @@ function ajax(url, method, data, headers, silentNotFound) {
|
||||
if (utils.isElectron()) {
|
||||
const ipc = utils.dynamicRequire('electron').ipcRenderer;
|
||||
|
||||
ipc.on('server-response', async (event, arg) => {
|
||||
ipc.on('server-response', async (event: string, arg: Arg) => {
|
||||
if (arg.statusCode >= 200 && arg.statusCode < 300) {
|
||||
handleSuccessfulResponse(arg);
|
||||
}
|
||||
@ -182,8 +216,8 @@ if (utils.isElectron()) {
|
||||
delete idToRequestMap[arg.requestId];
|
||||
});
|
||||
|
||||
function handleSuccessfulResponse(arg) {
|
||||
if (arg.headers['Content-Type'] === 'application/json') {
|
||||
function handleSuccessfulResponse(arg: Arg) {
|
||||
if (arg.headers['Content-Type'] === 'application/json' && typeof arg.body === "string") {
|
||||
arg.body = JSON.parse(arg.body);
|
||||
}
|
||||
|
||||
@ -199,21 +233,23 @@ if (utils.isElectron()) {
|
||||
}
|
||||
}
|
||||
|
||||
async function reportError(method, url, statusCode, response) {
|
||||
async function reportError(method: string, url: string, statusCode: number, response: unknown) {
|
||||
let message = response;
|
||||
|
||||
if (typeof response === 'string') {
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
message = response.message;
|
||||
message = (response as any).message;
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
|
||||
const toastService = (await import("./toast.js")).default;
|
||||
|
||||
const messageStr = (typeof message === "string" ? message : JSON.stringify(message));
|
||||
|
||||
if ([400, 404].includes(statusCode) && response && typeof response === 'object') {
|
||||
toastService.showError(message);
|
||||
toastService.showError(messageStr);
|
||||
throw new ValidationError({
|
||||
requestUrl: url,
|
||||
method,
|
||||
@ -222,7 +258,7 @@ async function reportError(method, url, statusCode, response) {
|
||||
});
|
||||
} else {
|
||||
const title = `${statusCode} ${method} ${url}`;
|
||||
toastService.showErrorTitleAndMessage(title, message);
|
||||
toastService.showErrorTitleAndMessage(title, messageStr);
|
||||
toastService.throwError(`${title} - ${message}`);
|
||||
}
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
type Callback = () => Promise<void>;
|
||||
|
||||
export default class SpacedUpdate {
|
||||
constructor(updater, updateInterval = 1000) {
|
||||
private updater: Callback;
|
||||
private lastUpdated: number;
|
||||
private changed: boolean;
|
||||
private updateInterval: number;
|
||||
private changeForbidden?: boolean;
|
||||
|
||||
constructor(updater: Callback, updateInterval = 1000) {
|
||||
this.updater = updater;
|
||||
this.lastUpdated = Date.now();
|
||||
this.changed = false;
|
||||
@ -52,7 +60,7 @@ export default class SpacedUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
async allowUpdateWithoutChange(callback) {
|
||||
async allowUpdateWithoutChange(callback: Callback) {
|
||||
this.changeForbidden = true;
|
||||
|
||||
try {
|
@ -1,7 +1,17 @@
|
||||
import ws from "./ws.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
function toast(options) {
|
||||
interface ToastOptions {
|
||||
id?: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
message: string;
|
||||
delay?: number;
|
||||
autohide?: boolean;
|
||||
closeAfter?: number;
|
||||
}
|
||||
|
||||
function toast(options: ToastOptions) {
|
||||
const $toast = $(
|
||||
`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
@ -36,7 +46,7 @@ function toast(options) {
|
||||
return $toast;
|
||||
}
|
||||
|
||||
function showPersistent(options) {
|
||||
function showPersistent(options: ToastOptions) {
|
||||
let $toast = $(`#toast-${options.id}`);
|
||||
|
||||
if ($toast.length > 0) {
|
||||
@ -53,11 +63,11 @@ function showPersistent(options) {
|
||||
}
|
||||
}
|
||||
|
||||
function closePersistent(id) {
|
||||
function closePersistent(id: string) {
|
||||
$(`#toast-${id}`).remove();
|
||||
}
|
||||
|
||||
function showMessage(message, delay = 2000) {
|
||||
function showMessage(message: string, delay = 2000) {
|
||||
console.debug(utils.now(), "message:", message);
|
||||
|
||||
toast({
|
||||
@ -69,13 +79,13 @@ function showMessage(message, delay = 2000) {
|
||||
});
|
||||
}
|
||||
|
||||
function showAndLogError(message, delay = 10000) {
|
||||
function showAndLogError(message: string, delay = 10000) {
|
||||
showError(message, delay);
|
||||
|
||||
ws.logError(message);
|
||||
}
|
||||
|
||||
function showError(message, delay = 10000) {
|
||||
function showError(message: string, delay = 10000) {
|
||||
console.log(utils.now(), "error: ", message);
|
||||
|
||||
toast({
|
||||
@ -87,7 +97,7 @@ function showError(message, delay = 10000) {
|
||||
});
|
||||
}
|
||||
|
||||
function showErrorTitleAndMessage(title, message, delay = 10000) {
|
||||
function showErrorTitleAndMessage(title: string, message: string, delay = 10000) {
|
||||
console.log(utils.now(), "error: ", message);
|
||||
|
||||
toast({
|
||||
@ -99,7 +109,7 @@ function showErrorTitleAndMessage(title, message, delay = 10000) {
|
||||
});
|
||||
}
|
||||
|
||||
function throwError(message) {
|
||||
function throwError(message: string) {
|
||||
ws.logError(message);
|
||||
|
||||
throw new Error(message);
|
@ -4,10 +4,18 @@ import froca from './froca.js';
|
||||
import hoistedNoteService from '../services/hoisted_note.js';
|
||||
import appContext from "../components/app_context.js";
|
||||
|
||||
export interface Node {
|
||||
getParent(): Node;
|
||||
data: {
|
||||
noteId?: string;
|
||||
isProtected?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string|null}
|
||||
*/
|
||||
async function resolveNotePath(notePath, hoistedNoteId = 'root') {
|
||||
async function resolveNotePath(notePath: string, hoistedNoteId = 'root') {
|
||||
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||
|
||||
return runPath ? runPath.join("/") : null;
|
||||
@ -17,10 +25,8 @@ async function resolveNotePath(notePath, hoistedNoteId = 'root') {
|
||||
* Accepts notePath which might or might not be valid and returns an existing path as close to the original
|
||||
* notePath as possible. Part of the path might not be valid because of note moving (which causes
|
||||
* path change) or other corruption, in that case, this will try to get some other valid path to the correct note.
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logErrors = true) {
|
||||
async function resolveNotePathToSegments(notePath: string, hoistedNoteId = 'root', logErrors = true) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
// we might get notePath with the params suffix, remove it if present
|
||||
@ -103,8 +109,14 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
|
||||
return effectivePathSegments;
|
||||
}
|
||||
else {
|
||||
const note = await froca.getNote(getNoteIdFromUrl(notePath));
|
||||
|
||||
const noteId = getNoteIdFromUrl(notePath);
|
||||
if (!noteId) {
|
||||
throw new Error(`Unable to find note with ID: ${noteId}.`);
|
||||
}
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
throw new Error(`Unable to find note: ${notePath}.`);
|
||||
}
|
||||
const bestNotePath = note.getBestNotePath(hoistedNoteId);
|
||||
|
||||
if (!bestNotePath) {
|
||||
@ -128,11 +140,11 @@ ws.subscribeToMessages(message => {
|
||||
}
|
||||
});
|
||||
|
||||
function getParentProtectedStatus(node) {
|
||||
function getParentProtectedStatus(node: Node) {
|
||||
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
|
||||
}
|
||||
|
||||
function getNoteIdFromUrl(urlOrNotePath) {
|
||||
function getNoteIdFromUrl(urlOrNotePath: string) {
|
||||
if (!urlOrNotePath) {
|
||||
return null;
|
||||
}
|
||||
@ -143,13 +155,16 @@ function getNoteIdFromUrl(urlOrNotePath) {
|
||||
return segments[segments.length - 1];
|
||||
}
|
||||
|
||||
async function getBranchIdFromUrl(urlOrNotePath) {
|
||||
async function getBranchIdFromUrl(urlOrNotePath: string) {
|
||||
const {noteId, parentNoteId} = getNoteIdAndParentIdFromUrl(urlOrNotePath);
|
||||
if (!parentNoteId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await froca.getBranchId(parentNoteId, noteId);
|
||||
}
|
||||
|
||||
function getNoteIdAndParentIdFromUrl(urlOrNotePath) {
|
||||
function getNoteIdAndParentIdFromUrl(urlOrNotePath: string) {
|
||||
if (!urlOrNotePath) {
|
||||
return {};
|
||||
}
|
||||
@ -182,7 +197,7 @@ function getNoteIdAndParentIdFromUrl(urlOrNotePath) {
|
||||
};
|
||||
}
|
||||
|
||||
function getNotePath(node) {
|
||||
function getNotePath(node: Node) {
|
||||
if (!node) {
|
||||
logError("Node is null");
|
||||
return "";
|
||||
@ -201,7 +216,7 @@ function getNotePath(node) {
|
||||
return path.reverse().join("/");
|
||||
}
|
||||
|
||||
async function getNoteTitle(noteId, parentNoteId = null) {
|
||||
async function getNoteTitle(noteId: string, parentNoteId: string | null = null) {
|
||||
utils.assertArguments(noteId);
|
||||
|
||||
const note = await froca.getNote(noteId);
|
||||
@ -226,7 +241,7 @@ async function getNoteTitle(noteId, parentNoteId = null) {
|
||||
return title;
|
||||
}
|
||||
|
||||
async function getNotePathTitleComponents(notePath) {
|
||||
async function getNotePathTitleComponents(notePath: string) {
|
||||
const titleComponents = [];
|
||||
|
||||
if (notePath.startsWith('root/')) {
|
||||
@ -249,7 +264,7 @@ async function getNotePathTitleComponents(notePath) {
|
||||
return titleComponents;
|
||||
}
|
||||
|
||||
async function getNotePathTitle(notePath) {
|
||||
async function getNotePathTitle(notePath: string) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
const titlePath = await getNotePathTitleComponents(notePath);
|
||||
@ -257,7 +272,7 @@ async function getNotePathTitle(notePath) {
|
||||
return titlePath.join(' / ');
|
||||
}
|
||||
|
||||
async function getNoteTitleWithPathAsSuffix(notePath) {
|
||||
async function getNoteTitleWithPathAsSuffix(notePath: string) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
const titleComponents = await getNotePathTitleComponents(notePath);
|
||||
@ -278,7 +293,7 @@ async function getNoteTitleWithPathAsSuffix(notePath) {
|
||||
return $titleWithPath;
|
||||
}
|
||||
|
||||
function formatNotePath(path) {
|
||||
function formatNotePath(path: string[]) {
|
||||
const $notePath = $('<span class="note-path">');
|
||||
|
||||
if (path.length > 0) {
|
||||
@ -299,7 +314,7 @@ function formatNotePath(path) {
|
||||
return $notePath;
|
||||
}
|
||||
|
||||
function isNotePathInHiddenSubtree(notePath) {
|
||||
function isNotePathInHiddenSubtree(notePath: string) {
|
||||
return notePath?.includes("root/_hidden");
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
function reloadFrontendApp(reason) {
|
||||
import dayjs from "dayjs";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
function reloadFrontendApp(reason?: string) {
|
||||
if (reason) {
|
||||
logInfo(`Frontend app reload: ${reason}`);
|
||||
}
|
||||
@ -6,33 +9,33 @@ function reloadFrontendApp(reason) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function parseDate(str) {
|
||||
function parseDate(str: string) {
|
||||
try {
|
||||
return new Date(Date.parse(str));
|
||||
}
|
||||
catch (e) {
|
||||
catch (e: any) {
|
||||
throw new Error(`Can't parse date from '${str}': ${e.message} ${e.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
function padNum(num) {
|
||||
function padNum(num: number) {
|
||||
return `${num <= 9 ? "0" : ""}${num}`;
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
function formatTime(date: Date) {
|
||||
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
function formatTimeWithSeconds(date) {
|
||||
function formatTimeWithSeconds(date: Date) {
|
||||
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}:${padNum(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
function formatTimeInterval(ms) {
|
||||
function formatTimeInterval(ms: number) {
|
||||
const seconds = Math.round(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
const plural = (count, name) => `${count} ${name}${count > 1 ? 's' : ''}`;
|
||||
const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? 's' : ''}`;
|
||||
const segments = [];
|
||||
|
||||
if (days > 0) {
|
||||
@ -60,20 +63,20 @@ function formatTimeInterval(ms) {
|
||||
return segments.join(", ");
|
||||
}
|
||||
|
||||
// this is producing local time!
|
||||
function formatDate(date) {
|
||||
/** this is producing local time! **/
|
||||
function formatDate(date: Date) {
|
||||
// return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
|
||||
// instead of european format we'll just use ISO as that's pretty unambiguous
|
||||
|
||||
return formatDateISO(date);
|
||||
}
|
||||
|
||||
// this is producing local time!
|
||||
function formatDateISO(date) {
|
||||
/** this is producing local time! **/
|
||||
function formatDateISO(date: Date) {
|
||||
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
|
||||
}
|
||||
|
||||
function formatDateTime(date) {
|
||||
function formatDateTime(date: Date) {
|
||||
return `${formatDate(date)} ${formatTime(date)}`;
|
||||
}
|
||||
|
||||
@ -96,20 +99,20 @@ function isMac() {
|
||||
return navigator.platform.indexOf('Mac') > -1;
|
||||
}
|
||||
|
||||
function isCtrlKey(evt) {
|
||||
function isCtrlKey(evt: KeyboardEvent) {
|
||||
return (!isMac() && evt.ctrlKey)
|
||||
|| (isMac() && evt.metaKey);
|
||||
}
|
||||
|
||||
function assertArguments() {
|
||||
for (const i in arguments) {
|
||||
if (!arguments[i]) {
|
||||
console.trace(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
|
||||
function assertArguments(...args: string[]) {
|
||||
for (const i in args) {
|
||||
if (!args[i]) {
|
||||
console.trace(`Argument idx#${i} should not be falsy: ${args[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entityMap = {
|
||||
const entityMap: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
@ -120,11 +123,11 @@ const entityMap = {
|
||||
'=': '='
|
||||
};
|
||||
|
||||
function escapeHtml(str) {
|
||||
function escapeHtml(str: string) {
|
||||
return str.replace(/[&<>"'`=\/]/g, s => entityMap[s]);
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
function formatSize(size: number) {
|
||||
size = Math.max(Math.round(size / 1024), 1);
|
||||
|
||||
if (size < 1024) {
|
||||
@ -135,8 +138,8 @@ function formatSize(size) {
|
||||
}
|
||||
}
|
||||
|
||||
function toObject(array, fn) {
|
||||
const obj = {};
|
||||
function toObject<T>(array: T[], fn: (arg0: T) => [key: string, value: T]) {
|
||||
const obj: Record<string, T> = {};
|
||||
|
||||
for (const item of array) {
|
||||
const [key, value] = fn(item);
|
||||
@ -147,7 +150,7 @@ function toObject(array, fn) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
function randomString(len) {
|
||||
function randomString(len: number) {
|
||||
let text = "";
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
@ -170,21 +173,22 @@ function isDesktop() {
|
||||
|| (!window.glob?.device && !/Mobi/.test(navigator.userAgent));
|
||||
}
|
||||
|
||||
// the cookie code below works for simple use cases only - ASCII only
|
||||
// not setting a path so that cookies do not leak into other websites if multiplexed with reverse proxy
|
||||
|
||||
function setCookie(name, value) {
|
||||
/**
|
||||
* the cookie code below works for simple use cases only - ASCII only
|
||||
* not setting a path so that cookies do not leak into other websites if multiplexed with reverse proxy
|
||||
*/
|
||||
function setCookie(name: string, value: string) {
|
||||
const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000);
|
||||
const expires = `; expires=${date.toUTCString()}`;
|
||||
|
||||
document.cookie = `${name}=${value || ""}${expires};`;
|
||||
}
|
||||
|
||||
function getNoteTypeClass(type) {
|
||||
function getNoteTypeClass(type: string) {
|
||||
return `type-${type}`;
|
||||
}
|
||||
|
||||
function getMimeTypeClass(mime) {
|
||||
function getMimeTypeClass(mime: string) {
|
||||
if (!mime) {
|
||||
return "";
|
||||
}
|
||||
@ -201,12 +205,12 @@ function getMimeTypeClass(mime) {
|
||||
|
||||
function closeActiveDialog() {
|
||||
if (glob.activeDialog) {
|
||||
bootstrap.Modal.getOrCreateInstance(glob.activeDialog).hide();
|
||||
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
|
||||
glob.activeDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
let $lastFocusedElement = null;
|
||||
let $lastFocusedElement: JQuery<HTMLElement> | null;
|
||||
|
||||
// perhaps there should be saved focused element per tab?
|
||||
function saveFocusedElement() {
|
||||
@ -238,14 +242,14 @@ function focusSavedElement() {
|
||||
$lastFocusedElement = null;
|
||||
}
|
||||
|
||||
async function openDialog($dialog, closeActDialog = true) {
|
||||
async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
|
||||
if (closeActDialog) {
|
||||
closeActiveDialog();
|
||||
glob.activeDialog = $dialog;
|
||||
}
|
||||
|
||||
saveFocusedElement();
|
||||
bootstrap.Modal.getOrCreateInstance($dialog).show();
|
||||
Modal.getOrCreateInstance($dialog[0]).show();
|
||||
|
||||
$dialog.on('hidden.bs.modal', () => {
|
||||
$(".aa-input").autocomplete("close");
|
||||
@ -255,11 +259,13 @@ async function openDialog($dialog, closeActDialog = true) {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Fix once keyboard_actions is ported.
|
||||
// @ts-ignore
|
||||
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
|
||||
keyboardActionsService.updateDisplayedShortcuts($dialog);
|
||||
}
|
||||
|
||||
function isHtmlEmpty(html) {
|
||||
function isHtmlEmpty(html: string) {
|
||||
if (!html) {
|
||||
return true;
|
||||
} else if (typeof html !== 'string') {
|
||||
@ -283,13 +289,13 @@ async function clearBrowserCache() {
|
||||
}
|
||||
|
||||
function copySelectionToClipboard() {
|
||||
const text = window.getSelection().toString();
|
||||
if (navigator.clipboard) {
|
||||
const text = window?.getSelection()?.toString();
|
||||
if (text && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
}
|
||||
|
||||
function dynamicRequire(moduleName) {
|
||||
function dynamicRequire(moduleName: string) {
|
||||
if (typeof __non_webpack_require__ !== 'undefined') {
|
||||
return __non_webpack_require__(moduleName);
|
||||
}
|
||||
@ -298,7 +304,7 @@ function dynamicRequire(moduleName) {
|
||||
}
|
||||
}
|
||||
|
||||
function timeLimit(promise, limitMs, errorMessage) {
|
||||
function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string) {
|
||||
if (!promise || !promise.then) { // it's not actually a promise
|
||||
return promise;
|
||||
}
|
||||
@ -306,7 +312,7 @@ function timeLimit(promise, limitMs, errorMessage) {
|
||||
// better stack trace if created outside of promise
|
||||
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
return new Promise<T>((res, rej) => {
|
||||
let resolved = false;
|
||||
|
||||
promise.then(result => {
|
||||
@ -323,7 +329,7 @@ function timeLimit(promise, limitMs, errorMessage) {
|
||||
});
|
||||
}
|
||||
|
||||
function initHelpDropdown($el) {
|
||||
function initHelpDropdown($el: JQuery<HTMLElement>) {
|
||||
// stop inside clicks from closing the menu
|
||||
const $dropdownMenu = $el.find('.help-dropdown .dropdown-menu');
|
||||
$dropdownMenu.on('click', e => e.stopPropagation());
|
||||
@ -334,7 +340,7 @@ function initHelpDropdown($el) {
|
||||
|
||||
const wikiBaseUrl = "https://triliumnext.github.io/Docs/Wiki/";
|
||||
|
||||
function openHelp($button) {
|
||||
function openHelp($button: JQuery<HTMLElement>) {
|
||||
const helpPage = $button.attr("data-help-page");
|
||||
|
||||
if (helpPage) {
|
||||
@ -344,7 +350,7 @@ function openHelp($button) {
|
||||
}
|
||||
}
|
||||
|
||||
function initHelpButtons($el) {
|
||||
function initHelpButtons($el: JQuery<HTMLElement>) {
|
||||
// for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button)
|
||||
// so we do it manually
|
||||
$el.on("click", e => {
|
||||
@ -353,35 +359,38 @@ function initHelpButtons($el) {
|
||||
});
|
||||
}
|
||||
|
||||
function filterAttributeName(name) {
|
||||
function filterAttributeName(name: string) {
|
||||
return name.replace(/[^\p{L}\p{N}_:]/ug, "");
|
||||
}
|
||||
|
||||
const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
|
||||
|
||||
function isValidAttributeName(name) {
|
||||
function isValidAttributeName(name: string) {
|
||||
return ATTR_NAME_MATCHER.test(name);
|
||||
}
|
||||
|
||||
function sleep(time_ms) {
|
||||
function sleep(time_ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, time_ms);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeRegExp(str) {
|
||||
function escapeRegExp(str: string) {
|
||||
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
||||
}
|
||||
|
||||
function areObjectsEqual() {
|
||||
let i, l, leftChain, rightChain;
|
||||
function areObjectsEqual () {
|
||||
let i;
|
||||
let l;
|
||||
let leftChain: Object[];
|
||||
let rightChain: Object[];
|
||||
|
||||
function compare2Objects(x, y) {
|
||||
function compare2Objects (x: unknown, y: unknown) {
|
||||
let p;
|
||||
|
||||
// remember that NaN === NaN returns false
|
||||
// and isNaN(undefined) returns true
|
||||
if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
|
||||
if (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -416,7 +425,7 @@ function areObjectsEqual() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x.prototype !== y.prototype) {
|
||||
if ((x as any).prototype !== (y as any).prototype) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -431,7 +440,7 @@ function areObjectsEqual() {
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
else if (typeof y[p] !== typeof x[p]) {
|
||||
else if (typeof (y as any)[p] !== typeof (x as any)[p]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -440,18 +449,18 @@ function areObjectsEqual() {
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
else if (typeof y[p] !== typeof x[p]) {
|
||||
else if (typeof (y as any)[p] !== typeof (x as any)[p]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (typeof (x[p])) {
|
||||
switch (typeof ((x as any)[p])) {
|
||||
case 'object':
|
||||
case 'function':
|
||||
|
||||
leftChain.push(x);
|
||||
rightChain.push(y);
|
||||
|
||||
if (!compare2Objects(x[p], y[p])) {
|
||||
if (!compare2Objects((x as any)[p], (y as any)[p])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -460,7 +469,7 @@ function areObjectsEqual() {
|
||||
break;
|
||||
|
||||
default:
|
||||
if (x[p] !== y[p]) {
|
||||
if ((x as any)[p] !== (y as any)[p]) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@ -488,10 +497,12 @@ function areObjectsEqual() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function copyHtmlToClipboard(content) {
|
||||
function listener(e) {
|
||||
e.clipboardData.setData("text/html", content);
|
||||
e.clipboardData.setData("text/plain", content);
|
||||
function copyHtmlToClipboard(content: string) {
|
||||
function listener(e: ClipboardEvent) {
|
||||
if (e.clipboardData) {
|
||||
e.clipboardData.setData("text/html", content);
|
||||
e.clipboardData.setData("text/plain", content);
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
document.addEventListener("copy", listener);
|
||||
@ -499,21 +510,18 @@ function copyHtmlToClipboard(content) {
|
||||
document.removeEventListener("copy", listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FNote} note
|
||||
* @return {string}
|
||||
*/
|
||||
function createImageSrcUrl(note) {
|
||||
// TODO: Set to FNote once the file is ported.
|
||||
function createImageSrcUrl(note: { noteId: string; title: string }) {
|
||||
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string representation of an SVG, triggers a download of the file on the client device.
|
||||
*
|
||||
* @param {string} nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
|
||||
* @param {string} svgContent the content of the SVG file download.
|
||||
* @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
|
||||
* @param svgContent the content of the SVG file download.
|
||||
*/
|
||||
function downloadSvg(nameWithoutExtension, svgContent) {
|
||||
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
||||
const filename = `${nameWithoutExtension}.svg`;
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
|
||||
@ -534,11 +542,11 @@ function downloadSvg(nameWithoutExtension, svgContent) {
|
||||
* 0 if v1 is equal to v2
|
||||
* -1 if v1 is less than v2
|
||||
*
|
||||
* @param {string} v1 First version string
|
||||
* @param {string} v2 Second version string
|
||||
* @returns {number}
|
||||
* @param v1 First version string
|
||||
* @param v2 Second version string
|
||||
* @returns
|
||||
*/
|
||||
function compareVersions(v1, v2) {
|
||||
function compareVersions(v1: string, v2: string): number {
|
||||
|
||||
// Remove 'v' prefix and everything after dash if present
|
||||
v1 = v1.replace(/^v/, '').split('-')[0];
|
||||
@ -571,11 +579,8 @@ function compareVersions(v1, v2) {
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
|
||||
* @param {string} latestVersion
|
||||
* @param {string} currentVersion
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isUpdateAvailable(latestVersion, currentVersion) {
|
||||
function isUpdateAvailable(latestVersion: string, currentVersion: string): boolean {
|
||||
return compareVersions(latestVersion, currentVersion) > 0;
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
export default class ValidationError {
|
||||
constructor(resp) {
|
||||
for (const key in resp) {
|
||||
this[key] = resp[key];
|
||||
}
|
||||
}
|
||||
}
|
7
src/public/app/services/validation_error.ts
Normal file
7
src/public/app/services/validation_error.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default class ValidationError {
|
||||
constructor(resp: Record<string, string | number>) {
|
||||
for (const key in resp) {
|
||||
(this as any)[key] = resp[key];
|
||||
}
|
||||
}
|
||||
}
|
@ -4,17 +4,20 @@ import server from "./server.js";
|
||||
import options from "./options.js";
|
||||
import frocaUpdater from "./froca_updater.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { EntityChange } from '../../../services/entity_changes_interface.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
const messageHandlers = [];
|
||||
type MessageHandler = (message: any) => void;
|
||||
const messageHandlers: MessageHandler[] = [];
|
||||
|
||||
let ws;
|
||||
let ws: WebSocket;
|
||||
let lastAcceptedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
|
||||
let lastAcceptedEntityChangeSyncId = window.glob.maxEntityChangeSyncIdAtLoad;
|
||||
let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
|
||||
let lastPingTs;
|
||||
let frontendUpdateDataQueue = [];
|
||||
let lastPingTs: number;
|
||||
let frontendUpdateDataQueue: EntityChange[] = [];
|
||||
|
||||
function logError(message) {
|
||||
function logError(message: string) {
|
||||
console.error(utils.now(), message); // needs to be separate from .trace()
|
||||
|
||||
if (ws && ws.readyState === 1) {
|
||||
@ -26,7 +29,7 @@ function logError(message) {
|
||||
}
|
||||
}
|
||||
|
||||
function logInfo(message) {
|
||||
function logInfo(message: string) {
|
||||
console.log(utils.now(), message);
|
||||
|
||||
if (ws && ws.readyState === 1) {
|
||||
@ -40,17 +43,17 @@ function logInfo(message) {
|
||||
window.logError = logError;
|
||||
window.logInfo = logInfo;
|
||||
|
||||
function subscribeToMessages(messageHandler) {
|
||||
function subscribeToMessages(messageHandler: MessageHandler) {
|
||||
messageHandlers.push(messageHandler);
|
||||
}
|
||||
|
||||
// used to serialize frontend update operations
|
||||
let consumeQueuePromise = null;
|
||||
let consumeQueuePromise: Promise<void> | null = null;
|
||||
|
||||
// to make sure each change event is processed only once. Not clear if this is still necessary
|
||||
const processedEntityChangeIds = new Set();
|
||||
|
||||
function logRows(entityChanges) {
|
||||
function logRows(entityChanges: EntityChange[]) {
|
||||
const filteredRows = entityChanges.filter(row =>
|
||||
!processedEntityChangeIds.has(row.id)
|
||||
&& (row.entityName !== 'options' || row.entityId !== 'openNoteContexts'));
|
||||
@ -60,7 +63,7 @@ function logRows(entityChanges) {
|
||||
}
|
||||
}
|
||||
|
||||
async function executeFrontendUpdate(entityChanges) {
|
||||
async function executeFrontendUpdate(entityChanges: EntityChange[]) {
|
||||
lastPingTs = Date.now();
|
||||
|
||||
if (entityChanges.length > 0) {
|
||||
@ -71,6 +74,10 @@ async function executeFrontendUpdate(entityChanges) {
|
||||
// we set lastAcceptedEntityChangeId even before frontend update processing and send ping so that backend can start sending more updates
|
||||
|
||||
for (const entityChange of entityChanges) {
|
||||
if (!entityChange.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lastAcceptedEntityChangeId = Math.max(lastAcceptedEntityChangeId, entityChange.id);
|
||||
|
||||
if (entityChange.isSynced) {
|
||||
@ -97,7 +104,7 @@ async function executeFrontendUpdate(entityChanges) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessage(event) {
|
||||
async function handleMessage(event: MessageEvent<any>) {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
for (const messageHandler of messageHandlers) {
|
||||
@ -126,24 +133,32 @@ async function handleMessage(event) {
|
||||
toastService.showMessage(message.message);
|
||||
}
|
||||
else if (message.type === 'execute-script') {
|
||||
const bundleService = (await import("../services/bundle.js")).default;
|
||||
const froca = (await import("../services/froca.js")).default;
|
||||
// TODO: Remove after porting the file
|
||||
// @ts-ignore
|
||||
const bundleService = (await import("../services/bundle.js")).default as any;
|
||||
// TODO: Remove after porting the file
|
||||
// @ts-ignore
|
||||
const froca = (await import("../services/froca.js")).default as any;
|
||||
const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null;
|
||||
|
||||
bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params);
|
||||
}
|
||||
}
|
||||
|
||||
let entityChangeIdReachedListeners = [];
|
||||
let entityChangeIdReachedListeners: {
|
||||
desiredEntityChangeId: number;
|
||||
resolvePromise: () => void;
|
||||
start: number;
|
||||
}[] = [];
|
||||
|
||||
function waitForEntityChangeId(desiredEntityChangeId) {
|
||||
function waitForEntityChangeId(desiredEntityChangeId: number) {
|
||||
if (desiredEntityChangeId <= lastProcessedEntityChangeId) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
console.debug(`Waiting for ${desiredEntityChangeId}, last processed is ${lastProcessedEntityChangeId}, last accepted ${lastAcceptedEntityChangeId}`);
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
return new Promise<void>((res, rej) => {
|
||||
entityChangeIdReachedListeners.push({
|
||||
desiredEntityChangeId: desiredEntityChangeId,
|
||||
resolvePromise: res,
|
||||
@ -178,7 +193,7 @@ async function consumeFrontendUpdateData() {
|
||||
try {
|
||||
await utils.timeLimit(frocaUpdater.processEntityChanges(nonProcessedEntityChanges), 30000);
|
||||
}
|
||||
catch (e) {
|
||||
catch (e: any) {
|
||||
logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`);
|
||||
|
||||
if (!glob.isDev && !options.is('debugModeEnabled')) {
|
||||
@ -196,7 +211,9 @@ async function consumeFrontendUpdateData() {
|
||||
for (const entityChange of nonProcessedEntityChanges) {
|
||||
processedEntityChangeIds.add(entityChange.id);
|
||||
|
||||
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, entityChange.id);
|
||||
if (entityChange.id) {
|
||||
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, entityChange.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,3 +265,4 @@ export default {
|
||||
waitForMaxKnownEntityChangeId,
|
||||
getMaxKnownEntityChangeSyncId: () => lastAcceptedEntityChangeSyncId
|
||||
};
|
||||
|
55
src/public/app/types.d.ts
vendored
Normal file
55
src/public/app/types.d.ts
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
import FNote from "./entities/fnote";
|
||||
|
||||
interface ElectronProcess {
|
||||
type: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
interface CustomGlobals {
|
||||
isDesktop: boolean;
|
||||
isMobile: boolean;
|
||||
device: "mobile" | "desktop";
|
||||
getComponentsByEl: (el: unknown) => unknown;
|
||||
getHeaders: Promise<Record<string, string>>;
|
||||
getReferenceLinkTitle: (href: string) => Promise<string>;
|
||||
getReferenceLinkTitleSync: (href: string) => string;
|
||||
getActiveContextNote: FNote;
|
||||
requireLibrary: (library: string) => Promise<void>;
|
||||
ESLINT: { js: string[]; };
|
||||
appContext: AppContext;
|
||||
froca: Froca;
|
||||
treeCache: Froca;
|
||||
importMarkdownInline: () => Promise<unknown>;
|
||||
SEARCH_HELP_TEXT: string;
|
||||
activeDialog: JQuery<HTMLElement> | null;
|
||||
componentId: string;
|
||||
csrfToken: string;
|
||||
baseApiUrl: string;
|
||||
isProtectedSessionAvailable: boolean;
|
||||
isDev: boolean;
|
||||
isMainWindow: boolean;
|
||||
maxEntityChangeIdAtLoad: number;
|
||||
maxEntityChangeSyncIdAtLoad: number;
|
||||
}
|
||||
|
||||
type RequireMethod = (moduleName: string) => any;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
logError(message: string);
|
||||
logInfo(message: string);
|
||||
|
||||
process?: ElectronProcess;
|
||||
glob?: CustomGlobals;
|
||||
}
|
||||
|
||||
interface JQuery {
|
||||
autocomplete: (action: "close") => void;
|
||||
}
|
||||
|
||||
declare var logError: (message: string) => void;
|
||||
declare var logInfo: (message: string) => void;
|
||||
declare var glob: CustomGlobals;
|
||||
declare var require: RequireMethod;
|
||||
declare var __non_webpack_require__: RequireMethod | undefined;
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
export default class Mutex {
|
||||
private current: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
this.current = Promise.resolve();
|
||||
}
|
||||
|
||||
/** @returns {Promise} */
|
||||
lock() {
|
||||
let resolveFun;
|
||||
const subPromise = new Promise(resolve => resolveFun = () => resolve());
|
||||
let resolveFun: () => void;
|
||||
const subPromise = new Promise<void>(resolve => resolveFun = () => resolve());
|
||||
// Caller gets a promise that resolves when the current outstanding lock resolves
|
||||
const newPromise = this.current.then(() => resolveFun);
|
||||
// Don't allow the next request until the new promise is done
|
||||
@ -15,7 +16,7 @@ export default class Mutex {
|
||||
return newPromise;
|
||||
};
|
||||
|
||||
async runExclusively(cb) {
|
||||
async runExclusively(cb: () => Promise<void>) {
|
||||
const unlock = await this.lock();
|
||||
|
||||
try {
|
@ -1,6 +1,20 @@
|
||||
// taken from the HTML source of https://boxicons.com/
|
||||
|
||||
const categories = [
|
||||
interface Category {
|
||||
name: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Icon {
|
||||
name: string;
|
||||
slug: string;
|
||||
category_id: number;
|
||||
type_of_icon: "REGULAR" | "SOLID" | "LOGO";
|
||||
term?: string[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const categories: Category[] = [
|
||||
{"name": "All categories", "id": 0},
|
||||
{
|
||||
"name": "Accessibility",
|
||||
@ -132,7 +146,7 @@ const categories = [
|
||||
}
|
||||
];
|
||||
|
||||
const icons = [
|
||||
const icons: Icon[] = [
|
||||
{
|
||||
"name": "child",
|
||||
"slug": "child-regular",
|
||||
@ -11175,7 +11189,7 @@ const icons = [
|
||||
}
|
||||
];
|
||||
|
||||
function getIconClass(icon) {
|
||||
function getIconClass(icon: Icon) {
|
||||
if (icon.type_of_icon === 'LOGO') {
|
||||
return `bxl-${icon.name}`;
|
||||
}
|
@ -20,16 +20,15 @@ async function register(app: express.Application) {
|
||||
if (env.isDev()) {
|
||||
const webpack = (await import("webpack")).default;
|
||||
const webpackMiddleware = (await import("webpack-dev-middleware")).default;
|
||||
const productionConfig = (await import("../../webpack.config.js")).default;
|
||||
|
||||
const frontendCompiler = webpack({
|
||||
mode: "development",
|
||||
entry: {
|
||||
setup: './src/public/app/setup.js',
|
||||
mobile: './src/public/app/mobile.js',
|
||||
desktop: './src/public/app/desktop.js',
|
||||
},
|
||||
devtool: 'source-map',
|
||||
target: 'electron-renderer'
|
||||
entry: productionConfig.entry,
|
||||
module: productionConfig.module,
|
||||
resolve: productionConfig.resolve,
|
||||
devtool: productionConfig.devtool,
|
||||
target: productionConfig.target
|
||||
});
|
||||
|
||||
app.use(`/${assetPath}/app`, webpackMiddleware(frontendCompiler));
|
||||
|
@ -1,25 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "./build",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["ES2022"],
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
"module": "NodeNext",
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "./build",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": [
|
||||
"ES2022"
|
||||
],
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.js",
|
||||
"./src/**/*.ts",
|
||||
"./*.ts",
|
||||
"./spec/**/*.ts",
|
||||
"./spec-es6/**/*.ts"
|
||||
"./src/**/*.js",
|
||||
"./src/**/*.ts",
|
||||
"./*.ts",
|
||||
"./spec/**/*.ts",
|
||||
"./spec-es6/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"./src/public/**/*",
|
||||
"./node_modules/**/*"
|
||||
],
|
||||
"exclude": ["./node_modules/**/*"],
|
||||
"files": [
|
||||
"src/types.d.ts"
|
||||
]
|
||||
|
24
tsconfig.webpack.json
Normal file
24
tsconfig.webpack.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "./build",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": [
|
||||
"ES2022"
|
||||
],
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
"./src/public/app/**/*",
|
||||
],
|
||||
"files": [
|
||||
"./src/public/app/types.d.ts"
|
||||
]
|
||||
}
|
@ -15,6 +15,28 @@ export default {
|
||||
path: path.resolve(rootDir, 'src/public/app-dist'),
|
||||
filename: '[name].js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: [{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: path.join(rootDir, "tsconfig.webpack.json")
|
||||
}
|
||||
}],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
extensionAlias: {
|
||||
".js": [".js", ".ts"],
|
||||
".cjs": [".cjs", ".cts"],
|
||||
".mjs": [".mjs", ".mts"]
|
||||
}
|
||||
},
|
||||
devtool: 'source-map',
|
||||
target: 'electron-renderer',
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user