Merge branch 'next53'

# Conflicts:
#	src/services/builtin_attributes.js
This commit is contained in:
zadam 2022-06-10 23:02:51 +02:00
commit 8e23c15763
69 changed files with 1376 additions and 527 deletions

2
.idea/misc.xml generated
View File

@ -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>

View 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
View File

@ -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",

View File

@ -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",

View File

@ -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)}`)
});
}

View 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);
}

View 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();
}
});

View File

@ -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();
});

View File

@ -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"
};
/**

View File

@ -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'))
)
)

View 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
};

View File

@ -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("&nbsp;");
}

View File

@ -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') {

View File

@ -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,

View File

@ -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
};

View 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
}

View File

@ -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') {

View File

@ -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
});

View File

@ -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");
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View 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;
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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");
}
}

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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")
});
}

View File

@ -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}) => {

View File

@ -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);
}
}

View File

@ -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');
}

View File

@ -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);
}

View File

@ -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 }
];

View File

@ -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')
);
}

View File

@ -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() {

View File

@ -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});

View 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();
}
}
}

View File

@ -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

View File

@ -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
});

View File

@ -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');

View 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();
}
}
}

View File

@ -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 {

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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
};

View File

@ -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)

View File

@ -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";

View File

@ -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' },

View 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
};

View File

@ -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'
];

View File

@ -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);

View File

@ -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,
};

View File

@ -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()

View File

@ -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%;

View File

@ -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 */

View File

@ -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>

View 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">&times;</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>

View 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">&times;</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>

View File

@ -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>

View File

@ -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>