mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
bookmarks use cloning
This commit is contained in:
parent
cd60ad4267
commit
27ce273d29
894
package-lock.json
generated
894
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -119,6 +119,19 @@ class Branch extends AbstractEntity {
|
|||||||
return !(this.branchId in this.becca.branches);
|
return !(this.branchId in this.becca.branches);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Branch is weak when its existence should not hinder deletion of its note.
|
||||||
|
* As a result, note with only weak branches should be immediately deleted.
|
||||||
|
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
|
||||||
|
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
|
||||||
|
* of deletion should not act as a clone.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
get isWeak() {
|
||||||
|
return ['share', 'lb_bookmarks'].includes(this.parentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a branch. If this is a last note's branch, delete the note as well.
|
* Delete a branch. If this is a last note's branch, delete the note as well.
|
||||||
*
|
*
|
||||||
@ -159,9 +172,13 @@ class Branch extends AbstractEntity {
|
|||||||
|
|
||||||
this.markAsDeleted(deleteId);
|
this.markAsDeleted(deleteId);
|
||||||
|
|
||||||
const notDeletedBranches = note.getParentBranches();
|
const notDeletedBranches = note.getStrongParentBranches();
|
||||||
|
|
||||||
if (notDeletedBranches.length === 0) {
|
if (notDeletedBranches.length === 0) {
|
||||||
|
for (const weakBranch of note.getParentBranches()) {
|
||||||
|
weakBranch.markAsDeleted(deleteId);
|
||||||
|
}
|
||||||
|
|
||||||
for (const childBranch of note.getChildBranches()) {
|
for (const childBranch of note.getChildBranches()) {
|
||||||
childBranch.deleteBranch(deleteId, taskContext);
|
childBranch.deleteBranch(deleteId, taskContext);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ const NoteRevision = require("./note_revision");
|
|||||||
const TaskContext = require("../../services/task_context");
|
const TaskContext = require("../../services/task_context");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require('dayjs/plugin/utc');
|
const utc = require('dayjs/plugin/utc');
|
||||||
const searchService = require("../../services/search/services/search.js");
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const LABEL = 'label';
|
const LABEL = 'label';
|
||||||
@ -155,6 +154,15 @@ class Note extends AbstractEntity {
|
|||||||
return this.parentBranches;
|
return this.parentBranches;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <i>strong</i> (as opposed to <i>weak</i>) parent branches. See isWeak for details.
|
||||||
|
*
|
||||||
|
* @returns {Branch[]}
|
||||||
|
*/
|
||||||
|
getStrongParentBranches() {
|
||||||
|
return this.getParentBranches().filter(branch => !branch.isWeak);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Branch[]}
|
* @returns {Branch[]}
|
||||||
* @deprecated use getParentBranches() instead
|
* @deprecated use getParentBranches() instead
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import FlexContainer from "./containers/flex_container.js";
|
import FlexContainer from "./containers/flex_container.js";
|
||||||
import searchService from "../services/search.js";
|
|
||||||
import OpenNoteButtonWidget from "./buttons/open_note_button_widget.js";
|
import OpenNoteButtonWidget from "./buttons/open_note_button_widget.js";
|
||||||
import BookmarkFolderWidget from "./buttons/bookmark_folder.js";
|
import BookmarkFolderWidget from "./buttons/bookmark_folder.js";
|
||||||
|
import froca from "../services/froca.js";
|
||||||
|
|
||||||
export default class BookmarkButtons extends FlexContainer {
|
export default class BookmarkButtons extends FlexContainer {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -11,13 +11,13 @@ export default class BookmarkButtons extends FlexContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
const bookmarkedNotes = await searchService.searchForNotes("#bookmarked or #bookmarkFolder");
|
|
||||||
|
|
||||||
this.$widget.empty();
|
this.$widget.empty();
|
||||||
this.children = [];
|
this.children = [];
|
||||||
this.noteIds = [];
|
this.noteIds = [];
|
||||||
|
|
||||||
for (const note of bookmarkedNotes) {
|
const bookmarkParentNote = await froca.getNote('lb_bookmarks');
|
||||||
|
|
||||||
|
for (const note of await bookmarkParentNote.getChildNotes()) {
|
||||||
this.noteIds.push(note.noteId);
|
this.noteIds.push(note.noteId);
|
||||||
|
|
||||||
const buttonWidget = note.hasLabel("bookmarkFolder")
|
const buttonWidget = note.hasLabel("bookmarkFolder")
|
||||||
@ -37,11 +37,7 @@ export default class BookmarkButtons extends FlexContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entitiesReloadedEvent({loadResults}) {
|
entitiesReloadedEvent({loadResults}) {
|
||||||
if (loadResults.getAttributes().find(attr => attr.type === 'label' && ['bookmarked', 'bookmarkFolder'].includes(attr.name))) {
|
if (loadResults.getBranches().find(branch => branch.parentNoteId === 'lb_bookmarks')) {
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadResults.getNoteIds().find(noteId => this.noteIds.includes(noteId))) {
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import attributeService from "../services/attributes.js";
|
|
||||||
import SwitchWidget from "./switch.js";
|
import SwitchWidget from "./switch.js";
|
||||||
|
import server from "../services/server.js";
|
||||||
|
import toastService from "../services/toast.js";
|
||||||
|
|
||||||
export default class BookmarkSwitchWidget extends SwitchWidget {
|
export default class BookmarkSwitchWidget extends SwitchWidget {
|
||||||
|
isEnabled() {
|
||||||
|
return super.isEnabled()
|
||||||
|
// it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle
|
||||||
|
&& !['root', 'hidden'].includes(this.noteId);
|
||||||
|
}
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
super.doRender();
|
super.doRender();
|
||||||
|
|
||||||
@ -12,32 +19,24 @@ export default class BookmarkSwitchWidget extends SwitchWidget {
|
|||||||
this.$switchOffButton.attr("title", "Remove bookmark");
|
this.$switchOffButton.attr("title", "Remove bookmark");
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchOff() {
|
async toggle(state) {
|
||||||
for (const label of this.note.getLabels('bookmarked')) {
|
const resp = await server.put(`notes/${this.noteId}/toggle-in-parent/lb_bookmarks/` + !!state);
|
||||||
await attributeService.removeAttributeById(this.noteId, label.attributeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switchOn() {
|
if (!resp.success) {
|
||||||
return attributeService.setLabel(this.noteId, 'bookmarked');
|
toastService.showError(resp.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshWithNote(note) {
|
refreshWithNote(note) {
|
||||||
const isBookmarked = note.hasLabel('bookmarked');
|
const isBookmarked = !!note.getParentBranches().find(b => b.parentNoteId === 'lb_bookmarks');
|
||||||
|
|
||||||
this.$switchOn.toggle(!isBookmarked);
|
this.$switchOn.toggle(!isBookmarked);
|
||||||
this.$switchOff.toggle(isBookmarked);
|
this.$switchOff.toggle(isBookmarked);
|
||||||
}
|
}
|
||||||
|
|
||||||
entitiesReloadedEvent({loadResults}) {
|
entitiesReloadedEvent({loadResults}) {
|
||||||
for (const attr of loadResults.getAttributes()) {
|
if (loadResults.getBranches().find(b => b.noteId === this.noteId)) {
|
||||||
if (attr.type === 'label'
|
|
||||||
&& attr.name === 'bookmarked'
|
|
||||||
&& attributeService.isAffecting(attr, this.note)) {
|
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -740,12 +740,14 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
extraClasses.push("shared");
|
extraClasses.push("shared");
|
||||||
}
|
}
|
||||||
else if (note.getParentNoteIds().length > 1) {
|
else if (note.getParentNoteIds().length > 1) {
|
||||||
const notSearchParents = note.getParentNoteIds()
|
const realClones = note.getParentNoteIds()
|
||||||
.map(noteId => froca.notes[noteId])
|
.map(noteId => froca.notes[noteId])
|
||||||
.filter(note => !!note)
|
.filter(note => !!note)
|
||||||
.filter(note => note.type !== 'search');
|
.filter(note =>
|
||||||
|
!['share', 'lb_bookmarks'].includes(note.noteId)
|
||||||
|
&& note.type !== 'search');
|
||||||
|
|
||||||
if (notSearchParents.length > 1) {
|
if (realClones.length > 1) {
|
||||||
extraClasses.push("multiple-parents");
|
extraClasses.push("multiple-parents");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ import dialogService from "../services/dialog.js";
|
|||||||
|
|
||||||
export default class SharedSwitchWidget extends SwitchWidget {
|
export default class SharedSwitchWidget extends SwitchWidget {
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
return super.isEnabled() && this.noteId !== 'root' && this.noteId !== 'share';
|
return super.isEnabled()
|
||||||
|
&& !['root', 'share', 'hidden'].includes(this.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
|
@ -101,18 +101,26 @@ export default class SwitchWidget extends NoteContextAwareWidget {
|
|||||||
this.$switchOnName = this.$widget.find(".switch-on-name");
|
this.$switchOnName = this.$widget.find(".switch-on-name");
|
||||||
this.$switchOnButton = this.$widget.find(".switch-on-button");
|
this.$switchOnButton = this.$widget.find(".switch-on-button");
|
||||||
|
|
||||||
this.$switchOnButton.on('click', () => this.switchOn());
|
this.$switchOnButton.on('click', () => this.toggle(true));
|
||||||
|
|
||||||
this.$switchOff = this.$widget.find(".switch-off");
|
this.$switchOff = this.$widget.find(".switch-off");
|
||||||
this.$switchOffName = this.$widget.find(".switch-off-name");
|
this.$switchOffName = this.$widget.find(".switch-off-name");
|
||||||
this.$switchOffButton = this.$widget.find(".switch-off-button");
|
this.$switchOffButton = this.$widget.find(".switch-off-button");
|
||||||
|
|
||||||
this.$switchOffButton.on('click', () => this.switchOff());
|
this.$switchOffButton.on('click', () => this.toggle(false));
|
||||||
|
|
||||||
this.$helpButton = this.$widget.find(".switch-help-button");
|
this.$helpButton = this.$widget.find(".switch-help-button");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggle(state) {
|
||||||
|
if (state) {
|
||||||
|
this.switchOn();
|
||||||
|
} else {
|
||||||
|
this.switchOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switchOff() {}
|
switchOff() {}
|
||||||
switchOn() {}
|
switchOn() {}
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,15 @@ function cloneNoteAfter(req) {
|
|||||||
return cloningService.cloneNoteAfter(noteId, afterBranchId);
|
return cloningService.cloneNoteAfter(noteId, afterBranchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleNoteInParent(req) {
|
||||||
|
const {noteId, parentNoteId, present} = req.params;
|
||||||
|
|
||||||
|
return cloningService.toggleNoteInParent(present === 'true', noteId, parentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
cloneNoteToBranch,
|
cloneNoteToBranch,
|
||||||
cloneNoteToNote,
|
cloneNoteToNote,
|
||||||
cloneNoteAfter
|
cloneNoteAfter,
|
||||||
|
toggleNoteInParent
|
||||||
};
|
};
|
||||||
|
@ -281,6 +281,7 @@ function register(app) {
|
|||||||
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
|
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
|
||||||
|
|
||||||
apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
|
apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
|
||||||
|
apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent);
|
||||||
apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote);
|
apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote);
|
||||||
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
|
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
|
||||||
|
|
||||||
|
@ -66,8 +66,10 @@ function cloneNoteToBranch(noteId, parentBranchId, prefix) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
|
function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
|
||||||
if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
|
if (isNoteDeleted(noteId)) {
|
||||||
return { success: false, message: 'Note is deleted.' };
|
return { success: false, message: `Note '${noteId}' is deleted.` };
|
||||||
|
} else if (isNoteDeleted(parentNoteId)) {
|
||||||
|
return { success: false, message: `Note '${parentNoteId}' is deleted.` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNote = becca.getNote(parentNoteId);
|
const parentNote = becca.getNote(parentNoteId);
|
||||||
@ -89,7 +91,7 @@ function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
|
|||||||
isExpanded: 0
|
isExpanded: 0
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
log.info(`Ensured note ${noteId} is in parent note ${parentNoteId} with prefix ${prefix}`);
|
log.info(`Ensured note '${noteId}' is in parent note '${parentNoteId}' with prefix '${prefix}'`);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
@ -99,22 +101,27 @@ function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
|
|||||||
const branch = becca.getBranch(branchId);
|
const branch = becca.getBranch(branchId);
|
||||||
|
|
||||||
if (branch) {
|
if (branch) {
|
||||||
if (branch.getNote().getParentBranches().length <= 1) {
|
if (!branch.isWeak && branch.getNote().getStrongParentBranches().length <= 1) {
|
||||||
throw new Error(`Cannot remove branch ${branch.branchId} between child ${noteId} and parent ${parentNoteId} because this would delete the note as well.`);
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Cannot remove branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' because this would delete the note as well.`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
branch.deleteBranch();
|
branch.deleteBranch();
|
||||||
|
|
||||||
log.info(`Ensured note ${noteId} is NOT in parent note ${parentNoteId}`);
|
log.info(`Ensured note '${noteId}' is NOT in parent note '${parentNoteId}'`);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNoteInParent(present, noteId, parentNoteId, prefix) {
|
function toggleNoteInParent(present, noteId, parentNoteId, prefix) {
|
||||||
if (present) {
|
if (present) {
|
||||||
ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
|
return ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ensureNoteIsAbsentFromParent(noteId, parentNoteId);
|
return ensureNoteIsAbsentFromParent(noteId, parentNoteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +167,7 @@ function cloneNoteAfter(noteId, afterBranchId) {
|
|||||||
isExpanded: 0
|
isExpanded: 0
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
log.info(`Cloned note ${noteId} into parent note ${afterNote.parentNoteId} after note ${afterNote.noteId}, branch ${afterBranchId}`);
|
log.info(`Cloned note '${noteId}' into parent note '${afterNote.parentNoteId}' after note '${afterNote.noteId}', branch ${afterBranchId}`);
|
||||||
|
|
||||||
return { success: true, branchId: branch.branchId };
|
return { success: true, branchId: branch.branchId };
|
||||||
}
|
}
|
||||||
@ -168,7 +175,7 @@ function cloneNoteAfter(noteId, afterBranchId) {
|
|||||||
function isNoteDeleted(noteId) {
|
function isNoteDeleted(noteId) {
|
||||||
const note = becca.getNote(noteId);
|
const note = becca.getNote(noteId);
|
||||||
|
|
||||||
return note.isDeleted;
|
return !note || note.isDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -58,7 +58,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (becca.getNote(parentNoteId).type === 'launcher') {
|
if (parentNoteId !== 'lb_bookmarks' && becca.getNote(parentNoteId).type === 'launcher') {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Launcher note cannot have any children.'
|
message: 'Launcher note cannot have any children.'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user