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.
- package/AGENTS.md +218 -0
- package/README.md +64 -0
- package/backend/.env.example +1 -0
- package/backend/__tests__/getJiraClient.test.js +57 -0
- package/backend/__tests__/issues.test.js +565 -0
- package/backend/__tests__/jiraService.test.js +1127 -0
- package/backend/__tests__/projects.test.js +256 -0
- package/backend/coverage/clover.xml +426 -0
- package/backend/coverage/coverage-final.json +4 -0
- package/backend/coverage/lcov-report/base.css +224 -0
- package/backend/coverage/lcov-report/block-navigation.js +87 -0
- package/backend/coverage/lcov-report/favicon.png +0 -0
- package/backend/coverage/lcov-report/index.html +131 -0
- package/backend/coverage/lcov-report/prettify.css +1 -0
- package/backend/coverage/lcov-report/prettify.js +2 -0
- package/backend/coverage/lcov-report/routes/index.html +131 -0
- package/backend/coverage/lcov-report/routes/issues.js.html +823 -0
- package/backend/coverage/lcov-report/routes/projects.js.html +190 -0
- package/backend/coverage/lcov-report/service/index.html +116 -0
- package/backend/coverage/lcov-report/service/jiraService.js.html +1663 -0
- package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/backend/coverage/lcov-report/sorter.js +210 -0
- package/backend/coverage/lcov.info +707 -0
- package/backend/index.js +38 -0
- package/backend/jest.config.js +11 -0
- package/backend/package-lock.json +5636 -0
- package/backend/package.json +28 -0
- package/backend/routes/issues.js +246 -0
- package/backend/routes/projects.js +35 -0
- package/backend/service/jiraService.js +526 -0
- package/bin/jira.js +92 -0
- package/frontend/.env.example +1 -0
- package/frontend/coverage/base.css +224 -0
- package/frontend/coverage/block-navigation.js +87 -0
- package/frontend/coverage/clover.xml +559 -0
- package/frontend/coverage/components/CreateIssueModal.jsx.html +592 -0
- package/frontend/coverage/components/IssueDetailPanel.jsx.html +1633 -0
- package/frontend/coverage/components/IssueDrawer.jsx.html +550 -0
- package/frontend/coverage/components/IssueTable.jsx.html +571 -0
- package/frontend/coverage/components/SkeletonComponents.jsx.html +223 -0
- package/frontend/coverage/components/ToastContainer.jsx.html +142 -0
- package/frontend/coverage/components/index.html +191 -0
- package/frontend/coverage/coverage-final.json +14 -0
- package/frontend/coverage/favicon.png +0 -0
- package/frontend/coverage/hooks/index.html +161 -0
- package/frontend/coverage/hooks/useFocusTrap.js.html +262 -0
- package/frontend/coverage/hooks/useIssueDrawer.js.html +1000 -0
- package/frontend/coverage/hooks/useIssuesList.js.html +175 -0
- package/frontend/coverage/hooks/useToasts.js.html +142 -0
- package/frontend/coverage/index.html +161 -0
- package/frontend/coverage/prettify.css +1 -0
- package/frontend/coverage/prettify.js +2 -0
- package/frontend/coverage/services/api.js.html +547 -0
- package/frontend/coverage/services/index.html +116 -0
- package/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/frontend/coverage/sorter.js +210 -0
- package/frontend/coverage/utils/index.html +131 -0
- package/frontend/coverage/utils/issueHelpers.jsx.html +334 -0
- package/frontend/coverage/utils/sanitize.js.html +166 -0
- package/frontend/index.html +13 -0
- package/frontend/package-lock.json +3436 -0
- package/frontend/package.json +30 -0
- package/frontend/src/App.jsx +447 -0
- package/frontend/src/__tests__/components/CreateIssueModal.test.jsx +375 -0
- package/frontend/src/__tests__/components/IssueDetailPanel.test.jsx +962 -0
- package/frontend/src/__tests__/components/IssueDrawer.test.jsx +240 -0
- package/frontend/src/__tests__/components/IssueTable.test.jsx +423 -0
- package/frontend/src/__tests__/components/ToastContainer.test.jsx +196 -0
- package/frontend/src/__tests__/hooks/useFocusTrap.test.js +197 -0
- package/frontend/src/__tests__/hooks/useIssueDrawer.test.js +1053 -0
- package/frontend/src/__tests__/hooks/useIssuesList.test.js +175 -0
- package/frontend/src/__tests__/hooks/useToasts.test.js +110 -0
- package/frontend/src/__tests__/services/api.test.js +568 -0
- package/frontend/src/__tests__/setup.js +54 -0
- package/frontend/src/__tests__/utils/issueHelpers.test.jsx +336 -0
- package/frontend/src/__tests__/utils/sanitize.test.js +238 -0
- package/frontend/src/components/CreateIssueModal.jsx +169 -0
- package/frontend/src/components/ErrorBoundary.jsx +52 -0
- package/frontend/src/components/IssueDetailPanel.jsx +517 -0
- package/frontend/src/components/IssueDrawer.jsx +155 -0
- package/frontend/src/components/IssueTable.jsx +162 -0
- package/frontend/src/components/SkeletonComponents.jsx +46 -0
- package/frontend/src/components/StandaloneIssuePage.jsx +176 -0
- package/frontend/src/components/ToastContainer.jsx +19 -0
- package/frontend/src/hooks/useFocusTrap.js +59 -0
- package/frontend/src/hooks/useIssueDrawer.js +305 -0
- package/frontend/src/hooks/useIssuesList.js +30 -0
- package/frontend/src/hooks/useToasts.js +19 -0
- package/frontend/src/index.css +2070 -0
- package/frontend/src/main.jsx +13 -0
- package/frontend/src/services/api.js +154 -0
- package/frontend/src/utils/issueHelpers.jsx +84 -0
- package/frontend/src/utils/sanitize.js +27 -0
- package/frontend/vite.config.js +15 -0
- package/package.json +19 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { X, AlertCircle, Loader2 } from 'lucide-react';
|
|
3
|
+
import { fetchIssueTypes, createIssue } from '../services/api';
|
|
4
|
+
import { useFocusTrap } from '../hooks/useFocusTrap.js';
|
|
5
|
+
|
|
6
|
+
export default React.memo(function CreateIssueModal({ isOpen, onClose, onCreated, projects, projectsLoading, addToast }) {
|
|
7
|
+
const [issueTypes, setIssueTypes] = useState([]);
|
|
8
|
+
const [createData, setCreateData] = useState({
|
|
9
|
+
projectKey: '',
|
|
10
|
+
summary: '',
|
|
11
|
+
description: '',
|
|
12
|
+
issueType: '',
|
|
13
|
+
priority: ''
|
|
14
|
+
});
|
|
15
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
16
|
+
const [createError, setCreateError] = useState('');
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (createData.projectKey) {
|
|
20
|
+
fetchIssueTypes(createData.projectKey).then(setIssueTypes).catch(console.error);
|
|
21
|
+
} else {
|
|
22
|
+
setIssueTypes([]);
|
|
23
|
+
}
|
|
24
|
+
}, [createData.projectKey]);
|
|
25
|
+
|
|
26
|
+
const modalRef = useRef(null);
|
|
27
|
+
useFocusTrap(modalRef, isOpen);
|
|
28
|
+
|
|
29
|
+
const handleCreateSubmit = async () => {
|
|
30
|
+
if (!createData.projectKey || !createData.summary || !createData.issueType) return;
|
|
31
|
+
setIsCreating(true);
|
|
32
|
+
setCreateError('');
|
|
33
|
+
try {
|
|
34
|
+
await createIssue(createData);
|
|
35
|
+
setCreateData({ projectKey: '', summary: '', description: '', issueType: '', priority: '' });
|
|
36
|
+
onCreated();
|
|
37
|
+
addToast('Issue created successfully', 'success');
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setCreateError(err.message || 'Failed to create issue');
|
|
40
|
+
addToast(`Error creating issue: ${err.message}`, 'error');
|
|
41
|
+
} finally {
|
|
42
|
+
setIsCreating(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!isOpen) return null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="modal-overlay" ref={modalRef} onClick={onClose}>
|
|
50
|
+
<div className="modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
51
|
+
<div className="modal-header">
|
|
52
|
+
<h2 id="modal-title">Create Issue</h2>
|
|
53
|
+
<button className="close-btn" onClick={onClose} aria-label="Close modal">
|
|
54
|
+
<X size={24} />
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="modal-body">
|
|
59
|
+
{createError && (
|
|
60
|
+
<div className="error" role="alert" style={{ marginBottom: '1rem' }}>
|
|
61
|
+
<AlertCircle size={16} style={{ marginRight: '0.5rem' }} />
|
|
62
|
+
{createError}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
<div className="input-group" style={{ marginBottom: '1rem' }}>
|
|
66
|
+
<label className="input-label">Project *</label>
|
|
67
|
+
<select
|
|
68
|
+
className="select-input"
|
|
69
|
+
style={{ width: '100%' }}
|
|
70
|
+
value={createData.projectKey}
|
|
71
|
+
onChange={(e) => setCreateData({ ...createData, projectKey: e.target.value, issueType: '' })}
|
|
72
|
+
aria-label="Select project"
|
|
73
|
+
>
|
|
74
|
+
<option value="">{projectsLoading ? 'Loading...' : 'Select Project'}</option>
|
|
75
|
+
{!projectsLoading && projects.map(p => (
|
|
76
|
+
<option key={p.id} value={p.key}>{p.name} ({p.key})</option>
|
|
77
|
+
))}
|
|
78
|
+
</select>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{createData.projectKey && (
|
|
82
|
+
<div className="input-group" style={{ marginBottom: '1rem' }}>
|
|
83
|
+
<label className="input-label">Issue Type *</label>
|
|
84
|
+
<select
|
|
85
|
+
className="select-input"
|
|
86
|
+
style={{ width: '100%' }}
|
|
87
|
+
value={createData.issueType}
|
|
88
|
+
onChange={(e) => setCreateData({ ...createData, issueType: e.target.value })}
|
|
89
|
+
aria-label="Select issue type"
|
|
90
|
+
>
|
|
91
|
+
<option value="">Select Type</option>
|
|
92
|
+
{issueTypes.filter(t => !t.subtask).map(t => (
|
|
93
|
+
<option key={t.id} value={t.id}>
|
|
94
|
+
{t.name}
|
|
95
|
+
</option>
|
|
96
|
+
))}
|
|
97
|
+
</select>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
<div className="input-group" style={{ marginBottom: '1rem' }}>
|
|
102
|
+
<label className="input-label">Summary *</label>
|
|
103
|
+
<input
|
|
104
|
+
type="text"
|
|
105
|
+
className="text-input"
|
|
106
|
+
style={{ width: '100%' }}
|
|
107
|
+
placeholder="Short description"
|
|
108
|
+
value={createData.summary}
|
|
109
|
+
onChange={(e) => setCreateData({ ...createData, summary: e.target.value })}
|
|
110
|
+
aria-label="Issue summary"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="input-group" style={{ marginBottom: '1rem' }}>
|
|
115
|
+
<label className="input-label">Description</label>
|
|
116
|
+
<textarea
|
|
117
|
+
className="comment-box"
|
|
118
|
+
style={{ width: '100%', minHeight: '100px' }}
|
|
119
|
+
placeholder="Detailed description..."
|
|
120
|
+
value={createData.description}
|
|
121
|
+
onChange={(e) => setCreateData({ ...createData, description: e.target.value })}
|
|
122
|
+
aria-label="Issue description"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="input-group" style={{ marginBottom: '1rem' }}>
|
|
127
|
+
<label className="input-label">Priority (Optional)</label>
|
|
128
|
+
<select
|
|
129
|
+
className="select-input"
|
|
130
|
+
style={{ width: '100%' }}
|
|
131
|
+
value={createData.priority}
|
|
132
|
+
onChange={(e) => setCreateData({ ...createData, priority: e.target.value })}
|
|
133
|
+
aria-label="Select priority"
|
|
134
|
+
>
|
|
135
|
+
<option value="">Default Priority</option>
|
|
136
|
+
<option value="1">Highest</option>
|
|
137
|
+
<option value="2">High</option>
|
|
138
|
+
<option value="3">Medium</option>
|
|
139
|
+
<option value="4">Low</option>
|
|
140
|
+
<option value="5">Lowest</option>
|
|
141
|
+
</select>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="modal-footer">
|
|
146
|
+
<button
|
|
147
|
+
className="btn-secondary"
|
|
148
|
+
onClick={onClose}
|
|
149
|
+
>
|
|
150
|
+
Cancel
|
|
151
|
+
</button>
|
|
152
|
+
<button
|
|
153
|
+
className="btn-primary"
|
|
154
|
+
onClick={handleCreateSubmit}
|
|
155
|
+
disabled={!createData.projectKey || !createData.summary || !createData.issueType || isCreating}
|
|
156
|
+
aria-label="Create issue"
|
|
157
|
+
>
|
|
158
|
+
{isCreating ? (
|
|
159
|
+
<>
|
|
160
|
+
<Loader2 size={16} className="spinner" />
|
|
161
|
+
Creating...
|
|
162
|
+
</>
|
|
163
|
+
) : 'Create'}
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AlertCircle, RefreshCw } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
class ErrorBoundary extends React.Component {
|
|
5
|
+
constructor(props) {
|
|
6
|
+
super(props);
|
|
7
|
+
this.state = { hasError: false, error: null };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static getDerivedStateFromError(error) {
|
|
11
|
+
return { hasError: true, error };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
componentDidCatch(error, errorInfo) {
|
|
15
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render() {
|
|
19
|
+
if (this.state.hasError) {
|
|
20
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="error-boundary-container">
|
|
24
|
+
<div className="error-boundary-icon">
|
|
25
|
+
<AlertCircle size={48} />
|
|
26
|
+
</div>
|
|
27
|
+
<h2 className="error-boundary-title">Something went wrong</h2>
|
|
28
|
+
|
|
29
|
+
{isDev && this.state.error && (
|
|
30
|
+
<>
|
|
31
|
+
<p className="error-boundary-message">{this.state.error.message}</p>
|
|
32
|
+
<pre className="error-boundary-stack">{this.state.error.stack}</pre>
|
|
33
|
+
</>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
{!isDev && (
|
|
37
|
+
<p className="error-boundary-fallback">Please refresh the page.</p>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
<button className="error-boundary-reload" onClick={() => window.location.reload()}>
|
|
41
|
+
<RefreshCw size={16} />
|
|
42
|
+
Reload
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return this.props.children;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default ErrorBoundary;
|