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,30 @@
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "test": "vitest",
11
+ "test:run": "vitest run"
12
+ },
13
+ "dependencies": {
14
+ "axios": "^1.7.9",
15
+ "dompurify": "^3.3.3",
16
+ "lucide-react": "^0.469.0",
17
+ "react": "^18.3.1",
18
+ "react-dom": "^18.3.1"
19
+ },
20
+ "devDependencies": {
21
+ "@testing-library/jest-dom": "^6.9.1",
22
+ "@testing-library/react": "^16.3.2",
23
+ "@testing-library/user-event": "^14.6.1",
24
+ "@vitejs/plugin-react": "^4.3.4",
25
+ "@vitest/coverage-v8": "^4.1.0",
26
+ "jsdom": "^29.0.1",
27
+ "vite": "^6.0.7",
28
+ "vitest": "^4.1.0"
29
+ }
30
+ }
@@ -0,0 +1,447 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { Plus, Search, X, AlertCircle, Loader2, Activity, Circle, Clock, CheckCircle2, XCircle, Archive, Sun, Moon } from 'lucide-react';
3
+ import { getIssues, fetchProjects } from './services/api';
4
+ import IssueTable from './components/IssueTable';
5
+ import IssueDrawer from './components/IssueDrawer';
6
+ import CreateIssueModal from './components/CreateIssueModal';
7
+ import ToastContainer from './components/ToastContainer';
8
+ import StandaloneIssuePage from './components/StandaloneIssuePage';
9
+ import { useToasts } from './hooks/useToasts';
10
+ import { useIssueDrawer } from './hooks/useIssueDrawer';
11
+ import './index.css';
12
+
13
+ const STATUS_CHIPS = [
14
+ { value: '', label: 'Active', icon: Activity },
15
+ { value: 'To Do', label: 'To Do', icon: Circle },
16
+ { value: 'In Progress', label: 'In Progress', icon: Clock },
17
+ { value: 'Done', label: 'Done', icon: CheckCircle2 },
18
+ { value: 'Cancelled', label: 'Cancelled', icon: XCircle },
19
+ { value: 'Closed', label: 'Closed', icon: Archive },
20
+ ];
21
+
22
+ function App() {
23
+ const [issues, setIssues] = useState([]);
24
+ const [loading, setLoading] = useState(true);
25
+ const [error, setError] = useState(null);
26
+ const [projectKey] = useState('ADW');
27
+ const [searchQuery, setSearchQuery] = useState('');
28
+ const [statusFilter, setStatusFilter] = useState('');
29
+ const [currentPage, setCurrentPage] = useState(0);
30
+ const [pageSize] = useState(25);
31
+ const [totalIssues, setTotalIssues] = useState(0);
32
+ const [projects, setProjects] = useState([]);
33
+ const [projectsLoading, setProjectsLoading] = useState(false);
34
+
35
+ const [selectedIssueKey, setSelectedIssueKey] = useState(null);
36
+ const [isDrawerExpanded, setIsDrawerExpanded] = useState(false);
37
+ const [isStandaloneView, setIsStandaloneView] = useState(false);
38
+ const [showCreateModal, setShowCreateModal] = useState(false);
39
+ const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light');
40
+
41
+ const [searchSuggestions, setSearchSuggestions] = useState([]);
42
+ const [showSuggestions, setShowSuggestions] = useState(false);
43
+ const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
44
+ const [suggestionsLoading, setSuggestionsLoading] = useState(false);
45
+ const isInitialLoad = useRef(true);
46
+
47
+ const { toasts, addToast, removeToast } = useToasts();
48
+
49
+ const refetchIssues = useCallback(() => {
50
+ setLoading(true);
51
+ getIssues({
52
+ project: projectKey,
53
+ status: statusFilter ? statusFilter : 'exclude:Done,Cancelled,Closed',
54
+ startAt: currentPage * pageSize,
55
+ maxResults: pageSize,
56
+ })
57
+ .then(data => {
58
+ setIssues(data.issues || []);
59
+ setTotalIssues(data.total || 0);
60
+ })
61
+ .catch(err => setError(err.message || 'Failed to fetch issues'))
62
+ .finally(() => setLoading(false));
63
+ }, [projectKey, statusFilter, currentPage, pageSize]);
64
+
65
+ const {
66
+ issueDetail,
67
+ comments,
68
+ transitions,
69
+ assignableUsers,
70
+ drawerLoading,
71
+ drawerError,
72
+ newCommentText,
73
+ setNewCommentText,
74
+ isPostingComment,
75
+ targetTransitionId,
76
+ setTargetTransitionId,
77
+ isTransitioning,
78
+ assigneeSearch,
79
+ setAssigneeSearch,
80
+ targetAccountId,
81
+ setTargetAccountId,
82
+ isAssigning,
83
+ projectVersions,
84
+ isUpdatingVersions,
85
+ newLabelText,
86
+ setNewLabelText,
87
+ labelSuggestions,
88
+ showLabelSuggestions,
89
+ setShowLabelSuggestions,
90
+ isUpdatingLabels,
91
+ isUploading,
92
+ uploadMessage,
93
+ handlePostComment,
94
+ handleAssignUser,
95
+ handleFileUpload,
96
+ handleUpdateVersion,
97
+ handleAddLabel,
98
+ handleRemoveLabel,
99
+ handleUpdateStatus
100
+ } = useIssueDrawer(selectedIssueKey, addToast, refetchIssues);
101
+
102
+ useEffect(() => {
103
+ setProjectsLoading(true);
104
+ fetchProjects()
105
+ .then(setProjects)
106
+ .catch(() => addToast('Failed to load projects', 'error'))
107
+ .finally(() => setProjectsLoading(false));
108
+ }, []);
109
+
110
+ useEffect(() => {
111
+ const checkStandalone = () => {
112
+ const hash = window.location.hash;
113
+ if (isInitialLoad.current) {
114
+ setIsStandaloneView(hash.startsWith('#/view/'));
115
+ isInitialLoad.current = false;
116
+ }
117
+ };
118
+
119
+ checkStandalone();
120
+ window.addEventListener('hashchange', checkStandalone);
121
+ window.addEventListener('popstate', checkStandalone);
122
+ window.addEventListener('load', checkStandalone);
123
+
124
+ return () => {
125
+ window.removeEventListener('hashchange', checkStandalone);
126
+ window.removeEventListener('popstate', checkStandalone);
127
+ window.removeEventListener('load', checkStandalone);
128
+ };
129
+ }, []);
130
+
131
+ useEffect(() => {
132
+ const checkHash = () => {
133
+ const hash = window.location.hash;
134
+ if (hash.startsWith('#/issue/')) {
135
+ const issueKey = hash.replace('#/issue/', '');
136
+ if (issueKey && issueKey !== selectedIssueKey) {
137
+ setSelectedIssueKey(issueKey);
138
+ }
139
+ }
140
+ };
141
+ checkHash();
142
+ window.addEventListener('hashchange', checkHash);
143
+ return () => window.removeEventListener('hashchange', checkHash);
144
+ }, []);
145
+
146
+ useEffect(() => {
147
+ setCurrentPage(0);
148
+ }, [statusFilter, searchQuery]);
149
+
150
+ useEffect(() => {
151
+ refetchIssues();
152
+ }, [refetchIssues]);
153
+
154
+ useEffect(() => {
155
+ if (!searchQuery) {
156
+ setSearchSuggestions([]);
157
+ setShowSuggestions(false);
158
+ setSuggestionsLoading(false);
159
+ return;
160
+ }
161
+ setSuggestionsLoading(true);
162
+ const timer = setTimeout(async () => {
163
+ try {
164
+ const data = await getIssues({
165
+ project: projectKey,
166
+ search: searchQuery,
167
+ });
168
+ const suggestions = (data.issues || []).slice(0, 6);
169
+ setSearchSuggestions(suggestions);
170
+ setShowSuggestions(suggestions.length > 0);
171
+ setSelectedSuggestionIndex(-1);
172
+ } catch (err) {
173
+ setSearchSuggestions([]);
174
+ setShowSuggestions(false);
175
+ } finally {
176
+ setSuggestionsLoading(false);
177
+ }
178
+ }, 500);
179
+ return () => clearTimeout(timer);
180
+ }, [searchQuery, projectKey]);
181
+
182
+ useEffect(() => {
183
+ const handleKeyDown = (e) => {
184
+ if (e.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
185
+ e.preventDefault();
186
+ document.querySelector('.text-input')?.focus();
187
+ }
188
+ if (e.key === 'Escape') {
189
+ if (showSuggestions) {
190
+ setShowSuggestions(false);
191
+ setSelectedSuggestionIndex(-1);
192
+ } else if (selectedIssueKey) {
193
+ setSelectedIssueKey(null);
194
+ } else if (showCreateModal) {
195
+ setShowCreateModal(false);
196
+ }
197
+ }
198
+ };
199
+ window.addEventListener('keydown', handleKeyDown);
200
+ return () => window.removeEventListener('keydown', handleKeyDown);
201
+ }, [selectedIssueKey, showCreateModal, showSuggestions]);
202
+
203
+ useEffect(() => {
204
+ const handleClickOutside = (e) => {
205
+ if (showSuggestions && !e.target.closest('.search-input-wrapper')) {
206
+ setShowSuggestions(false);
207
+ setSelectedSuggestionIndex(-1);
208
+ }
209
+ };
210
+ document.addEventListener('click', handleClickOutside);
211
+ return () => document.removeEventListener('click', handleClickOutside);
212
+ }, [showSuggestions]);
213
+
214
+ useEffect(() => {
215
+ document.documentElement.setAttribute('data-theme', theme);
216
+ localStorage.setItem('theme', theme);
217
+ }, [theme]);
218
+
219
+ const toggleTheme = () => {
220
+ setTheme(prev => prev === 'light' ? 'dark' : 'light');
221
+ };
222
+
223
+ const openDrawer = useCallback((issueKey) => {
224
+ setSelectedIssueKey(issueKey);
225
+ setIsDrawerExpanded(false);
226
+ }, []);
227
+
228
+ const closeDrawer = useCallback(() => {
229
+ setSelectedIssueKey(null);
230
+ }, []);
231
+
232
+ const handleSubtaskClick = useCallback((key) => {
233
+ setSelectedIssueKey(key);
234
+ }, []);
235
+
236
+ const handleCreateSuccess = useCallback(() => {
237
+ setShowCreateModal(false);
238
+ refetchIssues();
239
+ }, [refetchIssues]);
240
+
241
+ const handleStatusFilterChange = useCallback((value) => {
242
+ setStatusFilter(prev => prev === value ? '' : value);
243
+ }, []);
244
+
245
+ if (isStandaloneView) {
246
+ return <StandaloneIssuePage />;
247
+ }
248
+
249
+ return (
250
+ <div className="dashboard-container">
251
+ <header className="header">
252
+ <h1>Jira Dashboard</h1>
253
+ <div className="header-actions">
254
+ <button
255
+ className="theme-toggle"
256
+ onClick={toggleTheme}
257
+ aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
258
+ title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
259
+ >
260
+ {theme === 'light' ? <Moon size={18} /> : <Sun size={18} />}
261
+ </button>
262
+ <button className="btn-primary" onClick={() => setShowCreateModal(true)} aria-label="Create new issue">
263
+ <Plus size={18} />
264
+ Create Issue
265
+ </button>
266
+ </div>
267
+ </header>
268
+
269
+ <div className="controls">
270
+ <div className="controls-main">
271
+ <div className="input-group search-group">
272
+ <label className="input-label">Search</label>
273
+ <div className="search-input-wrapper">
274
+ <Search size={16} className="search-icon" />
275
+ <input
276
+ type="text"
277
+ className="text-input"
278
+ placeholder="Search issues or jump to (e.g. ADW-123)..."
279
+ value={searchQuery}
280
+ onChange={(e) => setSearchQuery(e.target.value)}
281
+ onKeyDown={(e) => {
282
+ if (e.key === 'Enter') {
283
+ if (selectedSuggestionIndex >= 0 && searchSuggestions[selectedSuggestionIndex]) {
284
+ openDrawer(searchSuggestions[selectedSuggestionIndex].key);
285
+ setSearchQuery('');
286
+ setShowSuggestions(false);
287
+ } else if (searchQuery.trim()) {
288
+ const query = searchQuery.trim().toUpperCase();
289
+ const jiraKeyPattern = /^[A-Z]+-\d+$/;
290
+ if (jiraKeyPattern.test(query)) {
291
+ openDrawer(query);
292
+ }
293
+ }
294
+ } else if (e.key === 'ArrowDown') {
295
+ e.preventDefault();
296
+ setSelectedSuggestionIndex(prev =>
297
+ prev < searchSuggestions.length - 1 ? prev + 1 : prev
298
+ );
299
+ } else if (e.key === 'ArrowUp') {
300
+ e.preventDefault();
301
+ setSelectedSuggestionIndex(prev => prev > 0 ? prev - 1 : -1);
302
+ } else if (e.key === 'Escape') {
303
+ setShowSuggestions(false);
304
+ setSelectedSuggestionIndex(-1);
305
+ }
306
+ }}
307
+ onFocus={() => {
308
+ if (searchSuggestions.length > 0) {
309
+ setShowSuggestions(true);
310
+ }
311
+ }}
312
+ aria-label="Search issues"
313
+ aria-autocomplete="list"
314
+ />
315
+ {searchQuery && (
316
+ <button
317
+ className="search-clear-btn"
318
+ onClick={() => {
319
+ setSearchQuery('');
320
+ setShowSuggestions(false);
321
+ }}
322
+ aria-label="Clear search"
323
+ >
324
+ <X size={14} />
325
+ </button>
326
+ )}
327
+ {showSuggestions && (
328
+ <div className="search-suggestions-dropdown">
329
+ {suggestionsLoading ? (
330
+ <div className="suggestions-skeleton">
331
+ <div className="skeleton" style={{ height: 14, width: '80%' }} />
332
+ <div className="skeleton" style={{ height: 14, width: '60%' }} />
333
+ <div className="skeleton" style={{ height: 14, width: '70%' }} />
334
+ </div>
335
+ ) : (
336
+ searchSuggestions.map((issue, index) => (
337
+ <div
338
+ key={issue.id}
339
+ className={`search-suggestion-item ${index === selectedSuggestionIndex ? 'selected' : ''}`}
340
+ onClick={() => {
341
+ openDrawer(issue.key);
342
+ setSearchQuery('');
343
+ setShowSuggestions(false);
344
+ }}
345
+ onMouseEnter={() => setSelectedSuggestionIndex(index)}
346
+ >
347
+ <span className="suggestion-key">{issue.key}</span>
348
+ <span className="suggestion-summary">{issue.fields.summary}</span>
349
+ </div>
350
+ ))
351
+ )}
352
+ </div>
353
+ )}
354
+ </div>
355
+ </div>
356
+ <div className="filter-chips">
357
+ {STATUS_CHIPS.map(chip => (
358
+ <button
359
+ key={chip.value}
360
+ className={`filter-chip${statusFilter === chip.value ? ' active' : ''}`}
361
+ onClick={() => handleStatusFilterChange(chip.value)}
362
+ >
363
+ {chip.icon && <chip.icon size={14} />}
364
+ {chip.label}
365
+ </button>
366
+ ))}
367
+ </div>
368
+ </div>
369
+ </div>
370
+
371
+ {error && (
372
+ <div className="error" role="alert">
373
+ <AlertCircle size={16} style={{ marginRight: '0.5rem', verticalAlign: 'middle' }} />
374
+ {error}
375
+ </div>
376
+ )}
377
+
378
+ <IssueTable
379
+ issues={issues}
380
+ loading={loading}
381
+ onRowClick={openDrawer}
382
+ currentPage={currentPage}
383
+ pageSize={pageSize}
384
+ total={totalIssues}
385
+ onPageChange={setCurrentPage}
386
+ />
387
+
388
+ {selectedIssueKey && (
389
+ <IssueDrawer
390
+ issueKey={selectedIssueKey}
391
+ isExpanded={isDrawerExpanded}
392
+ onClose={closeDrawer}
393
+ onExpand={setIsDrawerExpanded}
394
+ addToast={addToast}
395
+ issueDetail={issueDetail}
396
+ comments={comments}
397
+ transitions={transitions}
398
+ assignableUsers={assignableUsers}
399
+ drawerLoading={drawerLoading}
400
+ drawerError={drawerError}
401
+ newCommentText={newCommentText}
402
+ setNewCommentText={setNewCommentText}
403
+ isPostingComment={isPostingComment}
404
+ targetTransitionId={targetTransitionId}
405
+ setTargetTransitionId={setTargetTransitionId}
406
+ isTransitioning={isTransitioning}
407
+ assigneeSearch={assigneeSearch}
408
+ setAssigneeSearch={setAssigneeSearch}
409
+ targetAccountId={targetAccountId}
410
+ setTargetAccountId={setTargetAccountId}
411
+ isAssigning={isAssigning}
412
+ projectVersions={projectVersions}
413
+ isUpdatingVersions={isUpdatingVersions}
414
+ newLabelText={newLabelText}
415
+ setNewLabelText={setNewLabelText}
416
+ labelSuggestions={labelSuggestions}
417
+ showLabelSuggestions={showLabelSuggestions}
418
+ setShowLabelSuggestions={setShowLabelSuggestions}
419
+ isUpdatingLabels={isUpdatingLabels}
420
+ isUploading={isUploading}
421
+ uploadMessage={uploadMessage}
422
+ handlePostComment={handlePostComment}
423
+ handleAssignUser={handleAssignUser}
424
+ handleFileUpload={handleFileUpload}
425
+ handleUpdateVersion={handleUpdateVersion}
426
+ handleAddLabel={handleAddLabel}
427
+ handleRemoveLabel={handleRemoveLabel}
428
+ handleUpdateStatus={handleUpdateStatus}
429
+ onSubtaskClick={handleSubtaskClick}
430
+ />
431
+ )}
432
+
433
+ <CreateIssueModal
434
+ isOpen={showCreateModal}
435
+ onClose={() => setShowCreateModal(false)}
436
+ onCreated={handleCreateSuccess}
437
+ projects={projects}
438
+ projectsLoading={projectsLoading}
439
+ addToast={addToast}
440
+ />
441
+
442
+ <ToastContainer toasts={toasts} onRemove={removeToast} />
443
+ </div>
444
+ );
445
+ }
446
+
447
+ export default App;