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/cli/index.js +169 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/web/index.html +11 -2
- package/dist/web/main.js +414 -64
- package/dist/web/style.css +122 -1
- package/package.json +9 -1
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",
|
|
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"
|
|
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
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
selectProject(project.id);
|
|
210
|
+
header.addEventListener("click", (event) => {
|
|
211
|
+
event.stopPropagation();
|
|
212
|
+
toggleTagExpand(tagKey);
|
|
152
213
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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 [
|
|
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,
|
|
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
|
}
|