@jagadeesh52423/atlassian-mcp-server 1.0.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/.env.example +26 -0
- package/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/atlassian-client.d.ts +10 -0
- package/dist/atlassian-client.d.ts.map +1 -0
- package/dist/atlassian-client.js +38 -0
- package/dist/atlassian-client.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +60 -0
- package/dist/config.js.map +1 -0
- package/dist/confluence-client.d.ts +11 -0
- package/dist/confluence-client.d.ts.map +1 -0
- package/dist/confluence-client.js +47 -0
- package/dist/confluence-client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2592 -0
- package/dist/index.js.map +1 -0
- package/dist/jira-client.d.ts +11 -0
- package/dist/jira-client.d.ts.map +1 -0
- package/dist/jira-client.js +47 -0
- package/dist/jira-client.js.map +1 -0
- package/dist/services/bitbucket.d.ts +21 -0
- package/dist/services/bitbucket.d.ts.map +1 -0
- package/dist/services/bitbucket.js +176 -0
- package/dist/services/bitbucket.js.map +1 -0
- package/dist/services/confluence.d.ts +41 -0
- package/dist/services/confluence.d.ts.map +1 -0
- package/dist/services/confluence.js +348 -0
- package/dist/services/confluence.js.map +1 -0
- package/dist/services/jira.d.ts +58 -0
- package/dist/services/jira.d.ts.map +1 -0
- package/dist/services/jira.js +1115 -0
- package/dist/services/jira.js.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
import { JiraClient } from '../jira-client.js';
|
|
2
|
+
export class JiraService {
|
|
3
|
+
client;
|
|
4
|
+
// Smart suggestion rules for common field patterns
|
|
5
|
+
fieldSuggestionRules = [
|
|
6
|
+
{
|
|
7
|
+
pattern: /db.*script/i,
|
|
8
|
+
defaultValue: 'No',
|
|
9
|
+
contextRules: [
|
|
10
|
+
{
|
|
11
|
+
condition: (issue) => /template|config|notification|mofu/i.test(issue.summary),
|
|
12
|
+
value: 'No',
|
|
13
|
+
reason: 'Template/config changes are typically done via MOFU tool and do not require database scripts'
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
pattern: /test.*case|unit.*test/i,
|
|
19
|
+
defaultValue: 'Yes',
|
|
20
|
+
contextRules: [
|
|
21
|
+
{
|
|
22
|
+
condition: (issue) => /template|notification|config/i.test(issue.summary),
|
|
23
|
+
value: 'No',
|
|
24
|
+
reason: 'Template and configuration changes may not require separate unit test cases'
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
constructor() {
|
|
30
|
+
this.client = new JiraClient();
|
|
31
|
+
}
|
|
32
|
+
async searchIssues(options = {}) {
|
|
33
|
+
const { query, limit = 50, startAt = 0 } = options;
|
|
34
|
+
// Create bounded JQL query (Jira requires bounded queries now)
|
|
35
|
+
let jql = 'created >= -90d ORDER BY created DESC';
|
|
36
|
+
if (query) {
|
|
37
|
+
jql = `text ~ "${query}" AND created >= -90d ORDER BY created DESC`;
|
|
38
|
+
}
|
|
39
|
+
const params = {
|
|
40
|
+
jql,
|
|
41
|
+
maxResults: limit,
|
|
42
|
+
startAt,
|
|
43
|
+
fields: 'summary,description,status,assignee,created,updated'
|
|
44
|
+
};
|
|
45
|
+
const response = await this.client.get('/rest/api/3/search/jql', params);
|
|
46
|
+
return response.issues.map((issue) => ({
|
|
47
|
+
id: issue.id,
|
|
48
|
+
key: issue.key,
|
|
49
|
+
summary: issue.fields.summary || '',
|
|
50
|
+
description: this.extractTextFromADF(issue.fields.description),
|
|
51
|
+
status: issue.fields.status?.name || '',
|
|
52
|
+
assignee: issue.fields.assignee?.displayName || '',
|
|
53
|
+
created: issue.fields.created || '',
|
|
54
|
+
updated: issue.fields.updated || ''
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
async getIssue(issueKey) {
|
|
58
|
+
const params = {
|
|
59
|
+
fields: ['summary', 'description', 'status', 'assignee', 'created', 'updated']
|
|
60
|
+
};
|
|
61
|
+
const issue = await this.client.get(`/rest/api/3/issue/${issueKey}`, params);
|
|
62
|
+
return {
|
|
63
|
+
id: issue.id,
|
|
64
|
+
key: issue.key,
|
|
65
|
+
summary: issue.fields.summary || '',
|
|
66
|
+
description: this.extractTextFromADF(issue.fields.description),
|
|
67
|
+
status: issue.fields.status?.name || '',
|
|
68
|
+
assignee: issue.fields.assignee?.displayName || '',
|
|
69
|
+
created: issue.fields.created || '',
|
|
70
|
+
updated: issue.fields.updated || ''
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async createIssue(projectKey, summary, description, issueType = 'Task', additionalFields) {
|
|
74
|
+
const fields = {
|
|
75
|
+
project: { key: projectKey },
|
|
76
|
+
summary,
|
|
77
|
+
description: this.parseDescriptionToADF(description),
|
|
78
|
+
issuetype: { name: issueType }
|
|
79
|
+
};
|
|
80
|
+
// Add optional fields if provided
|
|
81
|
+
if (additionalFields) {
|
|
82
|
+
// Due date (format: YYYY-MM-DD)
|
|
83
|
+
if (additionalFields.duedate) {
|
|
84
|
+
fields.duedate = additionalFields.duedate;
|
|
85
|
+
}
|
|
86
|
+
// Priority (by name)
|
|
87
|
+
if (additionalFields.priority) {
|
|
88
|
+
fields.priority = { name: additionalFields.priority };
|
|
89
|
+
}
|
|
90
|
+
// Labels (array of strings)
|
|
91
|
+
if (additionalFields.labels && additionalFields.labels.length > 0) {
|
|
92
|
+
fields.labels = additionalFields.labels;
|
|
93
|
+
}
|
|
94
|
+
// Components (by name)
|
|
95
|
+
if (additionalFields.components && additionalFields.components.length > 0) {
|
|
96
|
+
fields.components = additionalFields.components.map(name => ({ name }));
|
|
97
|
+
}
|
|
98
|
+
// Assignee (by account ID)
|
|
99
|
+
if (additionalFields.assignee) {
|
|
100
|
+
fields.assignee = { accountId: additionalFields.assignee };
|
|
101
|
+
}
|
|
102
|
+
// Custom fields (merge directly into fields object)
|
|
103
|
+
if (additionalFields.customFields) {
|
|
104
|
+
Object.assign(fields, additionalFields.customFields);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const data = { fields };
|
|
108
|
+
const response = await this.client.post('/rest/api/3/issue', data);
|
|
109
|
+
return this.getIssue(response.key);
|
|
110
|
+
}
|
|
111
|
+
async updateIssue(issueKey, fields) {
|
|
112
|
+
const updateFields = {};
|
|
113
|
+
if (fields.summary) {
|
|
114
|
+
updateFields.summary = fields.summary;
|
|
115
|
+
}
|
|
116
|
+
if (fields.description) {
|
|
117
|
+
// Use proper ADF format for description updates
|
|
118
|
+
updateFields.description = this.parseDescriptionToADF(fields.description);
|
|
119
|
+
}
|
|
120
|
+
if (fields.assignee) {
|
|
121
|
+
updateFields.assignee = { accountId: fields.assignee };
|
|
122
|
+
}
|
|
123
|
+
const data = { fields: updateFields };
|
|
124
|
+
await this.client.put(`/rest/api/3/issue/${issueKey}`, data);
|
|
125
|
+
return this.getIssue(issueKey);
|
|
126
|
+
}
|
|
127
|
+
async transitionIssue(issueKey, transitionId, fieldValues) {
|
|
128
|
+
try {
|
|
129
|
+
const data = {
|
|
130
|
+
transition: { id: transitionId }
|
|
131
|
+
};
|
|
132
|
+
// Add field values if provided
|
|
133
|
+
if (fieldValues) {
|
|
134
|
+
data.fields = fieldValues;
|
|
135
|
+
}
|
|
136
|
+
await this.client.post(`/rest/api/3/issue/${issueKey}/transitions`, data);
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
message: `Issue ${issueKey} successfully transitioned`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
// Check if this is a required field error
|
|
144
|
+
if (error.response?.status === 400) {
|
|
145
|
+
const requiredFields = await this.handleTransitionError(issueKey, transitionId, error);
|
|
146
|
+
if (requiredFields.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
requiresInput: true,
|
|
150
|
+
requiredFields,
|
|
151
|
+
message: 'Transition requires additional fields. Please provide values for the required fields.',
|
|
152
|
+
issueKey,
|
|
153
|
+
transitionId
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Re-throw if not a field validation error
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async getIssueTransitions(issueKey) {
|
|
162
|
+
const response = await this.client.get(`/rest/api/3/issue/${issueKey}/transitions`);
|
|
163
|
+
return response.transitions;
|
|
164
|
+
}
|
|
165
|
+
async addComment(issueKey, comment) {
|
|
166
|
+
const data = {
|
|
167
|
+
body: this.parseDescriptionToADF(comment)
|
|
168
|
+
};
|
|
169
|
+
await this.client.post(`/rest/api/3/issue/${issueKey}/comment`, data);
|
|
170
|
+
}
|
|
171
|
+
async getComments(issueKey) {
|
|
172
|
+
const response = await this.client.get(`/rest/api/3/issue/${issueKey}/comment`);
|
|
173
|
+
return response.comments.map((comment) => ({
|
|
174
|
+
id: comment.id,
|
|
175
|
+
author: comment.author?.displayName || 'Unknown',
|
|
176
|
+
authorAccountId: comment.author?.accountId,
|
|
177
|
+
body: this.extractTextFromADF(comment.body),
|
|
178
|
+
created: comment.created,
|
|
179
|
+
updated: comment.updated,
|
|
180
|
+
updateAuthor: comment.updateAuthor?.displayName
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
async updateComment(issueKey, commentId, comment) {
|
|
184
|
+
const data = {
|
|
185
|
+
body: this.parseDescriptionToADF(comment)
|
|
186
|
+
};
|
|
187
|
+
const response = await this.client.put(`/rest/api/3/issue/${issueKey}/comment/${commentId}`, data);
|
|
188
|
+
return {
|
|
189
|
+
id: response.id,
|
|
190
|
+
author: response.author?.displayName || 'Unknown',
|
|
191
|
+
body: this.extractTextFromADF(response.body),
|
|
192
|
+
created: response.created,
|
|
193
|
+
updated: response.updated,
|
|
194
|
+
updateAuthor: response.updateAuthor?.displayName
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async deleteComment(issueKey, commentId) {
|
|
198
|
+
await this.client.delete(`/rest/api/3/issue/${issueKey}/comment/${commentId}`);
|
|
199
|
+
}
|
|
200
|
+
extractTextFromADF(body) {
|
|
201
|
+
if (!body)
|
|
202
|
+
return '';
|
|
203
|
+
// If body is already a string (wiki markup), return it directly
|
|
204
|
+
if (typeof body === 'string') {
|
|
205
|
+
return this.formatWikiMarkup(body);
|
|
206
|
+
}
|
|
207
|
+
// If body doesn't have content property, it might be wiki markup in another format
|
|
208
|
+
if (!body.content) {
|
|
209
|
+
return this.formatWikiMarkup(String(body));
|
|
210
|
+
}
|
|
211
|
+
let text = '';
|
|
212
|
+
const extractFromContent = (content) => {
|
|
213
|
+
for (const item of content) {
|
|
214
|
+
if (item.type === 'text' && item.text) {
|
|
215
|
+
text += item.text;
|
|
216
|
+
}
|
|
217
|
+
else if (item.type === 'hardBreak') {
|
|
218
|
+
text += '\n';
|
|
219
|
+
}
|
|
220
|
+
else if (item.type === 'paragraph') {
|
|
221
|
+
if (item.content) {
|
|
222
|
+
extractFromContent(item.content);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (item.type === 'heading') {
|
|
226
|
+
const level = item.attrs?.level || 1;
|
|
227
|
+
const prefix = '#'.repeat(level);
|
|
228
|
+
text += `${prefix} `;
|
|
229
|
+
if (item.content) {
|
|
230
|
+
extractFromContent(item.content);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (item.type === 'bulletList' || item.type === 'orderedList') {
|
|
234
|
+
if (item.content) {
|
|
235
|
+
extractFromContent(item.content);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (item.type === 'listItem') {
|
|
239
|
+
text += '- ';
|
|
240
|
+
if (item.content) {
|
|
241
|
+
extractFromContent(item.content);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (item.type === 'codeBlock') {
|
|
245
|
+
text += '```';
|
|
246
|
+
if (item.content) {
|
|
247
|
+
extractFromContent(item.content);
|
|
248
|
+
}
|
|
249
|
+
text += '```';
|
|
250
|
+
}
|
|
251
|
+
else if (item.type === 'table') {
|
|
252
|
+
// Extract table as markdown
|
|
253
|
+
if (item.content) {
|
|
254
|
+
const rows = [];
|
|
255
|
+
item.content.forEach((row) => {
|
|
256
|
+
if (row.type === 'tableRow' && row.content) {
|
|
257
|
+
const cells = [];
|
|
258
|
+
row.content.forEach((cell) => {
|
|
259
|
+
if ((cell.type === 'tableCell' || cell.type === 'tableHeader') && cell.content) {
|
|
260
|
+
let cellText = '';
|
|
261
|
+
const extractCellText = (cellContent) => {
|
|
262
|
+
cellContent.forEach((cellItem) => {
|
|
263
|
+
if (cellItem.type === 'paragraph' && cellItem.content) {
|
|
264
|
+
cellItem.content.forEach((textItem) => {
|
|
265
|
+
if (textItem.type === 'text' && textItem.text) {
|
|
266
|
+
cellText += textItem.text;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
extractCellText(cell.content);
|
|
273
|
+
cells.push(cellText.trim());
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
if (cells.length > 0) {
|
|
277
|
+
rows.push(cells);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
// Format as markdown table
|
|
282
|
+
if (rows.length > 0) {
|
|
283
|
+
const [headerRow, ...dataRows] = rows;
|
|
284
|
+
// Header row
|
|
285
|
+
text += '\n| ' + headerRow.join(' | ') + ' |\n';
|
|
286
|
+
// Separator row
|
|
287
|
+
text += '|' + headerRow.map(() => '--------').join('|') + '|\n';
|
|
288
|
+
// Data rows
|
|
289
|
+
dataRows.forEach(row => {
|
|
290
|
+
text += '| ' + row.join(' | ') + ' |\n';
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (item.content) {
|
|
296
|
+
extractFromContent(item.content);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
extractFromContent(body.content);
|
|
301
|
+
return this.formatWikiMarkup(text.trim());
|
|
302
|
+
}
|
|
303
|
+
formatWikiMarkup(text) {
|
|
304
|
+
if (!text)
|
|
305
|
+
return '';
|
|
306
|
+
return text
|
|
307
|
+
// Convert Confluence headers to markdown headers
|
|
308
|
+
.replace(/^h([1-6])\.\s*/gm, (match, level) => '#'.repeat(parseInt(level)) + ' ')
|
|
309
|
+
// Convert horizontal rules
|
|
310
|
+
.replace(/^----+$/gm, '---')
|
|
311
|
+
// Handle panels - convert to blockquotes for better readability
|
|
312
|
+
.replace(/\{panel:title=([^|]*)[^}]*\}/g, '\n> **$1**\n>')
|
|
313
|
+
.replace(/\{panel\}/g, '\n')
|
|
314
|
+
// Handle colored text/backgrounds - keep the text but remove styling
|
|
315
|
+
.replace(/\{color:[^}]*\}([^{]*)\{color\}/g, '$1')
|
|
316
|
+
// Convert checkboxes
|
|
317
|
+
.replace(/☐/g, '- [ ]')
|
|
318
|
+
.replace(/☑/g, '- [x]')
|
|
319
|
+
// Clean up multiple newlines
|
|
320
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
321
|
+
// Ensure proper spacing around headers
|
|
322
|
+
.replace(/\n(#{1,6}\s)/g, '\n\n$1')
|
|
323
|
+
.trim();
|
|
324
|
+
}
|
|
325
|
+
async getProjects() {
|
|
326
|
+
const response = await this.client.get('/rest/api/3/project');
|
|
327
|
+
return response;
|
|
328
|
+
}
|
|
329
|
+
async getIssueTypes(projectKey) {
|
|
330
|
+
const response = await this.client.get(`/rest/api/3/issue/createmeta/${projectKey}/issuetypes`);
|
|
331
|
+
return response.issueTypes;
|
|
332
|
+
}
|
|
333
|
+
async assignIssue(issueKey, accountId) {
|
|
334
|
+
const data = { accountId };
|
|
335
|
+
await this.client.put(`/rest/api/3/issue/${issueKey}/assignee`, data);
|
|
336
|
+
}
|
|
337
|
+
async getUsersByQuery(query) {
|
|
338
|
+
const params = {
|
|
339
|
+
query,
|
|
340
|
+
maxResults: 10
|
|
341
|
+
};
|
|
342
|
+
const response = await this.client.get('/rest/api/3/user/search', params);
|
|
343
|
+
return response;
|
|
344
|
+
}
|
|
345
|
+
// Attachment Methods
|
|
346
|
+
async getAttachments(issueKey) {
|
|
347
|
+
const issue = await this.client.get(`/rest/api/3/issue/${issueKey}`, {
|
|
348
|
+
fields: ['attachment']
|
|
349
|
+
});
|
|
350
|
+
return issue.fields.attachment?.map((att) => ({
|
|
351
|
+
id: att.id,
|
|
352
|
+
filename: att.filename,
|
|
353
|
+
author: att.author?.displayName || 'Unknown',
|
|
354
|
+
created: att.created,
|
|
355
|
+
size: att.size,
|
|
356
|
+
mimeType: att.mimeType,
|
|
357
|
+
content: att.content
|
|
358
|
+
})) || [];
|
|
359
|
+
}
|
|
360
|
+
async addAttachment(issueKey, filename, fileContent) {
|
|
361
|
+
// Note: fileContent should be base64 encoded
|
|
362
|
+
const FormData = (await import('form-data')).default;
|
|
363
|
+
const form = new FormData();
|
|
364
|
+
const buffer = Buffer.from(fileContent, 'base64');
|
|
365
|
+
form.append('file', buffer, { filename });
|
|
366
|
+
const response = await this.client.postFormData(`/rest/api/3/issue/${issueKey}/attachments`, form);
|
|
367
|
+
return response;
|
|
368
|
+
}
|
|
369
|
+
async deleteAttachment(attachmentId) {
|
|
370
|
+
await this.client.delete(`/rest/api/3/attachment/${attachmentId}`);
|
|
371
|
+
}
|
|
372
|
+
// Issue Linking Methods
|
|
373
|
+
async getIssueLinks(issueKey) {
|
|
374
|
+
const issue = await this.client.get(`/rest/api/3/issue/${issueKey}`, {
|
|
375
|
+
fields: ['issuelinks']
|
|
376
|
+
});
|
|
377
|
+
return issue.fields.issuelinks?.map((link) => ({
|
|
378
|
+
id: link.id,
|
|
379
|
+
type: link.type?.name || 'Unknown',
|
|
380
|
+
inward: link.inwardIssue ? {
|
|
381
|
+
key: link.inwardIssue.key,
|
|
382
|
+
summary: link.inwardIssue.fields?.summary,
|
|
383
|
+
status: link.inwardIssue.fields?.status?.name
|
|
384
|
+
} : null,
|
|
385
|
+
outward: link.outwardIssue ? {
|
|
386
|
+
key: link.outwardIssue.key,
|
|
387
|
+
summary: link.outwardIssue.fields?.summary,
|
|
388
|
+
status: link.outwardIssue.fields?.status?.name
|
|
389
|
+
} : null
|
|
390
|
+
})) || [];
|
|
391
|
+
}
|
|
392
|
+
async createIssueLink(inwardIssueKey, outwardIssueKey, linkType) {
|
|
393
|
+
const data = {
|
|
394
|
+
type: { name: linkType },
|
|
395
|
+
inwardIssue: { key: inwardIssueKey },
|
|
396
|
+
outwardIssue: { key: outwardIssueKey }
|
|
397
|
+
};
|
|
398
|
+
const response = await this.client.post('/rest/api/3/issueLink', data);
|
|
399
|
+
return response;
|
|
400
|
+
}
|
|
401
|
+
async deleteIssueLink(linkId) {
|
|
402
|
+
await this.client.delete(`/rest/api/3/issueLink/${linkId}`);
|
|
403
|
+
}
|
|
404
|
+
async getIssueLinkTypes() {
|
|
405
|
+
const response = await this.client.get('/rest/api/3/issueLinkType');
|
|
406
|
+
return response.issueLinkTypes.map((type) => ({
|
|
407
|
+
id: type.id,
|
|
408
|
+
name: type.name,
|
|
409
|
+
inward: type.inward,
|
|
410
|
+
outward: type.outward
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
// Work Log Methods
|
|
414
|
+
async getWorkLogs(issueKey) {
|
|
415
|
+
const response = await this.client.get(`/rest/api/3/issue/${issueKey}/worklog`);
|
|
416
|
+
return response.worklogs?.map((worklog) => ({
|
|
417
|
+
id: worklog.id,
|
|
418
|
+
author: worklog.author?.displayName || 'Unknown',
|
|
419
|
+
authorAccountId: worklog.author?.accountId,
|
|
420
|
+
comment: this.extractTextFromADF(worklog.comment),
|
|
421
|
+
created: worklog.created,
|
|
422
|
+
updated: worklog.updated,
|
|
423
|
+
started: worklog.started,
|
|
424
|
+
timeSpent: worklog.timeSpent,
|
|
425
|
+
timeSpentSeconds: worklog.timeSpentSeconds
|
|
426
|
+
})) || [];
|
|
427
|
+
}
|
|
428
|
+
async addWorkLog(issueKey, timeSpentSeconds, comment, started) {
|
|
429
|
+
const data = {
|
|
430
|
+
timeSpentSeconds
|
|
431
|
+
};
|
|
432
|
+
if (started) {
|
|
433
|
+
data.started = started;
|
|
434
|
+
}
|
|
435
|
+
if (comment) {
|
|
436
|
+
data.comment = this.parseDescriptionToADF(comment);
|
|
437
|
+
}
|
|
438
|
+
const response = await this.client.post(`/rest/api/3/issue/${issueKey}/worklog`, data);
|
|
439
|
+
return response;
|
|
440
|
+
}
|
|
441
|
+
async updateWorkLog(issueKey, worklogId, timeSpentSeconds, comment) {
|
|
442
|
+
const data = {};
|
|
443
|
+
if (timeSpentSeconds !== undefined) {
|
|
444
|
+
data.timeSpentSeconds = timeSpentSeconds;
|
|
445
|
+
}
|
|
446
|
+
if (comment) {
|
|
447
|
+
data.comment = this.parseDescriptionToADF(comment);
|
|
448
|
+
}
|
|
449
|
+
const response = await this.client.put(`/rest/api/3/issue/${issueKey}/worklog/${worklogId}`, data);
|
|
450
|
+
return response;
|
|
451
|
+
}
|
|
452
|
+
async deleteWorkLog(issueKey, worklogId) {
|
|
453
|
+
await this.client.delete(`/rest/api/3/issue/${issueKey}/worklog/${worklogId}`);
|
|
454
|
+
}
|
|
455
|
+
// Watchers Methods
|
|
456
|
+
async getWatchers(issueKey) {
|
|
457
|
+
const response = await this.client.get(`/rest/api/3/issue/${issueKey}/watchers`);
|
|
458
|
+
return {
|
|
459
|
+
watchCount: response.watchCount,
|
|
460
|
+
isWatching: response.isWatching,
|
|
461
|
+
watchers: response.watchers?.map((watcher) => ({
|
|
462
|
+
accountId: watcher.accountId,
|
|
463
|
+
displayName: watcher.displayName,
|
|
464
|
+
emailAddress: watcher.emailAddress,
|
|
465
|
+
active: watcher.active
|
|
466
|
+
})) || []
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async addWatcher(issueKey, accountId) {
|
|
470
|
+
await this.client.post(`/rest/api/3/issue/${issueKey}/watchers`, JSON.stringify(accountId));
|
|
471
|
+
}
|
|
472
|
+
async removeWatcher(issueKey, accountId) {
|
|
473
|
+
await this.client.delete(`/rest/api/3/issue/${issueKey}/watchers?accountId=${accountId}`);
|
|
474
|
+
}
|
|
475
|
+
// Sub-task Methods
|
|
476
|
+
async getSubTasks(issueKey) {
|
|
477
|
+
const issue = await this.client.get(`/rest/api/3/issue/${issueKey}`, {
|
|
478
|
+
fields: ['subtasks']
|
|
479
|
+
});
|
|
480
|
+
return issue.fields.subtasks?.map((subtask) => ({
|
|
481
|
+
id: subtask.id,
|
|
482
|
+
key: subtask.key,
|
|
483
|
+
summary: subtask.fields?.summary,
|
|
484
|
+
status: subtask.fields?.status?.name,
|
|
485
|
+
assignee: subtask.fields?.assignee?.displayName
|
|
486
|
+
})) || [];
|
|
487
|
+
}
|
|
488
|
+
async createSubTask(parentKey, summary, description) {
|
|
489
|
+
const data = {
|
|
490
|
+
fields: {
|
|
491
|
+
project: { key: parentKey.split('-')[0] },
|
|
492
|
+
parent: { key: parentKey },
|
|
493
|
+
summary,
|
|
494
|
+
description: this.parseDescriptionToADF(description),
|
|
495
|
+
issuetype: { name: 'Sub-task' }
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const response = await this.client.post('/rest/api/3/issue', data);
|
|
499
|
+
return this.getIssue(response.key);
|
|
500
|
+
}
|
|
501
|
+
// Issue History Methods
|
|
502
|
+
async getIssueHistory(issueKey) {
|
|
503
|
+
const response = await this.client.get(`/rest/api/3/issue/${issueKey}/changelog`);
|
|
504
|
+
return response.values?.map((change) => ({
|
|
505
|
+
id: change.id,
|
|
506
|
+
author: change.author?.displayName || 'Unknown',
|
|
507
|
+
authorAccountId: change.author?.accountId,
|
|
508
|
+
created: change.created,
|
|
509
|
+
items: change.items?.map((item) => ({
|
|
510
|
+
field: item.field,
|
|
511
|
+
fieldtype: item.fieldtype,
|
|
512
|
+
from: item.fromString,
|
|
513
|
+
to: item.toString
|
|
514
|
+
}))
|
|
515
|
+
})) || [];
|
|
516
|
+
}
|
|
517
|
+
// Smart Field Handling Methods
|
|
518
|
+
async handleTransitionError(issueKey, transitionId, error) {
|
|
519
|
+
try {
|
|
520
|
+
// Get transition metadata with field information
|
|
521
|
+
const response = await this.client.get(`/rest/api/3/issue/${issueKey}/transitions?expand=transitions.fields`);
|
|
522
|
+
const transition = response.transitions.find((t) => t.id === transitionId);
|
|
523
|
+
if (!transition || !transition.fields) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
// Get issue details for context
|
|
527
|
+
const issue = await this.getIssue(issueKey);
|
|
528
|
+
// Analyze required fields and fields that may need values
|
|
529
|
+
const requiredFields = [];
|
|
530
|
+
for (const [fieldKey, fieldInfo] of Object.entries(transition.fields)) {
|
|
531
|
+
// Process all fields that have validation requirements or are commonly needed
|
|
532
|
+
const field = this.analyzeField(fieldKey, fieldInfo, issue);
|
|
533
|
+
requiredFields.push(field);
|
|
534
|
+
}
|
|
535
|
+
return requiredFields;
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
console.error('Error analyzing transition fields:', err);
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
analyzeField(fieldKey, fieldInfo, issue) {
|
|
543
|
+
const field = {
|
|
544
|
+
key: fieldKey,
|
|
545
|
+
name: fieldInfo.name || fieldKey,
|
|
546
|
+
type: fieldInfo.schema?.type || 'unknown',
|
|
547
|
+
required: fieldInfo.required || false
|
|
548
|
+
};
|
|
549
|
+
// Extract options for select fields
|
|
550
|
+
if (fieldInfo.allowedValues) {
|
|
551
|
+
field.options = fieldInfo.allowedValues.map((val) => ({
|
|
552
|
+
id: val.id,
|
|
553
|
+
value: val.value
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
// Generate smart suggestions
|
|
557
|
+
field.suggestion = this.generateFieldSuggestion(fieldInfo.name, field.options, issue);
|
|
558
|
+
return field;
|
|
559
|
+
}
|
|
560
|
+
generateFieldSuggestion(fieldName, options, issue) {
|
|
561
|
+
if (!fieldName)
|
|
562
|
+
return undefined;
|
|
563
|
+
// Apply suggestion rules
|
|
564
|
+
for (const rule of this.fieldSuggestionRules) {
|
|
565
|
+
if (rule.pattern.test(fieldName)) {
|
|
566
|
+
// Check context rules first
|
|
567
|
+
if (rule.contextRules && issue) {
|
|
568
|
+
for (const contextRule of rule.contextRules) {
|
|
569
|
+
if (contextRule.condition(issue)) {
|
|
570
|
+
// Find matching option ID if available
|
|
571
|
+
const matchingOption = options?.find(opt => opt.value === contextRule.value);
|
|
572
|
+
return {
|
|
573
|
+
value: contextRule.value,
|
|
574
|
+
id: matchingOption?.id,
|
|
575
|
+
reason: contextRule.reason
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Fall back to default value
|
|
581
|
+
const matchingOption = options?.find(opt => opt.value === rule.defaultValue);
|
|
582
|
+
return {
|
|
583
|
+
value: rule.defaultValue,
|
|
584
|
+
id: matchingOption?.id,
|
|
585
|
+
reason: `Default suggestion for ${fieldName.toLowerCase()}`
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Generic suggestions based on field name patterns
|
|
590
|
+
if (options && options.length > 0) {
|
|
591
|
+
if (/required|mandatory|needed/i.test(fieldName)) {
|
|
592
|
+
const yesOption = options.find(opt => /yes|true|required/i.test(opt.value));
|
|
593
|
+
if (yesOption) {
|
|
594
|
+
return {
|
|
595
|
+
value: yesOption.value,
|
|
596
|
+
id: yesOption.id,
|
|
597
|
+
reason: 'Field name suggests this is typically required'
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (/optional|not.*required|skip/i.test(fieldName)) {
|
|
602
|
+
const noOption = options.find(opt => /no|false|not.*required|skip/i.test(opt.value));
|
|
603
|
+
if (noOption) {
|
|
604
|
+
return {
|
|
605
|
+
value: noOption.value,
|
|
606
|
+
id: noOption.id,
|
|
607
|
+
reason: 'Field name suggests this is typically not required'
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
// Enhanced transition method with smart field handling
|
|
615
|
+
async transitionIssueInteractive(issueKey, transitionId, providedFields) {
|
|
616
|
+
// First attempt with provided fields
|
|
617
|
+
const result = await this.transitionIssue(issueKey, transitionId, providedFields);
|
|
618
|
+
if (result.success || !result.requiresInput) {
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
// If user provided fields but still failed, try to auto-fill based on suggestions
|
|
622
|
+
if (result.requiredFields && result.requiredFields.length > 0) {
|
|
623
|
+
const autoFields = { ...providedFields };
|
|
624
|
+
let hasAutoSuggestions = false;
|
|
625
|
+
for (const field of result.requiredFields) {
|
|
626
|
+
if (field.suggestion && !autoFields[field.key]) {
|
|
627
|
+
if (field.suggestion.id) {
|
|
628
|
+
autoFields[field.key] = { id: field.suggestion.id };
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
autoFields[field.key] = field.suggestion.value;
|
|
632
|
+
}
|
|
633
|
+
hasAutoSuggestions = true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Try transition again with auto-filled suggestions
|
|
637
|
+
if (hasAutoSuggestions) {
|
|
638
|
+
const retryResult = await this.transitionIssue(issueKey, transitionId, autoFields);
|
|
639
|
+
if (retryResult.success) {
|
|
640
|
+
retryResult.message = `${retryResult.message} (auto-filled based on smart suggestions)`;
|
|
641
|
+
}
|
|
642
|
+
return retryResult;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
// Convert description to wiki markup text format
|
|
648
|
+
parseDescriptionToWikiMarkup(description) {
|
|
649
|
+
const lines = description.split('\n');
|
|
650
|
+
const result = [];
|
|
651
|
+
for (let i = 0; i < lines.length; i++) {
|
|
652
|
+
const line = lines[i].trim();
|
|
653
|
+
if (line === '') {
|
|
654
|
+
result.push('');
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
// Handle markdown headers
|
|
658
|
+
if (line.startsWith('###')) {
|
|
659
|
+
result.push('h3. ' + line.substring(3).trim());
|
|
660
|
+
}
|
|
661
|
+
else if (line.startsWith('##')) {
|
|
662
|
+
result.push('h2. ' + line.substring(2).trim());
|
|
663
|
+
}
|
|
664
|
+
else if (line.startsWith('#')) {
|
|
665
|
+
result.push('h1. ' + line.substring(1).trim());
|
|
666
|
+
}
|
|
667
|
+
// Handle markdown tables
|
|
668
|
+
else if (line.includes('|') && line.trim() !== '|') {
|
|
669
|
+
const cells = line.split('|').map(cell => cell.trim()).filter(cell => cell !== '');
|
|
670
|
+
if (cells.length >= 2) {
|
|
671
|
+
// Collect table rows
|
|
672
|
+
const allTableRows = [];
|
|
673
|
+
let currentLine = i;
|
|
674
|
+
while (currentLine < lines.length) {
|
|
675
|
+
const tableLine = lines[currentLine].trim();
|
|
676
|
+
if (tableLine === '') {
|
|
677
|
+
currentLine++;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (!tableLine.includes('|')) {
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
// Skip separator lines
|
|
684
|
+
if (tableLine.match(/^\|?[\s\-:|]+\|?$/)) {
|
|
685
|
+
currentLine++;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const rowCells = tableLine.split('|').map(cell => cell.trim()).filter(cell => cell !== '');
|
|
689
|
+
if (rowCells.length >= 2) {
|
|
690
|
+
allTableRows.push(rowCells);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
currentLine++;
|
|
696
|
+
}
|
|
697
|
+
// Convert to Confluence wiki markup
|
|
698
|
+
if (allTableRows.length > 0) {
|
|
699
|
+
// Header row
|
|
700
|
+
if (allTableRows.length > 0) {
|
|
701
|
+
result.push('||*' + allTableRows[0].join('*||*') + '*||');
|
|
702
|
+
// Add separator line based on number of columns
|
|
703
|
+
const numColumns = allTableRows[0].length;
|
|
704
|
+
const separator = '|' + '-|'.repeat(numColumns);
|
|
705
|
+
result.push(separator);
|
|
706
|
+
}
|
|
707
|
+
// Data rows
|
|
708
|
+
for (let rowIdx = 1; rowIdx < allTableRows.length; rowIdx++) {
|
|
709
|
+
result.push('|' + allTableRows[rowIdx].join('|') + '|');
|
|
710
|
+
}
|
|
711
|
+
// Skip processed lines
|
|
712
|
+
i = currentLine - 1;
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// If not a valid table, treat as regular text
|
|
717
|
+
result.push(line);
|
|
718
|
+
}
|
|
719
|
+
// Handle bullet points
|
|
720
|
+
else if (line.startsWith('- ') || line.startsWith('* ')) {
|
|
721
|
+
result.push('* ' + line.substring(2).trim());
|
|
722
|
+
}
|
|
723
|
+
// Regular text
|
|
724
|
+
else {
|
|
725
|
+
result.push(line);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return result.join('\n');
|
|
729
|
+
}
|
|
730
|
+
// Enhanced description parser that converts text to proper ADF format
|
|
731
|
+
parseDescriptionToADF(description) {
|
|
732
|
+
const lines = description.split('\n');
|
|
733
|
+
const content = [];
|
|
734
|
+
for (let i = 0; i < lines.length; i++) {
|
|
735
|
+
const line = lines[i].trim();
|
|
736
|
+
if (line === '') {
|
|
737
|
+
// Skip empty lines - let Claude handle spacing naturally
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
// Handle Confluence wiki markup headers (h1., h2., h3., etc.)
|
|
741
|
+
const confluenceHeaderMatch = line.match(/^h([1-6])\.\s*(.*)$/);
|
|
742
|
+
if (confluenceHeaderMatch) {
|
|
743
|
+
const level = parseInt(confluenceHeaderMatch[1]);
|
|
744
|
+
const headerText = confluenceHeaderMatch[2];
|
|
745
|
+
content.push({
|
|
746
|
+
type: 'heading',
|
|
747
|
+
attrs: { level },
|
|
748
|
+
content: [{
|
|
749
|
+
type: 'text',
|
|
750
|
+
text: headerText
|
|
751
|
+
}]
|
|
752
|
+
});
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
// Handle markdown headers (## Header, ### Header)
|
|
756
|
+
if (line.startsWith('###')) {
|
|
757
|
+
content.push({
|
|
758
|
+
type: 'heading',
|
|
759
|
+
attrs: { level: 3 },
|
|
760
|
+
content: [{
|
|
761
|
+
type: 'text',
|
|
762
|
+
text: line.substring(3).trim()
|
|
763
|
+
}]
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
else if (line.startsWith('##')) {
|
|
767
|
+
content.push({
|
|
768
|
+
type: 'heading',
|
|
769
|
+
attrs: { level: 2 },
|
|
770
|
+
content: [{
|
|
771
|
+
type: 'text',
|
|
772
|
+
text: line.substring(2).trim()
|
|
773
|
+
}]
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
else if (line.startsWith('#')) {
|
|
777
|
+
content.push({
|
|
778
|
+
type: 'heading',
|
|
779
|
+
attrs: { level: 1 },
|
|
780
|
+
content: [{
|
|
781
|
+
type: 'text',
|
|
782
|
+
text: line.substring(1).trim()
|
|
783
|
+
}]
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
// Handle horizontal rules (---- or ----)
|
|
787
|
+
else if (/^-{4,}$/.test(line)) {
|
|
788
|
+
content.push({
|
|
789
|
+
type: 'rule'
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
// Handle Confluence panels
|
|
793
|
+
else if (line.match(/^\{panel:/)) {
|
|
794
|
+
const panelMatch = line.match(/^\{panel:title=([^|]*)[^}]*\}$/);
|
|
795
|
+
if (panelMatch) {
|
|
796
|
+
const title = panelMatch[1];
|
|
797
|
+
// Start collecting panel content
|
|
798
|
+
const panelContent = [];
|
|
799
|
+
i++; // Move to next line
|
|
800
|
+
// Collect content until we hit {panel}
|
|
801
|
+
while (i < lines.length && !lines[i].trim().match(/^\{panel\}$/)) {
|
|
802
|
+
const panelLine = lines[i].trim();
|
|
803
|
+
if (panelLine) {
|
|
804
|
+
panelContent.push({
|
|
805
|
+
type: 'paragraph',
|
|
806
|
+
content: this.parseInlineFormatting(panelLine)
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
i++;
|
|
810
|
+
}
|
|
811
|
+
// Create panel as an info panel (closest ADF equivalent)
|
|
812
|
+
content.push({
|
|
813
|
+
type: 'panel',
|
|
814
|
+
attrs: { panelType: 'info' },
|
|
815
|
+
content: [
|
|
816
|
+
{
|
|
817
|
+
type: 'paragraph',
|
|
818
|
+
content: [{
|
|
819
|
+
type: 'text',
|
|
820
|
+
text: title,
|
|
821
|
+
marks: [{ type: 'strong' }]
|
|
822
|
+
}]
|
|
823
|
+
},
|
|
824
|
+
...panelContent
|
|
825
|
+
]
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Handle bullet points
|
|
830
|
+
else if (line.startsWith('- ') || line.startsWith('* ')) {
|
|
831
|
+
// Check if we need to start a new bullet list or continue existing one
|
|
832
|
+
const lastItem = content[content.length - 1];
|
|
833
|
+
const bulletText = line.substring(2).trim();
|
|
834
|
+
if (lastItem && lastItem.type === 'bulletList') {
|
|
835
|
+
lastItem.content.push({
|
|
836
|
+
type: 'listItem',
|
|
837
|
+
content: [{
|
|
838
|
+
type: 'paragraph',
|
|
839
|
+
content: this.parseInlineFormatting(bulletText)
|
|
840
|
+
}]
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
content.push({
|
|
845
|
+
type: 'bulletList',
|
|
846
|
+
content: [{
|
|
847
|
+
type: 'listItem',
|
|
848
|
+
content: [{
|
|
849
|
+
type: 'paragraph',
|
|
850
|
+
content: this.parseInlineFormatting(bulletText)
|
|
851
|
+
}]
|
|
852
|
+
}]
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// Handle numbered lists
|
|
857
|
+
else if (/^\d+\.\s/.test(line)) {
|
|
858
|
+
const match = line.match(/^\d+\.\s(.+)$/);
|
|
859
|
+
if (match) {
|
|
860
|
+
const listText = match[1];
|
|
861
|
+
const lastItem = content[content.length - 1];
|
|
862
|
+
if (lastItem && lastItem.type === 'orderedList') {
|
|
863
|
+
lastItem.content.push({
|
|
864
|
+
type: 'listItem',
|
|
865
|
+
content: [{
|
|
866
|
+
type: 'paragraph',
|
|
867
|
+
content: this.parseInlineFormatting(listText)
|
|
868
|
+
}]
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
content.push({
|
|
873
|
+
type: 'orderedList',
|
|
874
|
+
content: [{
|
|
875
|
+
type: 'listItem',
|
|
876
|
+
content: [{
|
|
877
|
+
type: 'paragraph',
|
|
878
|
+
content: this.parseInlineFormatting(listText)
|
|
879
|
+
}]
|
|
880
|
+
}]
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Handle code blocks (```)
|
|
886
|
+
else if (line.startsWith('```')) {
|
|
887
|
+
const codeLines = [];
|
|
888
|
+
i++; // Skip the opening ```
|
|
889
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
890
|
+
codeLines.push(lines[i]);
|
|
891
|
+
i++;
|
|
892
|
+
}
|
|
893
|
+
content.push({
|
|
894
|
+
type: 'codeBlock',
|
|
895
|
+
content: [{
|
|
896
|
+
type: 'text',
|
|
897
|
+
text: codeLines.join('\n')
|
|
898
|
+
}]
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
// Handle markdown tables - convert to proper ADF table structure
|
|
902
|
+
else if (line.includes('|') && line.trim() !== '|') {
|
|
903
|
+
const cells = line.split('|').map(cell => cell.trim()).filter(cell => cell !== '');
|
|
904
|
+
if (cells.length >= 2) {
|
|
905
|
+
// Collect all table rows from current position
|
|
906
|
+
const allTableRows = [];
|
|
907
|
+
// Start from current line and collect consecutive table rows
|
|
908
|
+
let tableEndIndex = i;
|
|
909
|
+
for (let tableIdx = i; tableIdx < lines.length; tableIdx++) {
|
|
910
|
+
const tableLine = lines[tableIdx].trim();
|
|
911
|
+
// Skip empty lines
|
|
912
|
+
if (tableLine === '') {
|
|
913
|
+
tableEndIndex = tableIdx;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
// Stop if no pipes (end of table)
|
|
917
|
+
if (!tableLine.includes('|')) {
|
|
918
|
+
tableEndIndex = tableIdx - 1;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
// Skip separator lines like |-|-|
|
|
922
|
+
if (tableLine.match(/^\|?[\s\-:|]+\|?$/)) {
|
|
923
|
+
tableEndIndex = tableIdx;
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
// Parse row cells
|
|
927
|
+
const rowCells = tableLine.split('|').map(cell => cell.trim()).filter(cell => cell !== '');
|
|
928
|
+
if (rowCells.length >= 2) {
|
|
929
|
+
allTableRows.push(rowCells);
|
|
930
|
+
tableEndIndex = tableIdx;
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
tableEndIndex = tableIdx - 1;
|
|
934
|
+
break; // End of table
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
// Convert to proper ADF table structure if we have table rows
|
|
938
|
+
if (allTableRows.length > 0) {
|
|
939
|
+
// Create proper ADF table structure
|
|
940
|
+
const tableRows = [];
|
|
941
|
+
// Header row (first row)
|
|
942
|
+
if (allTableRows.length > 0) {
|
|
943
|
+
const headerCells = allTableRows[0].map(cellText => ({
|
|
944
|
+
type: 'tableHeader',
|
|
945
|
+
attrs: {},
|
|
946
|
+
content: [{
|
|
947
|
+
type: 'paragraph',
|
|
948
|
+
content: this.parseInlineFormatting(cellText)
|
|
949
|
+
}]
|
|
950
|
+
}));
|
|
951
|
+
tableRows.push({
|
|
952
|
+
type: 'tableRow',
|
|
953
|
+
content: headerCells
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
// Data rows (remaining rows)
|
|
957
|
+
for (let rowIdx = 1; rowIdx < allTableRows.length; rowIdx++) {
|
|
958
|
+
const dataCells = allTableRows[rowIdx].map(cellText => ({
|
|
959
|
+
type: 'tableCell',
|
|
960
|
+
attrs: {},
|
|
961
|
+
content: [{
|
|
962
|
+
type: 'paragraph',
|
|
963
|
+
content: this.parseInlineFormatting(cellText)
|
|
964
|
+
}]
|
|
965
|
+
}));
|
|
966
|
+
tableRows.push({
|
|
967
|
+
type: 'tableRow',
|
|
968
|
+
content: dataCells
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
// Create the complete table structure
|
|
972
|
+
content.push({
|
|
973
|
+
type: 'table',
|
|
974
|
+
attrs: {
|
|
975
|
+
isNumberColumnEnabled: false,
|
|
976
|
+
layout: 'default'
|
|
977
|
+
},
|
|
978
|
+
content: tableRows
|
|
979
|
+
});
|
|
980
|
+
// Skip the lines we've processed
|
|
981
|
+
i = tableEndIndex; // Skip the table rows we just processed
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// If we reach here, it's not a valid table, treat as regular paragraph
|
|
986
|
+
content.push({
|
|
987
|
+
type: 'paragraph',
|
|
988
|
+
content: this.parseInlineFormatting(line)
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
// Regular paragraph
|
|
992
|
+
else {
|
|
993
|
+
content.push({
|
|
994
|
+
type: 'paragraph',
|
|
995
|
+
content: this.parseInlineFormatting(line)
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// If no content was parsed, add a simple paragraph
|
|
1000
|
+
if (content.length === 0) {
|
|
1001
|
+
content.push({
|
|
1002
|
+
type: 'paragraph',
|
|
1003
|
+
content: [{
|
|
1004
|
+
type: 'text',
|
|
1005
|
+
text: description || ''
|
|
1006
|
+
}]
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
type: 'doc',
|
|
1011
|
+
version: 1,
|
|
1012
|
+
content
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
// Parse inline formatting like **bold**, *italic*, `code`, {{code}}, emojis
|
|
1016
|
+
parseInlineFormatting(text) {
|
|
1017
|
+
const result = [];
|
|
1018
|
+
let currentPos = 0;
|
|
1019
|
+
// Process patterns in order of precedence to avoid conflicts
|
|
1020
|
+
// **bold** should be processed before *italic* to handle **text** correctly
|
|
1021
|
+
const patterns = [
|
|
1022
|
+
{ regex: /\*\*(.*?)\*\*/g, type: 'strong' }, // **bold** (process first)
|
|
1023
|
+
{ regex: /`([^`]+?)`/g, type: 'code' }, // `code` (markdown style)
|
|
1024
|
+
{ regex: /\{\{(.+?)\}\}/g, type: 'code' }, // {{code}} (Confluence/Jira wiki style)
|
|
1025
|
+
];
|
|
1026
|
+
let matches = [];
|
|
1027
|
+
// Find all formatting matches
|
|
1028
|
+
patterns.forEach(pattern => {
|
|
1029
|
+
let match;
|
|
1030
|
+
const regex = new RegExp(pattern.regex.source, 'g');
|
|
1031
|
+
while ((match = regex.exec(text)) !== null) {
|
|
1032
|
+
matches.push({
|
|
1033
|
+
start: match.index,
|
|
1034
|
+
end: match.index + match[0].length,
|
|
1035
|
+
text: match[1],
|
|
1036
|
+
type: pattern.type,
|
|
1037
|
+
fullMatch: match[0]
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
// Sort matches by position and remove overlapping matches
|
|
1042
|
+
matches.sort((a, b) => a.start - b.start);
|
|
1043
|
+
// After processing bold and code, handle italic patterns that don't overlap
|
|
1044
|
+
let tempText = text;
|
|
1045
|
+
// Remove already matched bold and code patterns from consideration
|
|
1046
|
+
matches.forEach(match => {
|
|
1047
|
+
tempText = tempText.substring(0, match.start) + ' '.repeat(match.end - match.start) + tempText.substring(match.end);
|
|
1048
|
+
});
|
|
1049
|
+
// Now find italic patterns in the remaining text
|
|
1050
|
+
const italicRegex = /\*([^*]+?)\*/g;
|
|
1051
|
+
let italicMatch;
|
|
1052
|
+
while ((italicMatch = italicRegex.exec(tempText)) !== null) {
|
|
1053
|
+
matches.push({
|
|
1054
|
+
start: italicMatch.index,
|
|
1055
|
+
end: italicMatch.index + italicMatch[0].length,
|
|
1056
|
+
text: italicMatch[1],
|
|
1057
|
+
type: 'em',
|
|
1058
|
+
fullMatch: italicMatch[0]
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
// Sort matches by position and remove overlapping matches
|
|
1062
|
+
matches.sort((a, b) => a.start - b.start);
|
|
1063
|
+
// Filter out overlapping matches (keep the first one in case of conflict)
|
|
1064
|
+
const filteredMatches = [];
|
|
1065
|
+
for (const match of matches) {
|
|
1066
|
+
const isOverlapping = filteredMatches.some(existing => (match.start >= existing.start && match.start < existing.end) ||
|
|
1067
|
+
(match.end > existing.start && match.end <= existing.end) ||
|
|
1068
|
+
(match.start <= existing.start && match.end >= existing.end));
|
|
1069
|
+
if (!isOverlapping) {
|
|
1070
|
+
filteredMatches.push(match);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// Sort filtered matches by position for proper processing
|
|
1074
|
+
filteredMatches.sort((a, b) => a.start - b.start);
|
|
1075
|
+
// Process text with formatting
|
|
1076
|
+
filteredMatches.forEach(match => {
|
|
1077
|
+
// Add text before the match
|
|
1078
|
+
if (match.start > currentPos) {
|
|
1079
|
+
const beforeText = text.substring(currentPos, match.start);
|
|
1080
|
+
if (beforeText) {
|
|
1081
|
+
result.push({
|
|
1082
|
+
type: 'text',
|
|
1083
|
+
text: beforeText
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
// Add formatted text
|
|
1088
|
+
result.push({
|
|
1089
|
+
type: 'text',
|
|
1090
|
+
text: match.text,
|
|
1091
|
+
marks: [{ type: match.type }]
|
|
1092
|
+
});
|
|
1093
|
+
currentPos = match.end;
|
|
1094
|
+
});
|
|
1095
|
+
// Add remaining text
|
|
1096
|
+
if (currentPos < text.length) {
|
|
1097
|
+
const remainingText = text.substring(currentPos);
|
|
1098
|
+
if (remainingText) {
|
|
1099
|
+
result.push({
|
|
1100
|
+
type: 'text',
|
|
1101
|
+
text: remainingText
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// If no formatting was found, return simple text
|
|
1106
|
+
if (result.length === 0) {
|
|
1107
|
+
result.push({
|
|
1108
|
+
type: 'text',
|
|
1109
|
+
text: text
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
return result;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
//# sourceMappingURL=jira.js.map
|