mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-11 05:45:26 -06:00
Add multi-branch prefix editing support (#7598)
This commit is contained in:
commit
97e87741ee
@ -137,7 +137,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
command: "editBranchPrefix",
|
command: "editBranchPrefix",
|
||||||
keyboardShortcut: "editBranchPrefix",
|
keyboardShortcut: "editBranchPrefix",
|
||||||
uiIcon: "bx bx-rename",
|
uiIcon: "bx bx-rename",
|
||||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
|
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
|
||||||
},
|
},
|
||||||
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
||||||
|
|
||||||
|
|||||||
@ -36,10 +36,13 @@
|
|||||||
},
|
},
|
||||||
"branch_prefix": {
|
"branch_prefix": {
|
||||||
"edit_branch_prefix": "Edit branch prefix",
|
"edit_branch_prefix": "Edit branch prefix",
|
||||||
|
"edit_branch_prefix_multiple": "Edit branch prefix for {{count}} branches",
|
||||||
"help_on_tree_prefix": "Help on Tree prefix",
|
"help_on_tree_prefix": "Help on Tree prefix",
|
||||||
"prefix": "Prefix: ",
|
"prefix": "Prefix: ",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"branch_prefix_saved": "Branch prefix has been saved."
|
"branch_prefix_saved": "Branch prefix has been saved.",
|
||||||
|
"branch_prefix_saved_multiple": "Branch prefix has been saved for {{count}} branches.",
|
||||||
|
"affected_branches": "Affected branches ({{count}}):"
|
||||||
},
|
},
|
||||||
"bulk_actions": {
|
"bulk_actions": {
|
||||||
"bulk_actions": "Bulk actions",
|
"bulk_actions": "Bulk actions",
|
||||||
|
|||||||
13
apps/client/src/widgets/dialogs/branch_prefix.css
Normal file
13
apps/client/src/widgets/dialogs/branch_prefix.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.branch-prefix-dialog .branch-prefix-notes-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-prefix-dialog .branch-prefix-notes-list ul {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-prefix-dialog .branch-prefix-current {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
@ -10,53 +10,86 @@ import Button from "../react/Button.jsx";
|
|||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||||
import FBranch from "../../entities/fbranch.js";
|
import FBranch from "../../entities/fbranch.js";
|
||||||
|
import type { ContextMenuCommandData } from "../../components/app_context.js";
|
||||||
|
import "./branch_prefix.css";
|
||||||
|
|
||||||
|
// Virtual branches (e.g., from search results) start with this prefix
|
||||||
|
const VIRTUAL_BRANCH_PREFIX = "virt-";
|
||||||
|
|
||||||
export default function BranchPrefixDialog() {
|
export default function BranchPrefixDialog() {
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
const [ branch, setBranch ] = useState<FBranch>();
|
const [ branches, setBranches ] = useState<FBranch[]>([]);
|
||||||
const [ prefix, setPrefix ] = useState("");
|
const [ prefix, setPrefix ] = useState("");
|
||||||
const branchInput = useRef<HTMLInputElement>(null);
|
const branchInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useTriliumEvent("editBranchPrefix", async () => {
|
useTriliumEvent("editBranchPrefix", async (data?: ContextMenuCommandData) => {
|
||||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
let branchIds: string[] = [];
|
||||||
if (!notePath) {
|
|
||||||
|
if (data?.selectedOrActiveBranchIds && data.selectedOrActiveBranchIds.length > 0) {
|
||||||
|
// Multi-select mode from tree context menu
|
||||||
|
branchIds = data.selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith(VIRTUAL_BRANCH_PREFIX));
|
||||||
|
} else {
|
||||||
|
// Single branch mode from keyboard shortcut or when no selection
|
||||||
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
|
if (!notePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
||||||
|
|
||||||
|
if (!noteId || !parentNoteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchId = await froca.getBranchId(parentNoteId, noteId);
|
||||||
|
if (!branchId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentNote = await froca.getNote(parentNoteId);
|
||||||
|
if (!parentNote || parentNote.type === "search") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
branchIds = [branchId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (branchIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
const newBranches = branchIds
|
||||||
|
.map(id => froca.getBranch(id))
|
||||||
|
.filter((branch): branch is FBranch => branch !== null);
|
||||||
|
|
||||||
if (!noteId || !parentNoteId) {
|
if (newBranches.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
|
setBranches(newBranches);
|
||||||
if (!newBranchId) {
|
// Use the prefix of the first branch as the initial value
|
||||||
return;
|
setPrefix(newBranches[0]?.prefix ?? "");
|
||||||
}
|
|
||||||
const parentNote = await froca.getNote(parentNoteId);
|
|
||||||
if (!parentNote || parentNote.type === "search") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newBranch = froca.getBranch(newBranchId);
|
|
||||||
setBranch(newBranch);
|
|
||||||
setPrefix(newBranch?.prefix ?? "");
|
|
||||||
setShown(true);
|
setShown(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
if (!branch) {
|
if (branches.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
savePrefix(branch.branchId, prefix);
|
if (branches.length === 1) {
|
||||||
|
await savePrefix(branches[0].branchId, prefix);
|
||||||
|
} else {
|
||||||
|
await savePrefixBatch(branches.map(b => b.branchId), prefix);
|
||||||
|
}
|
||||||
setShown(false);
|
setShown(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSingleBranch = branches.length === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="branch-prefix-dialog"
|
className="branch-prefix-dialog"
|
||||||
title={t("branch_prefix.edit_branch_prefix")}
|
title={isSingleBranch ? t("branch_prefix.edit_branch_prefix") : t("branch_prefix.edit_branch_prefix_multiple", { count: branches.length })}
|
||||||
size="lg"
|
size="lg"
|
||||||
onShown={() => branchInput.current?.focus()}
|
onShown={() => branchInput.current?.focus()}
|
||||||
onHidden={() => setShown(false)}
|
onHidden={() => setShown(false)}
|
||||||
@ -69,9 +102,27 @@ export default function BranchPrefixDialog() {
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
||||||
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
||||||
<div class="branch-prefix-note-title input-group-text"> - {branch && branch.getNoteFromCache().title}</div>
|
{isSingleBranch && branches[0] && (
|
||||||
|
<div class="branch-prefix-note-title input-group-text"> - {branches[0].getNoteFromCache().title}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
{!isSingleBranch && (
|
||||||
|
<div className="branch-prefix-notes-list">
|
||||||
|
<strong>{t("branch_prefix.affected_branches", { count: branches.length })}</strong>
|
||||||
|
<ul>
|
||||||
|
{branches.map((branch) => {
|
||||||
|
const note = branch.getNoteFromCache();
|
||||||
|
return (
|
||||||
|
<li key={branch.branchId}>
|
||||||
|
{branch.prefix && <span className="branch-prefix-current">{branch.prefix} - </span>}
|
||||||
|
{note.title}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -80,3 +131,8 @@ async function savePrefix(branchId: string, prefix: string) {
|
|||||||
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
||||||
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function savePrefixBatch(branchIds: string[], prefix: string) {
|
||||||
|
await server.put("branches/set-prefix-batch", { branchIds, prefix });
|
||||||
|
toast.showMessage(t("branch_prefix.branch_prefix_saved_multiple", { count: branchIds.length }));
|
||||||
|
}
|
||||||
|
|||||||
@ -1591,6 +1591,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
this.clearSelectedNodes();
|
this.clearSelectedNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async editBranchPrefixCommand({ node }: CommandListenerData<"editBranchPrefix">) {
|
||||||
|
const branchIds = this.getSelectedOrActiveBranchIds(node).filter((branchId) => !branchId.startsWith("virt-"));
|
||||||
|
|
||||||
|
if (!branchIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the event with the selected branch IDs
|
||||||
|
appContext.triggerEvent("editBranchPrefix", {
|
||||||
|
selectedOrActiveBranchIds: branchIds,
|
||||||
|
node: node
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
|
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
|
||||||
if (node.data.noteId === "root") {
|
if (node.data.noteId === "root") {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -270,6 +270,38 @@ function setPrefix(req: Request) {
|
|||||||
branch.save();
|
branch.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setPrefixBatch(req: Request) {
|
||||||
|
const { branchIds, prefix } = req.body;
|
||||||
|
|
||||||
|
if (!Array.isArray(branchIds)) {
|
||||||
|
throw new ValidationError("branchIds must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that prefix is a string or null/undefined to prevent prototype pollution
|
||||||
|
if (prefix !== null && prefix !== undefined && typeof prefix !== 'string') {
|
||||||
|
throw new ValidationError("prefix must be a string or null");
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPrefix = utils.isEmptyOrWhitespace(prefix) ? null : prefix;
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
for (const branchId of branchIds) {
|
||||||
|
const branch = becca.getBranch(branchId);
|
||||||
|
if (branch) {
|
||||||
|
branch.prefix = normalizedPrefix;
|
||||||
|
branch.save();
|
||||||
|
updatedCount++;
|
||||||
|
} else {
|
||||||
|
log.info(`Branch ${branchId} not found, skipping prefix update`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: updatedCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
moveBranchToParent,
|
moveBranchToParent,
|
||||||
moveBranchBeforeNote,
|
moveBranchBeforeNote,
|
||||||
@ -277,5 +309,6 @@ export default {
|
|||||||
setExpanded,
|
setExpanded,
|
||||||
setExpandedForSubtree,
|
setExpandedForSubtree,
|
||||||
deleteBranch,
|
deleteBranch,
|
||||||
setPrefix
|
setPrefix,
|
||||||
|
setPrefixBatch
|
||||||
};
|
};
|
||||||
|
|||||||
@ -154,6 +154,7 @@ function register(app: express.Application) {
|
|||||||
apiRoute(PUT, "/api/branches/:branchId/expanded-subtree/:expanded", branchesApiRoute.setExpandedForSubtree);
|
apiRoute(PUT, "/api/branches/:branchId/expanded-subtree/:expanded", branchesApiRoute.setExpandedForSubtree);
|
||||||
apiRoute(DEL, "/api/branches/:branchId", branchesApiRoute.deleteBranch);
|
apiRoute(DEL, "/api/branches/:branchId", branchesApiRoute.deleteBranch);
|
||||||
apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix);
|
apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix);
|
||||||
|
apiRoute(PUT, "/api/branches/set-prefix-batch", branchesApiRoute.setPrefixBatch);
|
||||||
|
|
||||||
apiRoute(GET, "/api/notes/:noteId/attachments", attachmentsApiRoute.getAttachments);
|
apiRoute(GET, "/api/notes/:noteId/attachments", attachmentsApiRoute.getAttachments);
|
||||||
apiRoute(PST, "/api/notes/:noteId/attachments", attachmentsApiRoute.saveAttachment);
|
apiRoute(PST, "/api/notes/:noteId/attachments", attachmentsApiRoute.saveAttachment);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user