mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
45 Commits
algolia_v1
...
v0.49.1-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad8d35efe9 | ||
|
|
0217b1c85d | ||
|
|
c0aa14f586 | ||
|
|
b54cfab4ff | ||
|
|
a08985e7a6 | ||
|
|
a789025025 | ||
|
|
3f307b117e | ||
|
|
a232035d47 | ||
|
|
9d38e9342d | ||
|
|
4bc4b9ade7 | ||
|
|
f0217cae5e | ||
|
|
da050c6369 | ||
|
|
842c317568 | ||
|
|
47845930f4 | ||
|
|
972f2f40bf | ||
|
|
94111c464b | ||
|
|
94e18dfb7c | ||
|
|
bc9903191e | ||
|
|
cd8c24ceae | ||
|
|
f9709c9c39 | ||
|
|
c0964a4f12 | ||
|
|
bcef8579ce | ||
|
|
1180be75d1 | ||
|
|
cfa49c7b1b | ||
|
|
8e4926ed7f | ||
|
|
2430dcba65 | ||
|
|
7c885a8b76 | ||
|
|
402e29d6dc | ||
|
|
e7faebfac3 | ||
|
|
10a5773c66 | ||
|
|
1e8472266f | ||
|
|
b30792a3da | ||
|
|
8b56fb10fd | ||
|
|
26602e8226 | ||
|
|
b8eeb0371c | ||
|
|
b1c4737e78 | ||
|
|
3860028a9e | ||
|
|
16d97b95af | ||
|
|
20465a4f71 | ||
|
|
2ff6e50af4 | ||
|
|
e0378c5064 | ||
|
|
e29aee1aae | ||
|
|
1aff42f453 | ||
|
|
a098630e09 | ||
|
|
074eb1c02f |
23
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
23
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -3,13 +3,6 @@ description: Report a bug
|
||||
title: "(Bug report) "
|
||||
labels: "Type: Bug"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I have searched the [issue tracker](https://www.github.com/zadam/trilium/issues) for a bug report that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Trilium Version
|
||||
@@ -30,7 +23,7 @@ body:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What is your setup?
|
||||
label: What is your setup?
|
||||
description: https://github.com/zadam/trilium/wiki#choose-the-setup
|
||||
options:
|
||||
- Local (no sync)
|
||||
@@ -47,17 +40,7 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
label: Description
|
||||
description: A clear and concise description of the bug and any additional information.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: A clear description of what actually happens.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
|
||||
|
||||
9
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
9
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,15 +1,8 @@
|
||||
name: Feature Request
|
||||
description: Report a bug
|
||||
description: Ask for a new feature to be added
|
||||
title: "(Feature request) "
|
||||
labels: "Type: Enhancement"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I have searched the [issue tracker](https://www.github.com/zadam/trilium/issues) for a feature request that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe feature
|
||||
|
||||
Binary file not shown.
Binary file not shown.
8
db/migrations/0188__set_hidden_branchId.sql
Normal file
8
db/migrations/0188__set_hidden_branchId.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
UPDATE branches SET branchId = 'hidden' where branchId = (
|
||||
SELECT branchId FROM branches
|
||||
WHERE parentNoteId = 'root'
|
||||
AND noteId = 'hidden'
|
||||
AND isDeleted = 0
|
||||
ORDER BY utcDateModified
|
||||
LIMIT 1
|
||||
);
|
||||
4
libraries/bootstrap/css/bootstrap.min.css
vendored
4
libraries/bootstrap/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11798
package-lock.json
generated
11798
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.48.8",
|
||||
"version": "0.49.1-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -28,7 +28,7 @@
|
||||
"async-mutex": "0.3.2",
|
||||
"axios": "0.24.0",
|
||||
"better-sqlite3": "7.4.5",
|
||||
"body-parser": "1.19.0",
|
||||
"body-parser": "1.19.1",
|
||||
"chokidar": "3.5.2",
|
||||
"cls-hooked": "4.2.2",
|
||||
"commonmark": "0.30.0",
|
||||
@@ -40,7 +40,8 @@
|
||||
"electron-dl": "3.3.0",
|
||||
"electron-find": "1.0.7",
|
||||
"electron-window-state": "5.0.3",
|
||||
"express": "4.17.1",
|
||||
"@electron/remote": "2.0.1",
|
||||
"express": "4.17.2",
|
||||
"express-partial-content": "^1.0.2",
|
||||
"express-rate-limit": "5.5.1",
|
||||
"express-session": "1.17.2",
|
||||
@@ -67,7 +68,7 @@
|
||||
"request": "^2.88.2",
|
||||
"rimraf": "3.0.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.6.0",
|
||||
"sanitize-html": "2.6.1",
|
||||
"sax": "1.2.4",
|
||||
"semver": "7.3.5",
|
||||
"serve-favicon": "2.5.0",
|
||||
@@ -77,13 +78,12 @@
|
||||
"tmp": "^0.2.1",
|
||||
"turndown": "7.1.1",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "8.3.0",
|
||||
"ws": "8.4.0",
|
||||
"yauzl": "2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "16.0.4",
|
||||
"@electron/remote": "2.0.1",
|
||||
"electron": "16.0.5",
|
||||
"electron-builder": "22.14.5",
|
||||
"electron-packager": "15.4.0",
|
||||
"electron-rebuild": "3.2.5",
|
||||
|
||||
@@ -42,7 +42,7 @@ class NoteBuilder {
|
||||
}
|
||||
|
||||
child(childNoteBuilder, prefix = "") {
|
||||
new Branch(becca, {
|
||||
new Branch({
|
||||
branchId: id(),
|
||||
noteId: childNoteBuilder.note.noteId,
|
||||
parentNoteId: this.note.noteId,
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("Parser", () => {
|
||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||
expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp");
|
||||
expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp");
|
||||
expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("BeccaFlatTextExp");
|
||||
expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||
expect(rootExp.subExpressions[1].subExpressions[0].tokens).toEqual(["hello", "hi"]);
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("Parser", () => {
|
||||
|
||||
const subs = rootExp.subExpressions[1].subExpressions;
|
||||
|
||||
expect(subs[0].constructor.name).toEqual("BeccaFlatTextExp");
|
||||
expect(subs[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||
expect(subs[0].tokens).toEqual(["hello", "hi"]);
|
||||
|
||||
expect(subs[1].constructor.name).toEqual("NoteContentProtectedFulltextExp");
|
||||
@@ -182,7 +182,7 @@ describe("Parser", () => {
|
||||
expect(firstSub.propertyName).toEqual('isArchived');
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("OrExp");
|
||||
expect(secondSub.subExpressions[0].constructor.name).toEqual("BeccaFlatTextExp");
|
||||
expect(secondSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||
expect(secondSub.subExpressions[0].tokens).toEqual(["hello"]);
|
||||
|
||||
expect(thirdSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
|
||||
@@ -13,7 +13,7 @@ describe("Search", () => {
|
||||
becca.reset();
|
||||
|
||||
rootNote = new NoteBuilder(new Note({noteId: 'root', title: 'root', type: 'text'}));
|
||||
new Branch(becca, {branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
|
||||
new Branch({branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
|
||||
});
|
||||
|
||||
it("simple path match", () => {
|
||||
@@ -157,6 +157,21 @@ describe("Search", () => {
|
||||
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("inherited label comparison", () => {
|
||||
rootNote
|
||||
.child(note("Europe")
|
||||
.label('country', '', true)
|
||||
.child(note("Austria"))
|
||||
.child(note("Czech Republic"))
|
||||
);
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
const searchResults = searchService.findResultsWithQuery('austria #country', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("numeric label comparison fallback to string comparison", () => {
|
||||
// dates should not be coerced into numbers which would then give wrong numbers
|
||||
|
||||
@@ -169,7 +184,7 @@ describe("Search", () => {
|
||||
.label('established', '1993-01-01'))
|
||||
.child(note("Hungary")
|
||||
.label('established', '1920-06-04'))
|
||||
);
|
||||
);
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
@@ -218,7 +233,7 @@ describe("Search", () => {
|
||||
test("#month = month", 1);
|
||||
test("#month = 'MONTH'", 0);
|
||||
|
||||
test("note.dateCreated =* month", 1);
|
||||
test("note.dateCreated =* month", 2);
|
||||
|
||||
test("#date = TODAY", 1);
|
||||
test("#date = today", 1);
|
||||
@@ -337,11 +352,11 @@ describe("Search", () => {
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
let searchResults = searchService.findResultsWithQuery('#city AND note.getAncestors().title = Europe', searchContext);
|
||||
let searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Europe', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy();
|
||||
|
||||
searchResults = searchService.findResultsWithQuery('#city AND note.getAncestors().title = Asia', searchContext);
|
||||
searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Asia', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Taipei")).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -58,7 +58,10 @@ class Branch extends AbstractEntity {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.becca.branches[this.branchId] = this;
|
||||
if (this.branchId) {
|
||||
this.becca.branches[this.branchId] = this;
|
||||
}
|
||||
|
||||
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||
|
||||
if (this.branchId === 'root') {
|
||||
@@ -84,7 +87,7 @@ class Branch extends AbstractEntity {
|
||||
/** @returns {Note} */
|
||||
get childNote() {
|
||||
if (!(this.noteId in this.becca.notes)) {
|
||||
// entities can come out of order in sync, create skeleton which will be filled later
|
||||
// entities can come out of order in sync/import, create skeleton which will be filled later
|
||||
this.becca.addNote(this.noteId, new Note({noteId: this.noteId}));
|
||||
}
|
||||
|
||||
@@ -98,7 +101,7 @@ class Branch extends AbstractEntity {
|
||||
/** @returns {Note} */
|
||||
get parentNote() {
|
||||
if (!(this.parentNoteId in this.becca.notes)) {
|
||||
// entities can come out of order in sync, create skeleton which will be filled later
|
||||
// entities can come out of order in sync/import, create skeleton which will be filled later
|
||||
this.becca.addNote(this.parentNoteId, new Note({noteId: this.parentNoteId}));
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,10 @@ class Note extends AbstractEntity {
|
||||
return this.parentBranches;
|
||||
}
|
||||
|
||||
/** @returns {Branch[]} */
|
||||
/**
|
||||
* @returns {Branch[]}
|
||||
* @deprecated use getParentBranches() instead
|
||||
*/
|
||||
getBranches() {
|
||||
return this.parentBranches;
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ const TPL = `
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image-jpeg-quality">JPEG quality (0 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
|
||||
<input class="form-control" id="image-jpeg-quality" min="0" max="100" type="number">
|
||||
<label for="image-jpeg-quality">JPEG quality (10 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
|
||||
<input class="form-control" id="image-jpeg-quality" min="10" max="100" type="number">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,18 +130,38 @@ class NoteShort {
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {string[]} */
|
||||
getBranchIds() {
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getParentBranchIds() {
|
||||
return Object.values(this.parentToBranch);
|
||||
}
|
||||
|
||||
/** @returns {Branch[]} */
|
||||
getBranches() {
|
||||
/**
|
||||
* @returns {string[]}
|
||||
* @deprecated use getParentBranchIds() instead
|
||||
*/
|
||||
getBranchIds() {
|
||||
return this.getParentBranchIds();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Branch[]}
|
||||
*/
|
||||
getParentBranches() {
|
||||
const branchIds = Object.values(this.parentToBranch);
|
||||
|
||||
return this.froca.getBranches(branchIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Branch[]}
|
||||
* @deprecated use getParentBranches() instead
|
||||
*/
|
||||
getBranches() {
|
||||
return this.getParentBranches();
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
hasChildren() {
|
||||
return this.children.length > 0;
|
||||
@@ -378,6 +398,9 @@ class NoteShort {
|
||||
else if (this.noteId === 'root') {
|
||||
return "bx bx-chevrons-right";
|
||||
}
|
||||
if (this.noteId === 'share') {
|
||||
return "bx bx-share-alt";
|
||||
}
|
||||
else if (this.type === 'text') {
|
||||
if (this.isFolder()) {
|
||||
return "bx bx-folder";
|
||||
@@ -620,8 +643,8 @@ class NoteShort {
|
||||
});
|
||||
}
|
||||
|
||||
hasAncestor(ancestorNote, visitedNoteIds = null) {
|
||||
if (this.noteId === ancestorNote.noteId) {
|
||||
hasAncestor(ancestorNoteId, visitedNoteIds = null) {
|
||||
if (this.noteId === ancestorNoteId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -635,13 +658,13 @@ class NoteShort {
|
||||
visitedNoteIds.add(this.noteId);
|
||||
|
||||
for (const templateNote of this.getTemplateNotes()) {
|
||||
if (templateNote.hasAncestor(ancestorNote, visitedNoteIds)) {
|
||||
if (templateNote.hasAncestor(ancestorNoteId, visitedNoteIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const parentNote of this.getParentNotes()) {
|
||||
if (parentNote.hasAncestor(ancestorNote, visitedNoteIds)) {
|
||||
if (parentNote.hasAncestor(ancestorNoteId, visitedNoteIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -758,6 +781,26 @@ class NoteShort {
|
||||
throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`);
|
||||
}
|
||||
}
|
||||
|
||||
isShared() {
|
||||
for (const parentNoteId of this.parents) {
|
||||
if (parentNoteId === 'root' || parentNoteId === 'none') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentNote = froca.notes[parentNoteId];
|
||||
|
||||
if (!parentNote) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parentNote.noteId === 'share' || parentNote.isShared()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteShort;
|
||||
|
||||
@@ -47,6 +47,7 @@ import MermaidWidget from "../widgets/mermaid.js";
|
||||
import BookmarkButtons from "../widgets/bookmark_buttons.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import BacklinksWidget from "../widgets/backlinks.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@@ -147,6 +148,7 @@ export default class DesktopLayout {
|
||||
.titlePlacement("bottom"))
|
||||
.button(new NoteActionsWidget())
|
||||
)
|
||||
.child(new SharedInfoWidget())
|
||||
.child(new NoteUpdateStatusWidget())
|
||||
.child(new BacklinksWidget())
|
||||
.child(new MermaidWidget())
|
||||
|
||||
@@ -50,7 +50,7 @@ function isAffecting(attrRow, affectedNote) {
|
||||
|
||||
if (this.isInheritable) {
|
||||
for (const owningNote of owningNotes) {
|
||||
if (owningNote.hasAncestor(attrNote)) {
|
||||
if (owningNote.hasAncestor(attrNote.noteId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,9 +213,13 @@ export default class Entrypoints extends Component {
|
||||
} else if (note.mime.endsWith("env=backend")) {
|
||||
await server.post('script/run/' + note.noteId);
|
||||
} else if (note.mime === 'text/x-sqlite;schema=trilium') {
|
||||
const {results} = await server.post("sql/execute/" + note.noteId);
|
||||
const resp = await server.post("sql/execute/" + note.noteId);
|
||||
|
||||
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: results});
|
||||
if (!resp.success) {
|
||||
alert("Error occurred while executing SQL query: " + resp.message);
|
||||
}
|
||||
|
||||
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
|
||||
}
|
||||
|
||||
toastService.showMessage("Note executed");
|
||||
|
||||
@@ -188,7 +188,7 @@ class Froca {
|
||||
froca.notes[note.noteId].childToBranch = {};
|
||||
}
|
||||
|
||||
const branches = [...note.getBranches(), ...note.getChildBranches()];
|
||||
const branches = [...note.getParentBranches(), ...note.getChildBranches()];
|
||||
|
||||
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
|
||||
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
|
||||
|
||||
@@ -75,7 +75,9 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
|
||||
if (logErrors) {
|
||||
const parent = froca.getNoteFromCache(parentNoteId);
|
||||
|
||||
console.debug(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})`)}. You can ignore this message as it is mostly harmless.`);
|
||||
console.debug(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})`)}.
|
||||
You can ignore this message as it is mostly harmless.`);
|
||||
}
|
||||
|
||||
const someNotePath = getSomeNotePath(child, hoistedNoteId);
|
||||
@@ -83,6 +85,10 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
|
||||
if (someNotePath) { // in case it's root the path may be empty
|
||||
const pathToRoot = someNotePath.split("/").reverse().slice(1);
|
||||
|
||||
if (!pathToRoot.includes("root")) {
|
||||
pathToRoot.push('root');
|
||||
}
|
||||
|
||||
for (const noteId of pathToRoot) {
|
||||
effectivePathSegments.push(noteId);
|
||||
}
|
||||
|
||||
@@ -340,10 +340,12 @@ function initHelpDropdown($el) {
|
||||
|
||||
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"
|
||||
|
||||
function openHelp(e) {
|
||||
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
|
||||
}
|
||||
|
||||
function initHelpButtons($el) {
|
||||
$el.on("click", "*[data-help-page]", e => {
|
||||
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
|
||||
});
|
||||
$el.on("click", "*[data-help-page]", e => openHelp(e));
|
||||
}
|
||||
|
||||
function filterAttributeName(name) {
|
||||
@@ -397,6 +399,7 @@ export default {
|
||||
timeLimit,
|
||||
initHelpDropdown,
|
||||
initHelpButtons,
|
||||
openHelp,
|
||||
filterAttributeName,
|
||||
isValidAttributeName
|
||||
};
|
||||
|
||||
@@ -210,7 +210,10 @@ const ATTR_HELP = {
|
||||
"hoistedInbox": "default inbox location for new notes when hoisted to some ancestor of this note",
|
||||
"sqlConsoleHome": "default location of SQL console notes",
|
||||
"bookmarked": "note with this label will appear in bookmarks",
|
||||
"bookmarkFolder": "note with this label will appear in bookmarks as folder (allowing access to its children)"
|
||||
"bookmarkFolder": "note with this label will appear in bookmarks as folder (allowing access to its children)",
|
||||
"shareHiddenFromTree": "this note is hidden from left navigation tree, but still accessible with its URL",
|
||||
"shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]",
|
||||
"shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.",
|
||||
},
|
||||
"relation": {
|
||||
"runOnNoteCreation": "executes when note is created on backend",
|
||||
@@ -221,7 +224,8 @@ const ATTR_HELP = {
|
||||
"runOnAttributeChange": "executes when attribute is changed under this note",
|
||||
"template": "attached note's attributes will be inherited even without parent-child relationship. See template for details.",
|
||||
"renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered',
|
||||
"widget": "target of this relation will be executed and rendered as a widget in the sidebar"
|
||||
"widget": "target of this relation will be executed and rendered as a widget in the sidebar",
|
||||
"shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.",
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -33,11 +33,7 @@ const TPL = `
|
||||
.backlinks-close-ticker {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backlinks-ticker:hover {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
|
||||
.backlinks-items {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
@@ -107,17 +103,19 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
this.clearItems();
|
||||
|
||||
const targetRelationCount = note.getTargetRelations().length;
|
||||
if (targetRelationCount === 0) {
|
||||
// can't use froca since that would count only relations from loaded notes
|
||||
const resp = await server.get(`notes/${this.noteId}/backlink-count`);
|
||||
|
||||
if (!resp || !resp.count) {
|
||||
this.$ticker.hide();
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.$ticker.show();
|
||||
this.$count.text(
|
||||
`${targetRelationCount} backlink`
|
||||
+ (targetRelationCount === 1 ? '' : 's')
|
||||
);
|
||||
}
|
||||
|
||||
this.$ticker.show();
|
||||
this.$count.text(
|
||||
`${resp.count} backlink`
|
||||
+ (resp.count === 1 ? '' : 's')
|
||||
);
|
||||
}
|
||||
|
||||
clearItems() {
|
||||
@@ -140,18 +138,22 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
await froca.getNotes(backlinks.map(bl => bl.noteId)); // prefetch all
|
||||
|
||||
for (const backlink of backlinks) {
|
||||
this.$items.append(await linkService.createNoteLink(backlink.noteId, {
|
||||
const $item = $("<div>");
|
||||
|
||||
$item.append(await linkService.createNoteLink(backlink.noteId, {
|
||||
showNoteIcon: true,
|
||||
showNotePath: true,
|
||||
showTooltip: false
|
||||
}));
|
||||
|
||||
if (backlink.relationName) {
|
||||
this.$items.append($("<p>").text("relation: " + backlink.relationName));
|
||||
$item.append($("<p>").text("relation: " + backlink.relationName));
|
||||
}
|
||||
else {
|
||||
this.$items.append(...backlink.excerpts);
|
||||
$item.append(...backlink.excerpts);
|
||||
}
|
||||
|
||||
this.$items.append($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,32 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import SwitchWidget from "./switch.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="bookmark-switch">
|
||||
<style>
|
||||
/* The switch - the box around the slider */
|
||||
.bookmark-switch .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.bookmark-switch .slider {
|
||||
border-radius: 24px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--more-accented-background-color);
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.bookmark-switch .slider:before {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: var(--main-background-color);
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.bookmark-switch .slider.checked {
|
||||
background-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.bookmark-switch .slider.checked:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="add-bookmark-button">
|
||||
Bookmark
|
||||
|
||||
|
||||
|
||||
<span title="Bookmark this note to the left side panel">
|
||||
<label class="switch">
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="remove-bookmark-button">
|
||||
Bookmark
|
||||
|
||||
|
||||
|
||||
<span title="Remove bookmark">
|
||||
<label class="switch">
|
||||
<span class="slider checked"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class BookmarkSwitchWidget extends NoteContextAwareWidget {
|
||||
export default class BookmarkSwitchWidget extends SwitchWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
super.doRender();
|
||||
|
||||
this.$addBookmarkButton = this.$widget.find(".add-bookmark-button");
|
||||
this.$addBookmarkButton.on('click', () => attributeService.setLabel(this.noteId, 'bookmarked'));
|
||||
this.$switchOnName.text("Bookmark");
|
||||
this.$switchOnButton.attr("title", "Bookmark this note to the left side panel");
|
||||
|
||||
this.$removeBookmarkButton = this.$widget.find(".remove-bookmark-button");
|
||||
this.$removeBookmarkButton.on('click', async () => {
|
||||
for (const label of this.note.getLabels('bookmarked')) {
|
||||
await attributeService.removeAttributeById(this.noteId, label.attributeId);
|
||||
}
|
||||
});
|
||||
this.$switchOffName.text("Bookmark");
|
||||
this.$switchOffButton.attr("title", "Remove bookmark");
|
||||
}
|
||||
|
||||
async switchOff() {
|
||||
for (const label of this.note.getLabels('bookmarked')) {
|
||||
await attributeService.removeAttributeById(this.noteId, label.attributeId);
|
||||
}
|
||||
}
|
||||
|
||||
switchOn() {
|
||||
return attributeService.setLabel(this.noteId, 'bookmarked');
|
||||
}
|
||||
|
||||
refreshWithNote(note) {
|
||||
const isBookmarked = note.hasLabel('bookmarked');
|
||||
|
||||
this.$addBookmarkButton.toggle(!isBookmarked);
|
||||
this.$removeBookmarkButton.toggle(isBookmarked);
|
||||
this.$switchOn.toggle(!isBookmarked);
|
||||
this.$switchOff.toggle(isBookmarked);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
|
||||
@@ -44,7 +44,10 @@ export default class ButtonWidget extends NoteContextAwareWidget {
|
||||
this.$widget.tooltip({
|
||||
html: true,
|
||||
title: () => {
|
||||
const title = this.settings.title;
|
||||
const title = typeof this.settings.title === "function"
|
||||
? this.settings.title()
|
||||
: this.settings.title;
|
||||
|
||||
const action = actions.find(act => act.actionName === this.settings.command);
|
||||
|
||||
if (action && action.effectiveShortcuts.length > 0) {
|
||||
|
||||
@@ -18,7 +18,12 @@ export default class OpenNoteButtonWidget extends ButtonWidget {
|
||||
}
|
||||
|
||||
this.icon(note.getIcon());
|
||||
this.title(note.title);
|
||||
this.title(() => {
|
||||
const n = froca.getNoteFromCache(noteId);
|
||||
|
||||
// always fresh, always decoded (when protected session is available)
|
||||
return n.title;
|
||||
});
|
||||
|
||||
this.refreshIcon();
|
||||
});
|
||||
|
||||
@@ -661,7 +661,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
extraClasses.push("protected");
|
||||
}
|
||||
|
||||
if (note.getParentNoteIds().length > 1) {
|
||||
if (note.isShared()) {
|
||||
extraClasses.push("shared");
|
||||
}
|
||||
else if (note.getParentNoteIds().length > 1) {
|
||||
const notSearchParents = note.getParentNoteIds()
|
||||
.map(noteId => froca.notes[noteId])
|
||||
.filter(note => !!note)
|
||||
@@ -1014,8 +1017,14 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
for (const ecBranch of loadResults.getBranches()) {
|
||||
// adding noteId itself to update all potential clones
|
||||
noteIdsToUpdate.add(ecBranch.noteId);
|
||||
if (ecBranch.parentNoteId === 'share') {
|
||||
// all shared notes have a sign in the tree, even the descendants of shared notes
|
||||
noteIdsToReload.add(ecBranch.noteId);
|
||||
}
|
||||
else {
|
||||
// adding noteId itself to update all potential clones
|
||||
noteIdsToUpdate.add(ecBranch.noteId);
|
||||
}
|
||||
|
||||
for (const node of this.getNodesByBranch(ecBranch)) {
|
||||
if (ecBranch.isDeleted) {
|
||||
|
||||
@@ -1,89 +1,28 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import SwitchWidget from "./switch.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="protected-note-switch">
|
||||
<style>
|
||||
/* The switch - the box around the slider */
|
||||
.protected-note-switch .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.protected-note-switch .slider {
|
||||
border-radius: 24px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--more-accented-background-color);
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.protected-note-switch .slider:before {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: var(--main-background-color);
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.protected-note-switch .slider.checked {
|
||||
background-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.protected-note-switch .slider.checked:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="protect-button">
|
||||
Protect the note
|
||||
|
||||
|
||||
|
||||
<span title="Note is not protected, click to make it protected">
|
||||
<label class="switch">
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="unprotect-button">
|
||||
Unprotect the note
|
||||
|
||||
|
||||
|
||||
<span title="Note is protected, click to make it unprotected">
|
||||
<label class="switch">
|
||||
<span class="slider checked"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class ProtectedNoteSwitchWidget extends NoteContextAwareWidget {
|
||||
export default class ProtectedNoteSwitchWidget extends SwitchWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
super.doRender();
|
||||
|
||||
this.$protectButton = this.$widget.find(".protect-button");
|
||||
this.$protectButton.on('click', () => protectedSessionService.protectNote(this.noteId, true, false));
|
||||
this.$switchOnName.text("Protect the note");
|
||||
this.$switchOnButton.attr("title", "Note is not protected, click to make it protected");
|
||||
|
||||
this.$unprotectButton = this.$widget.find(".unprotect-button");
|
||||
this.$unprotectButton.on('click', () => protectedSessionService.protectNote(this.noteId, false, false));
|
||||
this.$switchOffName.text("Unprotect the note");
|
||||
this.$switchOffButton.attr("title", "Note is protected, click to make it unprotected");
|
||||
}
|
||||
|
||||
switchOn() {
|
||||
protectedSessionService.protectNote(this.noteId, true, false);
|
||||
}
|
||||
|
||||
switchOff() {
|
||||
protectedSessionService.protectNote(this.noteId, false, false)
|
||||
}
|
||||
|
||||
refreshWithNote(note) {
|
||||
this.$protectButton.toggle(!note.isProtected);
|
||||
this.$unprotectButton.toggle(!!note.isProtected);
|
||||
this.$switchOn.toggle(!note.isProtected);
|
||||
this.$switchOff.toggle(!!note.isProtected);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import NoteTypeWidget from "../note_type.js";
|
||||
import ProtectedNoteSwitchWidget from "../protected_note_switch.js";
|
||||
import EditabilitySelectWidget from "../editability_select.js";
|
||||
import BookmarkSwitchWidget from "../bookmark_switch.js";
|
||||
import SharedSwitchWidget from "../shared_switch.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="basic-properties-widget">
|
||||
@@ -36,6 +37,8 @@ const TPL = `
|
||||
</div>
|
||||
|
||||
<div class="bookmark-switch-container"></div>
|
||||
|
||||
<div class="shared-switch-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
@@ -46,12 +49,14 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
this.protectedNoteSwitchWidget = new ProtectedNoteSwitchWidget().contentSized();
|
||||
this.editabilitySelectWidget = new EditabilitySelectWidget().contentSized();
|
||||
this.bookmarkSwitchWidget = new BookmarkSwitchWidget().contentSized();
|
||||
this.sharedSwitchWidget = new SharedSwitchWidget().contentSized();
|
||||
|
||||
this.child(
|
||||
this.noteTypeWidget,
|
||||
this.protectedNoteSwitchWidget,
|
||||
this.editabilitySelectWidget,
|
||||
this.bookmarkSwitchWidget
|
||||
this.bookmarkSwitchWidget,
|
||||
this.sharedSwitchWidget
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,6 +88,7 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
this.$widget.find(".protected-note-switch-container").append(this.protectedNoteSwitchWidget.render());
|
||||
this.$widget.find(".editability-select-container").append(this.editabilitySelectWidget.render());
|
||||
this.$widget.find(".bookmark-switch-container").append(this.bookmarkSwitchWidget.render());
|
||||
this.$widget.find(".shared-switch-container").append(this.sharedSwitchWidget.render());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
|
||||
57
src/public/app/widgets/shared_info.js
Normal file
57
src/public/app/widgets/shared_info.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import options from "../services/options.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="shared-info-widget alert alert-warning">
|
||||
<style>
|
||||
.shared-info-widget {
|
||||
margin: 10px;
|
||||
contain: none;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="share-text"></span> <a class="share-link external"></a>. For help visit <a href="https://github.com/zadam/trilium/wiki/Sharing">wiki</a>.
|
||||
</div>`;
|
||||
|
||||
export default class SharedInfoWidget extends NoteContextAwareWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteId !== 'share' && this.note.hasAncestor('share');
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$shareLink = this.$widget.find(".share-link");
|
||||
this.$shareText = this.$widget.find(".share-text");
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const syncServerHost = options.get("syncServerHost");
|
||||
let link;
|
||||
|
||||
const shareId = note.getOwnedLabelValue('shareAlias') || note.noteId;
|
||||
|
||||
if (syncServerHost) {
|
||||
link = syncServerHost + "/share/" + shareId;
|
||||
this.$shareText.text("This note is shared publicly on");
|
||||
}
|
||||
else {
|
||||
link = location.protocol + '//' + location.host + location.pathname + "share/" + shareId;
|
||||
this.$shareText.text("This note is shared locally on");
|
||||
}
|
||||
|
||||
this.$shareLink.attr("href", link).text(link);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes().find(attr => attr.name.startsWith("share") && attributeService.isAffecting(attr, this.note))) {
|
||||
this.refresh();
|
||||
}
|
||||
else if (loadResults.getBranches().find(branch => branch.noteId === this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/public/app/widgets/shared_switch.js
Normal file
71
src/public/app/widgets/shared_switch.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import SwitchWidget from "./switch.js";
|
||||
import branchService from "../services/branches.js";
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
export default class SharedSwitchWidget extends SwitchWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteId !== 'root' && this.noteId !== 'share';
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.$switchOnName.text("Shared");
|
||||
this.$switchOnButton.attr("title", "Share the note");
|
||||
|
||||
this.$switchOffName.text("Shared");
|
||||
this.$switchOffButton.attr("title", "Unshare the note");
|
||||
|
||||
this.$helpButton.attr("data-help-page", "Sharing").show();
|
||||
this.$helpButton.on('click', e => utils.openHelp(e));
|
||||
}
|
||||
|
||||
switchOn() {
|
||||
branchService.cloneNoteTo(this.noteId, 'share');
|
||||
}
|
||||
|
||||
async switchOff() {
|
||||
const shareBranch = this.note.getParentBranches().find(b => b.parentNoteId === 'share');
|
||||
|
||||
if (!shareBranch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.note.getParentBranches().length === 1) {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
|
||||
const text = "This note exists only as a shared note, unsharing would delete it. Do you want to continue and thus delete this note?";
|
||||
|
||||
if (!await confirmDialog.confirm(text)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await server.remove(`branches/${shareBranch.branchId}?taskId=no-progress-reporting`);
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const isShared = note.hasAncestor('share');
|
||||
const canBeUnshared = isShared && note.getParentBranches().find(b => b.parentNoteId === 'share');
|
||||
const switchDisabled = isShared && !canBeUnshared;
|
||||
|
||||
this.$switchOn.toggle(!isShared);
|
||||
this.$switchOff.toggle(!!isShared);
|
||||
|
||||
if (switchDisabled) {
|
||||
this.$widget.attr("title", "Note cannot be unshared here because it is shared through inheritance from an ancestor.");
|
||||
this.$switchOff.addClass("switch-disabled");
|
||||
}
|
||||
else {
|
||||
this.$widget.removeAttr("title");
|
||||
this.$switchOff.removeClass("switch-disabled");
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getBranches().find(b => b.noteId === this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/public/app/widgets/switch.js
Normal file
117
src/public/app/widgets/switch.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="switch-widget">
|
||||
<style>
|
||||
.switch-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* The switch - the box around the slider */
|
||||
.switch-widget .switch {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.switch-on, .switch-off {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.switch-widget .slider {
|
||||
border-radius: 24px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--more-accented-background-color);
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.switch-widget .slider:before {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: var(--main-background-color);
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.switch-widget .slider.checked {
|
||||
background-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.switch-widget .slider.checked:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.switch-widget .switch-disabled {
|
||||
opacity: 70%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.switch-widget .switch-help-button {
|
||||
font-weight: 900;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="switch-on">
|
||||
<span class="switch-on-name"></span>
|
||||
|
||||
|
||||
|
||||
<span class="switch-on-button">
|
||||
<label class="switch">
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="switch-off">
|
||||
<span class="switch-off-name"></span>
|
||||
|
||||
|
||||
|
||||
<span class="switch-off-button">
|
||||
<label class="switch">
|
||||
<span class="slider checked"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button class="switch-help-button" type="button" data-help-page="" title="Open help page" style="display: none;">?</button>
|
||||
</div>`;
|
||||
|
||||
export default class SwitchWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$switchOn = this.$widget.find(".switch-on");
|
||||
this.$switchOnName = this.$widget.find(".switch-on-name");
|
||||
this.$switchOnButton = this.$widget.find(".switch-on-button");
|
||||
|
||||
this.$switchOnButton.on('click', () => this.switchOn());
|
||||
|
||||
this.$switchOff = this.$widget.find(".switch-off");
|
||||
this.$switchOffName = this.$widget.find(".switch-off-name");
|
||||
this.$switchOffButton = this.$widget.find(".switch-off-button");
|
||||
|
||||
this.$switchOffButton.on('click', () => this.switchOff());
|
||||
|
||||
this.$helpButton = this.$widget.find(".switch-help-button");
|
||||
|
||||
}
|
||||
|
||||
switchOff() {}
|
||||
switchOn() {}
|
||||
}
|
||||
@@ -14,20 +14,10 @@ const TPL = `
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trilium-api-docs-button {
|
||||
/*display: none;*/
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.note-detail-code-editor {
|
||||
min-height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class="btn bx bx-help-circle trilium-api-docs-button icon-button floating-button"
|
||||
title="Open Trilium API docs"></button>
|
||||
|
||||
<div class="note-detail-code-editor"></div>
|
||||
|
||||
@@ -37,6 +27,13 @@ const TPL = `
|
||||
Execute <kbd data-command="runActiveNote"></kbd>
|
||||
</button>
|
||||
|
||||
<button class="no-print trilium-api-docs-button btn btn-sm"
|
||||
title="Open Trilium API docs">
|
||||
<span class="bx bx-help-circle"></span>
|
||||
|
||||
API docs
|
||||
</button>
|
||||
|
||||
<button class="no-print save-to-note-button btn btn-sm">
|
||||
|
||||
<span class="bx bx-save"></span>
|
||||
|
||||
@@ -84,5 +84,11 @@ export default class EmptyTypeWidget extends TypeWidget {
|
||||
.on('click', () => this.triggerCommand('hoistNote', {noteId: workspaceNote.noteId}))
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceNotes.length === 0) {
|
||||
this.$autoComplete
|
||||
.trigger('focus')
|
||||
.trigger('select');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
body {
|
||||
font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
|
||||
}
|
||||
|
||||
#layout {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
#menu {
|
||||
padding: 20px;
|
||||
padding: 25px;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
@@ -32,14 +36,14 @@
|
||||
|
||||
#title {
|
||||
margin: 0;
|
||||
padding: 10px 20px 0 20px;
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.type-image img {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -48,40 +52,59 @@ pre {
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
#menuLink {
|
||||
iframe.pdf-view {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
}
|
||||
|
||||
#menuButton {
|
||||
display: none;
|
||||
position: fixed;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
left: 5px;
|
||||
width: 1.4em;
|
||||
background: #000;
|
||||
background: rgba(0,0,0,0.7);
|
||||
border-radius: 5px;
|
||||
border: 1px solid #aaa;
|
||||
font-size: 2rem;
|
||||
z-index: 10;
|
||||
height: auto;
|
||||
color: white;
|
||||
border: none;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#menuButton::after {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
#layout.active #menu {
|
||||
#layout.navMenu #menu {
|
||||
display: block;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
#menuButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#layout.active #main {
|
||||
#layout.navMenu #main {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#layout.active #menuLink::after {
|
||||
#title {
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
#layout.navMenu #menuButton::after {
|
||||
content: "«";
|
||||
}
|
||||
|
||||
#menuButton::after {
|
||||
content: "»";
|
||||
}
|
||||
|
||||
#menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#menuLink::after {
|
||||
content: "»";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,13 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
content: " *"
|
||||
content: " *";
|
||||
}
|
||||
|
||||
span.fancytree-node.shared .fancytree-title::after {
|
||||
font-family: 'boxicons' !important;
|
||||
font-size: smaller;
|
||||
content: " \ec03"; /* lookup code for "share-alt" in boxicons.css */
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
const keyboardActions = require('../../services/keyboard_actions');
|
||||
const sql = require('../../services/sql');
|
||||
const becca = require('../../becca/becca');
|
||||
|
||||
function getKeyboardActions() {
|
||||
return keyboardActions.getKeyboardActions();
|
||||
}
|
||||
|
||||
function getShortcutsForNotes() {
|
||||
return sql.getMap(`
|
||||
SELECT value, noteId
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND type = 'label'
|
||||
AND name = 'keyboardShortcut'`);
|
||||
const attrs = becca.findAttributes('label', 'keyboardShortcut');
|
||||
|
||||
const map = {};
|
||||
|
||||
for (const attr of attrs) {
|
||||
map[attr.value] = attr.noteId;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -73,7 +73,7 @@ function deleteNote(req) {
|
||||
|
||||
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
|
||||
|
||||
for (const branch of note.getBranches()) {
|
||||
for (const branch of note.getParentBranches()) {
|
||||
noteService.deleteBranch(branch, deleteId, taskContext);
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ function getDeleteNotesPreview(req) {
|
||||
|
||||
const note = branch.getNote();
|
||||
|
||||
if (deleteAllClones || note.getBranches().length <= branchCountToDelete[branch.branchId]) {
|
||||
if (deleteAllClones || note.getParentBranches().length <= branchCountToDelete[branch.branchId]) {
|
||||
noteIdsToBeDeleted.add(note.noteId);
|
||||
|
||||
for (const childBranch of note.getChildBranches()) {
|
||||
@@ -302,6 +302,21 @@ function uploadModifiedFile(req) {
|
||||
note.setContent(fileContent);
|
||||
}
|
||||
|
||||
function getBacklinkCount(req) {
|
||||
const {noteId} = req.params;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return [404, "Not found"];
|
||||
}
|
||||
else {
|
||||
return {
|
||||
count: note.getTargetRelations().length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNote,
|
||||
updateNote,
|
||||
@@ -316,5 +331,6 @@ module.exports = {
|
||||
duplicateSubtree,
|
||||
eraseDeletedNotesNow,
|
||||
getDeleteNotesPreview,
|
||||
uploadModifiedFile
|
||||
uploadModifiedFile,
|
||||
getBacklinkCount
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ function run(req) {
|
||||
}
|
||||
|
||||
function getBundlesWithLabel(label, value) {
|
||||
const notes = attributeService.getNotesWithLabel(label, value);
|
||||
const notes = attributeService.getNotesWithLabelFast(label, value);
|
||||
|
||||
const bundles = [];
|
||||
|
||||
|
||||
@@ -17,7 +17,17 @@ function uploadImage(req) {
|
||||
|
||||
const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
|
||||
|
||||
const {noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
|
||||
const {note, noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
|
||||
|
||||
const labelsStr = req.headers['x-labels'];
|
||||
|
||||
if (labelsStr?.trim()) {
|
||||
const labels = JSON.parse(labelsStr);
|
||||
|
||||
for (const {name, value} of labels) {
|
||||
note.setLabel(attributeService.sanitizeAttributeName(name), value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
noteId: noteId
|
||||
|
||||
@@ -220,6 +220,7 @@ function register(app) {
|
||||
apiRoute(DELETE, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision);
|
||||
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
|
||||
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
|
||||
apiRoute(GET, '/api/notes/:noteId/backlink-count', notesApiRoute.getBacklinkCount);
|
||||
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
|
||||
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
|
||||
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
|
||||
|
||||
@@ -4,7 +4,7 @@ const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 187;
|
||||
const APP_DB_VERSION = 188;
|
||||
const SYNC_VERSION = 23;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ const BUILTIN_ATTRIBUTES = [
|
||||
{ type: 'label', name: 'sorted' },
|
||||
{ type: 'label', name: 'top' },
|
||||
{ type: 'label', name: 'fullContentWidth' },
|
||||
{ type: 'label', name: 'shareHiddenFromTree' },
|
||||
{ type: 'label', name: 'shareAlias' },
|
||||
{ type: 'label', name: 'shareOmitDefaultCss' },
|
||||
|
||||
// relation names
|
||||
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
|
||||
@@ -62,7 +65,8 @@ const BUILTIN_ATTRIBUTES = [
|
||||
{ type: 'relation', name: 'runOnAttributeChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'template' },
|
||||
{ type: 'relation', name: 'widget', isDangerous: true },
|
||||
{ type: 'relation', name: 'renderNote', isDangerous: true }
|
||||
{ type: 'relation', name: 'renderNote', isDangerous: true },
|
||||
{ type: 'relation', name: 'shareCss', isDangerous: false },
|
||||
];
|
||||
|
||||
/** @returns {Note[]} */
|
||||
@@ -95,6 +99,24 @@ function getNoteWithLabel(name, value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not take into account templates and inheritance
|
||||
*/
|
||||
function getNotesWithLabelFast(name, value) {
|
||||
// optimized version (~20 times faster) without using normal search, useful for e.g. finding date notes
|
||||
const attrs = becca.findAttributes('label', name);
|
||||
|
||||
if (value === undefined) {
|
||||
return attrs.map(attr => attr.getNote());
|
||||
}
|
||||
|
||||
value = value?.toLowerCase();
|
||||
|
||||
return attrs
|
||||
.filter(attr => attr.value.toLowerCase() === value)
|
||||
.map(attr => attr.getNote());
|
||||
}
|
||||
|
||||
function createLabel(noteId, name, value = "") {
|
||||
return createAttribute({
|
||||
noteId: noteId,
|
||||
@@ -186,6 +208,7 @@ function sanitizeAttributeName(origName) {
|
||||
|
||||
module.exports = {
|
||||
getNotesWithLabel,
|
||||
getNotesWithLabelFast,
|
||||
getNoteWithLabel,
|
||||
createLabel,
|
||||
createRelation,
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2021-12-13T11:12:31+01:00", buildRevision: "d9550dd59b9b0dff0b229c400cdf6585abcb226a" };
|
||||
module.exports = { buildDate:"2021-12-24T23:05:10+01:00", buildRevision: "0217b1c85de9a2824e7f07d07a357064c5803383" };
|
||||
|
||||
@@ -11,6 +11,12 @@ const becca = require("../becca/becca");
|
||||
const beccaService = require("../becca/becca_service");
|
||||
|
||||
function cloneNoteToParent(noteId, parentBranchId, prefix) {
|
||||
if (parentBranchId === 'share') {
|
||||
const specialNotesService = require('./special_notes');
|
||||
// share root note is created lazily
|
||||
specialNotesService.getShareRoot();
|
||||
}
|
||||
|
||||
const parentBranch = becca.getBranch(parentBranchId);
|
||||
|
||||
if (isNoteDeleted(noteId) || isNoteDeleted(parentBranch.noteId)) {
|
||||
|
||||
@@ -258,7 +258,8 @@ class ConsistencyChecks {
|
||||
FROM branches
|
||||
WHERE noteId = ?
|
||||
and parentNoteId = ?
|
||||
and isDeleted = 0`, [noteId, parentNoteId]);
|
||||
and isDeleted = 0
|
||||
ORDER BY utcDateCreated`, [noteId, parentNoteId]);
|
||||
|
||||
const branches = branchIds.map(branchId => becca.getBranch(branchId));
|
||||
|
||||
@@ -537,6 +538,27 @@ class ConsistencyChecks {
|
||||
logError(`Unrecognized entity change id=${id}, entityName=${entityName}, entityId=${entityId}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT
|
||||
id, entityId
|
||||
FROM
|
||||
entity_changes
|
||||
JOIN ${entityName} ON entityId = ${key}
|
||||
WHERE
|
||||
entity_changes.isErased = 1
|
||||
AND entity_changes.entityName = '${entityName}'`,
|
||||
({id, entityId}) => {
|
||||
if (this.autoFix) {
|
||||
sql.execute(`DELETE FROM ${entityName} WHERE ${key} = ?`, [entityId]);
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Erasing entityName=${entityName}, entityId=${entityId} since entity change id=${id} has it as erased.`);
|
||||
} else {
|
||||
logError(`Entity change id=${id} has entityName=${entityName}, entityId=${entityId} as erased, but it's not.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findEntityChangeIssues() {
|
||||
@@ -603,14 +625,14 @@ class ConsistencyChecks {
|
||||
this.fixedIssues = false;
|
||||
this.reloadNeeded = false;
|
||||
|
||||
this.findEntityChangeIssues();
|
||||
|
||||
this.findBrokenReferenceIssues();
|
||||
|
||||
this.findExistencyIssues();
|
||||
|
||||
this.findLogicIssues();
|
||||
|
||||
this.findEntityChangeIssues();
|
||||
|
||||
this.findWronglyNamedAttributes();
|
||||
|
||||
this.findSyncIssues();
|
||||
|
||||
@@ -50,7 +50,13 @@ function exportToZip(taskContext, branch, format, res) {
|
||||
}
|
||||
|
||||
function getDataFileName(note, baseFileName, existingFileNames) {
|
||||
const existingExtension = path.extname(baseFileName).toLowerCase();
|
||||
let fileName = baseFileName;
|
||||
|
||||
if (fileName.length > 30) {
|
||||
fileName = fileName.substr(0, 30);
|
||||
}
|
||||
|
||||
let existingExtension = path.extname(fileName).toLowerCase();
|
||||
let newExtension;
|
||||
|
||||
// following two are handled specifically since we always want to have these extensions no matter the automatic detection
|
||||
@@ -68,13 +74,12 @@ function exportToZip(taskContext, branch, format, res) {
|
||||
newExtension = null;
|
||||
}
|
||||
else {
|
||||
newExtension = mimeTypes.extension(note.mime) || "dat";
|
||||
}
|
||||
|
||||
let fileName = baseFileName;
|
||||
|
||||
if (fileName.length > 30) {
|
||||
fileName = fileName.substr(0, 30);
|
||||
if (note.mime?.toLowerCase()?.trim() === "image/jpg") {
|
||||
newExtension = 'jpg';
|
||||
}
|
||||
else {
|
||||
newExtension = mimeTypes.extension(note.mime) || "dat";
|
||||
}
|
||||
}
|
||||
|
||||
// if the note is already named with extension (e.g. "jquery"), then it's silly to append exact same extension again
|
||||
|
||||
@@ -123,7 +123,11 @@ function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch,
|
||||
}
|
||||
|
||||
async function shrinkImage(buffer, originalName) {
|
||||
const jpegQuality = optionService.getOptionInt('imageJpegQuality');
|
||||
let jpegQuality = optionService.getOptionInt('imageJpegQuality');
|
||||
|
||||
if (jpegQuality < 10 || jpegQuality > 100) {
|
||||
jpegQuality = 75;
|
||||
}
|
||||
|
||||
let finalImageBuffer;
|
||||
try {
|
||||
|
||||
@@ -351,7 +351,19 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
|
||||
let note = becca.getNote(noteId);
|
||||
|
||||
const isProtected = importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable();
|
||||
|
||||
if (note) {
|
||||
// only skeleton was created because of altered order of cloned notes in ZIP, we need to update
|
||||
// https://github.com/zadam/trilium/issues/2440
|
||||
if (note.type === undefined) {
|
||||
note.type = type;
|
||||
note.mime = mime;
|
||||
note.title = noteTitle;
|
||||
note.isProtected = isProtected;
|
||||
note.save();
|
||||
}
|
||||
|
||||
note.setContent(content);
|
||||
}
|
||||
else {
|
||||
@@ -367,7 +379,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
// root notePosition should be ignored since it relates to original document
|
||||
// now import root should be placed after existing notes into new parent
|
||||
notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined,
|
||||
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
isProtected: isProtected,
|
||||
}));
|
||||
|
||||
createdNoteIds[note.noteId] = true;
|
||||
|
||||
@@ -118,6 +118,7 @@ function createNewNote(params) {
|
||||
note.setContent(params.content);
|
||||
|
||||
const branch = new Branch({
|
||||
branchId: params.branchId,
|
||||
noteId: note.noteId,
|
||||
parentNoteId: params.parentNoteId,
|
||||
notePosition: params.notePosition !== undefined ? params.notePosition : getNewNotePosition(params.parentNoteId),
|
||||
@@ -540,7 +541,7 @@ function deleteBranch(branch, deleteId, taskContext) {
|
||||
branch.markAsDeleted(deleteId);
|
||||
|
||||
const note = branch.getNote();
|
||||
const notDeletedBranches = note.getBranches();
|
||||
const notDeletedBranches = note.getParentBranches();
|
||||
|
||||
if (notDeletedBranches.length === 0) {
|
||||
for (const childBranch of note.getChildBranches()) {
|
||||
@@ -732,23 +733,26 @@ function eraseAttributes(attributeIdsToErase) {
|
||||
}
|
||||
|
||||
function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) {
|
||||
if (eraseEntitiesAfterTimeInSeconds === null) {
|
||||
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds');
|
||||
}
|
||||
// this is important also so that the erased entity changes are sent to the connected clients
|
||||
sql.transactional(() => {
|
||||
if (eraseEntitiesAfterTimeInSeconds === null) {
|
||||
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds');
|
||||
}
|
||||
|
||||
const cutoffDate = new Date(Date.now() - eraseEntitiesAfterTimeInSeconds * 1000);
|
||||
const cutoffDate = new Date(Date.now() - eraseEntitiesAfterTimeInSeconds * 1000);
|
||||
|
||||
const noteIdsToErase = sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
|
||||
const noteIdsToErase = sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
|
||||
|
||||
eraseNotes(noteIdsToErase);
|
||||
eraseNotes(noteIdsToErase);
|
||||
|
||||
const branchIdsToErase = sql.getColumn("SELECT branchId FROM branches WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
|
||||
const branchIdsToErase = sql.getColumn("SELECT branchId FROM branches WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
|
||||
|
||||
eraseBranches(branchIdsToErase);
|
||||
eraseBranches(branchIdsToErase);
|
||||
|
||||
const attributeIdsToErase = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
|
||||
const attributeIdsToErase = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateTimeStr(cutoffDate)]);
|
||||
|
||||
eraseAttributes(attributeIdsToErase);
|
||||
eraseAttributes(attributeIdsToErase);
|
||||
});
|
||||
}
|
||||
|
||||
function eraseNotesWithDeleteId(deleteId) {
|
||||
@@ -785,7 +789,7 @@ function duplicateSubtree(origNoteId, newParentNoteId) {
|
||||
|
||||
const origNote = becca.notes[origNoteId];
|
||||
// might be null if orig note is not in the target newParentNoteId
|
||||
const origBranch = origNote.getBranches().find(branch => branch.parentNoteId === newParentNoteId);
|
||||
const origBranch = origNote.getParentBranches().find(branch => branch.parentNoteId === newParentNoteId);
|
||||
|
||||
const noteIdMapping = getNoteIdMapping(origNote);
|
||||
|
||||
|
||||
@@ -23,20 +23,18 @@ class AttributeExistsExp extends Expression {
|
||||
for (const attr of attrs) {
|
||||
const note = attr.note;
|
||||
|
||||
if (inputNoteSet.hasNoteId(note.noteId)) {
|
||||
if (attr.isInheritable) {
|
||||
resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated());
|
||||
}
|
||||
else if (note.isTemplate()) {
|
||||
resultNoteSet.addAll(note.getTemplatedNotes());
|
||||
}
|
||||
else {
|
||||
resultNoteSet.add(note);
|
||||
}
|
||||
if (attr.isInheritable) {
|
||||
resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated());
|
||||
}
|
||||
else if (note.isTemplate()) {
|
||||
resultNoteSet.addAll(note.getTemplatedNotes());
|
||||
}
|
||||
else {
|
||||
resultNoteSet.add(note);
|
||||
}
|
||||
}
|
||||
|
||||
return resultNoteSet;
|
||||
return resultNoteSet.intersection(inputNoteSet);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const RelationWhereExp = require('../expressions/relation_where');
|
||||
const PropertyComparisonExp = require('../expressions/property_comparison');
|
||||
const AttributeExistsExp = require('../expressions/attribute_exists');
|
||||
const LabelComparisonExp = require('../expressions/label_comparison');
|
||||
const BeccaFlatTextExp = require('../expressions/note_cache_flat_text');
|
||||
const NoteFlatTextExp = require('../expressions/note_flat_text.js');
|
||||
const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext');
|
||||
const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext');
|
||||
const OrderByAndLimitExp = require('../expressions/order_by_and_limit');
|
||||
@@ -31,13 +31,13 @@ function getFulltext(tokens, searchContext) {
|
||||
|
||||
if (!searchContext.fastSearch) {
|
||||
return new OrExp([
|
||||
new BeccaFlatTextExp(tokens),
|
||||
new NoteFlatTextExp(tokens),
|
||||
new NoteContentProtectedFulltextExp('*=*', tokens),
|
||||
new NoteContentUnprotectedFulltextExp('*=*', tokens)
|
||||
]);
|
||||
}
|
||||
else {
|
||||
return new BeccaFlatTextExp(tokens);
|
||||
return new NoteFlatTextExp(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ function getHiddenRoot() {
|
||||
|
||||
if (!hidden) {
|
||||
hidden = noteService.createNewNote({
|
||||
branchId: 'hidden',
|
||||
noteId: 'hidden',
|
||||
title: 'hidden',
|
||||
type: 'text',
|
||||
@@ -206,11 +207,12 @@ function getShareRoot() {
|
||||
|
||||
if (!shareRoot) {
|
||||
shareRoot = noteService.createNewNote({
|
||||
branchId: 'share',
|
||||
noteId: 'share',
|
||||
title: 'share',
|
||||
title: 'Shared notes',
|
||||
type: 'text',
|
||||
content: '',
|
||||
parentNoteId: getHiddenRoot().noteId
|
||||
parentNoteId: 'root'
|
||||
}).note;
|
||||
}
|
||||
|
||||
@@ -223,7 +225,7 @@ function createMissingSpecialNotes() {
|
||||
getSinglesNoteRoot();
|
||||
getSinglesNoteRoot();
|
||||
getGlobalNoteMap();
|
||||
getShareRoot();
|
||||
// share root is not automatically created since it's visible in the tree and many won't need it/use it
|
||||
|
||||
const hidden = getHiddenRoot();
|
||||
|
||||
@@ -238,5 +240,6 @@ module.exports = {
|
||||
saveSqlConsole,
|
||||
createSearchNote,
|
||||
saveSearchNote,
|
||||
createMissingSpecialNotes
|
||||
createMissingSpecialNotes,
|
||||
getShareRoot
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
/**
|
||||
* @module sql
|
||||
*
|
||||
* TODO: some methods (like getValue()) could use raw rows
|
||||
*/
|
||||
|
||||
const log = require('./log');
|
||||
@@ -89,13 +91,7 @@ function getRowOrNull(query, params = []) {
|
||||
}
|
||||
|
||||
function getValue(query, params = []) {
|
||||
const row = getRowOrNull(query, params);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return row[Object.keys(row)[0]];
|
||||
return wrap(query, s => s.pluck().get(params));
|
||||
}
|
||||
|
||||
// smaller values can result in better performance due to better usage of statement cache
|
||||
@@ -144,32 +140,17 @@ function iterateRows(query, params = []) {
|
||||
|
||||
function getMap(query, params = []) {
|
||||
const map = {};
|
||||
const results = getRows(query, params);
|
||||
const results = getRawRows(query, params);
|
||||
|
||||
for (const row of results) {
|
||||
const keys = Object.keys(row);
|
||||
|
||||
map[row[keys[0]]] = row[keys[1]];
|
||||
map[row[0]] = row[1];
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function getColumn(query, params = []) {
|
||||
const list = [];
|
||||
const result = getRows(query, params);
|
||||
|
||||
if (result.length === 0) {
|
||||
return list;
|
||||
}
|
||||
|
||||
const key = Object.keys(result[0])[0];
|
||||
|
||||
for (const row of result) {
|
||||
list.push(row[key]);
|
||||
}
|
||||
|
||||
return list;
|
||||
return wrap(query, s => s.pluck().all(params));
|
||||
}
|
||||
|
||||
function execute(query, params = []) {
|
||||
|
||||
@@ -94,7 +94,7 @@ async function createInitialDatabase(username, password, theme) {
|
||||
|
||||
log.info("Importing demo content ...");
|
||||
|
||||
const dummyTaskContext = new TaskContext("initial-demo-import", 'import', false);
|
||||
const dummyTaskContext = new TaskContext("no-progress-reporting", 'import', false);
|
||||
|
||||
const zipImportService = require("./import/zip");
|
||||
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
|
||||
|
||||
@@ -34,7 +34,7 @@ class TaskContext {
|
||||
increaseProgressCount() {
|
||||
this.progressCount++;
|
||||
|
||||
if (Date.now() - this.lastSentCountTs >= 300 && this.taskId !== 'initial-demo-import') {
|
||||
if (Date.now() - this.lastSentCountTs >= 300 && this.taskId !== 'no-progress-reporting') {
|
||||
this.lastSentCountTs = Date.now();
|
||||
|
||||
ws.sendMessageToAllClients({
|
||||
|
||||
@@ -41,10 +41,15 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
|
||||
|
||||
const existing = getExistingBranch(parentNoteId, childNoteId);
|
||||
|
||||
console.log("BBBB", existing);
|
||||
|
||||
if (existing && (branchId === null || existing.branchId !== branchId)) {
|
||||
const parentNote = becca.getNote(parentNoteId);
|
||||
const childNote = becca.getNote(childNoteId);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'This note already exists in the target.'
|
||||
message: `Note "${childNote.title}" note already exists in the "${parentNote.title}".`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,7 +64,12 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
|
||||
}
|
||||
|
||||
function getExistingBranch(parentNoteId, childNoteId) {
|
||||
const branchId = sql.getValue('SELECT branchId FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0', [childNoteId, parentNoteId]);
|
||||
const branchId = sql.getValue(`
|
||||
SELECT branchId
|
||||
FROM branches
|
||||
WHERE noteId = ?
|
||||
AND parentNoteId = ?
|
||||
AND isDeleted = 0`, [childNoteId, parentNoteId]);
|
||||
|
||||
return becca.getBranch(branchId);
|
||||
}
|
||||
@@ -173,7 +183,7 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
|
||||
let position = 10;
|
||||
|
||||
for (const note of notes) {
|
||||
const branch = note.getBranches().find(b => b.parentNoteId === parentNoteId);
|
||||
const branch = note.getParentBranches().find(b => b.parentNoteId === parentNoteId);
|
||||
|
||||
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
|
||||
[position, branch.branchId]);
|
||||
|
||||
@@ -59,6 +59,7 @@ async function createMainWindow() {
|
||||
height: mainWindowState.height,
|
||||
title: 'Trilium Notes',
|
||||
webPreferences: {
|
||||
enableRemoteModule: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: spellcheckEnabled
|
||||
|
||||
98
src/share/content_renderer.js
Normal file
98
src/share/content_renderer.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const {JSDOM} = require("jsdom");
|
||||
const NO_CONTENT = '<p>This note has no content.</p>';
|
||||
const shaca = require("./shaca/shaca");
|
||||
|
||||
function getChildrenList(note) {
|
||||
if (note.hasChildren()) {
|
||||
const document = new JSDOM().window.document;
|
||||
|
||||
const ulEl = document.createElement("ul");
|
||||
|
||||
for (const childNote of note.getChildNotes()) {
|
||||
const li = document.createElement("li");
|
||||
const link = document.createElement("a");
|
||||
link.appendChild(document.createTextNode(childNote.title));
|
||||
link.setAttribute("href", childNote.noteId);
|
||||
|
||||
li.appendChild(link);
|
||||
ulEl.appendChild(li);
|
||||
}
|
||||
|
||||
return '<p>Child notes:</p>' + ulEl.outerHTML;
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getContent(note) {
|
||||
let content = note.getContent();
|
||||
|
||||
if (note.type === 'text') {
|
||||
const document = new JSDOM(content || "").window.document;
|
||||
|
||||
const isEmpty = document.body.textContent.trim().length === 0
|
||||
&& document.querySelectorAll("img").length === 0;
|
||||
|
||||
if (isEmpty) {
|
||||
content = NO_CONTENT + getChildrenList(note);
|
||||
}
|
||||
else {
|
||||
for (const linkEl of document.querySelectorAll("a")) {
|
||||
const href = linkEl.getAttribute("href");
|
||||
|
||||
if (href?.startsWith("#")) {
|
||||
const notePathSegments = href.split("/");
|
||||
|
||||
const noteId = notePathSegments[notePathSegments.length - 1];
|
||||
const linkedNote = shaca.getNote(noteId);
|
||||
|
||||
if (linkedNote) {
|
||||
linkEl.setAttribute("href", linkedNote.shareId);
|
||||
}
|
||||
else {
|
||||
linkEl.removeAttribute("href");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content = document.body.innerHTML;
|
||||
}
|
||||
}
|
||||
else if (note.type === 'code' || note.type === 'mermaid') {
|
||||
if (!content?.trim()) {
|
||||
content = NO_CONTENT + getChildrenList(note);
|
||||
}
|
||||
else {
|
||||
const document = new JSDOM().window.document;
|
||||
|
||||
const preEl = document.createElement('pre');
|
||||
preEl.appendChild(document.createTextNode(content));
|
||||
|
||||
content = preEl.outerHTML;
|
||||
}
|
||||
}
|
||||
else if (note.type === 'image') {
|
||||
content = `<img src="api/images/${note.noteId}/${note.title}?${note.utcDateModified}">`;
|
||||
}
|
||||
else if (note.type === 'file') {
|
||||
if (note.mime === 'application/pdf') {
|
||||
content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`
|
||||
}
|
||||
else {
|
||||
content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
|
||||
}
|
||||
}
|
||||
else if (note.type === 'book') {
|
||||
content = getChildrenList(note);
|
||||
}
|
||||
else {
|
||||
content = '<p>This note type cannot be displayed.</p>' + getChildrenList(note);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getContent
|
||||
};
|
||||
@@ -1,123 +1,46 @@
|
||||
const shaca = require("./shaca/shaca");
|
||||
const shacaLoader = require("./shaca/shaca_loader");
|
||||
const shareRoot = require("./share_root");
|
||||
const {JSDOM} = require("jsdom");
|
||||
const contentRenderer = require("./content_renderer.js");
|
||||
|
||||
function getSubRoot(note) {
|
||||
function getSharedSubTreeRoot(note) {
|
||||
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
// share root itself is not shared
|
||||
return null;
|
||||
}
|
||||
|
||||
// every path leads to share root, but which one to choose?
|
||||
// for sake of simplicity URLs are not note paths
|
||||
const parentNote = note.getParentNotes()[0];
|
||||
|
||||
if (parentNote.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
return note;
|
||||
}
|
||||
|
||||
return getSubRoot(parentNote);
|
||||
}
|
||||
|
||||
const NO_CONTENT = '<p>This note has no content.</p>';
|
||||
|
||||
function getChildrenList(note) {
|
||||
if (note.hasChildren()) {
|
||||
const document = new JSDOM().window.document;
|
||||
|
||||
const ulEl = document.createElement("ul");
|
||||
|
||||
for (const childNote of note.getChildNotes()) {
|
||||
const li = document.createElement("li");
|
||||
const link = document.createElement("a");
|
||||
link.appendChild(document.createTextNode(childNote.title));
|
||||
link.setAttribute("href", childNote.noteId);
|
||||
|
||||
li.appendChild(link);
|
||||
ulEl.appendChild(li);
|
||||
}
|
||||
|
||||
return '<p>Child notes:</p>' + ulEl.outerHTML;
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getContent(note) {
|
||||
let content = note.getContent();
|
||||
|
||||
if (note.type === 'text') {
|
||||
const document = new JSDOM(content || "").window.document;
|
||||
|
||||
const isEmpty = document.body.textContent.trim().length === 0
|
||||
&& document.querySelectorAll("img").length === 0;
|
||||
|
||||
if (isEmpty) {
|
||||
content = NO_CONTENT + getChildrenList(note);
|
||||
}
|
||||
else {
|
||||
for (const linkEl of document.querySelectorAll("a")) {
|
||||
const href = linkEl.getAttribute("href");
|
||||
|
||||
if (href?.startsWith("#")) {
|
||||
const notePathSegments = href.split("/");
|
||||
|
||||
linkEl.setAttribute("href", notePathSegments[notePathSegments.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
content = document.body.innerHTML;
|
||||
}
|
||||
}
|
||||
else if (note.type === 'code') {
|
||||
if (!content?.trim()) {
|
||||
content = NO_CONTENT + getChildrenList(note);
|
||||
}
|
||||
else {
|
||||
const document = new JSDOM().window.document;
|
||||
|
||||
const preEl = document.createElement('pre');
|
||||
preEl.appendChild(document.createTextNode(content));
|
||||
|
||||
content = preEl.outerHTML;
|
||||
}
|
||||
}
|
||||
else if (note.type === 'image') {
|
||||
content = `<img src="api/images/${note.noteId}/${note.title}?${note.utcDateModified}">`;
|
||||
}
|
||||
else if (note.type === 'file') {
|
||||
content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
|
||||
}
|
||||
else if (note.type === 'book') {
|
||||
content = getChildrenList(note);
|
||||
}
|
||||
else {
|
||||
content = '<p>This note type cannot be displayed.</p>' + getChildrenList(note);
|
||||
}
|
||||
|
||||
return `<div class="type-${note.type}">${content}</content>`;
|
||||
return getSharedSubTreeRoot(parentNote);
|
||||
}
|
||||
|
||||
function register(router) {
|
||||
router.get('/share/:noteId', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
router.get('/share/:shareId', (req, res, next) => {
|
||||
const {shareId} = req.params;
|
||||
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
if (noteId in shaca.notes) {
|
||||
const note = shaca.notes[noteId];
|
||||
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
||||
|
||||
const content = getContent(note);
|
||||
if (note) {
|
||||
const content = contentRenderer.getContent(note);
|
||||
|
||||
const subRoot = getSubRoot(note);
|
||||
const subRoot = getSharedSubTreeRoot(note);
|
||||
|
||||
res.render("share", {
|
||||
res.render("share/page", {
|
||||
note,
|
||||
content,
|
||||
subRoot
|
||||
});
|
||||
}
|
||||
else {
|
||||
res.send("FFF");
|
||||
res.status(404).render("share/404");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -125,10 +48,10 @@ function register(router) {
|
||||
const image = shaca.getNote(req.params.noteId);
|
||||
|
||||
if (!image) {
|
||||
return res.sendStatus(404);
|
||||
return res.status(404).send("Not found");
|
||||
}
|
||||
else if (image.type !== 'image') {
|
||||
return res.sendStatus(400);
|
||||
return res.status(400).send("Requested note is not an image");
|
||||
}
|
||||
|
||||
res.set('Content-Type', image.mime);
|
||||
@@ -136,12 +59,12 @@ function register(router) {
|
||||
res.send(image.getContent());
|
||||
});
|
||||
|
||||
router.get('/share/api/notes/:noteId/:download', (req, res, next) => {
|
||||
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).send(`Note ${noteId} doesn't exist.`);
|
||||
return res.status(404).send(`Not found`);
|
||||
}
|
||||
|
||||
const utils = require("../services/utils");
|
||||
@@ -155,6 +78,26 @@ function register(router) {
|
||||
|
||||
res.send(note.getContent());
|
||||
});
|
||||
|
||||
router.get('/share/api/notes/:noteId/view', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).send(`Not found`);
|
||||
}
|
||||
|
||||
const utils = require("../services/utils");
|
||||
|
||||
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
||||
|
||||
// res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
res.send(note.getContent());
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -39,6 +39,10 @@ class Attribute extends AbstractEntity {
|
||||
linkedChildNote.parents = linkedChildNote.parents.filter(parentNote => parentNote.noteId !== this.noteId);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.type === 'label' && this.name === 'shareAlias' && this.value.trim()) {
|
||||
this.shaca.aliasToNote[this.value.trim()] = this.note;
|
||||
}
|
||||
}
|
||||
|
||||
get isAffectingSubtree() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
const AbstractEntity = require('./abstract_entity');
|
||||
const shareRoot = require("../../share_root");
|
||||
|
||||
class Branch extends AbstractEntity {
|
||||
constructor([branchId, noteId, parentNoteId, prefix, isExpanded]) {
|
||||
@@ -18,10 +17,6 @@ class Branch extends AbstractEntity {
|
||||
/** @param {boolean} */
|
||||
this.isExpanded = !!isExpanded;
|
||||
|
||||
if (this.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childNote = this.childNote;
|
||||
const parentNote = this.parentNote;
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@ class Note extends AbstractEntity {
|
||||
this.targetRelations = [];
|
||||
|
||||
this.shaca.notes[this.noteId] = this;
|
||||
|
||||
/** @param {Note[]|null} */
|
||||
this.ancestorCache = null;
|
||||
}
|
||||
|
||||
getParentBranches() {
|
||||
@@ -93,36 +90,6 @@ class Note extends AbstractEntity {
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {*} */
|
||||
getJsonContent() {
|
||||
const content = this.getContent();
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is of application/json content type */
|
||||
isJson() {
|
||||
return this.mime === "application/json";
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
|
||||
isJavaScript() {
|
||||
return (this.type === "code" || this.type === "file")
|
||||
&& (this.mime.startsWith("application/javascript")
|
||||
|| this.mime === "application/x-javascript"
|
||||
|| this.mime === "text/javascript");
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is HTML */
|
||||
isHtml() {
|
||||
return ["code", "file", "render"].includes(this.type)
|
||||
&& this.mime === "text/html";
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if the note has string content (not binary) */
|
||||
isStringNote() {
|
||||
return utils.isStringNote(this.type, this.mime);
|
||||
@@ -218,16 +185,6 @@ class Note extends AbstractEntity {
|
||||
return !!this.getAttributes().find(attr => attr.type === type && attr.name === name);
|
||||
}
|
||||
|
||||
getAttributeCaseInsensitive(type, name, value) {
|
||||
name = name.toLowerCase();
|
||||
value = value ? value.toLowerCase() : null;
|
||||
|
||||
return this.getAttributes().find(
|
||||
attr => attr.type === type
|
||||
&& attr.name.toLowerCase() === name
|
||||
&& (!value || attr.value.toLowerCase() === value));
|
||||
}
|
||||
|
||||
getRelationTarget(name) {
|
||||
const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name);
|
||||
|
||||
@@ -444,110 +401,14 @@ class Note extends AbstractEntity {
|
||||
return !!this.targetRelations.find(rel => rel.name === 'template');
|
||||
}
|
||||
|
||||
/** @return {Note[]} */
|
||||
getSubtreeNotesIncludingTemplated() {
|
||||
const arr = [[this]];
|
||||
|
||||
for (const childNote of this.children) {
|
||||
arr.push(childNote.getSubtreeNotesIncludingTemplated());
|
||||
}
|
||||
|
||||
for (const targetRelation of this.targetRelations) {
|
||||
if (targetRelation.name === 'template') {
|
||||
const note = targetRelation.note;
|
||||
|
||||
if (note) {
|
||||
arr.push(note.getSubtreeNotesIncludingTemplated());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arr.flat();
|
||||
}
|
||||
|
||||
/** @return {Note[]} */
|
||||
getSubtreeNotes(includeArchived = true) {
|
||||
const noteSet = new Set();
|
||||
|
||||
function addSubtreeNotesInner(note) {
|
||||
if (!includeArchived && note.isArchived) {
|
||||
return;
|
||||
}
|
||||
|
||||
noteSet.add(note);
|
||||
|
||||
for (const childNote of note.children) {
|
||||
addSubtreeNotesInner(childNote);
|
||||
}
|
||||
}
|
||||
|
||||
addSubtreeNotesInner(this);
|
||||
|
||||
return Array.from(noteSet);
|
||||
}
|
||||
|
||||
/** @return {String[]} */
|
||||
getSubtreeNoteIds() {
|
||||
return this.getSubtreeNotes().map(note => note.noteId);
|
||||
}
|
||||
|
||||
getDescendantNoteIds() {
|
||||
return this.getSubtreeNoteIds();
|
||||
}
|
||||
|
||||
getAncestors() {
|
||||
if (!this.ancestorCache) {
|
||||
const noteIds = new Set();
|
||||
this.ancestorCache = [];
|
||||
|
||||
for (const parent of this.parents) {
|
||||
if (!noteIds.has(parent.noteId)) {
|
||||
this.ancestorCache.push(parent);
|
||||
noteIds.add(parent.noteId);
|
||||
}
|
||||
|
||||
for (const ancestorNote of parent.getAncestors()) {
|
||||
if (!noteIds.has(ancestorNote.noteId)) {
|
||||
this.ancestorCache.push(ancestorNote);
|
||||
noteIds.add(ancestorNote.noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.ancestorCache;
|
||||
}
|
||||
|
||||
getTargetRelations() {
|
||||
return this.targetRelations;
|
||||
}
|
||||
|
||||
/** @return {Note[]} - returns only notes which are templated, does not include their subtrees
|
||||
* in effect returns notes which are influenced by note's non-inheritable attributes */
|
||||
getTemplatedNotes() {
|
||||
const arr = [this];
|
||||
get shareId() {
|
||||
const sharedAlias = this.getOwnedLabelValue("shareAlias");
|
||||
|
||||
for (const targetRelation of this.targetRelations) {
|
||||
if (targetRelation.name === 'template') {
|
||||
const note = targetRelation.note;
|
||||
|
||||
if (note) {
|
||||
arr.push(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ancestorNoteId
|
||||
* @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
|
||||
*/
|
||||
isDescendantOfNote(ancestorNoteId) {
|
||||
const notePaths = this.getAllNotePaths();
|
||||
|
||||
return notePaths.some(path => path.includes(ancestorNoteId));
|
||||
return sharedAlias || this.noteId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ class Shaca {
|
||||
this.childParentToBranch = {};
|
||||
/** @type {Object.<String, Attribute>} */
|
||||
this.attributes = {};
|
||||
/** @type {Object.<String, String>} */
|
||||
this.aliasToNote = {};
|
||||
|
||||
this.loaded = false;
|
||||
}
|
||||
@@ -22,6 +24,10 @@ class Shaca {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
hasNote(noteId) {
|
||||
return noteId in this.notes;
|
||||
}
|
||||
|
||||
getNotes(noteIds, ignoreMissing = false) {
|
||||
const filteredNotes = [];
|
||||
|
||||
|
||||
@@ -34,31 +34,44 @@ function load() {
|
||||
|
||||
const noteIdStr = noteIds.map(noteId => `'${noteId}'`).join(",");
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, utcDateModified FROM notes WHERE isDeleted = 0 AND noteId IN (${noteIdStr})`)) {
|
||||
const rawNoteRows = sql.getRawRows(`
|
||||
SELECT noteId, title, type, mime, utcDateModified
|
||||
FROM notes
|
||||
WHERE isDeleted = 0
|
||||
AND noteId IN (${noteIdStr})`);
|
||||
|
||||
for (const row of rawNoteRows) {
|
||||
new Note(row);
|
||||
}
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0 AND noteId IN (${noteIdStr}) ORDER BY notePosition`)) {
|
||||
const rawBranchRows = sql.getRawRows(`
|
||||
SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified
|
||||
FROM branches
|
||||
WHERE isDeleted = 0
|
||||
AND parentNoteId IN (${noteIdStr})
|
||||
ORDER BY notePosition`);
|
||||
|
||||
for (const row of rawBranchRows) {
|
||||
new Branch(row);
|
||||
}
|
||||
|
||||
const attributes = sql.getRawRows(`
|
||||
const rawAttributeRows = sql.getRawRows(`
|
||||
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND noteId IN (${noteIdStr})
|
||||
AND (
|
||||
(type = 'label' AND name IN ('archived'))
|
||||
OR (type = 'relation' AND name IN ('imageLink', 'template'))
|
||||
(type = 'label' AND name IN ('archived', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss'))
|
||||
OR (type = 'relation' AND name IN ('imageLink', 'template', 'shareCss'))
|
||||
)`, []);
|
||||
|
||||
for (const row of attributes) {
|
||||
for (const row of rawAttributeRows) {
|
||||
new Attribute(row);
|
||||
}
|
||||
|
||||
shaca.loaded = true;
|
||||
|
||||
log.info(`Shaca load took ${Date.now() - start}ms`);
|
||||
log.info(`Shaca loaded ${rawNoteRows.length} notes, ${rawBranchRows.length} branches, ${rawAttributeRows.length} attributes took ${Date.now() - start}ms`);
|
||||
}
|
||||
|
||||
function ensureLoad() {
|
||||
|
||||
143
src/share/sql.js
143
src/share/sql.js
@@ -1,6 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const log = require('../services/log');
|
||||
const Database = require('better-sqlite3');
|
||||
const dataDir = require('../services/data_dir');
|
||||
|
||||
@@ -16,152 +15,20 @@ const dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true });
|
||||
});
|
||||
});
|
||||
|
||||
const statementCache = {};
|
||||
|
||||
function stmt(sql) {
|
||||
if (!(sql in statementCache)) {
|
||||
statementCache[sql] = dbConnection.prepare(sql);
|
||||
}
|
||||
|
||||
return statementCache[sql];
|
||||
function getRawRows(query, params = []) {
|
||||
return dbConnection.prepare(query).raw().all(params);
|
||||
}
|
||||
|
||||
function getRow(query, params = []) {
|
||||
return wrap(query, s => s.get(params));
|
||||
}
|
||||
|
||||
function getRowOrNull(query, params = []) {
|
||||
const all = getRows(query, params);
|
||||
|
||||
return all.length > 0 ? all[0] : null;
|
||||
}
|
||||
|
||||
function getValue(query, params = []) {
|
||||
const row = getRowOrNull(query, params);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return row[Object.keys(row)[0]];
|
||||
}
|
||||
|
||||
// smaller values can result in better performance due to better usage of statement cache
|
||||
const PARAM_LIMIT = 100;
|
||||
|
||||
function getManyRows(query, params) {
|
||||
let results = [];
|
||||
|
||||
while (params.length > 0) {
|
||||
const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
|
||||
params = params.slice(curParams.length);
|
||||
|
||||
const curParamsObj = {};
|
||||
|
||||
let j = 1;
|
||||
for (const param of curParams) {
|
||||
curParamsObj['param' + j++] = param;
|
||||
}
|
||||
|
||||
let i = 1;
|
||||
const questionMarks = curParams.map(() => ":param" + i++).join(",");
|
||||
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
||||
|
||||
const statement = curParams.length === PARAM_LIMIT
|
||||
? stmt(curQuery)
|
||||
: dbConnection.prepare(curQuery);
|
||||
|
||||
const subResults = statement.all(curParamsObj);
|
||||
results = results.concat(subResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function getRows(query, params = []) {
|
||||
return wrap(query, s => s.all(params));
|
||||
}
|
||||
|
||||
function getRawRows(query, params = []) {
|
||||
return wrap(query, s => s.raw().all(params));
|
||||
}
|
||||
|
||||
function iterateRows(query, params = []) {
|
||||
return stmt(query).iterate(params);
|
||||
}
|
||||
|
||||
function getMap(query, params = []) {
|
||||
const map = {};
|
||||
const results = getRows(query, params);
|
||||
|
||||
for (const row of results) {
|
||||
const keys = Object.keys(row);
|
||||
|
||||
map[row[keys[0]]] = row[keys[1]];
|
||||
}
|
||||
|
||||
return map;
|
||||
return dbConnection.prepare(query).get(params);
|
||||
}
|
||||
|
||||
function getColumn(query, params = []) {
|
||||
const list = [];
|
||||
const result = getRows(query, params);
|
||||
|
||||
if (result.length === 0) {
|
||||
return list;
|
||||
}
|
||||
|
||||
const key = Object.keys(result[0])[0];
|
||||
|
||||
for (const row of result) {
|
||||
list.push(row[key]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function wrap(query, func) {
|
||||
const startTimestamp = Date.now();
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = func(stmt(query));
|
||||
}
|
||||
catch (e) {
|
||||
if (e.message.includes("The database connection is not open")) {
|
||||
// this often happens on killing the app which puts these alerts in front of user
|
||||
// in these cases error should be simply ignored.
|
||||
console.log(e.message);
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
const milliseconds = Date.now() - startTimestamp;
|
||||
|
||||
if (milliseconds >= 20) {
|
||||
if (query.includes("WITH RECURSIVE")) {
|
||||
log.info(`Slow recursive query took ${milliseconds}ms.`);
|
||||
}
|
||||
else {
|
||||
log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return dbConnection.prepare(query).pluck().all(params);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dbConnection,
|
||||
getValue,
|
||||
getRow,
|
||||
getRowOrNull,
|
||||
getRows,
|
||||
getRawRows,
|
||||
iterateRows,
|
||||
getManyRows,
|
||||
getMap,
|
||||
getRow,
|
||||
getColumn
|
||||
};
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
<%- include('dialogs/delete_notes.ejs') %>
|
||||
|
||||
<script type="text/javascript">
|
||||
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
|
||||
window.baseApiUrl = 'api/';
|
||||
window.device = "desktop";
|
||||
window.glob = {
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
<%- include('dialogs/confirm.ejs') %>
|
||||
|
||||
<script type="text/javascript">
|
||||
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
|
||||
window.baseApiUrl = 'api/';
|
||||
window.device = "mobile";
|
||||
window.glob = {
|
||||
|
||||
@@ -189,6 +189,8 @@
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
|
||||
window.glob = {
|
||||
sourceId: ''
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="../favicon.ico">
|
||||
<link href="../libraries/normalize.min.css" rel="stylesheet">
|
||||
<link href="../stylesheets/share.css" rel="stylesheet">
|
||||
<% if (note.type === 'text' || note.type === 'book') { %>
|
||||
<link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
|
||||
<% } %>
|
||||
<title><%= note.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="layout">
|
||||
<button id="menuLink"></button>
|
||||
|
||||
<div id="menu">
|
||||
<%- include('share-tree-item', {note: subRoot, activeNote: note}) %>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<h1 id="title"><%= note.title %></h1>
|
||||
|
||||
<div id="content">
|
||||
<div class="ck-content"><%- content %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function (window, document) {
|
||||
|
||||
// we fetch the elements each time because docusaurus removes the previous
|
||||
// element references on page navigation
|
||||
function getElements() {
|
||||
return {
|
||||
layout: document.getElementById('layout'),
|
||||
menu: document.getElementById('menu'),
|
||||
menuLink: document.getElementById('menuLink')
|
||||
};
|
||||
}
|
||||
|
||||
function toggleClass(element, className) {
|
||||
var classes = element.className.split(/\s+/);
|
||||
var length = classes.length;
|
||||
var i = 0;
|
||||
|
||||
for (; i < length; i++) {
|
||||
if (classes[i] === className) {
|
||||
classes.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The className is not found
|
||||
if (length === classes.length) {
|
||||
classes.push(className);
|
||||
}
|
||||
|
||||
element.className = classes.join(' ');
|
||||
}
|
||||
|
||||
function toggleAll() {
|
||||
var active = 'active';
|
||||
var elements = getElements();
|
||||
|
||||
toggleClass(elements.layout, active);
|
||||
toggleClass(elements.menu, active);
|
||||
toggleClass(elements.menuLink, active);
|
||||
}
|
||||
|
||||
function handleEvent(e) {
|
||||
var elements = getElements();
|
||||
|
||||
if (e.target.id === elements.menuLink.id) {
|
||||
toggleAll();
|
||||
e.preventDefault();
|
||||
} else if (elements.menu.className.indexOf('active') !== -1) {
|
||||
toggleAll();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleEvent);
|
||||
|
||||
}(this, this.document));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
11
src/views/share/404.ejs
Normal file
11
src/views/share/404.ejs
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="../favicon.ico">
|
||||
<title>Not found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Not found</h1>
|
||||
</body>
|
||||
</html>
|
||||
46
src/views/share/page.ejs
Normal file
46
src/views/share/page.ejs
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="../favicon.ico">
|
||||
<% if (!note.hasLabel("shareOmitDefaultCss")) { %>
|
||||
<link href="../libraries/normalize.min.css" rel="stylesheet">
|
||||
<link href="../stylesheets/share.css" rel="stylesheet">
|
||||
<% } %>
|
||||
<% if (note.type === 'text' || note.type === 'book') { %>
|
||||
<link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
|
||||
<% } %>
|
||||
<% for (const cssRelation of note.getRelations("shareCss")) { %>
|
||||
<link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
|
||||
<% } %>
|
||||
<title><%= note.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="layout">
|
||||
<div id="main">
|
||||
<h1 id="title"><%= note.title %></h1>
|
||||
|
||||
<div id="content" class="note-<%= note.type %> <% if (note.type === 'text') { %>ck-content<% } %>">
|
||||
<%- content %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (subRoot.hasChildren()) { %>
|
||||
<button id="menuButton"></button>
|
||||
|
||||
<nav id="menu">
|
||||
<%- include('tree_item', {note: subRoot, activeNote: note}) %>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const menuButton = document.getElementById('menuButton');
|
||||
const layout = document.getElementById('layout');
|
||||
|
||||
menuButton.addEventListener('click', () => layout.classList.toggle('navMenu'));
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,16 +2,18 @@
|
||||
<% if (activeNote.noteId === note.noteId) { %>
|
||||
<strong><%= note.title %></strong>
|
||||
<% } else { %>
|
||||
<a href="./<%= note.noteId %>"><%= note.title %></a>
|
||||
<a href="./<%= note.shareId %>"><%= note.title %></a>
|
||||
<% } %>
|
||||
</p>
|
||||
|
||||
<% if (note.hasChildren()) { %>
|
||||
<ul>
|
||||
<% note.getChildNotes().forEach(function (childNote) { %>
|
||||
<% if (!childNote.hasLabel("shareHiddenFromTree")) { %>
|
||||
<li>
|
||||
<%- include('share-tree-item', {note: childNote}) %>
|
||||
<%- include('tree_item', {note: childNote}) %>
|
||||
</li>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } %>
|
||||
@@ -10,5 +10,6 @@ module.exports = {
|
||||
path: path.resolve(__dirname, 'src/public/app-dist'),
|
||||
filename: 'desktop.js'
|
||||
},
|
||||
devtool: 'source-map'
|
||||
};
|
||||
devtool: 'source-map',
|
||||
target: 'electron-renderer'
|
||||
};
|
||||
|
||||
@@ -10,5 +10,6 @@ module.exports = {
|
||||
path: path.resolve(__dirname, 'src/public/app-dist'),
|
||||
filename: 'mobile.js'
|
||||
},
|
||||
devtool: 'source-map'
|
||||
};
|
||||
devtool: 'source-map',
|
||||
target: 'electron-renderer'
|
||||
};
|
||||
|
||||
@@ -10,5 +10,6 @@ module.exports = {
|
||||
path: path.resolve(__dirname, 'src/public/app-dist'),
|
||||
filename: 'setup.js'
|
||||
},
|
||||
devtool: 'source-map'
|
||||
};
|
||||
devtool: 'source-map',
|
||||
target: 'electron-renderer'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user