jira-pat 1.0.4 → 1.0.6

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 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/index.js CHANGED
@@ -47,29 +47,29 @@ app.get('/api/health', (req, res) => {
47
47
 
48
48
  // Serve frontend static files
49
49
  const frontendDist = path.resolve(__dirname, '..', 'frontend', 'dist');
50
- console.log('--- Static Assets Setup ---');
51
- console.log('Searching for frontend at:', frontendDist);
52
-
53
- if (require('fs').existsSync(frontendDist)) {
54
- console.log('✅ Frontend dist found. Serving static files.');
50
+ console.debug('--- Static Assets Setup ---');
51
+ console.debug('Searching for frontend at:', frontendDist);
52
+ if (require('fs').existsSync(frontendDist)) {
53
+ console.debug('✅ Frontend dist found. Serving static files.');
54
+
55
55
  app.use(express.static(frontendDist));
56
56
 
57
- app.use((req, res, next) => {
58
- if (!req.path.startsWith('/api') && req.method === 'GET') {
59
- const indexPath = path.resolve(frontendDist, 'index.html');
60
- if (require('fs').existsSync(indexPath)) {
61
- return res.sendFile(indexPath);
62
- } else {
63
- console.error('❌ Error: index.html not found even though dist exists!');
64
- return res.status(404).send('Dashboard files missing (index.html)');
65
- }
66
- }
67
- next();
68
- });
69
- } else {
70
- console.warn('⚠️ Warning: frontend/dist directory NOT found at ' + frontendDist);
71
- }
57
+ app.use((req, res, next) => {
58
+ if (!req.path.startsWith('/api') && req.method === 'GET') {
59
+ const indexPath = path.resolve(frontendDist, 'index.html');
60
+ if (require('fs').existsSync(indexPath)) {
61
+ return res.sendFile(indexPath);
62
+ } else {
63
+ console.error('❌ Error: index.html not found even though dist exists!');
64
+ return res.status(404).send('Dashboard files missing (index.html)');
65
+ }
66
+ }
67
+ next();
68
+ });
69
+ } else {
70
+ console.warn('⚠️ Warning: frontend/dist directory NOT found at ' + frontendDist);
71
+ }
72
72
 
73
- app.listen(PORT, () => {
74
- console.log(`Server is running on port ${PORT}`);
75
- });
73
+ app.listen(PORT, () => {
74
+ console.debug(`Server is running on port ${PORT}`);
75
+ });
@@ -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;
@@ -100,12 +100,12 @@ const searchIssues = async (params) => {
100
100
  const cacheKey = `search_${jql}_${startAt}_${maxResults}`;
101
101
  const cachedResult = cache.get(cacheKey);
102
102
  if (cachedResult) {
103
- console.log('Returning from cache for JQL:', jql);
103
+ console.debug('Returning from cache for JQL:', jql);
104
104
  return cachedResult;
105
105
  }
106
106
 
107
107
  const client = getJiraClient();
108
- console.log(`Querying Jira API: ${jql}`);
108
+ console.debug(`Querying Jira API: ${jql}`);
109
109
 
110
110
  const response = await client.get('/rest/api/3/search/jql', {
111
111
  params: {
@@ -305,7 +305,7 @@ const addComment = async (issueKey, text) => {
305
305
  const editComment = async (issueKey, commentId, text) => {
306
306
  try {
307
307
  const client = getJiraClient();
308
- console.log(`Editing comment ${commentId} on ${issueKey}...`);
308
+ console.debug(`Editing comment ${commentId} on ${issueKey}...`);
309
309
  const adfBody = {
310
310
  version: 1,
311
311
  type: "doc",
@@ -341,14 +341,14 @@ const editComment = async (issueKey, commentId, text) => {
341
341
  const getCurrentUser = async () => {
342
342
  try {
343
343
  const client = getJiraClient();
344
- console.log('Fetching current user info from Jira...');
344
+ console.debug('Fetching current user info from Jira...');
345
345
  const response = await client.get('/rest/api/3/myself');
346
346
  const user = {
347
347
  accountId: response.data.accountId,
348
348
  displayName: response.data.displayName,
349
349
  email: response.data.emailAddress
350
350
  };
351
- console.log('Current user loaded:', user.displayName, '(', user.accountId, ')');
351
+ console.debug('Current user loaded:', user.displayName, '(', user.accountId, ')');
352
352
  return user;
353
353
  } catch (error) {
354
354
  console.error('Error fetching current user:', error.message);
@@ -386,13 +386,13 @@ const getAssignableUsers = async (issueKey, query) => {
386
386
  const cachedUsers = cache.get(cacheKey);
387
387
 
388
388
  if (cachedUsers) {
389
- console.log(`Returning cached assignable users for ${issueKey}${query ? ` (query: ${query})` : ''}`);
389
+ console.debug(`Returning cached assignable users for ${issueKey}${query ? ` (query: ${query})` : ''}`);
390
390
  return cachedUsers;
391
391
  }
392
392
 
393
393
  try {
394
394
  const client = getJiraClient();
395
- console.log(`Querying Jira API for assignable users: ${issueKey}${query ? ` (query: ${query})` : ''}`);
395
+ console.debug(`Querying Jira API for assignable users: ${issueKey}${query ? ` (query: ${query})` : ''}`);
396
396
 
397
397
  const params = { issueKey };
398
398
  if (query) params.query = query;
@@ -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(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/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.debug(`Returning cached project members for ${projectKey}`);
780
+ return cachedMembers;
781
+ }
782
+
783
+ try {
784
+ const client = getJiraClient();
785
+ console.debug(`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
  };
@@ -0,0 +1 @@
1
+ import{j as e}from"./vendor-tiptap-D82WYWec.js";import{R as v,a as r,X as b,b as N,h as f}from"./vendor-ui-Cdo3Wdx4.js";import{f as C,c as S}from"./index-CadqjZnU.js";import{u as T}from"./useFocusTrap-DDp1tqrR.js";const B=v.memo(function({isOpen:i,onClose:l,onCreated:h,projects:j,projectsLoading:o,addToast:c}){const[x,n]=r.useState([]),[a,t]=r.useState({projectKey:"ADW",summary:"",description:"",issueType:"",priority:""}),[u,m]=r.useState(!1),[d,p]=r.useState("");r.useEffect(()=>{a.projectKey?C(a.projectKey).then(n).catch(s=>console.error("Error fetching issue types:",s)):n([])},[a.projectKey]);const y=r.useRef(null);T(y,i);const g=async()=>{if(!(!a.projectKey||!a.summary||!a.issueType)){m(!0),p("");try{await S(a),t({projectKey:"",summary:"",description:"",issueType:"",priority:""}),h(),c("Issue created successfully","success")}catch(s){p(s.message||"Failed to create issue"),c(`Error creating issue: ${s.message}`,"error")}finally{m(!1)}}};return i?e.jsx("div",{className:"modal-overlay",ref:y,onClick:l,children:e.jsxs("div",{className:"modal",onClick:s=>s.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":"modal-title",children:[e.jsxs("div",{className:"modal-header",children:[e.jsx("h2",{id:"modal-title",children:"Create Issue"}),e.jsx("button",{className:"close-btn",onClick:l,"aria-label":"Close modal",children:e.jsx(b,{size:24})})]}),e.jsxs("div",{className:"modal-body",children:[d&&e.jsxs("div",{className:"error",role:"alert",style:{marginBottom:"1rem"},children:[e.jsx(N,{size:16,style:{marginRight:"0.5rem"}}),d]}),e.jsxs("div",{className:"input-group",style:{marginBottom:"1rem"},children:[e.jsx("label",{className:"input-label",children:"Project *"}),e.jsxs("select",{className:"select-input",style:{width:"100%"},value:a.projectKey,onChange:s=>t({...a,projectKey:s.target.value,issueType:""}),"aria-label":"Select project",children:[e.jsx("option",{value:"",children:o?"Loading...":"Select Project"}),!o&&j.map(s=>e.jsxs("option",{value:s.key,children:[s.name," (",s.key,")"]},s.id))]})]}),a.projectKey&&e.jsxs("div",{className:"input-group",style:{marginBottom:"1rem"},children:[e.jsx("label",{className:"input-label",children:"Issue Type *"}),e.jsxs("select",{className:"select-input",style:{width:"100%"},value:a.issueType,onChange:s=>t({...a,issueType:s.target.value}),"aria-label":"Select issue type",children:[e.jsx("option",{value:"",children:"Select Type"}),x.filter(s=>!s.subtask).map(s=>e.jsx("option",{value:s.id,children:s.name},s.id))]})]}),e.jsxs("div",{className:"input-group",style:{marginBottom:"1rem"},children:[e.jsx("label",{className:"input-label",children:"Summary *"}),e.jsx("input",{type:"text",className:"text-input",style:{width:"100%"},placeholder:"Short description",value:a.summary,onChange:s=>t({...a,summary:s.target.value}),"aria-label":"Issue summary"})]}),e.jsxs("div",{className:"input-group",style:{marginBottom:"1rem"},children:[e.jsx("label",{className:"input-label",children:"Description"}),e.jsx("textarea",{className:"comment-box",style:{width:"100%",minHeight:"100px"},placeholder:"Detailed description...",value:a.description,onChange:s=>t({...a,description:s.target.value}),"aria-label":"Issue description"})]}),e.jsxs("div",{className:"input-group",style:{marginBottom:"1rem"},children:[e.jsx("label",{className:"input-label",children:"Priority (Optional)"}),e.jsxs("select",{className:"select-input",style:{width:"100%"},value:a.priority,onChange:s=>t({...a,priority:s.target.value}),"aria-label":"Select priority",children:[e.jsx("option",{value:"",children:"Default Priority"}),e.jsx("option",{value:"1",children:"Highest"}),e.jsx("option",{value:"2",children:"High"}),e.jsx("option",{value:"3",children:"Medium"}),e.jsx("option",{value:"4",children:"Low"}),e.jsx("option",{value:"5",children:"Lowest"})]})]})]}),e.jsxs("div",{className:"modal-footer",children:[e.jsx("button",{className:"btn-secondary",onClick:l,children:"Cancel"}),e.jsx("button",{className:"btn-primary",onClick:g,disabled:!a.projectKey||!a.summary||!a.issueType||u,"aria-label":"Create issue",children:u?e.jsxs(e.Fragment,{children:[e.jsx(f,{size:16,className:"spinner"}),"Creating..."]}):"Create"})]})]})}):null});export{B as default};