@muzzamil-khan/redmine-agent-mcp 1.0.0 → 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/config/env.d.ts +2 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +8 -0
- package/dist/config/env.js.map +1 -1
- package/dist/mcp/server.js +719 -147
- package/dist/mcp/server.js.map +1 -1
- package/dist/services/assigneeDirectory.d.ts +34 -0
- package/dist/services/assigneeDirectory.d.ts.map +1 -0
- package/dist/services/assigneeDirectory.js +138 -0
- package/dist/services/assigneeDirectory.js.map +1 -0
- package/dist/services/googleSheets.d.ts +32 -0
- package/dist/services/googleSheets.d.ts.map +1 -0
- package/dist/services/googleSheets.js +109 -0
- package/dist/services/googleSheets.js.map +1 -0
- package/dist/services/redmine.d.ts +72 -0
- package/dist/services/redmine.d.ts.map +1 -1
- package/dist/services/redmine.js +141 -0
- package/dist/services/redmine.js.map +1 -1
- package/dist/services/redmineConfig.d.ts +90 -0
- package/dist/services/redmineConfig.d.ts.map +1 -0
- package/dist/services/redmineConfig.js +300 -0
- package/dist/services/redmineConfig.js.map +1 -0
- package/dist/spreadsheet/batchColumnMapping.d.ts +93 -0
- package/dist/spreadsheet/batchColumnMapping.d.ts.map +1 -0
- package/dist/spreadsheet/batchColumnMapping.js +666 -0
- package/dist/spreadsheet/batchColumnMapping.js.map +1 -0
- package/dist/spreadsheet/batchCreationTypes.d.ts +61 -0
- package/dist/spreadsheet/batchCreationTypes.d.ts.map +1 -0
- package/dist/spreadsheet/batchCreationTypes.js +18 -0
- package/dist/spreadsheet/batchCreationTypes.js.map +1 -0
- package/dist/spreadsheet/batchIssueConfig.d.ts +26 -0
- package/dist/spreadsheet/batchIssueConfig.d.ts.map +1 -0
- package/dist/spreadsheet/batchIssueConfig.js +106 -0
- package/dist/spreadsheet/batchIssueConfig.js.map +1 -0
- package/dist/spreadsheet/batchPreviewFormat.d.ts +41 -0
- package/dist/spreadsheet/batchPreviewFormat.d.ts.map +1 -0
- package/dist/spreadsheet/batchPreviewFormat.js +194 -0
- package/dist/spreadsheet/batchPreviewFormat.js.map +1 -0
- package/dist/spreadsheet/batchSheetHeuristics.d.ts +13 -0
- package/dist/spreadsheet/batchSheetHeuristics.d.ts.map +1 -0
- package/dist/spreadsheet/batchSheetHeuristics.js +30 -0
- package/dist/spreadsheet/batchSheetHeuristics.js.map +1 -0
- package/dist/spreadsheet/bulkUpdateExtract.d.ts +14 -0
- package/dist/spreadsheet/bulkUpdateExtract.d.ts.map +1 -0
- package/dist/spreadsheet/bulkUpdateExtract.js +83 -0
- package/dist/spreadsheet/bulkUpdateExtract.js.map +1 -0
- package/dist/spreadsheet/csvUtils.d.ts +14 -0
- package/dist/spreadsheet/csvUtils.d.ts.map +1 -0
- package/dist/spreadsheet/csvUtils.js +113 -0
- package/dist/spreadsheet/csvUtils.js.map +1 -0
- package/dist/spreadsheet/extractRawBatchSheet.d.ts +14 -0
- package/dist/spreadsheet/extractRawBatchSheet.d.ts.map +1 -0
- package/dist/spreadsheet/extractRawBatchSheet.js +33 -0
- package/dist/spreadsheet/extractRawBatchSheet.js.map +1 -0
- package/dist/spreadsheet/featureRow.d.ts +15 -0
- package/dist/spreadsheet/featureRow.d.ts.map +1 -0
- package/dist/spreadsheet/featureRow.js +119 -0
- package/dist/spreadsheet/featureRow.js.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsv.d.ts +8 -0
- package/dist/spreadsheet/parseBatchCreationCsv.d.ts.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsv.js +22 -0
- package/dist/spreadsheet/parseBatchCreationCsv.js.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsvV1.d.ts +3 -0
- package/dist/spreadsheet/parseBatchCreationCsvV1.d.ts.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsvV1.js +226 -0
- package/dist/spreadsheet/parseBatchCreationCsvV1.js.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsvV2.d.ts +4 -0
- package/dist/spreadsheet/parseBatchCreationCsvV2.d.ts.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsvV2.js +180 -0
- package/dist/spreadsheet/parseBatchCreationCsvV2.js.map +1 -0
- package/dist/spreadsheet/redmineIssueCell.d.ts +38 -0
- package/dist/spreadsheet/redmineIssueCell.d.ts.map +1 -0
- package/dist/spreadsheet/redmineIssueCell.js +176 -0
- package/dist/spreadsheet/redmineIssueCell.js.map +1 -0
- package/dist/spreadsheet/sheetColumnMap.d.ts +38 -0
- package/dist/spreadsheet/sheetColumnMap.d.ts.map +1 -0
- package/dist/spreadsheet/sheetColumnMap.js +75 -0
- package/dist/spreadsheet/sheetColumnMap.js.map +1 -0
- package/dist/spreadsheet/sheetGridToCsv.d.ts +28 -0
- package/dist/spreadsheet/sheetGridToCsv.d.ts.map +1 -0
- package/dist/spreadsheet/sheetGridToCsv.js +88 -0
- package/dist/spreadsheet/sheetGridToCsv.js.map +1 -0
- package/dist/spreadsheet/sheetRedmineFields.d.ts +7 -0
- package/dist/spreadsheet/sheetRedmineFields.d.ts.map +1 -0
- package/dist/spreadsheet/sheetRedmineFields.js +22 -0
- package/dist/spreadsheet/sheetRedmineFields.js.map +1 -0
- package/dist/spreadsheet/sheetWriteBack.d.ts +27 -0
- package/dist/spreadsheet/sheetWriteBack.d.ts.map +1 -0
- package/dist/spreadsheet/sheetWriteBack.js +90 -0
- package/dist/spreadsheet/sheetWriteBack.js.map +1 -0
- package/dist/workflows/batchCreateIssuesWorkflow.d.ts +69 -0
- package/dist/workflows/batchCreateIssuesWorkflow.d.ts.map +1 -0
- package/dist/workflows/batchCreateIssuesWorkflow.js +1102 -0
- package/dist/workflows/batchCreateIssuesWorkflow.js.map +1 -0
- package/dist/workflows/bulkUpdateIssuesWorkflow.d.ts +21 -0
- package/dist/workflows/bulkUpdateIssuesWorkflow.d.ts.map +1 -0
- package/dist/workflows/bulkUpdateIssuesWorkflow.js +384 -0
- package/dist/workflows/bulkUpdateIssuesWorkflow.js.map +1 -0
- package/dist/workflows/createIssueWorkflow.d.ts +41 -0
- package/dist/workflows/createIssueWorkflow.d.ts.map +1 -0
- package/dist/workflows/createIssueWorkflow.js +697 -0
- package/dist/workflows/createIssueWorkflow.js.map +1 -0
- package/dist/workflows/getAllUsersWorkflow.d.ts +27 -0
- package/dist/workflows/getAllUsersWorkflow.d.ts.map +1 -0
- package/dist/workflows/getAllUsersWorkflow.js +48 -0
- package/dist/workflows/getAllUsersWorkflow.js.map +1 -0
- package/dist/workflows/issueCreationHelpers.d.ts +54 -0
- package/dist/workflows/issueCreationHelpers.d.ts.map +1 -0
- package/dist/workflows/issueCreationHelpers.js +272 -0
- package/dist/workflows/issueCreationHelpers.js.map +1 -0
- package/dist/workflows/logTimeWorkflow.d.ts +6 -1
- package/dist/workflows/logTimeWorkflow.d.ts.map +1 -1
- package/dist/workflows/logTimeWorkflow.js +139 -20
- package/dist/workflows/logTimeWorkflow.js.map +1 -1
- package/dist/workflows/sheetToCsvWorkflow.d.ts +15 -0
- package/dist/workflows/sheetToCsvWorkflow.d.ts.map +1 -0
- package/dist/workflows/sheetToCsvWorkflow.js +65 -0
- package/dist/workflows/sheetToCsvWorkflow.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { extractRawBatchSheet } from "../spreadsheet/extractRawBatchSheet.js";
|
|
4
|
+
import { BATCH_COLUMN_FIELD_SCHEMA, buildColumnMappingQuestions, buildMappingPreview, parseBatchWithColumnMapping, STEPS_TO_REPRODUCE_RULE, suggestColumnMappingFromHeaders, validateColumnMapping, } from "../spreadsheet/batchColumnMapping.js";
|
|
5
|
+
import { BATCH_CREATE_LEVEL_ORDER, } from "../spreadsheet/batchCreationTypes.js";
|
|
6
|
+
import { buildPreviewMermaidDiagram, buildPreviewOverview, buildPreviewTree, } from "../spreadsheet/batchPreviewFormat.js";
|
|
7
|
+
import { loadAssigneeDirectory, matchAssigneeId, } from "../services/assigneeDirectory.js";
|
|
8
|
+
import { columnIndexToLetter } from "../services/googleSheets.js";
|
|
9
|
+
import { createProjectVersion, getProjects, getProjectVersions, } from "../services/redmine.js";
|
|
10
|
+
import { enumerationOptions, loadRedmineConfig, STEPS_TO_REPRODUCE_TRACKER_NAMES, } from "../services/redmineConfig.js";
|
|
11
|
+
import { enrichBatchIssuesFromConfig } from "../spreadsheet/batchIssueConfig.js";
|
|
12
|
+
import { createRedmineIssueFromBatch, parseProjectSelection, } from "./issueCreationHelpers.js";
|
|
13
|
+
import { findFeatureTitleColumnIndex } from "../spreadsheet/featureRow.js";
|
|
14
|
+
import { resolveTaskIdColumnForWriteBack, TASK_ID_COLUMN_HEADER, usesCompositeTicketColumn, } from "../spreadsheet/sheetColumnMap.js";
|
|
15
|
+
/** AskQuestion option: user skips the row (agent adds nodeId to skipNodeIds). */
|
|
16
|
+
export const ASSIGNEE_SKIP_OPTION = "Skip this row";
|
|
17
|
+
/** AskQuestion option: create row without assigned_to (clear invalid sheet label). */
|
|
18
|
+
export const ASSIGNEE_UNASSIGNED_OPTION = "Leave unassigned";
|
|
19
|
+
/** AskQuestion option: create user story without fixed_version. */
|
|
20
|
+
export const TARGET_VERSION_SKIP_OPTION = "Skip target version";
|
|
21
|
+
/** Agent retry: `Create: <name>` creates a project version with that name (e.g. user typed name via AskQuestion Other). */
|
|
22
|
+
const TARGET_VERSION_CREATE_PREFIX = /^Create:\s*(.+)$/i;
|
|
23
|
+
const DEFAULT_STATUS = "New";
|
|
24
|
+
const DEFAULT_PRIORITY = "Normal";
|
|
25
|
+
/** Above this, UI should ask show_preview / proceed / cancel instead of dumping full preview. */
|
|
26
|
+
const PREVIEW_LONG_MIN_ISSUES = 8;
|
|
27
|
+
const PREVIEW_LONG_MIN_CHARS = 1200;
|
|
28
|
+
function applyExistingNodeIds(issues, existing) {
|
|
29
|
+
if (!existing)
|
|
30
|
+
return issues;
|
|
31
|
+
return issues.map((issue) => {
|
|
32
|
+
const id = existing[issue.nodeId];
|
|
33
|
+
if (id === undefined)
|
|
34
|
+
return issue;
|
|
35
|
+
return { ...issue, existingRedmineId: id };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function applyRowFixes(issues, fixes) {
|
|
39
|
+
if (!fixes)
|
|
40
|
+
return issues;
|
|
41
|
+
return issues.map((issue) => {
|
|
42
|
+
const fix = fixes[issue.nodeId];
|
|
43
|
+
if (!fix)
|
|
44
|
+
return issue;
|
|
45
|
+
return {
|
|
46
|
+
...issue,
|
|
47
|
+
...(fix.subject !== undefined ? { subject: fix.subject.trim() } : {}),
|
|
48
|
+
...(fix.assignedTo !== undefined
|
|
49
|
+
? { assignedTo: fix.assignedTo.trim() }
|
|
50
|
+
: {}),
|
|
51
|
+
...(fix.targetVersion !== undefined
|
|
52
|
+
? { targetVersion: fix.targetVersion.trim() }
|
|
53
|
+
: {}),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function findMissingTargetVersionStories(issues, skipSet) {
|
|
58
|
+
return issues.filter((issue) => issue.level === "userStory" &&
|
|
59
|
+
!issue.targetVersion.trim() &&
|
|
60
|
+
issue.existingRedmineId === undefined &&
|
|
61
|
+
!skipSet.has(issue.nodeId));
|
|
62
|
+
}
|
|
63
|
+
function applyMissingTargetVersionFix(issues, fix, missingStories) {
|
|
64
|
+
if (!fix?.trim() || missingStories.length === 0)
|
|
65
|
+
return issues;
|
|
66
|
+
if (fix.trim() === TARGET_VERSION_SKIP_OPTION)
|
|
67
|
+
return issues;
|
|
68
|
+
const missingIds = new Set(missingStories.map((m) => m.nodeId));
|
|
69
|
+
const version = fix.trim();
|
|
70
|
+
return issues.map((issue) => missingIds.has(issue.nodeId) ? { ...issue, targetVersion: version } : issue);
|
|
71
|
+
}
|
|
72
|
+
async function loadProjectVersionOptions(projectId) {
|
|
73
|
+
const data = await getProjectVersions(projectId);
|
|
74
|
+
const versions = (data.versions ?? []);
|
|
75
|
+
return versions.map((v) => `${v.id} — ${v.name}`);
|
|
76
|
+
}
|
|
77
|
+
async function resolveMissingTargetVersionFix(params) {
|
|
78
|
+
const fix = params.fix.trim();
|
|
79
|
+
if (!fix) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: "Target version selection is required.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (fix === TARGET_VERSION_SKIP_OPTION) {
|
|
86
|
+
return { ok: true, resolved: fix };
|
|
87
|
+
}
|
|
88
|
+
const createPrefixMatch = TARGET_VERSION_CREATE_PREFIX.exec(fix);
|
|
89
|
+
const createName = createPrefixMatch?.[1]?.trim() || "";
|
|
90
|
+
if (createPrefixMatch !== null) {
|
|
91
|
+
if (!createName) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: 'Enter a version name via AskQuestion Other, then retry with missingTargetVersionFix: "Create: <name>".',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const created = await createProjectVersion(params.projectId, createName);
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
resolved: `${created.id} — ${created.name}`,
|
|
102
|
+
created,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const message = err instanceof Error
|
|
107
|
+
? err.message
|
|
108
|
+
: "Failed to create target version in Redmine.";
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
error: `Could not create target version "${createName}": ${message}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, resolved: fix };
|
|
116
|
+
}
|
|
117
|
+
function applyAssigneeNameFixes(issues, nameFixes, skipSet) {
|
|
118
|
+
if (!nameFixes || Object.keys(nameFixes).length === 0) {
|
|
119
|
+
return issues;
|
|
120
|
+
}
|
|
121
|
+
return issues.map((issue) => {
|
|
122
|
+
if (issue.level !== "subtask")
|
|
123
|
+
return issue;
|
|
124
|
+
const sheetLabel = issue.assignedTo.trim();
|
|
125
|
+
if (!sheetLabel)
|
|
126
|
+
return issue;
|
|
127
|
+
const fix = nameFixes[sheetLabel];
|
|
128
|
+
if (fix === undefined)
|
|
129
|
+
return issue;
|
|
130
|
+
if (fix.trim() === ASSIGNEE_SKIP_OPTION) {
|
|
131
|
+
skipSet.add(issue.nodeId);
|
|
132
|
+
return issue;
|
|
133
|
+
}
|
|
134
|
+
if (fix.trim() === ASSIGNEE_UNASSIGNED_OPTION) {
|
|
135
|
+
return { ...issue, assignedTo: "" };
|
|
136
|
+
}
|
|
137
|
+
return { ...issue, assignedTo: fix.trim() };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function applyPriorityNameFixes(issues, nameFixes) {
|
|
141
|
+
if (!nameFixes || Object.keys(nameFixes).length === 0) {
|
|
142
|
+
return issues;
|
|
143
|
+
}
|
|
144
|
+
return issues.map((issue) => {
|
|
145
|
+
const sheetLabel = issue.priority?.trim();
|
|
146
|
+
if (!sheetLabel)
|
|
147
|
+
return issue;
|
|
148
|
+
const fix = nameFixes[sheetLabel];
|
|
149
|
+
if (fix === undefined)
|
|
150
|
+
return issue;
|
|
151
|
+
return { ...issue, priority: fix.trim() };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function applyTrackerNameFixes(issues, nameFixes) {
|
|
155
|
+
if (!nameFixes || Object.keys(nameFixes).length === 0) {
|
|
156
|
+
return issues;
|
|
157
|
+
}
|
|
158
|
+
return issues.map((issue) => {
|
|
159
|
+
const sheetLabel = issue.sheetType?.trim();
|
|
160
|
+
if (!sheetLabel)
|
|
161
|
+
return issue;
|
|
162
|
+
const fix = nameFixes[sheetLabel];
|
|
163
|
+
if (fix === undefined)
|
|
164
|
+
return issue;
|
|
165
|
+
return { ...issue, sheetType: fix.trim() };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function applyStatusNameFixes(issues, nameFixes) {
|
|
169
|
+
if (!nameFixes || Object.keys(nameFixes).length === 0) {
|
|
170
|
+
return issues;
|
|
171
|
+
}
|
|
172
|
+
return issues.map((issue) => {
|
|
173
|
+
const sheetLabel = issue.status?.trim();
|
|
174
|
+
if (!sheetLabel)
|
|
175
|
+
return issue;
|
|
176
|
+
const fix = nameFixes[sheetLabel];
|
|
177
|
+
if (fix === undefined)
|
|
178
|
+
return issue;
|
|
179
|
+
return { ...issue, status: fix.trim() };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function resolveAssigneeLabel(assignedTo, users) {
|
|
183
|
+
const trimmed = assignedTo.trim();
|
|
184
|
+
if (!trimmed)
|
|
185
|
+
return { label: "" };
|
|
186
|
+
if (!users?.length)
|
|
187
|
+
return { label: trimmed };
|
|
188
|
+
const id = matchAssigneeId(trimmed, users);
|
|
189
|
+
if (id === undefined)
|
|
190
|
+
return { label: trimmed };
|
|
191
|
+
const member = users.find((u) => u.id === id);
|
|
192
|
+
return { id, label: member?.label ?? trimmed };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Mandatory: when every non-skipped Task under a User Story has the same assignee
|
|
196
|
+
* (matched by member id when directory is available, else exact label), set that
|
|
197
|
+
* assignee on the User Story too.
|
|
198
|
+
*/
|
|
199
|
+
function propagateAssigneeToUserStories(issues, skipSet, users) {
|
|
200
|
+
return issues.map((issue) => {
|
|
201
|
+
if (issue.level !== "userStory")
|
|
202
|
+
return issue;
|
|
203
|
+
if (skipSet.has(issue.nodeId))
|
|
204
|
+
return issue;
|
|
205
|
+
const subtasks = issues.filter((s) => s.level === "subtask" &&
|
|
206
|
+
s.parentNodeId === issue.nodeId &&
|
|
207
|
+
!skipSet.has(s.nodeId));
|
|
208
|
+
if (subtasks.length === 0)
|
|
209
|
+
return issue;
|
|
210
|
+
const resolved = subtasks.map((s) => resolveAssigneeLabel(s.assignedTo, users));
|
|
211
|
+
if (resolved.some((r) => !r.label))
|
|
212
|
+
return issue;
|
|
213
|
+
const ids = resolved.map((r) => r.id ?? r.label);
|
|
214
|
+
const unique = new Set(ids);
|
|
215
|
+
if (unique.size !== 1)
|
|
216
|
+
return issue;
|
|
217
|
+
const canonical = resolved[0].label;
|
|
218
|
+
return { ...issue, assignedTo: canonical };
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function groupInvalidAssigneesByName(invalidAssignees) {
|
|
222
|
+
const byName = new Map();
|
|
223
|
+
for (const issue of invalidAssignees) {
|
|
224
|
+
const key = issue.assignedTo.trim();
|
|
225
|
+
const list = byName.get(key) ?? [];
|
|
226
|
+
list.push(issue);
|
|
227
|
+
byName.set(key, list);
|
|
228
|
+
}
|
|
229
|
+
return [...byName.entries()].map(([assignedTo, group]) => ({
|
|
230
|
+
assignedTo,
|
|
231
|
+
nodeIds: group.map((i) => i.nodeId),
|
|
232
|
+
rowIndexes: group.map((i) => i.rowIndex).sort((a, b) => a - b),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
function assigneeFixQuestionId(sheetLabel) {
|
|
236
|
+
return `assignee-name-${encodeURIComponent(sheetLabel)}`;
|
|
237
|
+
}
|
|
238
|
+
function buildPreviewRows(issues, skipSet) {
|
|
239
|
+
return issues.map((i) => ({
|
|
240
|
+
nodeId: i.nodeId,
|
|
241
|
+
rowIndex: i.rowIndex,
|
|
242
|
+
level: i.level,
|
|
243
|
+
tracker: i.tracker,
|
|
244
|
+
subject: i.subject,
|
|
245
|
+
descriptionLength: i.description.length,
|
|
246
|
+
parentNodeId: i.parentNodeId,
|
|
247
|
+
...(i.estimatedHours !== undefined ? { estimatedHours: i.estimatedHours } : {}),
|
|
248
|
+
targetVersion: i.targetVersion,
|
|
249
|
+
assignedTo: i.assignedTo,
|
|
250
|
+
system: i.system,
|
|
251
|
+
skipped: skipSet.has(i.nodeId),
|
|
252
|
+
onRedmine: Boolean(i.existingRedmineId),
|
|
253
|
+
...(i.existingRedmineId !== undefined
|
|
254
|
+
? { existingRedmineId: i.existingRedmineId }
|
|
255
|
+
: {}),
|
|
256
|
+
...(i.status ? { status: i.status } : {}),
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
function partitionIssues(issues, skipSet) {
|
|
260
|
+
const active = issues.filter((i) => !skipSet.has(i.nodeId));
|
|
261
|
+
const alreadyOnRedmine = active.filter((i) => i.existingRedmineId !== undefined);
|
|
262
|
+
const toCreate = sortIssuesForUpload(active.filter((i) => i.existingRedmineId === undefined));
|
|
263
|
+
return { active, alreadyOnRedmine, toCreate };
|
|
264
|
+
}
|
|
265
|
+
function isPreviewLong(previewText, issueCount) {
|
|
266
|
+
return (issueCount > PREVIEW_LONG_MIN_ISSUES ||
|
|
267
|
+
previewText.length > PREVIEW_LONG_MIN_CHARS ||
|
|
268
|
+
previewText.split("\n").length > PREVIEW_LONG_MIN_ISSUES);
|
|
269
|
+
}
|
|
270
|
+
function buildPreviewSummary(rows, toCreateCount, skippedCount, alreadyOnRedmineCount, featureTitle) {
|
|
271
|
+
const active = rows.filter((r) => !r.skipped);
|
|
272
|
+
const features = active.filter((r) => r.level === "feature");
|
|
273
|
+
const stories = active.filter((r) => r.level === "userStory");
|
|
274
|
+
const subtaskCount = active.filter((r) => r.level === "subtask").length;
|
|
275
|
+
const newStories = stories.filter((r) => !r.onRedmine).length;
|
|
276
|
+
const newTasks = active.filter((r) => r.level === "subtask" && !r.onRedmine).length;
|
|
277
|
+
const title = featureTitle ?? features[0]?.subject;
|
|
278
|
+
const lines = [];
|
|
279
|
+
if (title) {
|
|
280
|
+
lines.push(`Feature "${title}": create ${toCreateCount} new issue(s) (${newStories} User Story(ies), ${newTasks} Task(s)).`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
lines.push(`Create ${toCreateCount} new issue(s): ${newStories} User Story(ies), ${newTasks} Task(s).`);
|
|
284
|
+
}
|
|
285
|
+
if (alreadyOnRedmineCount > 0) {
|
|
286
|
+
lines.push(`${alreadyOnRedmineCount} row(s) already on Redmine (Task ID set) — will not upload.`);
|
|
287
|
+
}
|
|
288
|
+
if (skippedCount > 0) {
|
|
289
|
+
lines.push(`${skippedCount} row(s) skipped.`);
|
|
290
|
+
}
|
|
291
|
+
lines.push("");
|
|
292
|
+
for (const story of stories) {
|
|
293
|
+
const subs = active.filter((r) => r.parentNodeId === story.nodeId);
|
|
294
|
+
const eta = story.estimatedHours !== undefined
|
|
295
|
+
? ` (${story.estimatedHours}h)`
|
|
296
|
+
: "";
|
|
297
|
+
const version = story.targetVersion
|
|
298
|
+
? ` · version: ${story.targetVersion}`
|
|
299
|
+
: "";
|
|
300
|
+
const storyAssign = story.assignedTo ? ` · assignee: ${story.assignedTo}` : "";
|
|
301
|
+
lines.push(`• User Story [row ${story.rowIndex}] ${story.subject}${eta}${version}${storyAssign} → ${subs.length} Task(s)`);
|
|
302
|
+
}
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
function sortIssuesForUpload(issues) {
|
|
306
|
+
return [...issues].sort((a, b) => BATCH_CREATE_LEVEL_ORDER[a.level] -
|
|
307
|
+
BATCH_CREATE_LEVEL_ORDER[b.level]);
|
|
308
|
+
}
|
|
309
|
+
function buildPreviewText(rows) {
|
|
310
|
+
const lines = rows.map((r) => {
|
|
311
|
+
const skip = r.skipped ? " [SKIP]" : "";
|
|
312
|
+
const onRedmine = r.onRedmine
|
|
313
|
+
? ` [ON REDMINE #${r.existingRedmineId}]`
|
|
314
|
+
: "";
|
|
315
|
+
const parent = r.parentNodeId ? ` parent=${r.parentNodeId}` : "";
|
|
316
|
+
const eta = r.estimatedHours !== undefined ? ` ETA=${r.estimatedHours}h` : "";
|
|
317
|
+
const desc = r.descriptionLength > 0 ? ` desc=${r.descriptionLength}ch` : "";
|
|
318
|
+
const version = r.targetVersion && r.level === "userStory"
|
|
319
|
+
? ` version=${r.targetVersion}`
|
|
320
|
+
: "";
|
|
321
|
+
const assign = r.assignedTo &&
|
|
322
|
+
(r.level === "subtask" || r.level === "userStory")
|
|
323
|
+
? ` assignee=${r.assignedTo}`
|
|
324
|
+
: "";
|
|
325
|
+
const levelLabel = r.level === "feature"
|
|
326
|
+
? "Feature"
|
|
327
|
+
: r.level === "userStory"
|
|
328
|
+
? "User Story"
|
|
329
|
+
: "Task";
|
|
330
|
+
return `- row ${r.rowIndex} [${levelLabel}] ${r.tracker}: ${r.subject}${eta}${version}${assign}${desc}${parent}${onRedmine}${skip}`;
|
|
331
|
+
});
|
|
332
|
+
return lines.join("\n");
|
|
333
|
+
}
|
|
334
|
+
function emptySubjectQuestions(issues) {
|
|
335
|
+
const questions = [];
|
|
336
|
+
for (const issue of issues) {
|
|
337
|
+
if (!issue.subject) {
|
|
338
|
+
questions.push({
|
|
339
|
+
id: issue.nodeId,
|
|
340
|
+
field: `rowFixes.${issue.nodeId}.subject`,
|
|
341
|
+
question: `Row ${issue.rowIndex} (${issue.level}): subject is empty — provide text`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return questions;
|
|
346
|
+
}
|
|
347
|
+
function groupUnresolvedByLabel(items) {
|
|
348
|
+
const groups = new Map();
|
|
349
|
+
for (const item of items) {
|
|
350
|
+
const group = groups.get(item.label) ?? { nodeIds: [], rowIndexes: [] };
|
|
351
|
+
group.nodeIds.push(item.nodeId);
|
|
352
|
+
group.rowIndexes.push(item.rowIndex);
|
|
353
|
+
groups.set(item.label, group);
|
|
354
|
+
}
|
|
355
|
+
return [...groups.entries()].map(([label, group]) => ({
|
|
356
|
+
label,
|
|
357
|
+
nodeIds: group.nodeIds,
|
|
358
|
+
rowIndexes: group.rowIndexes,
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
function priorityValidationQuestions(unresolvedPriorities, priorityOptions) {
|
|
362
|
+
return groupUnresolvedByLabel(unresolvedPriorities).map((group) => {
|
|
363
|
+
const rowList = group.rowIndexes.join(", ");
|
|
364
|
+
return {
|
|
365
|
+
id: `priority-name-${group.label}`,
|
|
366
|
+
field: `priorityNameFixes.${group.label}`,
|
|
367
|
+
question: `Priority "${group.label}" is not in Redmine config (${group.nodeIds.length} row(s): ${rowList}). Pick the matching priority from config.`,
|
|
368
|
+
options: priorityOptions,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function trackerValidationQuestions(unresolvedTrackers, trackerOptions) {
|
|
373
|
+
return groupUnresolvedByLabel(unresolvedTrackers).map((group) => {
|
|
374
|
+
const rowList = group.rowIndexes.join(", ");
|
|
375
|
+
return {
|
|
376
|
+
id: `tracker-name-${group.label}`,
|
|
377
|
+
field: `trackerNameFixes.${group.label}`,
|
|
378
|
+
question: `Type/tracker "${group.label}" is not in Redmine config (${group.nodeIds.length} row(s): ${rowList}). Pick the matching tracker from config.`,
|
|
379
|
+
options: trackerOptions,
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function statusValidationQuestions(unresolvedStatuses, statusOptions) {
|
|
384
|
+
return groupUnresolvedByLabel(unresolvedStatuses).map((group) => {
|
|
385
|
+
const rowList = group.rowIndexes.join(", ");
|
|
386
|
+
return {
|
|
387
|
+
id: `status-name-${group.label}`,
|
|
388
|
+
field: `statusNameFixes.${group.label}`,
|
|
389
|
+
question: `Status "${group.label}" is not in Redmine config (${group.nodeIds.length} row(s): ${rowList}). Pick the matching status from config.`,
|
|
390
|
+
options: statusOptions,
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function assigneeValidationQuestions(invalidAssignees, assigneeOptions) {
|
|
395
|
+
const assigneeChoices = [
|
|
396
|
+
...assigneeOptions,
|
|
397
|
+
ASSIGNEE_UNASSIGNED_OPTION,
|
|
398
|
+
ASSIGNEE_SKIP_OPTION,
|
|
399
|
+
];
|
|
400
|
+
return groupInvalidAssigneesByName(invalidAssignees).map((group) => {
|
|
401
|
+
const rowList = group.rowIndexes.join(", ");
|
|
402
|
+
return {
|
|
403
|
+
id: assigneeFixQuestionId(group.assignedTo),
|
|
404
|
+
field: `assigneeNameFixes.${group.assignedTo}`,
|
|
405
|
+
question: `Assigned To "${group.assignedTo}" is not a project member (${group.nodeIds.length} task row(s): ${rowList}). Pick the correct assignee, leave unassigned, or skip all rows.`,
|
|
406
|
+
options: assigneeChoices,
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async function loadProjectOptions() {
|
|
411
|
+
const projectsData = await getProjects();
|
|
412
|
+
const projects = (projectsData.projects ?? []);
|
|
413
|
+
return {
|
|
414
|
+
projects,
|
|
415
|
+
projectOptions: projects.map((p) => `${p.id} — ${p.name}`),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function pickProjectResponse(params) {
|
|
419
|
+
const { projectOptions } = await loadProjectOptions();
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
needsInput: true,
|
|
423
|
+
question: {
|
|
424
|
+
field: "project",
|
|
425
|
+
question: "Select the Redmine project for this batch *",
|
|
426
|
+
options: projectOptions,
|
|
427
|
+
},
|
|
428
|
+
data: {
|
|
429
|
+
step: "pick_project",
|
|
430
|
+
projectOptions,
|
|
431
|
+
featureTitle: params.rawSheet.featureTitle,
|
|
432
|
+
detectedHeaderRowIndex: params.rawSheet.detectedHeaderRowIndex,
|
|
433
|
+
detectedHeaders: params.rawSheet.detectedHeaders,
|
|
434
|
+
...(params.loaded.sheetSource
|
|
435
|
+
? { sheetSource: params.loaded.sheetSource }
|
|
436
|
+
: {}),
|
|
437
|
+
hint: "STOP: AskQuestion for project first. Retry with project (id or \"id — name\"). Do NOT map columns until project is set.",
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
async function pickTargetVersionResponse(params) {
|
|
442
|
+
let versionOptions = [];
|
|
443
|
+
try {
|
|
444
|
+
versionOptions = await loadProjectVersionOptions(params.projectId);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
versionOptions = [];
|
|
448
|
+
}
|
|
449
|
+
const versionChoices = [...versionOptions, TARGET_VERSION_SKIP_OPTION];
|
|
450
|
+
return {
|
|
451
|
+
success: false,
|
|
452
|
+
needsInput: true,
|
|
453
|
+
question: {
|
|
454
|
+
field: "missingTargetVersionFix",
|
|
455
|
+
question: versionOptions.length > 0
|
|
456
|
+
? "Pick the default target version for this batch. Rows with a Target Version column value use that instead. Choose Skip target version to continue without fixed_version, or pick Other and type a new version name."
|
|
457
|
+
: "This project has no target versions yet. Choose Skip target version to continue without fixed_version, or pick Other and type a new version name to create one.",
|
|
458
|
+
options: versionChoices,
|
|
459
|
+
},
|
|
460
|
+
data: {
|
|
461
|
+
step: "pick_target_version",
|
|
462
|
+
project: params.projectId,
|
|
463
|
+
versionOptions,
|
|
464
|
+
targetVersionSkipOption: TARGET_VERSION_SKIP_OPTION,
|
|
465
|
+
featureTitle: params.rawSheet.featureTitle,
|
|
466
|
+
detectedHeaderRowIndex: params.rawSheet.detectedHeaderRowIndex,
|
|
467
|
+
detectedHeaders: params.rawSheet.detectedHeaders,
|
|
468
|
+
...(params.loaded.sheetSource
|
|
469
|
+
? { sheetSource: params.loaded.sheetSource }
|
|
470
|
+
: {}),
|
|
471
|
+
hint: 'STOP: Always AskQuestion for batch default target version — never infer from missing sheet column. Retry with missingTargetVersionFix: existing "id — name", "Skip target version", or "Create: <name>" when user typed a new name via Other.',
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async function findInvalidAssignees(issues, projectId, skipSet) {
|
|
476
|
+
const numericProjectId = Number(projectId);
|
|
477
|
+
const directory = await loadAssigneeDirectory(Number.isFinite(numericProjectId) ? { projectId: numericProjectId } : {});
|
|
478
|
+
const assigneeOptions = directory.users.map((u) => u.label);
|
|
479
|
+
const invalid = issues.filter((issue) => {
|
|
480
|
+
if (issue.level !== "subtask")
|
|
481
|
+
return false;
|
|
482
|
+
if (issue.existingRedmineId !== undefined)
|
|
483
|
+
return false;
|
|
484
|
+
if (skipSet.has(issue.nodeId))
|
|
485
|
+
return false;
|
|
486
|
+
const assignedTo = issue.assignedTo.trim();
|
|
487
|
+
if (!assignedTo)
|
|
488
|
+
return false;
|
|
489
|
+
return matchAssigneeId(assignedTo, directory.users) === undefined;
|
|
490
|
+
});
|
|
491
|
+
return { invalid, assigneeOptions };
|
|
492
|
+
}
|
|
493
|
+
async function mapColumnsResponse(params) {
|
|
494
|
+
const config = await loadRedmineConfig();
|
|
495
|
+
const suggestedColumnMapping = suggestColumnMappingFromHeaders(params.rawSheet.detectedHeaders, params.rawSheet.detectedHeaderRowIndex);
|
|
496
|
+
const sampleRows = params.rawSheet.rows
|
|
497
|
+
.filter((r) => r.sheetRow > params.rawSheet.detectedHeaderRowIndex + 1)
|
|
498
|
+
.slice(0, 12)
|
|
499
|
+
.map((r) => ({
|
|
500
|
+
sheetRow: r.sheetRow,
|
|
501
|
+
cells: r.cells,
|
|
502
|
+
}));
|
|
503
|
+
return {
|
|
504
|
+
success: false,
|
|
505
|
+
needsInput: true,
|
|
506
|
+
question: {
|
|
507
|
+
field: "columnMapping",
|
|
508
|
+
question: "Use suggestedColumnMapping (or adjust), then retry with columnMapping → confirm_mapping. Required: userStory + subtask.",
|
|
509
|
+
},
|
|
510
|
+
data: {
|
|
511
|
+
step: "map_columns",
|
|
512
|
+
...(params.projectId ? { project: params.projectId } : {}),
|
|
513
|
+
...(params.missingTargetVersionFix !== undefined
|
|
514
|
+
? { missingTargetVersionFix: params.missingTargetVersionFix }
|
|
515
|
+
: {}),
|
|
516
|
+
...(params.createdTargetVersion
|
|
517
|
+
? { createdTargetVersion: params.createdTargetVersion }
|
|
518
|
+
: {}),
|
|
519
|
+
featureTitle: params.rawSheet.featureTitle,
|
|
520
|
+
detectedHeaderRowIndex: params.rawSheet.detectedHeaderRowIndex,
|
|
521
|
+
detectedHeaders: params.rawSheet.detectedHeaders,
|
|
522
|
+
suggestedColumnMapping,
|
|
523
|
+
fieldSchema: BATCH_COLUMN_FIELD_SCHEMA,
|
|
524
|
+
stepsToReproduceRule: STEPS_TO_REPRODUCE_RULE,
|
|
525
|
+
sampleRows,
|
|
526
|
+
totalDataRows: params.rawSheet.rows.length -
|
|
527
|
+
params.rawSheet.detectedHeaderRowIndex -
|
|
528
|
+
1,
|
|
529
|
+
...(params.loaded.sheetSource
|
|
530
|
+
? { sheetSource: params.loaded.sheetSource }
|
|
531
|
+
: {}),
|
|
532
|
+
trackerOptions: enumerationOptions(config.trackers),
|
|
533
|
+
priorityOptions: enumerationOptions(config.priorities),
|
|
534
|
+
trackerAliases: config.trackerAliases,
|
|
535
|
+
priorityAliases: config.priorityAliases,
|
|
536
|
+
stepsToReproduceTrackers: [...STEPS_TO_REPRODUCE_TRACKER_NAMES],
|
|
537
|
+
hint: "STOP: Use data.suggestedColumnMapping as columnMapping (User Stories→userStory, Tasks→subtask). If ambiguousRoles is non-empty, AskQuestion. Then confirm_mapping.",
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function confirmMappingResponse(params) {
|
|
542
|
+
const headers = params.rawSheet.rows[params.mapping.headerRowIndex]?.cells ??
|
|
543
|
+
params.rawSheet.detectedHeaders;
|
|
544
|
+
const mappingPreview = buildMappingPreview(params.mapping, headers);
|
|
545
|
+
const questions = [
|
|
546
|
+
{
|
|
547
|
+
id: "confirm-column-mapping",
|
|
548
|
+
field: "confirmMappingAction",
|
|
549
|
+
question: "Use this column mapping to parse the sheet and build the issue preview?",
|
|
550
|
+
options: ["proceed with this mapping", "change mapping"],
|
|
551
|
+
},
|
|
552
|
+
];
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
needsInput: true,
|
|
556
|
+
questions,
|
|
557
|
+
data: {
|
|
558
|
+
batchMode: true,
|
|
559
|
+
step: "confirm_mapping",
|
|
560
|
+
columnMapping: params.mapping,
|
|
561
|
+
mappingPreview,
|
|
562
|
+
detectedHeaderRowIndex: params.mapping.headerRowIndex,
|
|
563
|
+
detectedHeaders: headers,
|
|
564
|
+
fieldSchema: BATCH_COLUMN_FIELD_SCHEMA,
|
|
565
|
+
stepsToReproduceRule: STEPS_TO_REPRODUCE_RULE,
|
|
566
|
+
featureTitle: params.rawSheet.featureTitle,
|
|
567
|
+
...(params.loaded.sheetSource
|
|
568
|
+
? { sheetSource: params.loaded.sheetSource }
|
|
569
|
+
: {}),
|
|
570
|
+
hint: 'STOP: Post mappingPreview (includes Steps to Reproduce sources). ONE AskQuestion proceed/change. Never map a Steps to Reproduce column — use comments/description roles per stepsToReproduceRule.',
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function fixColumnMappingResponse(params) {
|
|
575
|
+
const questions = buildColumnMappingQuestions(params.validation);
|
|
576
|
+
const mappingProblems = params.validation.problems.map((p) => ({
|
|
577
|
+
role: p.role,
|
|
578
|
+
issue: p.issue,
|
|
579
|
+
...(p.spec !== undefined ? { spec: p.spec } : {}),
|
|
580
|
+
}));
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
needsInput: true,
|
|
584
|
+
questions,
|
|
585
|
+
data: {
|
|
586
|
+
batchMode: true,
|
|
587
|
+
step: "fix_column_mapping",
|
|
588
|
+
mappingProblems,
|
|
589
|
+
detectedHeaders: params.validation.detectedHeaders,
|
|
590
|
+
headerRowIndex: params.validation.headerRowIndex,
|
|
591
|
+
fieldSchema: BATCH_COLUMN_FIELD_SCHEMA,
|
|
592
|
+
stepsToReproduceRule: STEPS_TO_REPRODUCE_RULE,
|
|
593
|
+
...(params.mapping ? { columnMapping: params.mapping } : {}),
|
|
594
|
+
...(params.parseWarnings?.length
|
|
595
|
+
? { parseWarnings: params.parseWarnings }
|
|
596
|
+
: {}),
|
|
597
|
+
...(params.loaded.sheetSource ? { sheetSource: params.loaded.sheetSource } : {}),
|
|
598
|
+
hint: params.hint ??
|
|
599
|
+
"STOP: ONE AskQuestion for all questions[]. Map userStory + subtask required. stepsToReproduceRule — map comments/description; no separate Steps to Reproduce column.",
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
async function loadCsvContent(input) {
|
|
604
|
+
const sheetSource = input.sheetSource;
|
|
605
|
+
if (input.csvContent?.trim()) {
|
|
606
|
+
return {
|
|
607
|
+
content: input.csvContent,
|
|
608
|
+
...(sheetSource ? { sheetSource } : {}),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (input.csvPath?.trim()) {
|
|
612
|
+
try {
|
|
613
|
+
const path = resolve(input.csvPath.trim());
|
|
614
|
+
const content = await readFile(path, "utf8");
|
|
615
|
+
return {
|
|
616
|
+
content,
|
|
617
|
+
...(sheetSource ? { sheetSource } : {}),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
return { error: `Could not read csvPath: ${input.csvPath}` };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
error: "Provide csvContent (from redmine_agent_sheet_to_csv) or csvPath",
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
export async function batchCreateIssuesWorkflow(input) {
|
|
629
|
+
const status = input.status?.trim() || DEFAULT_STATUS;
|
|
630
|
+
const priority = input.priority?.trim() || DEFAULT_PRIORITY;
|
|
631
|
+
const skipSet = new Set(input.skipNodeIds ?? []);
|
|
632
|
+
const loaded = await loadCsvContent(input);
|
|
633
|
+
if (loaded.error || !loaded.content) {
|
|
634
|
+
return { success: false, error: loaded.error ?? "No CSV content" };
|
|
635
|
+
}
|
|
636
|
+
const rawSheet = extractRawBatchSheet(loaded.content);
|
|
637
|
+
if (!input.project?.trim()) {
|
|
638
|
+
return pickProjectResponse({ rawSheet, loaded });
|
|
639
|
+
}
|
|
640
|
+
const projectId = parseProjectSelection(input.project);
|
|
641
|
+
if (!projectId) {
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
error: `Invalid project selection: ${input.project}`,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (input.missingTargetVersionFix === undefined) {
|
|
648
|
+
return pickTargetVersionResponse({ projectId, rawSheet, loaded });
|
|
649
|
+
}
|
|
650
|
+
const versionResolve = await resolveMissingTargetVersionFix({
|
|
651
|
+
projectId,
|
|
652
|
+
fix: input.missingTargetVersionFix,
|
|
653
|
+
});
|
|
654
|
+
if (!versionResolve.ok) {
|
|
655
|
+
return { success: false, error: versionResolve.error };
|
|
656
|
+
}
|
|
657
|
+
const resolvedTargetVersionFix = versionResolve.resolved;
|
|
658
|
+
if (input.sheetParseMode === "structure_only" || !input.columnMapping) {
|
|
659
|
+
return await mapColumnsResponse({
|
|
660
|
+
rawSheet,
|
|
661
|
+
loaded,
|
|
662
|
+
projectId,
|
|
663
|
+
missingTargetVersionFix: resolvedTargetVersionFix,
|
|
664
|
+
...(versionResolve.created
|
|
665
|
+
? { createdTargetVersion: versionResolve.created }
|
|
666
|
+
: {}),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const mappingValidation = validateColumnMapping(rawSheet, input.columnMapping);
|
|
670
|
+
if (!mappingValidation.valid) {
|
|
671
|
+
return fixColumnMappingResponse({
|
|
672
|
+
rawSheet,
|
|
673
|
+
mapping: input.columnMapping,
|
|
674
|
+
validation: mappingValidation,
|
|
675
|
+
loaded,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if (input.confirmMappingAction !== "proceed") {
|
|
679
|
+
return confirmMappingResponse({
|
|
680
|
+
rawSheet,
|
|
681
|
+
mapping: input.columnMapping,
|
|
682
|
+
loaded,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const parsed = parseBatchWithColumnMapping(rawSheet, input.columnMapping);
|
|
686
|
+
if (parsed.headerRowIndex < 0 || parsed.issues.length === 0) {
|
|
687
|
+
const emptyParseProblems = [
|
|
688
|
+
...mappingValidation.problems,
|
|
689
|
+
{
|
|
690
|
+
role: "userStory",
|
|
691
|
+
issue: parsed.parseWarnings[0] ??
|
|
692
|
+
"No User Stories or Tasks parsed — check userStory and subtask columns match data rows.",
|
|
693
|
+
},
|
|
694
|
+
];
|
|
695
|
+
return fixColumnMappingResponse({
|
|
696
|
+
rawSheet,
|
|
697
|
+
mapping: input.columnMapping,
|
|
698
|
+
validation: {
|
|
699
|
+
...mappingValidation,
|
|
700
|
+
valid: false,
|
|
701
|
+
problems: emptyParseProblems,
|
|
702
|
+
},
|
|
703
|
+
loaded,
|
|
704
|
+
parseWarnings: parsed.parseWarnings,
|
|
705
|
+
hint: "STOP: Mapping resolved but no issues were parsed. ONE AskQuestion for all questions[], then retry with corrected columnMapping.",
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
let issues = applyRowFixes(parsed.issues, input.rowFixes);
|
|
709
|
+
const missingTargetVersionsEarly = findMissingTargetVersionStories(issues, skipSet);
|
|
710
|
+
issues = applyMissingTargetVersionFix(issues, resolvedTargetVersionFix, missingTargetVersionsEarly);
|
|
711
|
+
issues = applyAssigneeNameFixes(issues, input.assigneeNameFixes, skipSet);
|
|
712
|
+
issues = applyPriorityNameFixes(issues, input.priorityNameFixes);
|
|
713
|
+
issues = applyTrackerNameFixes(issues, input.trackerNameFixes);
|
|
714
|
+
issues = applyStatusNameFixes(issues, input.statusNameFixes);
|
|
715
|
+
issues = applyExistingNodeIds(issues, input.existingNodeIds);
|
|
716
|
+
issues = propagateAssigneeToUserStories(issues, skipSet);
|
|
717
|
+
const config = await loadRedmineConfig();
|
|
718
|
+
const configEnrichment = enrichBatchIssuesFromConfig(issues, config, {
|
|
719
|
+
priority,
|
|
720
|
+
status,
|
|
721
|
+
});
|
|
722
|
+
issues = configEnrichment.issues;
|
|
723
|
+
if (configEnrichment.warnings.length > 0) {
|
|
724
|
+
parsed.parseWarnings.push(...configEnrichment.warnings);
|
|
725
|
+
}
|
|
726
|
+
if (configEnrichment.unresolvedTrackers.length > 0 ||
|
|
727
|
+
configEnrichment.unresolvedPriorities.length > 0 ||
|
|
728
|
+
configEnrichment.unresolvedStatuses.length > 0) {
|
|
729
|
+
const trackerOptions = config.trackers.map((t) => `${t.id} — ${t.name}`);
|
|
730
|
+
const priorityOptions = config.priorities.map((p) => `${p.id} — ${p.name}`);
|
|
731
|
+
const statusOptions = config.statuses.map((s) => `${s.id} — ${s.name}`);
|
|
732
|
+
const questions = [
|
|
733
|
+
...priorityValidationQuestions(configEnrichment.unresolvedPriorities, priorityOptions),
|
|
734
|
+
...trackerValidationQuestions(configEnrichment.unresolvedTrackers, trackerOptions),
|
|
735
|
+
...statusValidationQuestions(configEnrichment.unresolvedStatuses, statusOptions),
|
|
736
|
+
];
|
|
737
|
+
return {
|
|
738
|
+
success: false,
|
|
739
|
+
needsInput: true,
|
|
740
|
+
...(questions.length > 0 ? { questions } : {}),
|
|
741
|
+
error: "Some sheet Type, Priority, or Status values do not match Redmine config.",
|
|
742
|
+
data: {
|
|
743
|
+
step: "fix_batch_gaps",
|
|
744
|
+
batchMode: true,
|
|
745
|
+
format: parsed.format,
|
|
746
|
+
parseWarnings: parsed.parseWarnings,
|
|
747
|
+
unresolvedTrackers: configEnrichment.unresolvedTrackers,
|
|
748
|
+
unresolvedPriorities: configEnrichment.unresolvedPriorities,
|
|
749
|
+
unresolvedStatuses: configEnrichment.unresolvedStatuses,
|
|
750
|
+
trackerOptions,
|
|
751
|
+
priorityOptions,
|
|
752
|
+
statusOptions,
|
|
753
|
+
priorityAliases: config.priorityAliases,
|
|
754
|
+
trackerAliases: config.trackerAliases,
|
|
755
|
+
statusAliases: config.statusAliases,
|
|
756
|
+
hint: "STOP: ONE AskQuestion for all questions[]. Retry with priorityNameFixes / trackerNameFixes / statusNameFixes from config options. Do NOT search repo files.",
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const { alreadyOnRedmine, toCreate: toCreateNew } = partitionIssues(issues, skipSet);
|
|
761
|
+
const { projects } = await loadProjectOptions();
|
|
762
|
+
const needsSubjectFix = issues.some((i) => !i.subject.trim());
|
|
763
|
+
const assigneeCheck = await findInvalidAssignees(issues, projectId, skipSet);
|
|
764
|
+
const invalidAssignees = assigneeCheck.invalid;
|
|
765
|
+
const assigneeOptions = assigneeCheck.assigneeOptions;
|
|
766
|
+
if (needsSubjectFix) {
|
|
767
|
+
const questions = emptySubjectQuestions(issues);
|
|
768
|
+
if (questions.length > 0) {
|
|
769
|
+
return {
|
|
770
|
+
success: false,
|
|
771
|
+
needsInput: true,
|
|
772
|
+
questions,
|
|
773
|
+
data: {
|
|
774
|
+
batchMode: true,
|
|
775
|
+
step: "fix_batch_gaps",
|
|
776
|
+
format: parsed.format,
|
|
777
|
+
parseWarnings: parsed.parseWarnings,
|
|
778
|
+
scopeNotes: parsed.scopeNotes,
|
|
779
|
+
issueCount: toCreateNew.length,
|
|
780
|
+
alreadyOnRedmineCount: alreadyOnRedmine.length,
|
|
781
|
+
trackerMapping: getTrackerMapping(),
|
|
782
|
+
featureTitle: parsed.featureTitle ?? null,
|
|
783
|
+
...(loaded.sheetSource ? { sheetSource: loaded.sheetSource } : {}),
|
|
784
|
+
hint: "Answer all questions in one AskQuestion, then retry with rowFixes.",
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (invalidAssignees.length > 0) {
|
|
790
|
+
const questions = assigneeValidationQuestions(invalidAssignees, assigneeOptions);
|
|
791
|
+
return {
|
|
792
|
+
success: false,
|
|
793
|
+
needsInput: true,
|
|
794
|
+
questions,
|
|
795
|
+
data: {
|
|
796
|
+
batchMode: true,
|
|
797
|
+
step: "fix_assignees",
|
|
798
|
+
format: parsed.format,
|
|
799
|
+
parseWarnings: parsed.parseWarnings,
|
|
800
|
+
scopeNotes: parsed.scopeNotes,
|
|
801
|
+
issueCount: toCreateNew.length,
|
|
802
|
+
alreadyOnRedmineCount: alreadyOnRedmine.length,
|
|
803
|
+
trackerMapping: getTrackerMapping(),
|
|
804
|
+
featureTitle: parsed.featureTitle ?? null,
|
|
805
|
+
project: projectId,
|
|
806
|
+
...(loaded.sheetSource ? { sheetSource: loaded.sheetSource } : {}),
|
|
807
|
+
invalidAssigneeGroups: groupInvalidAssigneesByName(invalidAssignees),
|
|
808
|
+
assigneeOptions,
|
|
809
|
+
hint: "STOP: ONE AskQuestion for all assignee questions. Retry with assigneeNameFixes per invalidAssigneeGroups name. Do NOT auto-guess.",
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (projectId) {
|
|
814
|
+
const directory = await loadAssigneeDirectory({
|
|
815
|
+
projectId: Number(projectId),
|
|
816
|
+
});
|
|
817
|
+
issues = propagateAssigneeToUserStories(issues, skipSet, directory.users);
|
|
818
|
+
}
|
|
819
|
+
const previewRows = buildPreviewRows(issues, skipSet);
|
|
820
|
+
const toCreate = toCreateNew;
|
|
821
|
+
const fullPreviewText = buildPreviewText(previewRows);
|
|
822
|
+
const previewSummary = buildPreviewSummary(previewRows, toCreate.length, skipSet.size, alreadyOnRedmine.length, parsed.featureTitle);
|
|
823
|
+
const projectName = projects.find((p) => String(p.id) === projectId)?.name;
|
|
824
|
+
const previewFormatOptions = {
|
|
825
|
+
project: projectId,
|
|
826
|
+
...(projectName ? { projectName } : {}),
|
|
827
|
+
status,
|
|
828
|
+
priority,
|
|
829
|
+
...(parsed.featureTitle ? { featureTitle: parsed.featureTitle } : {}),
|
|
830
|
+
};
|
|
831
|
+
const activeStories = previewRows.filter((r) => r.level === "userStory" && !r.skipped);
|
|
832
|
+
const previewOverview = buildPreviewOverview(previewRows, previewFormatOptions, {
|
|
833
|
+
toCreateCount: toCreate.length,
|
|
834
|
+
newStories: activeStories.filter((r) => !r.onRedmine).length,
|
|
835
|
+
newTasks: previewRows.filter((r) => r.level === "subtask" && !r.skipped && !r.onRedmine).length,
|
|
836
|
+
alreadyOnRedmine: alreadyOnRedmine.length,
|
|
837
|
+
skipped: skipSet.size,
|
|
838
|
+
});
|
|
839
|
+
const previewDiagram = buildPreviewMermaidDiagram(previewRows, previewFormatOptions);
|
|
840
|
+
const previewTree = buildPreviewTree(previewRows, previewFormatOptions);
|
|
841
|
+
const previewIsLong = isPreviewLong(fullPreviewText, toCreate.length);
|
|
842
|
+
const previewDataBase = {
|
|
843
|
+
project: projectId,
|
|
844
|
+
status,
|
|
845
|
+
priority,
|
|
846
|
+
format: parsed.format,
|
|
847
|
+
featureTitle: parsed.featureTitle ?? null,
|
|
848
|
+
...(loaded.sheetSource ? { sheetSource: loaded.sheetSource } : {}),
|
|
849
|
+
scopeNotes: parsed.scopeNotes,
|
|
850
|
+
parseWarnings: parsed.parseWarnings,
|
|
851
|
+
previewSummary,
|
|
852
|
+
previewOverview,
|
|
853
|
+
previewDiagram,
|
|
854
|
+
previewTree,
|
|
855
|
+
previewText: fullPreviewText,
|
|
856
|
+
previewIsLong,
|
|
857
|
+
previewRows,
|
|
858
|
+
issueCount: toCreate.length,
|
|
859
|
+
alreadyOnRedmineCount: alreadyOnRedmine.length,
|
|
860
|
+
skippedCount: skipSet.size,
|
|
861
|
+
trackerMapping: getTrackerMapping(),
|
|
862
|
+
trackerOptions: enumerationOptions(config.trackers),
|
|
863
|
+
trackerAliases: config.trackerAliases,
|
|
864
|
+
stepsToReproduceTrackers: [...STEPS_TO_REPRODUCE_TRACKER_NAMES],
|
|
865
|
+
stepsToReproduceRule: STEPS_TO_REPRODUCE_RULE,
|
|
866
|
+
};
|
|
867
|
+
if (input.previewAction === "cancel") {
|
|
868
|
+
return {
|
|
869
|
+
success: false,
|
|
870
|
+
error: "Batch issue creation cancelled by user",
|
|
871
|
+
data: { step: "cancelled", ...previewDataBase },
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const wantsUpload = input.confirmSave === "yes" || input.previewAction === "proceed";
|
|
875
|
+
if (toCreate.length === 0) {
|
|
876
|
+
return {
|
|
877
|
+
success: true,
|
|
878
|
+
data: {
|
|
879
|
+
uploaded: false,
|
|
880
|
+
message: "Nothing to create — all rows already on Redmine (Task ID) or linked via existingNodeIds.",
|
|
881
|
+
...previewDataBase,
|
|
882
|
+
alreadyOnRedmine: alreadyOnRedmine.map((i) => ({
|
|
883
|
+
nodeId: i.nodeId,
|
|
884
|
+
rowIndex: i.rowIndex,
|
|
885
|
+
level: i.level,
|
|
886
|
+
issueId: i.existingRedmineId,
|
|
887
|
+
subject: i.subject,
|
|
888
|
+
})),
|
|
889
|
+
},
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (!wantsUpload) {
|
|
893
|
+
if (input.previewAction === "show_preview") {
|
|
894
|
+
return {
|
|
895
|
+
success: false,
|
|
896
|
+
needsInput: true,
|
|
897
|
+
question: {
|
|
898
|
+
field: "previewAction",
|
|
899
|
+
question: `Full preview (${toCreate.length} issues). Proceed to post to Redmine or cancel?`,
|
|
900
|
+
options: ["proceed", "cancel"],
|
|
901
|
+
},
|
|
902
|
+
data: {
|
|
903
|
+
step: "full_preview",
|
|
904
|
+
batchMode: false,
|
|
905
|
+
...previewDataBase,
|
|
906
|
+
hint: "STOP: User must confirm in chat. AskQuestion proceed | cancel only. Do NOT set previewAction: proceed until they choose proceed.",
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
const shortPreviewIntro = previewIsLong
|
|
911
|
+
? `Batch preview ready (${toCreate.length} issues). Summary below — choose an action.`
|
|
912
|
+
: `Batch preview (${toCreate.length} issues):`;
|
|
913
|
+
return {
|
|
914
|
+
success: false,
|
|
915
|
+
needsInput: true,
|
|
916
|
+
question: {
|
|
917
|
+
field: "previewAction",
|
|
918
|
+
question: previewIsLong
|
|
919
|
+
? `${shortPreviewIntro}\n\n${previewSummary}\n\nShow full line-by-line preview, post to Redmine, or cancel?`
|
|
920
|
+
: `${shortPreviewIntro}\n\n${previewSummary}\n\n${fullPreviewText}\n\nPost to Redmine or cancel?`,
|
|
921
|
+
options: previewIsLong
|
|
922
|
+
? ["show_preview", "proceed", "cancel"]
|
|
923
|
+
: ["proceed", "cancel"],
|
|
924
|
+
},
|
|
925
|
+
data: {
|
|
926
|
+
step: "preview_gate",
|
|
927
|
+
batchMode: false,
|
|
928
|
+
...previewDataBase,
|
|
929
|
+
hint: "STOP: Post in chat in order: (1) data.previewOverview, (2) data.previewDiagram as mermaid, (3) data.previewTree. Then AskQuestion yes/no. Do NOT set previewAction: proceed on this call.",
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
if (input.confirmSave === "no") {
|
|
934
|
+
return {
|
|
935
|
+
success: false,
|
|
936
|
+
error: "Batch issue creation cancelled by user",
|
|
937
|
+
data: { step: "cancelled", ...previewDataBase },
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
if (!projectId) {
|
|
941
|
+
return {
|
|
942
|
+
success: false,
|
|
943
|
+
error: "Project is required before upload",
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const createdByNodeId = new Map();
|
|
947
|
+
const ordered = sortIssuesForUpload(issues.filter((i) => !skipSet.has(i.nodeId)));
|
|
948
|
+
for (const issue of ordered) {
|
|
949
|
+
if (issue.existingRedmineId !== undefined) {
|
|
950
|
+
createdByNodeId.set(issue.nodeId, issue.existingRedmineId);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const created = [];
|
|
954
|
+
const failed = [];
|
|
955
|
+
const allWarnings = [...parsed.parseWarnings];
|
|
956
|
+
for (const issue of toCreate) {
|
|
957
|
+
let parentIssueId;
|
|
958
|
+
if (issue.parentNodeId) {
|
|
959
|
+
parentIssueId = createdByNodeId.get(issue.parentNodeId);
|
|
960
|
+
if (!parentIssueId) {
|
|
961
|
+
failed.push({
|
|
962
|
+
nodeId: issue.nodeId,
|
|
963
|
+
rowIndex: issue.rowIndex,
|
|
964
|
+
error: `Parent ${issue.parentNodeId} was not created (missing or failed earlier)`,
|
|
965
|
+
});
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const result = await createRedmineIssueFromBatch({
|
|
970
|
+
projectId,
|
|
971
|
+
...(issue.trackerId !== undefined
|
|
972
|
+
? { trackerId: issue.trackerId }
|
|
973
|
+
: { trackerName: issue.tracker }),
|
|
974
|
+
subject: issue.subject,
|
|
975
|
+
...(issue.description ? { description: issue.description } : {}),
|
|
976
|
+
...(issue.comments ? { comments: issue.comments } : {}),
|
|
977
|
+
statusName: issue.status ?? status,
|
|
978
|
+
...(issue.priorityId !== undefined
|
|
979
|
+
? {
|
|
980
|
+
priorityId: issue.priorityId,
|
|
981
|
+
priorityName: issue.priority ?? priority,
|
|
982
|
+
}
|
|
983
|
+
: { priorityName: issue.priority ?? priority }),
|
|
984
|
+
...(issue.assignedTo ? { assignedTo: issue.assignedTo } : {}),
|
|
985
|
+
...(issue.targetVersion
|
|
986
|
+
? { targetVersion: issue.targetVersion }
|
|
987
|
+
: {}),
|
|
988
|
+
...(parentIssueId !== undefined ? { parentIssueId } : {}),
|
|
989
|
+
...(issue.estimatedHours !== undefined
|
|
990
|
+
? { estimatedHours: issue.estimatedHours }
|
|
991
|
+
: {}),
|
|
992
|
+
});
|
|
993
|
+
if (!result.success) {
|
|
994
|
+
failed.push({
|
|
995
|
+
nodeId: issue.nodeId,
|
|
996
|
+
rowIndex: issue.rowIndex,
|
|
997
|
+
error: result.error,
|
|
998
|
+
});
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
createdByNodeId.set(issue.nodeId, result.issueId);
|
|
1002
|
+
allWarnings.push(...result.warnings);
|
|
1003
|
+
created.push({
|
|
1004
|
+
nodeId: issue.nodeId,
|
|
1005
|
+
rowIndex: issue.rowIndex,
|
|
1006
|
+
issueId: result.issueId,
|
|
1007
|
+
issueUrl: result.issueUrl,
|
|
1008
|
+
subject: issue.subject,
|
|
1009
|
+
tracker: result.actualTracker?.name ?? issue.tracker,
|
|
1010
|
+
level: issue.level,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
const headerRow = rawSheet.rows.find((r) => r.sheetRow === parsed.headerRowIndex)?.cells ??
|
|
1014
|
+
[];
|
|
1015
|
+
const idColumn = resolveTaskIdColumnForWriteBack(headerRow, parsed.sheetColumns);
|
|
1016
|
+
const compositeTicketColumn = parsed.sheetColumns !== undefined &&
|
|
1017
|
+
usesCompositeTicketColumn(parsed.sheetColumns);
|
|
1018
|
+
const featureCreated = created.find((entry) => entry.level === "feature");
|
|
1019
|
+
const sheetMatrix = rawSheet.rows.map((row) => row.cells);
|
|
1020
|
+
const featureWriteBack = featureCreated && parsed.featureTitle
|
|
1021
|
+
? (() => {
|
|
1022
|
+
const headerRow0 = parsed.headerRowIndex - 1;
|
|
1023
|
+
const featureRow = sheetMatrix[featureCreated.rowIndex - 1] ?? [];
|
|
1024
|
+
const titleCol = findFeatureTitleColumnIndex(featureRow);
|
|
1025
|
+
const columnIndex = titleCol >= 0 ? titleCol : 0;
|
|
1026
|
+
return {
|
|
1027
|
+
required: true,
|
|
1028
|
+
rowIndex: featureCreated.rowIndex,
|
|
1029
|
+
columnIndex,
|
|
1030
|
+
columnLetter: columnIndexToLetter(columnIndex),
|
|
1031
|
+
issueId: featureCreated.issueId,
|
|
1032
|
+
issueUrl: featureCreated.issueUrl,
|
|
1033
|
+
title: parsed.featureTitle,
|
|
1034
|
+
formulaTemplate: '=HYPERLINK("{issueUrl}","Feature: {title} #{issueId}")',
|
|
1035
|
+
hint: "MANDATORY Step 9b: write Feature HYPERLINK in the feature title cell (optional title row above header — not a mapped column). Do this before Ticket/MainTicket write-back.",
|
|
1036
|
+
};
|
|
1037
|
+
})()
|
|
1038
|
+
: undefined;
|
|
1039
|
+
const writeBackHints = loaded.sheetSource && parsed.sheetColumns
|
|
1040
|
+
? {
|
|
1041
|
+
sheetSource: loaded.sheetSource,
|
|
1042
|
+
headerRowIndex: parsed.headerRowIndex,
|
|
1043
|
+
idColumnIndex: idColumn.columnIndex,
|
|
1044
|
+
idColumnLetter: columnIndexToLetter(idColumn.columnIndex),
|
|
1045
|
+
appendColumnIfMissing: idColumn.createdColumn,
|
|
1046
|
+
idColumnHeader: TASK_ID_COLUMN_HEADER,
|
|
1047
|
+
compositeTicketColumn,
|
|
1048
|
+
formulaTemplate: compositeTicketColumn
|
|
1049
|
+
? '=HYPERLINK("{issueUrl}","Ticket:#{issueId}") for tasks; =HYPERLINK("{issueUrl}","MainTicket:#{issueId}") for user stories; same row: =HYPERLINK(url1,"Ticket:#id") & " " & HYPERLINK(url2,"MainTicket:#id") (Ticket first)'
|
|
1050
|
+
: '=HYPERLINK("{issueUrl}","#{issueId}")',
|
|
1051
|
+
unmergeBeforeWriteBack: true,
|
|
1052
|
+
unmergeHint: "Step 9a: get_sheet_data(include_grid_data:true) → batch_update unmergeCells on id column (idColumnIndex) from first data row through max(created.rowIndex). Then batch_update_cells.",
|
|
1053
|
+
levelPriority: ["subtask", "userStory", "feature"],
|
|
1054
|
+
ticketCellRule: "Ticket label = Task · MainTicket label = User Story · same row: Ticket:… MainTicket:…",
|
|
1055
|
+
...(featureWriteBack ? { featureWriteBack } : {}),
|
|
1056
|
+
}
|
|
1057
|
+
: undefined;
|
|
1058
|
+
return {
|
|
1059
|
+
success: failed.length === 0,
|
|
1060
|
+
...(failed.length > 0
|
|
1061
|
+
? {
|
|
1062
|
+
error: `${failed.length} issue(s) failed; ${created.length} created`,
|
|
1063
|
+
}
|
|
1064
|
+
: {}),
|
|
1065
|
+
data: {
|
|
1066
|
+
uploaded: true,
|
|
1067
|
+
project: projectId,
|
|
1068
|
+
created,
|
|
1069
|
+
alreadyOnRedmine: alreadyOnRedmine.map((i) => ({
|
|
1070
|
+
nodeId: i.nodeId,
|
|
1071
|
+
rowIndex: i.rowIndex,
|
|
1072
|
+
level: i.level,
|
|
1073
|
+
issueId: i.existingRedmineId,
|
|
1074
|
+
subject: i.subject,
|
|
1075
|
+
})),
|
|
1076
|
+
failed,
|
|
1077
|
+
skipped: [...skipSet],
|
|
1078
|
+
...(writeBackHints ? { writeBackHints } : {}),
|
|
1079
|
+
warnings: allWarnings,
|
|
1080
|
+
format: parsed.format,
|
|
1081
|
+
trackerMapping: getTrackerMapping(),
|
|
1082
|
+
},
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function getTrackerMapping() {
|
|
1086
|
+
return {
|
|
1087
|
+
featureRow: 'Title row "Feature: …" above header → Redmine Feature (parent of all stories)',
|
|
1088
|
+
userStoryColumn: "userStory column → User Story under Feature; first line = subject, rest = description",
|
|
1089
|
+
subtaskColumn: "subtask column → Task (or Type override e.g. Bug) under User Story",
|
|
1090
|
+
descriptionColumn: "description column → task body when subtask present",
|
|
1091
|
+
hoursColumn: "estimated_hours on User Story or subtask row",
|
|
1092
|
+
targetVersionColumn: "fixed_version on User Story (applies until next story row)",
|
|
1093
|
+
assignedToColumn: "assigned_to on subtask rows (blank if empty)",
|
|
1094
|
+
sheetTypeColumn: "Type column → resolved via live Redmine config trackers + trackerAliases (e.g. Bug, CR→Change Request); preview/upload use id — name from config",
|
|
1095
|
+
stepsToReproduceColumn: "Bug/Task only — derived from mapped roles (see stepsToReproduceRule): comments → description → issue subject → \"...\". Not a sheet column.",
|
|
1096
|
+
taskIdColumn: "Task ID: single HYPERLINK (#id). Ticket column: Ticket:#id (Task), MainTicket:#id (User Story); same-row story+task → both labels; task-only rows → Ticket:#id only",
|
|
1097
|
+
commentsColumn: "appended to issue description",
|
|
1098
|
+
googleSheet: "sheetSource from redmine_agent_sheet_to_csv: agent writes Task ID HYPERLINKs via Google Sheets MCP (see batch skill Step 7)",
|
|
1099
|
+
columnMapping: "Agent maps sheet headers to roles via columnMapping — no layout-specific parser",
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
//# sourceMappingURL=batchCreateIssuesWorkflow.js.map
|