batch-run-local 1.1.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/web/main.js CHANGED
@@ -1,10 +1,16 @@
1
+ const UNGROUPED_TAG = "";
2
+ const UNGROUPED_TAG_LABEL = "未分组";
3
+
1
4
  const state = {
2
5
  projects: [],
6
+ tagOrder: [],
3
7
  branches: [],
4
8
  currentProjectId: undefined,
5
9
  draggedProjectId: undefined,
10
+ draggedTagKey: undefined,
6
11
  suppressProjectClick: false,
7
12
  editingProjectId: undefined,
13
+ tagExpandState: new Map(),
8
14
  };
9
15
 
10
16
  const projectList = document.querySelector("#projectList");
@@ -20,12 +26,16 @@ const closeProjectConfig = document.querySelector("#closeProjectConfig");
20
26
  const projectConfigModal = document.querySelector("#projectConfigModal");
21
27
  const projectNameInput = document.querySelector("#projectName");
22
28
  const projectRootInput = document.querySelector("#projectRoot");
29
+ const projectTagInput = document.querySelector("#projectTag");
30
+ const tagOptions = document.querySelector("#tagOptions");
23
31
  const projectConfigTitle = document.querySelector("#projectConfigTitle");
24
32
  const projectSubmit = document.querySelector("#projectSubmit");
25
33
  const branchPanel = document.querySelector("#branchPanel");
26
34
  const branchSelect = document.querySelector("#branchSelect");
27
35
 
28
- openProjectConfig.addEventListener("click", openCreateProjectModal);
36
+ openProjectConfig.addEventListener("click", () => {
37
+ openCreateProjectModal().catch((error) => setStatus(error.message));
38
+ });
29
39
 
30
40
  closeProjectConfig.addEventListener("click", closeConfigModal);
31
41
 
