jira-pat 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.
Files changed (95) hide show
  1. package/AGENTS.md +218 -0
  2. package/README.md +64 -0
  3. package/backend/.env.example +1 -0
  4. package/backend/__tests__/getJiraClient.test.js +57 -0
  5. package/backend/__tests__/issues.test.js +565 -0
  6. package/backend/__tests__/jiraService.test.js +1127 -0
  7. package/backend/__tests__/projects.test.js +256 -0
  8. package/backend/coverage/clover.xml +426 -0
  9. package/backend/coverage/coverage-final.json +4 -0
  10. package/backend/coverage/lcov-report/base.css +224 -0
  11. package/backend/coverage/lcov-report/block-navigation.js +87 -0
  12. package/backend/coverage/lcov-report/favicon.png +0 -0
  13. package/backend/coverage/lcov-report/index.html +131 -0
  14. package/backend/coverage/lcov-report/prettify.css +1 -0
  15. package/backend/coverage/lcov-report/prettify.js +2 -0
  16. package/backend/coverage/lcov-report/routes/index.html +131 -0
  17. package/backend/coverage/lcov-report/routes/issues.js.html +823 -0
  18. package/backend/coverage/lcov-report/routes/projects.js.html +190 -0
  19. package/backend/coverage/lcov-report/service/index.html +116 -0
  20. package/backend/coverage/lcov-report/service/jiraService.js.html +1663 -0
  21. package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  22. package/backend/coverage/lcov-report/sorter.js +210 -0
  23. package/backend/coverage/lcov.info +707 -0
  24. package/backend/index.js +38 -0
  25. package/backend/jest.config.js +11 -0
  26. package/backend/package-lock.json +5636 -0
  27. package/backend/package.json +28 -0
  28. package/backend/routes/issues.js +246 -0
  29. package/backend/routes/projects.js +35 -0
  30. package/backend/service/jiraService.js +526 -0
  31. package/bin/jira.js +92 -0
  32. package/frontend/.env.example +1 -0
  33. package/frontend/coverage/base.css +224 -0
  34. package/frontend/coverage/block-navigation.js +87 -0
  35. package/frontend/coverage/clover.xml +559 -0
  36. package/frontend/coverage/components/CreateIssueModal.jsx.html +592 -0
  37. package/frontend/coverage/components/IssueDetailPanel.jsx.html +1633 -0
  38. package/frontend/coverage/components/IssueDrawer.jsx.html +550 -0
  39. package/frontend/coverage/components/IssueTable.jsx.html +571 -0
  40. package/frontend/coverage/components/SkeletonComponents.jsx.html +223 -0
  41. package/frontend/coverage/components/ToastContainer.jsx.html +142 -0
  42. package/frontend/coverage/components/index.html +191 -0
  43. package/frontend/coverage/coverage-final.json +14 -0
  44. package/frontend/coverage/favicon.png +0 -0
  45. package/frontend/coverage/hooks/index.html +161 -0
  46. package/frontend/coverage/hooks/useFocusTrap.js.html +262 -0
  47. package/frontend/coverage/hooks/useIssueDrawer.js.html +1000 -0
  48. package/frontend/coverage/hooks/useIssuesList.js.html +175 -0
  49. package/frontend/coverage/hooks/useToasts.js.html +142 -0
  50. package/frontend/coverage/index.html +161 -0
  51. package/frontend/coverage/prettify.css +1 -0
  52. package/frontend/coverage/prettify.js +2 -0
  53. package/frontend/coverage/services/api.js.html +547 -0
  54. package/frontend/coverage/services/index.html +116 -0
  55. package/frontend/coverage/sort-arrow-sprite.png +0 -0
  56. package/frontend/coverage/sorter.js +210 -0
  57. package/frontend/coverage/utils/index.html +131 -0
  58. package/frontend/coverage/utils/issueHelpers.jsx.html +334 -0
  59. package/frontend/coverage/utils/sanitize.js.html +166 -0
  60. package/frontend/index.html +13 -0
  61. package/frontend/package-lock.json +3436 -0
  62. package/frontend/package.json +30 -0
  63. package/frontend/src/App.jsx +447 -0
  64. package/frontend/src/__tests__/components/CreateIssueModal.test.jsx +375 -0
  65. package/frontend/src/__tests__/components/IssueDetailPanel.test.jsx +962 -0
  66. package/frontend/src/__tests__/components/IssueDrawer.test.jsx +240 -0
  67. package/frontend/src/__tests__/components/IssueTable.test.jsx +423 -0
  68. package/frontend/src/__tests__/components/ToastContainer.test.jsx +196 -0
  69. package/frontend/src/__tests__/hooks/useFocusTrap.test.js +197 -0
  70. package/frontend/src/__tests__/hooks/useIssueDrawer.test.js +1053 -0
  71. package/frontend/src/__tests__/hooks/useIssuesList.test.js +175 -0
  72. package/frontend/src/__tests__/hooks/useToasts.test.js +110 -0
  73. package/frontend/src/__tests__/services/api.test.js +568 -0
  74. package/frontend/src/__tests__/setup.js +54 -0
  75. package/frontend/src/__tests__/utils/issueHelpers.test.jsx +336 -0
  76. package/frontend/src/__tests__/utils/sanitize.test.js +238 -0
  77. package/frontend/src/components/CreateIssueModal.jsx +169 -0
  78. package/frontend/src/components/ErrorBoundary.jsx +52 -0
  79. package/frontend/src/components/IssueDetailPanel.jsx +517 -0
  80. package/frontend/src/components/IssueDrawer.jsx +155 -0
  81. package/frontend/src/components/IssueTable.jsx +162 -0
  82. package/frontend/src/components/SkeletonComponents.jsx +46 -0
  83. package/frontend/src/components/StandaloneIssuePage.jsx +176 -0
  84. package/frontend/src/components/ToastContainer.jsx +19 -0
  85. package/frontend/src/hooks/useFocusTrap.js +59 -0
  86. package/frontend/src/hooks/useIssueDrawer.js +305 -0
  87. package/frontend/src/hooks/useIssuesList.js +30 -0
  88. package/frontend/src/hooks/useToasts.js +19 -0
  89. package/frontend/src/index.css +2070 -0
  90. package/frontend/src/main.jsx +13 -0
  91. package/frontend/src/services/api.js +154 -0
  92. package/frontend/src/utils/issueHelpers.jsx +84 -0
  93. package/frontend/src/utils/sanitize.js +27 -0
  94. package/frontend/vite.config.js +15 -0
  95. package/package.json +19 -0
