mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
duplicate (single) note
This commit is contained in:
parent
00bb1236ce
commit
b16c2d19b6
22
db/migrations/0148__make_isExpanded_not_null.sql
Normal file
22
db/migrations/0148__make_isExpanded_not_null.sql
Normal file
@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS "mig_branches" (
|
||||
`branchId` TEXT NOT NULL,
|
||||
`noteId` TEXT NOT NULL,
|
||||
`parentNoteId` TEXT NOT NULL,
|
||||
`notePosition` INTEGER NOT NULL,
|
||||
`prefix` TEXT,
|
||||
`isExpanded` INTEGER NOT NULL DEFAULT 0,
|
||||
`isDeleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
hash TEXT DEFAULT "" NOT NULL,
|
||||
PRIMARY KEY(`branchId`));
|
||||
|
||||
INSERT INTO mig_branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, utcDateModified, utcDateCreated, hash)
|
||||
SELECT branchId, noteId, parentNoteId, notePosition, prefix, COALESCE(isExpanded, 0), isDeleted, utcDateModified, utcDateCreated, hash FROM branches;
|
||||
|
||||
DROP TABLE branches;
|
||||
ALTER TABLE mig_branches RENAME TO branches;
|
||||
|
||||
CREATE INDEX `IDX_branches_noteId` ON `branches` (`noteId`);
|
||||
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
2
db/migrations/0149__space_out_positions.sql
Normal file
2
db/migrations/0149__space_out_positions.sql
Normal file
@ -0,0 +1,2 @@
|
||||
UPDATE branches SET notePosition = notePosition * 10;
|
||||
UPDATE attributes SET position = position * 10;
|
@ -29,25 +29,6 @@ CREATE TABLE IF NOT EXISTS "api_tokens"
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0,
|
||||
hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`branchId` TEXT NOT NULL,
|
||||
`noteId` TEXT NOT NULL,
|
||||
`parentNoteId` TEXT NOT NULL,
|
||||
`notePosition` INTEGER NOT NULL,
|
||||
`prefix` TEXT,
|
||||
`isExpanded` BOOLEAN,
|
||||
`isDeleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
hash TEXT DEFAULT "" NOT NULL,
|
||||
PRIMARY KEY(`branchId`)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "event_log" (
|
||||
`eventId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT,
|
||||
`comment` TEXT,
|
||||
`utcDateCreated` TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "options"
|
||||
(
|
||||
name TEXT not null PRIMARY KEY,
|
||||
@ -84,21 +65,6 @@ CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "note_contents" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`content` TEXT NULL DEFAULT NULL,
|
||||
`hash` TEXT DEFAULT "" NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`)
|
||||
);
|
||||
CREATE TABLE recent_notes
|
||||
(
|
||||
noteId TEXT not null primary key,
|
||||
notePath TEXT not null,
|
||||
hash TEXT default "" not null,
|
||||
utcDateCreated TEXT not null,
|
||||
isDeleted INT
|
||||
);
|
||||
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
|
||||
`entityName`,
|
||||
`entityId`
|
||||
@ -115,14 +81,6 @@ CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` (
|
||||
CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` (
|
||||
`utcDateModifiedTo`
|
||||
);
|
||||
CREATE INDEX `IDX_branches_noteId` ON `branches` (
|
||||
`noteId`
|
||||
);
|
||||
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
|
||||
`noteId`,
|
||||
`parentNoteId`
|
||||
);
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
CREATE INDEX IDX_attributes_name_value
|
||||
on attributes (name, value);
|
||||
CREATE INDEX IDX_attributes_name_index
|
||||
@ -131,3 +89,33 @@ CREATE INDEX IDX_attributes_noteId_index
|
||||
on attributes (noteId);
|
||||
CREATE INDEX IDX_attributes_value_index
|
||||
on attributes (value);
|
||||
CREATE TABLE IF NOT EXISTS "note_contents" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`content` TEXT NULL DEFAULT NULL,
|
||||
`hash` TEXT DEFAULT "" NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`)
|
||||
);
|
||||
CREATE TABLE recent_notes
|
||||
(
|
||||
noteId TEXT not null primary key,
|
||||
notePath TEXT not null,
|
||||
hash TEXT default "" not null,
|
||||
utcDateCreated TEXT not null,
|
||||
isDeleted INT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`branchId` TEXT NOT NULL,
|
||||
`noteId` TEXT NOT NULL,
|
||||
`parentNoteId` TEXT NOT NULL,
|
||||
`notePosition` INTEGER NOT NULL,
|
||||
`prefix` TEXT,
|
||||
`isExpanded` INTEGER NOT NULL DEFAULT 0,
|
||||
`isDeleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
hash TEXT DEFAULT "" NOT NULL,
|
||||
PRIMARY KEY(`branchId`));
|
||||
CREATE INDEX `IDX_branches_noteId` ON `branches` (`noteId`);
|
||||
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
|
@ -42,7 +42,7 @@ class Branch extends Entity {
|
||||
async beforeSaving() {
|
||||
if (this.notePosition === undefined) {
|
||||
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
|
||||
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10;
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
|
@ -47,14 +47,15 @@ function AttributesModel() {
|
||||
};
|
||||
|
||||
this.updateAttributePositions = function() {
|
||||
let position = 0;
|
||||
let position = 10;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// attributes in the viewmodel (self.ownedAttributes()) stays the same
|
||||
$ownedAttributesBody.find('input[name="position"]').each(function() {
|
||||
const attribute = self.getTargetAttribute(this);
|
||||
|
||||
attribute().position = position++;
|
||||
attribute().position = position;
|
||||
position += 10;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -845,6 +845,18 @@ $tree.on('mousedown', '.fancytree-title', e => {
|
||||
}
|
||||
});
|
||||
|
||||
async function duplicateNote(noteId, parentNoteId) {
|
||||
const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`);
|
||||
|
||||
await reload();
|
||||
|
||||
await activateNote(note.noteId);
|
||||
|
||||
const origNote = await treeCache.getNote(noteId);
|
||||
infoService.showMessage(`Note "${origNote.title}" has been duplicated`);
|
||||
}
|
||||
|
||||
|
||||
utils.bindGlobalShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
|
||||
$collapseTreeButton.click(() => collapseTree());
|
||||
|
||||
@ -882,5 +894,6 @@ export default {
|
||||
resolveNotePath,
|
||||
getSomeNotePath,
|
||||
focusTree,
|
||||
scrollToActiveNote
|
||||
scrollToActiveNote,
|
||||
duplicateNote
|
||||
};
|
@ -8,6 +8,7 @@ import syncService from "./sync.js";
|
||||
import hoistedNoteService from './hoisted_note.js';
|
||||
import noteDetailService from './note_detail.js';
|
||||
import clipboard from './clipboard.js';
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
|
||||
class TreeContextMenu {
|
||||
constructor(node) {
|
||||
@ -68,6 +69,8 @@ class TreeContextMenu {
|
||||
enabled: !clipboard.isEmpty() && note.type !== 'search' && noSelectedNotes },
|
||||
{ title: "Paste after", cmd: "pasteAfter", uiIcon: "clipboard",
|
||||
enabled: !clipboard.isEmpty() && isNotRoot && parentNote.type !== 'search' && noSelectedNotes },
|
||||
{ title: "Duplicate note here", cmd: "duplicateNote", uiIcon: "empty",
|
||||
enabled: noSelectedNotes && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
|
||||
{ title: "----" },
|
||||
{ title: "Export", cmd: "export", uiIcon: "empty",
|
||||
enabled: note.type !== 'search' && noSelectedNotes },
|
||||
@ -152,6 +155,11 @@ class TreeContextMenu {
|
||||
else if (cmd === "unhoist") {
|
||||
hoistedNoteService.unhoist();
|
||||
}
|
||||
else if (cmd === "duplicateNote") {
|
||||
const branch = await treeCache.getBranch(this.node.data.branchId);
|
||||
|
||||
treeService.duplicateNote(this.node.data.noteId, branch.parentNoteId);
|
||||
}
|
||||
else {
|
||||
ws.logError("Unknown command: " + cmd);
|
||||
}
|
||||
|
@ -14,8 +14,7 @@ const TaskContext = require('../../services/task_context');
|
||||
*/
|
||||
|
||||
async function moveBranchToParent(req) {
|
||||
const branchId = req.params.branchId;
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
const {branchId, parentNoteId} = req.params;
|
||||
|
||||
const noteToMove = await tree.getBranch(branchId);
|
||||
|
||||
@ -26,7 +25,7 @@ async function moveBranchToParent(req) {
|
||||
}
|
||||
|
||||
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentNoteId]);
|
||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
|
||||
|
||||
const branch = await repository.getBranch(branchId);
|
||||
branch.parentNoteId = parentNoteId;
|
||||
@ -37,8 +36,7 @@ async function moveBranchToParent(req) {
|
||||
}
|
||||
|
||||
async function moveBranchBeforeNote(req) {
|
||||
const branchId = req.params.branchId;
|
||||
const beforeBranchId = req.params.beforeBranchId;
|
||||
const {branchId, beforeBranchId} = req.params;
|
||||
|
||||
const noteToMove = await tree.getBranch(branchId);
|
||||
const beforeNote = await tree.getBranch(beforeBranchId);
|
||||
@ -51,7 +49,7 @@ async function moveBranchBeforeNote(req) {
|
||||
|
||||
// we don't change utcDateModified so other changes are prioritized in case of conflict
|
||||
// also we would have to sync all those modified branches otherwise hash checks would fail
|
||||
await sql.execute("UPDATE branches SET notePosition = notePosition + 1 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0",
|
||||
await sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0",
|
||||
[beforeNote.parentNoteId, beforeNote.notePosition]);
|
||||
|
||||
await sync_table.addNoteReorderingSync(beforeNote.parentNoteId);
|
||||
@ -65,8 +63,7 @@ async function moveBranchBeforeNote(req) {
|
||||
}
|
||||
|
||||
async function moveBranchAfterNote(req) {
|
||||
const branchId = req.params.branchId;
|
||||
const afterBranchId = req.params.afterBranchId;
|
||||
const {branchId, afterBranchId} = req.params;
|
||||
|
||||
const noteToMove = await tree.getBranch(branchId);
|
||||
const afterNote = await tree.getBranch(afterBranchId);
|
||||
@ -79,22 +76,21 @@ async function moveBranchAfterNote(req) {
|
||||
|
||||
// we don't change utcDateModified so other changes are prioritized in case of conflict
|
||||
// also we would have to sync all those modified branches otherwise hash checks would fail
|
||||
await sql.execute("UPDATE branches SET notePosition = notePosition + 1 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
|
||||
await sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
|
||||
[afterNote.parentNoteId, afterNote.notePosition]);
|
||||
|
||||
await sync_table.addNoteReorderingSync(afterNote.parentNoteId);
|
||||
|
||||
const branch = await repository.getBranch(branchId);
|
||||
branch.parentNoteId = afterNote.parentNoteId;
|
||||
branch.notePosition = afterNote.notePosition + 1;
|
||||
branch.notePosition = afterNote.notePosition + 10;
|
||||
await branch.save();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function setExpanded(req) {
|
||||
const branchId = req.params.branchId;
|
||||
const expanded = req.params.expanded;
|
||||
const {branchId, expanded} = req.params;
|
||||
|
||||
await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
|
||||
// we don't sync expanded label
|
||||
|
@ -180,6 +180,12 @@ async function changeTitle(req) {
|
||||
await note.save();
|
||||
}
|
||||
|
||||
async function duplicateNote(req) {
|
||||
const {noteId, parentNoteId} = req.params;
|
||||
|
||||
return await noteService.duplicateNote(noteId, parentNoteId);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNote,
|
||||
updateNote,
|
||||
@ -190,5 +196,6 @@ module.exports = {
|
||||
setNoteTypeMime,
|
||||
getChildren,
|
||||
getRelationMap,
|
||||
changeTitle
|
||||
changeTitle,
|
||||
duplicateNote
|
||||
};
|
@ -135,6 +135,7 @@ function register(app) {
|
||||
apiRoute(GET, '/api/notes/:noteId/revision-list', noteRevisionsApiRoute.getNoteRevisionList);
|
||||
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
|
||||
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
|
||||
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateNote);
|
||||
|
||||
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
|
||||
|
||||
|
@ -4,7 +4,7 @@ const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 147;
|
||||
const APP_DB_VERSION = 149;
|
||||
const SYNC_VERSION = 10;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
|
@ -81,7 +81,7 @@ async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
|
||||
// we don't change utcDateModified so other changes are prioritized in case of conflict
|
||||
// also we would have to sync all those modified branches otherwise hash checks would fail
|
||||
await sql.execute("UPDATE branches SET notePosition = notePosition + 1 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
|
||||
await sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
|
||||
[afterNote.parentNoteId, afterNote.notePosition]);
|
||||
|
||||
await syncTable.addNoteReorderingSync(afterNote.parentNoteId);
|
||||
@ -89,7 +89,7 @@ async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
const branch = await new Branch({
|
||||
noteId: noteId,
|
||||
parentNoteId: afterNote.parentNoteId,
|
||||
notePosition: afterNote.notePosition + 1,
|
||||
notePosition: afterNote.notePosition + 10,
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
|
||||
|
@ -198,7 +198,7 @@ async function findExistencyIssues() {
|
||||
const branches = await repository.getEntities(`SELECT * FROM branches WHERE noteId = ? and parentNoteId = ? and isDeleted = 1`, [noteId, parentNoteId]);
|
||||
|
||||
// it's not necessarily "original" branch, it's just the only one which will survive
|
||||
const origBranch = branches.get(0);
|
||||
const origBranch = branches[0];
|
||||
|
||||
// delete all but the first branch
|
||||
for (const branch of branches.slice(1)) {
|
||||
|
@ -21,15 +21,15 @@ async function getNewNotePosition(parentNoteId, noteData) {
|
||||
if (noteData.target === 'into') {
|
||||
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentNoteId]);
|
||||
|
||||
newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||
newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
|
||||
}
|
||||
else if (noteData.target === 'after') {
|
||||
const afterNote = await sql.getRow('SELECT notePosition FROM branches WHERE branchId = ?', [noteData.target_branchId]);
|
||||
|
||||
newNotePos = afterNote.notePosition + 1;
|
||||
newNotePos = afterNote.notePosition + 10;
|
||||
|
||||
// not updating utcDateModified to avoig having to sync whole rows
|
||||
await sql.execute('UPDATE branches SET notePosition = notePosition + 1 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0',
|
||||
await sql.execute('UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0',
|
||||
[parentNoteId, afterNote.notePosition]);
|
||||
|
||||
await syncTableService.addNoteReorderingSync(parentNoteId);
|
||||
@ -465,6 +465,44 @@ async function cleanupDeletedNotes() {
|
||||
await sql.execute("UPDATE note_revisions SET content = NULL WHERE note_revisions.content IS NOT NULL AND noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1 AND notes.utcDateModified <= ?)", [dateUtils.utcDateStr(cutoffDate)]);
|
||||
}
|
||||
|
||||
async function duplicateNote(noteId, parentNoteId) {
|
||||
const origNote = await repository.getNote(noteId);
|
||||
|
||||
if (origNote.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
|
||||
throw new Error(`Cannot duplicate note=${origNote.noteId} because it is protected and protected session is not available`);
|
||||
}
|
||||
|
||||
// might be null if orig note is not in the target parentNoteId
|
||||
const origBranch = (await origNote.getBranches()).find(branch => branch.parentNoteId === parentNoteId);
|
||||
|
||||
const newNote = new Note(origNote);
|
||||
newNote.noteId = undefined; // force creation of new note
|
||||
newNote.title += " (dup)";
|
||||
|
||||
await newNote.save();
|
||||
await newNote.setContent(await origNote.getContent());
|
||||
|
||||
const newBranch = await new Branch({
|
||||
noteId: newNote.noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
// here increasing just by 1 to make sure it's directly after original
|
||||
notePosition: origBranch ? origBranch.notePosition + 1 : null
|
||||
}).save();
|
||||
|
||||
for (const attribute of await origNote.getAttributes()) {
|
||||
const attr = new Attribute(attribute);
|
||||
attr.attributeId = undefined; // force creation of new attribute
|
||||
attr.noteId = newNote.noteId;
|
||||
|
||||
await attr.save();
|
||||
}
|
||||
|
||||
return {
|
||||
note: newNote,
|
||||
branch: newBranch
|
||||
};
|
||||
}
|
||||
|
||||
sqlInit.dbReady.then(() => {
|
||||
// first cleanup kickoff 5 minutes after startup
|
||||
setTimeout(cls.wrap(cleanupDeletedNotes), 5 * 60 * 1000);
|
||||
@ -478,5 +516,6 @@ module.exports = {
|
||||
updateNote,
|
||||
deleteBranch,
|
||||
protectNoteRecursively,
|
||||
scanForLinks
|
||||
scanForLinks,
|
||||
duplicateNote
|
||||
};
|
@ -109,7 +109,7 @@ async function createInitialDatabase(username, password, theme) {
|
||||
noteId: 'root',
|
||||
parentNoteId: 'none',
|
||||
isExpanded: true,
|
||||
notePosition: 0
|
||||
notePosition: 10
|
||||
}).save();
|
||||
|
||||
const dummyTaskContext = new TaskContext("1", 'import', false);
|
||||
|
@ -110,13 +110,13 @@ async function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
|
||||
}
|
||||
});
|
||||
|
||||
let position = 1;
|
||||
let position = 10;
|
||||
|
||||
for (const note of notes) {
|
||||
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
|
||||
[position, note.branchId]);
|
||||
|
||||
position++;
|
||||
position += 10;
|
||||
}
|
||||
|
||||
await syncTableService.addNoteReorderingSync(parentNoteId);
|
||||
|
Loading…
x
Reference in New Issue
Block a user