Compare commits

...

85 Commits

Author SHA1 Message Date
zadam
25ce2e4253 release 0.46.9 2021-04-22 20:50:22 +02:00
zadam
c27f573eed fix line continuation in the release script 2021-04-22 19:39:57 +02:00
zadam
1d99c4e80b fix moving notes with keyboard on Firefox, closes #1865 2021-04-21 22:03:41 +02:00
zadam
dc7c64a94d Merge remote-tracking branch 'origin/stable' into stable 2021-04-21 20:38:15 +02:00
zadam
dc6a530d8c clear note selection anytime a new note is activated 2021-04-21 20:38:07 +02:00
zadam
a674a12706 disable tar polluting build log 2021-04-19 20:56:05 +02:00
zadam
c29f1af48f use n to force node version
(cherry picked from commit 51d1d8efb8)
2021-04-19 20:51:06 +02:00
zadam
a8d72c46e4 release 0.46.8 2021-04-18 21:47:45 +02:00
zadam
ec36fbd83e release 0.46.8 2021-04-18 21:46:35 +02:00
zadam
aedb05cbab release 0.46.8 2021-04-18 21:39:01 +02:00
zadam
bb16840a72 release 0.46.8 2021-04-18 21:30:43 +02:00
zadam
7b5d44a329 use official gh client instead of github-release to execute releases 2021-04-18 21:29:10 +02:00
zadam
a53a65be1f clipper fix, closes #1840 2021-04-12 23:29:02 +02:00
zadam
44af431a93 ctrl + click / middle click now opens image in a text note in a new tab 2021-04-11 22:24:21 +02:00
zadam
caa11b8f7e fix inheriting inheritable attributes through template, closes #1828 2021-04-05 20:52:19 +02:00
zadam
41cce4dcb9 improved task manager with allowed year range, #1823 2021-04-04 23:57:55 +02:00
zadam
858072cc10 fix lexer to parse correctly quoted empty strings, #1825 2021-04-04 23:13:18 +02:00
zadam
855c5e0e67 fix unit tests 2021-04-04 22:44:22 +02:00
zadam
f0cc3d0bcd use entity changes instead of actual tables to fill in sector to sync, fixes #1809 2021-04-04 22:02:40 +02:00
zadam
7672f22ce0 release 0.46.7 2021-04-03 22:37:04 +02:00
zadam
ef37a52a06 when leaving protected session don't forget to reset note cache (titles), #1810 2021-04-03 22:02:25 +02:00
zadam
2318d615bb make note cache decryption more robust - one failure will not crash the whole process, #1810 2021-04-03 21:48:16 +02:00
zadam
bc14c3d665 backport not deforming standard top widget in small window size from master 2021-03-31 22:59:07 +02:00
zadam
b89ea9a684 check for note cache branch existence, #1808 2021-03-31 22:35:10 +02:00
zadam
496767a52b release 0.46.6 2021-03-25 20:28:57 +01:00
zadam
9139c597e5 use only one label for icon class, fixes #1791 2021-03-25 19:46:10 +01:00
zadam
942132c01d release 0.46.6 2021-03-23 23:57:48 +01:00
zadam
1862acd1ff avoid getting refreshes on note title 2021-03-23 22:46:18 +01:00
zadam
ce7e18d0b0 fix syncing protected notes in case there wasn't any other change, closes #1778 2021-03-23 22:18:23 +01:00
zadam
65280d5ba3 avoid ugly error in the logs, #1778 2021-03-22 23:27:41 +01:00
zadam
7e3d424e23 fix deleting all revisions, closes #1774 2021-03-22 23:07:43 +01:00
zadam
bff04c121a fix scrolling in mobile frontend, closes #1768 2021-03-20 12:47:47 +01:00
zadam
e8903e82a1 add option to disable note tree auto collapse, fixes #1751 2021-03-18 20:11:58 +01:00
zadam
0cfd95d9b8 not finding node for a note path does not have to be always an error 2021-03-17 23:17:54 +01:00
zadam
1aa5349628 make edit button visible for empty readonly notes, fixes #1760 2021-03-17 22:41:37 +01:00
zadam
4e21d12202 fix nodejs 10 compatibility, closes #1746 2021-03-15 20:31:12 +01:00
zadam
fdce218e88 release 0.46.5 2021-03-14 22:56:27 +01:00
zadam
6c8d20288d fix 0.46 regression to set up sync from client, fixes #1742 2021-03-13 22:54:00 +01:00
zadam
88d04772c4 make sure the CLS entity changes are cleared after roll backed transaction, #1736 2021-03-12 23:48:14 +01:00
zadam
9fd26a9b9f when creating new note into inbox use current hoisted note in new tab as well 2021-03-12 21:47:26 +01:00
zadam
98f02c3c9a added "hoistedInbox" label 2021-03-12 21:29:50 +01:00
zadam
584fea1992 Revert "delete notes skeleton dialog"
This reverts commit 03a11e6f
2021-03-12 20:44:19 +01:00
zadam
af50a1ec52 Merge remote-tracking branch 'origin/stable' into stable 2021-03-12 20:39:51 +01:00
zadam
4e76d1fa85 hide some "boring" attributes from book listing view 2021-03-12 20:39:42 +01:00
zadam
03a11e6f77 delete notes skeleton dialog 2021-03-11 22:35:53 +01:00
zadam
e1a16b4a9f defensive check, #1736 2021-03-11 20:28:07 +01:00
zadam
12b468d3dc release 0.46.4-beta 2021-03-10 23:35:12 +01:00
zadam
6f901e6852 use icons instead of plain text to differentiate between different paths in note paths widget 2021-03-10 23:11:48 +01:00
zadam
09e9ac4d00 prevent cycles in resolving the notepath, fixes #1730 2021-03-10 22:54:55 +01:00
zadam
a33ac65fdf fix focus on moving notes with keyboard 2021-03-09 22:24:59 +01:00
zadam
f8fb071a6f added option to bring back plain (non-markdown) headings, closes #1678 2021-03-09 22:06:40 +01:00
zadam
a654078e56 fix broken template copy, closes #1724 2021-03-09 20:51:57 +01:00
zadam
fba68681aa when resolving note path check if there's a hoisted note in it, if not, try again to find some path with hoisted note, closes #1718 2021-03-09 20:37:56 +01:00
zadam
50c84e0f5f release 0.46.3-beta 2021-03-08 23:11:11 +01:00
zadam
f27370d44f fix putting focus back to the note tree after note deletion 2021-03-08 23:10:34 +01:00
zadam
c6c9202c00 fix note icon color in dark mode 2021-03-08 22:06:26 +01:00
zadam
2cafda5f66 note paths visually distinguishes between different note paths, closes #1669 2021-03-08 22:04:52 +01:00
zadam
873953cbaf fix bug 2021-03-08 00:09:48 +01:00
zadam
d51744ce19 make note paths current path underlined 2021-03-08 00:07:00 +01:00
zadam
7df8c940b6 have paths in "note paths" widget also sorted by priority 2021-03-08 00:04:43 +01:00
zadam
9bac2a4819 refresh inherited attribute list after attr change, closes #1717 2021-03-07 23:31:56 +01:00
zadam
88147f7a0a fixed "create note after" position issue 2021-03-06 23:53:10 +01:00
zadam
ca77211b38 improved template code with better heuristics on when to copy things from the template 2021-03-06 21:34:03 +01:00
zadam
4606e8d118 non-search notes should have no children limit, #1673 2021-03-06 20:31:12 +01:00
zadam
9f002fa802 change the heuristics to choose the best note path when ambiguous/incomplete/just noteId is provided, #1711 2021-03-06 20:23:29 +01:00
zadam
060d4fc27b fix duplication of hoisted note tree when hoisted note has clones 2021-03-04 23:19:27 +01:00
zadam
bf0fbe201e hack when hoisted note is cloned then it could be filtered multiple times while we want only 1 2021-03-03 23:00:16 +01:00
zadam
721e5da672 use notePath instead of noteId for note creation to correctly work with cloned ancestors 2021-03-03 22:48:06 +01:00
zadam
8192b51b8a cleanup of createTopLevelNote 2021-03-03 22:27:57 +01:00
zadam
73514a63d8 when resolving note path attempt to find one going through hoisted note 2021-03-03 21:49:57 +01:00
zadam
f8c310eb8f added license mention into the README noting AGPL v3+, #1708 2021-03-03 21:41:44 +01:00
zadam
b9422b0efd cssClass is now added also to link map and relation map, closes #1702 2021-03-02 23:20:53 +01:00
zadam
14ced949a9 fix modifying index in note cache when deleting attribute, closes #1706 2021-03-02 23:10:42 +01:00
zadam
5b5c2a2dbb fix null utcDateChanged in entity_changes, closes #1705 2021-03-02 22:08:29 +01:00
zadam
2c958eaacb Merge branch 'sort-by' 2021-02-28 23:40:37 +01:00
zadam
4aa27b6033 added "sort by" dialog 2021-02-28 23:40:15 +01:00
zadam
89a0c5a1c9 fix "no data" when switching between tabs with different hoisted notes, closes #1699 2021-02-28 19:46:04 +01:00
zadam
78e48095e6 prompt user when there are unsaved changes, #1692 2021-02-27 23:39:02 +01:00
zadam
02016ed031 fix create full search note 2021-02-27 21:19:54 +01:00
zadam
cb91dadeca add possibility to define search home for hoisted notes, #1694 2021-02-27 21:18:10 +01:00
zadam
2c755bcc38 don't fail (immediatelly) when sql insert doesn't return lastInsertRowid, #1665 2021-02-27 21:09:13 +01:00
zadam
3c7a6bc1e4 use longer update interval for web 2021-02-27 21:08:27 +01:00
zadam
3fe87259e2 if search note would end up outside of current hoisting, save it under the hoisted note, closes #1694 2021-02-26 23:33:22 +01:00
zadam
d476dfc53b fix searching the second time in quick search, #1694 2021-02-26 23:20:49 +01:00
zadam
1c59bc4d3c sort child notes by ... WIP 2021-02-25 22:26:46 +01:00
84 changed files with 1617 additions and 614 deletions

1
.idea/vcs.xml generated
View File

@@ -2,6 +2,5 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component> </component>
</project> </project>

View File

@@ -55,3 +55,7 @@ npm run start-server
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it. * [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages * [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map) * [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
## License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

View File

@@ -10,7 +10,7 @@ fi
cd dist cd dist
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz
tar xvfJ node-v${NODE_VERSION}-linux-x64.tar.xz tar xfJ node-v${NODE_VERSION}-linux-x64.tar.xz
rm node-v${NODE_VERSION}-linux-x64.tar.xz rm node-v${NODE_VERSION}-linux-x64.tar.xz
cd .. cd ..

View File

@@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
exit 1 exit 1
fi fi
npm run webpack n exec 12 npm run webpack
DIR=$1 DIR=$1
@@ -27,7 +27,7 @@ cp -r electron.js $DIR/
cp webpack-* $DIR/ cp webpack-* $DIR/
# run in subshell (so we return to original dir) # run in subshell (so we return to original dir)
(cd $DIR && npm install --only=prod) (cd $DIR && n exec 12 npm install --only=prod)
# cleanup of useless files in dependencies # cleanup of useless files in dependencies
rm -r $DIR/node_modules/image-q/demo rm -r $DIR/node_modules/image-q/demo

View File

@@ -55,47 +55,20 @@ echo "Creating release in GitHub"
EXTRA= EXTRA=
if [[ $TAG == *"beta"* ]]; then if [[ $TAG == *"beta"* ]]; then
EXTRA=--pre-release EXTRA=--prerelease
fi fi
github-release release \ echo "$GITHUB_CLI_AUTH_TOKEN" | gh auth login --with-token
--tag $TAG \
--name "$TAG release" $EXTRA
echo "Uploading debian x64 package" gh release create "$TAG" \
--title "$TAG release" \
github-release upload \ --notes "" \
--tag $TAG \ $EXTRA \
--name "$DEBIAN_X64_BUILD" \ "dist/$DEBIAN_X64_BUILD" \
--file "dist/$DEBIAN_X64_BUILD" "dist/$LINUX_X64_BUILD" \
"dist/$WINDOWS_X64_BUILD" \
echo "Uploading linux x64 build" "dist/$MAC_X64_BUILD" \
"dist/$SERVER_BUILD"
github-release upload \
--tag $TAG \
--name "$LINUX_X64_BUILD" \
--file "dist/$LINUX_X64_BUILD"
echo "Uploading windows x64 build"
github-release upload \
--tag $TAG \
--name "$WINDOWS_X64_BUILD" \
--file "dist/$WINDOWS_X64_BUILD"
echo "Uploading mac x64 build"
github-release upload \
--tag $TAG \
--name "$MAC_X64_BUILD" \
--file "dist/$MAC_X64_BUILD"
echo "Uploading linux x64 server build"
github-release upload \
--tag $TAG \
--name "$SERVER_BUILD" \
--file "dist/$SERVER_BUILD"
echo "Building docker image" echo "Building docker image"

Binary file not shown.

921
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.46.2-beta", "version": "0.46.9",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@@ -14,7 +14,7 @@
}, },
"scripts": { "scripts": {
"start-server": "cross-env TRILIUM_ENV=dev node ./src/www", "start-server": "cross-env TRILIUM_ENV=dev node ./src/www",
"start-electron": "cross-env TRILIUM_ENV=dev electron .", "start-electron": "cross-env TRILIUM_ENV=dev electron --inspect=5858 .",
"build-backend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js", "build-backend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
"build-frontend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/collapsible_widget.js", "build-frontend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/collapsible_widget.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs", "build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
@@ -35,7 +35,7 @@
"dayjs": "1.10.4", "dayjs": "1.10.4",
"ejs": "3.1.6", "ejs": "3.1.6",
"electron-debug": "3.2.0", "electron-debug": "3.2.0",
"electron-dl": "3.1.0", "electron-dl": "3.2.0",
"electron-find": "1.0.6", "electron-find": "1.0.6",
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"express": "4.17.1", "express": "4.17.1",
@@ -51,10 +51,11 @@
"is-animated": "^2.0.1", "is-animated": "^2.0.1",
"is-svg": "4.2.1", "is-svg": "4.2.1",
"jimp": "0.16.1", "jimp": "0.16.1",
"jsdom": "^16.4.0", "joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "16.5.0",
"mime-types": "2.1.29", "mime-types": "2.1.29",
"multer": "1.4.2", "multer": "1.4.2",
"node-abi": "2.19.3", "node-abi": "2.26.0",
"open": "7.4.2", "open": "7.4.2",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"rand-token": "1.0.1", "rand-token": "1.0.1",
@@ -70,16 +71,15 @@
"striptags": "3.1.1", "striptags": "3.1.1",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"turndown": "7.0.0", "turndown": "7.0.0",
"joplin-turndown-plugin-gfm": "1.0.12",
"unescape": "1.0.1", "unescape": "1.0.1",
"ws": "7.4.3", "ws": "7.4.4",
"yauzl": "2.10.0", "yauzl": "2.10.0",
"yazl": "2.5.1" "yazl": "2.5.1"
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "9.4.3", "electron": "9.4.4",
"electron-builder": "22.9.1", "electron-builder": "22.10.5",
"electron-packager": "15.2.0", "electron-packager": "15.2.0",
"electron-rebuild": "2.3.5", "electron-rebuild": "2.3.5",
"esm": "3.2.25", "esm": "3.2.25",
@@ -87,7 +87,7 @@
"jsdoc": "3.6.6", "jsdoc": "3.6.6",
"lorem-ipsum": "2.0.3", "lorem-ipsum": "2.0.3",
"rcedit": "3.0.0", "rcedit": "3.0.0",
"webpack": "5.24.2", "webpack": "5.24.4",
"webpack-cli": "4.5.0" "webpack-cli": "4.5.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@@ -87,14 +87,16 @@ describe("Lexer expression", () => {
.toEqual(["#label", "*=*", "text"]); .toEqual(["#label", "*=*", "text"]);
}); });
it("simple label operator with in quotes and without", () => { it("simple label operator with in quotes", () => {
expect(lex("#label*=*'text'").expressionTokens) expect(lex("#label*=*'text'").expressionTokens)
.toEqual([ .toEqual([
{token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5},
{token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8}, {token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8},
{token: "text", inQuotes: true, startIndex: 10, endIndex: 13} {token: "text", inQuotes: true, startIndex: 10, endIndex: 13}
]); ]);
});
it("simple label operator with param without quotes", () => {
expect(lex("#label*=*text").expressionTokens) expect(lex("#label*=*text").expressionTokens)
.toEqual([ .toEqual([
{token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5},
@@ -103,6 +105,16 @@ describe("Lexer expression", () => {
]); ]);
}); });
it("simple label operator with empty string param", () => {
expect(lex("#label = ''").expressionTokens)
.toEqual([
{token: "#label", inQuotes: false, startIndex: 0, endIndex: 5},
{token: "=", inQuotes: false, startIndex: 7, endIndex: 7},
// weird case for empty strings which ends up with endIndex < startIndex :-(
{token: "", inQuotes: true, startIndex: 10, endIndex: 9}
]);
});
it("note. prefix also separates fulltext from expression", () => { it("note. prefix also separates fulltext from expression", () => {
expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(t => t.token)) expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(t => t.token))
.toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]); .toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]);

