jira-pat 1.0.4 → 1.0.5
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/README.md +35 -0
- package/backend/__tests__/projects.test.js +128 -0
- package/backend/routes/issues.js +34 -0
- package/backend/routes/projects.js +11 -0
- package/backend/service/jiraService.js +276 -2
- package/frontend/dist/assets/index-BHcwfjLq.js +434 -0
- package/frontend/dist/assets/{index-C3FqLdJB.css → index-CvGcEGZd.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/frontend/package.json +6 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-Bw-K2Au_.js +0 -232
package/README.md
CHANGED
|
@@ -118,6 +118,41 @@ http://localhost:5173
|
|
|
118
118
|
|
|
119
119
|
---
|
|
120
120
|
|
|
121
|
+
## Features
|
|
122
|
+
|
|
123
|
+
### Dashboard & Search
|
|
124
|
+
- View all Jira tickets assigned to you
|
|
125
|
+
- Search tickets by keyword, issue key, or summary
|
|
126
|
+
- Filter by project and status
|
|
127
|
+
- Keyboard shortcut `/` to quickly access search
|
|
128
|
+
|
|
129
|
+
### Issue Management
|
|
130
|
+
- View full issue details in a side panel
|
|
131
|
+
- Update issue status (transitions)
|
|
132
|
+
- Reassign issues to other users
|
|
133
|
+
- Add and remove labels
|
|
134
|
+
- Update fix versions
|
|
135
|
+
- Upload attachments (drag & drop)
|
|
136
|
+
- **Edit issue summary** — click the pencil icon next to the title
|
|
137
|
+
- **Edit issue description** — click the pencil icon next to the description header
|
|
138
|
+
|
|
139
|
+
### Project Management
|
|
140
|
+
- Create new issues (Task, Bug, Story, Epic, etc.)
|
|
141
|
+
- View available projects
|
|
142
|
+
- Browse project versions/releases
|
|
143
|
+
|
|
144
|
+
### Comments
|
|
145
|
+
- View and add comments on issues
|
|
146
|
+
- Edit your own comments
|
|
147
|
+
- **@mention team members** — type `@` in the comment box to search and mention project members
|
|
148
|
+
|
|
149
|
+
### Real-time Updates
|
|
150
|
+
- Optimistic UI updates for faster feedback
|
|
151
|
+
- Loading states and error handling
|
|
152
|
+
- Toast notifications for actions
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
121
156
|
## License
|
|
122
157
|
|
|
123
158
|
ISC
|
|
@@ -265,5 +265,133 @@ describe('projects routes', () => {
|
|
|
265
265
|
expect(response.body).toHaveProperty('details');
|
|
266
266
|
});
|
|
267
267
|
});
|
|
268
|
+
|
|
269
|
+
describe('GET /api/projects/:projectKey/members', () => {
|
|
270
|
+
it('should fetch project members', async () => {
|
|
271
|
+
const mockMembers = [
|
|
272
|
+
{
|
|
273
|
+
accountId: 'acc1',
|
|
274
|
+
displayName: 'John Doe',
|
|
275
|
+
emailAddress: 'john@example.com',
|
|
276
|
+
avatarUrls: { '48x48': 'http://example.com/avatar1.png' }
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
accountId: 'acc2',
|
|
280
|
+
displayName: 'Jane Smith',
|
|
281
|
+
emailAddress: 'jane@example.com',
|
|
282
|
+
avatarUrls: {}
|
|
283
|
+
}
|
|
284
|
+
];
|
|
285
|
+
jiraService.getProjectMembers.mockResolvedValue(mockMembers);
|
|
286
|
+
|
|
287
|
+
const response = await request(server)
|
|
288
|
+
.get('/api/projects/ADW/members');
|
|
289
|
+
|
|
290
|
+
expect(response.status).toBe(200);
|
|
291
|
+
expect(response.body).toEqual(mockMembers);
|
|
292
|
+
expect(jiraService.getProjectMembers).toHaveBeenCalledWith('ADW', undefined);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should pass query parameter when searching', async () => {
|
|
296
|
+
const mockMembers = [
|
|
297
|
+
{ accountId: 'acc1', displayName: 'John Doe', emailAddress: 'john@example.com', avatarUrls: {} }
|
|
298
|
+
];
|
|
299
|
+
jiraService.getProjectMembers.mockResolvedValue(mockMembers);
|
|
300
|
+
|
|
301
|
+
const response = await request(server)
|
|
302
|
+
.get('/api/projects/ADW/members?query=john');
|
|
303
|
+
|
|
304
|
+
expect(response.status).toBe(200);
|
|
305
|
+
expect(jiraService.getProjectMembers).toHaveBeenCalledWith('ADW', 'john');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should return empty array when no members', async () => {
|
|
309
|
+
jiraService.getProjectMembers.mockResolvedValue([]);
|
|
310
|
+
|
|
311
|
+
const response = await request(server)
|
|
312
|
+
.get('/api/projects/EMPTY/members');
|
|
313
|
+
|
|
314
|
+
expect(response.status).toBe(200);
|
|
315
|
+
expect(response.body).toEqual([]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should handle Jira API errors', async () => {
|
|
319
|
+
jiraService.getProjectMembers.mockRejectedValue(
|
|
320
|
+
new Error('Permission denied')
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const response = await request(server)
|
|
324
|
+
.get('/api/projects/ADW/members');
|
|
325
|
+
|
|
326
|
+
expect(response.status).toBe(500);
|
|
327
|
+
expect(response.body.error).toBe('Failed to fetch project members');
|
|
328
|
+
expect(response.body.details).toContain('Permission denied');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should handle authentication errors', async () => {
|
|
332
|
+
jiraService.getProjectMembers.mockRejectedValue(
|
|
333
|
+
new Error('Jira API Error: 401 - Unauthorized')
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const response = await request(server)
|
|
337
|
+
.get('/api/projects/ADW/members');
|
|
338
|
+
|
|
339
|
+
expect(response.status).toBe(500);
|
|
340
|
+
expect(response.body.error).toBe('Failed to fetch project members');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should handle network errors', async () => {
|
|
344
|
+
jiraService.getProjectMembers.mockRejectedValue(
|
|
345
|
+
new Error('connect ECONNREFUSED')
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const response = await request(server)
|
|
349
|
+
.get('/api/projects/ADW/members');
|
|
350
|
+
|
|
351
|
+
expect(response.status).toBe(500);
|
|
352
|
+
expect(response.body.details).toContain('ECONNREFUSED');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should return members with all properties', async () => {
|
|
356
|
+
const mockMembers = [
|
|
357
|
+
{
|
|
358
|
+
accountId: 'acc123',
|
|
359
|
+
displayName: 'Test User',
|
|
360
|
+
emailAddress: 'test@company.com',
|
|
361
|
+
avatarUrls: {
|
|
362
|
+
'48x48': 'http://example.com/avatar.png',
|
|
363
|
+
'32x32': 'http://example.com/avatar32.png'
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
];
|
|
367
|
+
jiraService.getProjectMembers.mockResolvedValue(mockMembers);
|
|
368
|
+
|
|
369
|
+
const response = await request(server)
|
|
370
|
+
.get('/api/projects/TEST/members');
|
|
371
|
+
|
|
372
|
+
expect(response.body[0]).toHaveProperty('accountId', 'acc123');
|
|
373
|
+
expect(response.body[0]).toHaveProperty('displayName', 'Test User');
|
|
374
|
+
expect(response.body[0]).toHaveProperty('emailAddress', 'test@company.com');
|
|
375
|
+
expect(response.body[0]).toHaveProperty('avatarUrls');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should pass project key to service', async () => {
|
|
379
|
+
jiraService.getProjectMembers.mockResolvedValue([]);
|
|
380
|
+
|
|
381
|
+
await request(server).get('/api/projects/PROJ123/members');
|
|
382
|
+
|
|
383
|
+
expect(jiraService.getProjectMembers).toHaveBeenCalledWith('PROJ123', undefined);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should handle empty query string', async () => {
|
|
387
|
+
jiraService.getProjectMembers.mockResolvedValue([]);
|
|
388
|
+
|
|
389
|
+
const response = await request(server)
|
|
390
|
+
.get('/api/projects/ADW/members?query=');
|
|
391
|
+
|
|
392
|
+
expect(response.status).toBe(200);
|
|
393
|
+
expect(jiraService.getProjectMembers).toHaveBeenCalledWith('ADW', '');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
268
396
|
});
|
|
269
397
|
|
package/backend/routes/issues.js
CHANGED
|
@@ -270,4 +270,38 @@ router.put('/:issueKey', mutationLimiter, async (req, res) => {
|
|
|
270
270
|
}
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
// PUT /api/issues/:issueKey/summary
|
|
274
|
+
router.put('/:issueKey/summary', mutationLimiter, async (req, res) => {
|
|
275
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
276
|
+
if (error) return res.status(400).json({ error });
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const { summary } = req.body;
|
|
280
|
+
if (!summary || typeof summary !== 'string') {
|
|
281
|
+
return res.status(400).json({ error: 'Summary is required' });
|
|
282
|
+
}
|
|
283
|
+
if (summary.length > 255) {
|
|
284
|
+
return res.status(400).json({ error: 'Summary must be under 255 characters' });
|
|
285
|
+
}
|
|
286
|
+
await jiraService.updateIssueSummary(req.params.issueKey, summary.trim());
|
|
287
|
+
res.json({ success: true });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
res.status(500).json({ error: 'Failed to update summary', details: error.message });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// PUT /api/issues/:issueKey/description
|
|
294
|
+
router.put('/:issueKey/description', mutationLimiter, async (req, res) => {
|
|
295
|
+
const error = validateIssueKey(req.params.issueKey);
|
|
296
|
+
if (error) return res.status(400).json({ error });
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const { description } = req.body;
|
|
300
|
+
await jiraService.updateIssueDescription(req.params.issueKey, description || '');
|
|
301
|
+
res.json({ success: true });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
res.status(500).json({ error: 'Failed to update description', details: error.message });
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
273
307
|
module.exports = router;
|
|
@@ -32,4 +32,15 @@ router.get('/:projectKey/versions', async (req, res) => {
|
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
// GET /api/projects/:projectKey/members
|
|
36
|
+
router.get('/:projectKey/members', async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const { query } = req.query;
|
|
39
|
+
const members = await jiraService.getProjectMembers(req.params.projectKey, query);
|
|
40
|
+
res.json(members);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
res.status(500).json({ error: 'Failed to fetch project members', details: error.message });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
35
46
|
module.exports = router;
|
|
@@ -549,6 +549,212 @@ const updateIssue = async (issueKey, fields) => {
|
|
|
549
549
|
}
|
|
550
550
|
};
|
|
551
551
|
|
|
552
|
+
const updateIssueSummary = async (issueKey, summary) => {
|
|
553
|
+
try {
|
|
554
|
+
const client = getJiraClient();
|
|
555
|
+
await client.put(`/rest/api/3/issue/${issueKey}`, {
|
|
556
|
+
fields: { summary }
|
|
557
|
+
});
|
|
558
|
+
invalidateIssueCache(issueKey);
|
|
559
|
+
return { success: true };
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error(`Error updating issue summary ${issueKey}:`, error.message);
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const convertHtmlToAdf = (html) => {
|
|
567
|
+
if (!html || !html.trim()) return null;
|
|
568
|
+
|
|
569
|
+
const doc = {
|
|
570
|
+
type: 'doc',
|
|
571
|
+
version: 1,
|
|
572
|
+
content: []
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
let content = [];
|
|
576
|
+
let remaining = html;
|
|
577
|
+
|
|
578
|
+
const extractTag = (str) => {
|
|
579
|
+
const match = str.match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
|
|
580
|
+
return match ? match[1].toLowerCase() : null;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const extractClosingTag = (str, tag) => {
|
|
584
|
+
return new RegExp(`</${tag}>`, 'i');
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const extractAttribute = (tagStr, attrName) => {
|
|
588
|
+
const match = tagStr.match(new RegExp(`${attrName}=["']([^"']*)["']`, 'i'));
|
|
589
|
+
return match ? match[1] : null;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const processElement = (htmlStr) => {
|
|
593
|
+
const tag = extractTag(htmlStr);
|
|
594
|
+
if (!tag) return null;
|
|
595
|
+
|
|
596
|
+
const tagMatch = htmlStr.match(new RegExp(`<${tag}[^>]*>`, 'i'));
|
|
597
|
+
if (!tagMatch) return null;
|
|
598
|
+
|
|
599
|
+
const fullTag = tagMatch[0];
|
|
600
|
+
const closingPattern = extractClosingTag(htmlStr, tag);
|
|
601
|
+
const closeMatch = htmlStr.match(closingPattern);
|
|
602
|
+
|
|
603
|
+
let innerContent = '';
|
|
604
|
+
let afterContent = htmlStr;
|
|
605
|
+
|
|
606
|
+
if (closeMatch) {
|
|
607
|
+
const closeIndex = htmlStr.indexOf(closeMatch[0]);
|
|
608
|
+
innerContent = htmlStr.substring(tagMatch[0].length, closeIndex);
|
|
609
|
+
afterContent = htmlStr.substring(closeIndex + closeMatch[0].length);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const cleanText = (text) => text.replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
613
|
+
|
|
614
|
+
switch (tag) {
|
|
615
|
+
case 'p':
|
|
616
|
+
case 'div':
|
|
617
|
+
if (innerContent.trim()) {
|
|
618
|
+
return {
|
|
619
|
+
node: { type: 'paragraph', content: [{ type: 'text', text: cleanText(innerContent) }] },
|
|
620
|
+
remaining: afterContent
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
return { node: null, remaining: afterContent };
|
|
624
|
+
|
|
625
|
+
case 'strong':
|
|
626
|
+
case 'b':
|
|
627
|
+
if (innerContent.trim()) {
|
|
628
|
+
return {
|
|
629
|
+
node: { type: 'text', marks: [{ type: 'bold' }], text: cleanText(innerContent) },
|
|
630
|
+
remaining: afterContent
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return { node: null, remaining: afterContent };
|
|
634
|
+
|
|
635
|
+
case 'em':
|
|
636
|
+
case 'i':
|
|
637
|
+
if (innerContent.trim()) {
|
|
638
|
+
return {
|
|
639
|
+
node: { type: 'text', marks: [{ type: 'italic' }], text: cleanText(innerContent) },
|
|
640
|
+
remaining: afterContent
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
return { node: null, remaining: afterContent };
|
|
644
|
+
|
|
645
|
+
case 'a':
|
|
646
|
+
const href = extractAttribute(fullTag, 'href');
|
|
647
|
+
if (href && innerContent.trim()) {
|
|
648
|
+
return {
|
|
649
|
+
node: { type: 'text', marks: [{ type: 'link', attrs: { href } }], text: cleanText(innerContent) },
|
|
650
|
+
remaining: afterContent
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return { node: null, remaining: afterContent };
|
|
654
|
+
|
|
655
|
+
case 'img':
|
|
656
|
+
const src = extractAttribute(fullTag, 'src');
|
|
657
|
+
if (src) {
|
|
658
|
+
return {
|
|
659
|
+
node: { type: 'image', attrs: { src } },
|
|
660
|
+
remaining: afterContent
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
return { node: null, remaining: afterContent };
|
|
664
|
+
|
|
665
|
+
case 'h1':
|
|
666
|
+
case 'h2':
|
|
667
|
+
case 'h3':
|
|
668
|
+
case 'h4':
|
|
669
|
+
case 'h5':
|
|
670
|
+
case 'h6':
|
|
671
|
+
const level = parseInt(tag.charAt(1));
|
|
672
|
+
if (innerContent.trim()) {
|
|
673
|
+
return {
|
|
674
|
+
node: { type: 'heading', attrs: { level }, content: [{ type: 'text', text: cleanText(innerContent) }] },
|
|
675
|
+
remaining: afterContent
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
return { node: null, remaining: afterContent };
|
|
679
|
+
|
|
680
|
+
case 'br':
|
|
681
|
+
return {
|
|
682
|
+
node: { type: 'text', text: '\n' },
|
|
683
|
+
remaining: afterContent
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
default:
|
|
687
|
+
if (innerContent.trim()) {
|
|
688
|
+
return {
|
|
689
|
+
node: { type: 'paragraph', content: [{ type: 'text', text: cleanText(innerContent) }] },
|
|
690
|
+
remaining: afterContent
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return { node: null, remaining: afterContent };
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
while (remaining.trim()) {
|
|
698
|
+
const ltIndex = remaining.indexOf('<');
|
|
699
|
+
|
|
700
|
+
if (ltIndex === -1) {
|
|
701
|
+
const text = remaining.trim();
|
|
702
|
+
if (text) {
|
|
703
|
+
content.push({ type: 'paragraph', content: [{ type: 'text', text }] });
|
|
704
|
+
}
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (ltIndex > 0) {
|
|
709
|
+
const text = remaining.substring(0, ltIndex).trim();
|
|
710
|
+
if (text) {
|
|
711
|
+
content.push({ type: 'paragraph', content: [{ type: 'text', text }] });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const result = processElement(remaining.substring(ltIndex));
|
|
716
|
+
if (result && result.node) {
|
|
717
|
+
content.push(result.node);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (result && result.remaining) {
|
|
721
|
+
remaining = result.remaining;
|
|
722
|
+
} else {
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (content.length === 0) {
|
|
728
|
+
content.push({
|
|
729
|
+
type: 'paragraph',
|
|
730
|
+
content: [{ type: 'text', text: '' }]
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
doc.content = content;
|
|
735
|
+
return doc;
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const updateIssueDescription = async (issueKey, description) => {
|
|
739
|
+
try {
|
|
740
|
+
const client = getJiraClient();
|
|
741
|
+
|
|
742
|
+
let descADF = null;
|
|
743
|
+
if (description && description.trim()) {
|
|
744
|
+
descADF = convertHtmlToAdf(description);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
await client.put(`/rest/api/3/issue/${issueKey}`, {
|
|
748
|
+
fields: { description: descADF }
|
|
749
|
+
});
|
|
750
|
+
invalidateIssueCache(issueKey);
|
|
751
|
+
return { success: true };
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error(`Error updating issue description ${issueKey}:`, error.message);
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
552
758
|
const searchLabels = async (query) => {
|
|
553
759
|
try {
|
|
554
760
|
const client = getJiraClient();
|
|
@@ -558,7 +764,6 @@ const searchLabels = async (query) => {
|
|
|
558
764
|
fieldValue: query || ''
|
|
559
765
|
}
|
|
560
766
|
});
|
|
561
|
-
// response.data.results is an array of { displayName, value }
|
|
562
767
|
return (response.data.results || []).map(item => item.value);
|
|
563
768
|
} catch (error) {
|
|
564
769
|
console.error('Error searching labels:', error.message);
|
|
@@ -566,6 +771,72 @@ const searchLabels = async (query) => {
|
|
|
566
771
|
}
|
|
567
772
|
};
|
|
568
773
|
|
|
774
|
+
const getProjectMembers = async (projectKey, query) => {
|
|
775
|
+
const cacheKey = `project_members_${projectKey}_${query || 'all'}`;
|
|
776
|
+
const cachedMembers = cache.get(cacheKey);
|
|
777
|
+
|
|
778
|
+
if (cachedMembers) {
|
|
779
|
+
console.log(`Returning cached project members for ${projectKey}`);
|
|
780
|
+
return cachedMembers;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
try {
|
|
784
|
+
const client = getJiraClient();
|
|
785
|
+
console.log(`Fetching project members for ${projectKey}${query ? ` (query: ${query})` : ''}`);
|
|
786
|
+
|
|
787
|
+
const seen = new Map();
|
|
788
|
+
|
|
789
|
+
const issuesResponse = await client.get('/rest/api/3/search/jql', {
|
|
790
|
+
params: {
|
|
791
|
+
jql: `project = "${projectKey}" ORDER BY updated DESC`,
|
|
792
|
+
maxResults: 100,
|
|
793
|
+
fields: 'assignee,reporter'
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
for (const issue of (issuesResponse.data.issues || [])) {
|
|
798
|
+
const assignee = issue.fields?.assignee;
|
|
799
|
+
if (assignee && assignee.accountId && !seen.has(assignee.accountId)) {
|
|
800
|
+
seen.set(assignee.accountId, {
|
|
801
|
+
accountId: assignee.accountId,
|
|
802
|
+
displayName: assignee.displayName,
|
|
803
|
+
emailAddress: assignee.emailAddress || '',
|
|
804
|
+
avatarUrls: assignee.avatarUrls || {}
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const reporter = issue.fields?.reporter;
|
|
809
|
+
if (reporter && reporter.accountId && !seen.has(reporter.accountId)) {
|
|
810
|
+
seen.set(reporter.accountId, {
|
|
811
|
+
accountId: reporter.accountId,
|
|
812
|
+
displayName: reporter.displayName,
|
|
813
|
+
emailAddress: reporter.emailAddress || '',
|
|
814
|
+
avatarUrls: reporter.avatarUrls || {}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let members = Array.from(seen.values());
|
|
820
|
+
|
|
821
|
+
if (query && query.trim()) {
|
|
822
|
+
const searchTerm = query.toLowerCase();
|
|
823
|
+
members = members.filter(m =>
|
|
824
|
+
m.displayName.toLowerCase().includes(searchTerm) ||
|
|
825
|
+
(m.emailAddress && m.emailAddress.toLowerCase().includes(searchTerm))
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
members.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
830
|
+
|
|
831
|
+
cache.set(cacheKey, members, 3600);
|
|
832
|
+
|
|
833
|
+
return members;
|
|
834
|
+
} catch (error) {
|
|
835
|
+
console.error(`Error fetching project members for ${projectKey}:`, error.message);
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
569
840
|
module.exports = {
|
|
570
841
|
searchIssues,
|
|
571
842
|
getIssueDetail,
|
|
@@ -582,8 +853,11 @@ module.exports = {
|
|
|
582
853
|
getIssueTypes,
|
|
583
854
|
getProjectVersions,
|
|
584
855
|
updateIssue,
|
|
856
|
+
updateIssueSummary,
|
|
857
|
+
updateIssueDescription,
|
|
585
858
|
searchLabels,
|
|
586
859
|
createIssue,
|
|
587
860
|
downloadAttachment,
|
|
588
|
-
adfToText
|
|
861
|
+
adfToText,
|
|
862
|
+
getProjectMembers
|
|
589
863
|
};
|