Merge remote-tracking branch 'origin/develop' into feat_erasure-timeout-ui

; Conflicts:
;	src/public/translations/ro/translation.json
This commit is contained in:
Elian Doran 2025-02-13 22:21:38 +02:00
commit 27b825e511
No known key found for this signature in database
71 changed files with 1231 additions and 323 deletions

View File

@ -18,5 +18,6 @@
"github-actions.workflows.pinned.workflows": [".github/workflows/nightly.yml"],
"[css]": {
"editor.defaultFormatter": "vscode.css-language-features"
}
},
"npm.exclude": ["**/build", "**/dist", "**/out/**"]
}

View File

@ -29,7 +29,7 @@ const copy = async () => {
fs.copySync(path.join("build", srcFile), destFile, { recursive: true });
}
const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json"];
const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json", "./src/etapi/etapi.openapi.yaml"];
for (const file of filesToCopy) {
log(`Copying ${file}`);
await fs.copy(file, path.join(DEST_DIR, file));

51
bin/generate-openapi.js Normal file
View File

@ -0,0 +1,51 @@
import swaggerJsdoc from 'swagger-jsdoc';
/*
* Usage: npm run generate-openapi | tail -n1 > x.json
*
* Inspect generated file by opening it in https://editor-next.swagger.io/
*
*/
const options = {
definition: {
openapi: '3.1.1',
info: {
title: 'Trilium Notes - Sync server API',
version: '0.96.6',
description: "This is the internal sync server API used by Trilium Notes / TriliumNext Notes.\n\n_If you're looking for the officially supported External Trilium API, see [here](https://triliumnext.github.io/Docs/Wiki/etapi.html)._\n\nThis page does not yet list all routes. For a full list, see the [route controller](https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/routes.ts).",
contact: {
name: "TriliumNext issue tracker",
url: "https://github.com/TriliumNext/Notes/issues",
},
license: {
name: "GNU Free Documentation License 1.3 (or later)",
url: "https://www.gnu.org/licenses/fdl-1.3",
},
},
},
apis: ['./src/routes/api/*.ts', './bin/generate-openapi.js'],
};
const openapiSpecification = swaggerJsdoc(options);
console.log(JSON.stringify(openapiSpecification));
/**
* @swagger
* components:
* schemas:
* UtcDateTime:
* type: string
* example: "2025-02-13T07:42:47.698Z"
* securitySchemes:
* user-password:
* type: apiKey
* name: trilium-cred
* in: header
* description: "Username and password, formatted as `user:password`"
* session:
* type: apiKey
* in: cookie
* name: trilium.sid
*/

View File

@ -28,6 +28,21 @@ keyPath=
# expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
trustedReverseProxy=false
[Session]
# Use this setting to set a custom value for the "Path" Attribute value of the session cookie.
# This can be useful, when you have several instances running on the same domain, under different paths (e.g. by using a reverse proxy).
# It prevents your instances from overwriting each others' cookies, allowing you to stay logged in multiple instances simultanteously.
# E.g. if you have instances running under https://your-domain.com/triliumNext/instanceA and https://your-domain.com/triliumNext/instanceB
# you would want to set the cookiePath value to "/triliumNext/instanceA" for your first and "/triliumNext/instanceB" for your second instance
cookiePath=/
# Use this setting to set a custom value for the "Max-Age" Attribute of the session cookie.
# This controls how long your session will be valid, before it expires and you need to log in again, when you use the "Remember Me" option.
# Value needs to be entered in Seconds.
# Default value is 1814400 Seconds, which is 21 Days.
cookieMaxAge=1814400
[Sync]
#syncServerHost=
#syncServerTimeout=

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -198,6 +198,7 @@
ext: [ "hcl " ],
mime: "text/x-hcl",
mode: "hcl",
name: "Terraform (HCL)"
});
});

303
package-lock.json generated
View File

@ -17,8 +17,10 @@
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.4",
"@triliumnext/express-partial-content": "1.0.1",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.16",
"@types/react-dom": "18.3.5",
"@types/swagger-ui-express": "4.1.7",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@ -30,7 +32,7 @@
"chokidar": "4.0.3",
"cls-hooked": "4.2.2",
"codemirror": "5.65.18",
"compression": "1.7.5",
"compression": "1.8.0",
"cookie-parser": "1.4.7",
"csrf-csrf": "3.1.0",
"dayjs": "1.11.13",
@ -64,6 +66,7 @@
"jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
"jquery.fancytree": "2.38.4",
"js-yaml": "4.1.0",
"jsdom": "26.0.0",
"jsplumb": "2.15.6",
"katex": "0.16.21",
@ -92,6 +95,7 @@
"split.js": "1.6.5",
"stream-throttle": "0.1.3",
"striptags": "3.2.0",
"swagger-ui-express": "5.0.1",
"tmp": "0.2.3",
"ts-loader": "9.5.2",
"turndown": "7.2.0",
@ -161,6 +165,7 @@
"prettier": "3.5.0",
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"swagger-jsdoc": "6.2.8",
"tslib": "2.8.1",
"tsx": "4.19.2",
"typedoc": "0.27.7",
@ -218,6 +223,54 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"dev": true,
"license": "MIT"
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^5.0.1"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.2.tgz",
@ -2711,6 +2764,13 @@
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true,
"license": "MIT"
},
"node_modules/@jsdoc/salty": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz",
@ -3436,6 +3496,12 @@
"win32"
]
},
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "1.24.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.2.tgz",
@ -3555,7 +3621,6 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@ -3618,7 +3683,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@ -3945,7 +4009,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
"integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@ -3958,7 +4021,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz",
"integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@ -4033,7 +4095,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ini": {
@ -4060,6 +4121,11 @@
"@types/sizzle": "*"
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="
},
"node_modules/@types/jsdom": {
"version": "21.1.7",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
@ -4145,7 +4211,6 @@
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime-types": {
@ -4192,14 +4257,12 @@
"version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
@ -4271,7 +4334,6 @@
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@ -4292,7 +4354,6 @@
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@ -4338,6 +4399,15 @@
"@types/node": "*"
}
},
"node_modules/@types/swagger-ui-express": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz",
"integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==",
"dependencies": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/tmp": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
@ -5172,7 +5242,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-flatten": {
@ -6008,6 +6077,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001689",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
@ -6534,9 +6610,9 @@
}
},
"node_modules/compression": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz",
"integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
@ -7786,6 +7862,19 @@
"p-limit": "^3.1.0 "
}
},
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@ -8979,6 +9068,16 @@
"@types/estree": "^1.0.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -11461,6 +11560,17 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/js2xmlparser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
@ -11938,6 +12048,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"dev": true,
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@ -13077,6 +13202,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/ora": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
@ -16064,6 +16197,104 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "^10.0.3",
"yaml": "2.0.0-1"
},
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/swagger-jsdoc/node_modules/commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/swagger-jsdoc/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/swagger-jsdoc/node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz",
"integrity": "sha512-G33HFW0iFNStfY2x6QXO2JYVMrFruc8AZRX0U/L71aA7WeWfX2E5Nm8E/tsipSZJeIZZbSjUDeynLK/wcuNWIw==",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
},
"node_modules/swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -17141,6 +17372,16 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
@ -18583,6 +18824,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/z-schema": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.7.0"
},
"bin": {
"z-schema": "bin/z-schema"
},
"engines": {
"node": ">=8.0.0"
},
"optionalDependencies": {
"commander": "^9.4.1"
}
},
"node_modules/z-schema/node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/zip-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",

View File

@ -53,6 +53,7 @@
"integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
"generate-openapi": "node bin/generate-openapi.js",
"ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts",
"prettier-check": "prettier . --check",
"prettier-fix": "prettier . --write"
@ -66,8 +67,10 @@
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.4",
"@triliumnext/express-partial-content": "1.0.1",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.16",
"@types/react-dom": "18.3.5",
"@types/swagger-ui-express": "4.1.7",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@ -79,7 +82,7 @@
"chokidar": "4.0.3",
"cls-hooked": "4.2.2",
"codemirror": "5.65.18",
"compression": "1.7.5",
"compression": "1.8.0",
"cookie-parser": "1.4.7",
"csrf-csrf": "3.1.0",
"dayjs": "1.11.13",
@ -113,6 +116,7 @@
"jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
"jquery.fancytree": "2.38.4",
"js-yaml": "4.1.0",
"jsdom": "26.0.0",
"jsplumb": "2.15.6",
"katex": "0.16.21",
@ -141,6 +145,7 @@
"split.js": "1.6.5",
"stream-throttle": "0.1.3",
"striptags": "3.2.0",
"swagger-ui-express": "5.0.1",
"tmp": "0.2.3",
"ts-loader": "9.5.2",
"turndown": "7.2.0",
@ -207,6 +212,7 @@
"prettier": "3.5.0",
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"swagger-jsdoc": "6.2.8",
"tslib": "2.8.1",
"tsx": "4.19.2",
"typedoc": "0.27.7",

