buddy-workbench 0.1.70 → 0.1.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,6 +54,131 @@ async function getJiraIssueSummary(issueKey) {
54
54
  return summary;
55
55
  }
56
56
 
57
+ function transformFieldValue(val) {
58
+ if (val === null || val === undefined) return undefined;
59
+
60
+ if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
61
+ return val;
62
+ }
63
+
64
+ if (Array.isArray(val)) {
65
+ if (val.length === 0) return undefined;
66
+ const items = val.map((item) => transformFieldValue(item)).filter((item) => item !== undefined);
67
+ return items.length > 0 ? items : undefined;
68
+ }
69
+
70
+ if (typeof val === 'object') {
71
+ const keys = Object.keys(val);
72
+ if (keys.length === 0) return undefined;
73
+
74
+ // Support cascading select (parent value + child value)
75
+ if (val.value !== undefined && val.child && typeof val.child === 'object') {
76
+ const childVal = transformFieldValue(val.child);
77
+ if (childVal) {
78
+ return { value: String(val.value), child: childVal };
79
+ }
80
+ }
81
+
82
+ if (val.id !== undefined && val.id !== null) {
83
+ return { id: String(val.id) };
84
+ }
85
+ if (val.accountId !== undefined && val.accountId !== null) {
86
+ return { accountId: String(val.accountId) };
87
+ }
88
+ if (val.value !== undefined && val.value !== null) {
89
+ return { value: String(val.value) };
90
+ }
91
+ if (val.key !== undefined && val.key !== null) {
92
+ return { key: String(val.key) };
93
+ }
94
+ if (val.name !== undefined && val.name !== null) {
95
+ return { name: String(val.name) };
96
+ }
97
+ }
98
+
99
+ return undefined;
100
+ }
101
+
102
+ function extractCloneableFields(origFields) {
103
+ if (!origFields || typeof origFields !== 'object') return {};
104
+
105
+ const fields = {};
106
+
107
+ // Standard fields
108
+ if (origFields.description !== undefined && origFields.description !== null) {
109
+ fields.description = origFields.description;
110
+ }
111
+
112
+ if (origFields.priority) {
113
+ if (origFields.priority.id) {
114
+ fields.priority = { id: String(origFields.priority.id) };
115
+ } else if (origFields.priority.name) {
116
+ fields.priority = { name: String(origFields.priority.name) };
117
+ }
118
+ }
119
+
120
+ if (Array.isArray(origFields.components) && origFields.components.length > 0) {
121
+ const components = origFields.components
122
+ .map((c) => (c.id ? { id: String(c.id) } : c.name ? { name: String(c.name) } : null))
123
+ .filter(Boolean);
124
+ if (components.length > 0) fields.components = components;
125
+ }
126
+
127
+ if (Array.isArray(origFields.labels) && origFields.labels.length > 0) {
128
+ const labels = origFields.labels.map((l) => String(l)).filter(Boolean);
129
+ if (labels.length > 0) fields.labels = labels;
130
+ }
131
+
132
+ if (Array.isArray(origFields.fixVersions) && origFields.fixVersions.length > 0) {
133
+ const fixVersions = origFields.fixVersions
134
+ .map((v) => (v.id ? { id: String(v.id) } : v.name ? { name: String(v.name) } : null))
135
+ .filter(Boolean);
136
+ if (fixVersions.length > 0) fields.fixVersions = fixVersions;
137
+ }
138
+
139
+ if (Array.isArray(origFields.versions) && origFields.versions.length > 0) {
140
+ const versions = origFields.versions
141
+ .map((v) => (v.id ? { id: String(v.id) } : v.name ? { name: String(v.name) } : null))
142
+ .filter(Boolean);
143
+ if (versions.length > 0) fields.versions = versions;
144
+ }
145
+
146
+ if (typeof origFields.environment === 'string' && origFields.environment) {
147
+ fields.environment = origFields.environment;
148
+ }
149
+
150
+ if (typeof origFields.duedate === 'string' && origFields.duedate) {
151
+ fields.duedate = origFields.duedate;
152
+ }
153
+
154
+ if (origFields.security) {
155
+ if (origFields.security.id) fields.security = { id: String(origFields.security.id) };
156
+ else if (origFields.security.name) fields.security = { name: String(origFields.security.name) };
157
+ }
158
+
159
+ if (origFields.parent) {
160
+ if (origFields.parent.id) fields.parent = { id: String(origFields.parent.id) };
161
+ else if (origFields.parent.key) fields.parent = { key: String(origFields.parent.key) };
162
+ }
163
+
164
+ if (origFields.assignee) {
165
+ if (origFields.assignee.accountId) fields.assignee = { accountId: String(origFields.assignee.accountId) };
166
+ else if (origFields.assignee.name) fields.assignee = { name: String(origFields.assignee.name) };
167
+ }
168
+
169
+ // Custom fields starting with customfield_
170
+ for (const [key, val] of Object.entries(origFields)) {
171
+ if (key.startsWith('customfield_') && val !== null && val !== undefined) {
172
+ const transformed = transformFieldValue(val);
173
+ if (transformed !== undefined) {
174
+ fields[key] = transformed;
175
+ }
176
+ }
177
+ }
178
+
179
+ return fields;
180
+ }
181
+
57
182
  // List all filters
