Skip to content

Commit 4fc434a

Browse files
Copiloteliandoran
andcommitted
Implement multi-branch prefix editing functionality
- Add setPrefixBatch API endpoint to handle batch prefix updates - Update branch_prefix dialog to support multiple branches - Remove noSelectedNotes constraint from edit branch prefix menu - Add translations for multi-branch prefix editing Co-authored-by: eliandoran <[email protected]>
1 parent 4c5b2a7 commit 4fc434a

File tree

92 files changed

+661
-23
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+661
-23
lines changed

apps/client/src/menus/tree_context_menu.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
137137
command: "editBranchPrefix",
138138
keyboardShortcut: "editBranchPrefix",
139139
uiIcon: "bx bx-rename",
140-
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
140+
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
141141
},
142142
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
143143

apps/client/src/translations/en/translation.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@
3636
},
3737
"branch_prefix": {
3838
"edit_branch_prefix": "Edit branch prefix",
39+
"edit_branch_prefix_multiple": "Edit branch prefix for {{count}} branches",
3940
"help_on_tree_prefix": "Help on Tree prefix",
4041
"prefix": "Prefix: ",
4142
"save": "Save",
42-
"branch_prefix_saved": "Branch prefix has been saved."
43+
"branch_prefix_saved": "Branch prefix has been saved.",
44+
"branch_prefix_saved_multiple": "Branch prefix has been saved for {{count}} branches.",
45+
"affected_branches": "Affected branches ({{count}}):"
4346
},
4447
"bulk_actions": {
4548
"bulk_actions": "Bulk actions",

apps/client/src/widgets/dialogs/branch_prefix.tsx

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,83 @@ import Button from "../react/Button.jsx";
1010
import FormGroup from "../react/FormGroup.js";
1111
import { useTriliumEvent } from "../react/hooks.jsx";
1212
import FBranch from "../../entities/fbranch.js";
13+
import type { ContextMenuCommandData } from "../../components/app_context.js";
1314

1415
export default function BranchPrefixDialog() {
1516
const [ shown, setShown ] = useState(false);
16-
const [ branch, setBranch ] = useState<FBranch>();
17+
const [ branches, setBranches ] = useState<FBranch[]>([]);
1718
const [ prefix, setPrefix ] = useState("");
1819
const branchInput = useRef<HTMLInputElement>(null);
1920

20-
useTriliumEvent("editBranchPrefix", async () => {
21-
const notePath = appContext.tabManager.getActiveContextNotePath();
22-
if (!notePath) {
23-
return;
24-
}
21+
useTriliumEvent("editBranchPrefix", async (data?: ContextMenuCommandData) => {
22+
let branchIds: string[] = [];
2523

26-
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
24+
if (data?.selectedOrActiveBranchIds && data.selectedOrActiveBranchIds.length > 0) {
25+
// Multi-select mode from tree context menu
26+
branchIds = data.selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("virt-"));
27+
} else {
28+
// Single branch mode from keyboard shortcut or when no selection
29+
const notePath = appContext.tabManager.getActiveContextNotePath();
30+
if (!notePath) {
31+
return;
32+
}
2733

28-
if (!noteId || !parentNoteId) {
29-
return;
34+
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
35+
36+
if (!noteId || !parentNoteId) {
37+
return;
38+
}
39+
40+
const branchId = await froca.getBranchId(parentNoteId, noteId);
41+
if (!branchId) {
42+
return;
43+
}
44+
const parentNote = await froca.getNote(parentNoteId);
45+
if (!parentNote || parentNote.type === "search") {
46+
return;
47+
}
48+
49+
branchIds = [branchId];
3050
}
3151

32-
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
33-
if (!newBranchId) {
52+
if (branchIds.length === 0) {
3453
return;
3554
}
36-
const parentNote = await froca.getNote(parentNoteId);
37-
if (!parentNote || parentNote.type === "search") {
55+
56+
const newBranches = branchIds
57+
.map(id => froca.getBranch(id))
58+
.filter((branch): branch is FBranch => branch !== null);
59+
60+
if (newBranches.length === 0) {
3861
return;
3962
}
4063

41-
const newBranch = froca.getBranch(newBranchId);
42-
setBranch(newBranch);
43-
setPrefix(newBranch?.prefix ?? "");
64+
setBranches(newBranches);
65+
// Use the prefix of the first branch as the initial value
66+
setPrefix(newBranches[0]?.prefix ?? "");
4467
setShown(true);
4568
});
4669

4770
async function onSubmit() {
48-
if (!branch) {
71+
if (branches.length === 0) {
4972
return;
5073
}
5174

52-
savePrefix(branch.branchId, prefix);
75+
if (branches.length === 1) {
76+
await savePrefix(branches[0].branchId, prefix);
77+
} else {
78+
await savePrefixBatch(branches.map(b => b.branchId), prefix);
79+
}
5380
setShown(false);
5481
}
5582

83+
const isSingleBranch = branches.length === 1;
84+
const titleKey = isSingleBranch ? "branch_prefix.edit_branch_prefix" : "branch_prefix.edit_branch_prefix_multiple";
85+
5686
return (
5787
<Modal
5888
className="branch-prefix-dialog"
59-
title={t("branch_prefix.edit_branch_prefix")}
89+
title={t(titleKey, { count: branches.length })}
6090
size="lg"
6191
onShown={() => branchInput.current?.focus()}
6292
onHidden={() => setShown(false)}
@@ -69,9 +99,27 @@ export default function BranchPrefixDialog() {
6999
<div class="input-group">
70100
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
71101
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
72-
<div class="branch-prefix-note-title input-group-text"> - {branch && branch.getNoteFromCache().title}</div>
102+
{isSingleBranch && branches[0] && (
103+
<div class="branch-prefix-note-title input-group-text"> - {branches[0].getNoteFromCache().title}</div>
104+
)}
73105
</div>
74106
</FormGroup>
107+
{!isSingleBranch && (
108+
<div className="branch-prefix-notes-list" style={{ marginTop: "10px" }}>
109+
<strong>{t("branch_prefix.affected_branches", { count: branches.length })}</strong>
110+
<ul style={{ maxHeight: "200px", overflow: "auto", marginTop: "5px" }}>
111+
{branches.map((branch) => {
112+
const note = branch.getNoteFromCache();
113+
return (
114+
<li key={branch.branchId}>
115+
{branch.prefix && <span style={{ color: "#888" }}>{branch.prefix} - </span>}
116+
{note.title}
117+
</li>
118+
);
119+
})}
120+
</ul>
121+
</div>
122+
)}
75123
</Modal>
76124
);
77125
}
@@ -80,3 +128,8 @@ async function savePrefix(branchId: string, prefix: string) {
80128
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
81129
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
82130
}
131+
132+
async function savePrefixBatch(branchIds: string[], prefix: string) {
133+
await server.put("branches/set-prefix-batch", { branchIds, prefix });
134+
toast.showMessage(t("branch_prefix.branch_prefix_saved_multiple", { count: branchIds.length }));
135+
}

apps/server/src/routes/api/branches.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,36 @@ function setPrefix(req: Request) {
270270
branch.save();
271271
}
272272

273+
function setPrefixBatch(req: Request) {
274+
const { branchIds, prefix } = req.body;
275+
276+
if (!Array.isArray(branchIds)) {
277+
throw new ValidationError("branchIds must be an array");
278+
}
279+
280+
const normalizedPrefix = utils.isEmptyOrWhitespace(prefix) ? null : prefix;
281+
282+
for (const branchId of branchIds) {
283+
const branch = becca.getBranch(branchId);
284+
if (branch) {
285+
branch.prefix = normalizedPrefix;
286+
branch.save();
287+
}
288+
}
289+
290+
return {
291+
success: true,
292+
count: branchIds.length
293+
};
294+
}
295+
273296
export default {
274297
moveBranchToParent,
275298
moveBranchBeforeNote,
276299
moveBranchAfterNote,
277300
setExpanded,
278301
setExpandedForSubtree,
279302
deleteBranch,
280-
setPrefix
303+
setPrefix,
304+
setPrefixBatch
281305
};

apps/server/src/routes/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ function register(app: express.Application) {
154154
apiRoute(PUT, "/api/branches/:branchId/expanded-subtree/:expanded", branchesApiRoute.setExpandedForSubtree);
155155
apiRoute(DEL, "/api/branches/:branchId", branchesApiRoute.deleteBranch);
156156
apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix);
157+
apiRoute(PUT, "/api/branches/set-prefix-batch", branchesApiRoute.setPrefixBatch);
157158

158159
apiRoute(GET, "/api/notes/:noteId/attachments", attachmentsApiRoute.getAttachments);
159160
apiRoute(PST, "/api/notes/:noteId/attachments", attachmentsApiRoute.saveAttachment);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
declare global {
2+
interface Window {
3+
editor: ClassicEditor;
4+
}
5+
}
6+
import { ClassicEditor } from 'ckeditor5';
7+
import 'ckeditor5/ckeditor5.css';

packages/ckeditor5-admonition/sample/ckeditor.js

Lines changed: 81 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ckeditor5-admonition/sample/ckeditor.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ckeditor5-admonition/src/admonition.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ckeditor5-admonition/src/admonitionautoformat.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)