@@ -47,6 +57,7 @@ projectForm.addEventListener("submit", async (event) => {
47
57
  const formData = new FormData(projectForm);
48
58
  const name = String(formData.get("name") ?? "");
49
59
  const root = String(formData.get("root") ?? "");
60
+ const tag = String(formData.get("tag") ?? "");
50
61
  const editingProjectId = state.editingProjectId;
51
62
 
52
63
  await request(editingProjectId ? `/api/projects/${editingProjectId}` : "/api/projects", {
@@ -54,7 +65,7 @@ projectForm.addEventListener("submit", async (event) => {
54
65
  headers: {
55
66
  "Content-Type": "application/json",
56
67
  },
57
- body: JSON.stringify({ name, root }),
68
+ body: JSON.stringify({ name, root, tag }),
58
69
  });
59
70
 
60
71
  projectForm.reset();
@@ -104,6 +115,7 @@ branchSelect.addEventListener("change", async () => {
104
115
  async function loadProjects() {
105
116
  const data = await request("/api/projects");
106
117
  state.projects = data.projects;
118
+ state.tagOrder = data.tagOrder ?? deriveTagOrder(state.projects);
107
119
  if (!state.projects.some((project) => project.id === state.currentProjectId)) {
108
120
  state.currentProjectId = undefined;
109
121
  state.branches = [];
@@ -114,11 +126,59 @@ async function loadProjects() {
114
126
  renderProjects();
115
127
  }
116
128
 
129
+ async function loadTagOptions() {
130
+ const data = await request("/api/tags");
131
+ tagOptions.innerHTML = data.tags
132
+ .map((tag) => `<option value="${escapeHtml(tag)}"></option>`)
133
+ .join("");
134
+ }
135
+
136
+ function deriveTagOrder(projects) {
137
+ const keys = [];
138
+ const seen = new Set();
139
+
140
+ for (const project of projects) {
141
+ const tagKey = getProjectTagKey(project);
142
+ if (!seen.has(tagKey)) {
143
+ seen.add(tagKey);
144
+ keys.push(tagKey);
145
+ }
146
+ }
147
+
148
+ return keys;
149
+ }
150
+
151
+ function getProjectTagKey(project) {
152
+ return project.tag ?? UNGROUPED_TAG;
153
+ }
154
+
155
+ function getTagLabel(tagKey) {
156
+ return tagKey === UNGROUPED_TAG ? UNGROUPED_TAG_LABEL : tagKey;
157
+ }
158
+
159
+ function getProjectsByTag(tagKey) {
160
+ return state.projects.filter((project) => getProjectTagKey(project) === tagKey);
161
+ }
162
+
163
+ function isTagExpanded(tagKey) {
164
+ if (state.tagExpandState.has(tagKey)) {
165
+ return state.tagExpandState.get(tagKey);
166
+ }
167
+
168
+ return tagKey === UNGROUPED_TAG;
169
+ }
170
+
171
+ function toggleTagExpand(tagKey) {
172
+ state.tagExpandState.set(tagKey, !isTagExpanded(tagKey));
173
+ renderProjects();
174
+ }
175
+
117
176
  function renderProjects() {
177
+ const tagOrder = state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects);
118
178
  projectCount.textContent = `共 ${state.projects.length} 个项目`;
119
179
 
120
180
  if (state.projects.length === 0) {
121
- projectList.innerHTML = `<div class="empty">暂无项目,请点击“项目配置”进行配置。</div>`;
181
+ projectList.innerHTML = `<div class="empty">暂无项目,请点击“新增项目”进行配置。</div>`;
122
182
  currentProjectTitle.textContent = "请选择项目";
123
183
  openProjectRoot.disabled = true;
124
184
  hideBranchPanel();
@@ -128,73 +188,237 @@ function renderProjects() {
128
188
 
129
189
  projectList.innerHTML = "";
130
190
 
131
- for (const project of state.projects) {
132
- const card = document.createElement("article");
133
- card.className = `project-card ${project.id === state.currentProjectId ? "active" : ""}`;
134
- card.draggable = true;
135
- card.dataset.projectId = project.id;
136
- card.innerHTML = `
137
- <div class="project-card-main">
138
- <h3>${escapeHtml(project.name)}</h3>
139
- <div class="path">${escapeHtml(project.root)}</div>
140
- </div>
141
- <div class="project-actions">
142
- <button class="project-edit" type="button" aria-label="编辑 ${escapeHtml(project.name)}">编辑</button>
143
- <button class="danger project-delete" type="button" aria-label="删除 ${escapeHtml(project.name)}">删除</button>
144
- </div>
191
+ for (const tagKey of tagOrder) {
192
+ const projects = getProjectsByTag(tagKey);
193
+ if (projects.length === 0) {
194
+ continue;
195
+ }
196
+
197
+ const expanded = isTagExpanded(tagKey);
198
+ const group = document.createElement("section");
199
+ group.className = `tag-group${expanded ? "" : " collapsed"}`;
200
+ group.dataset.tagKey = tagKey;
201
+
202
+ const header = document.createElement("div");
203
+ header.className = "tag-header tag-toggle";
204
+ header.dataset.tagKey = tagKey;
205
+ header.innerHTML = `
206
+ <span class="expanded-icon ${expanded ? "expanded" : "collapsed"}"></span>
207
+ <span class="tag-title" draggable="true">${escapeHtml(getTagLabel(tagKey))}</span>
208
+ <span class="tag-count">${projects.length}</span>
145
209
  `;
146
-
147
- card.addEventListener("click", () => {
148
- if (state.suppressProjectClick) {
149
- return;
150
- }
151
- selectProject(project.id);
210
+ header.addEventListener("click", (event) => {
211
+ event.stopPropagation();
212
+ toggleTagExpand(tagKey);
152
213
  });
153
- card.addEventListener("dragstart", (event) => {
154
- state.draggedProjectId = project.id;
155
- state.suppressProjectClick = true;
156
- event.dataTransfer.effectAllowed = "move";
157
- event.dataTransfer.setData("text/plain", project.id);
158
- card.classList.add("dragging");
214
+ bindTagHeaderDrag(header, tagKey);
215
+
216
+ const projectsContainer = document.createElement("div");
217
+ projectsContainer.className = "tag-projects";
218
+ projectsContainer.dataset.tagKey = tagKey;
219
+ projectsContainer.hidden = !expanded;
220
+ bindTagProjectsDropZone(projectsContainer, tagKey);
221
+
222
+ for (const project of projects) {
223
+ projectsContainer.append(createProjectCard(project, tagKey));
224
+ }
225
+
226
+ group.append(header, projectsContainer);
227
+ projectList.append(group);
228
+ }
229
+ }
230
+
231
+ function createProjectCard(project, tagKey) {
232
+ const card = document.createElement("article");
233
+ card.className = `project-card ${project.id === state.currentProjectId ? "active" : ""}`;
234
+ card.draggable = true;
235
+ card.dataset.projectId = project.id;
236
+ card.dataset.tagKey = tagKey;
237
+ card.innerHTML = `
238
+ <div class="project-card-main">
239
+ <h3>${escapeHtml(project.name)}</h3>
240
+ <div class="path">${escapeHtml(project.root)}</div>
241
+ </div>
242
+ <div class="project-actions">
243
+ <button class="project-edit" type="button" aria-label="编辑 ${escapeHtml(project.name)}">编辑</button>
244
+ <button class="danger project-delete" type="button" aria-label="删除 ${escapeHtml(project.name)}">删除</button>
245
+ </div>
246
+ `;
247
+
248
+ card.addEventListener("click", () => {
249
+ if (state.suppressProjectClick) {
250
+ return;
251
+ }
252
+ selectProject(project.id);
253
+ });
254
+ card.addEventListener("dragstart", (event) => {
255
+ state.draggedProjectId = project.id;
256
+ state.draggedTagKey = undefined;
257
+ state.suppressProjectClick = true;
258
+ event.dataTransfer.effectAllowed = "move";
259
+ event.dataTransfer.setData("text/plain", project.id);
260
+ card.classList.add("dragging");
261
+ });
262
+ card.addEventListener("dragover", (event) => {
263
+ if (!state.draggedProjectId || state.draggedProjectId === project.id) {
264
+ return;
265
+ }
266
+ event.preventDefault();
267
+ event.stopPropagation();
268
+ event.dataTransfer.dropEffect = "move";
269
+ card.classList.toggle("drop-after", shouldDropAfter(event, card));
270
+ });
271
+ card.addEventListener("dragleave", () => {
272
+ card.classList.remove("drop-after");
273
+ });
274
+ card.addEventListener("drop", (event) => {
275
+ if (!state.draggedProjectId) {
276
+ return;
277
+ }
278
+
279
+ event.preventDefault();
280
+ event.stopPropagation();
281
+ card.classList.remove("drop-after");
282
+ moveProject(
283
+ state.draggedProjectId,
284
+ project.id,
285
+ shouldDropAfter(event, card),
286
+ getProjectTagKey(project),
287
+ ).catch((error) => {
288
+ setStatus(error.message);
159
289
  });
160
- card.addEventListener("dragover", (event) => {
161
- if (!state.draggedProjectId || state.draggedProjectId === project.id) {
162
- return;
290
+ });
291
+ card.addEventListener("dragend", () => {
292
+ state.draggedProjectId = undefined;
293
+ card.classList.remove("dragging", "drop-after");
294
+ clearTagDropIndicators();
295
+ setTimeout(() => {
296
+ state.suppressProjectClick = false;
297
+ }, 0);
298
+ });
299
+ card.querySelector(".project-delete").addEventListener("click", (event) => {
300
+ event.stopPropagation();
301
+ deleteProject(project);
302
+ });
303
+ card.querySelector(".project-edit").addEventListener("click", (event) => {
304
+ event.stopPropagation();
305
+ openEditProjectModal(project).catch((error) => setStatus(error.message));
306
+ });
307
+
308
+ return card;
309
+ }
310
+
311
+ function bindTagHeaderDrag(header, tagKey) {
312
+ const dragHandle = header.querySelector(".tag-title");
313
+
314
+ dragHandle.addEventListener("dragstart", (event) => {
315
+ state.draggedTagKey = tagKey;
316
+ state.draggedProjectId = undefined;
317
+ event.dataTransfer.effectAllowed = "move";
318
+ event.dataTransfer.setData("application/x-tag-key", tagKey);
319
+ header.classList.add("dragging");
320
+ });
321
+ header.addEventListener("dragover", (event) => {
322
+ if (state.draggedProjectId) {
323
+ const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
324
+ if (draggedProject && getProjectTagKey(draggedProject) !== tagKey) {
325
+ event.preventDefault();
326
+ event.stopPropagation();
327
+ event.dataTransfer.dropEffect = "move";
328
+ header.classList.add("collapsed-drop-target");
163
329
  }
164
- event.preventDefault();
165
- event.dataTransfer.dropEffect = "move";
166
- card.classList.toggle("drop-after", shouldDropAfter(event, card));
167
- });
168
- card.addEventListener("dragleave", () => {
169
- card.classList.remove("drop-after");
170
- });
171
- card.addEventListener("drop", (event) => {
172
- if (!state.draggedProjectId) {
330
+ return;
331
+ }
332
+
333
+ if (state.draggedTagKey === undefined || state.draggedTagKey === tagKey) {
334
+ return;
335
+ }
336
+ event.preventDefault();
337
+ event.dataTransfer.dropEffect = "move";
338
+ header.classList.toggle("drop-after", shouldDropAfter(event, header));
339
+ });
340
+ header.addEventListener("dragleave", (event) => {
341
+ if (event.currentTarget.contains(event.relatedTarget)) {
342
+ return;
343
+ }
344
+ header.classList.remove("drop-after", "collapsed-drop-target");
345
+ });
346
+ header.addEventListener("drop", (event) => {
347
+ if (state.draggedProjectId) {
348
+ const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
349
+ if (!draggedProject || getProjectTagKey(draggedProject) === tagKey) {
173
350
  return;
174
351
  }
175
352
 
176
353
  event.preventDefault();
177
- card.classList.remove("drop-after");
178
- moveProject(state.draggedProjectId, project.id, shouldDropAfter(event, card)).catch((error) => {
354
+ event.stopPropagation();
355
+ header.classList.remove("collapsed-drop-target");
356
+ state.tagExpandState.set(tagKey, true);
357
+ moveProjectToTagEnd(state.draggedProjectId, tagKey).catch((error) => {
179
358
  setStatus(error.message);
180
359
  });
360
+ return;
361
+ }
362
+
363
+ if (state.draggedTagKey === undefined || state.draggedTagKey === tagKey) {
364
+ return;
365
+ }
366
+
367
+ event.preventDefault();
368
+ event.stopPropagation();
369
+ header.classList.remove("drop-after");
370
+ moveTagGroup(state.draggedTagKey, tagKey, shouldDropAfter(event, header)).catch((error) => {
371
+ setStatus(error.message);
181
372
  });
182
- card.addEventListener("dragend", () => {
183
- state.draggedProjectId = undefined;
184
- card.classList.remove("dragging", "drop-after");
185
- setTimeout(() => {
186
- state.suppressProjectClick = false;
187
- }, 0);
188
- });
189
- card.querySelector(".project-delete").addEventListener("click", (event) => {
190
- event.stopPropagation();
191
- deleteProject(project);
192
- });
193
- card.querySelector(".project-edit").addEventListener("click", (event) => {
194
- event.stopPropagation();
195
- openEditProjectModal(project);
373
+ });
374
+ dragHandle.addEventListener("dragend", () => {
375
+ state.draggedTagKey = undefined;
376
+ header.classList.remove("dragging", "drop-after", "collapsed-drop-target");
377
+ clearTagDropIndicators();
378
+ });
379
+ }
380
+
381
+ function bindTagProjectsDropZone(container, tagKey) {
382
+ container.addEventListener("dragover", (event) => {
383
+ if (!state.draggedProjectId) {
384
+ return;
385
+ }
386
+
387
+ const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
388
+ if (!draggedProject || getProjectTagKey(draggedProject) === tagKey) {
389
+ return;
390
+ }
391
+
392
+ event.preventDefault();
393
+ event.stopPropagation();
394
+ event.dataTransfer.dropEffect = "move";
395
+ container.classList.add("drag-over");
396
+ });
397
+ container.addEventListener("dragleave", (event) => {
398
+ if (event.currentTarget.contains(event.relatedTarget)) {
399
+ return;
400
+ }
401
+ container.classList.remove("drag-over");
402
+ });
403
+ container.addEventListener("drop", (event) => {
404
+ if (!state.draggedProjectId) {
405
+ return;
406
+ }
407
+
408
+ event.preventDefault();
409
+ event.stopPropagation();
410
+ container.classList.remove("drag-over");
411
+ moveProjectToTagEnd(state.draggedProjectId, tagKey).catch((error) => {
412
+ setStatus(error.message);
196
413
  });
197
- projectList.append(card);
414
+ });
415
+ }
416
+
417
+ function clearTagDropIndicators() {
418
+ for (const element of projectList.querySelectorAll(
419
+ ".tag-header.drop-after, .tag-header.collapsed-drop-target, .tag-projects.drag-over",
420
+ )) {
421
+ element.classList.remove("drop-after", "collapsed-drop-target", "drag-over");
198
422
  }
199
423
  }
200
424
 
@@ -292,24 +516,146 @@ function hideBranchPanel() {
292
516
  branchSelect.disabled = true;
293
517
  }
294
518
 
295
- async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget) {
519
+ async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget, targetTagKey) {
296
520
  if (draggedProjectId === targetProjectId) {
297
521
  return;
298
522
  }
299
523
 
524
+ const draggedProject = state.projects.find((project) => project.id === draggedProjectId);
525
+ if (!draggedProject) {
526
+ return;
527
+ }
528
+
529
+ const normalizedTag = targetTagKey === UNGROUPED_TAG ? undefined : targetTagKey;
530
+ const tagChanged = getProjectTagKey(draggedProject) !== targetTagKey;
531
+
532
+ if (tagChanged) {
533
+ await request(`/api/projects/${draggedProjectId}`, {
534
+ method: "PUT",
535
+ headers: {
536
+ "Content-Type": "application/json",
537
+ },
538
+ body: JSON.stringify({
539
+ name: draggedProject.name,
540
+ root: draggedProject.root,
541
+ tag: normalizedTag,
542
+ }),
543
+ });
544
+ draggedProject.tag = normalizedTag;
545
+ }
546
+
300
547
  const nextProjects = [...state.projects];
301
548
  const draggedIndex = nextProjects.findIndex((project) => project.id === draggedProjectId);
302
549
  if (draggedIndex < 0) {
303
550
  return;
304
551
  }
305
552
 
306
- const [draggedProject] = nextProjects.splice(draggedIndex, 1);
553
+ const [movingProject] = nextProjects.splice(draggedIndex, 1);
554
+ const updatedProject = { ...movingProject, tag: normalizedTag };
307
555
  const targetIndex = nextProjects.findIndex((project) => project.id === targetProjectId);
308
556
  if (targetIndex < 0) {
309
557
  return;
310
558
  }
311
559
 
312
- nextProjects.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0, draggedProject);
560
+ nextProjects.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0, updatedProject);
561
+ await persistProjectOrder(nextProjects, tagChanged ? "项目已移动到新标签。" : "项目排序已保存。");
562
+ }
563
+
564
+ async function moveProjectToTagEnd(draggedProjectId, targetTagKey) {
565
+ const draggedProject = state.projects.find((project) => project.id === draggedProjectId);
566
+ if (!draggedProject) {
567
+ return;
568
+ }
569
+
570
+ const normalizedTag = targetTagKey === UNGROUPED_TAG ? undefined : targetTagKey;
571
+ const tagChanged = getProjectTagKey(draggedProject) !== targetTagKey;
572
+
573
+ if (tagChanged) {
574
+ await request(`/api/projects/${draggedProjectId}`, {
575
+ method: "PUT",
576
+ headers: {
577
+ "Content-Type": "application/json",
578
+ },
579
+ body: JSON.stringify({
580
+ name: draggedProject.name,
581
+ root: draggedProject.root,
582
+ tag: normalizedTag,
583
+ }),
584
+ });
585
+ draggedProject.tag = normalizedTag;
586
+ }
587
+
588
+ const nextProjects = state.projects.filter((project) => project.id !== draggedProjectId);
589
+ const updatedProject = { ...draggedProject, tag: normalizedTag };
590
+ const projectsInTargetTag = nextProjects.filter((project) => getProjectTagKey(project) === targetTagKey);
591
+ const lastProject = projectsInTargetTag[projectsInTargetTag.length - 1];
592
+
593
+ if (lastProject) {
594
+ const lastIndex = nextProjects.findIndex((project) => project.id === lastProject.id);
595
+ nextProjects.splice(lastIndex + 1, 0, updatedProject);
596
+ } else {
597
+ const tagOrder = state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects);
598
+ const targetTagIndex = tagOrder.indexOf(targetTagKey);
599
+ if (targetTagIndex < 0) {
600
+ nextProjects.push(updatedProject);
601
+ } else {
602
+ let insertIndex = nextProjects.length;
603
+ for (let index = targetTagIndex + 1; index < tagOrder.length; index += 1) {
604
+ const nextTagProjectIndex = nextProjects.findIndex(
605
+ (project) => getProjectTagKey(project) === tagOrder[index],
606
+ );
607
+ if (nextTagProjectIndex >= 0) {
608
+ insertIndex = nextTagProjectIndex;
609
+ break;
610
+ }
611
+ }
612
+ nextProjects.splice(insertIndex, 0, updatedProject);
613
+ }
614
+ }
615
+
616
+ await persistProjectOrder(nextProjects, "项目已移动到新标签。");
617
+ }
618
+
619
+ async function moveTagGroup(draggedTagKey, targetTagKey, insertAfterTarget) {
620
+ if (draggedTagKey === targetTagKey) {
621
+ return;
622
+ }
623
+
624
+ const nextTagOrder = [...(state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects))];
625
+ const draggedIndex = nextTagOrder.indexOf(draggedTagKey);
626
+ if (draggedIndex < 0) {
627
+ return;
628
+ }
629
+
630
+ nextTagOrder.splice(draggedIndex, 1);
631
+ const targetIndex = nextTagOrder.indexOf(targetTagKey);
632
+ if (targetIndex < 0) {
633
+ return;
634
+ }
635
+
636
+ nextTagOrder.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0, draggedTagKey);
637
+ state.tagOrder = nextTagOrder;
638
+ renderProjects();
639
+
640
+ try {
641
+ const data = await request("/api/tag-order", {
642
+ method: "PUT",
643
+ headers: {
644
+ "Content-Type": "application/json",
645
+ },
646
+ body: JSON.stringify({ tagKeys: nextTagOrder }),
647
+ });
648
+ state.projects = data.projects;
649
+ state.tagOrder = data.tagOrder ?? nextTagOrder;
650
+ renderProjects();
651
+ setStatus("标签排序已保存。");
652
+ } catch (error) {
653
+ await loadProjects();
654
+ throw error;
655
+ }
656
+ }
657
+
658
+ async function persistProjectOrder(nextProjects, successMessage) {
313
659
  state.projects = nextProjects;
314
660
  renderProjects();
315
661
 
@@ -324,8 +670,9 @@ async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget)
324
670
  }),
325
671
  });
326
672
  state.projects = data.projects;
673
+ state.tagOrder = data.tagOrder ?? deriveTagOrder(state.projects);
327
674
  renderProjects();
328
- setStatus("项目排序已保存。");
675
+ setStatus(successMessage);
329
676
  } catch (error) {
330
677
  await loadProjects();
331
678
  throw error;
@@ -337,21 +684,24 @@ function shouldDropAfter(event, element) {
337
684
  return event.clientY > rect.top + rect.height / 2;
338
685
  }
339
686
 
340
- function openCreateProjectModal() {
687
+ async function openCreateProjectModal() {
341
688
  state.editingProjectId = undefined;
342
689
  projectForm.reset();
343
690
  projectConfigTitle.textContent = "项目配置";
344
691
  projectSubmit.textContent = "新增项目";
692
+ await loadTagOptions();
345
693
  projectConfigModal.hidden = false;
346
694
  projectNameInput.focus();
347
695
  }
348
696
 
349
- function openEditProjectModal(project) {
697
+ async function openEditProjectModal(project) {
350
698
  state.editingProjectId = project.id;
351
699
  projectConfigTitle.textContent = "编辑项目";
352
700
  projectSubmit.textContent = "保存修改";
701
+ await loadTagOptions();
353
702
  projectNameInput.value = project.name;
354
703
  projectRootInput.value = project.root;
704
+ projectTagInput.value = project.tag ?? "";
355
705
  projectConfigModal.hidden = false;
356
706
  projectNameInput.focus();
357
707
  }