58
183
  router.get('/', (_req, res) => {
59
184
  res.json(listJiraFilters());
@@ -271,12 +396,11 @@ router.post('/issues/clone', async (req, res) => {
271
396
  recordApiError({ source: 'Jira API (Clone Issue)', method: 'GET', url: myselfUrl, message: error.message || 'Failed to fetch current user.' });
272
397
  }
273
398
 
274
- // Fetch original issue details to preserve issue type, project, description, priority
399
+ // Fetch original issue details to preserve issue type, project, description, priority, components, labels, fixVersions, versions, environment, duedate, security, parent, custom fields
400
+ let origFields = null;
275
401
  let origIssueType = null;
276
402
  let origProjectKey = null;
277
403
  let origSummary = null;
278
- let origDescription = null;
279
- let origPriority = null;
280
404
 
281
405
  if (issueKey) {
282
406
  const issueUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}`;
@@ -286,29 +410,19 @@ router.post('/issues/clone', async (req, res) => {
286
410
  recordApiError({ source: 'Jira API (Clone Issue)', method: 'GET', url: issueUrl, status: origRes.status, message: `Jira issue API returned ${origRes.status}: ${origRes.statusText || 'Failed to fetch original issue.'}` });
287
411
  }
288
412
  if (origRes.status === 200 && origRes.data?.fields) {
289
- const fields = origRes.data.fields;
290
- if (fields.issuetype) {
291
- if (fields.issuetype.id) {
292
- origIssueType = { id: fields.issuetype.id };
293
- } else if (fields.issuetype.name) {
294
- origIssueType = { name: fields.issuetype.name };
413
+ origFields = origRes.data.fields;
414
+ if (origFields.issuetype) {
415
+ if (origFields.issuetype.id) {
416
+ origIssueType = { id: String(origFields.issuetype.id) };
417
+ } else if (origFields.issuetype.name) {
418
+ origIssueType = { name: String(origFields.issuetype.name) };
295
419
  }
296
420
  }
297
- if (fields.project?.key) {
298
- origProjectKey = fields.project.key;
299
- }
300
- if (fields.summary) {
301
- origSummary = String(fields.summary).trim();
421
+ if (origFields.project?.key) {
422
+ origProjectKey = origFields.project.key;
302
423
  }
303
- if (fields.description) {
304
- origDescription = fields.description;
305
- }
306
- if (fields.priority) {
307
- if (fields.priority.id) {
308
- origPriority = { id: fields.priority.id };
309
- } else if (fields.priority.name) {
310
- origPriority = { name: fields.priority.name };
311
- }
424
+ if (origFields.summary) {
425
+ origSummary = String(origFields.summary).trim();
312
426
  }
313
427
  }
314
428
  } catch (error) {
@@ -321,26 +435,60 @@ router.post('/issues/clone', async (req, res) => {
321
435
  const projectKey = origProjectKey || (issueKey ? issueKey.split('-')[0] : '');
322
436
  const requestedSummary = summary.trim();
323
437
  const cloneSummary = origSummary || requestedSummary;
438
+
439
+ // Extract all cloneable standard & custom fields from original issue
440
+ const cloneableFields = extractCloneableFields(origFields);
441
+
442
+ // Override assignee with current authenticated user if resolved
443
+ if (assigneeField) {
444
+ cloneableFields.assignee = assigneeField;
445
+ }
446
+
447
+ // Core fields required for issue creation
448
+ const coreFields = {
449
+ summary: cloneSummary,
450
+ ...(projectKey ? { project: { key: projectKey } } : {}),
451
+ issuetype: targetIssueType
452
+ };
453
+
454
+ let activeFields = { ...cloneableFields, ...coreFields };
455
+ let pendingPutFields = {};
456
+
324
457
  const url = `https://${jiraHost}/rest/api/2/issue`;
325
- const payload = {
326
- fields: {
327
- summary: cloneSummary,
328
- ...(projectKey ? { project: { key: projectKey } } : {}),
329
- issuetype: targetIssueType,
330
- ...(assigneeField ? { assignee: assigneeField } : {}),
331
- ...(origDescription ? { description: origDescription } : {}),
332
- ...(origPriority ? { priority: origPriority } : {})
458
+ let response = await httpClient.post(url, { fields: activeFields }, { headers });
459
+
460
+ // Handle field rejection on POST creation (e.g. fields not configured on Create Screen)
461
+ let maxRetries = 3;
462
+ while (response.status !== 201 && response.status !== 200 && maxRetries > 0) {
463
+ maxRetries--;
464
+ const errFields = response.data?.errors ? Object.keys(response.data.errors) : [];
465
+ if (errFields.length > 0) {
466
+ let strippedAny = false;
467
+ for (const errField of errFields) {
468
+ if (activeFields[errField] !== undefined && !['summary', 'project', 'issuetype'].includes(errField)) {
469
+ pendingPutFields[errField] = activeFields[errField];
470
+ delete activeFields[errField];
471
+ strippedAny = true;
472
+ }
473
+ }
474
+ if (strippedAny) {
475
+ response = await httpClient.post(url, { fields: activeFields }, { headers });
476
+ continue;
477
+ }
333
478
  }
334
- };
479
+ break;
480
+ }
335
481
 
336
- let response = await httpClient.post(url, payload, { headers });
337
- // Fallback if optional fields like description or priority caused issue creation rejection
338
- if (response.status !== 201 && response.status !== 200 && (origPriority || origDescription)) {
482
+ // Fallback attempt with minimal core fields if POST creation still failed
483
+ if (response.status !== 201 && response.status !== 200) {
484
+ for (const [k, v] of Object.entries(activeFields)) {
485
+ if (!['summary', 'project', 'issuetype'].includes(k)) {
486
+ pendingPutFields[k] = v;
487
+ }
488
+ }
339
489
  const fallbackPayload = {
340
490
  fields: {
341
- summary: cloneSummary,
342
- ...(projectKey ? { project: { key: projectKey } } : {}),
343
- issuetype: targetIssueType,
491
+ ...coreFields,
344
492
  ...(assigneeField ? { assignee: assigneeField } : {})
345
493
  }
346
494
  };
@@ -358,7 +506,28 @@ router.post('/issues/clone', async (req, res) => {
358
506
  const created = response.data || {};
359
507
  const createdKey = created.key || issueKey;
360
508
 
361
- // Fallback: Assign explicitly if not assigned during creation
509
+ // Apply pending fields via PUT update if any fields could not be set on creation screen
510
+ if (createdKey && Object.keys(pendingPutFields).length > 0) {
511
+ const updateUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(createdKey)}`;
512
+ try {
513
+ let putRes = await httpClient.put(updateUrl, { fields: pendingPutFields }, { headers });
514
+ if (putRes.status < 200 || putRes.status >= 300) {
515
+ const putErrFields = putRes.data?.errors ? Object.keys(putRes.data.errors) : [];
516
+ if (putErrFields.length > 0) {
517
+ for (const ef of putErrFields) {
518
+ delete pendingPutFields[ef];
519
+ }
520
+ if (Object.keys(pendingPutFields).length > 0) {
521
+ await httpClient.put(updateUrl, { fields: pendingPutFields }, { headers });
522
+ }
523
+ }
524
+ }
525
+ } catch (err) {
526
+ recordApiError({ source: 'Jira API (Clone Issue PUT fields)', method: 'PUT', url: `https://${jiraHost}/rest/api/2/issue/${createdKey}`, message: err.message || 'Failed to PUT cloned fields.' });
527
+ }
528
+ }
529
+
530
+ // Fallback: Assign explicitly if not assigned during creation/PUT
362
531
  if (createdKey && assigneeField) {
363
532
  const assignUrl = `https://${jiraHost}/rest/api/2/issue/${createdKey}/assignee`;
364
533
  try {