View File

@ -80,6 +80,7 @@ export type CommandMappings = {
};
closeTocCommand: CommandData;
showLaunchBarSubtree: CommandData;
showRevisions: CommandData;
showOptions: CommandData & {
section: string;
};
@ -112,6 +113,8 @@ export type CommandMappings = {
openNoteInNewWindow: CommandData;
hideLeftPane: CommandData;
showLeftPane: CommandData;
leaveProtectedSession: CommandData;
enterProtectedSession: CommandData;
openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData;
@ -210,6 +213,12 @@ export type CommandMappings = {
reEvaluateRightPaneVisibility: CommandData;
runActiveNote: CommandData;
scrollContainerToCommand: CommandData & {
position: number;
};
moveThisNoteSplit: CommandData & {
isMovingLeft: boolean;
};
// Geomap
deleteFromMap: { noteId: string },
@ -291,6 +300,7 @@ type EventMappings = {
noteContextReorderEvent: {
oldMainNtxId: string;
newMainNtxId: string;
ntxIdsInOrder: string[];
};
newNoteContextCreated: {
noteContext: NoteContext;
@ -299,7 +309,7 @@ type EventMappings = {
ntxIds: string[];
};
exportSvg: {
ntxId: string;
ntxId: string | null | undefined;
};
geoMapCreateChildNote: {
ntxId: string | null | undefined; // TODO: deduplicate ntxId

View File

@ -30,6 +30,7 @@ import HelpDialog from "../widgets/dialogs/help.js";
import type AppContext from "../components/app_context.js";
import TabRowWidget from "../widgets/tab_row.js";
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
const MOBILE_CSS = `
<style>
@ -187,6 +188,7 @@ export default class MobileLayout {
.child(new ClassicEditorToolbar())
.child(new AboutDialog())
.child(new HelpDialog())
.child(new RecentChangesDialog())
.child(new JumpToNoteDialog());
}
}

View File

@ -32,7 +32,7 @@ const CODE_MIRROR: Library = {
const mimeTypes = mimeTypesService.getMimeTypes();
for (const mimeType of mimeTypes) {
if (mimeType.codeMirrorSource) {
if (mimeType.enabled && mimeType.codeMirrorSource) {
scriptsToLoad.push(mimeType.codeMirrorSource);
}
}

View File

@ -4,6 +4,16 @@ import appContext, { type NoteCommandData } from "../components/app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
// Be consistent with `allowedSchemes` in `src\services\html_sanitizer.ts`
// TODO: Deduplicate with server once we can.
export const ALLOWED_PROTOCOLS = [
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo',
'mid'
];
function getNotePathFromUrl(url: string) {
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
@ -296,58 +306,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
electron.shell.openPath(hrefLink);
} else {
// Enable protocols supported by CKEditor 5 to be clickable.
// Refer to `allowedProtocols` in https://github.com/TriliumNext/trilium-ckeditor5/blob/main/packages/ckeditor5-build-balloon-block/src/ckeditor.ts.
// And be consistent with `allowedSchemes` in `src\services\html_sanitizer.ts`
const allowedSchemes = [
"http",
"https",
"ftp",
"ftps",
"mailto",
"data",
"evernote",
"file",
"facetime",
"gemini",
"git",
"gopher",
"imap",
"irc",
"irc6",
"jabber",
"jar",
"lastfm",
"ldap",
"ldaps",
"magnet",
"message",
"mumble",
"nfs",
"onenote",
"pop",
"rmi",
"s3",
"sftp",
"skype",
"sms",
"spotify",
"steam",
"svn",
"udp",
"view-source",
"vlc",
"vnc",
"ws",
"wss",
"xmpp",
"jdbc",
"slack",
"tel",
"smb",
"zotero",
"geo"
];
if (allowedSchemes.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
window.open(hrefLink, "_blank");
}
}

View File

@ -2,6 +2,9 @@ import { MIME_TYPE_AUTO, MIME_TYPES_DICT, normalizeMimeTypeForCKEditor, type Mim
import options from "./options.js";
interface MimeType extends MimeTypeDefinition {
/**
* True if this mime type was enabled by the user in the "Available MIME types in the dropdown" option in the Code Notes settings.
*/
enabled: boolean;
}

View File

@ -151,11 +151,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
$el.addClass("note-autocomplete-input");
const $clearTextButton = $("<button>").addClass("input-group-text input-clearer-button bx bxs-tag-x").prop("title", t("note_autocomplete.clear-text-field"));
const $clearTextButton = $("<a>").addClass("input-group-text input-clearer-button bx bxs-tag-x").prop("title", t("note_autocomplete.clear-text-field"));
const $showRecentNotesButton = $("<button>").addClass("input-group-text show-recent-notes-button bx bx-time").prop("title", t("note_autocomplete.show-recent-notes"));
const $showRecentNotesButton = $("<a>").addClass("input-group-text show-recent-notes-button bx bx-time").prop("title", t("note_autocomplete.show-recent-notes"));
const $fullTextSearchButton = $("<button>")
const $fullTextSearchButton = $("<a>")
.addClass("input-group-text full-text-search-button bx bx-search")
.prop("title", `${t("note_autocomplete.full-text-search")} (Shift+Enter)`);

View File

@ -15,6 +15,7 @@ import type { CommandData, EventData, EventListener, FilteredCommandNames } from
import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
import type FNote from "../../entities/fnote.js";
import { escapeQuotes } from "../../services/utils.js";
import { buildConfig } from "../type_widgets/ckeditor/toolbars.js";
const HELP_TEXT = `
<p>${t("attribute_editor.help_text_body1")}</p>
@ -130,6 +131,7 @@ const mentionSetup: MentionConfig = {
};
const editorConfig = {
...buildConfig(),
removePlugins: [
"Heading",
"Link",

View File

@ -2,13 +2,23 @@ import SwitchWidget from "./switch.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
// TODO: Deduplicate
type Response = {
success: true;
} | {
success: false;
message: string;
}
export default class BookmarkSwitchWidget extends SwitchWidget {
isEnabled() {
return (
super.isEnabled() &&
// it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle
!["root", "_hidden"].includes(this.noteId)
!["root", "_hidden"].includes(this.noteId ?? "")
);
}
@ -22,21 +32,21 @@ export default class BookmarkSwitchWidget extends SwitchWidget {
this.switchOffTooltip = t("bookmark_switch.remove_bookmark");
}
async toggle(state) {
const resp = await server.put(`notes/${this.noteId}/toggle-in-parent/_lbBookmarks/${!!state}`);
async toggle(state: boolean | null | undefined) {
const resp = await server.put<Response>(`notes/${this.noteId}/toggle-in-parent/_lbBookmarks/${!!state}`);
if (!resp.success) {
toastService.showError(resp.message);
}
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
const isBookmarked = !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
this.isToggled = isBookmarked;
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getBranchRows().find((b) => b.noteId === this.noteId)) {
this.refresh();
}

View File

@ -19,7 +19,7 @@ export default class AbstractButtonWidget<SettingsT extends AbstractButtonWidget
protected settings!: SettingsT;
protected tooltip!: bootstrap.Tooltip;
isEnabled() {
isEnabled(): boolean | null | undefined {
return true;
}

View File

@ -1,15 +1,19 @@
import froca from "../../services/froca.js";
import attributeService from "../../services/attributes.js";
import CommandButtonWidget from "./command_button.js";
import type { EventData } from "../../components/app_context.js";
export type ButtonNoteIdProvider = () => string;
export default class ButtonFromNoteWidget extends CommandButtonWidget {
constructor() {
super();
this.settings.buttonNoteIdProvider = null;
}
buttonNoteIdProvider(provider) {
buttonNoteIdProvider(provider: ButtonNoteIdProvider) {
this.settings.buttonNoteIdProvider = provider;
return this;
}
@ -21,6 +25,11 @@ export default class ButtonFromNoteWidget extends CommandButtonWidget {
}
updateIcon() {
if (!this.settings.buttonNoteIdProvider) {
console.error(`buttonNoteId for '${this.componentId}' is not defined.`);
return;
}
const buttonNoteId = this.settings.buttonNoteIdProvider();
if (!buttonNoteId) {
@ -29,13 +38,18 @@ export default class ButtonFromNoteWidget extends CommandButtonWidget {
}
froca.getNote(buttonNoteId).then((note) => {
this.settings.icon = note.getIcon();
const icon = note?.getIcon();
if (icon) {
this.settings.icon = icon;
}
this.refreshIcon();
});
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// TODO: this seems incorrect
//@ts-ignore
const buttonNote = froca.getNoteFromCache(this.buttonNoteIdProvider());
if (!buttonNote) {

View File

@ -1,3 +1,4 @@
import type { EventData } from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
import OnClickButtonWidget from "./onclick_button.js";
@ -11,7 +12,7 @@ export default class ClosePaneButton extends OnClickButtonWidget {
);
}
async noteContextReorderEvent({ ntxIdsInOrder }) {
async noteContextReorderEvent({ ntxIdsInOrder }: EventData<"noteContextReorderEvent">) {
this.refresh();
}

View File

@ -1,6 +1,7 @@
import type { CommandNames } from "../../components/app_context.js";
import keyboardActionsService, { type Action } from "../../services/keyboard_actions.js";
import AbstractButtonWidget, { type AbstractButtonWidgetSettings } from "./abstract_button.js";
import type { ButtonNoteIdProvider } from "./button_from_note.js";
let actions: Action[];
@ -13,6 +14,7 @@ type CommandOrCallback = CommandNames | (() => CommandNames);
interface CommandButtonWidgetSettings extends AbstractButtonWidgetSettings {
command?: CommandOrCallback;
onClick?: ClickHandler;
buttonNoteIdProvider?: ButtonNoteIdProvider | null;
}
export default class CommandButtonWidget extends AbstractButtonWidget<CommandButtonWidgetSettings> {

View File

@ -3,7 +3,10 @@ import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
export default class MovePaneButton extends OnClickButtonWidget {
constructor(isMovingLeft) {
private isMovingLeft: boolean;
constructor(isMovingLeft: boolean) {
super();
this.isMovingLeft = isMovingLeft;

View File

@ -2,9 +2,13 @@ import OnClickButtonWidget from "./onclick_button.js";
import linkContextMenuService from "../../menus/link_context_menu.js";
import utils from "../../services/utils.js";
import appContext from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
export default class OpenNoteButtonWidget extends OnClickButtonWidget {
constructor(noteToOpen) {
private noteToOpen: FNote;
constructor(noteToOpen: FNote) {
super();
this.noteToOpen = noteToOpen;
@ -13,10 +17,14 @@ export default class OpenNoteButtonWidget extends OnClickButtonWidget {
.icon(() => this.noteToOpen.getIcon())
.onClick((widget, evt) => this.launch(evt))
.onAuxClick((widget, evt) => this.launch(evt))
.onContextMenu((evt) => linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt));
.onContextMenu((evt) => {
if (evt) {
linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt);
}
});
}
async launch(evt) {
async launch(evt: JQuery.ClickEvent | JQuery.TriggeredEvent | JQuery.ContextMenuEvent) {
if (evt.which === 3) {
return;
}

View File

@ -9,6 +9,6 @@ export default class RevisionsButton extends CommandButtonWidget {
}
isEnabled() {
return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type);
return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type ?? "");
}
}

View File

@ -20,7 +20,7 @@ export default class RightPaneContainer extends FlexContainer<RightPanelWidget>
return super.isEnabled() && !this.rightPaneHidden && this.children.length > 0 && !!this.children.find((ch) => ch.isEnabled() && ch.canBeShown());
}
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
const promise = super.handleEventInChildren(name, data);
if (["activeContextChanged", "noteSwitchedAndActivated", "noteSwitched"].includes(name)) {

View File

@ -1,7 +1,8 @@
import type BasicWidget from "../basic_widget.js";
import FlexContainer from "./flex_container.js";
export default class RootContainer extends FlexContainer {
constructor(isHorizontalLayout) {
export default class RootContainer extends FlexContainer<BasicWidget> {
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "column" : "row");
this.id("root-widget");

View File

@ -1,50 +0,0 @@
import Container from "./container.js";
export default class ScrollingContainer extends Container {
constructor() {
super();
this.class("scrolling-container");
this.css("overflow", "auto");
this.css("scroll-behavior", "smooth");
this.css("position", "relative");
}
setNoteContextEvent({ noteContext }) {
/** @var {NoteContext} */
this.noteContext = noteContext;
}
async noteSwitchedEvent({ noteContext, notePath }) {
this.$widget.scrollTop(0);
}
async noteSwitchedAndActivatedEvent({ noteContext, notePath }) {
this.noteContext = noteContext;
this.$widget.scrollTop(0);
}
async activeContextChangedEvent({ noteContext }) {
this.noteContext = noteContext;
}
handleEventInChildren(name, data) {
if (name === "readOnlyTemporarilyDisabled" && this.noteContext && this.noteContext.ntxId === data.noteContext.ntxId) {
const scrollTop = this.$widget.scrollTop();
const promise = super.handleEventInChildren(name, data);
// there seems to be some asynchronicity, and we need to wait a bit before scrolling
promise.then(() => setTimeout(() => this.$widget.scrollTop(scrollTop), 500));
return promise;
} else {
return super.handleEventInChildren(name, data);
}
}
scrollContainerToCommand({ position }) {
this.$widget.scrollTop(position);
}
}

View File

@ -0,0 +1,57 @@
import type { CommandListenerData, EventData, EventNames } from "../../components/app_context.js";
import type NoteContext from "../../components/note_context.js";
import type BasicWidget from "../basic_widget.js";
import Container from "./container.js";
export default class ScrollingContainer extends Container<BasicWidget> {
private noteContext?: NoteContext;
constructor() {
super();
this.class("scrolling-container");
this.css("overflow", "auto");
this.css("scroll-behavior", "smooth");
this.css("position", "relative");
}
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
this.noteContext = noteContext;
}
async noteSwitchedEvent({ noteContext, notePath }: EventData<"noteSwitched">) {
this.$widget.scrollTop(0);
}
async noteSwitchedAndActivatedEvent({ noteContext, notePath }: EventData<"noteSwitchedAndActivatedEvent">) {
this.noteContext = noteContext;
this.$widget.scrollTop(0);
}
async activeContextChangedEvent({ noteContext }: EventData<"activeContextChanged">) {
this.noteContext = noteContext;
}
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
if (name === "readOnlyTemporarilyDisabled" && this.noteContext && "noteContext" in data && this.noteContext.ntxId === data.noteContext?.ntxId) {
const scrollTop = this.$widget.scrollTop() ?? 0;
const promise = super.handleEventInChildren(name, data);
// there seems to be some asynchronicity, and we need to wait a bit before scrolling
if (promise) {
promise.then(() => setTimeout(() => this.$widget.scrollTop(scrollTop), 500));
}
return promise;
} else {
return super.handleEventInChildren(name, data);
}
}
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerToCommand">) {
this.$widget.scrollTop(position);
}
}

View File

@ -12,7 +12,7 @@ const TPL = `
</div>
<div class="modal-body">
${t("password_not_set.body1")}
${t("password_not_set.body2")}
</div>
</div>
@ -21,8 +21,13 @@ const TPL = `
`;
export default class PasswordNoteSetDialog extends BasicWidget {
private modal!: bootstrap.Modal;
private $openPasswordOptionsButton!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
//@ts-ignore fix once bootstrap is imported via JQuery.
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$openPasswordOptionsButton = this.$widget.find(".open-password-options-button");
this.$openPasswordOptionsButton.on("click", () => {

View File

@ -9,6 +9,8 @@ import protectedSessionHolder from "../../services/protected_session_holder.js";
import BasicWidget from "../basic_widget.js";
import dialogService from "../../services/dialog.js";
import options from "../../services/options.js";
import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js";
const TPL = `
<div class="revisions-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -76,7 +78,43 @@ const TPL = `
</div>
</div>`;
interface RevisionItem {
noteId: string;
revisionId: string;
dateLastEdited: string;
contentLength: number;
type: NoteType;
title: string;
isProtected: boolean;
mime: string;
}
interface FullRevision {
content: string;
mime: string;
}
export default class RevisionsDialog extends BasicWidget {
private revisionItems: RevisionItem[];
private note: FNote | null;
private revisionId: string | null;
//@ts-ignore
private modal: bootstrap.Modal;
//@ts-ignore
private listDropdown: bootstrap.Dropdown;
private $list!: JQuery<HTMLElement>;
private $listDropdown!: JQuery<HTMLElement>;
private $content!: JQuery<HTMLElement>;
private $title!: JQuery<HTMLElement>;
private $titleButtons!: JQuery<HTMLElement>;
private $eraseAllRevisionsButton!: JQuery<HTMLElement>;
private $maximumRevisions!: JQuery<HTMLElement>;
private $snapshotInterval!: JQuery<HTMLElement>;
private $revisionSettingsButton!: JQuery<HTMLElement>;
constructor() {
super();
@ -87,11 +125,13 @@ export default class RevisionsDialog extends BasicWidget {
doRender() {
this.$widget = $(TPL);
//@ts-ignore
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$list = this.$widget.find(".revision-list");
this.$listDropdown = this.$widget.find(".revision-list-dropdown");
this.listDropdown = bootstrap.Dropdown.getOrCreateInstance(this.$listDropdown);
//@ts-ignore
this.listDropdown = bootstrap.Dropdown.getOrCreateInstance(this.$listDropdown, { autoClose: false });
this.$content = this.$widget.find(".revision-content");
this.$title = this.$widget.find(".revision-title");
this.$titleButtons = this.$widget.find(".revision-title-buttons");
@ -102,26 +142,18 @@ export default class RevisionsDialog extends BasicWidget {
this.listDropdown.show();
this.$listDropdown.parent().on("hide.bs.dropdown", (e) => {
// Prevent closing dropdown by pressing ESC and clicking outside
e.preventDefault();
this.modal.hide();
});
document.addEventListener(
"keydown",
(e) => {
// Close the revision dialog when revision element is focused and ESC is pressed
if (e.key === "Escape" || e.target.classList.contains(["dropdown-item", "active"])) {
this.modal.hide();
}
},
true
);
this.$widget.on("shown.bs.modal", () => {
this.$list.find(`[data-revision-id="${this.revisionId}"]`).trigger("focus");
});
this.$eraseAllRevisionsButton.on("click", async () => {
if (!this.note) {
return;
}
const text = t("revisions.confirm_delete_all");
if (await dialogService.confirm(text)) {
@ -147,18 +179,22 @@ export default class RevisionsDialog extends BasicWidget {
}
async showRevisionsEvent({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
if (!noteId) {
return;
}
utils.openDialog(this.$widget);
await this.loadRevisions(noteId);
}
async loadRevisions(noteId) {
async loadRevisions(noteId: string) {
this.$list.empty();
this.$content.empty();
this.$titleButtons.empty();
this.note = appContext.tabManager.getActiveContextNote();
this.revisionItems = await server.get(`notes/${noteId}/revisions`);
this.revisionItems = await server.get<RevisionItem[]>(`notes/${noteId}/revisions`);
for (const item of this.revisionItems) {
this.$list.append(
@ -184,9 +220,9 @@ export default class RevisionsDialog extends BasicWidget {
// Show the footer of the revisions dialog
this.$snapshotInterval.text(t("revisions.snapshot_interval", { seconds: options.getInt("revisionSnapshotTimeInterval") }));
let revisionsNumberLimit = parseInt(this.note.getLabelValue("versioningLimit") ?? "");
let revisionsNumberLimit: number | string = parseInt(this.note?.getLabelValue("versioningLimit") ?? "");
if (!Number.isInteger(revisionsNumberLimit)) {
revisionsNumberLimit = parseInt(options.getInt("revisionSnapshotNumberLimit"));
revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0;
}
if (revisionsNumberLimit === -1) {
revisionsNumberLimit = "∞";
@ -198,6 +234,9 @@ export default class RevisionsDialog extends BasicWidget {
const revisionId = this.$list.find(".active").attr("data-revision-id");
const revisionItem = this.revisionItems.find((r) => r.revisionId === revisionId);
if (!revisionItem) {
return;
}
this.$title.html(revisionItem.title);
@ -206,7 +245,7 @@ export default class RevisionsDialog extends BasicWidget {
await this.renderContent(revisionItem);
}
renderContentButtons(revisionItem) {
renderContentButtons(revisionItem: RevisionItem) {
this.$titleButtons.empty();
const $restoreRevisionButton = $(`<button class="btn btn-sm" type="button">${t("revisions.restore_button")}</button>`);
@ -252,13 +291,13 @@ export default class RevisionsDialog extends BasicWidget {
}
}
async renderContent(revisionItem) {
async renderContent(revisionItem: RevisionItem) {
this.$content.empty();
const fullRevision = await server.get(`revisions/${revisionItem.revisionId}`);
const fullRevision = await server.get<FullRevision>(`revisions/${revisionItem.revisionId}`);
if (revisionItem.type === "text") {
this.$content.html(fullRevision.content);
this.$content.html(`<div class="ck-content">${fullRevision.content}</div>`);
if (this.$content.find("span.math-tex").length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
@ -266,11 +305,15 @@ export default class RevisionsDialog extends BasicWidget {
renderMathInElement(this.$content[0], { trust: true });
}
} else if (revisionItem.type === "code") {
this.$content.html($("<pre>").text(fullRevision.content));
this.$content.html($("<pre>")
.text(fullRevision.content).html());
} else if (revisionItem.type === "image") {
if (fullRevision.mime === "image/svg+xml") {
let encodedSVG = encodeURIComponent(fullRevision.content); //Base64 of other format images may be embedded in svg
this.$content.html($("<img>").attr("src", `data:${fullRevision.mime};utf8,${encodedSVG}`).css("max-width", "100%").css("max-height", "100%"));
this.$content.html($("<img>")
.attr("src", `data:${fullRevision.mime};utf8,${encodedSVG}`)
.css("max-width", "100%")
.css("max-height", "100%").html());
} else {
this.$content.html(
$("<img>")
@ -278,13 +321,16 @@ export default class RevisionsDialog extends BasicWidget {
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
.attr("src", `data:${fullRevision.mime};base64,${fullRevision.content}`)
.css("max-width", "100%")
.css("max-height", "100%")
.css("max-height", "100%").html()
);
}
} else if (revisionItem.type === "file") {
const $table = $("<table cellpadding='10'>")
.append($("<tr>").append($("<th>").text(t("revisions.mime")), $("<td>").text(revisionItem.mime)))
.append($("<tr>").append($("<th>").text(t("revisions.file_size")), $("<td>").text(utils.formatSize(revisionItem.contentLength))));
.append($("<tr>")
.append(
$("<th>").text(t("revisions.mime")),
$("<td>").text(revisionItem.mime)))
.append($("<tr>").append($("<th>").text(t("revisions.file_size")), $("<td>").text(utils.formatSize(revisionItem.contentLength))));
if (fullRevision.content) {
$table.append(
@ -294,15 +340,23 @@ export default class RevisionsDialog extends BasicWidget {
);
}
this.$content.html($table);
this.$content.html($table.html());
} else if (["canvas", "mindMap"].includes(revisionItem.type)) {
const encodedTitle = encodeURIComponent(revisionItem.title);
this.$content.html($("<img>").attr("src", `api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`).css("max-width", "100%"));
this.$content.html(
$("<img>")
.attr("src", `api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`)
.css("max-width", "100%")
.html());
} else if (revisionItem.type === "mermaid") {
const encodedTitle = encodeURIComponent(revisionItem.title);
this.$content.html($("<img>").attr("src", `api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`).css("max-width", "100%"));
this.$content.html(
$("<img>")
.attr("src", `api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`)
.css("max-width", "100%")
.html());
this.$content.append($("<pre>").text(fullRevision.content));
} else {

View File

@ -8,13 +8,16 @@ const TPL = `
class="copy-image-reference-button"
title="${t("copy_image_reference_button.button_title")}">
<span class="bx bx-copy"></span>
<div class="hidden-image-copy"></div>
</button>`;
export default class CopyImageReferenceButton extends NoteContextAwareWidget {
private $hiddenImageCopy!: JQuery<HTMLElement>;
isEnabled() {
return super.isEnabled() && ["mermaid", "canvas", "mindMap"].includes(this.note?.type) && this.note.isContentAvailable() && this.noteContext?.viewScope.viewMode === "default";
return super.isEnabled() && ["mermaid", "canvas", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {
@ -24,6 +27,10 @@ export default class CopyImageReferenceButton extends NoteContextAwareWidget {
this.$hiddenImageCopy = this.$widget.find(".hidden-image-copy");
this.$widget.on("click", () => {
if (!this.note) {
return;
}
this.$hiddenImageCopy.empty().append($("<img>").attr("src", utils.createImageSrcUrl(this.note)));
imageService.copyImageReferenceToClipboard(this.$hiddenImageCopy);

View File

@ -11,7 +11,7 @@ const TPL = `
export default class SvgExportButton extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type) && this.note.isContentAvailable() && this.noteContext?.viewScope.viewMode === "default";
return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {

View File

@ -1,3 +1,5 @@
import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.js";
import { t } from "../services/i18n.js";
import protectedSessionService from "../services/protected_session.js";
import SwitchWidget from "./switch.js";
@ -14,18 +16,22 @@ export default class ProtectedNoteSwitchWidget extends SwitchWidget {
}
switchOn() {
protectedSessionService.protectNote(this.noteId, true, false);
if (this.noteId) {
protectedSessionService.protectNote(this.noteId, true, false);
}
}
switchOff() {
protectedSessionService.protectNote(this.noteId, false, false);
if (this.noteId) {
protectedSessionService.protectNote(this.noteId, false, false);
}
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
this.isToggled = note.isProtected;
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}

View File

@ -10,7 +10,10 @@ import QuickSearchWidget from "./quick_search.js";
* - Hiding the widget on mobile.
*/
export default class QuickSearchLauncherWidget extends QuickSearchWidget {
constructor(isHorizontalLayout) {
private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}

View File

@ -1,3 +1,4 @@
import type FNote from "../../entities/fnote.js";
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
@ -19,6 +20,9 @@ const TPL = `
* TODO: figure out better name or conceptualize better.
*/
export default class NotePropertiesWidget extends NoteContextAwareWidget {
private $pageUrl!: JQuery<HTMLElement>;
isEnabled() {
return this.note && !!this.note.getLabelValue("pageUrl");
}
@ -39,9 +43,9 @@ export default class NotePropertiesWidget extends NoteContextAwareWidget {
this.$pageUrl = this.$widget.find(".page-url");
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
const pageUrl = note.getLabelValue("pageUrl");
this.$pageUrl.attr("href", pageUrl).attr("title", pageUrl).text(pageUrl);
this.$pageUrl.attr("href", pageUrl).attr("title", pageUrl).text(pageUrl ?? "");
}
}

View File

@ -3,8 +3,11 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = `<div class="scroll-padding-widget"></div>`;
export default class ScrollPaddingWidget extends NoteContextAwareWidget {
private $scrollingContainer!: JQuery<HTMLElement>;
isEnabled() {
return super.isEnabled() && ["text", "code"].includes(this.note?.type);
return super.isEnabled() && ["text", "code"].includes(this.note?.type ?? "");
}
doRender() {
@ -25,6 +28,6 @@ export default class ScrollPaddingWidget extends NoteContextAwareWidget {
refreshHeight() {
const containerHeight = this.$scrollingContainer.height();
this.$widget.css("height", Math.round(containerHeight / 2));
this.$widget.css("height", Math.round((containerHeight ?? 0) / 2));
}
}

View File

@ -27,7 +27,7 @@ export default class Debug extends AbstractSearchOption {
return "label";
}
static async create(noteId) {
static async create(noteId: string) {
await AbstractSearchOption.setAttribute(noteId, "label", "debug");
}

View File

@ -12,7 +12,7 @@ const TPL = `
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
${t("fast_search.description")}
</div>
</div>
</div>
<span class="bx bx-x icon-action search-option-del"></span>
</td>
@ -26,7 +26,7 @@ export default class FastSearch extends AbstractSearchOption {
return "label";
}
static async create(noteId) {
static async create(noteId: string) {
await AbstractSearchOption.setAttribute(noteId, "label", "fastSearch");
}

View File

@ -20,7 +20,7 @@ export default class IncludeArchivedNotes extends AbstractSearchOption {
return "label";
}
static async create(noteId) {
static async create(noteId: string) {
await AbstractSearchOption.setAttribute(noteId, "label", "includeArchivedNotes");
}

View File

@ -123,13 +123,13 @@ export default class SwitchWidget extends NoteContextAwareWidget {
private $switchButton!: JQuery<HTMLElement>;
private $switchToggle!: JQuery<HTMLElement>;
private $switchName!: JQuery<HTMLElement>;
private $helpButton!: JQuery<HTMLElement>;
protected $helpButton!: JQuery<HTMLElement>;
private switchOnName = "";
private switchOnTooltip = "";
protected switchOnName = "";
protected switchOnTooltip = "";
private switchOffName = "";
private switchOffTooltip = "";
protected switchOffName = "";
protected switchOffTooltip = "";
private disabledTooltip = "";

View File

@ -1,13 +1,16 @@
import SwitchWidget from "./switch.js";
import attributeService from "../services/attributes.js";
import { t } from "../services/i18n.js";
import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.js";
/**
* Switch for the basic properties widget which allows the user to select whether the note is a template or not, which toggles the `#template` attribute.
*/
export default class TemplateSwitchWidget extends SwitchWidget {
isEnabled() {
return super.isEnabled() && !this.noteId.startsWith("_options");
return super.isEnabled() && !this.noteId?.startsWith("_options");
}
doRender() {
@ -23,21 +26,25 @@ export default class TemplateSwitchWidget extends SwitchWidget {
}
async switchOn() {
await attributeService.setLabel(this.noteId, "template");
}
async switchOff() {
for (const templateAttr of this.note.getOwnedLabels("template")) {
await attributeService.removeAttributeById(this.noteId, templateAttr.attributeId);
if (this.noteId) {
await attributeService.setLabel(this.noteId, "template");
}
}
async refreshWithNote(note) {
async switchOff() {
if (this.note && this.noteId) {
for (const templateAttr of this.note.getOwnedLabels("template")) {
await attributeService.removeAttributeById(this.noteId, templateAttr.attributeId);
}
}
}
async refreshWithNote(note: FNote) {
const isTemplate = note.hasLabel("template");
this.isToggled = isTemplate;
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows().find((attr) => attr.type === "label" && attr.name === "template" && attr.noteId === this.noteId)) {
this.refresh();
}

View File

@ -1,5 +1,6 @@
import TypeWidget from "./type_widget.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
const TPL = `
<div class="note-detail-book note-detail-printable">
@ -19,6 +20,9 @@ const TPL = `
</div>`;
export default class BookTypeWidget extends TypeWidget {
private $helpNoChildren!: JQuery<HTMLElement>;
static getType() {
return "book";
}
@ -30,7 +34,7 @@ export default class BookTypeWidget extends TypeWidget {
super.doRender();
}
async doRefresh(note) {
this.$helpNoChildren.toggle(!this.note.hasChildren());
async doRefresh(note: FNote) {
this.$helpNoChildren.toggle(!this.note?.hasChildren());
}
}

View File

@ -0,0 +1,223 @@
import { ALLOWED_PROTOCOLS } from "../../../services/link.js";
import options from "../../../services/options.js";
import utils from "../../../services/utils.js";
export function buildConfig() {
return {
image: {
styles: {
options: [
'inline',
'alignBlockLeft',
'alignCenter',
'alignBlockRight',
'alignLeft',
'alignRight',
'full', // full and side are for BC since the old images have been created with these styles
'side'
]
},
resizeOptions: [
{
name: 'imageResize:original',
value: null,
icon: 'original'
},
{
name: 'imageResize:25',
value: '25',
icon: 'small'
},
{
name: 'imageResize:50',
value: '50',
icon: 'medium'
},
{
name: 'imageResize:75',
value: '75',
icon: 'medium'
}
],
toolbar: [
// Image styles, see https://ckeditor.com/docs/ckeditor5/latest/features/images/images-styles.html#demo.
'imageStyle:inline',
'imageStyle:alignCenter',
{
name: "imageStyle:wrapText",
title: "Wrap text",
items: [
'imageStyle:alignLeft',
'imageStyle:alignRight',
],
defaultItem: 'imageStyle:alignRight'
},
{
name: "imageStyle:block",
title: "Block align",
items: [
'imageStyle:alignBlockLeft',
'imageStyle:alignBlockRight'
],
defaultItem: "imageStyle:alignBlockLeft",
},
'|',
'imageResize:25',
'imageResize:50',
'imageResize:original',
'|',
'toggleImageCaption'
],
upload: {
types: [ 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'svg+xml', 'avif' ]
}
},
heading: {
options: [
{ model: 'paragraph' as const, title: 'Paragraph', class: 'ck-heading_paragraph' },
// // heading1 is not used since that should be a note's title
{ model: 'heading2' as const, view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3' as const, view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
{ model: 'heading4' as const, view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' },
{ model: 'heading5' as const, view: 'h5', title: 'Heading 5', class: 'ck-heading_heading5' },
{ model: 'heading6' as const, view: 'h6', title: 'Heading 6', class: 'ck-heading_heading6' }
]
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells',
'tableProperties',
'tableCellProperties',
'toggleTableCaption'
]
},
list: {
properties: {
styles: true,
startIndex: true,
reversed: true
}
},
link: {
defaultProtocol: 'https://',
allowedProtocols: ALLOWED_PROTOCOLS
},
// This value must be kept in sync with the language defined in webpack.config.js.
language: 'en'
}
}
export function buildToolbarConfig(isClassicToolbar: boolean) {
if (isClassicToolbar) {
const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true"
return buildClassicToolbar(multilineToolbar);
} else {
return buildFloatingToolbar();
}
}
function buildClassicToolbar(multilineToolbar: boolean) {
// For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars.
return {
toolbar: {
items: [
'heading', 'fontSize',
'|',
'bold', 'italic',
{
label: "Text formatting",
icon: "text",
items: [
'underline',
'strikethrough',
'superscript',
'subscript',
'code',
],
},
'|',
'fontColor', 'fontBackgroundColor', 'removeFormat',
'|',
'bulletedList', 'numberedList', 'todoList',
'|',
'blockQuote', 'insertTable', 'codeBlock', 'footnote',
{
label: "Insert",
icon: "plus",
items: [
'imageUpload',
'|',
'link',
'internallink',
'includeNote',
'|',
'specialCharacters',
'math',
'mermaid',
'horizontalLine',
'pageBreak'
]
},
'|',
'outdent', 'indent',
'|',
'markdownImport', 'cuttonote', 'findAndReplace'
],
shouldNotGroupWhenFull: multilineToolbar
}
}
}
function buildFloatingToolbar() {
return {
toolbar: {
items: [
'fontSize',
'bold',
'italic',
'underline',
'strikethrough',
'superscript',
'subscript',
'fontColor',
'fontBackgroundColor',
'code',
'link',
'removeFormat',
'internallink',
'cuttonote'
]
},
blockToolbar: [
'heading',
'|',
'bulletedList', 'numberedList', 'todoList',
'|',
'blockQuote', 'codeBlock', 'insertTable',
'footnote',
{
label: "Insert",
icon: "plus",
items: [
'internallink',
'includeNote',
'|',
'math',
'mermaid',
'horizontalLine',
'pageBreak'
]
},
'|',
'outdent', 'indent',
'|',
'imageUpload',
'markdownImport',
'specialCharacters',
'findAndReplace'
]
};
}

View File

@ -1,3 +1,5 @@
import type { EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
import { t } from "../../services/i18n.js";
import keyboardActionService from "../../services/keyboard_actions.js";
import options from "../../services/options.js";
@ -10,7 +12,7 @@ const TPL = `
position: relative;
height: 100%;
}
.note-detail-code-editor {
min-height: 50px;
height: 100%;
@ -21,6 +23,9 @@ const TPL = `
</div>`;
export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
private $editor!: JQuery<HTMLElement>;
static getType() {
return "editableCode";
}
@ -50,11 +55,11 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
this.codeEditor.on("change", () => this.spacedUpdate.scheduleUpdate());
}
async doRefresh(note) {
const blob = await this.note.getBlob();
async doRefresh(note: FNote) {
const blob = await this.note?.getBlob();
await this.spacedUpdate.allowUpdateWithoutChange(() => {
this._update(note, blob.content);
this._update(note, blob?.content);
});
this.show();
@ -66,7 +71,7 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
};
}
async executeWithCodeEditorEvent({ resolve, ntxId }) {
async executeWithCodeEditorEvent({ resolve, ntxId }: EventData<"executeWithCodeEditor">) {
if (!this.isNoteContext(ntxId)) {
return;
}

View File

@ -15,6 +15,7 @@ import options from "../../services/options.js";
import toast from "../../services/toast.js";
import { getMermaidConfig } from "../mermaid.js";
import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js";
import { buildConfig, buildToolbarConfig } from "./ckeditor/toolbars.js";
const ENABLE_INSPECTOR = false;
@ -183,16 +184,11 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
logInfo("Creating new CKEditor");
const extraOpts = {};
if (isClassicEditor) {
extraOpts.toolbar = {
shouldNotGroupWhenFull: utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true"
};
}
const editor = await editorClass.create(elementOrData, {
...editorConfig,
...extraOpts,
...buildConfig(),
...buildToolbarConfig(isClassicEditor),
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags")),
styles: true,

View File

@ -1,9 +1,13 @@
import TypeWidget from "./type_widget.js";
import NoteMapWidget from "../note_map.js";
import type FNote from "../../entities/fnote.js";
const TPL = `<div class="note-detail-note-map note-detail-printable"></div>`;
export default class NoteMapTypeWidget extends TypeWidget {
private noteMapWidget: NoteMapWidget;
static getType() {
return "noteMap";
}
@ -22,7 +26,7 @@ export default class NoteMapTypeWidget extends TypeWidget {
super.doRender();
}
async doRefresh(note) {
async doRefresh(note: FNote) {
await this.noteMapWidget.refresh();
}
}

View File

@ -28,6 +28,10 @@ const TPL = `
</div>`;
export default class ProtectedSessionTypeWidget extends TypeWidget {
private $passwordForm!: JQuery<HTMLElement>;
private $passwordInput!: JQuery<HTMLElement>;
static getType() {
return "protectedSession";
}
@ -38,7 +42,7 @@ export default class ProtectedSessionTypeWidget extends TypeWidget {
this.$passwordInput = this.$widget.find(".protected-session-password");
this.$passwordForm.on("submit", () => {
const password = this.$passwordInput.val();
const password = String(this.$passwordInput.val());
this.$passwordInput.val("");
protectedSessionService.setupProtectedSession(password);

View File

@ -17,7 +17,7 @@ export default abstract class TypeWidget extends NoteContextAwareWidget {
return super.doRender();
}
abstract doRefresh(note: FNote | null | undefined): Promise<void>;
doRefresh(note: FNote | null | undefined) {}
async refresh() {
const thisWidgetType = (this.constructor as any).getType();

View File

@ -294,6 +294,8 @@ button kbd {
color: var(--menu-text-color) !important;
font-size: inherit;
background-color: var(--menu-background-color) !important;
user-select: none;
-webkit-user-select: none;
}
body.desktop .dropdown-menu {

View File

@ -283,10 +283,6 @@ input::selection,
/* Combo box-like dropdown buttons */
.select-button.dropdown-toggle {
padding-right: 40px;
}
.select-button.dropdown-toggle::after {
/* Remove the original arrow */
content: unset;
@ -298,10 +294,13 @@ select,
select.form-select,
select.form-control,
.select-button.dropdown-toggle.btn {
--dropdown-arrow: var(--select-arrow-svg) right 0.75rem center/15px 20px no-repeat;
outline: 3px solid transparent;
outline-offset: 6px;
padding-right: calc(15px + 1.5rem);
background: var(--input-background-color)
var(--select-arrow-svg) right 0.75rem center/15px 20px no-repeat;
var(--dropdown-arrow);
color: var(--input-text-color);
border: unset;
border-radius: 0.375rem;
@ -311,7 +310,8 @@ select:hover,
select.form-select:hover,
select.form-control:hover,
.select-button.dropdown-toggle.btn:hover {
background-color: var(--input-hover-background);
background: var(--input-hover-background)
var(--dropdown-arrow);
color: var(--input-hover-color);
}
@ -327,7 +327,8 @@ select.form-control:focus,
box-shadow: unset;
outline: 3px solid var(--input-focus-outline-color);
outline-offset: 0;
background-color: var(--select-focus-background);
background: var(--select-focus-background)
var(--dropdown-arrow);
color: var(--select-focus-text-color);
transition: outline-color 50ms linear,
outline-offset 200ms ease-out;

View File

@ -1783,4 +1783,27 @@ div.bookmark-folder-widget .note-link .bx {
.delete-notes-list .note-path {
padding-left: 8px;
}
/* The "Change note icon" button */
.note-icon-widget .note-icon {
border: none;
border-radius: 8px;
}
.note-icon-widget .note-icon:hover {
background: var(--icon-button-hover-background);
color: var(--icon-button-hover-color);
}
/* Note icon popup */
.note-icon-widget .icon-list span {
border-radius: 8px;
}
.note-icon-widget .icon-list span:hover {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}

View File

@ -109,7 +109,8 @@
"choose_export_type": "Por favor, elija primero el tipo de exportación",
"export_status": "Estado de exportación",
"export_in_progress": "Exportación en curso: {{progressCount}}",
"export_finished_successfully": "La exportación finalizó exitosamente."
"export_finished_successfully": "La exportación finalizó exitosamente.",
"format_pdf": "PDF - para propósitos de impresión o compartición."
},
"help": {
"fullDocumentation": "Ayuda (la documentación completa está disponible <a class=\"external\" href=\"https://triliumnext.github.io/Docs/\">online</a>)",
@ -437,7 +438,9 @@
"share_favicon": "La nota de favicon se configurará en la página compartida. Por lo general, se desea configurarlo para que comparta la raíz y lo haga heredable. La nota de Favicon también debe estar en el subárbol compartido. Considere usar 'share_hidden_from_tree'.",
"is_owned_by_note": "es propiedad de una nota",
"other_notes_with_name": "Otras notas con nombre de {{attributeType}} \"{{attributeName}}\"",
"and_more": "... y {{count}} más."
"and_more": "... y {{count}} más.",
"print_landscape": "Al exportar a PDF, cambia la orientación de la página a paisaje en lugar de retrato.",
"print_page_size": "Al exportar a PDF, cambia el tamaño de la página. Valores soportados: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>."
},
"attribute_editor": {
"help_text_body1": "Para agregar una etiqueta, simplemente escriba, por ejemplo. <code>#rock</code> o si desea agregar también valor, p.e. <code>#año = 2020</code>",
@ -638,7 +641,8 @@
"show_hidden_subtree": "Mostrar subárbol oculto",
"show_help": "Mostrar ayuda",
"about": "Acerca de TriliumNext Notes",
"logout": "Cerrar sesión"
"logout": "Cerrar sesión",
"show-cheatsheet": "Mostrar hoja de trucos"
},
"sync_status": {
"unknown": "<p>El estado de sincronización será conocido una vez que el siguiente intento de sincronización comience.</p><p>Dé clic para activar la sincronización ahora</p>",
@ -672,7 +676,8 @@
"save_revision": "Guardar revisión",
"convert_into_attachment_failed": "La conversión de nota '{{title}}' falló.",
"convert_into_attachment_successful": "La nota '{{title}}' ha sido convertida a un archivo adjunto.",
"convert_into_attachment_prompt": "¿Está seguro que desea convertir la nota '{{title}}' en un archivo adjunto de la nota padre?"
"convert_into_attachment_prompt": "¿Está seguro que desea convertir la nota '{{title}}' en un archivo adjunto de la nota padre?",
"print_pdf": "Exportar como PDF..."
},
"onclick_button": {
"no_click_handler": "El widget de botón '{{componentId}}' no tiene un controlador de clics definido"
@ -1409,7 +1414,9 @@
"launcher": "Lanzador",
"doc": "Doc",
"widget": "Widget",
"confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?"
"confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?",
"geo-map": "Mapa Geo",
"beta-feature": "Beta"
},
"protect_note": {
"toggle-on": "Proteger la nota",
@ -1629,5 +1636,17 @@
},
"note_tooltip": {
"note-has-been-deleted": "La nota ha sido eliminada."
},
"geo-map": {
"create-child-note-title": "Crear una nueva subnota y agregarla al mapa",
"create-child-note-instruction": "Dé clic en el mapa para crear una nueva nota en esa ubicación o presione Escape para cancelar.",
"unable-to-load-map": "No se puede cargar el mapa."
},
"geo-map-context": {
"open-location": "Abrir ubicación",
"remove-from-map": "Eliminar del mapa"
},
"help-button": {
"title": "Abrir la página de ayuda relevante"
}
}

View File

@ -230,7 +230,9 @@
"workspace_search_home": "notițele de căutare vor fi create sub această notiță",
"workspace_tab_background_color": "Culoare CSS ce va fi folosită în tab-urile ce aparțin spațiului de lucru",
"workspace_template": "Această notița va apărea în lista de șabloane când se crează o nouă notiță, dar doar când spațiul de lucru în care se află notița este focalizat",
"app_theme_base": "setați valoarea la „next” pentru a folosi drept temă de bază „TriliumNext” în loc de cea clasică."
"app_theme_base": "setați valoarea la „next” pentru a folosi drept temă de bază „TriliumNext” în loc de cea clasică.",
"print_landscape": "Schimbă orientarea paginii din portret în peisaj atunci când se exportă în PDF.",
"print_page_size": "Schimbă dimensiunea paginii când se exportă în PDF. Valori suportate: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>."
},
"attribute_editor": {
"add_a_new_attribute": "Adaugă un nou attribut",
@ -520,7 +522,8 @@
"format_opml": "OPML - format de interschimbare pentru editoare cu structură ierarhică (outline). Formatarea, imaginile și fișierele nu vor fi incluse.",
"opml_version_1": "OPML v1.0 - text simplu",
"opml_version_2": "OPML v2.0 - permite și HTML",
"format_html": "HTML - recomandat deoarece păstrează toata formatarea"
"format_html": "HTML - recomandat deoarece păstrează toata formatarea",
"format_pdf": "PDF - cu scopul de printare sau partajare."
},
"fast_search": {
"description": "Căutarea rapidă dezactivează căutarea la nivel de conținut al notițelor cu scopul de a îmbunătăți performanța de căutare pentru baze de date mari.",
@ -588,7 +591,8 @@
"toggle_fullscreen": "Comută mod ecran complet",
"zoom": "Zoom",
"zoom_in": "Mărește",
"zoom_out": "Micșorează"
"zoom_out": "Micșorează",
"show-cheatsheet": "Afișează ghidul rapid"
},
"heading_style": {
"markdown": "Stil Markdown",
@ -1650,5 +1654,8 @@
"hours": "ore",
"minutes": "minute",
"seconds": "secunde"
},
"help-button": {
"title": "Deschide ghidul relevant"
}
}

View File

@ -2,6 +2,51 @@
import appInfo from "../../services/app_info.js";
/**
* @swagger
* /api/app-info:
* get:
* summary: Get installation info
* operationId: app-info
* externalDocs:
* description: Server implementation
* url: https://github.com/TriliumNext/Notes/blob/v0.91.6/src/services/app_info.ts
* responses:
* '200':
* content:
* application/json:
* schema:
* type: object
* properties:
* appVersion:
* type: string
* example: "0.91.6"
* dbVersion:
* type: integer
* example: 228
* nodeVersion:
* type: string
* description: "value of process.version"
* syncVersion:
* type: integer
* example: 34
* buildDate:
* type: string
* example: "2024-09-07T18:36:34Z"
* buildRevision:
* type: string
* example: "7c0d6930fa8f20d269dcfbcbc8f636a25f6bb9a7"
* dataDirectory:
* type: string
* example: "/var/lib/trilium"
* clipperProtocolVersion:
* type: string
* example: "1.0"
* utcDateTime:
* $ref: '#/components/schemas/UtcDateTime'
* security:
* - session: []
*/
function getAppInfo() {
return appInfo;
}

View File

@ -14,6 +14,68 @@ import ws from "../../services/ws.js";
import etapiTokenService from "../../services/etapi_tokens.js";
import type { Request } from "express";
/**
* @swagger
* /api/login/sync:
* post:
* tags:
* - auth
* summary: Log in using documentSecret
* description: The `hash` parameter is computed using a HMAC of the `documentSecret` and `timestamp`.
* operationId: login-sync
* externalDocs:
* description: HMAC calculation
* url: https://github.com/TriliumNext/Notes/blob/v0.91.6/src/services/utils.ts#L62-L66
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* timestamp:
* $ref: '#/components/schemas/UtcDateTime'
* hash:
* type: string
* syncVersion:
* type: integer
* example: 34
* responses:
* '200':
* description: Successful operation
* content:
* application/json:
* schema:
* type: object
* properties:
* syncVersion:
* type: integer
* example: 34
* options:
* type: object
* properties:
* documentSecret:
* type: string
* '400':
* description: Sync version / document secret mismatch
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: "Non-matching sync versions, local is version ${server syncVersion}, remote is ${requested syncVersion}. It is recommended to run same version of Trilium on both sides of sync"
* '401':
* description: Timestamp mismatch
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: "Auth request time is out of sync, please check that both client and server have correct time. The difference between clocks has to be smaller than 5 minutes"
*/
function loginSync(req: Request) {
if (!sqlInit.schemaExists()) {
return [500, { message: "DB schema does not exist, can't sync." }];

View File

@ -45,6 +45,34 @@ function saveSyncSeed(req: Request) {
sqlInit.createDatabaseForSync(options);
}
/**
* @swagger
* /api/setup/sync-seed:
* get:
* tags:
* - auth
* summary: Sync documentSecret value
* description: First step to logging in.
* operationId: setup-sync-seed
* responses:
* '200':
* description: Successful operation
* content:
* application/json:
* schema:
* type: object
* properties:
* syncVersion:
* type: integer
* example: 34
* options:
* type: object
* properties:
* documentSecret:
* type: string
* security:
* - user-password: []
*/
function getSyncSeed() {
log.info("Serving sync seed.");

27
src/routes/api_docs.ts Normal file
View File

@ -0,0 +1,27 @@
import type { Router } from "express";
import swaggerUi from "swagger-ui-express";
import { readFile } from "fs/promises";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import yaml from "js-yaml";
import type { JsonObject } from "swagger-ui-express";
const __dirname = dirname(fileURLToPath(import.meta.url));
const swaggerDocument = yaml.load(
await readFile(join(__dirname, "../etapi/etapi.openapi.yaml"), "utf8")
) as JsonObject;
function register(router: Router) {
router.use(
"/etapi",
swaggerUi.serve,
swaggerUi.setup(swaggerDocument, {
explorer: true,
customSiteTitle: "TriliumNext ETAPI Documentation"
})
);
}
export default {
register
};

View File

@ -1,11 +1,12 @@
import { doubleCsrf } from "csrf-csrf";
import sessionSecret from "../services/session_secret.js";
import { isElectron } from "../services/utils.js";
import config from "../services/config.js";
const doubleCsrfUtilities = doubleCsrf({
getSecret: () => sessionSecret,
cookieOptions: {
path: "", // empty, so cookie is valid only for the current path
path: config.Session.cookiePath,
secure: false,
sameSite: "strict",
httpOnly: !isElectron // set to false for Electron, see https://github.com/TriliumNext/Notes/pull/966

View File

@ -57,30 +57,29 @@ function setPassword(req: Request, res: Response) {
}
function login(req: Request, res: Response) {
const guessedPassword = req.body.password;
const { password, rememberMe } = req.body;
if (verifyPassword(guessedPassword)) {
const rememberMe = req.body.rememberMe;
req.session.regenerate(() => {
if (rememberMe) {
req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks
} else {
req.session.cookie.expires = null;
}
req.session.loggedIn = true;
res.redirect(".");
});
} else {
if (!verifyPassword(password)) {
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
res.status(401).render("login", {
return res.status(401).render("login", {
failedAuth: true,
assetPath: assetPath
});
}
req.session.regenerate(() => {
if (!rememberMe) {
// unset default maxAge set by sessionParser
// Cookie becomes non-persistent and expires after current browser session (e.g. when browser is closed)
req.session.cookie.maxAge = undefined;
}
req.session.loggedIn = true;
res.redirect(".");
});
}
function verifyPassword(guessedPassword: string) {

View File

@ -71,7 +71,7 @@ import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
import etapiSpecRoute from "../etapi/spec.js";
import etapiBackupRoute from "../etapi/backup.js";
import apiDocsRoute from "./api_docs.js";
const MAX_ALLOWED_FILE_SIZE_MB = 250;
const GET = "get",
@ -369,6 +369,9 @@ function register(app: express.Application) {
etapiSpecRoute.register(router);
etapiBackupRoute.register(router);
// API Documentation
apiDocsRoute.register(app);
app.use("", router);
}

View File

@ -2,6 +2,7 @@ import session from "express-session";
import sessionFileStore from "session-file-store";
import sessionSecret from "../services/session_secret.js";
import dataDir from "../services/data_dir.js";
import config from "../services/config.js";
const FileStore = sessionFileStore(session);
const sessionParser = session({
@ -9,13 +10,13 @@ const sessionParser = session({
resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request.
saveUninitialized: false, // true forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified.
cookie: {
// path: "/",
path: config.Session.cookiePath,
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
maxAge: config.Session.cookieMaxAge * 1000 // needs value in milliseconds
},
name: "trilium.sid",
store: new FileStore({
ttl: 30 * 24 * 3600,
ttl: config.Session.cookieMaxAge,
path: `${dataDir.TRILIUM_DATA_DIR}/sessions`
})
});

View File

@ -32,6 +32,10 @@ export interface TriliumConfig {
keyPath: string;
trustedReverseProxy: boolean | string;
};
Session: {
cookiePath: string;
cookieMaxAge: number;
}
Sync: {
syncServerHost: string;
syncServerTimeout: string;
@ -76,6 +80,14 @@ const config: TriliumConfig = {
process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false
},
Session: {
cookiePath:
process.env.TRILIUM_SESSION_COOKIEPATH || iniConfig?.Session?.cookiePath || "/",
cookieMaxAge:
parseInt(String(process.env.TRILIUM_SESSION_COOKIEMAXAGE)) || parseInt(iniConfig?.Session?.cookieMaxAge) || 21 * 24 * 60 * 60 // 21 Days in Seconds
},
Sync: {
syncServerHost:
process.env.TRILIUM_SYNC_SERVER_HOST || iniConfig?.Sync?.syncServerHost || "",

View File

@ -34,6 +34,12 @@ export default function buildLaunchBarConfig() {
type: "launcher",
builtinWidget: "calendar",
icon: "bx bx-calendar"
},
recentChanges: {
title: t("hidden-subtree.recent-changes-title"),
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history"
}
};
@ -63,14 +69,7 @@ export default function buildLaunchBarConfig() {
},
{ id: "_lbNoteMap", title: t("hidden-subtree.note-map-title"), type: "launcher", targetNoteId: "_globalNoteMap", icon: "bx bxs-network-chart" },
{ id: "_lbCalendar", ...sharedLaunchers.calendar },
{
id: "_lbRecentChanges",
title: t("hidden-subtree.recent-changes-title"),
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{ id: "_lbRecentChanges", ...sharedLaunchers.recentChanges },
{ id: "_lbSpacer1", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "50", growthFactor: "0" },
{ id: "_lbBookmarks", title: t("hidden-subtree.bookmarks-title"), type: "launcher", builtinWidget: "bookmarks", icon: "bx bx-bookmark" },
{ id: "_lbToday", ...sharedLaunchers.openToday },
@ -90,7 +89,8 @@ export default function buildLaunchBarConfig() {
{ id: "_lbMobileBackInHistory", ...sharedLaunchers.backInHistory },
{ id: "_lbMobileForwardInHistory", ...sharedLaunchers.forwardInHistory },
{ id: "_lbMobileJumpTo", title: t("hidden-subtree.jump-to-note-title"), type: "launcher", command: "jumpToNote", icon: "bx bx-plus-circle" },
{ id: "_lbMobileCalendar", ...sharedLaunchers.calendar }
{ id: "_lbMobileCalendar", ...sharedLaunchers.calendar },
{ id: "_lbMobileRecentChanges", ...sharedLaunchers.recentChanges }
];
return {

View File

@ -2,6 +2,16 @@ import sanitizeHtml from "sanitize-html";
import sanitizeUrl from "@braintree/sanitize-url";
import optionService from "./options.js";
// Be consistent with `ALLOWED_PROTOCOLS` in `src\public\app\services\link.js`
// TODO: Deduplicate with client once we can.
export const ALLOWED_PROTOCOLS = [
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo',
'mid'
];
// Default list of allowed HTML tags
export const DEFAULT_ALLOWED_TAGS = [
"h1",
@ -138,56 +148,7 @@ function sanitize(dirtyHtml: string) {
"*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"],
input: ["type", "checked"]
},
// Be consistent with `allowedSchemes` in `src\public\app\services\link.js`
allowedSchemes: [
"http",
"https",
"ftp",
"ftps",
"mailto",
"data",
"evernote",
"file",
"facetime",
"gemini",
"git",
"gopher",
"imap",
"irc",
"irc6",
"jabber",
"jar",
"lastfm",
"ldap",
"ldaps",
"magnet",
"message",
"mumble",
"nfs",
"onenote",
"pop",
"rmi",
"s3",
"sftp",
"skype",
"sms",
"spotify",
"steam",
"svn",
"udp",
"view-source",
"vlc",
"vnc",
"ws",
"wss",
"xmpp",
"jdbc",
"slack",
"tel",
"smb",
"zotero",
"geo"
],
allowedSchemes: ALLOWED_PROTOCOLS,
nonTextTags: ["head"],
transformTags
});

View File

@ -143,7 +143,7 @@ function register(router: Router) {
addNoIndexHeader(note, res);
if (note.isLabelTruthy("shareRaw")) {
if (note.isLabelTruthy("shareRaw") || typeof req.query.raw !== "undefined") {
res.setHeader("Content-Type", note.mime).send(note.getContent());
return;

View File

@ -90,7 +90,9 @@
"force-save-revision": "Forzar la creación/guardado de una nueva revisión de nota de la nota activa",
"show-help": "Muestra ayuda/hoja de referencia integrada",
"toggle-book-properties": "Alternar propiedades del libro",
"toggle-classic-editor-toolbar": "Alternar la pestaña de formato por el editor con barra de herramientas fija"
"toggle-classic-editor-toolbar": "Alternar la pestaña de formato por el editor con barra de herramientas fija",
"export-as-pdf": "Exporta la nota actual como un PDF",
"show-cheatsheet": "Muestra un modal con operaciones de teclado comunes"
},
"login": {
"title": "Iniciar sesión",
@ -240,7 +242,8 @@
"sync-title": "Sincronizar",
"other": "Otros",
"advanced-title": "Avanzado",
"visible-launchers-title": "Lanzadores visibles"
"visible-launchers-title": "Lanzadores visibles",
"user-guide": "Guía de Usuario"
},
"notes": {
"new-note": "Nueva nota",
@ -250,5 +253,23 @@
"backend_log": {
"log-does-not-exist": "El archivo de registro del backend '{{fileName}}' no existe (aún).",
"reading-log-failed": "Leer el archivo de registro del backend '{{fileName}}' falló."
},
"content_renderer": {
"note-cannot-be-displayed": "Este tipo de nota no puede ser mostrado."
},
"pdf": {
"export_filter": "Documento PDF (*.pdf)",
"unable-to-export-message": "La nota actual no pudo ser exportada como PDF.",
"unable-to-export-title": "No es posible exportar como PDF",
"unable-to-save-message": "No se pudo escribir en el archivo seleccionado. Intente de nuevo o seleccione otro destino."
},
"tray": {
"tooltip": "TriliumNext Notes",
"close": "Cerrar Trilium",
"recents": "Notas recientes",
"bookmarks": "Marcadores",
"today": "Abrir nota del diario de hoy",
"new-note": "Nueva nota",
"show-windows": "Mostrar ventanas"
}
}

View File

@ -91,7 +91,8 @@
"zoom-in": "Mărește zoom-ul",
"zoom-out": "Micșorează zoom-ul",
"toggle-classic-editor-toolbar": "Comută tab-ul „Formatare” pentru editorul cu bară fixă",
"export-as-pdf": "Exportă notița curentă ca PDF"
"export-as-pdf": "Exportă notița curentă ca PDF",
"show-cheatsheet": "Afișează o fereastră cu scurtături de la tastatură comune"
},
"login": {
"button": "Autentifică",
@ -241,7 +242,8 @@
"sql-console-history-title": "Istoricul consolei SQL",
"go-to-next-note-title": "Mergi la notița următoare",
"launch-bar-templates-title": "Șabloane bară de lansare",
"visible-launchers-title": "Lansatoare vizibile"
"visible-launchers-title": "Lansatoare vizibile",
"user-guide": "Ghidul de utilizare"
},
"notes": {
"new-note": "Notiță nouă"