View File

@@ -19,6 +19,13 @@ function tokens(toks, cur = 0) {
}); });
} }
function assertIsArchived(exp) {
expect(exp.constructor.name).toEqual("PropertyComparisonExp");
expect(exp.propertyName).toEqual("isArchived");
expect(exp.operator).toEqual("=");
expect(exp.comparedValue).toEqual("false");
}
describe("Parser", () => { describe("Parser", () => {
it("fulltext parser without content", () => { it("fulltext parser without content", () => {
const rootExp = parse({ const rootExp = parse({
@@ -29,8 +36,9 @@ describe("Parser", () => {
expect(rootExp.constructor.name).toEqual("AndExp"); expect(rootExp.constructor.name).toEqual("AndExp");
expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp");
expect(rootExp.subExpressions[1].constructor.name).toEqual("NoteCacheFlatTextExp"); expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp");
expect(rootExp.subExpressions[1].tokens).toEqual(["hello", "hi"]); expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp");
expect(rootExp.subExpressions[1].subExpressions[0].tokens).toEqual(["hello", "hi"]);
}); });
it("fulltext parser with content", () => { it("fulltext parser with content", () => {
@@ -40,9 +48,12 @@ describe("Parser", () => {
searchContext: new SearchContext({includeNoteContent: true}) searchContext: new SearchContext({includeNoteContent: true})
}); });
expect(rootExp.constructor.name).toEqual("OrExp"); expect(rootExp.constructor.name).toEqual("AndExp");
assertIsArchived(rootExp.subExpressions[0]);
const subs = rootExp.subExpressions; expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp");
const subs = rootExp.subExpressions[1].subExpressions;
expect(subs[0].constructor.name).toEqual("NoteCacheFlatTextExp"); expect(subs[0].constructor.name).toEqual("NoteCacheFlatTextExp");
expect(subs[0].tokens).toEqual(["hello", "hi"]); expect(subs[0].tokens).toEqual(["hello", "hi"]);
@@ -61,10 +72,12 @@ describe("Parser", () => {
searchContext: new SearchContext() searchContext: new SearchContext()
}); });
expect(rootExp.constructor.name).toEqual("LabelComparisonExp"); expect(rootExp.constructor.name).toEqual("AndExp");
expect(rootExp.attributeType).toEqual("label"); assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.attributeName).toEqual("mylabel"); expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp");
expect(rootExp.comparator).toBeTruthy(); expect(rootExp.subExpressions[1].attributeType).toEqual("label");
expect(rootExp.subExpressions[1].attributeName).toEqual("mylabel");
expect(rootExp.subExpressions[1].comparator).toBeTruthy();
}); });
it("simple attribute negation", () => { it("simple attribute negation", () => {
@@ -74,10 +87,12 @@ describe("Parser", () => {
searchContext: new SearchContext() searchContext: new SearchContext()
}); });
expect(rootExp.constructor.name).toEqual("NotExp"); expect(rootExp.constructor.name).toEqual("AndExp");
expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpression.attributeType).toEqual("label"); expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp");
expect(rootExp.subExpression.attributeName).toEqual("mylabel"); expect(rootExp.subExpressions[1].subExpression.constructor.name).toEqual("AttributeExistsExp");
expect(rootExp.subExpressions[1].subExpression.attributeType).toEqual("label");
expect(rootExp.subExpressions[1].subExpression.attributeName).toEqual("mylabel");
rootExp = parse({ rootExp = parse({
fulltextTokens: [], fulltextTokens: [],
@@ -85,10 +100,12 @@ describe("Parser", () => {
searchContext: new SearchContext() searchContext: new SearchContext()
}); });
expect(rootExp.constructor.name).toEqual("NotExp"); expect(rootExp.constructor.name).toEqual("AndExp");
expect(rootExp.subExpression.constructor.name).toEqual("AttributeExistsExp"); assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpression.attributeType).toEqual("relation"); expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp");
expect(rootExp.subExpression.attributeName).toEqual("myrelation"); expect(rootExp.subExpressions[1].subExpression.constructor.name).toEqual("AttributeExistsExp");
expect(rootExp.subExpressions[1].subExpression.attributeType).toEqual("relation");
expect(rootExp.subExpressions[1].subExpression.attributeName).toEqual("myrelation");
}); });
it("simple label AND", () => { it("simple label AND", () => {
@@ -99,7 +116,10 @@ describe("Parser", () => {
}); });
expect(rootExp.constructor.name).toEqual("AndExp"); expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions; assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions;
expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first"); expect(firstSub.attributeName).toEqual("first");
@@ -116,7 +136,10 @@ describe("Parser", () => {
}); });
expect(rootExp.constructor.name).toEqual("AndExp"); expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions; assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions;
expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first"); expect(firstSub.attributeName).toEqual("first");
@@ -132,8 +155,11 @@ describe("Parser", () => {
searchContext: new SearchContext() searchContext: new SearchContext()
}); });
expect(rootExp.constructor.name).toEqual("OrExp"); expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions; assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp");
const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions;
expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first"); expect(firstSub.attributeName).toEqual("first");
@@ -155,8 +181,9 @@ describe("Parser", () => {
expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); expect(firstSub.constructor.name).toEqual("PropertyComparisonExp");
expect(firstSub.propertyName).toEqual('isArchived'); expect(firstSub.propertyName).toEqual('isArchived');
expect(secondSub.constructor.name).toEqual("NoteCacheFlatTextExp"); expect(secondSub.constructor.name).toEqual("OrExp");
expect(secondSub.tokens).toEqual(["hello"]); expect(secondSub.subExpressions[0].constructor.name).toEqual("NoteCacheFlatTextExp");
expect(secondSub.subExpressions[0].tokens).toEqual(["hello"]);
expect(thirdSub.constructor.name).toEqual("LabelComparisonExp"); expect(thirdSub.constructor.name).toEqual("LabelComparisonExp");
expect(thirdSub.attributeName).toEqual("mylabel"); expect(thirdSub.attributeName).toEqual("mylabel");
@@ -169,8 +196,11 @@ describe("Parser", () => {
searchContext: new SearchContext() searchContext: new SearchContext()
}); });
expect(rootExp.constructor.name).toEqual("OrExp"); expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions; assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp");
const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions;
expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first"); expect(firstSub.attributeName).toEqual("first");
@@ -232,10 +262,12 @@ describe("Invalid expressions", () => {
searchContext: new SearchContext() searchContext: new SearchContext()
}); });
expect(rootExp.constructor.name).toEqual("LabelComparisonExp"); expect(rootExp.constructor.name).toEqual("AndExp");
expect(rootExp.attributeType).toEqual("label"); assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.attributeName).toEqual("first"); expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp");
expect(rootExp.comparator).toBeTruthy(); expect(rootExp.subExpressions[1].attributeType).toEqual("label");
expect(rootExp.subExpressions[1].attributeName).toEqual("first");
expect(rootExp.subExpressions[1].comparator).toBeTruthy();
}); });
it("searching by relation without note property", () => { it("searching by relation without note property", () => {

View File

@@ -562,8 +562,8 @@ describe("Search", () => {
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria"); expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy"); expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy");
searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 0', searchContext); searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1', searchContext);
expect(searchResults.length).toEqual(0); expect(searchResults.length).toEqual(1);
searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); searchResults = searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext);
expect(searchResults.length).toEqual(4); expect(searchResults.length).toEqual(4);

View File

@@ -1,6 +1,9 @@
const {note} = require('./note_cache_mocking.js'); const {note} = require('./note_cache_mocking.js');
const ValueExtractor = require('../../src/services/search/value_extractor.js'); const ValueExtractor = require('../../src/services/search/value_extractor.js');
const noteCache = require('../../src/services/note_cache/note_cache.js'); const noteCache = require('../../src/services/note_cache/note_cache.js');
const SearchContext = require("../../src/services/search/search_context.js");
const dsc = new SearchContext();
describe("Value extractor", () => { describe("Value extractor", () => {
beforeEach(() => { beforeEach(() => {
@@ -10,7 +13,7 @@ describe("Value extractor", () => {
it("simple title extraction", async () => { it("simple title extraction", async () => {
const europe = note("Europe").note; const europe = note("Europe").note;
const valueExtractor = new ValueExtractor(["note", "title"]); const valueExtractor = new ValueExtractor(dsc, ["note", "title"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(europe)).toEqual("Europe"); expect(valueExtractor.extract(europe)).toEqual("Europe");
@@ -21,12 +24,12 @@ describe("Value extractor", () => {
.label("Capital", "Vienna") .label("Capital", "Vienna")
.note; .note;
let valueExtractor = new ValueExtractor(["note", "labels", "capital"]); let valueExtractor = new ValueExtractor(dsc, ["note", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria)).toEqual("Vienna"); expect(valueExtractor.extract(austria)).toEqual("Vienna");
valueExtractor = new ValueExtractor(["#capital"]); valueExtractor = new ValueExtractor(dsc, ["#capital"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria)).toEqual("Vienna"); expect(valueExtractor.extract(austria)).toEqual("Vienna");
@@ -38,12 +41,12 @@ describe("Value extractor", () => {
.child(note("Austria") .child(note("Austria")
.child(vienna)); .child(vienna));
let valueExtractor = new ValueExtractor(["note", "children", "children", "title"]); let valueExtractor = new ValueExtractor(dsc, ["note", "children", "children", "title"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(europe.note)).toEqual("Vienna"); expect(valueExtractor.extract(europe.note)).toEqual("Vienna");
valueExtractor = new ValueExtractor(["note", "parents", "parents", "title"]); valueExtractor = new ValueExtractor(dsc, ["note", "parents", "parents", "title"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(vienna.note)).toEqual("Europe"); expect(valueExtractor.extract(vienna.note)).toEqual("Europe");
@@ -56,12 +59,12 @@ describe("Value extractor", () => {
.relation('neighbor', czechRepublic.note) .relation('neighbor', czechRepublic.note)
.relation('neighbor', slovakia.note); .relation('neighbor', slovakia.note);
let valueExtractor = new ValueExtractor(["note", "relations", "neighbor", "labels", "capital"]); let valueExtractor = new ValueExtractor(dsc, ["note", "relations", "neighbor", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria.note)).toEqual("Prague"); expect(valueExtractor.extract(austria.note)).toEqual("Prague");
valueExtractor = new ValueExtractor(["~neighbor", "labels", "capital"]); valueExtractor = new ValueExtractor(dsc, ["~neighbor", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria.note)).toEqual("Prague"); expect(valueExtractor.extract(austria.note)).toEqual("Prague");
@@ -70,17 +73,17 @@ describe("Value extractor", () => {
describe("Invalid value extractor property path", () => { describe("Invalid value extractor property path", () => {
it('each path must start with "note" (or label/relation)', it('each path must start with "note" (or label/relation)',
() => expect(new ValueExtractor(["neighbor"]).validate()).toBeTruthy()); () => expect(new ValueExtractor(dsc, ["neighbor"]).validate()).toBeTruthy());
it("extra path element after terminal label", it("extra path element after terminal label",
() => expect(new ValueExtractor(["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy()); () => expect(new ValueExtractor(dsc, ["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy());
it("extra path element after terminal title", it("extra path element after terminal title",
() => expect(new ValueExtractor(["note", "title", "isProtected"]).validate()).toBeTruthy()); () => expect(new ValueExtractor(dsc, ["note", "title", "isProtected"]).validate()).toBeTruthy());
it("relation name and note property is missing", it("relation name and note property is missing",
() => expect(new ValueExtractor(["note", "relations"]).validate()).toBeTruthy()); () => expect(new ValueExtractor(dsc, ["note", "relations"]).validate()).toBeTruthy());
it("relation is specified but target note property is not specified", it("relation is specified but target note property is not specified",
() => expect(new ValueExtractor(["note", "relations", "myrel"]).validate()).toBeTruthy()); () => expect(new ValueExtractor(dsc, ["note", "relations", "myrel"]).validate()).toBeTruthy());
}); });

View File

@@ -41,7 +41,7 @@ class Entity {
} }
getUtcDateChanged() { getUtcDateChanged() {
return this.utcDateModified; return this.utcDateModified || this.utcDateCreated;
} }
get repository() { get repository() {

View File

@@ -49,8 +49,13 @@ class Note extends Entity {
this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable(); this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable();
if (this.isContentAvailable) { if (this.isContentAvailable) {
try {
this.title = protectedSessionService.decryptString(this.title); this.title = protectedSessionService.decryptString(this.title);
} }
catch (e) {
throw new Error(`Could not decrypt title of note ${this.noteId}: ${e.message} ${e.stack}`)
}
}
else { else {
this.title = "[protected]"; this.title = "[protected]";
} }
@@ -156,14 +161,14 @@ class Note extends Entity {
sql.upsert("note_contents", "noteId", pojo); sql.upsert("note_contents", "noteId", pojo);
const hash = utils.hash(this.noteId + "|" + content.toString()); const hash = utils.hash(this.noteId + "|" + pojo.content.toString());
entityChangesService.addEntityChange({ entityChangesService.addEntityChange({
entityName: 'note_contents', entityName: 'note_contents',
entityId: this.noteId, entityId: this.noteId,
hash: hash, hash: hash,
isErased: false, isErased: false,
utcDateChanged: this.getUtcDateChanged() utcDateChanged: pojo.utcDateModified
}, null); }, null);
} }
@@ -365,7 +370,6 @@ class Note extends Entity {
WHERE attributes.isDeleted = 0 WHERE attributes.isDeleted = 0
AND attributes.type = 'relation' AND attributes.type = 'relation'
AND attributes.name = 'template' AND attributes.name = 'template'
AND (treeWithAttrs.level = 0 OR attributes.isInheritable = 1)
) )
SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId
WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR treeWithAttrs.level = 0) WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR treeWithAttrs.level = 0)

View File

@@ -29,6 +29,16 @@ const TPL = `
</div> </div>
</div> </div>
<div class="form-group row">
<div class="col-4">
<label for="heading-style">Heading style</label>
<select class="form-control" id="heading-style">
<option value="plain">Plain</option>
<option value="markdown">Markdown-style</option>
</select>
</div>
</div>
<p>Zooming can be controlled with CTRL+- and CTRL+= shortcuts as well.</p> <p>Zooming can be controlled with CTRL+- and CTRL+= shortcuts as well.</p>
<h4>Font sizes</h4> <h4>Font sizes</h4>
@@ -78,6 +88,7 @@ export default class ApperanceOptions {
this.$themeSelect = $("#theme-select"); this.$themeSelect = $("#theme-select");
this.$zoomFactorSelect = $("#zoom-factor-select"); this.$zoomFactorSelect = $("#zoom-factor-select");
this.$nativeTitleBarSelect = $("#native-title-bar-select"); this.$nativeTitleBarSelect = $("#native-title-bar-select");
this.$headingStyle = $("#heading-style");
this.$mainFontSize = $("#main-font-size"); this.$mainFontSize = $("#main-font-size");
this.$treeFontSize = $("#tree-font-size"); this.$treeFontSize = $("#tree-font-size");
this.$detailFontSize = $("#detail-font-size"); this.$detailFontSize = $("#detail-font-size");
@@ -86,11 +97,7 @@ export default class ApperanceOptions {
this.$themeSelect.on('change', () => { this.$themeSelect.on('change', () => {
const newTheme = this.$themeSelect.val(); const newTheme = this.$themeSelect.val();
for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes this.toggleBodyClass("theme-", newTheme);
if (clazz.startsWith("theme-")) {
this.$body.removeClass(clazz);
}
}
const noteId = $(this).find(":selected").attr("data-note-id"); const noteId = $(this).find(":selected").attr("data-note-id");
@@ -100,8 +107,6 @@ export default class ApperanceOptions {
libraryLoader.requireCss(`api/notes/download/${noteId}`); libraryLoader.requireCss(`api/notes/download/${noteId}`);
} }
this.$body.addClass("theme-" + newTheme);
server.put('options/theme/' + newTheme); server.put('options/theme/' + newTheme);
}); });
@@ -113,6 +118,14 @@ export default class ApperanceOptions {
server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible); server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible);
}); });
this.$headingStyle.on('change', () => {
const newHeadingStyle = this.$headingStyle.val();
this.toggleBodyClass("heading-style-", newHeadingStyle);
server.put('options/headingStyle/' + newHeadingStyle);
});
this.$mainFontSize.on('change', async () => { this.$mainFontSize.on('change', async () => {
await server.put('options/mainFontSize/' + this.$mainFontSize.val()); await server.put('options/mainFontSize/' + this.$mainFontSize.val());
@@ -132,6 +145,16 @@ export default class ApperanceOptions {
}); });
} }
toggleBodyClass(prefix, value) {
for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz.startsWith(prefix)) {
this.$body.removeClass(clazz);
}
}
this.$body.addClass(prefix + value);
}
async optionsLoaded(options) { async optionsLoaded(options) {
const themes = [ const themes = [
{ val: 'white', title: 'White' }, { val: 'white', title: 'White' },
@@ -159,6 +182,8 @@ export default class ApperanceOptions {
this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide'); this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide');
this.$headingStyle.val(options.headingStyle);
this.$mainFontSize.val(options.mainFontSize); this.$mainFontSize.val(options.mainFontSize);
this.$treeFontSize.val(options.treeFontSize); this.$treeFontSize.val(options.treeFontSize);
this.$detailFontSize.val(options.detailFontSize); this.$detailFontSize.val(options.detailFontSize);

View File

@@ -0,0 +1,24 @@
import server from "../services/server.js";
import utils from "../services/utils.js";
const $dialog = $("#sort-child-notes-dialog");
const $form = $("#sort-child-notes-form");
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();
await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection});
utils.closeActiveDialog();
});
export async function showDialog(noteId) {
parentNoteId = noteId;
utils.openDialog($dialog);
$form.find('input:first').focus();
}

View File

@@ -2,6 +2,7 @@ import server from '../services/server.js';
import noteAttributeCache from "../services/note_attribute_cache.js"; import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
import options from "../services/options.js"; import options from "../services/options.js";
import treeCache from "../services/tree_cache.js";
const LABEL = 'label'; const LABEL = 'label';
const RELATION = 'relation'; const RELATION = 'relation';
@@ -253,6 +254,72 @@ class NoteShort {
return noteAttributeCache.attributes[this.noteId]; return noteAttributeCache.attributes[this.noteId];
} }
getAllNotePaths(encounteredNoteIds = null) {
if (this.noteId === 'root') {
return [['root']];
}
if (!encounteredNoteIds) {
encounteredNoteIds = new Set();
}
encounteredNoteIds.add(this.noteId);
const parentNotes = this.getParentNotes();
let paths;
if (parentNotes.length === 1) { // optimization for the most common case
if (encounteredNoteIds.has(parentNotes[0].noteId)) {
return [];
}
else {
paths = parentNotes[0].getAllNotePaths(encounteredNoteIds);
}
}
else {
paths = [];
for (const parentNote of parentNotes) {
if (encounteredNoteIds.has(parentNote.noteId)) {
continue;
}
const newSet = new Set(encounteredNoteIds);
paths.push(...parentNote.getAllNotePaths(newSet));
}
}
for (const path of paths) {
path.push(this.noteId);
}
return paths;
}
getSortedNotePaths(hoistedNotePath = 'root') {
const notePaths = this.getAllNotePaths().map(path => ({
notePath: path,
isInHoistedSubTree: path.includes(hoistedNotePath),
isArchived: path.find(noteId => treeCache.notes[noteId].hasLabel('archived')),
isSearch: path.find(noteId => treeCache.notes[noteId].type === 'search')
}));
notePaths.sort((a, b) => {
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
return a.isInHoistedSubTree ? -1 : 1;
} else if (a.isSearch !== b.isSearch) {
return a.isSearch ? 1 : -1;
} else if (a.isArchived !== b.isArchived) {
return a.isArchived ? 1 : -1;
} else {
return a.notePath.length - b.notePath.length;
}
});
return notePaths;
}
__filterAttrs(attributes, type, name) { __filterAttrs(attributes, type, name) {
if (!type && !name) { if (!type && !name) {
return attributes; return attributes;
@@ -292,7 +359,7 @@ class NoteShort {
const workspaceIconClass = this.getWorkspaceIconClass(); const workspaceIconClass = this.getWorkspaceIconClass();
if (iconClassLabels.length > 0) { if (iconClassLabels.length > 0) {
return iconClassLabels.map(l => l.value).join(' '); return iconClassLabels[0].value;
} }
else if (workspaceIconClass) { else if (workspaceIconClass) {
return workspaceIconClass; return workspaceIconClass;
@@ -542,7 +609,7 @@ class NoteShort {
}); });
} }
hasAncestor(ancestorNote, visitedNoteIds) { hasAncestor(ancestorNote, visitedNoteIds = null) {
if (this.noteId === ancestorNote.noteId) { if (this.noteId === ancestorNote.noteId) {
return true; return true;
} }

View File

@@ -83,6 +83,9 @@ export default class MobileLayout {
.child(new NoteTitleWidget()) .child(new NoteTitleWidget())
.child(new CloseDetailButtonWidget())) .child(new CloseDetailButtonWidget()))
.child(new NoteDetailWidget() .child(new NoteDetailWidget()
.css('padding', '5px 20px 10px 0'))); .css('padding', '5px 20px 10px 0')
.css('overflow', 'auto')
.css('height', '100%')
));
} }
} }

View File

@@ -12,6 +12,7 @@ import keyboardActionsService from "./keyboard_actions.js";
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js"; import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
import MainTreeExecutors from "./main_tree_executors.js"; import MainTreeExecutors from "./main_tree_executors.js";
import protectedSessionHolder from "./protected_session_holder.js"; import protectedSessionHolder from "./protected_session_holder.js";
import toast from "./toast.js";
class AppContext extends Component { class AppContext extends Component {
constructor(isMainWindow) { constructor(isMainWindow) {
@@ -19,6 +20,7 @@ class AppContext extends Component {
this.isMainWindow = isMainWindow; this.isMainWindow = isMainWindow;
this.executors = []; this.executors = [];
this.beforeUnloadListeners = [];
} }
setLayout(layout) { setLayout(layout) {
@@ -104,6 +106,15 @@ class AppContext extends Component {
getComponentByEl(el) { getComponentByEl(el) {
return $(el).closest(".component").prop('component'); return $(el).closest(".component").prop('component');
} }
addBeforeUnloadListener(obj) {
if (typeof WeakRef !== "function") {
// older browsers don't support WeakRef
return;
}
this.beforeUnloadListeners.push(new WeakRef(obj));
}
} }
const appContext = new AppContext(window.glob.isMainWindow); const appContext = new AppContext(window.glob.isMainWindow);
@@ -112,7 +123,29 @@ const appContext = new AppContext(window.glob.isMainWindow);
$(window).on('beforeunload', () => { $(window).on('beforeunload', () => {
protectedSessionHolder.resetSessionCookie(); protectedSessionHolder.resetSessionCookie();
appContext.triggerEvent('beforeUnload'); let allSaved = true;
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());
for (const weakRef of appContext.beforeUnloadListeners) {
const component = weakRef.deref();
if (!component) {
continue;
}
if (!component.beforeUnloadEvent()) {
console.log(`Component ${component.componentId} is not finished saving its state.`);
toast.showMessage("Please wait for a couple of seconds for the save to finish, then you can try again.", 10000);
allSaved = false;
}
}
if (!allSaved) {
return "some string";
}
}); });
function isNotePathInAddress() { function isNotePathInAddress() {

View File

@@ -79,6 +79,15 @@ async function renderAttributes(attributes, renderIsInheritable) {
return $container; return $container;
} }
const HIDDEN_ATTRIBUTES = [
'originalFileName',
'template',
'cssClass',
'iconClass',
'pageSize',
'viewType'
];
async function renderNormalAttributes(note) { async function renderNormalAttributes(note) {
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
let attrs = note.getAttributes(); let attrs = note.getAttributes();
@@ -90,6 +99,7 @@ async function renderNormalAttributes(note) {
attrs = attrs.filter( attrs = attrs.filter(
attr => !attr.isDefinition() attr => !attr.isDefinition()
&& !attr.isAutoLink && !attr.isAutoLink
&& !HIDDEN_ATTRIBUTES.includes(attr.name)
&& attr.noteId === note.noteId && attr.noteId === note.noteId
); );
} }

View File

@@ -125,7 +125,7 @@ async function deleteNotes(branchIdsToDelete) {
} }
async function moveNodeUpInHierarchy(node) { async function moveNodeUpInHierarchy(node) {
if (hoistedNoteService.isRootNode(node) if (hoistedNoteService.isHoistedNode(node)
|| hoistedNoteService.isTopLevelNode(node) || hoistedNoteService.isTopLevelNode(node)
|| node.getParent().data.noteType === 'search') { || node.getParent().data.noteType === 'search') {
return; return;

View File

@@ -74,7 +74,11 @@ export default class Entrypoints extends Component {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.openTabWithNote(note.noteId, true); const hoistedNoteId = appContext.tabManager.getActiveTabContext()
? appContext.tabManager.getActiveTabContext().hoistedNoteId
: 'root';
await appContext.tabManager.openTabWithNote(note.noteId, true, null, hoistedNoteId);
appContext.triggerEvent('focusAndSelectTitle'); appContext.triggerEvent('focusAndSelectTitle');
} }
@@ -182,8 +186,6 @@ export default class Entrypoints extends Component {
utils.reloadApp(); utils.reloadApp();
} }
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
async openInWindowCommand({notePath, hoistedNoteId}) { async openInWindowCommand({notePath, hoistedNoteId}) {
if (!hoistedNoteId) { if (!hoistedNoteId) {
hoistedNoteId = 'root'; hoistedNoteId = 'root';

View File

@@ -16,19 +16,17 @@ async function unhoist() {
} }
function isTopLevelNode(node) { function isTopLevelNode(node) {
return isRootNode(node.getParent()); return isHoistedNode(node.getParent());
} }
function isRootNode(node) { function isHoistedNode(node) {
// even though check for 'root' should not be necessary, we keep it just in case // even though check for 'root' should not be necessary, we keep it just in case
return node.data.noteId === "root" return node.data.noteId === "root"
|| node.data.noteId === getHoistedNoteId(); || node.data.noteId === getHoistedNoteId();
} }
async function checkNoteAccess(notePath, tabContext) { async function checkNoteAccess(notePath, tabContext) {
// notePath argument can contain only noteId which is not good when hoisted since const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId);
// then we need to check the whole note path
const resolvedNotePath = await treeService.resolveNotePath(notePath);
if (!resolvedNotePath) { if (!resolvedNotePath) {
console.log("Cannot activate " + notePath); console.log("Cannot activate " + notePath);
@@ -37,7 +35,7 @@ async function checkNoteAccess(notePath, tabContext) {
const hoistedNoteId = tabContext.hoistedNoteId; const hoistedNoteId = tabContext.hoistedNoteId;
if (hoistedNoteId !== 'root' && !resolvedNotePath.includes(hoistedNoteId)) { if (!resolvedNotePath.includes(hoistedNoteId)) {
const confirmDialog = await import('../dialogs/confirm.js'); const confirmDialog = await import('../dialogs/confirm.js');
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?")) { if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?")) {
@@ -55,6 +53,6 @@ export default {
getHoistedNoteId, getHoistedNoteId,
unhoist, unhoist,
isTopLevelNode, isTopLevelNode,
isRootNode, isHoistedNode,
checkNoteAccess checkNoteAccess
} }

View File

@@ -117,7 +117,8 @@ export default class LinkMap {
const $noteBox = $("<div>") const $noteBox = $("<div>")
.addClass("note-box") .addClass("note-box")
.prop("id", noteBoxId); .prop("id", noteBoxId)
.addClass(note.getCssClass());
const $link = $linkTitles[noteId]; const $link = $linkTitles[noteId];

View File

@@ -27,28 +27,28 @@ export default class MainTreeExecutors extends Component {
} }
async createNoteIntoCommand() { async createNoteIntoCommand() {
const activeNote = appContext.tabManager.getActiveTabNote(); const activeTabContext = appContext.tabManager.getActiveTabContext();
if (!activeNote) { if (!activeTabContext) {
return; return;
} }
await noteCreateService.createNote(activeNote.noteId, { await noteCreateService.createNote(activeTabContext.notePath, {
isProtected: activeNote.isProtected, isProtected: activeTabContext.note.isProtected,
saveSelection: false saveSelection: false
}); });
} }
async createNoteAfterCommand() { async createNoteAfterCommand() {
const node = this.tree.getActiveNode(); const node = this.tree.getActiveNode();
const parentNoteId = node.data.parentNoteId; const parentNotePath = treeService.getNotePath(node.getParent());
const isProtected = await treeService.getParentProtectedStatus(node); const isProtected = await treeService.getParentProtectedStatus(node);
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) { if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
return; return;
} }
await noteCreateService.createNote(parentNoteId, { await noteCreateService.createNote(parentNotePath, {
target: 'after', target: 'after',
targetBranchId: node.data.branchId, targetBranchId: node.data.branchId,
isProtected: isProtected, isProtected: isProtected,

View File

@@ -1,19 +1,13 @@
import hoistedNoteService from "./hoisted_note.js";
import appContext from "./app_context.js"; import appContext from "./app_context.js";
import utils from "./utils.js"; import utils from "./utils.js";
import protectedSessionHolder from "./protected_session_holder.js"; import protectedSessionHolder from "./protected_session_holder.js";
import server from "./server.js"; import server from "./server.js";
import ws from "./ws.js"; import ws from "./ws.js";
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import treeService from "./tree.js";
import toastService from "./toast.js"; import toastService from "./toast.js";
async function createNewTopLevelNote() { async function createNote(parentNotePath, options = {}) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
await createNote(hoistedNoteId);
}
async function createNote(parentNoteId, options = {}) {
options = Object.assign({ options = Object.assign({
activate: true, activate: true,
focus: 'title', focus: 'title',
@@ -36,6 +30,8 @@ async function createNote(parentNoteId, options = {}) {
const newNoteName = options.title || "new note"; const newNoteName = options.title || "new note";
const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath);
const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId}`, { const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId}`, {
title: newNoteName, title: newNoteName,
content: options.content || "", content: options.content || "",
@@ -53,7 +49,7 @@ async function createNote(parentNoteId, options = {}) {
if (options.activate) { if (options.activate) {
const activeTabContext = appContext.tabManager.getActiveTabContext(); const activeTabContext = appContext.tabManager.getActiveTabContext();
await activeTabContext.setNote(note.noteId); await activeTabContext.setNote(`${parentNotePath}/${note.noteId}`);
if (options.focus === 'title') { if (options.focus === 'title') {
appContext.triggerEvent('focusAndSelectTitle'); appContext.triggerEvent('focusAndSelectTitle');
@@ -88,12 +84,13 @@ function parseSelectedHtml(selectedHtml) {
} }
} }
async function duplicateSubtree(noteId, parentNoteId) { async function duplicateSubtree(noteId, parentNotePath) {
const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath);
const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`); const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`);
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.activateOrOpenNote(note.noteId); await appContext.tabManager.activateOrOpenNote(`${parentNotePath}/${note.noteId}`);
const origNote = await treeCache.getNote(noteId); const origNote = await treeCache.getNote(noteId);
toastService.showMessage(`Note "${origNote.title}" has been duplicated`); toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
@@ -101,6 +98,5 @@ async function duplicateSubtree(noteId, parentNoteId) {
export default { export default {
createNote, createNote,
createNewTopLevelNote,
duplicateSubtree duplicateSubtree
}; };

View File

@@ -1,5 +1,6 @@
import utils from "./utils.js"; import utils from "./utils.js";
import options from './options.js'; import options from './options.js';
import server from "./server.js";
const PROTECTED_SESSION_ID_KEY = 'protectedSessionId'; const PROTECTED_SESSION_ID_KEY = 'protectedSessionId';
@@ -23,11 +24,11 @@ function resetSessionCookie() {
utils.setSessionCookie(PROTECTED_SESSION_ID_KEY, null); utils.setSessionCookie(PROTECTED_SESSION_ID_KEY, null);
} }
function resetProtectedSession() { async function resetProtectedSession() {
resetSessionCookie(); resetSessionCookie();
// most secure solution - guarantees nothing remained in memory await server.post("logout/protected");
// since this expires because user doesn't use the app, it shouldn't be disruptive
utils.reloadApp(); utils.reloadApp();
} }

View File

@@ -15,9 +15,25 @@ export default class SpacedUpdate {
async updateNowIfNecessary() { async updateNowIfNecessary() {
if (this.changed) { if (this.changed) {
this.changed = false; this.changed = false; // optimistic...
try {
await this.updater(); await this.updater();
} }
catch (e) {
this.changed = true;
throw e;
}
}
}
isAllSavedAndTriggerUpdate() {
const allSaved = !this.changed;
this.updateNowIfNecessary();
return allSaved;
} }
triggerUpdate() { triggerUpdate() {

View File

@@ -79,7 +79,7 @@ class TabContext extends Component {
return inputNotePath; return inputNotePath;
} }
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath); const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId);
if (!resolvedNotePath) { if (!resolvedNotePath) {
logError(`Cannot resolve note path ${inputNotePath}`); logError(`Cannot resolve note path ${inputNotePath}`);

View File

@@ -27,6 +27,8 @@ export default class TabManager extends Component {
openTabs: JSON.stringify(openTabs) openTabs: JSON.stringify(openTabs)
}); });
}); });
appContext.addBeforeUnloadListener(this);
} }
/** @type {TabContext[]} */ /** @type {TabContext[]} */
@@ -203,7 +205,7 @@ export default class TabManager extends Component {
let hoistedNoteId = 'root'; let hoistedNoteId = 'root';
if (tabContext) { if (tabContext) {
const resolvedNotePath = await treeService.resolveNotePath(notePath); const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId);
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) { if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
hoistedNoteId = tabContext.hoistedNoteId; hoistedNoteId = tabContext.hoistedNoteId;
@@ -329,6 +331,8 @@ export default class TabManager extends Component {
beforeUnloadEvent() { beforeUnloadEvent() {
this.tabsUpdate.updateNowIfNecessary(); this.tabsUpdate.updateNowIfNecessary();
return true; // don't block closing the tab, this metadata is not that important
} }
openNewTabCommand() { openNewTabCommand() {

View File

@@ -8,8 +8,8 @@ import appContext from "./app_context.js";
/** /**
* @return {string|null} * @return {string|null}
*/ */
async function resolveNotePath(notePath) { async function resolveNotePath(notePath, hoistedNoteId = 'root') {
const runPath = await resolveNotePathToSegments(notePath); const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
return runPath ? runPath.join("/") : null; return runPath ? runPath.join("/") : null;
} }
@@ -21,7 +21,7 @@ async function resolveNotePath(notePath) {
* *
* @return {string[]} * @return {string[]}
*/ */
async function resolveNotePathToSegments(notePath, logErrors = true) { async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logErrors = true) {
utils.assertArguments(notePath); utils.assertArguments(notePath);
// we might get notePath with the tabId suffix, remove it if present // we might get notePath with the tabId suffix, remove it if present
@@ -37,7 +37,7 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
path.push('root'); path.push('root');
} }
const effectivePath = []; const effectivePathSegments = [];
let childNoteId = null; let childNoteId = null;
let i = 0; let i = 0;
@@ -75,13 +75,13 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
console.log(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}`); console.log(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}`);
} }
const someNotePath = getSomeNotePath(parents[0]); const someNotePath = getSomeNotePath(child, hoistedNoteId);
if (someNotePath) { // in case it's root the path may be empty if (someNotePath) { // in case it's root the path may be empty
const pathToRoot = someNotePath.split("/").reverse(); const pathToRoot = someNotePath.split("/").reverse().slice(1);
for (const noteId of pathToRoot) { for (const noteId of pathToRoot) {
effectivePath.push(noteId); effectivePathSegments.push(noteId);
} }
} }
@@ -89,36 +89,37 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
} }
} }
effectivePath.push(parentNoteId); effectivePathSegments.push(parentNoteId);
childNoteId = parentNoteId; childNoteId = parentNoteId;
} }
return effectivePath.reverse(); effectivePathSegments.reverse();
if (effectivePathSegments.includes(hoistedNoteId)) {
return effectivePathSegments;
}
else {
const note = await treeCache.getNote(getNoteIdFromNotePath(notePath));
const someNotePathSegments = getSomeNotePathSegments(note, hoistedNoteId);
// if there isn't actually any note path with hoisted note then return the original resolved note path
return someNotePathSegments.includes(hoistedNoteId) ? someNotePathSegments : effectivePathSegments;
}
} }
function getSomeNotePath(note) { function getSomeNotePathSegments(note, hoistedNotePath = 'root') {
utils.assertArguments(note); utils.assertArguments(note);
const path = []; const notePaths = note.getSortedNotePaths(hoistedNotePath);
let cur = note; return notePaths[0].notePath;
}
while (cur.noteId !== 'root') { function getSomeNotePath(note, hoistedNotePath = 'root') {
path.push(cur.noteId); const notePath = getSomeNotePathSegments(note, hoistedNotePath);
const parents = cur.getParentNotes().filter(note => note.type !== 'search'); return notePath.join('/');
if (!parents.length) {
logError(`Can't find parents for note ${cur.noteId}`);
return;
}
cur = parents[0];
}
path.push('root');
return path.reverse().join('/');
} }
async function sortAlphabetically(noteId) { async function sortAlphabetically(noteId) {
@@ -138,7 +139,7 @@ ws.subscribeToMessages(message => {
}); });
function getParentProtectedStatus(node) { function getParentProtectedStatus(node) {
return hoistedNoteService.isRootNode(node) ? 0 : node.getParent().data.isProtected; return hoistedNoteService.isHoistedNode(node) ? 0 : node.getParent().data.isProtected;
} }
function getNoteIdFromNotePath(notePath) { function getNoteIdFromNotePath(notePath) {
@@ -198,7 +199,7 @@ function getNotePath(node) {
const path = []; const path = [];
while (node && !hoistedNoteService.isRootNode(node)) { while (node) {
if (node.data.noteId) { if (node.data.noteId) {
path.push(node.data.noteId); path.push(node.data.noteId);
} }
@@ -206,10 +207,6 @@ function getNotePath(node) {
node = node.getParent(); node = node.getParent();
} }
if (node) { // null node can happen directly after unhoisting when tree is still hoisted but option has been changed already
path.push(node.data.noteId); // root or hoisted noteId
}
return path.reverse().join("/"); return path.reverse().join("/");
} }
@@ -313,6 +310,7 @@ export default {
resolveNotePath, resolveNotePath,
resolveNotePathToSegments, resolveNotePathToSegments,
getSomeNotePath, getSomeNotePath,
getSomeNotePathSegments,
getParentProtectedStatus, getParentProtectedStatus,
getNotePath, getNotePath,
getNoteIdFromNotePath, getNoteIdFromNotePath,

View File

@@ -75,7 +75,7 @@ class TreeContextMenu {
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes }, { 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: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes },
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes }, { title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
{ title: 'Sort alphabetically <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch }, { 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: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes }
] }, ] },
{ title: "----" }, { title: "----" },
@@ -112,10 +112,10 @@ class TreeContextMenu {
appContext.tabManager.openTabWithNoteWithHoisting(notePath); appContext.tabManager.openTabWithNoteWithHoisting(notePath);
} }
else if (command === "insertNoteAfter") { else if (command === "insertNoteAfter") {
const parentNoteId = this.node.data.parentNoteId; const parentNotePath = treeService.getNotePath(this.node.getParent());
const isProtected = await treeService.getParentProtectedStatus(this.node); const isProtected = await treeService.getParentProtectedStatus(this.node);
noteCreateService.createNote(parentNoteId, { noteCreateService.createNote(parentNotePath, {
target: 'after', target: 'after',
targetBranchId: this.node.data.branchId, targetBranchId: this.node.data.branchId,
type: type, type: type,
@@ -123,14 +123,14 @@ class TreeContextMenu {
}); });
} }
else if (command === "insertChildNote") { else if (command === "insertChildNote") {
noteCreateService.createNote(noteId, { const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
type: type, type: type,
isProtected: this.node.data.isProtected isProtected: this.node.data.isProtected
}); });
} }
else { else {
console.log("Triggering", command, notePath);
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath}); this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
} }
} }

View File

@@ -194,9 +194,18 @@ const ATTR_HELP = {
"appTheme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.", "appTheme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.",
"cssClass": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.", "cssClass": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.",
"iconClass": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.", "iconClass": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.",
"bookZoomLevel": 'applies only to book note and sets the "zoom level" (how many notes fit on 1 row)', "pageSize": "number of items per page in note listing",
"customRequestHandler": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>', "customRequestHandler": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>',
"customResourceProvider": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>' "customResourceProvider": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>',
"widget": "marks this note as a custom widget which will be added to the Trilium component tree",
"workspace": "marks this note as a workspace which allows easy hoisting",
"workspaceIconClass": "defines box icon CSS class which will be used in tab when hoisted to this note",
"workspaceTabBackgroundColor": "CSS color used in the note tab when hoisted to this note",
"searchHome": "new search notes will be created as children of this note",
"hoistedSearchHome": "new search notes will be created as children of this note when hoisted to some ancestor of this note",
"inbox": "default inbox location for new notes",
"hoistedInbox": "default inbox location for new notes when hoisted to some ancestor of this note",
"sqlConsoleHome": "default location of SQL console notes",
}, },
"relation": { "relation": {
"runOnNoteCreation": "executes when note is created on backend", "runOnNoteCreation": "executes when note is created on backend",

View File

@@ -491,7 +491,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
} }
async createNoteForReferenceLink(title) { async createNoteForReferenceLink(title) {
const {note} = await noteCreateService.createNote(this.noteId, { const {note} = await noteCreateService.createNote(this.notePath, {
activate: false, activate: false,
title: title title: title
}); });

View File

@@ -26,7 +26,7 @@ class MobileDetailMenuWidget extends BasicWidget {
], ],
selectMenuItemHandler: async ({command}) => { selectMenuItemHandler: async ({command}) => {
if (command === "insertChildNote") { if (command === "insertChildNote") {
noteCreateService.createNote(note.noteId); noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath());
} }
else if (command === "delete") { else if (command === "delete") {
const notePath = appContext.tabManager.getActiveTabNotePath(); const notePath = appContext.tabManager.getActiveTabNotePath();

View File

@@ -13,7 +13,7 @@ const WIDGET_TPL = `
} }
</style> </style>
<a data-trigger-command="createTopLevelNote" title="Create new top level note" class="icon-action bx bx-folder-plus"></a> <a data-trigger-command="createNoteIntoInbox" title="New note" class="icon-action bx bx-folder-plus"></a>
<a data-trigger-command="collapseTree" title="Collapse note tree" class="icon-action bx bx-layer-minus"></a> <a data-trigger-command="collapseTree" title="Collapse note tree" class="icon-action bx bx-layer-minus"></a>

View File

@@ -65,6 +65,8 @@ export default class NoteDetailWidget extends TabAwareWidget {
await server.put('notes/' + noteId, dto, this.componentId); await server.put('notes/' + noteId, dto, this.componentId);
}); });
appContext.addBeforeUnloadListener(this);
} }
isEnabled() { isEnabled() {
@@ -276,7 +278,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
const label = attrs.find(attr => const label = attrs.find(attr =>
attr.type === 'label' attr.type === 'label'
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'bookZoomLevel', 'displayRelations'].includes(attr.name) && ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'displayRelations'].includes(attr.name)
&& attr.isAffecting(this.note)); && attr.isAffecting(this.note));
const relation = attrs.find(attr => const relation = attrs.find(attr =>
@@ -293,7 +295,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
} }
beforeUnloadEvent() { beforeUnloadEvent() {
this.spacedUpdate.updateNowIfNecessary(); return this.spacedUpdate.isAllSavedAndTriggerUpdate();
} }
textPreviewDisabledEvent({tabContext}) { textPreviewDisabledEvent({tabContext}) {
@@ -316,7 +318,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
} }
// without await as this otherwise causes deadlock through component mutex // without await as this otherwise causes deadlock through component mutex
noteCreateService.createNote(note.noteId, { noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath(), {
isProtected: note.isProtected, isProtected: note.isProtected,
saveSelection: true saveSelection: true
}); });

View File

@@ -16,6 +16,7 @@ const TPL = `
border: 1px solid transparent; border: 1px solid transparent;
cursor: pointer; cursor: pointer;
padding: 6px; padding: 6px;
color: var(--main-text-color);
} }
.note-icon-container button.note-icon:hover { .note-icon-container button.note-icon:hover {

View File

@@ -15,9 +15,21 @@ const TPL = `
} }
.note-path-list { .note-path-list {
max-height: 600px; max-height: 700px;
overflow-y: auto; overflow-y: auto;
} }
.note-path-list .path-current {
font-weight: bold;
}
.note-path-list .path-archived {
color: var(--muted-text-color) !important;
}
.note-path-list .path-search {
font-style: italic;
}
</style> </style>
<button class="btn dropdown-toggle note-path-list-button bx bx-collection" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Note paths"></button> <button class="btn dropdown-toggle note-path-list-button bx bx-collection" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Note paths"></button>
@@ -43,20 +55,12 @@ export default class NotePathsWidget extends TabAwareWidget {
); );
if (this.noteId === 'root') { if (this.noteId === 'root') {
await this.addPath('root', true); await this.addPath('root');
return; return;
} }
const pathSegments = treeService.parseNotePath(this.notePath); for (const notePathRecord of this.note.getSortedNotePaths(this.hoistedNoteId)) {
const activeNoteParentNoteId = pathSegments[pathSegments.length - 2]; // we know this is not root so there must be a parent await this.addPath(notePathRecord);
for (const parentNote of this.note.getParentNotes()) {
const parentNotePath = treeService.getSomeNotePath(parentNote);
// this is to avoid having root notes leading '/'
const notePath = parentNotePath ? (parentNotePath + '/' + this.noteId) : this.noteId;
const isCurrent = activeNoteParentNoteId === parentNote.noteId;
await this.addPath(notePath, isCurrent);
} }
const cloneLink = $("<div>") const cloneLink = $("<div>")
@@ -70,7 +74,9 @@ export default class NotePathsWidget extends TabAwareWidget {
this.$notePathList.append(cloneLink); this.$notePathList.append(cloneLink);
} }
async addPath(notePath, isCurrent) { async addPath(notePathRecord) {
const notePath = notePathRecord.notePath.join('/');
const title = await treeService.getNotePathTitle(notePath); const title = await treeService.getNotePathTitle(notePath);
const $noteLink = await linkService.createNoteLink(notePath, {title}); const $noteLink = await linkService.createNoteLink(notePath, {title});
@@ -82,8 +88,33 @@ export default class NotePathsWidget extends TabAwareWidget {
.find('a') .find('a')
.addClass("no-tooltip-preview"); .addClass("no-tooltip-preview");
if (isCurrent) { const icons = [];
$noteLink.addClass("current");
if (this.notePath === notePath) {
$noteLink.addClass("path-current");
}
if (notePathRecord.isInHoistedSubTree) {
$noteLink.addClass("path-in-hoisted-subtree");
}
else {
icons.push(`<span class="bx bx-trending-up" title="This path is outside of hoisted note and you would have to unhoist."></span>`);
}
if (notePathRecord.isArchived) {
$noteLink.addClass("path-archived");
icons.push(`<span class="bx bx-archive" title="Archived"></span>`);
}
if (notePathRecord.isSearch) {
$noteLink.addClass("path-search");
icons.push(`<span class="bx bx-search" title="Search"></span>`);
}
if (icons.length > 0) {
$noteLink.append(` ${icons.join(' ')}`);
} }
this.$notePathList.append($noteLink); this.$notePathList.append($noteLink);
@@ -96,4 +127,10 @@ export default class NotePathsWidget extends TabAwareWidget {
this.refresh(); this.refresh();
} }
} }
async refresh() {
await super.refresh();
this.$widget.find('.dropdown-toggle').dropdown('hide');
}
} }

View File

@@ -3,6 +3,7 @@ import utils from "../services/utils.js";
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js"; import server from "../services/server.js";
import SpacedUpdate from "../services/spaced_update.js"; import SpacedUpdate from "../services/spaced_update.js";
import appContext from "../services/app_context.js";
const TPL = ` const TPL = `
<div class="note-title-container"> <div class="note-title-container">
@@ -35,8 +36,10 @@ export default class NoteTitleWidget extends TabAwareWidget {
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note); protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
await server.put(`notes/${this.noteId}/change-title`, {title}); await server.put(`notes/${this.noteId}/change-title`, {title}, this.componentId);
}); });
appContext.addBeforeUnloadListener(this);
} }
doRender() { doRender() {
@@ -101,6 +104,6 @@ export default class NoteTitleWidget extends TabAwareWidget {
} }
beforeUnloadEvent() { beforeUnloadEvent() {
this.spacedUpdate.updateNowIfNecessary(); return this.spacedUpdate.isAllSavedAndTriggerUpdate();
} }
} }

View File

@@ -176,6 +176,15 @@ const TPL = `
title="Images which are shown in the parent text note will not be displayed in the tree"></span> title="Images which are shown in the parent text note will not be displayed in the tree"></span>
</label> </label>
</div> </div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input auto-collapse-note-tree" type="checkbox" value="">
Automatically collapse notes
<span class="bx bx-info-circle"
title="Notes will be collapsed after period of inactivity to declutter the tree."></span>
</label>
</div>
<br/> <br/>
@@ -203,8 +212,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e)); this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
this.$tree.on("mousedown", ".add-note-button", e => { this.$tree.on("mousedown", ".add-note-button", e => {
const node = $.ui.fancytree.getNode(e); const node = $.ui.fancytree.getNode(e);
const parentNotePath = treeService.getNotePath(node);
noteCreateService.createNote(node.data.noteId, { noteCreateService.createNote(parentNotePath, {
isProtected: node.data.isProtected isProtected: node.data.isProtected
}); });
}); });
@@ -234,6 +244,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup'); this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup');
this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes'); this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes');
this.$hideIncludedImages = this.$treeSettingsPopup.find('.hide-included-images'); this.$hideIncludedImages = this.$treeSettingsPopup.find('.hide-included-images');
this.$autoCollapseNoteTree = this.$treeSettingsPopup.find('.auto-collapse-note-tree');
this.$treeSettingsButton = this.$widget.find('.tree-settings-button'); this.$treeSettingsButton = this.$widget.find('.tree-settings-button');
this.$treeSettingsButton.on("click", e => { this.$treeSettingsButton.on("click", e => {
@@ -244,6 +255,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes); this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes);
this.$hideIncludedImages.prop("checked", this.hideIncludedImages); this.$hideIncludedImages.prop("checked", this.hideIncludedImages);
this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree);
let top = this.$treeSettingsButton[0].offsetTop; let top = this.$treeSettingsButton[0].offsetTop;
let left = this.$treeSettingsButton[0].offsetLeft; let left = this.$treeSettingsButton[0].offsetLeft;
@@ -271,6 +283,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.$saveTreeSettingsButton.on('click', async () => { this.$saveTreeSettingsButton.on('click', async () => {
await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked")); await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked"));
await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked")); await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked"));
await this.setAutoCollapseNoteTree(this.$autoCollapseNoteTree.prop("checked"));
this.$treeSettingsPopup.hide(); this.$treeSettingsPopup.hide();
@@ -326,6 +339,14 @@ export default class NoteTreeWidget extends TabAwareWidget {
await options.save("hideIncludedImages_" + this.treeName, val.toString()); await options.save("hideIncludedImages_" + this.treeName, val.toString());
} }
get autoCollapseNoteTree() {
return options.is("autoCollapseNoteTree");
}
async setAutoCollapseNoteTree(val) {
await options.save("autoCollapseNoteTree", val.toString());
}
initFancyTree() { initFancyTree() {
const treeData = [this.prepareRootNode()]; const treeData = [this.prepareRootNode()];
@@ -361,8 +382,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
else { else {
node.setActive(); node.setActive();
this.clearSelectedNodes();
} }
return false; return false;
@@ -372,6 +391,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
// click event won't propagate so let's close context menu manually // click event won't propagate so let's close context menu manually
contextMenu.hide(); contextMenu.hide();
this.clearSelectedNodes();
const notePath = treeService.getNotePath(data.node); const notePath = treeService.getNotePath(data.node);
const activeTabContext = appContext.tabManager.getActiveTabContext(); const activeTabContext = appContext.tabManager.getActiveTabContext();
@@ -594,7 +615,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
let childBranches = parentNote.getFilteredChildBranches(); let childBranches = parentNote.getFilteredChildBranches();
if (childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) { if (parentNote.type === 'search' && childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) {
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE); childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
} }
@@ -796,20 +817,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
const node = await this.expandToNote(activeContext.notePath); const node = await this.expandToNote(activeContext.notePath);
if (node) {
await node.makeVisible({scrollIntoView: true}); await node.makeVisible({scrollIntoView: true});
node.setActive(true, {noEvents: true, noFocus: false}); node.setActive(true, {noEvents: true, noFocus: false});
} }
} }
}
/** @return {FancytreeNode} */ /** @return {FancytreeNode} */
async getNodeFromPath(notePath, expand = false, logErrors = true) { async getNodeFromPath(notePath, expand = false, logErrors = true) {
utils.assertArguments(notePath); utils.assertArguments(notePath);
/** @let {FancytreeNode} */
let parentNode = this.getNodesByNoteId('root')[0];
const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); let resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, this.hoistedNoteId, logErrors);
/** @const {FancytreeNode} */
let parentNode = null;
const resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, logErrors);
if (!resolvedNotePathSegments) { if (!resolvedNotePathSegments) {
if (logErrors) { if (logErrors) {
@@ -819,14 +840,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
return; return;
} }
resolvedNotePathSegments = resolvedNotePathSegments.slice(1);
for (const childNoteId of resolvedNotePathSegments) { for (const childNoteId of resolvedNotePathSegments) {
if (childNoteId === hoistedNoteId) {
// there must be exactly one node with given hoistedNoteId
parentNode = this.getNodesByNoteId(childNoteId)[0];
continue;
}
// we expand only after hoisted note since before then nodes are not actually present in the tree // we expand only after hoisted note since before then nodes are not actually present in the tree
if (parentNode) { if (parentNode) {
if (!parentNode.isLoaded()) { if (!parentNode.isLoaded()) {
@@ -857,7 +873,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
// these are real notes with real notePath, user can display them in a detail // these are real notes with real notePath, user can display them in a detail
// but they don't have a node in the tree // but they don't have a node in the tree
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`); const childNote = await treeCache.getNote(childNoteId);
if (!childNote || childNote.type !== 'image') {
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`);
}
} }
return; return;
@@ -961,6 +981,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
this.autoCollapseTimeoutId = setTimeout(() => { this.autoCollapseTimeoutId = setTimeout(() => {
if (!this.autoCollapseNoteTree) {
return;
}
/* /*
* We're collapsing notes after period of inactivity to "cleanup" the tree - users rarely * We're collapsing notes after period of inactivity to "cleanup" the tree - users rarely
* collapse the notes and the tree becomes unusuably large. * collapse the notes and the tree becomes unusuably large.
@@ -999,6 +1023,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const activeNodeFocused = activeNode && activeNode.hasFocus(); const activeNodeFocused = activeNode && activeNode.hasFocus();
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null; const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null; const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null; const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
const activeNoteId = activeNode ? activeNode.data.noteId : null; const activeNoteId = activeNode ? activeNode.data.noteId : null;
@@ -1119,7 +1144,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
if (node) { if (node) {
node.setActive(true, {noEvents: true, noFocus: true}); if (activeNodeFocused) {
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
this.tree.$container.focus();
}
await node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
} }
else { else {
// this is used when original note has been deleted and we want to move the focus to the note above/below // this is used when original note has been deleted and we want to move the focus to the note above/below
@@ -1131,15 +1161,15 @@ export default class NoteTreeWidget extends TabAwareWidget {
// this should be done by tabcontext / tabmanager and note tree should only listen to // this should be done by tabcontext / tabmanager and note tree should only listen to
// changes in active note and just set the "active" state // changes in active note and just set the "active" state
// We don't await since that can bring up infinite cycles when e.g. custom widget does some backend requests which wait for max sync ID processed // We don't await since that can bring up infinite cycles when e.g. custom widget does some backend requests which wait for max sync ID processed
appContext.tabManager.getActiveTabContext().setNote(nextNotePath); appContext.tabManager.getActiveTabContext().setNote(nextNotePath).then(() => {
}
}
const newActiveNode = this.getActiveNode(); const newActiveNode = this.getActiveNode();
// return focus if the previously active node was also focused // return focus if the previously active node was also focused
if (newActiveNode && activeNodeFocused) { if (newActiveNode && activeNodeFocused) {console.log("FOCUSING!!!");
await newActiveNode.setFocus(true); newActiveNode.setFocus(true);
}
});
}
} }
} }
@@ -1207,13 +1237,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
} }
filterHoistedBranch() { async filterHoistedBranch() {
if (this.tabContext) { if (this.tabContext) {
// make sure the hoisted node is loaded (can be unloaded e.g. after tree collapse in another tab)
const hoistedNotePath = await treeService.resolveNotePath(this.tabContext.hoistedNoteId);
await this.getNodeFromPath(hoistedNotePath);
if (this.tabContext.hoistedNoteId === 'root') { if (this.tabContext.hoistedNoteId === 'root') {
this.tree.clearFilter(); this.tree.clearFilter();
} }
else { else {
this.tree.filterBranches(node => node.data.noteId === this.tabContext.hoistedNoteId); // hack when hoisted note is cloned then it could be filtered multiple times while we want only 1
this.tree.filterBranches(node =>
node.data.noteId === this.tabContext.hoistedNoteId // optimization to not having always resolve the node path
&& treeService.getNotePath(node) === hoistedNotePath);
} }
} }
} }
@@ -1370,7 +1407,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
sortChildNotesCommand({node}) { sortChildNotesCommand({node}) {
treeService.sortAlphabetically(node.data.noteId); import("../dialogs/sort_child_notes.js").then(d => d.showDialog(node.data.noteId));
} }
async recentChangesInSubtreeCommand({node}) { async recentChangesInSubtreeCommand({node}) {

View File

@@ -49,7 +49,11 @@ export default class QuickSearchWidget extends BasicWidget {
this.$widget.find('.input-group-append').on('shown.bs.dropdown', () => this.search()); this.$widget.find('.input-group-append').on('shown.bs.dropdown', () => this.search());
utils.bindElShortcut(this.$searchString, 'return', () => { utils.bindElShortcut(this.$searchString, 'return', () => {
if (this.$dropdownMenu.is(":visible")) {
this.search(); // just update already visible dropdown
} else {
this.$dropdownToggle.dropdown('show'); this.$dropdownToggle.dropdown('show');
}
this.$searchString.focus(); this.$searchString.focus();
}); });

View File

@@ -5,7 +5,7 @@ const TPL = `
<td colspan="2"> <td colspan="2">
<span class="bx bx-trash"></span> <span class="bx bx-trash"></span>
Delete matched note Delete matched notes
</td> </td>
<td class="button-column"> <td class="button-column">
<span class="bx bx-x icon-action action-conf-del"></span> <span class="bx bx-x icon-action action-conf-del"></span>

View File

@@ -14,6 +14,10 @@ const TPL = `
height: 35px; height: 35px;
} }
.standard-top-widget > div {
flex-shrink: 0; /* fixes https://github.com/zadam/trilium/issues/1745 */
}
.standard-top-widget button.noborder { .standard-top-widget button.noborder {
padding: 1px 5px 1px 5px; padding: 1px 5px 1px 5px;
font-size: 90%; font-size: 90%;

View File

@@ -22,6 +22,10 @@ export default class TabAwareWidget extends BasicWidget {
return this.tabContext && this.tabContext.notePath; return this.tabContext && this.tabContext.notePath;
} }
get hoistedNoteId() {
return this.tabContext && this.tabContext.hoistedNoteId;
}
isEnabled() { isEnabled() {
return !!this.note; return !!this.note;
} }

View File

@@ -83,4 +83,10 @@ export default class InheritedAttributesWidget extends TabAwareWidget {
getInheritedAttributes(note) { getInheritedAttributes(note) {
return note.getAttributes().filter(attr => attr.noteId !== this.noteId); return note.getAttributes().filter(attr => attr.noteId !== this.noteId);
} }
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
this.refresh();
}
}
} }

View File

@@ -5,24 +5,47 @@ import linkService from "../../services/link.js";
import noteContentRenderer from "../../services/note_content_renderer.js"; import noteContentRenderer from "../../services/note_content_renderer.js";
export default class AbstractTextTypeWidget extends TypeWidget { export default class AbstractTextTypeWidget extends TypeWidget {
doRender() { setupImageOpening(singleClickOpens) {
this.$widget.on("dblclick", "img", e => { this.$widget.on("dblclick", "img", e => this.openImageInCurrentTab($(e.target)));
const $img = $(e.target);
const src = $img.prop("src");
const match = src.match(/\/api\/images\/([A-Za-z0-9]+)\//); this.$widget.on("click", "img", e => {
if ((e.which === 1 && e.ctrlKey) || e.which === 2) {
if (match) { this.openImageInNewTab($(e.target));
const noteId = match[1];
appContext.tabManager.getActiveTabContext().setNote(noteId);
} }
else { else if (e.which === 1 && singleClickOpens) {
window.open(src, '_blank'); this.openImageInCurrentTab($(e.target));
} }
}); });
} }
openImageInCurrentTab($img) {
const imgSrc = $img.prop("src");
const noteId = this.getNoteIdFromImage(imgSrc);
if (noteId) {
appContext.tabManager.getActiveTabContext().setNote(noteId);
} else {
window.open(imgSrc, '_blank');
}
}
openImageInNewTab($img) {
const imgSrc = $img.prop("src");
const noteId = this.getNoteIdFromImage(imgSrc);
if (noteId) {
appContext.tabManager.openTabWithNoteWithHoisting(noteId);
} else {
window.open(imgSrc, '_blank');
}
}
getNoteIdFromImage(imgSrc) {
const match = imgSrc.match(/\/api\/images\/([A-Za-z0-9]+)\//);
return match ? match[1] : null;
}
async loadIncludedNote(noteId, $el) { async loadIncludedNote(noteId, $el) {
const note = await treeCache.getNote(noteId); const note = await treeCache.getNote(noteId);

View File

@@ -49,15 +49,16 @@ const TPL = `
} }
.note-detail-editable-text h2 { font-size: 1.8em; } .note-detail-editable-text h2 { font-size: 1.8em; }
.note-detail-editable-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
.note-detail-editable-text h3 { font-size: 1.6em; } .note-detail-editable-text h3 { font-size: 1.6em; }
.note-detail-editable-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
.note-detail-editable-text h4 { font-size: 1.4em; } .note-detail-editable-text h4 { font-size: 1.4em; }
.note-detail-editable-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
.note-detail-editable-text h5 { font-size: 1.2em; } .note-detail-editable-text h5 { font-size: 1.2em; }
.note-detail-editable-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
.note-detail-editable-text h6 { font-size: 1.1em; } .note-detail-editable-text h6 { font-size: 1.1em; }
.note-detail-editable-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
body.heading-style-markdown .note-detail-editable-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
body.heading-style-markdown .note-detail-editable-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
body.heading-style-markdown .note-detail-editable-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
body.heading-style-markdown .note-detail-editable-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
body.heading-style-markdown .note-detail-editable-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
.note-detail-editable-text-editor { .note-detail-editable-text-editor {
padding-top: 10px; padding-top: 10px;
@@ -83,6 +84,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
keyboardActionService.setupActionsForElement('text-detail', this.$widget, this); keyboardActionService.setupActionsForElement('text-detail', this.$widget, this);
this.setupImageOpening(false);
super.doRender(); super.doRender();
} }
@@ -274,7 +277,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
} }
async createNoteForReferenceLink(title) { async createNoteForReferenceLink(title) {
const {note} = await noteCreateService.createNote(this.noteId, { const {note} = await noteCreateService.createNote(this.notePath, {
activate: false, activate: false,
title: title title: title
}); });

View File

@@ -5,6 +5,7 @@ const TPL = `
<style> <style>
.note-detail-read-only-code { .note-detail-read-only-code {
position: relative; position: relative;
min-height: 50px;
} }
.note-detail-read-only-code-content { .note-detail-read-only-code-content {

View File

@@ -14,22 +14,18 @@ const TPL = `
.note-detail-readonly-text h5 { font-size: 1.2em; } .note-detail-readonly-text h5 { font-size: 1.2em; }
.note-detail-readonly-text h6 { font-size: 1.1em; } .note-detail-readonly-text h6 { font-size: 1.1em; }
.note-detail-readonly-text h2 { font-size: 1.8em; } body.heading-style-markdown .note-detail-readonly-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h2::before { content: "##\\2004"; color: var(--muted-text-color); } body.heading-style-markdown .note-detail-readonly-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h3 { font-size: 1.6em; } body.heading-style-markdown .note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h3::before { content: "###\\2004"; color: var(--muted-text-color); } body.heading-style-markdown .note-detail-readonly-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h4 { font-size: 1.4em; } body.heading-style-markdown .note-detail-readonly-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h5 { font-size: 1.2em; }
.note-detail-readonly-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h6 { font-size: 1.1em; }
.note-detail-readonly-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text { .note-detail-readonly-text {
padding-left: 22px; padding-left: 22px;
padding-top: 10px; padding-top: 10px;
font-family: var(--detail-text-font-family); font-family: var(--detail-text-font-family);
position: relative; position: relative;
min-height: 50px;
} }
.note-detail-readonly-text p:first-child, .note-detail-readonly-text::before { .note-detail-readonly-text p:first-child, .note-detail-readonly-text::before {
@@ -38,6 +34,7 @@ const TPL = `
.note-detail-readonly-text img { .note-detail-readonly-text img {
max-width: 100%; max-width: 100%;
cursor: pointer;
} }
.edit-text-note-container { .edit-text-note-container {
@@ -70,6 +67,8 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
this.triggerEvent('textPreviewDisabled', {tabContext: this.tabContext}); this.triggerEvent('textPreviewDisabled', {tabContext: this.tabContext});
}); });
this.setupImageOpening(true);
super.doRender(); super.doRender();
} }

View File

@@ -7,6 +7,7 @@ import attributeAutocompleteService from "../../services/attribute_autocomplete.
import TypeWidget from "./type_widget.js"; import TypeWidget from "./type_widget.js";
import appContext from "../../services/app_context.js"; import appContext from "../../services/app_context.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import treeCache from "../../services/tree_cache.js";
const uniDirectionalOverlays = [ const uniDirectionalOverlays = [
[ "Arrow", { [ "Arrow", {
@@ -531,8 +532,11 @@ export default class RelationMapTypeWidget extends TypeWidget {
linkService.goToLink(e); linkService.goToLink(e);
}); });
const note = await treeCache.getNote(noteId);
const $noteBox = $("<div>") const $noteBox = $("<div>")
.addClass("note-box") .addClass("note-box")
.addClass(note.getCssClass())
.prop("id", this.noteIdToId(noteId)) .prop("id", this.noteIdToId(noteId))
.append($("<span>").addClass("title").append($link)) .append($("<span>").addClass("title").append($link))
.append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note.")) .append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note."))

View File

@@ -106,8 +106,12 @@ function processContent(images, note, content) {
for (const {src, dataUrl, imageId} of images) { for (const {src, dataUrl, imageId} of images) {
const filename = path.basename(src); const filename = path.basename(src);
if (!dataUrl.startsWith("data:image")) { if (!dataUrl || !dataUrl.startsWith("data:image")) {
log.info("Image could not be recognized as data URL:", dataUrl.substr(0, Math.min(100, dataUrl.length))); const excerpt = dataUrl
? dataUrl.substr(0, Math.min(100, dataUrl.length))
: "null";
log.info("Image could not be recognized as data URL: " + excerpt);
continue; continue;
} }

View File

@@ -5,10 +5,31 @@ const sql = require('../../services/sql');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const attributeService = require('../../services/attributes'); const attributeService = require('../../services/attributes');
const cls = require('../../services/cls');
const repository = require('../../services/repository');
function getInboxNote(req) { function getInboxNote(req) {
return attributeService.getNoteWithLabel('inbox') const hoistedNote = getHoistedNote();
let inbox;
if (hoistedNote) {
([inbox] = hoistedNote.getDescendantNotesWithLabel('hoistedInbox'));
if (!inbox) {
([inbox] = hoistedNote.getDescendantNotesWithLabel('inbox'));
}
if (!inbox) {
inbox = hoistedNote;
}
}
else {
inbox = attributeService.getNoteWithLabel('inbox')
|| dateNoteService.getDateNote(req.params.date); || dateNoteService.getDateNote(req.params.date);
}
return inbox;
} }
function getDateNote(req) { function getDateNote(req) {
@@ -62,12 +83,33 @@ function createSqlConsole() {
function createSearchNote(req) { function createSearchNote(req) {
const params = req.body; const params = req.body;
const searchString = params.searchString || ""; const searchString = params.searchString || "";
let ancestorNoteId = params.ancestorNoteId;
const hoistedNote = getHoistedNote();
let searchHome;
if (hoistedNote) {
([searchHome] = hoistedNote.getDescendantNotesWithLabel('hoistedSearchHome'));
if (!searchHome) {
([searchHome] = hoistedNote.getDescendantNotesWithLabel('searchHome'));
}
if (!searchHome) {
searchHome = hoistedNote;
}
if (!ancestorNoteId) {
ancestorNoteId = hoistedNote.noteId;
}
}
else {
const today = dateUtils.localNowDate(); const today = dateUtils.localNowDate();
const searchHome = searchHome = attributeService.getNoteWithLabel('searchHome')
attributeService.getNoteWithLabel('searchHome')
|| dateNoteService.getDateNote(today); || dateNoteService.getDateNote(today);
}
const {note} = noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId: searchHome.noteId, parentNoteId: searchHome.noteId,
@@ -79,13 +121,19 @@ function createSearchNote(req) {
note.setLabel('searchString', searchString); note.setLabel('searchString', searchString);
if (params.ancestorNoteId) { if (ancestorNoteId) {
note.setRelation('ancestor', params.ancestorNoteId); note.setRelation('ancestor', ancestorNoteId);
} }
return note; return note;
} }
function getHoistedNote() {
return cls.getHoistedNoteId() && cls.getHoistedNoteId() !== 'root'
? repository.getNote(cls.getHoistedNoteId())
: null;
}
module.exports = { module.exports = {
getInboxNote, getInboxNote,
getDateNote, getDateNote,

View File

@@ -78,6 +78,12 @@ function loginToProtectedSession(req) {
}; };
} }
function logoutFromProtectedSession() {
protectedSessionService.resetDataKey();
eventService.emit(eventService.LEAVE_PROTECTED_SESSION);
}
function token(req) { function token(req) {
const username = req.body.username; const username = req.body.username;
const password = req.body.password; const password = req.body.password;
@@ -101,5 +107,6 @@ function token(req) {
module.exports = { module.exports = {
loginSync, loginSync,
loginToProtectedSession, loginToProtectedSession,
logoutFromProtectedSession,
token token
}; };

View File

@@ -5,6 +5,7 @@ const noteCacheService = require('../../services/note_cache/note_cache_service')
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const noteRevisionService = require('../../services/note_revisions'); const noteRevisionService = require('../../services/note_revisions');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const sql = require('../../services/sql');
const path = require('path'); const path = require('path');
function getNoteRevisions(req) { function getNoteRevisions(req) {

View File

@@ -4,6 +4,7 @@ const noteService = require('../../services/notes');
const treeService = require('../../services/tree'); const treeService = require('../../services/tree');
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const log = require('../../services/log');
const TaskContext = require('../../services/task_context'); const TaskContext = require('../../services/task_context');
function getNote(req) { function getNote(req) {
@@ -85,10 +86,20 @@ function undeleteNote(req) {
taskContext.taskSucceeded(); taskContext.taskSucceeded();
} }
function sortNotes(req) { function sortChildNotes(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const {sortBy, sortDirection} = req.body;
treeService.sortNotesAlphabetically(noteId); log.info(`Sorting ${noteId} children with ${sortBy} ${sortDirection}`);
const reverse = sortDirection === 'desc';
if (sortBy === 'title') {
treeService.sortNotesByTitle(noteId, false, reverse);
}
else {
treeService.sortNotes(noteId, sortBy, reverse);
}
} }
function protectNote(req) { function protectNote(req) {
@@ -215,7 +226,7 @@ module.exports = {
deleteNote, deleteNote,
undeleteNote, undeleteNote,
createNote, createNote,
sortNotes, sortChildNotes,
protectNote, protectNote,
setNoteTypeMime, setNoteTypeMime,
getRelationMap, getRelationMap,

View File

@@ -40,7 +40,9 @@ const ALLOWED_OPTIONS = new Set([
'nativeTitleBarVisible', 'nativeTitleBarVisible',
'attributeListExpanded', 'attributeListExpanded',
'promotedAttributesExpanded', 'promotedAttributesExpanded',
'similarNotesExpanded' 'similarNotesExpanded',
'headingStyle',
'autoCollapseNoteTree'
]); ]);
function getOptions() { function getOptions() {

View File

@@ -200,9 +200,7 @@ function queueSector(req) {
const entityName = utils.sanitizeSqlIdentifier(req.params.entityName); const entityName = utils.sanitizeSqlIdentifier(req.params.entityName);
const sector = utils.sanitizeSqlIdentifier(req.params.sector); const sector = utils.sanitizeSqlIdentifier(req.params.sector);
const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; entityChangesService.addEntityChangesForSector(entityName, sector);
entityChangesService.addEntityChangesForSector(entityName, entityPrimaryKey, sector);
} }
module.exports = { module.exports = {

View File

@@ -19,6 +19,7 @@ function index(req, res) {
res.render(view, { res.render(view, {
csrfToken: csrfToken, csrfToken: csrfToken,
theme: options.theme, theme: options.theme,
headingStyle: options.headingStyle,
mainFontSize: parseInt(options.mainFontSize), mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize), treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize), detailFontSize: parseInt(options.detailFontSize),

View File

@@ -150,7 +150,7 @@ function register(app) {
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote); apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote); apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote);
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
apiRoute(PUT, '/api/notes/:noteId/sort', notesApiRoute.sortNotes); apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes);
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote);
apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime); apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
@@ -270,6 +270,8 @@ function register(app) {
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); 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) // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession); apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession);
apiRoute(POST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession);
route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler); route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler);
// in case of local electron, local calls are allowed unauthenticated, for server they need auth // in case of local electron, local calls are allowed unauthenticated, for server they need auth

View File

@@ -27,7 +27,6 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'run', isDangerous: true }, { type: 'label', name: 'run', isDangerous: true },
{ type: 'label', name: 'customRequestHandler', isDangerous: true }, { type: 'label', name: 'customRequestHandler', isDangerous: true },
{ type: 'label', name: 'customResourceProvider', isDangerous: true }, { type: 'label', name: 'customResourceProvider', isDangerous: true },
{ type: 'label', name: 'bookZoomLevel', isDangerous: false },
{ type: 'label', name: 'widget', isDangerous: true }, { type: 'label', name: 'widget', isDangerous: true },
{ type: 'label', name: 'noteInfoWidgetDisabled' }, { type: 'label', name: 'noteInfoWidgetDisabled' },
{ type: 'label', name: 'linkMapWidgetDisabled' }, { type: 'label', name: 'linkMapWidgetDisabled' },
@@ -38,8 +37,12 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'workspaceIconClass' }, { type: 'label', name: 'workspaceIconClass' },
{ type: 'label', name: 'workspaceTabBackgroundColor' }, { type: 'label', name: 'workspaceTabBackgroundColor' },
{ type: 'label', name: 'searchHome' }, { type: 'label', name: 'searchHome' },
{ type: 'label', name: 'hoistedInbox' },
{ type: 'label', name: 'hoistedSearchHome' },
{ type: 'label', name: 'sqlConsoleHome' }, { type: 'label', name: 'sqlConsoleHome' },
{ type: 'label', name: 'datePattern' }, { type: 'label', name: 'datePattern' },
{ type: 'label', name: 'pageSize' },
{ type: 'label', name: 'viewType' },
// relation names // relation names
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true }, { type: 'relation', name: 'runOnNoteCreation', isDangerous: true },

View File

@@ -359,7 +359,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @method * @method
* @param {string} parentNoteId - this note's child notes will be sorted * @param {string} parentNoteId - this note's child notes will be sorted
*/ */
this.sortNotesAlphabetically = treeService.sortNotesAlphabetically; this.sortNotesByTitle = treeService.sortNotesByTitle;
/** /**
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId * This method finds note by its noteId and prefix and either sets it to the given parentNoteId

View File

@@ -1 +1 @@
module.exports = { buildDate:"2021-02-25T22:41:35+01:00", buildRevision: "cde41b268e4e88b3fe3601d9d19b3f5241625ada" }; module.exports = { buildDate:"2021-04-22T20:50:22+02:00", buildRevision: "c27f573eed8905fc1d958adf5bdee0efc3aff593" };

View File

@@ -44,10 +44,14 @@ function isEntityEventsDisabled() {
return !!namespace.get('disableEntityEvents'); return !!namespace.get('disableEntityEvents');
} }
function clearEntityChanges() {
namespace.set('entityChanges', []);
}
function getAndClearEntityChanges() { function getAndClearEntityChanges() {
const entityChanges = namespace.get('entityChanges') || []; const entityChanges = namespace.get('entityChanges') || [];
namespace.set('entityChanges', []); clearEntityChanges();
return entityChanges; return entityChanges;
} }
@@ -92,6 +96,7 @@ module.exports = {
disableEntityEvents, disableEntityEvents,
isEntityEventsDisabled, isEntityEventsDisabled,
reset, reset,
clearEntityChanges,
getAndClearEntityChanges, getAndClearEntityChanges,
addEntityChange, addEntityChange,
getEntityFromCache, getEntityFromCache,

View File

@@ -30,28 +30,37 @@ function addEntityChange(entityChange, sourceId, isSynced) {
cls.addEntityChange(localEntityChange); cls.addEntityChange(localEntityChange);
} }
function addNoteReorderingEntityChange(parentNoteId, sourceId) {
addEntityChange({
entityName: "note_reordering",
entityId: parentNoteId,
hash: 'N/A',
isErased: false,
utcDateChanged: dateUtils.utcNowDateTime()
}, sourceId);
const eventService = require('./events');
eventService.emit(eventService.ENTITY_CHANGED, {
entityName: 'note_reordering',
entity: sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [parentNoteId])
});
}
function moveEntityChangeToTop(entityName, entityId) { function moveEntityChangeToTop(entityName, entityId) {
const [hash, isSynced] = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]); const [hash, isSynced] = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]);
addEntityChange(entityName, entityId, hash, null, isSynced); addEntityChange(entityName, entityId, hash, null, isSynced);
} }
function addEntityChangesForSector(entityName, entityPrimaryKey, sector) { function addEntityChangesForSector(entityName, sector) {
const startTime = Date.now(); const startTime = Date.now();
const repository = require('./repository');
const entityChanges = sql.getRows(`SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ?`, [entityName, sector]);
sql.transactional(() => { sql.transactional(() => {
const entityIds = sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName} WHERE SUBSTR(${entityPrimaryKey}, 1, 1) = ?`, [sector]); for (const ec of entityChanges) {
insertEntityChange(entityName, ec.entityId, ec.hash, ec.isErased, ec.utcDateChanged, ec.sourceId, ec.isSynced);
for (const entityId of entityIds) {
// retrieving entity one by one to avoid memory issues with note_contents
const entity = repository.getEntity(`SELECT * FROM ${entityName} WHERE ${entityPrimaryKey} = ?`, [entityId]);
if (entityName === 'options' && !entity.isSynced) {
continue
}
insertEntityChange(entityName, entityId, entity.generateHash(), false, entity.getUtcDateChanged(), 'content-check', true);
} }
}); });
@@ -121,13 +130,7 @@ function fillAllEntityChanges() {
} }
module.exports = { module.exports = {
addNoteReorderingEntityChange: (parentNoteId, sourceId) => addEntityChange({ addNoteReorderingEntityChange,
entityName: "note_reordering",
entityId: parentNoteId,
hash: 'N/A',
isErased: false,
utcDateChanged: dateUtils.utcNowDateTime()
}, sourceId),
moveEntityChangeToTop, moveEntityChangeToTop,
addEntityChange, addEntityChange,
fillAllEntityChanges, fillAllEntityChanges,

View File

@@ -2,6 +2,7 @@ const log = require('./log');
const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED"; const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED";
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION"; const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
const LEAVE_PROTECTED_SESSION = "LEAVE_PROTECTED_SESSION";
const ENTITY_CREATED = "ENTITY_CREATED"; const ENTITY_CREATED = "ENTITY_CREATED";
const ENTITY_CHANGED = "ENTITY_CHANGED"; const ENTITY_CHANGED = "ENTITY_CHANGED";
const ENTITY_DELETED = "ENTITY_DELETED"; const ENTITY_DELETED = "ENTITY_DELETED";
@@ -47,6 +48,7 @@ module.exports = {
// event types: // event types:
NOTE_TITLE_CHANGED, NOTE_TITLE_CHANGED,
ENTER_PROTECTED_SESSION, ENTER_PROTECTED_SESSION,
LEAVE_PROTECTED_SESSION,
ENTITY_CREATED, ENTITY_CREATED,
ENTITY_CHANGED, ENTITY_CHANGED,
ENTITY_DELETED, ENTITY_DELETED,

View File

@@ -31,7 +31,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
for (const parentNote of noteFromCache.parents) { for (const parentNote of noteFromCache.parents) {
if (parentNote.hasLabel("sorted")) { if (parentNote.hasLabel("sorted")) {
treeService.sortNotesAlphabetically(parentNote.noteId); treeService.sortNotesByTitle(parentNote.noteId);
} }
} }
} }
@@ -53,23 +53,19 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
if (entity.type === 'relation' && entity.name === 'template') { if (entity.type === 'relation' && entity.name === 'template') {
const note = repository.getNote(entity.noteId); const note = repository.getNote(entity.noteId);
if (!["text", "code"].includes(note.type)) {
return;
}
const content = note.getContent();
if (content && content.trim().length > 0) {
return;
}
const templateNote = repository.getNote(entity.value); const templateNote = repository.getNote(entity.value);
if (!templateNote) { if (!templateNote) {
return; return;
} }
if (templateNote.isStringNote()) { const content = note.getContent();
if (["text", "code"].includes(note.type)
// if the note has already content we're not going to overwrite it with template's one
&& (!content || content.trim().length === 0)
&& templateNote.isStringNote()) {
const templateNoteContent = templateNote.getContent(); const templateNoteContent = templateNote.getContent();
if (templateNoteContent) { if (templateNoteContent) {
@@ -81,17 +77,21 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
note.save(); note.save();
} }
// we'll copy the children notes only if there's none so far
// this protects against e.g. multiple assignment of template relation resulting in having multiple copies of the subtree
if (note.getChildNotes().length === 0 && !note.isDescendantOfNote(templateNote.noteId)) {
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId); noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
} }
}
else if (entity.type === 'label' && entity.name === 'sorted') { else if (entity.type === 'label' && entity.name === 'sorted') {
treeService.sortNotesAlphabetically(entity.noteId); treeService.sortNotesByTitle(entity.noteId);
if (entity.isInheritable) { if (entity.isInheritable) {
const note = noteCache.notes[entity.noteId]; const note = noteCache.notes[entity.noteId];
if (note) { if (note) {
for (const noteId of note.subtreeNoteIds) { for (const noteId of note.subtreeNoteIds) {
treeService.sortNotesAlphabetically(noteId); treeService.sortNotesByTitle(noteId);
} }
} }
} }

View File

@@ -463,7 +463,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
if (!metaFile) { if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that // if there's no meta file then the notes are created based on the order in that tar file but that
// is usually quite random so we sort the notes in the way they would appear in the file manager // is usually quite random so we sort the notes in the way they would appear in the file manager
treeService.sortNotesAlphabetically(noteId, true); treeService.sortNotesByTitle(noteId, true);
} }
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();

View File

@@ -1,6 +1,7 @@
"use strict"; "use strict";
const protectedSessionService = require('../../protected_session'); const protectedSessionService = require('../../protected_session');
const log = require('../../log');
class Note { class Note {
constructor(noteCache, row) { constructor(noteCache, row) {
@@ -405,7 +406,7 @@ class Note {
return 0; return 0;
} }
let minDistance = 999_999; let minDistance = 999999;
for (const parent of this.parents) { for (const parent of this.parents) {
minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1); minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1);
@@ -416,10 +417,15 @@ class Note {
decrypt() { decrypt() {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
this.title = protectedSessionService.decryptString(this.title); this.title = protectedSessionService.decryptString(this.title);
this.isDecrypted = true; this.isDecrypted = true;
} }
catch (e) {
log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`);
}
}
} }
// for logging etc // for logging etc

View File

@@ -120,7 +120,11 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
delete noteCache.attributes[attributeId]; delete noteCache.attributes[attributeId];
if (attr) { if (attr) {
delete noteCache.attributeIndex[`${attr.type}-${attr.name.toLowerCase()}`]; const key = `${attr.type}-${attr.name.toLowerCase()}`;
if (key in noteCache.attributeIndex) {
noteCache.attributeIndex[key] = noteCache.attributeIndex[key].filter(attr => attr.attributeId !== attributeId);
}
} }
} }
else if (attributeId in noteCache.attributes) { else if (attributeId in noteCache.attributes) {
@@ -149,6 +153,19 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
} }
} }
} }
else if (entityName === 'note_reordering') {
const parentNoteIds = new Set();
for (const branchId in entity) {
const branch = noteCache.branches[branchId];
if (branch) {
branch.notePosition = entity[branchId];
parentNoteIds.add(branch.parentNoteId);
}
}
}
}); });
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => { eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
@@ -160,6 +177,10 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
} }
}); });
eventService.subscribe(eventService.LEAVE_PROTECTED_SESSION, () => {
load();
});
module.exports = { module.exports = {
load load
}; };

View File

@@ -23,7 +23,6 @@ const IGNORED_ATTR_NAMES = [
"archived", "archived",
"hidepromotedattributes", "hidepromotedattributes",
"keyboardshortcut", "keyboardshortcut",
"bookzoomlevel",
"noteinfowidgetdisabled", "noteinfowidgetdisabled",
"linkmapwidgetdisabled", "linkmapwidgetdisabled",
"noterevisionswidgetdisabled", "noterevisionswidgetdisabled",
@@ -234,7 +233,7 @@ async function findSimilarNotes(noteId) {
const baseNote = noteCache.notes[noteId]; const baseNote = noteCache.notes[noteId];
if (!baseNote) { if (!baseNote || !baseNote.utcDateCreated) {
return []; return [];
} }

View File

@@ -84,7 +84,9 @@ const defaultOptions = [
{ name: 'attributeListExpanded', value: 'false', isSynced: false }, { name: 'attributeListExpanded', value: 'false', isSynced: false },
{ name: 'promotedAttributesExpanded', value: 'true', isSynced: true }, { name: 'promotedAttributesExpanded', value: 'true', isSynced: true },
{ name: 'similarNotesExpanded', value: 'true', isSynced: true }, { name: 'similarNotesExpanded', value: 'true', isSynced: true },
{ name: 'debugModeEnabled', value: 'false', isSynced: false } { name: 'debugModeEnabled', value: 'false', isSynced: false },
{ name: 'headingStyle', value: 'markdown', isSynced: true },
{ name: 'autoCollapseNoteTree', value: 'true', isSynced: true },
]; ];
function initStartupOptions() { function initStartupOptions() {

View File

@@ -5,7 +5,7 @@ const log = require('./log');
const dataEncryptionService = require('./data_encryption'); const dataEncryptionService = require('./data_encryption');
const cls = require('./cls'); const cls = require('./cls');
const dataKeyMap = {}; let dataKeyMap = {};
function setDataKey(decryptedDataKey) { function setDataKey(decryptedDataKey) {
const protectedSessionId = utils.randomSecureToken(32); const protectedSessionId = utils.randomSecureToken(32);
@@ -29,6 +29,10 @@ function getDataKey() {
return dataKeyMap[protectedSessionId]; return dataKeyMap[protectedSessionId];
} }
function resetDataKey() {
dataKeyMap = {};
}
function isProtectedSessionAvailable() { function isProtectedSessionAvailable() {
const protectedSessionId = getProtectedSessionId(); const protectedSessionId = getProtectedSessionId();
@@ -71,6 +75,7 @@ function decryptString(cipherText) {
module.exports = { module.exports = {
setDataKey, setDataKey,
getDataKey, getDataKey,
resetDataKey,
isProtectedSessionAvailable, isProtectedSessionAvailable,
encrypt, encrypt,
decrypt, decrypt,

View File

@@ -84,7 +84,15 @@ function exec(opts) {
}); });
}); });
request.end(opts.body); let payload;
if (opts.body) {
payload = typeof opts.body === 'object'
? JSON.stringify(opts.body)
: opts.body;
}
request.end(payload);
} }
catch (e) { catch (e) {
reject(generateError(opts, e.message)); reject(generateError(opts, e.message));

View File

@@ -21,8 +21,8 @@ function lex(str) {
} }
} }
function finishWord(endIndex) { function finishWord(endIndex, createAlsoForEmptyWords = false) {
if (currentWord === '') { if (currentWord === '' && !createAlsoForEmptyWords) {
return; return;
} }
@@ -71,7 +71,7 @@ function lex(str) {
} }
} }
else if (quotes === chr) { else if (quotes === chr) {
finishWord(i - 1); finishWord(i - 1, true);
quotes = false; quotes = false;
} }

View File

@@ -3,6 +3,7 @@
const log = require('./log'); const log = require('./log');
const Database = require('better-sqlite3'); const Database = require('better-sqlite3');
const dataDir = require('./data_dir'); const dataDir = require('./data_dir');
const cls = require('./cls');
const dbConnection = new Database(dataDir.DOCUMENT_PATH); const dbConnection = new Database(dataDir.DOCUMENT_PATH);
dbConnection.pragma('journal_mode = WAL'); dbConnection.pragma('journal_mode = WAL');
@@ -31,7 +32,7 @@ function insert(tableName, rec, replace = false) {
const res = execute(query, Object.values(rec)); const res = execute(query, Object.values(rec));
return res.lastInsertRowid; return res ? res.lastInsertRowid : null;
} }
function replace(tableName, rec) { function replace(tableName, rec) {
@@ -229,6 +230,7 @@ function wrap(query, func) {
} }
function transactional(func) { function transactional(func) {
try {
const ret = dbConnection.transaction(func).deferred(); const ret = dbConnection.transaction(func).deferred();
if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released) if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released)
@@ -236,6 +238,12 @@ function transactional(func) {
} }
return ret; return ret;
}
catch (e) {
cls.clearEntityChanges();
throw e;
}
} }
function fillNoteIdList(noteIds, truncate = true) { function fillNoteIdList(noteIds, truncate = true) {

View File

@@ -245,9 +245,7 @@ async function checkContentHash(syncContext) {
const failedChecks = contentHashService.checkContentHashes(resp.entityHashes); const failedChecks = contentHashService.checkContentHashes(resp.entityHashes);
for (const {entityName, sector} of failedChecks) { for (const {entityName, sector} of failedChecks) {
const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; entityChangesService.addEntityChangesForSector(entityName, sector);
entityChangesService.addEntityChangesForSector(entityName, entityPrimaryKey, sector);
await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`); await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`);
} }

View File

@@ -26,9 +26,7 @@ function updateEntity(entityChange, entity, sourceId) {
? updateNoteReordering(entityChange, entity, sourceId) ? updateNoteReordering(entityChange, entity, sourceId)
: updateNormalEntity(entityChange, entity, sourceId); : updateNormalEntity(entityChange, entity, sourceId);
// currently making exception for protected notes and note revisions because here if (updated && !entityChange.isErased) {
// the title and content are not available decrypted as listeners would expect
if (updated && !entity.isProtected && !entityChange.isErased) {
eventService.emit(eventService.ENTITY_SYNCED, { eventService.emit(eventService.ENTITY_SYNCED, {
entityName: entityChange.entityName, entityName: entityChange.entityName,
entity entity
@@ -44,7 +42,7 @@ function updateNormalEntity(remoteEntityChange, entity, sourceId) {
if (localEntityChange && !localEntityChange.isErased && remoteEntityChange.isErased) { if (localEntityChange && !localEntityChange.isErased && remoteEntityChange.isErased) {
sql.transactional(() => { sql.transactional(() => {
const primaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; const primaryKey = entityConstructor.getEntityFromEntityName(remoteEntityChange.entityName).primaryKeyName;
sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId); sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId);

View File

@@ -1,6 +1,7 @@
"use strict"; "use strict";
const sql = require('./sql'); const sql = require('./sql');
const log = require('./log');
const repository = require('./repository'); const repository = require('./repository');
const Branch = require('../entities/branch'); const Branch = require('../entities/branch');
const entityChangesService = require('./entity_changes.js'); const entityChangesService = require('./entity_changes.js');
@@ -106,7 +107,7 @@ function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) {
} }
} }
function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) { function sortNotesByTitle(parentNoteId, foldersFirst = false, reverse = false) {
sql.transactional(() => { sql.transactional(() => {
const notes = sql.getRows( const notes = sql.getRows(
`SELECT branches.branchId, notes.noteId, title, isProtected, `SELECT branches.branchId, notes.noteId, title, isProtected,
@@ -120,7 +121,7 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
protectedSessionService.decryptNotes(notes); protectedSessionService.decryptNotes(notes);
notes.sort((a, b) => { notes.sort((a, b) => {
if (directoriesFirst && ((a.hasChildren && !b.hasChildren) || (!a.hasChildren && b.hasChildren))) { if (foldersFirst && ((a.hasChildren && !b.hasChildren) || (!a.hasChildren && b.hasChildren))) {
// exactly one note of the two is a directory so the sorting will be done based on this status // exactly one note of the two is a directory so the sorting will be done based on this status
return a.hasChildren ? -1 : 1; return a.hasChildren ? -1 : 1;
} }
@@ -129,13 +130,49 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
} }
}); });
if (reverse) {
notes.reverse();
}
let position = 10; let position = 10;
for (const note of notes) { for (const note of notes) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
[position, note.branchId]); [position, note.branchId]);
if (note.branchId in noteCache.branches) {
noteCache.branches[note.branchId].notePosition = position; noteCache.branches[note.branchId].notePosition = position;
}
else {
log.info(`Branch "${note.branchId}" was not found in note cache.`);
}
position += 10;
}
entityChangesService.addNoteReorderingEntityChange(parentNoteId);
});
}
function sortNotes(parentNoteId, sortBy, reverse = false) {
sql.transactional(() => {
const notes = repository.getNote(parentNoteId).getChildNotes();
notes.sort((a, b) => a[sortBy] < b[sortBy] ? -1 : 1);
if (reverse) {
notes.reverse();
}
let position = 10;
for (const note of notes) {
const branch = note.getBranches().find(b => b.parentNoteId === parentNoteId);
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
[position, branch.branchId]);
noteCache.branches[branch.branchId].notePosition = position;
position += 10; position += 10;
} }
@@ -194,6 +231,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
module.exports = { module.exports = {
getNotes, getNotes,
validateParentChild, validateParentChild,
sortNotesAlphabetically, sortNotesByTitle,
sortNotes,
setNoteToParent setNoteToParent
}; };

View File

@@ -106,8 +106,6 @@ function sendPing(client, entityChanges = []) {
} }
} }
const stats = require('./sync').stats;
sendMessage(client, { sendMessage(client, {
type: 'sync', type: 'sync',
data: entityChanges data: entityChanges
@@ -118,9 +116,7 @@ function sendTransactionSyncsToAllClients() {
if (webSocketServer) { if (webSocketServer) {
const entityChanges = cls.getAndClearEntityChanges(); const entityChanges = cls.getAndClearEntityChanges();
webSocketServer.clients.forEach(function each(client) { webSocketServer.clients.forEach(client => sendPing(client, entityChanges));
sendPing(client, entityChanges);
});
} }
} }

View File

@@ -5,7 +5,7 @@
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<title>Trilium Notes</title> <title>Trilium Notes</title>
</head> </head>
<body class="desktop theme-<%= theme %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;"> <body class="desktop theme-<%= theme %> heading-style-<%= headingStyle %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
<noscript>Trilium requires JavaScript to be enabled.</noscript> <noscript>Trilium requires JavaScript to be enabled.</noscript>
<script> <script>
@@ -39,6 +39,7 @@
<%- include('dialogs/move_to.ejs') %> <%- include('dialogs/move_to.ejs') %>
<%- include('dialogs/backend_log.ejs') %> <%- include('dialogs/backend_log.ejs') %>
<%- include('dialogs/include_note.ejs') %> <%- include('dialogs/include_note.ejs') %>
<%- include('dialogs/sort_child_notes.ejs') %>
<script type="text/javascript"> <script type="text/javascript">
window.baseApiUrl = 'api/'; window.baseApiUrl = 'api/';

View File

@@ -0,0 +1,60 @@
<div id="sort-child-notes-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="max-width: 500px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title mr-auto">Sort children by ...</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>
<form id="sort-child-notes-form">
<div class="modal-body">
<h5>Sorting criteria</h5>
<div class="form-check">
<input class="form-check-input" type="radio" name="sort-by" value="title" id="sort-by-title" checked>
<label class="form-check-label" for="sort-by-title">
title
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="sort-by" value="dateCreated" id="sort-by-date-created">
<label class="form-check-label" for="sort-by-date-created">
date created
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="sort-by" value="dateModified" id="sort-by-date-modified">
<label class="form-check-label" for="sort-by-date-modified">
date modified
</label>
</div>
<br/>
<h5>Sorting direction</h5>
<div class="form-check">
<input class="form-check-input" type="radio" name="sort-direction" value="asc" id="sort-direction-asc" checked>
<label class="form-check-label" for="sort-direction-asc">
ascending
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="sort-direction" value="desc" id="sort-direction-desc">
<label class="form-check-label" for="sort-direction-desc">
descending
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Sort <kbd>enter</kbd></button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -95,7 +95,7 @@
} }
</style> </style>
</head> </head>
<body class="mobile theme-<%= theme %>"> <body class="mobile theme-<%= theme %> heading-style-<%= headingStyle %>">
<noscript>Trilium requires JavaScript to be enabled.</noscript> <noscript>Trilium requires JavaScript to be enabled.</noscript>
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div> <div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>

View File

@@ -7,6 +7,9 @@
<sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/dist" /> <excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/src/public/app-dist" /> <excludeFolder url="file://$MODULE_DIR$/src/public/app-dist" />
<excludeFolder url="file://$MODULE_DIR$/libraries" />
<excludeFolder url="file://$MODULE_DIR$/docs" />
<excludeFolder url="file://$MODULE_DIR$/bin/better-sqlite3" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />