batch-run-local 1.1.1 → 1.2.0
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/cli/index.js +269 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/web/index.html +78 -70
- package/dist/web/main.js +495 -67
- package/dist/web/style.css +124 -2
- package/package.json +9 -1
package/dist/web/main.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
const UNGROUPED_TAG = "";
|
|
2
|
+
const UNGROUPED_TAG_LABEL = "未分组";
|
|
3
|
+
|
|
1
4
|
const state = {
|
|
2
5
|
projects: [],
|
|
6
|
+
tagOrder: [],
|
|
7
|
+
branches: [],
|
|
3
8
|
currentProjectId: undefined,
|
|
4
9
|
draggedProjectId: undefined,
|
|
10
|
+
draggedTagKey: undefined,
|
|
5
11
|
suppressProjectClick: false,
|
|
6
12
|
editingProjectId: undefined,
|
|
13
|
+
tagExpandState: new Map(),
|
|
7
14
|
};
|
|
8
15
|
|
|
9
16
|
const projectList = document.querySelector("#projectList");
|
|
@@ -19,10 +26,16 @@ const closeProjectConfig = document.querySelector("#closeProjectConfig");
|
|
|
19
26
|
const projectConfigModal = document.querySelector("#projectConfigModal");
|
|
20
27
|
const projectNameInput = document.querySelector("#projectName");
|
|
21
28
|
const projectRootInput = document.querySelector("#projectRoot");
|
|
29
|
+
const projectTagInput = document.querySelector("#projectTag");
|
|
30
|
+
const tagOptions = document.querySelector("#tagOptions");
|
|
22
31
|
const projectConfigTitle = document.querySelector("#projectConfigTitle");
|
|
23
32
|
const projectSubmit = document.querySelector("#projectSubmit");
|
|
33
|
+
const branchPanel = document.querySelector("#branchPanel");
|
|
34
|
+
const branchSelect = document.querySelector("#branchSelect");
|
|
24
35
|
|
|
25
|
-
openProjectConfig.addEventListener("click",
|
|
36
|
+
openProjectConfig.addEventListener("click", () => {
|
|
37
|
+
openCreateProjectModal().catch((error) => setStatus(error.message));
|
|
38
|
+
});
|
|
26
39
|
|
|
27
40
|
closeProjectConfig.addEventListener("click", closeConfigModal);
|
|
28
41
|
|
|
@@ -44,6 +57,7 @@ projectForm.addEventListener("submit", async (event) => {
|
|
|
44
57
|
const formData = new FormData(projectForm);
|
|
45
58
|
const name = String(formData.get("name") ?? "");
|
|
46
59
|
const root = String(formData.get("root") ?? "");
|
|
60
|
+
const tag = String(formData.get("tag") ?? "");
|
|
47
61
|
const editingProjectId = state.editingProjectId;
|
|
48
62
|
|
|
49
63
|
await request(editingProjectId ? `/api/projects/${editingProjectId}` : "/api/projects", {
|
|
@@ -51,7 +65,7 @@ projectForm.addEventListener("submit", async (event) => {
|
|
|
51
65
|
headers: {
|
|
52
66
|
"Content-Type": "application/json",
|
|
53
67
|
},
|
|
54
|
-
body: JSON.stringify({ name, root }),
|
|
68
|
+
body: JSON.stringify({ name, root, tag }),
|
|
55
69
|
});
|
|
56
70
|
|
|
57
71
|
projectForm.reset();
|
|
@@ -73,97 +87,340 @@ openProjectRoot.addEventListener("click", async () => {
|
|
|
73
87
|
setStatus("已打开项目目录。");
|
|
74
88
|
});
|
|
75
89
|
|
|
90
|
+
branchSelect.addEventListener("change", async () => {
|
|
91
|
+
if (!state.currentProjectId || !branchSelect.value) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const branch = branchSelect.value;
|
|
96
|
+
branchSelect.disabled = true;
|
|
97
|
+
scriptList.textContent = `正在切换到 ${branch}...`;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const data = await request(`/api/projects/${state.currentProjectId}/branches/switch`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify({ branch }),
|
|
106
|
+
});
|
|
107
|
+
renderBranches(data);
|
|
108
|
+
renderScripts(state.currentProjectId, data.scripts);
|
|
109
|
+
setStatus(`已切换到分支:${data.current ?? branch}`);
|
|
110
|
+
} finally {
|
|
111
|
+
branchSelect.disabled = false;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
76
115
|
async function loadProjects() {
|
|
77
116
|
const data = await request("/api/projects");
|
|
78
117
|
state.projects = data.projects;
|
|
118
|
+
state.tagOrder = data.tagOrder ?? deriveTagOrder(state.projects);
|
|
79
119
|
if (!state.projects.some((project) => project.id === state.currentProjectId)) {
|
|
80
120
|
state.currentProjectId = undefined;
|
|
121
|
+
state.branches = [];
|
|
81
122
|
currentProjectTitle.textContent = "请选择项目";
|
|
82
123
|
openProjectRoot.disabled = true;
|
|
124
|
+
hideBranchPanel();
|
|
83
125
|
}
|
|
84
126
|
renderProjects();
|
|
85
127
|
}
|
|
86
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
|
+
|
|
87
176
|
function renderProjects() {
|
|
177
|
+
const tagOrder = state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects);
|
|
88
178
|
projectCount.textContent = `共 ${state.projects.length} 个项目`;
|
|
89
179
|
|
|
90
180
|
if (state.projects.length === 0) {
|
|
91
|
-
projectList.innerHTML = `<div class="empty"
|
|
181
|
+
projectList.innerHTML = `<div class="empty">暂无项目,请点击“新增项目”进行配置。</div>`;
|
|
92
182
|
currentProjectTitle.textContent = "请选择项目";
|
|
93
183
|
openProjectRoot.disabled = true;
|
|
184
|
+
hideBranchPanel();
|
|
94
185
|
scriptList.textContent = "选择左侧项目后读取 scripts";
|
|
95
186
|
return;
|
|
96
187
|
}
|
|
97
188
|
|
|
98
189
|
projectList.innerHTML = "";
|
|
99
190
|
|
|
100
|
-
for (const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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";
|
|
204
|
+
header.dataset.tagKey = tagKey;
|
|
205
|
+
header.innerHTML = `
|
|
206
|
+
<button class="tag-toggle" type="button" aria-expanded="${expanded}" aria-label="${expanded ? "折叠" : "展开"} ${escapeHtml(getTagLabel(tagKey))}">
|
|
207
|
+
${expanded ? "▼" : "▶"}
|
|
208
|
+
</button>
|
|
209
|
+
<span class="tag-title" draggable="true">${escapeHtml(getTagLabel(tagKey))}</span>
|
|
210
|
+
<span class="tag-count">${projects.length}</span>
|
|
114
211
|
`;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
selectProject(project.id);
|
|
212
|
+
header.querySelector(".tag-toggle").addEventListener("click", (event) => {
|
|
213
|
+
event.stopPropagation();
|
|
214
|
+
toggleTagExpand(tagKey);
|
|
121
215
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
216
|
+
bindTagHeaderDrag(header, tagKey);
|
|
217
|
+
|
|
218
|
+
const projectsContainer = document.createElement("div");
|
|
219
|
+
projectsContainer.className = "tag-projects";
|
|
220
|
+
projectsContainer.dataset.tagKey = tagKey;
|
|
221
|
+
projectsContainer.hidden = !expanded;
|
|
222
|
+
bindTagProjectsDropZone(projectsContainer, tagKey);
|
|
223
|
+
|
|
224
|
+
for (const project of projects) {
|
|
225
|
+
projectsContainer.append(createProjectCard(project, tagKey));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
group.append(header, projectsContainer);
|
|
229
|
+
projectList.append(group);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function createProjectCard(project, tagKey) {
|
|
234
|
+
const card = document.createElement("article");
|
|
235
|
+
card.className = `project-card ${project.id === state.currentProjectId ? "active" : ""}`;
|
|
236
|
+
card.draggable = true;
|
|
237
|
+
card.dataset.projectId = project.id;
|
|
238
|
+
card.dataset.tagKey = tagKey;
|
|
239
|
+
card.innerHTML = `
|
|
240
|
+
<div class="project-card-main">
|
|
241
|
+
<h3>${escapeHtml(project.name)}</h3>
|
|
242
|
+
<div class="path">${escapeHtml(project.root)}</div>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="project-actions">
|
|
245
|
+
<button class="project-edit" type="button" aria-label="编辑 ${escapeHtml(project.name)}">编辑</button>
|
|
246
|
+
<button class="danger project-delete" type="button" aria-label="删除 ${escapeHtml(project.name)}">删除</button>
|
|
247
|
+
</div>
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
card.addEventListener("click", () => {
|
|
251
|
+
if (state.suppressProjectClick) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
selectProject(project.id);
|
|
255
|
+
});
|
|
256
|
+
card.addEventListener("dragstart", (event) => {
|
|
257
|
+
state.draggedProjectId = project.id;
|
|
258
|
+
state.draggedTagKey = undefined;
|
|
259
|
+
state.suppressProjectClick = true;
|
|
260
|
+
event.dataTransfer.effectAllowed = "move";
|
|
261
|
+
event.dataTransfer.setData("text/plain", project.id);
|
|
262
|
+
card.classList.add("dragging");
|
|
263
|
+
});
|
|
264
|
+
card.addEventListener("dragover", (event) => {
|
|
265
|
+
if (!state.draggedProjectId || state.draggedProjectId === project.id) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
event.stopPropagation();
|
|
270
|
+
event.dataTransfer.dropEffect = "move";
|
|
271
|
+
card.classList.toggle("drop-after", shouldDropAfter(event, card));
|
|
272
|
+
});
|
|
273
|
+
card.addEventListener("dragleave", () => {
|
|
274
|
+
card.classList.remove("drop-after");
|
|
275
|
+
});
|
|
276
|
+
card.addEventListener("drop", (event) => {
|
|
277
|
+
if (!state.draggedProjectId) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
event.stopPropagation();
|
|
283
|
+
card.classList.remove("drop-after");
|
|
284
|
+
moveProject(
|
|
285
|
+
state.draggedProjectId,
|
|
286
|
+
project.id,
|
|
287
|
+
shouldDropAfter(event, card),
|
|
288
|
+
getProjectTagKey(project),
|
|
289
|
+
).catch((error) => {
|
|
290
|
+
setStatus(error.message);
|
|
128
291
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
292
|
+
});
|
|
293
|
+
card.addEventListener("dragend", () => {
|
|
294
|
+
state.draggedProjectId = undefined;
|
|
295
|
+
card.classList.remove("dragging", "drop-after");
|
|
296
|
+
clearTagDropIndicators();
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
state.suppressProjectClick = false;
|
|
299
|
+
}, 0);
|
|
300
|
+
});
|
|
301
|
+
card.querySelector(".project-delete").addEventListener("click", (event) => {
|
|
302
|
+
event.stopPropagation();
|
|
303
|
+
deleteProject(project);
|
|
304
|
+
});
|
|
305
|
+
card.querySelector(".project-edit").addEventListener("click", (event) => {
|
|
306
|
+
event.stopPropagation();
|
|
307
|
+
openEditProjectModal(project).catch((error) => setStatus(error.message));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return card;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function bindTagHeaderDrag(header, tagKey) {
|
|
314
|
+
const dragHandle = header.querySelector(".tag-title");
|
|
315
|
+
|
|
316
|
+
dragHandle.addEventListener("dragstart", (event) => {
|
|
317
|
+
state.draggedTagKey = tagKey;
|
|
318
|
+
state.draggedProjectId = undefined;
|
|
319
|
+
event.dataTransfer.effectAllowed = "move";
|
|
320
|
+
event.dataTransfer.setData("application/x-tag-key", tagKey);
|
|
321
|
+
header.classList.add("dragging");
|
|
322
|
+
});
|
|
323
|
+
header.addEventListener("dragover", (event) => {
|
|
324
|
+
if (state.draggedProjectId) {
|
|
325
|
+
const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
|
|
326
|
+
if (draggedProject && getProjectTagKey(draggedProject) !== tagKey) {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
event.stopPropagation();
|
|
329
|
+
event.dataTransfer.dropEffect = "move";
|
|
330
|
+
header.classList.add("collapsed-drop-target");
|
|
132
331
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (state.draggedTagKey === undefined || state.draggedTagKey === tagKey) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
event.preventDefault();
|
|
339
|
+
event.dataTransfer.dropEffect = "move";
|
|
340
|
+
header.classList.toggle("drop-after", shouldDropAfter(event, header));
|
|
341
|
+
});
|
|
342
|
+
header.addEventListener("dragleave", (event) => {
|
|
343
|
+
if (event.currentTarget.contains(event.relatedTarget)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
header.classList.remove("drop-after", "collapsed-drop-target");
|
|
347
|
+
});
|
|
348
|
+
header.addEventListener("drop", (event) => {
|
|
349
|
+
if (state.draggedProjectId) {
|
|
350
|
+
const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
|
|
351
|
+
if (!draggedProject || getProjectTagKey(draggedProject) === tagKey) {
|
|
142
352
|
return;
|
|
143
353
|
}
|
|
144
354
|
|
|
145
355
|
event.preventDefault();
|
|
146
|
-
|
|
147
|
-
|
|
356
|
+
event.stopPropagation();
|
|
357
|
+
header.classList.remove("collapsed-drop-target");
|
|
358
|
+
state.tagExpandState.set(tagKey, true);
|
|
359
|
+
moveProjectToTagEnd(state.draggedProjectId, tagKey).catch((error) => {
|
|
148
360
|
setStatus(error.message);
|
|
149
361
|
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (state.draggedTagKey === undefined || state.draggedTagKey === tagKey) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
event.stopPropagation();
|
|
371
|
+
header.classList.remove("drop-after");
|
|
372
|
+
moveTagGroup(state.draggedTagKey, tagKey, shouldDropAfter(event, header)).catch((error) => {
|
|
373
|
+
setStatus(error.message);
|
|
150
374
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
375
|
+
});
|
|
376
|
+
dragHandle.addEventListener("dragend", () => {
|
|
377
|
+
state.draggedTagKey = undefined;
|
|
378
|
+
header.classList.remove("dragging", "drop-after", "collapsed-drop-target");
|
|
379
|
+
clearTagDropIndicators();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function bindTagProjectsDropZone(container, tagKey) {
|
|
384
|
+
container.addEventListener("dragover", (event) => {
|
|
385
|
+
if (!state.draggedProjectId) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
|
|
390
|
+
if (!draggedProject || getProjectTagKey(draggedProject) === tagKey) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
event.stopPropagation();
|
|
396
|
+
event.dataTransfer.dropEffect = "move";
|
|
397
|
+
container.classList.add("drag-over");
|
|
398
|
+
});
|
|
399
|
+
container.addEventListener("dragleave", (event) => {
|
|
400
|
+
if (event.currentTarget.contains(event.relatedTarget)) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
container.classList.remove("drag-over");
|
|
404
|
+
});
|
|
405
|
+
container.addEventListener("drop", (event) => {
|
|
406
|
+
if (!state.draggedProjectId) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
event.stopPropagation();
|
|
412
|
+
container.classList.remove("drag-over");
|
|
413
|
+
moveProjectToTagEnd(state.draggedProjectId, tagKey).catch((error) => {
|
|
414
|
+
setStatus(error.message);
|
|
165
415
|
});
|
|
166
|
-
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function clearTagDropIndicators() {
|
|
420
|
+
for (const element of projectList.querySelectorAll(
|
|
421
|
+
".tag-header.drop-after, .tag-header.collapsed-drop-target, .tag-projects.drag-over",
|
|
422
|
+
)) {
|
|
423
|
+
element.classList.remove("drop-after", "collapsed-drop-target", "drag-over");
|
|
167
424
|
}
|
|
168
425
|
}
|
|
169
426
|
|
|
@@ -175,9 +432,20 @@ async function selectProject(projectId) {
|
|
|
175
432
|
currentProjectTitle.textContent = project?.name ?? "请选择项目";
|
|
176
433
|
openProjectRoot.disabled = !project;
|
|
177
434
|
scriptList.textContent = "正在读取 scripts...";
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
435
|
+
branchPanel.hidden = false;
|
|
436
|
+
branchSelect.innerHTML = `<option value="">读取中...</option>`;
|
|
437
|
+
branchSelect.disabled = true;
|
|
438
|
+
|
|
439
|
+
const [scriptData, branchData] = await Promise.all([
|
|
440
|
+
request(`/api/projects/${projectId}/scripts`),
|
|
441
|
+
request(`/api/projects/${projectId}/branches`).catch((error) => ({
|
|
442
|
+
current: undefined,
|
|
443
|
+
branches: [],
|
|
444
|
+
message: error.message,
|
|
445
|
+
})),
|
|
446
|
+
]);
|
|
447
|
+
renderBranches(branchData);
|
|
448
|
+
renderScripts(projectId, scriptData.scripts);
|
|
181
449
|
}
|
|
182
450
|
|
|
183
451
|
function renderScripts(projectId, scripts) {
|
|
@@ -218,24 +486,178 @@ async function runScript(projectId, script) {
|
|
|
218
486
|
setStatus(`已打开 Git Bash 执行:npm run ${script}${data.pid ? `\nPID: ${data.pid}` : ""}`);
|
|
219
487
|
}
|
|
220
488
|
|
|
221
|
-
|
|
489
|
+
function renderBranches(data) {
|
|
490
|
+
state.branches = data.branches ?? [];
|
|
491
|
+
|
|
492
|
+
if (data.message) {
|
|
493
|
+
branchSelect.innerHTML = `<option value="">${escapeHtml(data.message)}</option>`;
|
|
494
|
+
branchSelect.disabled = true;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
branchPanel.hidden = false;
|
|
499
|
+
|
|
500
|
+
if (state.branches.length === 0) {
|
|
501
|
+
branchSelect.innerHTML = `<option value="">未读取到分支</option>`;
|
|
502
|
+
branchSelect.disabled = true;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
branchSelect.innerHTML = state.branches
|
|
507
|
+
.map((branch) => {
|
|
508
|
+
const selected = branch.current ? " selected" : "";
|
|
509
|
+
return `<option value="${escapeHtml(branch.name)}"${selected}>${escapeHtml(branch.name)}</option>`;
|
|
510
|
+
})
|
|
511
|
+
.join("");
|
|
512
|
+
branchSelect.disabled = false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function hideBranchPanel() {
|
|
516
|
+
branchPanel.hidden = true;
|
|
517
|
+
branchSelect.innerHTML = "";
|
|
518
|
+
branchSelect.disabled = true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget, targetTagKey) {
|
|
222
522
|
if (draggedProjectId === targetProjectId) {
|
|
223
523
|
return;
|
|
224
524
|
}
|
|
225
525
|
|
|
526
|
+
const draggedProject = state.projects.find((project) => project.id === draggedProjectId);
|
|
527
|
+
if (!draggedProject) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const normalizedTag = targetTagKey === UNGROUPED_TAG ? undefined : targetTagKey;
|
|
532
|
+
const tagChanged = getProjectTagKey(draggedProject) !== targetTagKey;
|
|
533
|
+
|
|
534
|
+
if (tagChanged) {
|
|
535
|
+
await request(`/api/projects/${draggedProjectId}`, {
|
|
536
|
+
method: "PUT",
|
|
537
|
+
headers: {
|
|
538
|
+
"Content-Type": "application/json",
|
|
539
|
+
},
|
|
540
|
+
body: JSON.stringify({
|
|
541
|
+
name: draggedProject.name,
|
|
542
|
+
root: draggedProject.root,
|
|
543
|
+
tag: normalizedTag,
|
|
544
|
+
}),
|
|
545
|
+
});
|
|
546
|
+
draggedProject.tag = normalizedTag;
|
|
547
|
+
}
|
|
548
|
+
|
|
226
549
|
const nextProjects = [...state.projects];
|
|
227
550
|
const draggedIndex = nextProjects.findIndex((project) => project.id === draggedProjectId);
|
|
228
551
|
if (draggedIndex < 0) {
|
|
229
552
|
return;
|
|
230
553
|
}
|
|
231
554
|
|
|
232
|
-
const [
|
|
555
|
+
const [movingProject] = nextProjects.splice(draggedIndex, 1);
|
|
556
|
+
const updatedProject = { ...movingProject, tag: normalizedTag };
|
|
233
557
|
const targetIndex = nextProjects.findIndex((project) => project.id === targetProjectId);
|
|
234
558
|
if (targetIndex < 0) {
|
|
235
559
|
return;
|
|
236
560
|
}
|
|
237
561
|
|
|
238
|
-
nextProjects.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0,
|
|
562
|
+
nextProjects.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0, updatedProject);
|
|
563
|
+
await persistProjectOrder(nextProjects, tagChanged ? "项目已移动到新标签。" : "项目排序已保存。");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function moveProjectToTagEnd(draggedProjectId, targetTagKey) {
|
|
567
|
+
const draggedProject = state.projects.find((project) => project.id === draggedProjectId);
|
|
568
|
+
if (!draggedProject) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const normalizedTag = targetTagKey === UNGROUPED_TAG ? undefined : targetTagKey;
|
|
573
|
+
const tagChanged = getProjectTagKey(draggedProject) !== targetTagKey;
|
|
574
|
+
|
|
575
|
+
if (tagChanged) {
|
|
576
|
+
await request(`/api/projects/${draggedProjectId}`, {
|
|
577
|
+
method: "PUT",
|
|
578
|
+
headers: {
|
|
579
|
+
"Content-Type": "application/json",
|
|
580
|
+
},
|
|
581
|
+
body: JSON.stringify({
|
|
582
|
+
name: draggedProject.name,
|
|
583
|
+
root: draggedProject.root,
|
|
584
|
+
tag: normalizedTag,
|
|
585
|
+
}),
|
|
586
|
+
});
|
|
587
|
+
draggedProject.tag = normalizedTag;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const nextProjects = state.projects.filter((project) => project.id !== draggedProjectId);
|
|
591
|
+
const updatedProject = { ...draggedProject, tag: normalizedTag };
|
|
592
|
+
const projectsInTargetTag = nextProjects.filter((project) => getProjectTagKey(project) === targetTagKey);
|
|
593
|
+
const lastProject = projectsInTargetTag[projectsInTargetTag.length - 1];
|
|
594
|
+
|
|
595
|
+
if (lastProject) {
|
|
596
|
+
const lastIndex = nextProjects.findIndex((project) => project.id === lastProject.id);
|
|
597
|
+
nextProjects.splice(lastIndex + 1, 0, updatedProject);
|
|
598
|
+
} else {
|
|
599
|
+
const tagOrder = state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects);
|
|
600
|
+
const targetTagIndex = tagOrder.indexOf(targetTagKey);
|
|
601
|
+
if (targetTagIndex < 0) {
|
|
602
|
+
nextProjects.push(updatedProject);
|
|
603
|
+
} else {
|
|
604
|
+
let insertIndex = nextProjects.length;
|
|
605
|
+
for (let index = targetTagIndex + 1; index < tagOrder.length; index += 1) {
|
|
606
|
+
const nextTagProjectIndex = nextProjects.findIndex(
|
|
607
|
+
(project) => getProjectTagKey(project) === tagOrder[index],
|
|
608
|
+
);
|
|
609
|
+
if (nextTagProjectIndex >= 0) {
|
|
610
|
+
insertIndex = nextTagProjectIndex;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
nextProjects.splice(insertIndex, 0, updatedProject);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
await persistProjectOrder(nextProjects, "项目已移动到新标签。");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function moveTagGroup(draggedTagKey, targetTagKey, insertAfterTarget) {
|
|
622
|
+
if (draggedTagKey === targetTagKey) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const nextTagOrder = [...(state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects))];
|
|
627
|
+
const draggedIndex = nextTagOrder.indexOf(draggedTagKey);
|
|
628
|
+
if (draggedIndex < 0) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
nextTagOrder.splice(draggedIndex, 1);
|
|
633
|
+
const targetIndex = nextTagOrder.indexOf(targetTagKey);
|
|
634
|
+
if (targetIndex < 0) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
nextTagOrder.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0, draggedTagKey);
|
|
639
|
+
state.tagOrder = nextTagOrder;
|
|
640
|
+
renderProjects();
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
const data = await request("/api/tag-order", {
|
|
644
|
+
method: "PUT",
|
|
645
|
+
headers: {
|
|
646
|
+
"Content-Type": "application/json",
|
|
647
|
+
},
|
|
648
|
+
body: JSON.stringify({ tagKeys: nextTagOrder }),
|
|
649
|
+
});
|
|
650
|
+
state.projects = data.projects;
|
|
651
|
+
state.tagOrder = data.tagOrder ?? nextTagOrder;
|
|
652
|
+
renderProjects();
|
|
653
|
+
setStatus("标签排序已保存。");
|
|
654
|
+
} catch (error) {
|
|
655
|
+
await loadProjects();
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function persistProjectOrder(nextProjects, successMessage) {
|
|
239
661
|
state.projects = nextProjects;
|
|
240
662
|
renderProjects();
|
|
241
663
|
|
|
@@ -250,8 +672,9 @@ async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget)
|
|
|
250
672
|
}),
|
|
251
673
|
});
|
|
252
674
|
state.projects = data.projects;
|
|
675
|
+
state.tagOrder = data.tagOrder ?? deriveTagOrder(state.projects);
|
|
253
676
|
renderProjects();
|
|
254
|
-
setStatus(
|
|
677
|
+
setStatus(successMessage);
|
|
255
678
|
} catch (error) {
|
|
256
679
|
await loadProjects();
|
|
257
680
|
throw error;
|
|
@@ -263,21 +686,24 @@ function shouldDropAfter(event, element) {
|
|
|
263
686
|
return event.clientY > rect.top + rect.height / 2;
|
|
264
687
|
}
|
|
265
688
|
|
|
266
|
-
function openCreateProjectModal() {
|
|
689
|
+
async function openCreateProjectModal() {
|
|
267
690
|
state.editingProjectId = undefined;
|
|
268
691
|
projectForm.reset();
|
|
269
692
|
projectConfigTitle.textContent = "项目配置";
|
|
270
693
|
projectSubmit.textContent = "新增项目";
|
|
694
|
+
await loadTagOptions();
|
|
271
695
|
projectConfigModal.hidden = false;
|
|
272
696
|
projectNameInput.focus();
|
|
273
697
|
}
|
|
274
698
|
|
|
275
|
-
function openEditProjectModal(project) {
|
|
699
|
+
async function openEditProjectModal(project) {
|
|
276
700
|
state.editingProjectId = project.id;
|
|
277
701
|
projectConfigTitle.textContent = "编辑项目";
|
|
278
702
|
projectSubmit.textContent = "保存修改";
|
|
703
|
+
await loadTagOptions();
|
|
279
704
|
projectNameInput.value = project.name;
|
|
280
705
|
projectRootInput.value = project.root;
|
|
706
|
+
projectTagInput.value = project.tag ?? "";
|
|
281
707
|
projectConfigModal.hidden = false;
|
|
282
708
|
projectNameInput.focus();
|
|
283
709
|
}
|
|
@@ -299,8 +725,10 @@ async function deleteProject(project) {
|
|
|
299
725
|
|
|
300
726
|
if (state.currentProjectId === project.id) {
|
|
301
727
|
state.currentProjectId = undefined;
|
|
728
|
+
state.branches = [];
|
|
302
729
|
currentProjectTitle.textContent = "请选择项目";
|
|
303
730
|
openProjectRoot.disabled = true;
|
|
731
|
+
hideBranchPanel();
|
|
304
732
|
scriptList.textContent = "选择左侧项目后读取 scripts";
|
|
305
733
|
}
|
|
306
734
|
|