mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 22:44:25 +01:00
Merge remote-tracking branch 'origin/develop' into feat_erasure-timeout-ui
; Conflicts: ; src/public/translations/ro/translation.json
This commit is contained in:
commit
27b825e511
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -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/**"]
|
||||
}
|
||||
|
||||
@ -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
51
bin/generate-openapi.js
Normal 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
|
||||
*/
|
||||
@ -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=
|
||||
|
||||
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
2
libraries/ckeditor/ckeditor.js.map
vendored
2
libraries/ckeditor/ckeditor.js.map
vendored
File diff suppressed because one or more lines are too long
1
libraries/codemirror/hcl.js
vendored
1
libraries/codemirror/hcl.js
vendored
@ -198,6 +198,7 @@
|
||||
ext: [ "hcl " ],
|
||||
mime: "text/x-hcl",
|
||||
mode: "hcl",
|
||||
name: "Terraform (HCL)"
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
303
package-lock.json
generated
303
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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)`);
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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 ?? "");
|
||||
}
|
||||
}
|
||||
@ -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)) {
|
||||
|
||||
@ -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");
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
57
src/public/app/widgets/containers/scrolling_container.ts
Normal file
57
src/public/app/widgets/containers/scrolling_container.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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", () => {
|
||||
@ -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 {
|
||||
@ -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);
|
||||
@ -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() {
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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 ?? "");
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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 = "";
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
223
src/public/app/widgets/type_widgets/ckeditor/toolbars.ts
Normal file
223
src/public/app/widgets/type_widgets/ckeditor/toolbars.ts
Normal 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'
|
||||
]
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -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();
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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." }];
|
||||
|
||||
@ -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
27
src/routes/api_docs.ts
Normal 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
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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`
|
||||
})
|
||||
});
|
||||
|
||||
@ -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 || "",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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ă"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user