@@ -0,0 +1,305 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ fetchIssueDetail, fetchComments, postComment, fetchTransitions,
4
+ transitionIssue, fetchAssignableUsers, assignIssue,
5
+ uploadAttachment, fetchProjectVersions, updateIssue, searchLabels
6
+ } from '../services/api';
7
+
8
+ export function useIssueDrawer(issueKey, addToast, refetch) {
9
+ const [issueDetail, setIssueDetail] = useState(null);
10
+ const [comments, setComments] = useState([]);
11
+ const [transitions, setTransitions] = useState([]);
12
+ const [assignableUsers, setAssignableUsers] = useState([]);
13
+ const [drawerLoading, setDrawerLoading] = useState(false);
14
+ const [drawerError, setDrawerError] = useState(null);
15
+
16
+ const [newCommentText, setNewCommentText] = useState('');
17
+ const [isPostingComment, setIsPostingComment] = useState(false);
18
+ const [targetTransitionId, setTargetTransitionId] = useState('');
19
+ const [isTransitioning, setIsTransitioning] = useState(false);
20
+ const [assigneeSearch, setAssigneeSearch] = useState('');
21
+ const [targetAccountId, setTargetAccountId] = useState('');
22
+ const [isAssigning, setIsAssigning] = useState(false);
23
+ const [projectVersions, setProjectVersions] = useState([]);
24
+ const [newLabelText, setNewLabelText] = useState('');
25
+ const [labelSuggestions, setLabelSuggestions] = useState([]);
26
+ const [showLabelSuggestions, setShowLabelSuggestions] = useState(false);
27
+ const [isUpdatingLabels, setIsUpdatingLabels] = useState(false);
28
+ const [isUpdatingVersions, setIsUpdatingVersions] = useState(false);
29
+ const [isUploading, setIsUploading] = useState(false);
30
+ const [uploadMessage, setUploadMessage] = useState('');
31
+ const [isTransitioningOptimistic, setIsTransitioningOptimistic] = useState(false);
32
+ const [optimisticStatus, setOptimisticStatus] = useState(null);
33
+ const [isAssigningOptimistic, setIsAssigningOptimistic] = useState(false);
34
+ const [optimisticAssignee, setOptimisticAssignee] = useState(null);
35
+
36
+ useEffect(() => {
37
+ if (!issueKey) return;
38
+
39
+ setDrawerLoading(true);
40
+ setDrawerError(null);
41
+ setIssueDetail(null);
42
+ setComments([]);
43
+ setTransitions([]);
44
+ setAssignableUsers([]);
45
+ setNewCommentText('');
46
+ setTargetTransitionId('');
47
+ setTargetAccountId('');
48
+ setAssigneeSearch('');
49
+ setUploadMessage('');
50
+ setNewLabelText('');
51
+
52
+ const loadIssueData = async () => {
53
+ try {
54
+ const [detailData, commentsData, transitionsData, usersData] = await Promise.all([
55
+ fetchIssueDetail(issueKey),
56
+ fetchComments(issueKey),
57
+ fetchTransitions(issueKey).catch(() => []),
58
+ fetchAssignableUsers(issueKey).catch(() => [])
59
+ ]);
60
+ setIssueDetail(detailData);
61
+ setComments(commentsData);
62
+ setTransitions(transitionsData);
63
+ setAssignableUsers(usersData);
64
+
65
+ if (detailData.fields.project?.key) {
66
+ const versions = await fetchProjectVersions(detailData.fields.project.key).catch(() => []);
67
+ setProjectVersions(versions);
68
+ }
69
+ } catch (err) {
70
+ setDrawerError(err.message || 'Failed to load issue details');
71
+ } finally {
72
+ setDrawerLoading(false);
73
+ }
74
+ };
75
+
76
+ loadIssueData();
77
+ }, [issueKey]);
78
+
79
+ useEffect(() => {
80
+ if (!newLabelText || newLabelText.length < 1) {
81
+ setLabelSuggestions([]);
82
+ setShowLabelSuggestions(false);
83
+ return;
84
+ }
85
+ const timer = setTimeout(async () => {
86
+ try {
87
+ const labels = await searchLabels(newLabelText);
88
+ const currentLabels = issueDetail?.fields?.labels || [];
89
+ const filtered = labels.filter(l => !currentLabels.includes(l));
90
+ setLabelSuggestions(filtered);
91
+ setShowLabelSuggestions(filtered.length > 0);
92
+ } catch (err) {
93
+ console.error('Label search error:', err);
94
+ }
95
+ }, 300);
96
+ return () => clearTimeout(timer);
97
+ }, [newLabelText, issueDetail]);
98
+
99
+ useEffect(() => {
100
+ if (!issueKey || !assigneeSearch || assigneeSearch.length < 1) return;
101
+ const timer = setTimeout(async () => {
102
+ try {
103
+ const users = await fetchAssignableUsers(issueKey, assigneeSearch);
104
+ setAssignableUsers(users);
105
+ } catch (err) {
106
+ console.error('Assignee search error:', err);
107
+ }
108
+ }, 500);
109
+ return () => clearTimeout(timer);
110
+ }, [assigneeSearch, issueKey]);
111
+
112
+ const handlePostComment = useCallback(async () => {
113
+ if (!newCommentText.trim() || !issueKey) return;
114
+ setIsPostingComment(true);
115
+ try {
116
+ const postedComment = await postComment(issueKey, newCommentText);
117
+ setComments(prev => [...prev, postedComment]);
118
+ setNewCommentText('');
119
+ addToast('Comment posted', 'success');
120
+ } catch (err) {
121
+ addToast(`Error posting comment: ${err.message}`, 'error');
122
+ } finally {
123
+ setIsPostingComment(false);
124
+ }
125
+ }, [newCommentText, issueKey, addToast]);
126
+
127
+ const handleAssignUser = useCallback(async (accountId, displayName) => {
128
+ if (!accountId || !issueKey) return;
129
+
130
+ const optimisticAssigneeObj = {
131
+ displayName,
132
+ accountId,
133
+ avatarUrls: { '48x48': '' },
134
+ };
135
+ setOptimisticAssignee(optimisticAssigneeObj);
136
+ setIsAssigningOptimistic(true);
137
+ setTargetAccountId('');
138
+ setAssigneeSearch('');
139
+ setIsAssigning(true);
140
+
141
+ try {
142
+ await assignIssue(issueKey, accountId);
143
+ fetchIssueDetail(issueKey).then(setIssueDetail).catch(() => {});
144
+ refetch?.();
145
+ addToast('Issue assigned successfully', 'success');
146
+ } catch (err) {
147
+ setOptimisticAssignee(null);
148
+ addToast(`Failed to assign: ${err.message}`, 'error');
149
+ } finally {
150
+ setIsAssigningOptimistic(false);
151
+ setIsAssigning(false);
152
+ }
153
+ }, [issueKey, addToast, refetch]);
154
+
155
+ const handleFileUpload = useCallback(async (e) => {
156
+ const file = e.target.files?.[0];
157
+ if (!file || !issueKey) return;
158
+
159
+ if (file.size > 10 * 1024 * 1024) {
160
+ setUploadMessage('Error: File size exceeds 10MB limit');
161
+ return;
162
+ }
163
+
164
+ const ALLOWED_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'application/pdf', 'text/plain', 'application/zip'];
165
+ if (!ALLOWED_TYPES.includes(file.type)) {
166
+ addToast('File type not allowed', 'error');
167
+ e.target.value = '';
168
+ return;
169
+ }
170
+
171
+ setIsUploading(true);
172
+ setUploadMessage('');
173
+ try {
174
+ await uploadAttachment(issueKey, file);
175
+ setUploadMessage('File uploaded successfully');
176
+ addToast('File attached successfully', 'success');
177
+ const updatedDetail = await fetchIssueDetail(issueKey);
178
+ setIssueDetail(updatedDetail);
179
+ } catch (err) {
180
+ setUploadMessage(`Error uploading file: ${err.message}`);
181
+ addToast(`Error attaching file: ${err.message}`, 'error');
182
+ } finally {
183
+ setIsUploading(false);
184
+ e.target.value = '';
185
+ }
186
+ }, [issueKey, addToast]);
187
+
188
+ const handleUpdateVersion = useCallback(async (versionId) => {
189
+ if (!issueKey) return;
190
+ setIsUpdatingVersions(true);
191
+ try {
192
+ const fixVersions = versionId ? [{ id: versionId }] : [];
193
+ await updateIssue(issueKey, { fixVersions });
194
+ const updatedDetail = await fetchIssueDetail(issueKey);
195
+ setIssueDetail(updatedDetail);
196
+ addToast('Fix version updated', 'success');
197
+ } catch (err) {
198
+ addToast(`Error updating version: ${err.message}`, 'error');
199
+ } finally {
200
+ setIsUpdatingVersions(false);
201
+ }
202
+ }, [issueKey, addToast]);
203
+
204
+ const handleAddLabel = useCallback(async (labelToAdd) => {
205
+ const label = (typeof labelToAdd === 'string' ? labelToAdd : newLabelText).trim();
206
+ if (!label || !issueKey) return;
207
+ const currentLabels = issueDetail?.fields?.labels || [];
208
+ if (currentLabels.includes(label)) return;
209
+
210
+ setIsUpdatingLabels(true);
211
+ try {
212
+ const updatedLabels = [...currentLabels, label];
213
+ await updateIssue(issueKey, { labels: updatedLabels });
214
+ const updatedDetail = await fetchIssueDetail(issueKey);
215
+ setIssueDetail(updatedDetail);
216
+ setNewLabelText('');
217
+ setShowLabelSuggestions(false);
218
+ addToast('Label added', 'success');
219
+ } catch (err) {
220
+ addToast(`Error adding label: ${err.message}`, 'error');
221
+ } finally {
222
+ setIsUpdatingLabels(false);
223
+ }
224
+ }, [newLabelText, issueKey, issueDetail, addToast]);
225
+
226
+ const handleRemoveLabel = useCallback(async (labelToRemove) => {
227
+ if (!issueKey) return;
228
+ setIsUpdatingLabels(true);
229
+ try {
230
+ const updatedLabels = issueDetail?.fields?.labels?.filter(l => l !== labelToRemove) || [];
231
+ await updateIssue(issueKey, { labels: updatedLabels });
232
+ const updatedDetail = await fetchIssueDetail(issueKey);
233
+ setIssueDetail(updatedDetail);
234
+ addToast('Label removed', 'success');
235
+ } catch (err) {
236
+ addToast(`Error removing label: ${err.message}`, 'error');
237
+ } finally {
238
+ setIsUpdatingLabels(false);
239
+ }
240
+ }, [issueKey, issueDetail, addToast]);
241
+
242
+ const handleUpdateStatus = useCallback(async (transitionId, transitionName) => {
243
+ if (!transitionId || !issueKey) return;
244
+
245
+ setOptimisticStatus(transitionName);
246
+ setIsTransitioningOptimistic(true);
247
+ setTargetTransitionId('');
248
+ setIsTransitioning(true);
249
+
250
+ try {
251
+ await transitionIssue(issueKey, transitionId);
252
+ fetchIssueDetail(issueKey).then(setIssueDetail).catch(() => {});
253
+ fetchTransitions(issueKey).then(setTransitions).catch(() => {});
254
+ refetch?.();
255
+ addToast('Status updated successfully', 'success');
256
+ } catch (err) {
257
+ setOptimisticStatus(null);
258
+ addToast(`Failed to update status: ${err.message}`, 'error');
259
+ } finally {
260
+ setIsTransitioningOptimistic(false);
261
+ setIsTransitioning(false);
262
+ }
263
+ }, [issueKey, addToast, refetch]);
264
+
265
+ return {
266
+ issueDetail,
267
+ comments,
268
+ transitions,
269
+ assignableUsers,
270
+ drawerLoading,
271
+ drawerError,
272
+ newCommentText,
273
+ setNewCommentText,
274
+ isPostingComment,
275
+ targetTransitionId,
276
+ setTargetTransitionId,
277
+ isTransitioning,
278
+ isTransitioningOptimistic,
279
+ optimisticStatus,
280
+ assigneeSearch,
281
+ setAssigneeSearch,
282
+ targetAccountId,
283
+ setTargetAccountId,
284
+ isAssigning,
285
+ isAssigningOptimistic,
286
+ optimisticAssignee,
287
+ projectVersions,
288
+ isUpdatingVersions,
289
+ newLabelText,
290
+ setNewLabelText,
291
+ labelSuggestions,
292
+ showLabelSuggestions,
293
+ setShowLabelSuggestions,
294
+ isUpdatingLabels,
295
+ isUploading,
296
+ uploadMessage,
297
+ handlePostComment,
298
+ handleAssignUser,
299
+ handleFileUpload,
300
+ handleUpdateVersion,
301
+ handleAddLabel,
302
+ handleRemoveLabel,
303
+ handleUpdateStatus
304
+ };
305
+ }
@@ -0,0 +1,30 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { getIssues } from '../services/api';
3
+
4
+ export function useIssuesList(projectKey, statusFilter) {
5
+ const [issues, setIssues] = useState([]);
6
+ const [loading, setLoading] = useState(true);
7
+ const [error, setError] = useState(null);
8
+
9
+ const refetch = async () => {
10
+ setLoading(true);
11
+ setError(null);
12
+ try {
13
+ const data = await getIssues({
14
+ project: projectKey,
15
+ status: statusFilter ? statusFilter : 'exclude:Done,Cancelled,Closed',
16
+ });
17
+ setIssues(data.issues || []);
18
+ } catch (err) {
19
+ setError(err.message || 'Failed to fetch issues');
20
+ } finally {
21
+ setLoading(false);
22
+ }
23
+ };
24
+
25
+ useEffect(() => {
26
+ refetch();
27
+ }, [projectKey, statusFilter]);
28
+
29
+ return { issues, loading, error, refetch };
30
+ }
@@ -0,0 +1,19 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export function useToasts() {
4
+ const [toasts, setToasts] = useState([]);
5
+
6
+ const addToast = useCallback((message, type = 'info') => {
7
+ const id = Date.now();
8
+ setToasts(prev => [...prev, { id, message, type }]);
9
+ setTimeout(() => {
10
+ setToasts(prev => prev.filter(t => t.id !== id));
11
+ }, 4000);
12
+ }, []);
13
+
14
+ const removeToast = useCallback((id) => {
15
+ setToasts(prev => prev.filter(t => t.id !== id));
16
+ }, []);
17
+
18
+ return { toasts, addToast, removeToast };
19
+ }