new settings tab

This commit is contained in:
Moritz Jung 2026-05-30 12:57:56 +02:00
parent c564c13d8a
commit 92a730ce3f
14 changed files with 824 additions and 348 deletions

View file

@ -5,24 +5,24 @@
"": {
"name": "obsidian-safe-js-plugin",
"dependencies": {
"ses": "^2.0.0",
"ses": "^2.1.0",
"zod": "^4.4.3",
},
"devDependencies": {
"@happy-dom/global-registrator": "^20.9.0",
"@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3",
"@types/bun": "^1.3.13",
"@types/bun": "^1.3.14",
"eslint": "^9.39.4",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-no-relative-import-paths": "^1.6.1",
"eslint-plugin-obsidianmd": "^0.2.9",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-only-warn": "^1.2.1",
"obsidian": "latest",
"obsidian": "obsidianmd/obsidian-api#2e88986ef3348e501d390afddcdfa08e132eb2f2",
"prettier": "^3.8.3",
"tslib": "^2.8.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"vite": "^8.0.12",
"typescript-eslint": "8.59.3",
"vite": "^8.0.14",
"vite-plugin-banner": "^0.8.1",
"vite-plugin-static-copy": "^4.1.0",
},
@ -89,41 +89,41 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="],
"@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="],
"@package-json/types": ["@package-json/types@0.0.12", "", {}, "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw=="],
"@pkgr/core": ["@pkgr/core@0.1.2", "", {}, "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="],
@ -131,7 +131,7 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="],
@ -249,7 +249,7 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
@ -345,7 +345,7 @@
"eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="],
"eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.2.9", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-+dF5Zz5T6/j0QYGu+wHbY3UZb45Kh+QFkFdfvkVk05o4YIIVqHMlrTFrlRVhuBd6Htu8QxcFOwzeMTN4aysVTA=="],
"eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.3.0", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-QvGDI6B2nxJBrsZKGTg31da2A/fEJNlnwN+fRZkaoPIu1QL3fYXUdpP7ThyMdr/0iTYQxifb9lt2X9cpydQx1w=="],
"eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="],
@ -589,7 +589,7 @@
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="],
"obsidian": ["obsidian@github:obsidianmd/obsidian-api#2e88986", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "obsidianmd-obsidian-api-2e88986", "sha512-miO210/4NYxwNHnoXgDdbskikOzTKBZV39i6nOOD9jokKfMeXbWoRUNEMJGwxyVDsTIvpUeHUjfZ3kDvRaob7A=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
@ -615,7 +615,7 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@ -645,7 +645,7 @@
"ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="],
"rolldown": ["rolldown@1.0.0", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@rolldown/pluginutils": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-x64-msvc": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA=="],
"rolldown": ["rolldown@1.0.2", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="],
"safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
@ -659,7 +659,7 @@
"semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"ses": ["ses@2.0.0", "", { "dependencies": { "@endo/cache-map": "^1.1.0", "@endo/env-options": "^1.1.11", "@endo/immutable-arraybuffer": "^1.1.2" } }, "sha512-Avcinq9dg/LS5W3YW+eRcpolfI0jXP4OsM/JJKCqyqHygUCqpNSsfn8YoNBmfyN2DkSMqjpbWvWPlfY1Jr4oXw=="],
"ses": ["ses@2.1.0", "", { "dependencies": { "@endo/cache-map": "^1.1.0", "@endo/env-options": "^1.1.11", "@endo/immutable-arraybuffer": "^1.1.2" } }, "sha512-mM3s60n24C42W5Q14vTBM9h9l2lfcUEtvoFSFkHSF8hMjMSDjLFObn8COjNbDfsRRS2gePkMves3GR2LPAEMKQ=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
@ -747,7 +747,7 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@8.0.12", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg=="],
"vite": ["vite@8.0.14", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
"vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="],
@ -815,6 +815,8 @@
"eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="],
"eslint-plugin-obsidianmd/obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="],
"eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],
"eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@ -849,6 +851,8 @@
"eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"eslint-plugin-obsidianmd/obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="],
"json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],

View file

@ -2,7 +2,7 @@
"id": "safe-js",
"name": "Safe JS",
"version": "0.1.3",
"minAppVersion": "1.12.3",
"minAppVersion": "1.13.0",
"description": "Run JavaScript through sandboxed Web Workers and a limited RPC surface.",
"author": "Moritz Jung",
"authorUrl": "https://www.moritzjung.dev/",

View file

@ -2,7 +2,7 @@
"id": "safe-js",
"name": "Safe JS",
"version": "0.1.3",
"minAppVersion": "1.12.3",
"minAppVersion": "1.13.0",
"description": "Run JavaScript through sandboxed Web Workers and a limited RPC surface.",
"author": "Moritz Jung",
"authorUrl": "https://www.moritzjung.dev/",

View file

@ -26,23 +26,23 @@
"devDependencies": {
"@happy-dom/global-registrator": "^20.9.0",
"@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3",
"@types/bun": "^1.3.13",
"@types/bun": "^1.3.14",
"eslint": "^9.39.4",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-no-relative-import-paths": "^1.6.1",
"eslint-plugin-obsidianmd": "^0.2.9",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-only-warn": "^1.2.1",
"obsidian": "latest",
"obsidian": "obsidianmd/obsidian-api#2e88986ef3348e501d390afddcdfa08e132eb2f2",
"prettier": "^3.8.3",
"tslib": "^2.8.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"vite": "^8.0.12",
"typescript-eslint": "8.59.3",
"vite": "^8.0.14",
"vite-plugin-banner": "^0.8.1",
"vite-plugin-static-copy": "^4.1.0"
},
"dependencies": {
"ses": "^2.0.0",
"ses": "^2.1.0",
"zod": "^4.4.3"
}
}

View file

@ -20,7 +20,7 @@ export default class SafeJsPlugin extends Plugin {
api!: SafeJsPublicApi;
private executionService!: SafeJsExecutionService;
private scriptManager!: VaultScriptManager;
settings!: SafeJsSettings;
declare settings: SafeJsSettings;
async onload(): Promise<void> {
await this.loadSettings();

View file

@ -1,6 +1,6 @@
import type { App } from 'obsidian';
import type { TextComponent } from 'obsidian';
import { Modal, Notice, Setting } from 'obsidian';
import { Modal, Notice, SettingGroup } from 'obsidian';
import { isJavaScriptVaultScriptPath } from 'packages/obsidian/src/scripts/script-settings';
export interface AddVaultScriptModalValues {
@ -43,55 +43,64 @@ export class AddVaultScriptModal extends Modal {
contentEl.empty();
contentEl.createEl('h2', { text: this.title });
let pathInput: TextComponent | undefined;
const group = new SettingGroup(contentEl);
new Setting(contentEl)
.setName('Vault path')
.setDesc('Use a vault-relative path that ends in .js.')
.addText(text => {
pathInput = text;
return text
.setPlaceholder('Scripts/example.js')
.setValue(this.path)
.onChange(value => {
this.path = value;
});
});
new Setting(contentEl)
.setName('Command name')
.setDesc('Leave blank to use the file name.')
.addText(text =>
text
.setPlaceholder('Example script')
.setValue(this.name)
.onChange(value => {
this.name = value;
}),
);
new Setting(contentEl)
.setName('Run on startup')
.setDesc('Run this script when Obsidian finishes loading.')
.addToggle(toggle =>
toggle.setValue(this.runOnStartup).onChange(value => {
this.runOnStartup = value;
group.addSetting(setting =>
void setting
.setName('Vault path')
.setDesc('Use a vault-relative path that ends in .js.')
.addText(text => {
pathInput = text;
return text
.setPlaceholder('Scripts/example.js')
.setValue(this.path)
.onChange(value => {
this.path = value;
});
}),
);
);
new Setting(contentEl)
.addButton(button =>
button.setButtonText('Cancel').onClick(() => {
this.close();
}),
)
.addButton(button =>
button
.setButtonText(this.actionText)
.setCta()
.onClick(() => {
void this.submit();
group.addSetting(setting =>
void setting
.setName('Command name')
.setDesc('Leave blank to use the file name.')
.addText(text =>
text
.setPlaceholder('Example script')
.setValue(this.name)
.onChange(value => {
this.name = value;
}),
),
);
group.addSetting(setting =>
void setting
.setName('Run on startup')
.setDesc('Run this script when Obsidian finishes loading.')
.addToggle(toggle =>
toggle.setValue(this.runOnStartup).onChange(value => {
this.runOnStartup = value;
}),
);
),
);
group.addSetting(setting =>
void setting
.addButton(button =>
button.setButtonText('Cancel').onClick(() => {
this.close();
}),
)
.addButton(button =>
button
.setButtonText(this.actionText)
.setCta()
.onClick(() => {
void this.submit();
}),
),
);
pathInput?.inputEl.focus();
}

View file

@ -1,6 +1,6 @@
import type { PermissionId } from '@lemons_dev/obsidian-safe-js-api';
import type { App } from 'obsidian';
import { Modal, Setting } from 'obsidian';
import { Modal, SettingGroup } from 'obsidian';
import type { PermissionPrompt, PermissionPromptRequest } from 'packages/obsidian/src/execution/execution-service';
import { RICH_OUTPUT_PERMISSION } from 'packages/obsidian/src/permissions/permissions';
import type { RpcRegistry } from 'packages/obsidian/src/rpc/rpc-registry';
@ -21,17 +21,16 @@ export class ObsidianPermissionPrompt implements PermissionPrompt {
return;
}
let modal: PermissionApprovalModal;
const modal = new PermissionApprovalModal(this.app, this.rpcRegistry, request, approved => {
cleanup();
resolve(approved);
});
const abortListener = (): void => {
modal.close();
};
const cleanup = (): void => {
request.signal?.removeEventListener('abort', abortListener);
};
modal = new PermissionApprovalModal(this.app, this.rpcRegistry, request, approved => {
cleanup();
resolve(approved);
});
request.signal?.addEventListener('abort', abortListener, { once: true });
modal.open();
});
@ -88,20 +87,23 @@ class PermissionApprovalModal extends Modal {
this.renderPermission(list, permission);
}
new Setting(contentEl)
.addButton(button =>
button.setButtonText('Cancel').onClick(() => {
this.resolve(false);
}),
)
.addButton(button =>
button
.setButtonText('Allow')
.setCta()
.onClick(() => {
this.resolve(true);
const group = new SettingGroup(contentEl);
group.addSetting(setting =>
void setting
.addButton(button =>
button.setButtonText('Cancel').onClick(() => {
this.resolve(false);
}),
);
)
.addButton(button =>
button
.setButtonText('Allow')
.setCta()
.onClick(() => {
this.resolve(true);
}),
),
);
}
onClose(): void {

View file

@ -1,4 +1,5 @@
import type { App } from 'obsidian';
import type { SettingDefinitionItem } from 'obsidian';
import { PluginSettingTab } from 'obsidian';
import type SafeJsPlugin from 'packages/obsidian/src/main';
import type { PermissionSettingsStore } from 'packages/obsidian/src/permissions/approval-store';
@ -15,7 +16,7 @@ import { VaultScriptsSettingsSection } from 'packages/obsidian/src/ui/settings/v
export class SafeJsSettingTab extends PluginSettingTab {
plugin: SafeJsPlugin;
private readonly approvalStore: LocalStoragePermissionApprovalStore;
private readonly permissionSettingsStore: PermissionSettingsStore;
private readonly runtimeSettingsSection: RuntimeSettingsSection;
private readonly scriptManager?: VaultScriptManager;
constructor(
@ -27,27 +28,42 @@ export class SafeJsSettingTab extends PluginSettingTab {
super(app, plugin);
this.plugin = plugin;
this.approvalStore = new LocalStoragePermissionApprovalStore(new AppPermissionStorage(app));
this.permissionSettingsStore = permissionSettingsStore;
this.runtimeSettingsSection = new RuntimeSettingsSection(plugin, permissionSettingsStore, () => {
this.update();
});
this.scriptManager = scriptManager;
}
getSettingDefinitions(): SettingDefinitionItem[] {
return [
...this.runtimeSettingsSection.getSettingDefinitions(),
...new VaultScriptsSettingsSection(
this.app,
this.plugin,
() => {
this.update();
},
this.scriptManager,
).getSettingDefinitions(),
...new StoredDataSettingsSection(this.app, this.approvalStore, () => {
this.update();
}).getSettingDefinitions(),
];
}
getControlValue(key: string): unknown {
return this.runtimeSettingsSection.getControlValue(key);
}
async setControlValue(key: string, value: unknown): Promise<void> {
await this.runtimeSettingsSection.setControlValue(key, value);
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new RuntimeSettingsSection(this.plugin, this.permissionSettingsStore, () => {
this.display();
}).render(containerEl);
new VaultScriptsSettingsSection(
this.app,
this.plugin,
() => {
this.display();
},
this.scriptManager,
).render(containerEl);
new StoredDataSettingsSection(this.app, this.approvalStore, () => {
this.display();
}).render(containerEl);
containerEl.createEl('p', {
text: 'Safe JS settings require Obsidian 1.13.0 or newer. This app appears to be running an older version.',
});
}
}

View file

@ -0,0 +1,251 @@
import { Notice, SettingGroup, SettingPage } from 'obsidian';
import type { PermissionApproval } from 'packages/obsidian/src/permissions/approval-store';
import type { LocalStoragePermissionApprovalStore } from 'packages/obsidian/src/permissions/approval-store';
import {
ALL_FILTER_VALUE,
formatCount,
formatDate,
groupBySorted,
renderHiddenSettingItemCount,
visibleSettingItems,
} from 'packages/obsidian/src/ui/settings/setting-page-utils';
export class ApprovedHashesSettingPage extends SettingPage {
private permissionFilter = ALL_FILTER_VALUE;
private readonly approvalStore: LocalStoragePermissionApprovalStore;
private readonly onStorageChanged: () => void;
private searchQuery = '';
private sourceFilter = ALL_FILTER_VALUE;
private readonly staleEntryAgeMs: number;
constructor(approvalStore: LocalStoragePermissionApprovalStore, staleEntryAgeMs: number, onStorageChanged: () => void) {
super();
this.approvalStore = approvalStore;
this.onStorageChanged = onStorageChanged;
this.staleEntryAgeMs = staleEntryAgeMs;
this.title = 'Approved script hashes';
}
display(): void {
this.containerEl.empty();
const approvals = this.approvalStore.list();
const filteredApprovals = this.filterApprovals(approvals);
this.renderApprovalActions(approvals);
this.renderApprovalFilters(approvals);
this.renderApprovalGroups(filteredApprovals);
}
private clearOldApprovals(): void {
const deletedCount = this.approvalStore.deleteOlderThan(Date.now() - this.staleEntryAgeMs);
new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`);
this.refreshSettings();
}
private clearAllApprovals(): void {
const deletedCount = this.approvalStore.deleteAll();
new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`);
this.refreshSettings();
}
private revokeApproval(approval: PermissionApproval | undefined): void {
if (approval === undefined) {
return;
}
this.approvalStore.delete(approval);
new Notice(`Revoked approval for script hash ${approval.codeHash}.`);
this.refreshSettings();
}
private revokeApprovalGroup(approvals: PermissionApproval[], source: string): void {
let deletedCount = 0;
for (const approval of approvals) {
if (this.approvalStore.delete(approval)) {
deletedCount += 1;
}
}
new Notice(`Revoked ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'} for ${formatSource(source)}.`);
this.refreshSettings();
}
private renderApprovalActions(approvals: PermissionApproval[]): void {
const group = new SettingGroup(this.containerEl);
group.addSetting(setting =>
void setting
.setName('Clear approved hashes')
.setDesc(`${formatCount(approvals.length, 'stored approval hash', 'stored approval hashes')}.`)
.addButton(button =>
button.setButtonText('Clear older than 30 days').onClick(() => {
this.clearOldApprovals();
}),
)
.addButton(button =>
button
.setButtonText('Clear all')
.setDestructive()
.onClick(() => {
this.clearAllApprovals();
}),
),
);
}
private renderApprovalFilters(approvals: PermissionApproval[]): void {
const group = new SettingGroup(this.containerEl);
group.addSetting(
setting =>
void setting
.setName('Search')
.setDesc('Filter by hash, source, or permission.')
.addSearch(search =>
search.setValue(this.searchQuery).onChange(value => {
this.searchQuery = value;
this.display();
}),
),
);
group.addSetting(
setting =>
void setting
.setName('Filters')
.addDropdown(dropdown =>
dropdown
.addOptions(this.getPermissionFilterOptions(approvals))
.setValue(this.permissionFilter)
.onChange(value => {
this.permissionFilter = value;
this.display();
}),
)
.addDropdown(dropdown =>
dropdown
.addOptions(this.getSourceFilterOptions(approvals))
.setValue(this.sourceFilter)
.onChange(value => {
this.sourceFilter = value;
this.display();
}),
)
.addButton(button =>
button.setButtonText('Clear filters').onClick(() => {
this.permissionFilter = ALL_FILTER_VALUE;
this.sourceFilter = ALL_FILTER_VALUE;
this.searchQuery = '';
this.display();
}),
),
);
}
private renderApprovalGroups(approvals: PermissionApproval[]): void {
if (approvals.length === 0) {
const group = new SettingGroup(this.containerEl);
group.addSetting(setting => void setting.setName('No approved script hashes match the current filters.'));
return;
}
for (const [source, sourceApprovals] of groupApprovalsBySource(approvals)) {
const group = new SettingGroup(this.containerEl).setHeading(formatSource(source));
group.addSetting(setting =>
void setting
.setName('Revoke group')
.setDesc(formatCount(sourceApprovals.length, 'approval hash'))
.addButton(button =>
button
.setButtonText('Revoke group')
.setDestructive()
.onClick(() => {
this.revokeApprovalGroup(sourceApprovals, source);
}),
),
);
for (const approval of visibleSettingItems(sourceApprovals)) {
group.addSetting(
setting =>
void setting
.setName(approval.codeHash)
.setDesc(`${approval.permissions.join(', ')} - ${formatDate(approval.updatedAt)}`)
.addButton(button =>
button.setButtonText('Revoke').onClick(() => {
this.revokeApproval(approval);
}),
),
);
}
renderHiddenSettingItemCount(group, sourceApprovals.length - visibleSettingItems(sourceApprovals).length, 'approved hashes');
}
}
private filterApprovals(approvals: PermissionApproval[]): PermissionApproval[] {
const normalizedQuery = this.searchQuery.trim().toLowerCase();
return approvals.filter(approval => {
if (this.permissionFilter !== ALL_FILTER_VALUE && !approval.permissions.some(permission => permission === this.permissionFilter)) {
return false;
}
if (this.sourceFilter !== ALL_FILTER_VALUE && sourceFilterValue(approval) !== this.sourceFilter) {
return false;
}
if (normalizedQuery === '') {
return true;
}
return approvalSearchText(approval).includes(normalizedQuery);
});
}
private getPermissionFilterOptions(approvals: PermissionApproval[]): Record<string, string> {
const options: Record<string, string> = { [ALL_FILTER_VALUE]: 'All permissions' };
for (const permission of [...new Set(approvals.flatMap(approval => approval.permissions))].sort()) {
options[permission] = permission;
}
return options;
}
private getSourceFilterOptions(approvals: PermissionApproval[]): Record<string, string> {
const options: Record<string, string> = { [ALL_FILTER_VALUE]: 'All sources' };
for (const approval of approvals) {
options[sourceFilterValue(approval)] = formatSource(sourceFilterValue(approval));
}
return options;
}
private refreshSettings(): void {
this.onStorageChanged();
this.display();
}
}
function approvalSearchText(approval: PermissionApproval): string {
return `${approval.codeHash} ${formatSource(sourceFilterValue(approval))} ${approval.permissions.join(' ')}`.toLowerCase();
}
function groupApprovalsBySource(approvals: PermissionApproval[]): [string, PermissionApproval[]][] {
return groupBySorted(
approvals,
approval => sourceFilterValue(approval),
(left, right) => formatSource(left).localeCompare(formatSource(right)),
);
}
function sourceFilterValue(approval: PermissionApproval): string {
return approval.callerPluginId === undefined ? 'source:notes' : `plugin:${approval.callerPluginId}`;
}
function formatSource(source: string): string {
return source === 'source:notes' ? 'notes' : source.slice('plugin:'.length);
}

View file

@ -1,9 +1,13 @@
import { Setting } from 'obsidian';
import type { SettingDefinitionItem } from 'obsidian';
import type SafeJsPlugin from 'packages/obsidian/src/main';
import type { PermissionSettingsStore } from 'packages/obsidian/src/permissions/approval-store';
import type { SafeJsSettings } from 'packages/obsidian/src/settings/settings-schema';
import { DEFAULT_SETTINGS } from 'packages/obsidian/src/settings/settings-schema';
export type RuntimeSettingsKey = keyof Pick<SafeJsSettings, 'debugBlocksEnabled' | 'executionTimeoutMs' | 'executionTimeoutsEnabled'>;
export const AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY = 'autoAllowLowRiskPermissions';
export type RuntimeControlKey = RuntimeSettingsKey | typeof AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY;
export class RuntimeSettingsSection {
private readonly onSettingsChanged: () => void;
private readonly permissionSettingsStore: PermissionSettingsStore;
@ -15,54 +19,75 @@ export class RuntimeSettingsSection {
this.plugin = plugin;
}
render(containerEl: HTMLElement): void {
this.createSection(containerEl, 'Runtime settings');
new Setting(containerEl)
.setName('Execution timeouts')
.setDesc('Cancel scripts that run longer than the configured timeout.')
.addToggle(toggle =>
toggle.setValue(this.plugin.settings.executionTimeoutsEnabled).onChange(async value => {
this.plugin.settings.executionTimeoutsEnabled = value;
await this.plugin.saveSettings();
this.onSettingsChanged();
}),
);
new Setting(containerEl)
.setName('Execution timeout')
.setDesc(this.executionTimeoutDescription(this.plugin.settings))
.addText(text =>
text
.setPlaceholder(String(DEFAULT_SETTINGS.executionTimeoutMs))
.setValue(String(this.plugin.settings.executionTimeoutMs))
.onChange(async value => {
this.plugin.settings.executionTimeoutMs = this.normalizeTimeoutMs(value);
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Debug blocks')
.setDesc('Enable support for the safe-js-debug code block language.')
.addToggle(toggle =>
toggle.setValue(this.plugin.settings.debugBlocksEnabled).onChange(async value => {
this.plugin.settings.debugBlocksEnabled = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Auto-allow low-risk permissions')
.setDesc('Allow low-risk permissions without prompting. Approvals are still remembered per script hash on this device.')
.addToggle(toggle =>
toggle.setValue(this.permissionSettingsStore.loadAutoAllowLowRiskPermissions()).onChange(value => {
this.permissionSettingsStore.saveAutoAllowLowRiskPermissions(value);
}),
);
getSettingDefinitions(): SettingDefinitionItem<RuntimeControlKey>[] {
return [
{
type: 'group',
items: [
{
name: 'Execution timeouts',
desc: 'Cancel scripts that run longer than the configured timeout.',
control: { type: 'toggle', key: 'executionTimeoutsEnabled' },
},
{
name: 'Execution timeout',
desc: this.executionTimeoutDescription(this.plugin.settings),
control: {
type: 'number',
key: 'executionTimeoutMs',
min: 1,
placeholder: String(DEFAULT_SETTINGS.executionTimeoutMs),
validate: value => (this.isValidTimeoutMs(value) ? undefined : 'Enter a positive timeout in milliseconds.'),
},
},
{
name: 'Debug blocks',
desc: 'Enable support for the safe-js-debug code block language.',
control: { type: 'toggle', key: 'debugBlocksEnabled' },
},
{
name: 'Auto-allow low-risk permissions',
desc: 'Allow low-risk permissions without prompting. Approvals are still remembered per script hash on this device.',
control: { type: 'toggle', key: AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY },
},
],
},
];
}
private createSection(containerEl: HTMLElement, heading: string): void {
new Setting(containerEl).setName(heading).setHeading();
getControlValue(key: string): unknown {
if (!isRuntimeControlKey(key)) {
return undefined;
}
if (key === AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY) {
return this.permissionSettingsStore.loadAutoAllowLowRiskPermissions();
}
return this.plugin.settings[key];
}
async setControlValue(key: string, value: unknown): Promise<void> {
if (!isRuntimeControlKey(key)) {
return;
}
if (key === AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY) {
this.permissionSettingsStore.saveAutoAllowLowRiskPermissions(value === true);
return;
}
if (key === 'executionTimeoutsEnabled') {
await this.saveExecutionTimeoutsEnabled(value);
return;
}
if (key === 'debugBlocksEnabled') {
await this.saveDebugBlocksEnabled(value);
return;
}
await this.saveExecutionTimeoutMs(value);
}
private executionTimeoutDescription(settings: SafeJsSettings): string {
@ -71,8 +96,39 @@ export class RuntimeSettingsSection {
: 'Timeouts are disabled. This value is kept for later use.';
}
private normalizeTimeoutMs(value: string): number {
const parsedValue = Number.parseInt(value, 10);
return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : DEFAULT_SETTINGS.executionTimeoutMs;
private isValidTimeoutMs(value: number): boolean {
return Number.isFinite(value) && value > 0;
}
private async saveDebugBlocksEnabled(value: unknown): Promise<void> {
if (typeof value !== 'boolean') {
return;
}
this.plugin.settings.debugBlocksEnabled = value;
await this.plugin.saveSettings();
}
private async saveExecutionTimeoutMs(value: unknown): Promise<void> {
if (typeof value !== 'number' || !this.isValidTimeoutMs(value)) {
return;
}
this.plugin.settings.executionTimeoutMs = value;
await this.plugin.saveSettings();
}
private async saveExecutionTimeoutsEnabled(value: unknown): Promise<void> {
if (typeof value !== 'boolean') {
return;
}
this.plugin.settings.executionTimeoutsEnabled = value;
await this.plugin.saveSettings();
this.onSettingsChanged();
}
}
function isRuntimeControlKey(key: string): key is RuntimeControlKey {
return key === AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY || key === 'debugBlocksEnabled' || key === 'executionTimeoutMs' || key === 'executionTimeoutsEnabled';
}

View file

@ -0,0 +1,208 @@
import type { App } from 'obsidian';
import { Notice, SettingGroup, SettingPage } from 'obsidian';
import type { ScriptStorageEntry } from 'packages/obsidian/src/storage/script-storage';
import { ScriptStorageManager } from 'packages/obsidian/src/storage/script-storage';
import {
ALL_FILTER_VALUE,
formatBytes,
formatCount,
formatDate,
groupBySorted,
renderHiddenSettingItemCount,
visibleSettingItems,
} from 'packages/obsidian/src/ui/settings/setting-page-utils';
export class ScriptStorageSettingPage extends SettingPage {
private readonly app: App;
private readonly onStorageChanged: () => void;
private scopeFilter = ALL_FILTER_VALUE;
private searchQuery = '';
private readonly staleEntryAgeMs: number;
constructor(app: App, staleEntryAgeMs: number, onStorageChanged: () => void) {
super();
this.app = app;
this.onStorageChanged = onStorageChanged;
this.staleEntryAgeMs = staleEntryAgeMs;
this.title = 'Script storage';
}
display(): void {
this.containerEl.empty();
const entries = ScriptStorageManager.listAll(this.app);
const filteredEntries = this.filterEntries(entries);
this.renderScriptStorageActions(entries);
this.renderScriptStorageFilters(entries);
this.renderScriptStorageGroups(filteredEntries);
}
private clearOldScriptStorage(): void {
const deletedCount = ScriptStorageManager.deleteOlderThanAll(this.app, Date.now() - this.staleEntryAgeMs);
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`);
this.refreshSettings();
}
private clearAllScriptStorage(): void {
const deletedCount = ScriptStorageManager.deleteAllKnown(this.app);
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`);
this.refreshSettings();
}
private clearScriptStorageScope(scope: string | null): void {
const deletedCount = new ScriptStorageManager(this.app, Date.now, scope).deleteAll();
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'} from ${formatScope(scope)}.`);
this.refreshSettings();
}
private deleteScriptStorageEntry(entry: ScriptStorageEntry): void {
new ScriptStorageManager(this.app, Date.now, entry.scope).delete(entry.key);
new Notice(`Deleted script storage key ${entry.key}.`);
this.refreshSettings();
}
private renderScriptStorageActions(entries: ScriptStorageEntry[]): void {
const group = new SettingGroup(this.containerEl);
group.addSetting(setting =>
void setting
.setName('Clear script storage')
.setDesc(`${formatCount(entries.length, 'indexed storage key')}.`)
.addButton(button =>
button.setButtonText('Clear older than 30 days').onClick(() => {
this.clearOldScriptStorage();
}),
)
.addButton(button =>
button
.setButtonText('Clear all')
.setDestructive()
.onClick(() => {
this.clearAllScriptStorage();
}),
),
);
}
private renderScriptStorageFilters(entries: ScriptStorageEntry[]): void {
const group = new SettingGroup(this.containerEl);
group.addSetting(setting => void setting
.setName('Search')
.setDesc('Filter by key or scope.')
.addSearch(search =>
search.setValue(this.searchQuery).onChange(value => {
this.searchQuery = value;
this.display();
}),
));
group.addSetting(setting => void setting
.setName('Filters')
.addDropdown(dropdown =>
dropdown
.addOptions(this.getScopeFilterOptions(entries))
.setValue(this.scopeFilter)
.onChange(value => {
this.scopeFilter = value;
this.display();
}),
)
.addButton(button =>
button.setButtonText('Clear filters').onClick(() => {
this.scopeFilter = ALL_FILTER_VALUE;
this.searchQuery = '';
this.display();
}),
));
}
private renderScriptStorageGroups(entries: ScriptStorageEntry[]): void {
if (entries.length === 0) {
const group = new SettingGroup(this.containerEl);
group.addSetting(setting => void setting.setName('No indexed script storage keys match the current filters.'));
return;
}
for (const [scope, scopeEntries] of groupEntriesByScope(entries)) {
const group = new SettingGroup(this.containerEl).setHeading(formatScope(scope));
group.addSetting(setting =>
void setting
.setName('Clear scope')
.setDesc(`${formatCount(scopeEntries.length, 'key')} - ${formatBytes(sumEntryBytes(scopeEntries))}`)
.addButton(button =>
button
.setButtonText('Clear scope')
.setDestructive()
.onClick(() => {
this.clearScriptStorageScope(scope);
}),
),
);
for (const entry of visibleSettingItems(scopeEntries)) {
group.addSetting(setting => void setting
.setName(entry.key)
.setDesc(`${formatBytes(entry.sizeBytes)} - ${formatDate(entry.updatedAt)}`)
.addButton(button =>
button.setButtonText('Delete').onClick(() => {
this.deleteScriptStorageEntry(entry);
}),
));
}
renderHiddenSettingItemCount(group, scopeEntries.length - visibleSettingItems(scopeEntries).length, 'storage keys');
}
}
private filterEntries(entries: ScriptStorageEntry[]): ScriptStorageEntry[] {
const normalizedQuery = this.searchQuery.trim().toLowerCase();
return entries.filter(entry => {
if (this.scopeFilter !== ALL_FILTER_VALUE && scopeFilterValue(entry.scope) !== this.scopeFilter) {
return false;
}
if (normalizedQuery === '') {
return true;
}
return `${entry.key} ${formatScope(entry.scope)}`.toLowerCase().includes(normalizedQuery);
});
}
private getScopeFilterOptions(entries: ScriptStorageEntry[]): Record<string, string> {
const options: Record<string, string> = { [ALL_FILTER_VALUE]: 'All scopes' };
for (const entry of entries) {
options[scopeFilterValue(entry.scope)] = formatScope(entry.scope);
}
return options;
}
private refreshSettings(): void {
this.onStorageChanged();
this.display();
}
}
function groupEntriesByScope(entries: ScriptStorageEntry[]): [string | null, ScriptStorageEntry[]][] {
return groupBySorted(
entries,
entry => entry.scope,
(left, right) => formatScope(left).localeCompare(formatScope(right)),
);
}
function scopeFilterValue(scope: string | null): string {
return scope === null ? 'global' : `scope:${scope}`;
}
function sumEntryBytes(entries: ScriptStorageEntry[]): number {
return entries.reduce((total, entry) => total + entry.sizeBytes, 0);
}
function formatScope(scope: string | null): string {
return scope === null ? 'global' : `scoped ${scope.slice(0, 12)}`;
}

View file

@ -0,0 +1,49 @@
import type { SettingGroup } from 'obsidian';
export const ALL_FILTER_VALUE = '__all__';
export const DEFAULT_VISIBLE_SETTING_ITEMS = 20;
export function formatBytes(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
}
return `${(bytes / 1024).toFixed(1)} KB`;
}
export function formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleString();
}
export function formatCount(count: number, singularLabel: string, pluralLabel: string = `${singularLabel}s`): string {
return `${count} ${count === 1 ? singularLabel : pluralLabel}`;
}
export function groupBySorted<TItem, TKey>(items: TItem[], getKey: (item: TItem) => TKey, compareKeys: (left: TKey, right: TKey) => number): [TKey, TItem[]][] {
const groupedItems = new Map<TKey, TItem[]>();
for (const item of items) {
const key = getKey(item);
const group = groupedItems.get(key);
if (group === undefined) {
groupedItems.set(key, [item]);
} else {
group.push(item);
}
}
return [...groupedItems.entries()].sort(([left], [right]) => compareKeys(left, right));
}
export function visibleSettingItems<TItem>(items: TItem[], limit: number = DEFAULT_VISIBLE_SETTING_ITEMS): TItem[] {
return items.slice(0, limit);
}
export function renderHiddenSettingItemCount(group: SettingGroup, hiddenCount: number, itemLabel: string): void {
if (hiddenCount <= 0) {
return;
}
group.addSetting(setting => void setting.setName(`${hiddenCount} more ${itemLabel} ${hiddenCount === 1 ? 'is' : 'are'} hidden.`));
}

View file

@ -1,9 +1,9 @@
import type { App } from 'obsidian';
import { Notice, Setting, SettingGroup } from 'obsidian';
import type { PermissionApproval } from 'packages/obsidian/src/permissions/approval-store';
import type { SettingDefinitionItem } from 'obsidian';
import type { LocalStoragePermissionApprovalStore } from 'packages/obsidian/src/permissions/approval-store';
import type { ScriptStorageEntry } from 'packages/obsidian/src/storage/script-storage';
import { ScriptStorageManager } from 'packages/obsidian/src/storage/script-storage';
import { ApprovedHashesSettingPage } from 'packages/obsidian/src/ui/settings/approved-hashes-settings-page';
import { ScriptStorageSettingPage } from 'packages/obsidian/src/ui/settings/script-storage-settings-page';
export class StoredDataSettingsSection {
private readonly app: App;
@ -17,133 +17,23 @@ export class StoredDataSettingsSection {
this.onStorageChanged = onStorageChanged;
}
render(containerEl: HTMLElement): void {
this.renderApprovalStorage(containerEl);
this.renderScriptStorage(containerEl);
}
private renderApprovalStorage(containerEl: HTMLElement): void {
getSettingDefinitions(): SettingDefinitionItem[] {
const approvals = this.approvalStore.list();
this.createSection(containerEl, 'Approved script hashes');
new Setting(containerEl)
.setName('Clear approved hashes')
.setDesc('Remove remembered permission approvals for changed or old scripts.')
.addButton(button =>
button.setButtonText('Clear older than 30 days').onClick(() => {
const deletedCount = this.approvalStore.deleteOlderThan(Date.now() - this.staleEntryAgeMs);
new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`);
this.onStorageChanged();
}),
)
.addButton(button =>
button.setButtonText('Clear all').onClick(() => {
const deletedCount = this.approvalStore.deleteAll();
new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`);
this.onStorageChanged();
}),
);
this.renderApprovalList(new SettingGroup(containerEl), approvals);
}
private renderApprovalList(group: SettingGroup, approvals: PermissionApproval[]): void {
if (approvals.length === 0) {
group.addSetting(setting => {
setting.setName('No approved script hashes are stored.');
});
return;
}
for (const approval of approvals.slice(0, 20)) {
group.addSetting(setting => {
setting
.setName(approval.codeHash)
.setDesc(`${formatCaller(approval.callerPluginId)} - ${approval.permissions.join(', ')} - ${formatDate(approval.updatedAt)}`)
.addButton(button =>
button.setButtonText('Revoke').onClick(() => {
this.approvalStore.delete(approval);
new Notice(`Revoked approval for script hash ${approval.codeHash}.`);
this.onStorageChanged();
}),
);
});
}
if (approvals.length > 20) {
group.addSetting(setting => {
setting.setName(`${approvals.length - 20} more approved hashes are hidden.`);
});
}
}
private renderScriptStorage(containerEl: HTMLElement): void {
const entries = ScriptStorageManager.listAll(this.app);
this.createSection(containerEl, 'Script storage');
new Setting(containerEl)
.setName('Clear script storage')
.setDesc('Remove data written through the storage API by scripts.')
.addButton(button =>
button.setButtonText('Clear older than 30 days').onClick(() => {
const deletedCount = ScriptStorageManager.deleteOlderThanAll(this.app, Date.now() - this.staleEntryAgeMs);
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`);
this.onStorageChanged();
}),
)
.addButton(button =>
button.setButtonText('Clear all').onClick(() => {
const deletedCount = ScriptStorageManager.deleteAllKnown(this.app);
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`);
this.onStorageChanged();
}),
);
this.renderScriptStorageList(new SettingGroup(containerEl), entries);
}
private renderScriptStorageList(group: SettingGroup, entries: ScriptStorageEntry[]): void {
if (entries.length === 0) {
group.addSetting(setting => {
setting.setName('No indexed script storage keys are stored.');
});
return;
}
for (const entry of entries.slice(0, 20)) {
group.addSetting(setting => {
setting.setName(entry.key).setDesc(`${formatScope(entry.scope)} - ${formatBytes(entry.sizeBytes)} - ${formatDate(entry.updatedAt)}`);
});
}
if (entries.length > 20) {
group.addSetting(setting => {
setting.setName(`${entries.length - 20} more storage keys are hidden.`);
});
}
}
private createSection(containerEl: HTMLElement, heading: string): void {
new Setting(containerEl).setName(heading).setHeading();
return [
{
type: 'page',
name: 'Approved script hashes',
desc: `${approvals.length} stored`,
page: () => new ApprovedHashesSettingPage(this.approvalStore, this.staleEntryAgeMs, this.onStorageChanged),
},
{
type: 'page',
name: 'Script storage',
desc: `${entries.length} indexed`,
page: () => new ScriptStorageSettingPage(this.app, this.staleEntryAgeMs, this.onStorageChanged),
},
];
}
}
function formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleString();
}
function formatBytes(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
}
return `${(bytes / 1024).toFixed(1)} KB`;
}
function formatCaller(callerPluginId: string | undefined): string {
return callerPluginId ?? 'notes';
}
function formatScope(scope: string | null): string {
return scope === null ? 'global' : `scoped ${scope.slice(0, 12)}`;
}

View file

@ -1,5 +1,5 @@
import type { App } from 'obsidian';
import { Setting, SettingGroup } from 'obsidian';
import type { SettingDefinitionItem } from 'obsidian';
import type SafeJsPlugin from 'packages/obsidian/src/main';
import type { SafeJsScriptConfig } from 'packages/obsidian/src/scripts/script-settings';
import { createScriptConfig, displayNameFromPath, isJavaScriptVaultScriptPath } from 'packages/obsidian/src/scripts/script-settings';
@ -20,49 +20,40 @@ export class VaultScriptsSettingsSection {
this.scriptManager = scriptManager;
}
render(containerEl: HTMLElement): void {
this.createSection(containerEl, 'Vault scripts');
const group = new SettingGroup(containerEl);
for (const script of this.plugin.settings.scripts) {
this.renderVaultScript(group, script);
}
this.renderAddVaultScript(group);
}
private renderVaultScript(group: SettingGroup, script: SafeJsScriptConfig): void {
group.addSetting(setting => {
setting
.setName(script.name)
.setDesc(formatVaultScriptDescription(script))
.addButton(button =>
button.setButtonText('Edit').onClick(() => {
this.openEditVaultScriptModal(script);
}),
)
.addButton(button =>
button.setButtonText('Remove').onClick(() => {
void this.removeVaultScript(script);
}),
);
});
}
private renderAddVaultScript(group: SettingGroup): void {
group.addSetting(setting => {
setting
.setName('Add script')
.setDesc('Configure a vault .js file as a command.')
.addButton(button =>
button
.setButtonText('Add script')
.setCta()
.onClick(() => {
this.openAddVaultScriptModal();
}),
);
});
getSettingDefinitions(): SettingDefinitionItem[] {
return [
{
type: 'page',
name: 'Vault scripts',
desc: 'Configure vault .js files as commands.',
items: [
{
type: 'list',
emptyState: 'No vault scripts are configured.',
addItem: {
name: 'Add script',
action: (): void => {
this.openAddVaultScriptModal();
},
},
onDelete: (index): void => {
void this.removeVaultScript(this.plugin.settings.scripts[index]);
},
items: this.plugin.settings.scripts.map(script => ({
name: script.name,
desc: formatVaultScriptDescription(script),
render: (setting): void => {
setting.addExtraButton(button =>
button.setIcon('edit').onClick(() => {
this.openEditVaultScriptModal(script);
}),
);
},
})),
},
],
},
];
}
private openAddVaultScriptModal(): void {
@ -81,7 +72,11 @@ export class VaultScriptsSettingsSection {
).open();
}
private async removeVaultScript(script: SafeJsScriptConfig): Promise<void> {
private async removeVaultScript(script: SafeJsScriptConfig | undefined): Promise<void> {
if (script === undefined) {
return;
}
this.plugin.settings.scripts = this.plugin.settings.scripts.filter(candidate => candidate.id !== script.id);
await this.saveScriptSettings();
this.onSettingsChanged();
@ -143,10 +138,6 @@ export class VaultScriptsSettingsSection {
await this.plugin.saveSettings();
this.scriptManager?.reloadCommands();
}
private createSection(containerEl: HTMLElement, heading: string): void {
new Setting(containerEl).setName(heading).setHeading();
}
}
function formatVaultScriptDescription(script: SafeJsScriptConfig): string {