mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 15:56:29 +01:00
Compare commits
65 Commits
algolia_v1
...
v0.49.2-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a85fe92aa | ||
|
|
feffd57f24 | ||
|
|
faf81ae056 | ||
|
|
003fec4b11 | ||
|
|
5ecb603e86 | ||
|
|
1fed71a92e | ||
|
|
dad82ea4e8 | ||
|
|
067251861d | ||
|
|
6bc8773d5f | ||
|
|
a910034c96 | ||
|
|
257cc66f62 | ||
|
|
00f24bdb63 | ||
|
|
fada3fe623 | ||
|
|
d97e454463 | ||
|
|
3128a7d62f | ||
|
|
b8fe9a41db | ||
|
|
8366a94bde | ||
|
|
f56123b864 | ||
|
|
ae951bfe23 | ||
|
|
ad8d35efe9 | ||
|
|
0217b1c85d | ||
|
|
c0aa14f586 | ||
|
|
b54cfab4ff | ||
|
|
a08985e7a6 | ||
|
|
a789025025 | ||
|
|
3f307b117e | ||
|
|
a232035d47 | ||
|
|
9d38e9342d | ||
|
|
4bc4b9ade7 | ||
|
|
f0217cae5e | ||
|
|
da050c6369 | ||
|
|
842c317568 | ||
|
|
47845930f4 | ||
|
|
972f2f40bf | ||
|
|
265401775b | ||
|
|
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
5734
libraries/codemirror/keymap/vim.js
vendored
Normal file
5734
libraries/codemirror/keymap/vim.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
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.2-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;
|
||||
}
|
||||
@@ -1111,7 +1114,7 @@ class Note extends AbstractEntity {
|
||||
|
||||
const branch = this.becca.getNote(parentNoteId).getParentBranches()[0];
|
||||
|
||||
return cloningService.cloneNoteToParent(this.noteId, branch.branchId);
|
||||
return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
|
||||
}
|
||||
|
||||
decrypt() {
|
||||
|
||||
@@ -48,7 +48,7 @@ async function cloneNotesTo(notePath) {
|
||||
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
|
||||
for (const cloneNoteId of clonedNoteIds) {
|
||||
await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val());
|
||||
await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, $clonePrefix.val());
|
||||
|
||||
const clonedNote = await froca.getNote(cloneNoteId);
|
||||
const targetNote = await froca.getBranch(targetBranchId).getNote();
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import mimeTypesService from "../../services/mime_types.js";
|
||||
import options from "../../services/options.js";
|
||||
import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = `
|
||||
<h4>Use vim keybindings in CodeNotes (no ex mode)</h4>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="vim-keymap-enabled">
|
||||
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label>
|
||||
</div>
|
||||
<h4>Available MIME types in the dropdown</h4>
|
||||
|
||||
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>`;
|
||||
@@ -10,12 +18,18 @@ export default class CodeNotesOptions {
|
||||
constructor() {
|
||||
$("#options-code-notes").html(TPL);
|
||||
|
||||
this.$vimKeymapEnabled = $("#vim-keymap-enabled");
|
||||
this.$vimKeymapEnabled.on('change', () => {
|
||||
const opts = { 'vimKeymapEnabled': this.$vimKeymapEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
return false;
|
||||
});
|
||||
this.$mimeTypes = $("#options-mime-types");
|
||||
}
|
||||
|
||||
async optionsLoaded() {
|
||||
async optionsLoaded(options) {
|
||||
this.$mimeTypes.empty();
|
||||
|
||||
this.$vimKeymapEnabled.prop("checked", options['vimKeymapEnabled'] === 'true');
|
||||
let idCtr = 1;
|
||||
|
||||
for (const mimeType of await mimeTypesService.getMimeTypes()) {
|
||||
@@ -45,4 +59,4 @@ export default class CodeNotesOptions {
|
||||
|
||||
mimeTypesService.loadMimeTypes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,8 +196,18 @@ ws.subscribeToMessages(async message => {
|
||||
}
|
||||
});
|
||||
|
||||
async function cloneNoteTo(childNoteId, parentBranchId, prefix) {
|
||||
const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, {
|
||||
async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) {
|
||||
const resp = await server.put(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function cloneNoteToNote(childNoteId, parentNoteId, prefix) {
|
||||
const resp = await server.put(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
@@ -222,5 +232,6 @@ export default {
|
||||
deleteNotes,
|
||||
moveNodeUpInHierarchy,
|
||||
cloneNoteAfter,
|
||||
cloneNoteTo
|
||||
cloneNoteToBranch,
|
||||
cloneNoteToNote,
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ async function pasteInto(parentBranchId) {
|
||||
for (const clipboardBranch of clipboardBranches) {
|
||||
const clipboardNote = await clipboardBranch.getNote();
|
||||
|
||||
await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId);
|
||||
await branchService.cloneNoteToBranch(clipboardNote.noteId, parentBranchId);
|
||||
}
|
||||
|
||||
// copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,7 @@ const CODE_MIRROR = {
|
||||
"libraries/codemirror/addon/edit/matchtags.js",
|
||||
"libraries/codemirror/addon/search/match-highlighter.js",
|
||||
"libraries/codemirror/mode/meta.js",
|
||||
"libraries/codemirror/keymap/vim.js",
|
||||
"libraries/codemirror/addon/lint/lint.js",
|
||||
"libraries/codemirror/addon/lint/eslint.js"
|
||||
],
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
21
src/public/app/share.js
Normal file
21
src/public/app/share.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Fetch note with given ID from backend
|
||||
*
|
||||
* @param noteId of the given note to be fetched. If falsy, fetches current note.
|
||||
*/
|
||||
async function fetchNote(noteId = null) {
|
||||
if (!noteId) {
|
||||
noteId = document.body.getAttribute("data-note-id");
|
||||
}
|
||||
|
||||
const resp = await fetch(`api/notes/${noteId}`);
|
||||
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggleMenuButton = document.getElementById('toggleMenuButton');
|
||||
const layout = document.getElementById('layout');
|
||||
|
||||
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
|
||||
}, false);
|
||||
@@ -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,10 @@ 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.",
|
||||
"shareJs": "JavaScript note which will be injected into the share page. JS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
|
||||
"shareFavicon": "Favicon note to be set in the shared page. Typically you want to set it to share root and make it inheritable. Favicon note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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.cloneNoteToNote(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() {}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import ws from "../../services/ws.js";
|
||||
import appContext from "../../services/app_context.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import options from "../../services/options.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-code note-detail-printable">
|
||||
@@ -14,20 +15,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 +28,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>
|
||||
@@ -97,6 +95,7 @@ export default class EditableCodeTypeWidget extends TypeWidget {
|
||||
viewportMargin: Infinity,
|
||||
indentUnit: 4,
|
||||
matchBrackets: true,
|
||||
keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
|
||||
matchTags: {bothTags: true},
|
||||
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
|
||||
lint: true,
|
||||
|
||||
@@ -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;
|
||||
@@ -28,18 +32,21 @@
|
||||
#main {
|
||||
flex-basis: 0;
|
||||
flex-grow: 3;
|
||||
overflow: auto;
|
||||
padding: 10px 20px 20px 20px;
|
||||
}
|
||||
|
||||
#parentLink {
|
||||
float: right;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#title {
|
||||
margin: 0;
|
||||
padding: 10px 20px 0 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.type-image img {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -48,40 +55,110 @@ pre {
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
#menuLink {
|
||||
iframe.pdf-view {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
}
|
||||
|
||||
#toggleMenuButton {
|
||||
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;
|
||||
}
|
||||
|
||||
#childLinks.grid ul {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#childLinks.grid ul li {
|
||||
width: 180px;
|
||||
height: 140px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#childLinks.grid ul li a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
#childLinks.grid ul li a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
#childLinks.list ul {
|
||||
list-style-type: none;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#childLinks.list ul li {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#noteClippedFrom {
|
||||
padding: 10px 0 10px 0;
|
||||
margin: 20px 0 20px 0;
|
||||
color: #666;
|
||||
border: 1px solid #ddd;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
#toggleMenuButton::after {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
#layout.active #menu {
|
||||
#layout.showMenu #menu {
|
||||
display: block;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
#toggleMenuButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#layout.active #main {
|
||||
#layout.showMenu #main {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#layout.active #menuLink::after {
|
||||
#title {
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
#layout.showMenu #toggleMenuButton::after {
|
||||
content: "«";
|
||||
}
|
||||
|
||||
#toggleMenuButton::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 {
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
const cloningService = require('../../services/cloning');
|
||||
|
||||
function cloneNoteToParent(req) {
|
||||
function cloneNoteToBranch(req) {
|
||||
const {noteId, parentBranchId} = req.params;
|
||||
const {prefix} = req.body;
|
||||
|
||||
return cloningService.cloneNoteToParent(noteId, parentBranchId, prefix);
|
||||
return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix);
|
||||
}
|
||||
|
||||
function cloneNoteToNote(req) {
|
||||
const {noteId, parentNoteId} = req.params;
|
||||
const {prefix} = req.body;
|
||||
|
||||
return cloningService.cloneNoteToNote(noteId, parentNoteId, prefix);
|
||||
}
|
||||
|
||||
function cloneNoteAfter(req) {
|
||||
@@ -16,6 +23,7 @@ function cloneNoteAfter(req) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cloneNoteToParent,
|
||||
cloneNoteToBranch,
|
||||
cloneNoteToNote,
|
||||
cloneNoteAfter
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'similarNotesWidget',
|
||||
'editedNotesWidget',
|
||||
'calendarWidget',
|
||||
'vimKeymapEnabled',
|
||||
'codeNotesMimeTypes',
|
||||
'spellCheckEnabled',
|
||||
'spellCheckLanguageCode',
|
||||
|
||||
@@ -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);
|
||||
@@ -228,7 +229,8 @@ function register(app) {
|
||||
|
||||
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
|
||||
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentBranchId', cloningApiRoute.cloneNoteToParent);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
|
||||
|
||||
route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||
|
||||
@@ -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,10 @@ 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 },
|
||||
{ type: 'relation', name: 'shareJs', isDangerous: false },
|
||||
{ type: 'relation', name: 'shareFavicon', isDangerous: false },
|
||||
];
|
||||
|
||||
/** @returns {Note[]} */
|
||||
@@ -95,6 +101,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 +210,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:"2022-01-02T22:43:30+01:00", buildRevision: "feffd57f240438d107c1ed1c1772545611a97dee" };
|
||||
|
||||
@@ -10,14 +10,18 @@ const utils = require('./utils');
|
||||
const becca = require("../becca/becca");
|
||||
const beccaService = require("../becca/becca_service");
|
||||
|
||||
function cloneNoteToParent(noteId, parentBranchId, prefix) {
|
||||
const parentBranch = becca.getBranch(parentBranchId);
|
||||
function cloneNoteToNote(noteId, parentNoteId, prefix) {
|
||||
if (parentNoteId === 'share') {
|
||||
const specialNotesService = require('./special_notes');
|
||||
// share root note is created lazily
|
||||
specialNotesService.getShareRoot();
|
||||
}
|
||||
|
||||
if (isNoteDeleted(noteId) || isNoteDeleted(parentBranch.noteId)) {
|
||||
if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
|
||||
return { success: false, message: 'Note is deleted.' };
|
||||
}
|
||||
|
||||
const validationResult = treeService.validateParentChild(parentBranch.noteId, noteId);
|
||||
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return validationResult;
|
||||
@@ -25,21 +29,33 @@ function cloneNoteToParent(noteId, parentBranchId, prefix) {
|
||||
|
||||
const branch = new Branch({
|
||||
noteId: noteId,
|
||||
parentNoteId: parentBranch.noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
prefix: prefix,
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
|
||||
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
|
||||
parentBranch.save();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
branchId: branch.branchId,
|
||||
notePath: beccaService.getNotePath(parentBranch.noteId).path + "/" + noteId
|
||||
notePath: beccaService.getNotePath(parentNoteId).path + "/" + noteId
|
||||
};
|
||||
}
|
||||
|
||||
function cloneNoteToBranch(noteId, parentBranchId, prefix) {
|
||||
const parentBranch = becca.getBranch(parentBranchId);
|
||||
|
||||
if (!parentBranch) {
|
||||
return { success: false, message: `Parent branch ${parentBranchId} does not exist.` };
|
||||
}
|
||||
|
||||
const ret = cloneNoteToNote(noteId, parentBranch.noteId, prefix);
|
||||
|
||||
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
|
||||
parentBranch.save();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
|
||||
if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
|
||||
return { success: false, message: 'Note is deleted.' };
|
||||
@@ -115,7 +131,8 @@ function isNoteDeleted(noteId) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cloneNoteToParent,
|
||||
cloneNoteToBranch,
|
||||
cloneNoteToNote,
|
||||
ensureNoteIsPresentInParent,
|
||||
ensureNoteIsAbsentFromParent,
|
||||
toggleNoteInParent,
|
||||
|
||||
@@ -258,7 +258,8 @@ class ConsistencyChecks {
|
||||
FROM branches
|
||||
WHERE noteId = ?
|
||||
and parentNoteId = ?
|
||||
and isDeleted = 0`, [noteId, parentNoteId]);
|
||||
and isDeleted = 0
|
||||
ORDER BY utcDateModified`, [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();
|
||||
|
||||
@@ -54,7 +54,7 @@ function getYearNote(dateStr, rootNote) {
|
||||
rootNote = getRootCalendarNote();
|
||||
}
|
||||
|
||||
const yearStr = dateStr.substr(0, 4);
|
||||
const yearStr = dateStr.trim().substr(0, 4);
|
||||
|
||||
let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr);
|
||||
|
||||
@@ -138,6 +138,8 @@ function getDateNoteTitle(rootNote, dayNumber, dateObj) {
|
||||
|
||||
/** @returns {Note} */
|
||||
function getDateNote(dateStr) {
|
||||
dateStr = dateStr.trim().substr(0, 10);
|
||||
|
||||
let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
|
||||
|
||||
if (dateNote) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,10 @@ const sanitizeHtml = require('sanitize-html');
|
||||
// intended mainly as protection against XSS via import
|
||||
// secondarily it (partly) protects against "CSS takeover"
|
||||
function sanitize(dirtyHtml) {
|
||||
if (!dirtyHtml) {
|
||||
return dirtyHtml;
|
||||
}
|
||||
|
||||
// avoid H1 per https://github.com/zadam/trilium/issues/1552
|
||||
// demote H1, and if that conflicts with existing H2, demote that, etc
|
||||
const transformTags = {};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -51,7 +51,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
|
||||
throw new Error("Unrecognized OPML version " + opmlVersion);
|
||||
}
|
||||
|
||||
content = htmlSanitizer.sanitize(content);
|
||||
content = htmlSanitizer.sanitize(content || "");
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
parentNoteId,
|
||||
|
||||
@@ -240,13 +240,15 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
}
|
||||
|
||||
if (noteMeta && noteMeta.isClone) {
|
||||
new Branch({
|
||||
noteId,
|
||||
parentNoteId,
|
||||
isExpanded: noteMeta.isExpanded,
|
||||
prefix: noteMeta.prefix,
|
||||
notePosition: noteMeta.notePosition
|
||||
}).save();
|
||||
if (!becca.getBranchFromChildAndParent(noteId, parentNoteId)) {
|
||||
new Branch({
|
||||
noteId,
|
||||
parentNoteId,
|
||||
isExpanded: noteMeta.isExpanded,
|
||||
prefix: noteMeta.prefix,
|
||||
notePosition: noteMeta.notePosition
|
||||
}).save();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -351,8 +353,30 @@ 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);
|
||||
|
||||
if (!becca.getBranchFromChildAndParent(noteId, parentNoteId)) {
|
||||
new Branch({
|
||||
noteId,
|
||||
parentNoteId,
|
||||
isExpanded: noteMeta.isExpanded,
|
||||
prefix: noteMeta.prefix,
|
||||
notePosition: noteMeta.notePosition
|
||||
}).save();
|
||||
}
|
||||
}
|
||||
else {
|
||||
({note} = noteService.createNewNote({
|
||||
@@ -367,7 +391,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),
|
||||
@@ -359,7 +360,7 @@ function downloadImages(noteId, content) {
|
||||
// which upon the download of all the images will update the note if the links have not been fixed before
|
||||
|
||||
sql.transactional(() => {
|
||||
const imageNotes = becca.getNotes(Object.values(imageUrlToNoteIdMapping));
|
||||
const imageNotes = becca.getNotes(Object.values(imageUrlToNoteIdMapping), true);
|
||||
|
||||
const origNote = becca.getNote(noteId);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
const becca = require('../becca/becca');
|
||||
const sql = require("./sql.js");
|
||||
|
||||
function getOption(name) {
|
||||
const option = require('../becca/becca').getOption(name);
|
||||
let option;
|
||||
|
||||
if (becca.loaded) {
|
||||
option = becca.getOption(name);
|
||||
}
|
||||
else {
|
||||
// e.g. in initial sync becca is not loaded because DB is not initialized
|
||||
option = sql.getRow("SELECT * FROM options WHERE name = ?", name);
|
||||
}
|
||||
|
||||
if (!option) {
|
||||
throw new Error(`Option "${name}" doesn't exist`);
|
||||
@@ -39,12 +48,12 @@ function getOptionBool(name) {
|
||||
}
|
||||
|
||||
function setOption(name, value) {
|
||||
const option = becca.getOption(name);
|
||||
|
||||
if (value === true || value === false) {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
const option = becca.getOption(name);
|
||||
|
||||
if (option) {
|
||||
option.value = value;
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ const defaultOptions = [
|
||||
{ name: 'imageMaxWidthHeight', value: '2000', isSynced: true },
|
||||
{ name: 'imageJpegQuality', value: '75', isSynced: true },
|
||||
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
|
||||
{ name: 'vimKeymapEnabled', value: 'false', isSynced: false },
|
||||
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml"]', isSynced: true },
|
||||
{ name: 'leftPaneWidth', value: '25', isSynced: false },
|
||||
{ name: 'leftPaneVisible', value: 'true', isSynced: false },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -371,7 +371,10 @@ function getLastSyncedPull() {
|
||||
|
||||
function setLastSyncedPull(entityChangeId) {
|
||||
const lastSyncedPullOption = becca.getOption('lastSyncedPull');
|
||||
lastSyncedPullOption.value = entityChangeId + '';
|
||||
|
||||
if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded
|
||||
lastSyncedPullOption.value = entityChangeId + '';
|
||||
}
|
||||
|
||||
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
|
||||
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPull']);
|
||||
@@ -389,7 +392,10 @@ function setLastSyncedPush(entityChangeId) {
|
||||
ws.setLastSyncedPush(entityChangeId);
|
||||
|
||||
const lastSyncedPushOption = becca.getOption('lastSyncedPush');
|
||||
lastSyncedPushOption.value = entityChangeId + '';
|
||||
|
||||
if (lastSyncedPushOption) { // might be null in initial sync when becca is not loaded
|
||||
lastSyncedPushOption.value = entityChangeId + '';
|
||||
}
|
||||
|
||||
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
|
||||
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPush']);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -16,7 +16,10 @@ let mainWindow;
|
||||
let setupWindow;
|
||||
|
||||
async function createExtraWindow(notePath, hoistedNoteId = 'root') {
|
||||
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
|
||||
|
||||
const {BrowserWindow} = require('electron');
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 800,
|
||||
@@ -25,7 +28,7 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
|
||||
enableRemoteModule: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: optionService.getOptionBool('spellCheckEnabled')
|
||||
spellcheck: spellcheckEnabled
|
||||
},
|
||||
frame: optionService.getOptionBool('nativeTitleBarVisible'),
|
||||
icon: getIcon()
|
||||
@@ -33,6 +36,8 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
win.loadURL('http://127.0.0.1:' + await port + '/?extra=1&extraHoistedNoteId=' + hoistedNoteId + '#' + notePath);
|
||||
|
||||
configureWebContents(win.webContents, spellcheckEnabled);
|
||||
}
|
||||
|
||||
ipcMain.on('create-extra-window', (event, arg) => {
|
||||
@@ -59,6 +64,7 @@ async function createMainWindow() {
|
||||
height: mainWindowState.height,
|
||||
title: 'Trilium Notes',
|
||||
webPreferences: {
|
||||
enableRemoteModule: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: spellcheckEnabled
|
||||
@@ -73,8 +79,10 @@ async function createMainWindow() {
|
||||
mainWindow.loadURL('http://127.0.0.1:' + await port);
|
||||
mainWindow.on('closed', () => mainWindow = null);
|
||||
|
||||
const {webContents} = mainWindow;
|
||||
configureWebContents(mainWindow.webContents, spellcheckEnabled);
|
||||
}
|
||||
|
||||
function configureWebContents(webContents, spellcheckEnabled) {
|
||||
require("@electron/remote/main").enable(webContents);
|
||||
|
||||
webContents.on('new-window', (e, url) => {
|
||||
|
||||
105
src/share/content_renderer.js
Normal file
105
src/share/content_renderer.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const {JSDOM} = require("jsdom");
|
||||
const shaca = require("./shaca/shaca");
|
||||
|
||||
function getContent(note) {
|
||||
let content = note.getContent();
|
||||
let header = '';
|
||||
let isEmpty = false;
|
||||
|
||||
if (note.type === 'text') {
|
||||
const document = new JSDOM(content || "").window.document;
|
||||
|
||||
isEmpty = document.body.textContent.trim().length === 0
|
||||
&& document.querySelectorAll("img").length === 0;
|
||||
|
||||
if (!isEmpty) {
|
||||
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);
|
||||
linkEl.classList.add("type-" + linkedNote.type);
|
||||
}
|
||||
else {
|
||||
linkEl.removeAttribute("href");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content = document.body.innerHTML;
|
||||
|
||||
if (content.includes(`<span class="math-tex">`)) {
|
||||
header += `
|
||||
<script src="../../libraries/katex/katex.min.js"></script>
|
||||
<link rel="stylesheet" href="../../libraries/katex/katex.min.css">
|
||||
<script src="../../libraries/katex/auto-render.min.js"></script>
|
||||
<script src="../../libraries/katex/mhchem.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
renderMathInElement(document.getElementById('content'));
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (note.type === 'code') {
|
||||
if (!content?.trim()) {
|
||||
isEmpty = true;
|
||||
}
|
||||
else {
|
||||
const document = new JSDOM().window.document;
|
||||
|
||||
const preEl = document.createElement('pre');
|
||||
preEl.appendChild(document.createTextNode(content));
|
||||
|
||||
content = preEl.outerHTML;
|
||||
}
|
||||
}
|
||||
else if (note.type === 'mermaid') {
|
||||
content = `
|
||||
<div class="mermaid">${content}</div>
|
||||
<hr>
|
||||
<details>
|
||||
<summary>Chart source</summary>
|
||||
<pre>${content}</pre>
|
||||
</details>`
|
||||
header += `<script src="../../libraries/mermaid.min.js"></script>`;
|
||||
}
|
||||
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') {
|
||||
isEmpty = true;
|
||||
}
|
||||
else {
|
||||
content = '<p>This note type cannot be displayed.</p>';
|
||||
}
|
||||
|
||||
return {
|
||||
header,
|
||||
content,
|
||||
isEmpty
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getContent
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,147 +1,68 @@
|
||||
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 {header, content, isEmpty} = contentRenderer.getContent(note);
|
||||
|
||||
const subRoot = getSubRoot(note);
|
||||
const subRoot = getSharedSubTreeRoot(note);
|
||||
|
||||
res.render("share", {
|
||||
res.render("share/page", {
|
||||
note,
|
||||
header,
|
||||
content,
|
||||
isEmpty,
|
||||
subRoot
|
||||
});
|
||||
}
|
||||
else {
|
||||
res.send("FFF");
|
||||
res.status(404).render("share/404");
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
|
||||
const image = shaca.getNote(req.params.noteId);
|
||||
|
||||
if (!image) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
else if (image.type !== 'image') {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
res.set('Content-Type', image.mime);
|
||||
|
||||
res.send(image.getContent());
|
||||
});
|
||||
|
||||
router.get('/share/api/notes/:noteId/:download', (req, res, next) => {
|
||||
router.get('/share/api/notes/:noteId', (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(`Note ${noteId} not found`);
|
||||
}
|
||||
|
||||
res.json(note.getPojoWithAttributes());
|
||||
});
|
||||
|
||||
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} not found`);
|
||||
}
|
||||
|
||||
const utils = require("../services/utils");
|
||||
@@ -155,6 +76,36 @@ function register(router) {
|
||||
|
||||
res.send(note.getContent());
|
||||
});
|
||||
|
||||
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
|
||||
const image = shaca.getNote(req.params.noteId);
|
||||
|
||||
if (!image) {
|
||||
return res.status(404).send(`Note ${noteId} not found`);
|
||||
}
|
||||
else if (image.type !== 'image') {
|
||||
return res.status(400).send("Requested note is not an image");
|
||||
}
|
||||
|
||||
res.set('Content-Type', image.mime);
|
||||
|
||||
res.send(image.getContent());
|
||||
});
|
||||
|
||||
// used for PDF viewing
|
||||
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(`Note ${noteId} not found`);
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -85,6 +89,18 @@ class Attribute extends AbstractEntity {
|
||||
|
||||
return this.shaca.getNote(this.value);
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
attributeId: this.attributeId,
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
name: this.name,
|
||||
position: this.position,
|
||||
value: this.value,
|
||||
isInheritable: this.isInheritable
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Attribute;
|
||||
|
||||
@@ -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,27 @@ 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;
|
||||
return sharedAlias || this.noteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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));
|
||||
getPojoWithAttributes() {
|
||||
return {
|
||||
noteId: this.noteId,
|
||||
title: this.title,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
utcDateModified: this.utcDateModified,
|
||||
attributes: this.getAttributes().map(attr => attr.getPojo()),
|
||||
parentNoteIds: this.parents.map(parentNote => parentNote.noteId),
|
||||
childNoteIds: this.children.map(child => child.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,40 @@ 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'))
|
||||
)`, []);
|
||||
AND noteId IN (${noteIdStr})`);
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ async function start() {
|
||||
const parentNoteId = getRandomNoteId();
|
||||
const prefix = Math.random() > 0.8 ? "prefix" : null;
|
||||
|
||||
const result = await cloningService.cloneNoteToParent(noteIdToClone, parentNoteId, prefix);
|
||||
const result = await cloningService.cloneNoteToBranch(noteIdToClone, parentNoteId, prefix);
|
||||
|
||||
console.log(`Cloning ${i}:`, result.success ? "succeeded" : "FAILED");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
80
src/views/share/page.ejs
Normal file
80
src/views/share/page.ejs
Normal file
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<% if (note.hasRelation("shareFavicon")) { %>
|
||||
<link rel="shortcut icon" href="api/notes/<%= note.getRelation("shareFavicon").value %>/download">
|
||||
<% } else { %>
|
||||
<link rel="shortcut icon" href="../favicon.ico">
|
||||
<% } %>
|
||||
<script src="../app/share.js"></script>
|
||||
<% 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">
|
||||
<% } %>
|
||||
<% for (const jsRelation of note.getRelations("shareJs")) { %>
|
||||
<script type="module" src="api/notes/<%= jsRelation.value %>/download"></script>
|
||||
<% } %>
|
||||
<%- header %>
|
||||
<title><%= note.title %></title>
|
||||
</head>
|
||||
<body data-note-id="<%= note.noteId %>">
|
||||
<div id="layout">
|
||||
<div id="main">
|
||||
<% if (note.parents[0].noteId !== 'share' && note.parents.length !== 0) { %>
|
||||
<nav id="parentLink">
|
||||
parent: <a href="<%= note.parents[0].noteId %>"
|
||||
class="type-<%= note.parents[0].type %>"><%= note.parents[0].title %></a>
|
||||
</nav>
|
||||
<% } %>
|
||||
|
||||
<h1 id="title"><%= note.title %></h1>
|
||||
|
||||
<% if (note.hasLabel("pageUrl")) { %>
|
||||
<div id="noteClippedFrom">This note was originally clipped from <a href="<%= note.getLabelValue("pageUrl") %>"><%= note.getLabelValue("pageUrl") %></a></div>
|
||||
<% } %>
|
||||
|
||||
<% if (note.type === 'book') { %>
|
||||
<% } else if (isEmpty) { %>
|
||||
<p>This note has no content.</p>
|
||||
<% } else { %>
|
||||
<div id="content" class="type-<%= note.type %><% if (note.type === 'text') { %> ck-content<% } %>">
|
||||
<%- content %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (note.hasChildren()) { %>
|
||||
<nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>">
|
||||
<% if (!isEmpty) { %>
|
||||
<hr>
|
||||
<span>Child notes: </span>
|
||||
<% } %>
|
||||
|
||||
<ul>
|
||||
<% for (const childNote of note.getChildNotes()) { %>
|
||||
<li>
|
||||
<a href="<%= childNote.shareId %>"
|
||||
class="type-<%= childNote.type %>"><%= childNote.title %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (subRoot.hasChildren()) { %>
|
||||
<button id="toggleMenuButton"></button>
|
||||
|
||||
<nav id="menu">
|
||||
<%- include('tree_item', {note: subRoot, activeNote: note}) %>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,16 +2,18 @@
|
||||
<% if (activeNote.noteId === note.noteId) { %>
|
||||
<strong><%= note.title %></strong>
|
||||
<% } else { %>
|
||||
<a href="./<%= note.noteId %>"><%= note.title %></a>
|
||||
<a class="type-<%= note.type %>" 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