From 380f4a1d54bae501e479c2a1fc248bd5a247cf64 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 24 Jul 2024 23:23:36 +0300 Subject: [PATCH 01/39] client-ts: Adapt progress script --- _check_ts_progress.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_check_ts_progress.sh b/_check_ts_progress.sh index c7b66ce23..3ff5f17e4 100755 --- a/_check_ts_progress.sh +++ b/_check_ts_progress.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash +cd src/public cloc HEAD \ --git --md \ --include-lang=javascript,typescript \ - --found=filelist.txt \ - --exclude-dir=public,libraries,views,docs + --found=filelist.txt grep -R \.js$ filelist.txt rm filelist.txt \ No newline at end of file From 3fbedfb0a165074a740dc06b42477712de22a9ee Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 24 Jul 2024 23:30:10 +0300 Subject: [PATCH 02/39] client-ts: Port services/validation_error --- src/public/app/services/validation_error.js | 7 ------- src/public/app/services/validation_error.ts | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 src/public/app/services/validation_error.js create mode 100644 src/public/app/services/validation_error.ts diff --git a/src/public/app/services/validation_error.js b/src/public/app/services/validation_error.js deleted file mode 100644 index 6d3423e6c..000000000 --- a/src/public/app/services/validation_error.js +++ /dev/null @@ -1,7 +0,0 @@ -export default class ValidationError { - constructor(resp) { - for (const key in resp) { - this[key] = resp[key]; - } - } -} \ No newline at end of file diff --git a/src/public/app/services/validation_error.ts b/src/public/app/services/validation_error.ts new file mode 100644 index 000000000..a37841a89 --- /dev/null +++ b/src/public/app/services/validation_error.ts @@ -0,0 +1,7 @@ +export default class ValidationError { + constructor(resp: Record) { + for (const key in resp) { + (this as any)[key] = resp[key]; + } + } +} \ No newline at end of file From 81327a09d594c986a13365828f6501c92a78463a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 24 Jul 2024 23:57:43 +0300 Subject: [PATCH 03/39] client-ts: Port services/promoted_attribute_definition_parser --- ...> promoted_attribute_definition_parser.ts} | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) rename src/public/app/services/{promoted_attribute_definition_parser.js => promoted_attribute_definition_parser.ts} (65%) diff --git a/src/public/app/services/promoted_attribute_definition_parser.js b/src/public/app/services/promoted_attribute_definition_parser.ts similarity index 65% rename from src/public/app/services/promoted_attribute_definition_parser.js rename to src/public/app/services/promoted_attribute_definition_parser.ts index 81d097c42..3cd71ef2f 100644 --- a/src/public/app/services/promoted_attribute_definition_parser.js +++ b/src/public/app/services/promoted_attribute_definition_parser.ts @@ -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', '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('='); From 6c94cbf3883fc34699a9339f2c837de0b682a2a6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 00:01:39 +0300 Subject: [PATCH 04/39] client-ts: Port services/css_class_manager --- package-lock.json | 16 ++++++++++++++++ package.json | 1 + ...css_class_manager.js => css_class_manager.ts} | 4 ++-- 3 files changed, 19 insertions(+), 2 deletions(-) rename src/public/app/services/{css_class_manager.js => css_class_manager.ts} (87%) diff --git a/package-lock.json b/package-lock.json index a6818fc89..7d0e67515 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,6 +106,7 @@ "@types/html": "^1.0.4", "@types/ini": "^4.1.0", "@types/jasmine": "^5.1.4", + "@types/jquery": "^3.5.30", "@types/jsdom": "^21.1.6", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", @@ -2364,6 +2365,15 @@ "integrity": "sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==", "dev": true }, + "node_modules/@types/jquery": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.30.tgz", + "integrity": "sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==", + "dev": true, + "dependencies": { + "@types/sizzle": "*" + } + }, "node_modules/@types/jsdom": { "version": "21.1.7", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", @@ -2656,6 +2666,12 @@ "@types/express-session": "*" } }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, "node_modules/@types/stream-throttle": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@types/stream-throttle/-/stream-throttle-0.1.4.tgz", diff --git a/package.json b/package.json index 1aa887245..cdf387bd3 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "@types/html": "^1.0.4", "@types/ini": "^4.1.0", "@types/jasmine": "^5.1.4", + "@types/jquery": "^3.5.30", "@types/jsdom": "^21.1.6", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", diff --git a/src/public/app/services/css_class_manager.js b/src/public/app/services/css_class_manager.ts similarity index 87% rename from src/public/app/services/css_class_manager.js rename to src/public/app/services/css_class_manager.ts index 01a513289..a458e995a 100644 --- a/src/public/app/services/css_class_manager.js +++ b/src/public/app/services/css_class_manager.ts @@ -1,6 +1,6 @@ -const registeredClasses = new Set(); +const registeredClasses = new Set(); -function createClassForColor(color) { +function createClassForColor(color: string) { if (!color?.trim()) { return ""; } From 679e9eba777d7687f83fe8e5a2fc5d36a5a24183 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 00:09:34 +0300 Subject: [PATCH 05/39] client-ts: Port services/load_results --- .../{load_results.js => load_results.ts} | 69 +++++++++++++++---- 1 file changed, 55 insertions(+), 14 deletions(-) rename src/public/app/services/{load_results.js => load_results.ts} (68%) diff --git a/src/public/app/services/load_results.js b/src/public/app/services/load_results.ts similarity index 68% rename from src/public/app/services/load_results.js rename to src/public/app/services/load_results.ts index 22f6bca94..5dfa8b34f 100644 --- a/src/public/app/services/load_results.js +++ b/src/public/app/services/load_results.ts @@ -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; +} + +interface ContentNoteIdToComponentIdRow { + noteId: string; + componentId: string; +} + +interface AttachmentRow {} + +interface ContentNoteIdToComponentIdRow { + noteId: string; + componentId: string; +} + export default class LoadResults { - constructor(entityChanges) { + private entities: Record>; + private noteIdToComponentId: Record; + private componentIdToNoteIds: Record; + 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,11 +68,11 @@ 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) { this.noteIdToComponentId[noteId] = this.noteIdToComponentId[noteId] || []; if (!this.noteIdToComponentId[noteId].includes(componentId)) { @@ -45,7 +86,7 @@ export default class LoadResults { } } - addBranch(branchId, componentId) { + addBranch(branchId: string, componentId: string) { this.branchRows.push({branchId, componentId}); } @@ -55,7 +96,7 @@ export default class LoadResults { .filter(branch => !!branch); } - addNoteReordering(parentNoteId, componentId) { + addNoteReordering(parentNoteId: string, componentId: string) { this.noteReorderings.push(parentNoteId); } @@ -63,7 +104,7 @@ export default class LoadResults { return this.noteReorderings; } - addAttribute(attributeId, componentId) { + addAttribute(attributeId: string, componentId: string) { this.attributeRows.push({attributeId, componentId}); } @@ -74,11 +115,11 @@ export default class LoadResults { .filter(attr => !!attr); } - addRevision(revisionId, noteId, componentId) { + addRevision(revisionId: string, noteId: string, componentId: string) { this.revisionRows.push({revisionId, noteId, componentId}); } - hasRevisionForNote(noteId) { + hasRevisionForNote(noteId: string) { return !!this.revisionRows.find(row => row.noteId === noteId); } @@ -86,7 +127,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 +136,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 +148,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 +160,7 @@ export default class LoadResults { return this.optionNames; } - addAttachmentRow(attachment) { + addAttachmentRow(attachment: AttachmentRow) { this.attachmentRows.push(attachment); } From bece0aa78492ff89878e4867309e5c3538ff2e8b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 00:12:24 +0300 Subject: [PATCH 06/39] client-ts: Port services/mutex --- src/public/app/utils/{mutex.js => mutex.ts} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename src/public/app/utils/{mutex.js => mutex.ts} (73%) diff --git a/src/public/app/utils/mutex.js b/src/public/app/utils/mutex.ts similarity index 73% rename from src/public/app/utils/mutex.js rename to src/public/app/utils/mutex.ts index 483e0f345..c37e78003 100644 --- a/src/public/app/utils/mutex.js +++ b/src/public/app/utils/mutex.ts @@ -1,12 +1,13 @@ export default class Mutex { + private current: Promise; + constructor() { this.current = Promise.resolve(); } - /** @returns {Promise} */ lock() { - let resolveFun; - const subPromise = new Promise(resolve => resolveFun = () => resolve()); + let resolveFun: () => void; + const subPromise = new Promise(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) { const unlock = await this.lock(); try { From 0c8092b8f4b37f82d546dc75250b3376c1b5a997 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 00:13:53 +0300 Subject: [PATCH 07/39] client-ts: Port services/entities/fblob --- src/public/app/entities/fblob.js | 39 -------------------------- src/public/app/entities/fblob.ts | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 39 deletions(-) delete mode 100644 src/public/app/entities/fblob.js create mode 100644 src/public/app/entities/fblob.ts diff --git a/src/public/app/entities/fblob.js b/src/public/app/entities/fblob.js deleted file mode 100644 index e335d7cb8..000000000 --- a/src/public/app/entities/fblob.js +++ /dev/null @@ -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; - } - } -} diff --git a/src/public/app/entities/fblob.ts b/src/public/app/entities/fblob.ts new file mode 100644 index 000000000..d260f7da6 --- /dev/null +++ b/src/public/app/entities/fblob.ts @@ -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; + } + } +} From ba7035a346991178b75f89270096fbb0f9b18c09 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 00:18:57 +0300 Subject: [PATCH 08/39] client-ts: Port services/utils --- package-lock.json | 20 +++ package.json | 1 + .../app/services/{utils.js => utils.ts} | 127 +++++++++--------- src/public/app/types.d.ts | 46 +++++++ tsconfig.json | 3 +- 5 files changed, 136 insertions(+), 61 deletions(-) rename src/public/app/services/{utils.js => utils.ts} (80%) create mode 100644 src/public/app/types.d.ts diff --git a/package-lock.json b/package-lock.json index 7d0e67515..451c22720 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^6.4.2", "@types/archiver": "^6.0.2", "@types/better-sqlite3": "^7.6.9", + "@types/bootstrap": "^5.2.10", "@types/cls-hooked": "^4.3.8", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.7", @@ -2060,6 +2061,16 @@ "node": ">=14" } }, + "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, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2153,6 +2164,15 @@ "@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, + "dependencies": { + "@popperjs/core": "^2.9.2" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", diff --git a/package.json b/package.json index cdf387bd3..b6a619c0d 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^6.4.2", "@types/archiver": "^6.0.2", "@types/better-sqlite3": "^7.6.9", + "@types/bootstrap": "^5.2.10", "@types/cls-hooked": "^4.3.8", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.7", diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.ts similarity index 80% rename from src/public/app/services/utils.js rename to src/public/app/services/utils.ts index 9e17f1c38..ef58b4a32 100644 --- a/src/public/app/services/utils.js +++ b/src/public/app/services/utils.ts @@ -1,4 +1,6 @@ -function reloadFrontendApp(reason) { +import dayjs from "dayjs"; + +function reloadFrontendApp(reason: string) { if (reason) { logInfo(`Frontend app reload: ${reason}`); } @@ -6,33 +8,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 +62,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)}`; } @@ -93,7 +95,7 @@ function isMac() { return navigator.platform.indexOf('Mac') > -1; } -function isCtrlKey(evt) { +function isCtrlKey(evt: KeyboardEvent) { return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); } @@ -106,7 +108,7 @@ function assertArguments() { } } -const entityMap = { +const entityMap: Record = { '&': '&', '<': '<', '>': '>', @@ -117,11 +119,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) { @@ -132,8 +134,8 @@ function formatSize(size) { } } -function toObject(array, fn) { - const obj = {}; +function toObject(array: T[], fn: (arg0: T) => [key: string, value: T]) { + const obj: Record = {}; for (const item of array) { const [key, value] = fn(item); @@ -144,7 +146,7 @@ function toObject(array, fn) { return obj; } -function randomString(len) { +function randomString(len: number) { let text = ""; const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@ -161,27 +163,28 @@ function isMobile() { || (!window.glob?.device && /Mobi/.test(navigator.userAgent)); } -function isDesktop() { +function isDesktop() { return window.glob?.device === "desktop" // window.glob.device is not available in setup || (!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 ""; } @@ -203,7 +206,7 @@ function closeActiveDialog() { } } -let $lastFocusedElement = null; +let $lastFocusedElement: JQuery | null; // perhaps there should be saved focused element per tab? function saveFocusedElement() { @@ -235,7 +238,7 @@ function focusSavedElement() { $lastFocusedElement = null; } -async function openDialog($dialog, closeActDialog = true) { +async function openDialog($dialog: JQuery, closeActDialog = true) { if (closeActDialog) { closeActiveDialog(); glob.activeDialog = $dialog; @@ -253,11 +256,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') { @@ -281,13 +286,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); } @@ -296,7 +301,7 @@ function dynamicRequire(moduleName) { } } -function timeLimit(promise, limitMs, errorMessage) { +function timeLimit(promise: Promise, limitMs: number, errorMessage: string) { if (!promise || !promise.then) { // it's not actually a promise return promise; } @@ -321,7 +326,7 @@ function timeLimit(promise, limitMs, errorMessage) { }); } -function initHelpDropdown($el) { +function initHelpDropdown($el: JQuery) { // stop inside clicks from closing the menu const $dropdownMenu = $el.find('.help-dropdown .dropdown-menu'); $dropdownMenu.on('click', e => e.stopPropagation()); @@ -332,7 +337,7 @@ function initHelpDropdown($el) { const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"; -function openHelp($button) { +function openHelp($button: JQuery) { const helpPage = $button.attr("data-help-page"); if (helpPage) { @@ -342,7 +347,7 @@ function openHelp($button) { } } -function initHelpButtons($el) { +function initHelpButtons($el: JQuery) { // 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 => { @@ -351,35 +356,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; } @@ -414,7 +422,7 @@ function areObjectsEqual() { return false; } - if (x.prototype !== y.prototype) { + if ((x as any).prototype !== (y as any).prototype) { return false; } @@ -429,7 +437,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; } } @@ -438,18 +446,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; } @@ -458,7 +466,7 @@ function areObjectsEqual() { break; default: - if (x[p] !== y[p]) { + if ((x as any)[p] !== (y as any)[p]) { return false; } break; @@ -486,10 +494,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); @@ -497,11 +507,8 @@ 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()}`; } diff --git a/src/public/app/types.d.ts b/src/public/app/types.d.ts new file mode 100644 index 000000000..daf31e929 --- /dev/null +++ b/src/public/app/types.d.ts @@ -0,0 +1,46 @@ +import FNote from "./entities/fnote"; + +interface ElectronProcess { + type: string; +} + +interface CustomGlobals { + isDesktop: boolean; + isMobile: boolean; + device: "mobile" | "desktop"; + getComponentsByEl: (el: unknown) => unknown; + getHeaders: Promise>; + getReferenceLinkTitle: (href: string) => Promise; + getReferenceLinkTitleSync: (href: string) => string; + getActiveContextNote: FNote; + requireLibrary: (library: string) => Promise; + ESLINT: { js: string[]; }; + appContext: AppContext; + froca: Froca; + treeCache: Froca; + importMarkdownInline: () => Promise; + SEARCH_HELP_TEXT: string; + activeDialog: JQuery | null; +} + +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; +} diff --git a/tsconfig.json b/tsconfig.json index 0d206e511..1bdd21ef5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ ], "exclude": ["./node_modules/**/*"], "files": [ - "src/types.d.ts" + "src/types.d.ts", + "src/public/app/types.d.ts" ] } From 5875aa3bef6e9aff61f1756737a767b027049222 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 00:23:29 +0300 Subject: [PATCH 09/39] client-ts: Port services/server --- .../app/services/{server.js => server.ts} | 86 +++++++++++++------ src/public/app/types.d.ts | 3 + tsconfig.json | 3 +- 3 files changed, 63 insertions(+), 29 deletions(-) rename src/public/app/services/{server.js => server.ts} (71%) diff --git a/src/public/app/services/server.js b/src/public/app/services/server.ts similarity index 71% rename from src/public/app/services/server.js rename to src/public/app/services/server.ts index b8a1d29fb..c24bb86f7 100644 --- a/src/public/app/services/server.js +++ b/src/public/app/services/server.ts @@ -1,13 +1,35 @@ import utils from './utils.js'; import ValidationError from "./validation_error.js"; -async function getHeaders(headers) { +type Headers = Record; + +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; +} + +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 +50,31 @@ async function getHeaders(headers) { return allHeaders; } -async function getWithSilentNotFound(url, componentId) { - return await call('GET', url, componentId, { silentNotFound: true }); +async function getWithSilentNotFound(url: string, componentId?: string) { + return await call('GET', url, componentId, { silentNotFound: true }); } -async function get(url, componentId) { - return await call('GET', url, componentId); +async function get(url: string, componentId?: string) { + return await call('GET', url, componentId); } -async function post(url, data, componentId) { - return await call('POST', url, componentId, { data }); +async function post(url: string, data: unknown, componentId?: string) { + return await call('POST', url, componentId, { data }); } -async function put(url, data, componentId) { - return await call('PUT', url, componentId, { data }); +async function put(url: string, data: unknown, componentId?: string) { + return await call('PUT', url, componentId, { data }); } -async function patch(url, data, componentId) { - return await call('PATCH', url, componentId, { data }); +async function patch(url: string, data: unknown, componentId?: string) { + return await call('PATCH', url, componentId, { data }); } -async function remove(url, componentId) { - return await call('DELETE', url, componentId); +async function remove(url: string, componentId?: string) { + return await call('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 +90,17 @@ async function upload(url, fileToUpload) { } let idCounter = 1; -const idToRequestMap = {}; + +const idToRequestMap: Record = {}; let maxKnownEntityChangeId = 0; -async function call(method, url, componentId, options = {}) { +interface CallOptions { + data?: unknown; + silentNotFound?: boolean; +} + +async function call(method: string, url: string, componentId?: string, options: CallOptions = {}) { let resp; const headers = await getHeaders({ @@ -98,7 +126,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 +138,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 { 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({ @@ -161,7 +191,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); } @@ -178,8 +208,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); } @@ -195,13 +225,13 @@ 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) {} } diff --git a/src/public/app/types.d.ts b/src/public/app/types.d.ts index daf31e929..9934211e9 100644 --- a/src/public/app/types.d.ts +++ b/src/public/app/types.d.ts @@ -21,6 +21,9 @@ interface CustomGlobals { importMarkdownInline: () => Promise; SEARCH_HELP_TEXT: string; activeDialog: JQuery | null; + componentId: string; + csrfToken: string; + baseApiUrl: string; } type RequireMethod = (moduleName: string) => any; diff --git a/tsconfig.json b/tsconfig.json index 1bdd21ef5..ac8d233d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "lib": ["ES2022"], "downlevelIteration": true, "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "allowJs": true }, "include": [ "./src/**/*.js", From 78f929ee69ab3278b520c0db8ef49faebc00a8ff Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 00:25:11 +0300 Subject: [PATCH 10/39] client-ts: Port services/options --- src/public/app/services/options.js | 61 ----------------------- src/public/app/services/options.ts | 79 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 61 deletions(-) delete mode 100644 src/public/app/services/options.js create mode 100644 src/public/app/services/options.ts diff --git a/src/public/app/services/options.js b/src/public/app/services/options.js deleted file mode 100644 index 954832eb6..000000000 --- a/src/public/app/services/options.js +++ /dev/null @@ -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; diff --git a/src/public/app/services/options.ts b/src/public/app/services/options.ts new file mode 100644 index 000000000..464011b9a --- /dev/null +++ b/src/public/app/services/options.ts @@ -0,0 +1,79 @@ + +import server from "./server.js"; + +type OptionValue = string; + +class Options { + private initializedPromise: Promise; + private arr!: Record; + + constructor() { + this.initializedPromise = server.get>('options').then(data => this.load(data)); + } + + load(arr: Record) { + 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 = {}; + 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; \ No newline at end of file From cf57819b2241c52f2a75416b76b62526e9498f62 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 00:26:27 +0300 Subject: [PATCH 11/39] client-ts: Port services/toast --- .../app/services/{toast.js => toast.ts} | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) rename src/public/app/services/{toast.js => toast.ts} (78%) diff --git a/src/public/app/services/toast.js b/src/public/app/services/toast.ts similarity index 78% rename from src/public/app/services/toast.js rename to src/public/app/services/toast.ts index 09c17ce7f..13deb3bfc 100644 --- a/src/public/app/services/toast.js +++ b/src/public/app/services/toast.ts @@ -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 = $(`