Compare commits

...

36 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
42 changed files with 278 additions and 158 deletions

3
.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

@@ -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"
@@ -105,4 +78,4 @@ echo "Pushing docker image to dockerhub"
bin/push-docker-image.sh $VERSION bin/push-docker-image.sh $VERSION
echo "Release finished!" echo "Release finished!"

Binary file not shown.

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.46.4-beta", "version": "0.46.7",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

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.5", "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",
@@ -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",
"joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "16.5.0", "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.21.0", "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,7 +71,6 @@
"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.4", "ws": "7.4.4",
"yauzl": "2.10.0", "yauzl": "2.10.0",

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

@@ -49,7 +49,12 @@ class Note extends Entity {
this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable(); this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable();
if (this.isContentAvailable) { if (this.isContentAvailable) {
this.title = protectedSessionService.decryptString(this.title); try {
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

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

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

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

@@ -36,7 +36,7 @@ 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); appContext.addBeforeUnloadListener(this);

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/>
@@ -235,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 => {
@@ -245,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;
@@ -272,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();
@@ -327,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()];
@@ -362,8 +382,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
else { else {
node.setActive(); node.setActive();
this.clearSelectedNodes();
} }
return false; return false;
@@ -373,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();
@@ -797,8 +817,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
const node = await this.expandToNote(activeContext.notePath); const node = await this.expandToNote(activeContext.notePath);
await node.makeVisible({scrollIntoView: true}); if (node) {
node.setActive(true, {noEvents: true, noFocus: false}); await node.makeVisible({scrollIntoView: true});
node.setActive(true, {noEvents: true, noFocus: false});
}
} }
} }
@@ -851,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=${hoistedNoteService.getHoistedNoteId()}, 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;
@@ -955,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.
@@ -1114,11 +1144,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
if (node) { if (node) {
node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
if (activeNodeFocused) { if (activeNodeFocused) {
node.setFocus(true); // 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

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

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

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

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

@@ -25,6 +25,7 @@ const TPL = `
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 {
@@ -33,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 {
@@ -65,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

@@ -107,7 +107,11 @@ function processContent(images, note, content) {
const filename = path.basename(src); const filename = path.basename(src);
if (!dataUrl || !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

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

@@ -41,7 +41,8 @@ const ALLOWED_OPTIONS = new Set([
'attributeListExpanded', 'attributeListExpanded',
'promotedAttributesExpanded', 'promotedAttributesExpanded',
'similarNotesExpanded', 'similarNotesExpanded',
'headingStyle' '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

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

@@ -1 +1 @@
module.exports = { buildDate:"2021-03-14T22:56:27+01:00", buildRevision: "6c8d20288df302f3a415bd1bdcace98bf29d4bf6" }; module.exports = { buildDate:"2021-04-22T20:50:22+02:00", buildRevision: "c27f573eed8905fc1d958adf5bdee0efc3aff593" };

View File

@@ -53,22 +53,14 @@ function moveEntityChangeToTop(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);
} }
}); });

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

@@ -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,9 +417,14 @@ class Note {
decrypt() { decrypt() {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
this.title = protectedSessionService.decryptString(this.title); try {
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}`);
}
} }
} }

View File

@@ -177,6 +177,10 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
} }
}); });
eventService.subscribe(eventService.LEAVE_PROTECTED_SESSION, () => {
load();
});
module.exports = { module.exports = {
load load
}; };

View File

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

@@ -86,6 +86,7 @@ const defaultOptions = [
{ 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: '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

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

@@ -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');
@@ -139,7 +140,12 @@ function sortNotesByTitle(parentNoteId, foldersFirst = false, reverse = false) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
[position, note.branchId]); [position, note.branchId]);
noteCache.branches[note.branchId].notePosition = position; if (note.branchId in noteCache.branches) {
noteCache.branches[note.branchId].notePosition = position;
}
else {
log.info(`Branch "${note.branchId}" was not found in note cache.`);
}
position += 10; position += 10;
} }

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" />