mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
Merge branch 'next53'
# Conflicts: # src/services/builtin_attributes.js
This commit is contained in:
commit
8e23c15763
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -3,7 +3,7 @@
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_18" default="true" project-jdk-name="openjdk-18" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
2
db/migrations/0196__rename_bulk_actions.sql
Normal file
2
db/migrations/0196__rename_bulk_actions.sql
Normal file
@ -0,0 +1,2 @@
|
||||
UPDATE attributes SET value = replace(value, 'setLabelValue', 'updateLabelValue') WHERE name = 'action' AND type = 'label';
|
||||
UPDATE attributes SET value = replace(value, 'setRelationTarget', 'updateRelationTarget') WHERE name = 'action' AND type = 'label';
|
197
package-lock.json
generated
197
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.52.1-beta",
|
||||
"version": "0.52.0-beta",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trilium",
|
||||
"version": "0.52.1-beta",
|
||||
"version": "0.52.0-beta",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
@ -21,7 +21,7 @@
|
||||
"commonmark": "0.30.0",
|
||||
"cookie-parser": "1.4.6",
|
||||
"csurf": "1.11.0",
|
||||
"dayjs": "1.11.3",
|
||||
"dayjs": "1.11.2",
|
||||
"ejs": "3.1.8",
|
||||
"electron-debug": "3.2.0",
|
||||
"electron-dl": "3.3.1",
|
||||
@ -44,8 +44,8 @@
|
||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||
"jsdom": "19.0.0",
|
||||
"mime-types": "2.1.35",
|
||||
"multer": "1.4.4",
|
||||
"node-abi": "3.21.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-abi": "3.22.0",
|
||||
"normalize-strings": "1.1.1",
|
||||
"open": "8.4.0",
|
||||
"portscanner": "2.2.0",
|
||||
@ -2161,38 +2161,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
||||
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"dicer": "0.2.5",
|
||||
"readable-stream": "1.1.x"
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy/node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"node_modules/busboy/node_modules/readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy/node_modules/string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@ -3115,9 +3093,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
|
||||
"integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz",
|
||||
"integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
@ -3267,39 +3245,6 @@
|
||||
"integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/dicer": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
||||
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
|
||||
"dependencies": {
|
||||
"readable-stream": "1.1.x",
|
||||
"streamsearch": "0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dicer/node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"node_modules/dicer/node_modules/readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/dicer/node_modules/string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
},
|
||||
"node_modules/dir-compare": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz",
|
||||
@ -7359,21 +7304,20 @@
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
|
||||
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
|
||||
"version": "1.4.5-lts.1",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
|
||||
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^0.2.11",
|
||||
"busboy": "^1.0.0",
|
||||
"concat-stream": "^1.5.2",
|
||||
"mkdirp": "^0.5.4",
|
||||
"object-assign": "^4.1.1",
|
||||
"on-finished": "^2.3.0",
|
||||
"type-is": "^1.6.4",
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
@ -7407,9 +7351,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.21.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.21.0.tgz",
|
||||
"integrity": "sha512-0ChvtQmmNYzXju0fjG0Vfg72q2D8FxUhluvV9uqivtXsKblSekJE2juxfg+9HoSgqPMqCmVEC/GHHtGzi4xYTg==",
|
||||
"version": "3.22.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz",
|
||||
"integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
@ -9463,11 +9407,11 @@
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
||||
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
@ -12550,35 +12494,11 @@
|
||||
}
|
||||
},
|
||||
"busboy": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
||||
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"requires": {
|
||||
"dicer": "0.2.5",
|
||||
"readable-stream": "1.1.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
}
|
||||
"streamsearch": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
@ -13271,9 +13191,9 @@
|
||||
}
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
|
||||
"integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz",
|
||||
"integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
@ -13383,38 +13303,6 @@
|
||||
"integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
|
||||
"optional": true
|
||||
},
|
||||
"dicer": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
||||
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
|
||||
"requires": {
|
||||
"readable-stream": "1.1.x",
|
||||
"streamsearch": "0.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"dir-compare": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz",
|
||||
@ -16530,16 +16418,15 @@
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"multer": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
|
||||
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
|
||||
"version": "1.4.5-lts.1",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
|
||||
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
|
||||
"requires": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^0.2.11",
|
||||
"busboy": "^1.0.0",
|
||||
"concat-stream": "^1.5.2",
|
||||
"mkdirp": "^0.5.4",
|
||||
"object-assign": "^4.1.1",
|
||||
"on-finished": "^2.3.0",
|
||||
"type-is": "^1.6.4",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
@ -16566,9 +16453,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node-abi": {
|
||||
"version": "3.21.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.21.0.tgz",
|
||||
"integrity": "sha512-0ChvtQmmNYzXju0fjG0Vfg72q2D8FxUhluvV9uqivtXsKblSekJE2juxfg+9HoSgqPMqCmVEC/GHHtGzi4xYTg==",
|
||||
"version": "3.22.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz",
|
||||
"integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==",
|
||||
"requires": {
|
||||
"semver": "^7.3.5"
|
||||
}
|
||||
@ -18169,9 +18056,9 @@
|
||||
}
|
||||
},
|
||||
"streamsearch": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
||||
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
|
@ -59,8 +59,8 @@
|
||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||
"jsdom": "19.0.0",
|
||||
"mime-types": "2.1.35",
|
||||
"multer": "1.4.4",
|
||||
"node-abi": "3.21.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-abi": "3.22.0",
|
||||
"normalize-strings": "1.1.1",
|
||||
"open": "8.4.0",
|
||||
"portscanner": "2.2.0",
|
||||
|
@ -52,13 +52,13 @@ if (utils.isElectron()) {
|
||||
title: suggestion,
|
||||
command: "replaceMisspelling",
|
||||
spellingSuggestion: suggestion,
|
||||
uiIcon: "empty"
|
||||
uiIcon: "bx bx-empty"
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: `Add "${params.misspelledWord}" to dictionary`,
|
||||
uiIcon: "plus",
|
||||
uiIcon: "bx bx-plus",
|
||||
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
|
||||
});
|
||||
|
||||
@ -69,7 +69,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canCut && hasText,
|
||||
title: `Cut <kbd>${platformModifier}+X`,
|
||||
uiIcon: "cut",
|
||||
uiIcon: "bx bx-cut",
|
||||
handler: () => webContents.cut()
|
||||
});
|
||||
}
|
||||
@ -78,7 +78,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canCopy && hasText,
|
||||
title: `Copy <kbd>${platformModifier}+C`,
|
||||
uiIcon: "copy",
|
||||
uiIcon: "bx bx-copy",
|
||||
handler: () => webContents.copy()
|
||||
});
|
||||
}
|
||||
@ -86,7 +86,7 @@ if (utils.isElectron()) {
|
||||
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') {
|
||||
items.push({
|
||||
title: `Copy link`,
|
||||
uiIcon: "copy",
|
||||
uiIcon: "bx bx-copy",
|
||||
handler: () => {
|
||||
electron.clipboard.write({
|
||||
bookmark: params.linkText,
|
||||
@ -100,7 +100,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canPaste,
|
||||
title: `Paste <kbd>${platformModifier}+V`,
|
||||
uiIcon: "paste",
|
||||
uiIcon: "bx bx-paste",
|
||||
handler: () => webContents.paste()
|
||||
});
|
||||
}
|
||||
@ -109,7 +109,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canPaste,
|
||||
title: `Paste as plain text <kbd>${platformModifier}+Shift+V`,
|
||||
uiIcon: "paste",
|
||||
uiIcon: "bx bx-paste",
|
||||
handler: () => webContents.pasteAndMatchStyle()
|
||||
});
|
||||
}
|
||||
@ -122,7 +122,7 @@ if (utils.isElectron()) {
|
||||
items.push({
|
||||
enabled: editFlags.canPaste,
|
||||
title: `Search for "${shortenedSelection}" with DuckDuckGo`,
|
||||
uiIcon: "search-alt",
|
||||
uiIcon: "bx bx-search-alt",
|
||||
handler: () => electron.shell.openExternal(`https://duckduckgo.com/?q=${encodeURIComponent(params.selectionText)}`)
|
||||
});
|
||||
}
|
||||
|
48
src/public/app/dialogs/bulk_assign_attributes.js
Normal file
48
src/public/app/dialogs/bulk_assign_attributes.js
Normal file
@ -0,0 +1,48 @@
|
||||
import utils from "../services/utils.js";
|
||||
import bulkActionService from "../services/bulk_action.js";
|
||||
import froca from "../services/froca.js";
|
||||
|
||||
const $dialog = $("#bulk-assign-attributes-dialog");
|
||||
const $availableActionList = $("#bulk-available-action-list");
|
||||
const $existingActionList = $("#bulk-existing-action-list");
|
||||
|
||||
$dialog.on('click', '[data-action-add]', async event => {
|
||||
const actionName = $(event.target).attr('data-action-add');
|
||||
|
||||
await bulkActionService.addAction('bulkaction', actionName);
|
||||
|
||||
await refresh();
|
||||
});
|
||||
|
||||
for (const actionGroup of bulkActionService.ACTION_GROUPS) {
|
||||
const $actionGroupList = $("<td>");
|
||||
const $actionGroup = $("<tr>")
|
||||
.append($("<td>").text(actionGroup.title + ": "))
|
||||
.append($actionGroupList);
|
||||
|
||||
for (const action of actionGroup.actions) {
|
||||
$actionGroupList.append(
|
||||
$('<button class="btn btn-sm">')
|
||||
.attr('data-action-add', action.actionName)
|
||||
.text(action.actionTitle)
|
||||
);
|
||||
}
|
||||
|
||||
$availableActionList.append($actionGroup);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const bulkActionNote = await froca.getNote('bulkaction');
|
||||
|
||||
const actions = bulkActionService.parseActions(bulkActionNote);
|
||||
|
||||
$existingActionList
|
||||
.empty()
|
||||
.append(...actions.map(action => action.render()));
|
||||
}
|
||||
|
||||
export async function showDialog(nodes) {
|
||||
await refresh();
|
||||
|
||||
utils.openDialog($dialog);
|
||||
}
|
94
src/public/app/dialogs/note_type_chooser.js
Normal file
94
src/public/app/dialogs/note_type_chooser.js
Normal file
@ -0,0 +1,94 @@
|
||||
import noteTypesService from "../services/note_types.js";
|
||||
|
||||
const $dialog = $("#note-type-chooser-dialog");
|
||||
const $noteTypeDropdown = $("#note-type-dropdown");
|
||||
const $noteTypeDropdownTrigger = $("#note-type-dropdown-trigger");
|
||||
$noteTypeDropdownTrigger.dropdown();
|
||||
|
||||
let resolve;
|
||||
let $originalFocused; // element focused before the dialog was opened, so we can return to it afterwards
|
||||
let $originalDialog;
|
||||
|
||||
export async function chooseNoteType() {
|
||||
$originalFocused = $(':focus');
|
||||
|
||||
const noteTypes = await noteTypesService.getNoteTypeItems();
|
||||
|
||||
$noteTypeDropdown.empty();
|
||||
|
||||
for (const noteType of noteTypes) {
|
||||
if (noteType.title === '----') {
|
||||
$noteTypeDropdown.append($('<h6 class="dropdown-header">').append("Templates:"));
|
||||
}
|
||||
else {
|
||||
$noteTypeDropdown.append(
|
||||
$('<a class="dropdown-item" tabindex="0">')
|
||||
.attr("data-note-type", noteType.type)
|
||||
.attr("data-template-note-id", noteType.templateNoteId)
|
||||
.append($("<span>").addClass(noteType.uiIcon))
|
||||
.append(" " + noteType.title)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$noteTypeDropdownTrigger.dropdown('show');
|
||||
|
||||
$originalDialog = glob.activeDialog;
|
||||
glob.activeDialog = $dialog;
|
||||
$dialog.modal();
|
||||
|
||||
$noteTypeDropdown.find(".dropdown-item:first").focus();
|
||||
|
||||
return new Promise((res, rej) => { resolve = res; });
|
||||
}
|
||||
|
||||
$dialog.on("hidden.bs.modal", () => {
|
||||
if (resolve) {
|
||||
resolve({success: false});
|
||||
}
|
||||
|
||||
if ($originalFocused) {
|
||||
$originalFocused.trigger('focus');
|
||||
$originalFocused = null;
|
||||
}
|
||||
|
||||
glob.activeDialog = $originalDialog;
|
||||
});
|
||||
|
||||
function doResolve(e) {
|
||||
const $item = $(e.target).closest(".dropdown-item");
|
||||
const noteType = $item.attr("data-note-type");
|
||||
const templateNoteId = $item.attr("data-template-note-id");
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
noteType,
|
||||
templateNoteId
|
||||
});
|
||||
resolve = null;
|
||||
|
||||
$dialog.modal("hide");
|
||||
}
|
||||
|
||||
$noteTypeDropdown.on('click', '.dropdown-item', e => doResolve(e));
|
||||
|
||||
$noteTypeDropdown.on('focus', '.dropdown-item', e => {
|
||||
$noteTypeDropdown.find('.dropdown-item').each((i, el) => {
|
||||
$(el).toggleClass('active', el === e.target);
|
||||
});
|
||||
});
|
||||
|
||||
$noteTypeDropdown.on('keydown', '.dropdown-item', e => {
|
||||
if (e.key === 'Enter') {
|
||||
doResolve(e);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$noteTypeDropdown.parent().on('hide.bs.dropdown', e => {
|
||||
// prevent closing dropdown by clicking outside
|
||||
if (e.clickEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
@ -9,8 +9,9 @@ let parentNoteId = null;
|
||||
$form.on('submit', async () => {
|
||||
const sortBy = $form.find("input[name='sort-by']:checked").val();
|
||||
const sortDirection = $form.find("input[name='sort-direction']:checked").val();
|
||||
const foldersFirst = $form.find("input[name='sort-folders-first']").is(":checked");
|
||||
|
||||
await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection});
|
||||
await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection, foldersFirst});
|
||||
|
||||
utils.closeActiveDialog();
|
||||
});
|
||||
|
@ -17,7 +17,8 @@ const NOTE_TYPE_ICONS = {
|
||||
"book": "bx bx-book",
|
||||
"note-map": "bx bx-map-alt",
|
||||
"mermaid": "bx bx-selection",
|
||||
"canvas": "bx bx-pen"
|
||||
"canvas": "bx bx-pen",
|
||||
"web-view": "bx bx-globe-alt"
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -49,6 +49,7 @@ import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import BacklinksWidget from "../widgets/backlinks.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import TocWidget from "../widgets/toc.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@ -169,6 +170,7 @@ export default class DesktopLayout {
|
||||
.child(...this.customWidgets.get('center-pane'))
|
||||
)
|
||||
.child(new RightPaneContainer()
|
||||
.child(new TocWidget())
|
||||
.child(...this.customWidgets.get('right-pane'))
|
||||
)
|
||||
)
|
||||
|
92
src/public/app/services/bulk_action.js
Normal file
92
src/public/app/services/bulk_action.js
Normal file
@ -0,0 +1,92 @@
|
||||
import server from "./server.js";
|
||||
import ws from "./ws.js";
|
||||
import MoveNoteBulkAction from "../widgets/bulk_actions/note/move_note.js";
|
||||
import DeleteNoteBulkAction from "../widgets/bulk_actions/note/delete_note.js";
|
||||
import DeleteNoteRevisionsBulkAction from "../widgets/bulk_actions/note/delete_note_revisions.js";
|
||||
import DeleteLabelBulkAction from "../widgets/bulk_actions/label/delete_label.js";
|
||||
import DeleteRelationBulkAction from "../widgets/bulk_actions/relation/delete_relation.js";
|
||||
import RenameLabelBulkAction from "../widgets/bulk_actions/label/rename_label.js";
|
||||
import RenameRelationBulkAction from "../widgets/bulk_actions/relation/rename_relation.js";
|
||||
import UpdateLabelValueBulkAction from "../widgets/bulk_actions/label/update_label_value.js";
|
||||
import UpdateRelationTargetBulkAction from "../widgets/bulk_actions/relation/update_relation_target.js";
|
||||
import ExecuteScriptBulkAction from "../widgets/bulk_actions/execute_script.js";
|
||||
import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
|
||||
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
|
||||
|
||||
const ACTION_GROUPS = [
|
||||
{
|
||||
title: 'Labels',
|
||||
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
||||
},
|
||||
{
|
||||
title: 'Relations',
|
||||
actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction]
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
actions: [DeleteNoteBulkAction, DeleteNoteRevisionsBulkAction, MoveNoteBulkAction],
|
||||
},
|
||||
{
|
||||
title: 'Other',
|
||||
actions: [ExecuteScriptBulkAction]
|
||||
}
|
||||
];
|
||||
|
||||
const ACTION_CLASSES = [
|
||||
MoveNoteBulkAction,
|
||||
DeleteNoteBulkAction,
|
||||
DeleteNoteRevisionsBulkAction,
|
||||
DeleteLabelBulkAction,
|
||||
DeleteRelationBulkAction,
|
||||
RenameLabelBulkAction,
|
||||
RenameRelationBulkAction,
|
||||
AddLabelBulkAction,
|
||||
AddRelationBulkAction,
|
||||
UpdateLabelValueBulkAction,
|
||||
UpdateRelationTargetBulkAction,
|
||||
ExecuteScriptBulkAction
|
||||
];
|
||||
|
||||
async function addAction(noteId, actionName) {
|
||||
await server.post(`notes/${noteId}/attributes`, {
|
||||
type: 'label',
|
||||
name: 'action',
|
||||
value: JSON.stringify({
|
||||
name: actionName
|
||||
})
|
||||
});
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
}
|
||||
|
||||
function parseActions(note) {
|
||||
const actionLabels = note.getLabels('action');
|
||||
|
||||
return actionLabels.map(actionAttr => {
|
||||
let actionDef;
|
||||
|
||||
try {
|
||||
actionDef = JSON.parse(actionAttr.value);
|
||||
} catch (e) {
|
||||
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const ActionClass = ACTION_CLASSES.find(actionClass => actionClass.actionName === actionDef.name);
|
||||
|
||||
if (!ActionClass) {
|
||||
logError(`No action class for '${actionDef.name}' found.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ActionClass(actionAttr, actionDef);
|
||||
})
|
||||
.filter(action => !!action);
|
||||
}
|
||||
|
||||
export default {
|
||||
addAction,
|
||||
parseActions,
|
||||
ACTION_CLASSES,
|
||||
ACTION_GROUPS
|
||||
};
|
@ -82,7 +82,7 @@ class ContextMenu {
|
||||
const $icon = $("<span>");
|
||||
|
||||
if (item.uiIcon) {
|
||||
$icon.addClass("bx bx-" + item.uiIcon);
|
||||
$icon.addClass(item.uiIcon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
|
@ -6,9 +6,9 @@ function openContextMenu(notePath, e) {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "empty"},
|
||||
{title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "dock-right"},
|
||||
{title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "window-open"}
|
||||
{title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "bx bx-empty"},
|
||||
{title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"},
|
||||
{title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"}
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => {
|
||||
if (command === 'openNoteInNewTab') {
|
||||
|
@ -140,7 +140,9 @@ function initNoteAutocomplete($el, options) {
|
||||
appendTo: document.querySelector('body'),
|
||||
hint: false,
|
||||
autoselect: true,
|
||||
openOnFocus: true,
|
||||
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
|
||||
// re-querying of the autocomplete source which then changes currently selected suggestion
|
||||
openOnFocus: false,
|
||||
minLength: 0,
|
||||
tabAutocomplete: false
|
||||
}, [
|
||||
@ -170,9 +172,18 @@ function initNoteAutocomplete($el, options) {
|
||||
}
|
||||
|
||||
if (suggestion.action === 'create-note') {
|
||||
const noteTypeChooserDialog = await import('../dialogs/note_type_chooser.js');
|
||||
const {success, noteType, templateNoteId} = await noteTypeChooserDialog.chooseNoteType();
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {note} = await noteCreateService.createNote(suggestion.parentNoteId, {
|
||||
title: suggestion.noteTitle,
|
||||
activate: false
|
||||
activate: false,
|
||||
type: noteType,
|
||||
templateNoteId: templateNoteId
|
||||
});
|
||||
|
||||
suggestion.notePath = treeService.getSomeNotePath(note);
|
||||
@ -261,7 +272,6 @@ function init() {
|
||||
}
|
||||
|
||||
export default {
|
||||
autocompleteSource,
|
||||
autocompleteSourceForCKEditor,
|
||||
initNoteAutocomplete,
|
||||
showRecentNotes,
|
||||
|
@ -43,7 +43,8 @@ async function createNote(parentNotePath, options = {}) {
|
||||
content: options.content || "",
|
||||
isProtected: options.isProtected,
|
||||
type: options.type,
|
||||
mime: options.mime
|
||||
mime: options.mime,
|
||||
templateNoteId: options.templateNoteId
|
||||
});
|
||||
|
||||
if (options.saveSelection) {
|
||||
@ -74,6 +75,20 @@ async function createNote(parentNotePath, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function createNoteWithTypePrompt(parentNotePath, options = {}) {
|
||||
const noteTypeChooserDialog = await import('../dialogs/note_type_chooser.js');
|
||||
const {success, noteType, templateNoteId} = await noteTypeChooserDialog.chooseNoteType();
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.type = noteType;
|
||||
options.templateNoteId = templateNoteId;
|
||||
|
||||
return await createNote(parentNotePath, options);
|
||||
}
|
||||
|
||||
/* If first element is heading, parse it out and use it as a new heading. */
|
||||
function parseSelectedHtml(selectedHtml) {
|
||||
const dom = $.parseHTML(selectedHtml);
|
||||
@ -105,5 +120,6 @@ async function duplicateSubtree(noteId, parentNotePath) {
|
||||
|
||||
export default {
|
||||
createNote,
|
||||
createNoteWithTypePrompt,
|
||||
duplicateSubtree
|
||||
};
|
||||
|
40
src/public/app/services/note_types.js
Normal file
40
src/public/app/services/note_types.js
Normal file
@ -0,0 +1,40 @@
|
||||
import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
|
||||
async function getNoteTypeItems(command) {
|
||||
const items = [
|
||||
{ title: "Text", command: command, type: "text", uiIcon: "bx bx-note" },
|
||||
{ title: "Code", command: command, type: "code", uiIcon: "bx bx-code" },
|
||||
{ title: "Saved Search", command: command, type: "search", uiIcon: "bx bx-file-find" },
|
||||
{ title: "Relation Map", command: command, type: "relation-map", uiIcon: "bx bx-map-alt" },
|
||||
{ title: "Note Map", command: command, type: "note-map", uiIcon: "bx bx-map-alt" },
|
||||
{ title: "Render Note", command: command, type: "render", uiIcon: "bx bx-extension" },
|
||||
{ title: "Book", command: command, type: "book", uiIcon: "bx bx-book" },
|
||||
{ title: "Mermaid Diagram", command: command, type: "mermaid", uiIcon: "bx bx-selection" },
|
||||
{ title: "Canvas", command: command, type: "canvas", uiIcon: "bx bx-pen" },
|
||||
{ title: "Web View", command: command, type: "iframe", uiIcon: "bx bx-globe-alt" },
|
||||
];
|
||||
|
||||
const templateNoteIds = await server.get("search-templates");
|
||||
const templateNotes = await froca.getNotes(templateNoteIds);
|
||||
|
||||
if (items.length > 0) {
|
||||
items.push({ title: "----" });
|
||||
|
||||
for (const templateNote of templateNotes) {
|
||||
items.push({
|
||||
title: templateNote.title,
|
||||
uiIcon: templateNote.getIcon(),
|
||||
command: command,
|
||||
type: templateNote.type,
|
||||
templateNoteId: templateNote.noteId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export default {
|
||||
getNoteTypeItems
|
||||
}
|
@ -4,6 +4,7 @@ import clipboard from './clipboard.js';
|
||||
import noteCreateService from "./note_create.js";
|
||||
import contextMenu from "./context_menu.js";
|
||||
import appContext from "./app_context.js";
|
||||
import noteTypesService from "./note_types.js";
|
||||
|
||||
class TreeContextMenu {
|
||||
/**
|
||||
@ -24,20 +25,6 @@ class TreeContextMenu {
|
||||
})
|
||||
}
|
||||
|
||||
getNoteTypeItems(command) {
|
||||
return [
|
||||
{ title: "Text", command: command, type: "text", uiIcon: "note" },
|
||||
{ title: "Code", command: command, type: "code", uiIcon: "code" },
|
||||
{ title: "Saved search", command: command, type: "search", uiIcon: "file-find" },
|
||||
{ title: "Relation Map", command: command, type: "relation-map", uiIcon: "map-alt" },
|
||||
{ title: "Note Map", command: command, type: "note-map", uiIcon: "map-alt" },
|
||||
{ title: "Render HTML note", command: command, type: "render", uiIcon: "extension" },
|
||||
{ title: "Book", command: command, type: "book", uiIcon: "book" },
|
||||
{ title: "Mermaid diagram", command: command, type: "mermaid", uiIcon: "selection" },
|
||||
{ title: "Canvas", command: command, type: "canvas", uiIcon: "pen" },
|
||||
];
|
||||
}
|
||||
|
||||
async getMenuItems() {
|
||||
const note = await froca.getNote(this.node.data.noteId);
|
||||
const branch = froca.getBranch(this.node.data.branchId);
|
||||
@ -57,58 +44,59 @@ class TreeContextMenu {
|
||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
||||
|
||||
return [
|
||||
{ title: 'Open in a new tab <kbd>Ctrl+Click</kbd>', command: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
|
||||
{ title: 'Open in a new split', command: "openNoteInSplit", uiIcon: "dock-right", enabled: noSelectedNotes },
|
||||
{ title: 'Insert note after <kbd data-command="createNoteAfter"></kbd>', command: "insertNoteAfter", uiIcon: "plus",
|
||||
items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null,
|
||||
{ title: 'Open in a new tab <kbd>Ctrl+Click</kbd>', command: "openInTab", uiIcon: "bx bx-empty", enabled: noSelectedNotes },
|
||||
{ title: 'Open in a new split', command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
|
||||
{ title: 'Insert note after <kbd data-command="createNoteAfter"></kbd>', command: "insertNoteAfter", uiIcon: "bx bx-plus",
|
||||
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
|
||||
enabled: insertNoteAfterEnabled && noSelectedNotes },
|
||||
{ title: 'Insert child note <kbd data-command="createNoteInto"></kbd>', command: "insertChildNote", uiIcon: "plus",
|
||||
items: notSearch ? this.getNoteTypeItems("insertChildNote") : null,
|
||||
{ title: 'Insert child note <kbd data-command="createNoteInto"></kbd>', command: "insertChildNote", uiIcon: "bx bx-plus",
|
||||
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
{ title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "trash",
|
||||
{ title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "bx bx-trash",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
||||
{ title: "----" },
|
||||
{ title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "search",
|
||||
{ title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "bx bx-search",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
||||
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "door-open" },
|
||||
{ title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "empty",
|
||||
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
|
||||
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||
{ title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "bx bx-empty",
|
||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
|
||||
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
|
||||
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes },
|
||||
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes },
|
||||
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
|
||||
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
||||
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes }
|
||||
{ title: "Advanced", uiIcon: "bx bx-empty", enabled: true, items: [
|
||||
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "bx bx-refresh", enabled: noSelectedNotes },
|
||||
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
|
||||
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes }
|
||||
] },
|
||||
{ title: "----" },
|
||||
{ title: "Protect subtree", command: "protectSubtree", uiIcon: "check-shield", enabled: noSelectedNotes },
|
||||
{ title: "Unprotect subtree", command: "unprotectSubtree", uiIcon: "shield", enabled: noSelectedNotes },
|
||||
{ title: "Protect subtree", command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
|
||||
{ title: "Unprotect subtree", command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
|
||||
{ title: "----" },
|
||||
{ title: 'Copy / clone <kbd data-command="copyNotesToClipboard"></kbd>', command: "copyNotesToClipboard", uiIcon: "copy",
|
||||
{ title: 'Copy / clone <kbd data-command="copyNotesToClipboard"></kbd>', command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
|
||||
enabled: isNotRoot && !isHoisted },
|
||||
{ title: 'Clone to ... <kbd data-command="cloneNotesTo"></kbd>', command: "cloneNotesTo", uiIcon: "empty",
|
||||
{ title: 'Clone to ... <kbd data-command="cloneNotesTo"></kbd>', command: "cloneNotesTo", uiIcon: "bx bx-empty",
|
||||
enabled: isNotRoot && !isHoisted },
|
||||
{ title: 'Cut <kbd data-command="cutNotesToClipboard"></kbd>', command: "cutNotesToClipboard", uiIcon: "cut",
|
||||
{ title: 'Cut <kbd data-command="cutNotesToClipboard"></kbd>', command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
||||
{ title: 'Move to ... <kbd data-command="moveNotesTo"></kbd>', command: "moveNotesTo", uiIcon: "empty",
|
||||
{ title: 'Move to ... <kbd data-command="moveNotesTo"></kbd>', command: "moveNotesTo", uiIcon: "bx bx-empty",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
||||
{ title: 'Paste into <kbd data-command="pasteNotesFromClipboard"></kbd>', command: "pasteNotesFromClipboard", uiIcon: "paste",
|
||||
{ title: 'Paste into <kbd data-command="pasteNotesFromClipboard"></kbd>', command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
|
||||
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
|
||||
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "paste",
|
||||
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
|
||||
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
|
||||
{ title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "empty",
|
||||
{ title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
|
||||
enabled: parentNotSearch && isNotRoot && !isHoisted },
|
||||
{ title: "----" },
|
||||
{ title: "Export", command: "exportNote", uiIcon: "empty",
|
||||
{ title: "Export", command: "exportNote", uiIcon: "bx bx-empty",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
{ title: "Import into note", command: "importIntoNote", uiIcon: "empty",
|
||||
enabled: notSearch && noSelectedNotes }
|
||||
{ title: "Import into note", command: "importIntoNote", uiIcon: "bx bx-empty",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
{ title: "Bulk assign attributes", command: "bulkAssignAttributes", uiIcon: "bx bx-empty",
|
||||
enabled: true }
|
||||
].filter(row => row !== null);
|
||||
}
|
||||
|
||||
async selectMenuItemHandler({command, type}) {
|
||||
const noteId = this.node.data.noteId;
|
||||
async selectMenuItemHandler({command, type, templateNoteId}) {
|
||||
const notePath = treeService.getNotePath(this.node);
|
||||
|
||||
if (command === 'openInTab') {
|
||||
@ -122,7 +110,8 @@ class TreeContextMenu {
|
||||
target: 'after',
|
||||
targetBranchId: this.node.data.branchId,
|
||||
type: type,
|
||||
isProtected: isProtected
|
||||
isProtected: isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
});
|
||||
}
|
||||
else if (command === "insertChildNote") {
|
||||
@ -130,7 +119,8 @@ class TreeContextMenu {
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
type: type,
|
||||
isProtected: this.node.data.isProtected
|
||||
isProtected: this.node.data.isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
});
|
||||
}
|
||||
else if (command === 'openNoteInSplit') {
|
||||
|
@ -217,11 +217,11 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
y: e.pageY,
|
||||
orientation: 'left',
|
||||
items: [
|
||||
{title: `Add new label <kbd data-command="addNewLabel"></kbd>`, command: "addNewLabel", uiIcon: "hash"},
|
||||
{title: `Add new relation <kbd data-command="addNewRelation"></kbd>`, command: "addNewRelation", uiIcon: "transfer"},
|
||||
{title: `Add new label <kbd data-command="addNewLabel"></kbd>`, command: "addNewLabel", uiIcon: "bx bx-hash"},
|
||||
{title: `Add new relation <kbd data-command="addNewRelation"></kbd>`, command: "addNewRelation", uiIcon: "bx bx-transfer"},
|
||||
{title: "----"},
|
||||
{title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "empty"},
|
||||
{title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "empty"},
|
||||
{title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "bx bx-empty"},
|
||||
{title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "bx bx-empty"},
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => this.handleAddNewAttributeCommand(command)
|
||||
});
|
||||
@ -485,7 +485,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async createNoteForReferenceLink(title) {
|
||||
const {note} = await noteCreateService.createNote(this.notePath, {
|
||||
const {note} = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
|
||||
activate: false,
|
||||
title: title
|
||||
});
|
||||
|
@ -103,10 +103,22 @@ class BasicWidget extends Component {
|
||||
this.$widget.toggleClass('hidden-int', !show);
|
||||
}
|
||||
|
||||
isHiddenInt() {
|
||||
return this.$widget.hasClass('hidden-int');
|
||||
}
|
||||
|
||||
toggleExt(show) {
|
||||
this.$widget.toggleClass('hidden-ext', !show);
|
||||
}
|
||||
|
||||
isHiddenExt() {
|
||||
return this.$widget.hasClass('hidden-ext');
|
||||
}
|
||||
|
||||
canBeShown() {
|
||||
return !this.isHiddenInt() && !this.isHiddenExt();
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
return this.$widget.is(":visible");
|
||||
}
|
||||
|
@ -3,10 +3,8 @@ import ws from "../../services/ws.js";
|
||||
import Component from "../component.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
export default class AbstractSearchAction extends Component {
|
||||
export default class AbstractBulkAction {
|
||||
constructor(attribute, actionDef) {
|
||||
super();
|
||||
|
||||
this.attribute = attribute;
|
||||
this.actionDef = actionDef;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import AbstractBulkAction from "./abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@ -33,8 +33,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class ExecuteScriptSearchAction extends AbstractSearchAction {
|
||||
export default class ExecuteScriptBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "executeScript"; }
|
||||
static get actionTitle() { return "Execute script"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
@ -1,11 +1,11 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Set label</div>
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Add label</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control label-name"
|
||||
@ -37,8 +37,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class SetLabelValueSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "setLabelValue"; }
|
||||
export default class AddLabelBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "addLabel"; }
|
||||
static get actionTitle() { return "Add label"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
@ -1,5 +1,5 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@ -18,8 +18,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class DeleteLabelSearchAction extends AbstractSearchAction {
|
||||
export default class DeleteLabelBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "deleteLabel"; }
|
||||
static get actionTitle() { return "Delete label"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
@ -1,5 +1,5 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@ -27,8 +27,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class RenameLabelSearchAction extends AbstractSearchAction {
|
||||
export default class RenameLabelBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "renameLabel"; }
|
||||
static get actionTitle() { return "Rename label"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
@ -0,0 +1,60 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Update label value</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control label-name"
|
||||
placeholder="label name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
|
||||
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to value</div>
|
||||
|
||||
<input type="text" class="form-control label-value" placeholder="new value"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="button-column">
|
||||
<div class="dropdown help-dropdown">
|
||||
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu dropdown-menu-right p-4">
|
||||
<p>On all matched notes, change value of the existing label.</p>
|
||||
|
||||
<p>You can also call this method without value, in such case label will be assigned to the note without value.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="bx bx-x icon-action action-conf-del"></span>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class UpdateLabelValueBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "updateLabelValue"; }
|
||||
static get actionTitle() { return "Update label value"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
||||
const $labelName = $action.find('.label-name');
|
||||
$labelName.val(this.actionDef.labelName || "");
|
||||
|
||||
const $labelValue = $action.find('.label-value');
|
||||
$labelValue.val(this.actionDef.labelValue || "");
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.saveAction({
|
||||
labelName: $labelName.val(),
|
||||
labelValue: $labelValue.val()
|
||||
});
|
||||
}, 1000)
|
||||
|
||||
$labelName.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
$labelValue.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
return $action;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@ -12,8 +12,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class DeleteNoteSearchAction extends AbstractSearchAction {
|
||||
export default class DeleteNoteBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "deleteNote"; }
|
||||
static get actionTitle() { return "Delete note"; }
|
||||
|
||||
doRender() {
|
||||
return $(TPL);
|
@ -1,4 +1,4 @@
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@ -19,8 +19,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class DeleteNoteRevisionsSearchAction extends AbstractSearchAction {
|
||||
export default class DeleteNoteRevisionsBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "deleteNoteRevisions"; }
|
||||
static get actionTitle() { return "Delete note revisions"; }
|
||||
|
||||
doRender() {
|
||||
return $(TPL);
|
@ -1,6 +1,6 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@ -33,8 +33,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class MoveNoteSearchAction extends AbstractSearchAction {
|
||||
export default class MoveNoteBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "moveNote"; }
|
||||
static get actionTitle() { return "Move note"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
65
src/public/app/widgets/bulk_actions/relation/add_relation.js
Normal file
65
src/public/app/widgets/bulk_actions/relation/add_relation.js
Normal file
@ -0,0 +1,65 @@
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Add relation</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control relation-name"
|
||||
placeholder="relation name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
style="flex-shrink: 3"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
|
||||
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">to</div>
|
||||
|
||||
<div class="input-group" style="flex-shrink: 2">
|
||||
<input type="text" class="form-control target-note" placeholder="target note"/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="button-column">
|
||||
<div class="dropdown help-dropdown">
|
||||
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu dropdown-menu-right p-4">
|
||||
<p>On all matched notes create given relation.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="bx bx-x icon-action action-conf-del"></span>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class AddRelationBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "addRelation"; }
|
||||
static get actionTitle() { return "Add relation"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
||||
const $relationName = $action.find('.relation-name');
|
||||
$relationName.val(this.actionDef.relationName || "");
|
||||
|
||||
const $targetNote = $action.find('.target-note');
|
||||
noteAutocompleteService.initNoteAutocomplete($targetNote);
|
||||
$targetNote.setNote(this.actionDef.targetNoteId);
|
||||
|
||||
$targetNote.on('autocomplete:closed', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.saveAction({
|
||||
relationName: $relationName.val(),
|
||||
targetNoteId: $targetNote.getSelectedNoteId()
|
||||
});
|
||||
}, 1000)
|
||||
|
||||
$relationName.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
$targetNote.on('input', () => spacedUpdate.scheduleUpdate());
|
||||
|
||||
return $action;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@ -20,8 +20,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class DeleteRelationSearchAction extends AbstractSearchAction {
|
||||
export default class DeleteRelationBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "deleteRelation"; }
|
||||
static get actionTitle() { return "Delete relation"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
@ -1,5 +1,5 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
@ -27,8 +27,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class RenameRelationSearchAction extends AbstractSearchAction {
|
||||
export default class RenameRelationBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "renameRelation"; }
|
||||
static get actionTitle() { return "Rename relation"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
@ -1,12 +1,12 @@
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import AbstractSearchAction from "./abstract_search_action.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import AbstractBulkAction from "../abstract_bulk_action.js";
|
||||
import noteAutocompleteService from "../../../services/note_autocomplete.js";
|
||||
|
||||
const TPL = `
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Set relation</div>
|
||||
<div style="margin-right: 10px;" class="text-nowrap">Update relation</div>
|
||||
|
||||
<input type="text"
|
||||
class="form-control relation-name"
|
||||
@ -39,8 +39,9 @@ const TPL = `
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
export default class SetRelationTargetSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "setRelationTarget"; }
|
||||
export default class UpdateRelationTargetBulkAction extends AbstractBulkAction {
|
||||
static get actionName() { return "updateRelationTarget"; }
|
||||
static get actionTitle() { return "Update relation target"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
@ -9,6 +9,9 @@ const WIDGET_TPL = `
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
/**
|
||||
* TODO: rename, it's not collapsible anymore
|
||||
*/
|
||||
export default class CollapsibleWidget extends NoteContextAwareWidget {
|
||||
get widgetTitle() { return "Untitled widget"; }
|
||||
|
||||
@ -32,8 +35,4 @@ export default class CollapsibleWidget extends NoteContextAwareWidget {
|
||||
|
||||
/** for overriding */
|
||||
async doRenderBody() {}
|
||||
|
||||
isExpanded() {
|
||||
return this.$bodyWrapper.hasClass("show");
|
||||
}
|
||||
}
|
||||
|
@ -195,6 +195,12 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
async noteSwitched() {
|
||||
this.lastActiveComponentId = null;
|
||||
|
||||
await super.noteSwitched();
|
||||
}
|
||||
|
||||
async refreshWithNote(note, noExplicitActivation = false) {
|
||||
this.lastNoteType = note.type;
|
||||
|
||||
|
@ -11,7 +11,9 @@ export default class RightPaneContainer extends FlexContainer {
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.children.length > 0 && !!this.children.find(ch => ch.isEnabled());
|
||||
return super.isEnabled()
|
||||
&& this.children.length > 0
|
||||
&& !!this.children.find(ch => ch.isEnabled() && ch.canBeShown());
|
||||
}
|
||||
|
||||
handleEventInChildren(name, data) {
|
||||
@ -21,13 +23,20 @@ export default class RightPaneContainer extends FlexContainer {
|
||||
// right pane is displayed only if some child widget is active
|
||||
// we'll reevaluate the visibility based on events which are probable to cause visibility change
|
||||
// but these events needs to be finished and only then we check
|
||||
promise.then(() => {
|
||||
this.toggleInt(this.isEnabled());
|
||||
|
||||
splitService.setupRightPaneResizer();
|
||||
});
|
||||
promise.then(() => this.reevaluateIsEnabledCommand());
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
reevaluateIsEnabledCommand() {
|
||||
const oldToggle = !this.isHiddenInt();
|
||||
const newToggle = this.isEnabled();
|
||||
|
||||
if (oldToggle !== newToggle) {
|
||||
this.toggleInt(newToggle);
|
||||
|
||||
splitService.setupRightPaneResizer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,8 +65,8 @@ export default class HistoryNavigationWidget extends BasicWidget {
|
||||
items.push({
|
||||
title,
|
||||
idx,
|
||||
uiIcon: idx == activeIndex ? "radio-circle-marked" : // compare with type coercion!
|
||||
(idx < activeIndex ? "left-arrow-alt" : "right-arrow-alt")
|
||||
uiIcon: idx == activeIndex ? "bx bx-radio-circle-marked" : // compare with type coercion!
|
||||
(idx < activeIndex ? "bx bx-left-arrow-alt" : "bx bx-right-arrow-alt")
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -18,9 +18,9 @@ class MobileDetailMenuWidget extends BasicWidget {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{ title: "Insert child note", command: "insertChildNote", uiIcon: "plus",
|
||||
{ title: "Insert child note", command: "insertChildNote", uiIcon: "bx bx-plus",
|
||||
enabled: note.type !== 'search' },
|
||||
{ title: "Delete this note", command: "delete", uiIcon: "trash",
|
||||
{ title: "Delete this note", command: "delete", uiIcon: "bx bx-trash",
|
||||
enabled: note.noteId !== 'root' }
|
||||
],
|
||||
selectMenuItemHandler: async ({command}) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
||||
|
||||
const WIDGET_TPL = `
|
||||
<div id="global-buttons">
|
||||
@ -39,6 +40,8 @@ const WIDGET_TPL = `
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" data-trigger-command="switchToDesktopVersion"><span class="bx bx-laptop"></span> Switch to desktop version</a>
|
||||
<a class="dropdown-item" data-trigger-command="enterProtectedSession"><span class="bx bx-shield-quarter"></span> Enter protected session</a>
|
||||
<a class="dropdown-item" data-trigger-command="leaveProtectedSession"><span class="bx bx-check-shield"></span> Leave protected session</a>
|
||||
<a class="dropdown-item" data-trigger-command="logout"><span class="bx bx-log-out"></span> Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -48,6 +51,18 @@ const WIDGET_TPL = `
|
||||
class MobileGlobalButtonsWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(WIDGET_TPL);
|
||||
this.updateSettings();
|
||||
}
|
||||
|
||||
protectedSessionStartedEvent() {
|
||||
this.updateSettings();
|
||||
}
|
||||
|
||||
updateSettings() {
|
||||
const protectedSession = protectedSessionHolder.isProtectedSessionAvailable();
|
||||
|
||||
this.$widget.find('[data-trigger-command="enterProtectedSession"]').toggle(!protectedSession);
|
||||
this.$widget.find('[data-trigger-command="leaveProtectedSession"]').toggle(protectedSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,12 @@ import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import SpacedUpdate from "../services/spaced_update.js";
|
||||
import server from "../services/server.js";
|
||||
import libraryLoader from "../services/library_loader.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
import keyboardActionsService from "../services/keyboard_actions.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import attributeRenderer from "../services/attribute_renderer.js";
|
||||
|
||||
import EmptyTypeWidget from "./type_widgets/empty.js";
|
||||
import EditableTextTypeWidget from "./type_widgets/editable_text.js";
|
||||
import EditableCodeTypeWidget from "./type_widgets/editable_code.js";
|
||||
@ -13,16 +19,12 @@ import RelationMapTypeWidget from "./type_widgets/relation_map.js";
|
||||
import CanvasTypeWidget from "./type_widgets/canvas.js";
|
||||
import ProtectedSessionTypeWidget from "./type_widgets/protected_session.js";
|
||||
import BookTypeWidget from "./type_widgets/book.js";
|
||||
import appContext from "../services/app_context.js";
|
||||
import keyboardActionsService from "../services/keyboard_actions.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import DeletedTypeWidget from "./type_widgets/deleted.js";
|
||||
import ReadOnlyTextTypeWidget from "./type_widgets/read_only_text.js";
|
||||
import ReadOnlyCodeTypeWidget from "./type_widgets/read_only_code.js";
|
||||
import NoneTypeWidget from "./type_widgets/none.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import NoteMapTypeWidget from "./type_widgets/note_map.js";
|
||||
import attributeRenderer from "../services/attribute_renderer.js";
|
||||
import WebViewTypeWidget from "./type_widgets/web_view.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail">
|
||||
@ -54,7 +56,8 @@ const typeWidgetClasses = {
|
||||
'canvas': CanvasTypeWidget,
|
||||
'protected-session': ProtectedSessionTypeWidget,
|
||||
'book': BookTypeWidget,
|
||||
'note-map': NoteMapTypeWidget
|
||||
'note-map': NoteMapTypeWidget,
|
||||
'web-view': WebViewTypeWidget
|
||||
};
|
||||
|
||||
export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
@ -154,7 +157,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
this.$widget.toggleClass("full-height",
|
||||
!this.noteContext.hasNoteList()
|
||||
&& ['editable-text', 'editable-code', 'canvas'].includes(this.type)
|
||||
&& ['editable-text', 'editable-code', 'canvas', 'web-view'].includes(this.type)
|
||||
&& this.mime !== 'text/x-sqlite;schema=trilium');
|
||||
}
|
||||
|
||||
|
@ -309,13 +309,39 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
if (targetType === 'title' || targetType === 'icon') {
|
||||
if (event.shiftKey) {
|
||||
node.setSelected(!node.isSelected());
|
||||
const activeNode = this.getActiveNode();
|
||||
|
||||
if (activeNode.getParent() !== node.getParent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearSelectedNodes();
|
||||
|
||||
function selectInBetween(first, second) {
|
||||
for (let i = 0; first && first !== second && i < 10000; i++) {
|
||||
first.setSelected(true);
|
||||
first = first.getNextSibling();
|
||||
}
|
||||
|
||||
second.setSelected();
|
||||
}
|
||||
|
||||
if (activeNode.getIndex() < node.getIndex()) {
|
||||
selectInBetween(activeNode, node);
|
||||
} else {
|
||||
selectInBetween(node, activeNode);
|
||||
}
|
||||
|
||||
node.setFocus(true);
|
||||
}
|
||||
else if (event.ctrlKey) {
|
||||
const notePath = treeService.getNotePath(node);
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
||||
}
|
||||
else if (event.altKey) {
|
||||
node.setSelected(!node.isSelected());
|
||||
node.setFocus(true);
|
||||
}
|
||||
else if (data.node.isActive()) {
|
||||
// this is important for single column mobile view, otherwise it's not possible to see again previously displayed note
|
||||
this.tree.reactivate(true);
|
||||
@ -513,6 +539,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
subNode.load();
|
||||
}
|
||||
});
|
||||
},
|
||||
select: () => {
|
||||
// TODO
|
||||
}
|
||||
});
|
||||
|
||||
@ -1422,6 +1451,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
importDialog.showDialog(node.data.noteId);
|
||||
}
|
||||
|
||||
async bulkAssignAttributesCommand({node}) {
|
||||
const bulkAssignAttributesDialog = await import('../dialogs/bulk_assign_attributes.js');
|
||||
bulkAssignAttributesDialog.showDialog(this.getSelectedOrActiveNodes(node));
|
||||
}
|
||||
|
||||
forceNoteSyncCommand({node}) {
|
||||
syncService.forceNoteSync(node.data.noteId);
|
||||
}
|
||||
|
@ -12,8 +12,9 @@ const NOTE_TYPES = [
|
||||
{ type: "relation-map", mime: "application/json", title: "Relation Map", selectable: true },
|
||||
{ type: "render", mime: '', title: "Render Note", selectable: true },
|
||||
{ type: "canvas", mime: 'application/json', title: "Canvas", selectable: true },
|
||||
{ type: "book", mime: '', title: "Book", selectable: true },
|
||||
{ type: "mermaid", mime: 'text/mermaid', title: "Mermaid Diagram", selectable: true },
|
||||
{ type: "book", mime: '', title: "Book", selectable: true },
|
||||
{ type: "web-view", mime: '', title: "Web View", selectable: true },
|
||||
{ type: "code", mime: 'text/plain', title: "Code", selectable: true }
|
||||
];
|
||||
|
||||
|
@ -36,7 +36,7 @@ export default class NoteWrapperWidget extends FlexContainer {
|
||||
const note = this.noteContext?.note;
|
||||
|
||||
this.$widget.toggleClass("full-content-width",
|
||||
['image', 'mermaid', 'book', 'render', 'canvas'].includes(note?.type)
|
||||
['image', 'mermaid', 'book', 'render', 'canvas', 'web-view'].includes(note?.type)
|
||||
|| !!note?.hasLabel('fullContentWidth')
|
||||
);
|
||||
}
|
||||
|
@ -5,14 +5,6 @@ import ws from "../../services/ws.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
|
||||
import DeleteNoteSearchAction from "../search_actions/delete_note.js";
|
||||
import DeleteLabelSearchAction from "../search_actions/delete_label.js";
|
||||
import DeleteRelationSearchAction from "../search_actions/delete_relation.js";
|
||||
import RenameLabelSearchAction from "../search_actions/rename_label.js";
|
||||
import SetLabelValueSearchAction from "../search_actions/set_label_value.js";
|
||||
import SetRelationTargetSearchAction from "../search_actions/set_relation_target.js";
|
||||
import RenameRelationSearchAction from "../search_actions/rename_relation.js";
|
||||
import ExecuteScriptSearchAction from "../search_actions/execute_script.js"
|
||||
import SearchString from "../search_options/search_string.js";
|
||||
import FastSearch from "../search_options/fast_search.js";
|
||||
import Ancestor from "../search_options/ancestor.js";
|
||||
@ -20,10 +12,9 @@ import IncludeArchivedNotes from "../search_options/include_archived_notes.js";
|
||||
import OrderBy from "../search_options/order_by.js";
|
||||
import SearchScript from "../search_options/search_script.js";
|
||||
import Limit from "../search_options/limit.js";
|
||||
import DeleteNoteRevisionsSearchAction from "../search_actions/delete_note_revisions.js";
|
||||
import Debug from "../search_options/debug.js";
|
||||
import appContext from "../../services/app_context.js";
|
||||
import MoveNoteSearchAction from "../search_actions/move_note.js";
|
||||
import bulkActionService from "../../services/bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="search-definition-widget">
|
||||
@ -73,6 +64,10 @@ const TPL = `
|
||||
.add-search-option button {
|
||||
margin-top: 5px; /* to give some spacing when buttons overflow on the next line */
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="search-settings">
|
||||
@ -127,28 +122,7 @@ const TPL = `
|
||||
<span class="bx bxs-zap"></span>
|
||||
action
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" data-action-add="moveNote">
|
||||
Move note</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteNote">
|
||||
Delete note</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteNoteRevisions">
|
||||
Delete note revisions</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteLabel">
|
||||
Delete label</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteRelation">
|
||||
Delete relation</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="renameLabel">
|
||||
Rename label</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="renameRelation">
|
||||
Rename relation</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="setLabelValue">
|
||||
Set label value</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="setRelationTarget">
|
||||
Set relation target</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="executeScript">
|
||||
Execute script</a>
|
||||
</div>
|
||||
<div class="dropdown-menu action-list"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -193,24 +167,11 @@ const OPTION_CLASSES = [
|
||||
Debug
|
||||
];
|
||||
|
||||
const ACTION_CLASSES = {};
|
||||
|
||||
for (const clazz of [
|
||||
MoveNoteSearchAction,
|
||||
DeleteNoteSearchAction,
|
||||
DeleteNoteRevisionsSearchAction,
|
||||
DeleteLabelSearchAction,
|
||||
DeleteRelationSearchAction,
|
||||
RenameLabelSearchAction,
|
||||
RenameRelationSearchAction,
|
||||
SetLabelValueSearchAction,
|
||||
SetRelationTargetSearchAction,
|
||||
ExecuteScriptSearchAction
|
||||
]) {
|
||||
ACTION_CLASSES[clazz.actionName] = clazz;
|
||||
}
|
||||
|
||||
export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
get name() {
|
||||
return "searchDefinition";
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.note && this.note.type === 'search';
|
||||
}
|
||||
@ -228,6 +189,19 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
this.$component = this.$widget.find('.search-definition-widget');
|
||||
this.$actionList = this.$widget.find('.action-list');
|
||||
|
||||
for (const actionGroup of bulkActionService.ACTION_GROUPS) {
|
||||
this.$actionList.append($('<h6 class="dropdown-header">').append(actionGroup.title));
|
||||
|
||||
for (const action of actionGroup.actions) {
|
||||
this.$actionList.append(
|
||||
$('<a class="dropdown-item" href="#">')
|
||||
.attr('data-action-add', action.actionName)
|
||||
.text(action.actionTitle)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.$widget.on('click', '[data-search-option-add]', async event => {
|
||||
const searchOptionName = $(event.target).attr('data-search-option-add');
|
||||
@ -244,19 +218,11 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
|
||||
this.$widget.on('click', '[data-action-add]', async event => {
|
||||
const actionName = $(event.target).attr('data-action-add');
|
||||
|
||||
await server.post(`notes/${this.noteId}/attributes`, {
|
||||
type: 'label',
|
||||
name: 'action',
|
||||
value: JSON.stringify({
|
||||
name: actionName
|
||||
})
|
||||
});
|
||||
|
||||
this.$widget.find('.action-add-toggle').dropdown('toggle');
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
const actionName = $(event.target).attr('data-action-add');
|
||||
|
||||
await bulkActionService.addAction(this.noteId, actionName);
|
||||
|
||||
this.refresh();
|
||||
});
|
||||
@ -319,35 +285,13 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
this.$actionOptions.empty();
|
||||
const actions = bulkActionService.parseActions(this.note);
|
||||
|
||||
const actionLabels = this.note.getLabels('action');
|
||||
this.$actionOptions
|
||||
.empty()
|
||||
.append(...actions.map(action => action.render()));
|
||||
|
||||
for (const actionAttr of actionLabels) {
|
||||
let actionDef;
|
||||
|
||||
try {
|
||||
actionDef = JSON.parse(actionAttr.value);
|
||||
}
|
||||
catch (e) {
|
||||
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ActionClass = ACTION_CLASSES[actionDef.name];
|
||||
|
||||
if (!ActionClass) {
|
||||
logError(`No action class for '${actionDef.name}' found.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const action = new ActionClass(actionAttr, actionDef).setParent(this);
|
||||
this.child(action);
|
||||
|
||||
this.$actionOptions.append(action.render());
|
||||
}
|
||||
|
||||
this.$searchAndExecuteButton.css('visibility', actionLabels.length > 0 ? 'visible' : 'hidden');
|
||||
this.$searchAndExecuteButton.css('visibility', actions.length > 0 ? 'visible' : 'hidden');
|
||||
}
|
||||
|
||||
getContent() {
|
||||
|
@ -262,9 +262,9 @@ export default class TabRowWidget extends BasicWidget {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "window-open"},
|
||||
{title: "Close all tabs", command: "removeAllTabs", uiIcon: "x"},
|
||||
{title: "Close all tabs except for this", command: "removeAllTabsExceptForThis", uiIcon: "x"},
|
||||
{title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"},
|
||||
{title: "Close all tabs", command: "removeAllTabs", uiIcon: "bx bx-x"},
|
||||
{title: "Close all tabs except for this", command: "removeAllTabsExceptForThis", uiIcon: "bx bx-x"},
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => {
|
||||
this.triggerCommand(command, {ntxId});
|
||||
|
272
src/public/app/widgets/toc.js
Normal file
272
src/public/app/widgets/toc.js
Normal file
@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Table of contents widget
|
||||
* (c) Antonio Tejada 2022
|
||||
*
|
||||
* By design there's no support for non-sensical or malformed constructs:
|
||||
* - headings inside elements (eg Trilium allows headings inside tables, but
|
||||
* not inside lists)
|
||||
* - nested headings when using raw HTML <H2><H3></H3></H2>
|
||||
* - malformed headings when using raw HTML <H2></H3></H2><H3>
|
||||
* - etc.
|
||||
*
|
||||
* In those cases the generated TOC may be incorrect or the navigation may lead
|
||||
* to the wrong heading (although what "right" means in those cases is not
|
||||
* clear), but it won't crash.
|
||||
*/
|
||||
|
||||
import attributeService from "../services/attributes.js";
|
||||
import CollapsibleWidget from "./collapsible_widget.js";
|
||||
|
||||
const TPL = `<div class="toc-widget">
|
||||
<style>
|
||||
.toc-widget {
|
||||
padding: 10px;
|
||||
contain: none;
|
||||
overflow:auto;
|
||||
}
|
||||
|
||||
.toc ol {
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.toc > ol {
|
||||
padding-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="toc"></span>
|
||||
</div>`;
|
||||
|
||||
/**
|
||||
* Find a heading node in the parent's children given its index.
|
||||
*
|
||||
* @param {Element} parent Parent node to find a headingIndex'th in.
|
||||
* @param {uint} headingIndex Index for the heading
|
||||
* @returns {Element|null} Heading node with the given index, null couldn't be
|
||||
* found (ie malformed like nested headings, etc)
|
||||
*/
|
||||
function findHeadingNodeByIndex(parent, headingIndex) {
|
||||
let headingNode = null;
|
||||
for (let i = 0; i < parent.childCount; ++i) {
|
||||
let child = parent.getChild(i);
|
||||
|
||||
// Headings appear as flattened top level children in the CKEditor
|
||||
// document named as "heading" plus the level, eg "heading2",
|
||||
// "heading3", "heading2", etc and not nested wrt the heading level. If
|
||||
// a heading node is found, decrement the headingIndex until zero is
|
||||
// reached
|
||||
if (child.name.startsWith("heading")) {
|
||||
if (headingIndex === 0) {
|
||||
headingNode = child;
|
||||
break;
|
||||
}
|
||||
headingIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
return headingNode;
|
||||
}
|
||||
|
||||
function findHeadingElementByIndex(parent, headingIndex) {
|
||||
let headingElement = null;
|
||||
for (let i = 0; i < parent.children.length; ++i) {
|
||||
const child = parent.children[i];
|
||||
// Headings appear as flattened top level children in the DOM named as
|
||||
// "H" plus the level, eg "H2", "H3", "H2", etc and not nested wrt the
|
||||
// heading level. If a heading node is found, decrement the headingIndex
|
||||
// until zero is reached
|
||||
|
||||
if (child.tagName.match(/H\d+/i) !== null) {
|
||||
if (headingIndex === 0) {
|
||||
headingElement = child;
|
||||
break;
|
||||
}
|
||||
headingIndex--;
|
||||
}
|
||||
}
|
||||
return headingElement;
|
||||
}
|
||||
|
||||
const MIN_HEADING_COUNT = 3;
|
||||
|
||||
export default class TocWidget extends CollapsibleWidget {
|
||||
get widgetTitle() {
|
||||
return "Table of Contents";
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& this.note.type === 'text'
|
||||
&& !this.note.hasLabel('noToc');
|
||||
}
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.empty().append($(TPL));
|
||||
this.$toc = this.$body.find('.toc');
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
let $toc = "", headingCount = 0;
|
||||
// Check for type text unconditionally in case alwaysShowWidget is set
|
||||
if (this.note.type === 'text') {
|
||||
const { content } = await note.getNoteComplement();
|
||||
({$toc, headingCount} = await this.getToc(content));
|
||||
}
|
||||
|
||||
this.$toc.html($toc);
|
||||
this.toggleInt(headingCount >= MIN_HEADING_COUNT);
|
||||
this.triggerCommand("reevaluateIsEnabled");
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a jquery table of contents.
|
||||
*
|
||||
* @param {String} html Note's html content
|
||||
* @returns {$toc: jQuery, headingCount: integer} ordered list table of headings, nested by heading level
|
||||
* with an onclick event that will cause the document to scroll to
|
||||
* the desired position.
|
||||
*/
|
||||
getToc(html) {
|
||||
// Regular expression for headings <h1>...</h1> using non-greedy
|
||||
// matching and backreferences
|
||||
const headingTagsRegex = /<h(\d+)>(.*?)<\/h\1>/g;
|
||||
|
||||
// Use jquery to build the table rather than html text, since it makes
|
||||
// it easier to set the onclick event that will be executed with the
|
||||
// right captured callback context
|
||||
const $toc = $("<ol>");
|
||||
// Note heading 2 is the first level Trilium makes available to the note
|
||||
let curLevel = 2;
|
||||
const $ols = [$toc];
|
||||
let headingCount;
|
||||
for (let m = null, headingIndex = 0; ((m = headingTagsRegex.exec(html)) !== null); headingIndex++) {
|
||||
//
|
||||
// Nest/unnest whatever necessary number of ordered lists
|
||||
//
|
||||
const newLevel = m[1];
|
||||
const levelDelta = newLevel - curLevel;
|
||||
if (levelDelta > 0) {
|
||||
// Open as many lists as newLevel - curLevel
|
||||
for (let i = 0; i < levelDelta; i++) {
|
||||
const $ol = $("<ol>");
|
||||
$ols[$ols.length - 1].append($ol);
|
||||
$ols.push($ol);
|
||||
}
|
||||
} else if (levelDelta < 0) {
|
||||
// Close as many lists as curLevel - newLevel
|
||||
for (let i = 0; i < -levelDelta; ++i) {
|
||||
$ols.pop();
|
||||
}
|
||||
}
|
||||
curLevel = newLevel;
|
||||
|
||||
//
|
||||
// Create the list item and set up the click callback
|
||||
//
|
||||
const $li = $('<li style="cursor:pointer">' + m[2] + '</li>');
|
||||
// XXX Do this with CSS? How to inject CSS in doRender?
|
||||
$li.hover(function () {
|
||||
$(this).css("font-weight", "bold");
|
||||
}).mouseout(function () {
|
||||
$(this).css("font-weight", "normal");
|
||||
});
|
||||
$li.on("click", () => this.jumpToHeading(headingIndex));
|
||||
$ols[$ols.length - 1].append($li);
|
||||
headingCount = headingIndex;
|
||||
}
|
||||
|
||||
return {
|
||||
$toc,
|
||||
headingCount
|
||||
};
|
||||
}
|
||||
|
||||
async jumpToHeading(headingIndex) {
|
||||
// A readonly note can change state to "readonly disabled
|
||||
// temporarily" (ie "edit this note" button) without any
|
||||
// intervening events, do the readonly calculation at navigation
|
||||
// time and not at outline creation time
|
||||
// See https://github.com/zadam/trilium/issues/2828
|
||||
const isReadOnly = await this.noteContext.isReadOnly();
|
||||
|
||||
if (isReadOnly) {
|
||||
const $readonlyTextContent = await this.noteContext.getContentElement();
|
||||
|
||||
const headingElement = findHeadingElementByIndex($readonlyTextContent[0], headingIndex);
|
||||
|
||||
if (headingElement != null) {
|
||||
headingElement.scrollIntoView();
|
||||
}
|
||||
} else {
|
||||
const textEditor = await this.noteContext.getTextEditor();
|
||||
|
||||
const model = textEditor.model;
|
||||
const doc = model.document;
|
||||
const root = doc.getRoot();
|
||||
|
||||
const headingNode = findHeadingNodeByIndex(root, headingIndex);
|
||||
|
||||
// headingNode could be null if the html was malformed or
|
||||
// with headings inside elements, just ignore and don't
|
||||
// navigate (note that the TOC rendering and other TOC
|
||||
// entries' navigation could be wrong too)
|
||||
if (headingNode != null) {
|
||||
// Setting the selection alone doesn't scroll to the
|
||||
// caret, needs to be done explicitly and outside of
|
||||
// the writer change callback so the scroll is
|
||||
// guaranteed to happen after the selection is
|
||||
// updated.
|
||||
|
||||
// In addition, scrolling to a caret later in the
|
||||
// document (ie "forward scrolls"), only scrolls
|
||||
// barely enough to place the caret at the bottom of
|
||||
// the screen, which is a usability issue, you would
|
||||
// like the caret to be placed at the top or center
|
||||
// of the screen.
|
||||
|
||||
// To work around that issue, first scroll to the
|
||||
// end of the document, then scroll to the desired
|
||||
// point. This causes all the scrolls to be
|
||||
// "backward scrolls" no matter the current caret
|
||||
// position, which places the caret at the top of
|
||||
// the screen.
|
||||
|
||||
// XXX This could be fixed in another way by using
|
||||
// the underlying CKEditor5
|
||||
// scrollViewportToShowTarget, which allows to
|
||||
// provide a larger "viewportOffset", but that
|
||||
// has coding complications (requires calling an
|
||||
// internal CKEditor utils funcion and passing
|
||||
// an HTML element, not a CKEditor node, and
|
||||
// CKEditor5 doesn't seem to have a
|
||||
// straightforward way to convert a node to an
|
||||
// HTML element? (in CKEditor4 this was done
|
||||
// with $(node.$) )
|
||||
|
||||
// Scroll to the end of the note to guarantee the
|
||||
// next scroll is a backwards scroll that places the
|
||||
// caret at the top of the screen
|
||||
model.change(writer => {
|
||||
writer.setSelection(root.getChild(root.childCount - 1), 0);
|
||||
});
|
||||
textEditor.editing.view.scrollToTheSelection();
|
||||
// Backwards scroll to the heading
|
||||
model.change(writer => {
|
||||
writer.setSelection(headingNode, 0);
|
||||
});
|
||||
textEditor.editing.view.scrollToTheSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
await this.refresh();
|
||||
} else if (loadResults.getAttributes().find(attr => attr.type === 'label'
|
||||
&& (attr.name.toLowerCase().includes('readonly') || attr.name === 'noToc')
|
||||
&& attributeService.isAffecting(attr, this.note))) {
|
||||
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ const {sleep} = utils;
|
||||
|
||||
const TPL = `
|
||||
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
|
||||
<style type="text/css">
|
||||
<style>
|
||||
.excalidraw .App-menu_top .buttonList {
|
||||
display: flex;
|
||||
}
|
||||
@ -336,6 +336,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
setDimensions(dimensions);
|
||||
|
||||
const onResize = () => {
|
||||
if (this.note?.type !== 'canvas') {
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = {
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height
|
||||
|
@ -305,7 +305,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
}
|
||||
|
||||
async createNoteForReferenceLink(title) {
|
||||
const {note} = await noteCreateService.createNote(this.notePath, {
|
||||
const {note} = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
|
||||
activate: false,
|
||||
title: title
|
||||
});
|
||||
|
@ -138,9 +138,9 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{title: "Open in new tab", command: "openInNewTab", uiIcon: "empty"},
|
||||
{title: "Remove note", command: "remove", uiIcon: "trash"},
|
||||
{title: "Edit title", command: "editTitle", uiIcon: "pencil"},
|
||||
{title: "Open in new tab", command: "openInNewTab", uiIcon: "bx bx-empty"},
|
||||
{title: "Remove note", command: "remove", uiIcon: "bx bx-trash"},
|
||||
{title: "Edit title", command: "editTitle", uiIcon: "bx bx-pencil"},
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => this.contextMenuHandler(command, e.target)
|
||||
});
|
||||
@ -446,7 +446,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
contextMenu.show({
|
||||
x: event.pageX,
|
||||
y: event.pageY,
|
||||
items: [ {title: "Remove relation", command: "remove", uiIcon: "trash"} ],
|
||||
items: [ {title: "Remove relation", command: "remove", uiIcon: "bx bx-trash"} ],
|
||||
selectMenuItemHandler: async ({command}) => {
|
||||
if (command === 'remove') {
|
||||
const confirmDialog = await import('../../dialogs/confirm.js');
|
||||
|
67
src/public/app/widgets/type_widgets/web_view.js
Normal file
67
src/public/app/widgets/type_widgets/web_view.js
Normal file
@ -0,0 +1,67 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-web-view note-detail-printable" style="height: 100%">
|
||||
<div class="note-detail-web-view-help alert alert-warning" style="margin: 50px; padding: 20px;">
|
||||
<p><strong>This help note is shown because this note of type WebView HTML doesn't have required label to function properly.</strong></p>
|
||||
|
||||
<p>Please create label with a URL address you want to embed, e.g. <code>#webViewSrc="http://www.google.com"</code></p>
|
||||
</div>
|
||||
|
||||
<webview class="note-detail-web-view-content"></webview>
|
||||
</div>`;
|
||||
|
||||
export default class WebViewTypeWidget extends TypeWidget {
|
||||
static getType() { return "web-view"; }
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$noteDetailWebViewHelp = this.$widget.find('.note-detail-web-view-help');
|
||||
this.$noteDetailWebViewContent = this.$widget.find('.note-detail-web-view-content');
|
||||
|
||||
window.addEventListener('resize', () => this.setDimensions(), false);
|
||||
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
async doRefresh(note) {
|
||||
this.$widget.show();
|
||||
this.$noteDetailWebViewHelp.hide();
|
||||
this.$noteDetailWebViewContent.hide();
|
||||
|
||||
const webViewSrc = this.note.getLabelValue('webViewSrc');
|
||||
|
||||
if (webViewSrc) {
|
||||
this.$noteDetailWebViewContent
|
||||
.show()
|
||||
.attr("src", webViewSrc);
|
||||
}
|
||||
else {
|
||||
this.$noteDetailWebViewContent.hide();
|
||||
this.$noteDetailWebViewHelp.show();
|
||||
}
|
||||
|
||||
this.setDimensions();
|
||||
|
||||
setTimeout(() => this.setDimensions(), 1000);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.$noteDetailWebViewContent.removeAttribute("src");
|
||||
}
|
||||
|
||||
setDimensions() {
|
||||
const $parent = this.$widget;
|
||||
|
||||
this.$noteDetailWebViewContent
|
||||
.height($parent.height())
|
||||
.width($parent.width());
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes().find(attr => attr.name === 'webViewSrc' && attributeService.isAffecting(attr, this.noteContext.note))) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
@ -192,6 +192,7 @@ div.ui-tooltip {
|
||||
.dropdown-menu a:hover:not(.disabled), .dropdown-item:hover:not(.disabled) {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--hover-item-border-color) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -210,17 +211,20 @@ div.ui-tooltip {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
.dropdown-item, .dropdown-header {
|
||||
color: var(--menu-text-color) !important;
|
||||
border: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled, .dropdown-item.disabled kbd {
|
||||
color: #aaa !important;
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
.dropdown-item.active, .dropdown-item:focus {
|
||||
color: var(--active-item-text-color) !important;
|
||||
background-color: var(--active-item-background-color) !important;
|
||||
border-color: var(--active-item-border-color) !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
@ -475,8 +479,8 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
|
||||
}
|
||||
|
||||
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
|
||||
color: var(--hover-item-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
color: var(--active-item-text-color);
|
||||
background-color: var(--active-item-background-color);
|
||||
}
|
||||
|
||||
.help-button {
|
||||
@ -945,7 +949,6 @@ input {
|
||||
border: 0;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
#right-pane .card-body ul {
|
||||
|
@ -36,11 +36,13 @@
|
||||
--input-text-color: #ccc;
|
||||
--input-background-color: #333;
|
||||
|
||||
--hover-item-text-color: black;
|
||||
--hover-item-background-color: #777;
|
||||
--hover-item-text-color: #ccc;
|
||||
--hover-item-background-color: transparent;
|
||||
--hover-item-border-color: #aaa;
|
||||
|
||||
--active-item-text-color: black;
|
||||
--active-item-background-color: #777;
|
||||
--active-item-border-color: transparent;
|
||||
|
||||
--menu-text-color: white;
|
||||
--menu-background-color: #222;
|
||||
|
@ -41,10 +41,12 @@ html {
|
||||
--input-background-color: transparent;
|
||||
|
||||
--hover-item-text-color: black;
|
||||
--hover-item-background-color: #ddd;
|
||||
--hover-item-background-color: transparent;
|
||||
--hover-item-border-color: #ccc;
|
||||
|
||||
--active-item-text-color: black;
|
||||
--active-item-background-color: #ddd;
|
||||
--active-item-border-color: transparent;
|
||||
|
||||
--menu-text-color: black;
|
||||
--menu-background-color: white;
|
||||
|
@ -94,13 +94,13 @@ function undeleteNote(req) {
|
||||
|
||||
function sortChildNotes(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const {sortBy, sortDirection} = req.body;
|
||||
const {sortBy, sortDirection, foldersFirst} = req.body;
|
||||
|
||||
log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}`);
|
||||
log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}, foldersFirst=${foldersFirst}`);
|
||||
|
||||
const reverse = sortDirection === 'desc';
|
||||
|
||||
treeService.sortNotes(noteId, sortBy, reverse);
|
||||
treeService.sortNotes(noteId, sortBy, reverse, foldersFirst);
|
||||
}
|
||||
|
||||
function protectNote(req) {
|
||||
|
@ -5,9 +5,7 @@ const SearchContext = require('../../services/search/search_context');
|
||||
const log = require('../../services/log');
|
||||
const scriptService = require('../../services/script');
|
||||
const searchService = require('../../services/search/services/search');
|
||||
const noteRevisionService = require("../../services/note_revisions");
|
||||
const branchService = require("../../services/branches");
|
||||
const cloningService = require("../../services/cloning");
|
||||
const bulkActionService = require("../../services/bulk_actions");
|
||||
const {formatAttrForSearch} = require("../../services/attribute_formatter");
|
||||
const utils = require("../../services/utils.js");
|
||||
|
||||
@ -60,98 +58,6 @@ async function searchFromNote(req) {
|
||||
return await searchFromNoteInt(note);
|
||||
}
|
||||
|
||||
const ACTION_HANDLERS = {
|
||||
deleteNote: (action, note) => {
|
||||
const deleteId = 'searchbulkaction-' + utils.randomString(10);
|
||||
|
||||
note.deleteNote(deleteId);
|
||||
},
|
||||
deleteNoteRevisions: (action, note) => {
|
||||
noteRevisionService.eraseNoteRevisions(note.getNoteRevisions().map(rev => rev.noteRevisionId));
|
||||
},
|
||||
deleteLabel: (action, note) => {
|
||||
for (const label of note.getOwnedLabels(action.labelName)) {
|
||||
label.markAsDeleted();
|
||||
}
|
||||
},
|
||||
deleteRelation: (action, note) => {
|
||||
for (const relation of note.getOwnedRelations(action.relationName)) {
|
||||
relation.markAsDeleted();
|
||||
}
|
||||
},
|
||||
renameLabel: (action, note) => {
|
||||
for (const label of note.getOwnedLabels(action.oldLabelName)) {
|
||||
label.name = action.newLabelName;
|
||||
label.save();
|
||||
}
|
||||
},
|
||||
renameRelation: (action, note) => {
|
||||
for (const relation of note.getOwnedRelations(action.oldRelationName)) {
|
||||
relation.name = action.newRelationName;
|
||||
relation.save();
|
||||
}
|
||||
},
|
||||
setLabelValue: (action, note) => {
|
||||
note.setLabel(action.labelName, action.labelValue);
|
||||
},
|
||||
setRelationTarget: (action, note) => {
|
||||
note.setRelation(action.relationName, action.targetNoteId);
|
||||
},
|
||||
moveNote: (action, note) => {
|
||||
const targetParentNote = becca.getNote(action.targetParentNoteId);
|
||||
|
||||
if (!targetParentNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
let res;
|
||||
|
||||
if (note.getParentBranches().length > 1) {
|
||||
res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId);
|
||||
}
|
||||
else {
|
||||
res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
|
||||
}
|
||||
|
||||
if (!res.success) {
|
||||
log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`);
|
||||
}
|
||||
},
|
||||
executeScript: (action, note) => {
|
||||
if (!action.script || !action.script.trim()) {
|
||||
log.info("Ignoring executeScript since the script is empty.")
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptFunc = new Function("note", action.script);
|
||||
scriptFunc(note);
|
||||
|
||||
note.save();
|
||||
}
|
||||
};
|
||||
|
||||
function getActions(note) {
|
||||
return note.getLabels('action')
|
||||
.map(actionLabel => {
|
||||
let action;
|
||||
|
||||
try {
|
||||
action = JSON.parse(actionLabel.value);
|
||||
} catch (e) {
|
||||
log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(action.name in ACTION_HANDLERS)) {
|
||||
log.error(`Cannot find '${action.name}' search action handler, skipping.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return action;
|
||||
})
|
||||
.filter(a => !!a);
|
||||
}
|
||||
|
||||
function searchAndExecute(req) {
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
|
||||
@ -170,26 +76,7 @@ function searchAndExecute(req) {
|
||||
|
||||
const searchResultNoteIds = searchFromNoteInt(note);
|
||||
|
||||
const actions = getActions(note);
|
||||
|
||||
for (const resultNoteId of searchResultNoteIds) {
|
||||
const resultNote = becca.getNote(resultNoteId);
|
||||
|
||||
if (!resultNote || resultNote.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const action of actions) {
|
||||
try {
|
||||
log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`);
|
||||
|
||||
ACTION_HANDLERS[action.name](action, resultNote);
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`ExecuteScript search action failed with ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
bulkActionService.executeActions(note, searchResultNoteIds);
|
||||
}
|
||||
|
||||
function searchFromRelation(note, relationName) {
|
||||
@ -296,10 +183,20 @@ function getRelatedNotes(req) {
|
||||
};
|
||||
}
|
||||
|
||||
function searchTemplates() {
|
||||
const query = formatAttrForSearch({type: 'label', name: "template"}, false);
|
||||
|
||||
return searchService.searchNotes(query, {
|
||||
includeArchivedNotes: true,
|
||||
ignoreHoistedNote: false
|
||||
}).map(note => note.noteId);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
searchFromNote,
|
||||
searchAndExecute,
|
||||
getRelatedNotes,
|
||||
quickSearch,
|
||||
search
|
||||
search,
|
||||
searchTemplates
|
||||
};
|
||||
|
@ -355,6 +355,7 @@ function register(app) {
|
||||
apiRoute(POST, '/api/search-and-execute-note/:noteId', searchRoute.searchAndExecute);
|
||||
apiRoute(POST, '/api/search-related', searchRoute.getRelatedNotes);
|
||||
apiRoute(GET, '/api/search/:searchString', searchRoute.search);
|
||||
apiRoute(GET, '/api/search-templates', searchRoute.searchTemplates);
|
||||
|
||||
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
|
||||
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
||||
|
@ -4,7 +4,7 @@ const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 195;
|
||||
const APP_DB_VERSION = 196;
|
||||
const SYNC_VERSION = 25;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
|
@ -51,6 +51,7 @@ module.exports = [
|
||||
{ type: 'label', name: 'displayRelations' },
|
||||
{ type: 'label', name: 'hideRelations' },
|
||||
{ type: 'label', name: 'titleTemplate', isDangerous: true },
|
||||
{ type: 'label', name: 'template' },
|
||||
|
||||
// relation names
|
||||
{ type: 'relation', name: 'internalLink' },
|
||||
|
142
src/services/bulk_actions.js
Normal file
142
src/services/bulk_actions.js
Normal file
@ -0,0 +1,142 @@
|
||||
const log = require("./log");
|
||||
const noteRevisionService = require("./note_revisions");
|
||||
const becca = require("../becca/becca");
|
||||
const cloningService = require("./cloning");
|
||||
const branchService = require("./branches");
|
||||
const utils = require("./utils");
|
||||
|
||||
const ACTION_HANDLERS = {
|
||||
addLabel: (action, note) => {
|
||||
note.addLabel(action.labelName, action.labelValue);
|
||||
},
|
||||
addRelation: (action, note) => {
|
||||
note.addRelation(action.relationName, action.targetNoteId);
|
||||
},
|
||||
deleteNote: (action, note) => {
|
||||
const deleteId = 'searchbulkaction-' + utils.randomString(10);
|
||||
|
||||
note.deleteNote(deleteId);
|
||||
},
|
||||
deleteNoteRevisions: (action, note) => {
|
||||
noteRevisionService.eraseNoteRevisions(note.getNoteRevisions().map(rev => rev.noteRevisionId));
|
||||
},
|
||||
deleteLabel: (action, note) => {
|
||||
for (const label of note.getOwnedLabels(action.labelName)) {
|
||||
label.markAsDeleted();
|
||||
}
|
||||
},
|
||||
deleteRelation: (action, note) => {
|
||||
for (const relation of note.getOwnedRelations(action.relationName)) {
|
||||
relation.markAsDeleted();
|
||||
}
|
||||
},
|
||||
renameLabel: (action, note) => {
|
||||
for (const label of note.getOwnedLabels(action.oldLabelName)) {
|
||||
// attribute name is immutable, renaming means delete old + create new
|
||||
const newLabel = label.createClone('label', action.newLabelName, label.value);
|
||||
|
||||
newLabel.save();
|
||||
label.markAsDeleted();
|
||||
}
|
||||
},
|
||||
renameRelation: (action, note) => {
|
||||
for (const relation of note.getOwnedRelations(action.oldRelationName)) {
|
||||
// attribute name is immutable, renaming means delete old + create new
|
||||
const newRelation = relation.createClone('relation', action.newRelationName, relation.value);
|
||||
|
||||
newRelation.save();
|
||||
relation.markAsDeleted();
|
||||
}
|
||||
},
|
||||
updateLabelValue: (action, note) => {
|
||||
for (const label of note.getOwnedLabels(action.labelName)) {
|
||||
label.value = action.labelValue;
|
||||
label.save();
|
||||
}
|
||||
},
|
||||
updateRelationTarget: (action, note) => {
|
||||
for (const relation of note.getOwnedLabels(action.relationName)) {
|
||||
relation.value = action.targetNoteId;
|
||||
relation.save();
|
||||
}
|
||||
},
|
||||
moveNote: (action, note) => {
|
||||
const targetParentNote = becca.getNote(action.targetParentNoteId);
|
||||
|
||||
if (!targetParentNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
let res;
|
||||
|
||||
if (note.getParentBranches().length > 1) {
|
||||
res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId);
|
||||
}
|
||||
else {
|
||||
res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
|
||||
}
|
||||
|
||||
if (!res.success) {
|
||||
log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`);
|
||||
}
|
||||
},
|
||||
executeScript: (action, note) => {
|
||||
if (!action.script || !action.script.trim()) {
|
||||
log.info("Ignoring executeScript since the script is empty.")
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptFunc = new Function("note", action.script);
|
||||
scriptFunc(note);
|
||||
|
||||
note.save();
|
||||
}
|
||||
};
|
||||
|
||||
function getActions(note) {
|
||||
return note.getLabels('action')
|
||||
.map(actionLabel => {
|
||||
let action;
|
||||
|
||||
try {
|
||||
action = JSON.parse(actionLabel.value);
|
||||
} catch (e) {
|
||||
log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(action.name in ACTION_HANDLERS)) {
|
||||
log.error(`Cannot find '${action.name}' search action handler, skipping.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return action;
|
||||
})
|
||||
.filter(a => !!a);
|
||||
}
|
||||
|
||||
function executeActions(note, searchResultNoteIds) {
|
||||
const actions = getActions(note);
|
||||
|
||||
for (const resultNoteId of searchResultNoteIds) {
|
||||
const resultNote = becca.getNote(resultNoteId);
|
||||
|
||||
if (!resultNote || resultNote.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const action of actions) {
|
||||
try {
|
||||
log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`);
|
||||
|
||||
ACTION_HANDLERS[action.name](action, resultNote);
|
||||
} catch (e) {
|
||||
log.error(`ExecuteScript search action failed with ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
executeActions
|
||||
};
|
@ -1,13 +1,14 @@
|
||||
module.exports = [
|
||||
'text',
|
||||
'code',
|
||||
'render',
|
||||
'file',
|
||||
'image',
|
||||
'search',
|
||||
'relation-map',
|
||||
'book',
|
||||
'text',
|
||||
'code',
|
||||
'render',
|
||||
'file',
|
||||
'image',
|
||||
'search',
|
||||
'relation-map',
|
||||
'book',
|
||||
'note-map',
|
||||
'mermaid',
|
||||
'canvas'
|
||||
];
|
||||
'canvas',
|
||||
'web-view'
|
||||
];
|
||||
|
@ -55,7 +55,7 @@ function deriveMime(type, mime) {
|
||||
mime = 'text/plain';
|
||||
} else if (['relation-map', 'search', 'canvas'].includes(type)) {
|
||||
mime = 'application/json';
|
||||
} else if (['render', 'book'].includes(type)) {
|
||||
} else if (['render', 'book', 'iframe'].includes(type)) {
|
||||
mime = '';
|
||||
} else {
|
||||
mime = 'application/octet-stream';
|
||||
@ -155,6 +155,14 @@ function createNewNote(params) {
|
||||
|
||||
scanForLinks(note);
|
||||
|
||||
if (params.templateNoteId) {
|
||||
if (!becca.getNote(params.templateNoteId)) {
|
||||
throw new Error(`Template note '${params.templateNoteId}' does not exist.`);
|
||||
}
|
||||
|
||||
note.addRelation('template', params.templateNoteId);
|
||||
}
|
||||
|
||||
copyChildAttributes(parentNote, note);
|
||||
|
||||
triggerNoteTitleChanged(note);
|
||||
|
@ -219,10 +219,28 @@ function getShareRoot() {
|
||||
return shareRoot;
|
||||
}
|
||||
|
||||
function getBulkActionNote() {
|
||||
let bulkActionNote = becca.getNote('bulkaction');
|
||||
|
||||
if (!bulkActionNote) {
|
||||
bulkActionNote = noteService.createNewNote({
|
||||
branchId: 'bulkaction',
|
||||
noteId: 'bulkaction',
|
||||
title: 'Bulk action',
|
||||
type: 'text',
|
||||
content: '',
|
||||
parentNoteId: getHiddenRoot().noteId
|
||||
}).note;
|
||||
}
|
||||
|
||||
return bulkActionNote;
|
||||
}
|
||||
|
||||
function createMissingSpecialNotes() {
|
||||
getSinglesNoteRoot();
|
||||
getSqlConsoleRoot();
|
||||
getGlobalNoteMap();
|
||||
getBulkActionNote();
|
||||
// share root is not automatically created since it's visible in the tree and many won't need it/use it
|
||||
|
||||
const hidden = getHiddenRoot();
|
||||
@ -239,5 +257,6 @@ module.exports = {
|
||||
createSearchNote,
|
||||
saveSearchNote,
|
||||
createMissingSpecialNotes,
|
||||
getShareRoot
|
||||
getShareRoot,
|
||||
getBulkActionNote,
|
||||
};
|
||||
|
@ -67,7 +67,8 @@ async function createMainWindow() {
|
||||
enableRemoteModule: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: spellcheckEnabled
|
||||
spellcheck: spellcheckEnabled,
|
||||
webviewTag: true
|
||||
},
|
||||
frame: optionService.getOptionBool('nativeTitleBarVisible'),
|
||||
icon: getIcon()
|
||||
|
@ -92,7 +92,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
header += `<script src="../../node_modules/react/umd/react.production.min.js"></script>`;
|
||||
header += `<script src="../../node_modules/react-dom/umd/react-dom.production.min.js"></script>`;
|
||||
header += `<script src="../../node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"></script>`;
|
||||
header += `<style type="text/css">
|
||||
header += `<style>
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 100%;
|
||||
|
@ -40,6 +40,8 @@
|
||||
<%- include('dialogs/sort_child_notes.ejs') %>
|
||||
<%- include('dialogs/delete_notes.ejs') %>
|
||||
<%- include('dialogs/password_not_set.ejs') %>
|
||||
<%- include('dialogs/bulk_assign_attributes.ejs') %>
|
||||
<%- include('dialogs/note_type_chooser.ejs') %>
|
||||
|
||||
<script type="text/javascript">
|
||||
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
|
@ -35,6 +35,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="add-link-title-form-group">
|
||||
<br/>
|
||||
<label for="link-title">Link title</label>
|
||||
<input id="link-title" class="form-control" style="width: 100%;">
|
||||
</div>
|
||||
|
40
src/views/dialogs/bulk_assign_attributes.ejs
Normal file
40
src/views/dialogs/bulk_assign_attributes.ejs
Normal file
@ -0,0 +1,40 @@
|
||||
<style>
|
||||
#bulk-available-action-list button {
|
||||
padding: 2px 7px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="bulk-assign-attributes-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title mr-auto">Bulk assign attributes</h5>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Affected notes: <span id="affected-note-count">0</span>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="include-descendants">
|
||||
<label class="form-check-label" for="include-descendants">
|
||||
Include descendant notes
|
||||
</label>
|
||||
</div>
|
||||
|
||||
Available actions:
|
||||
|
||||
<table id="bulk-available-action-list"></table>
|
||||
|
||||
<div id="bulk-existing-action-list"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">Execute bulk actions</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
34
src/views/dialogs/note_type_chooser.ejs
Normal file
34
src/views/dialogs/note_type_chooser.ejs
Normal file
@ -0,0 +1,34 @@
|
||||
<style>
|
||||
#note-type-dropdown {
|
||||
position: relative;
|
||||
font-size: large;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="note-type-chooser-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" style="max-width: 500px;" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title mr-auto">Choose note type</h5>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Choose note type / template of the new note:
|
||||
|
||||
<div class="dropdown">
|
||||
<button id="note-type-dropdown-trigger" type="button" style="display: none;" data-toggle="dropdown">Dropdown trigger</button>
|
||||
|
||||
<div id="note-type-dropdown" class="dropdown-menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -50,6 +50,17 @@
|
||||
descending
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<h5>Folders</h5>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="sort-folders-first" value="1" id="sort-folders-first">
|
||||
<label class="form-check-label" for="sort-folders-first">
|
||||
sort folders at the top
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">Sort <kbd>enter</kbd></button>
|
||||
|
@ -103,6 +103,7 @@
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
|
||||
|
||||
<%- include('dialogs/confirm.ejs') %>
|
||||
<%- include('dialogs/protected_session_password.ejs') %>
|
||||
|
||||
<script type="text/javascript">
|
||||
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
@ -116,7 +117,8 @@
|
||||
instanceName: '<%= instanceName %>',
|
||||
csrfToken: '<%= csrfToken %>',
|
||||
isDev: <%= isDev %>,
|
||||
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>
|
||||
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
|
||||
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>
|
||||
};
|
||||
</script>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user