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,13 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+ import ErrorBoundary from './components/ErrorBoundary.jsx'
6
+
7
+ createRoot(document.getElementById('root')).render(
8
+ <StrictMode>
9
+ <ErrorBoundary>
10
+ <App />
11
+ </ErrorBoundary>
12
+ </StrictMode>,
13
+ )
@@ -0,0 +1,154 @@
1
+ import axios from 'axios';
2
+
3
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api';
4
+ const TIMEOUT_MS = 15000;
5
+
6
+ const createSignal = (ms) => {
7
+ const controller = new AbortController();
8
+ const timer = setTimeout(() => controller.abort(), ms);
9
+ return { controller, timer };
10
+ };
11
+
12
+ export const withRetry = async (fn, maxAttempts = 3, baseDelayMs = 500) => {
13
+ let lastError;
14
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
15
+ try {
16
+ return await fn();
17
+ } catch (error) {
18
+ lastError = error;
19
+ const isClientError = error?.response?.status >= 400 && error?.response?.status < 500;
20
+ if (isClientError || attempt === maxAttempts - 1) {
21
+ throw error;
22
+ }
23
+ const delay = baseDelayMs * Math.pow(2, attempt);
24
+ await new Promise(resolve => setTimeout(resolve, delay));
25
+ }
26
+ }
27
+ throw lastError;
28
+ };
29
+
30
+ const request = async (method, url, config = {}) => {
31
+ const { controller, timer } = createSignal(TIMEOUT_MS);
32
+ try {
33
+ const response = await axios({ method, url, ...config, signal: controller.signal });
34
+ return response;
35
+ } catch (error) {
36
+ if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
37
+ throw new Error('Request timed out. Please check your connection.');
38
+ }
39
+ throw error;
40
+ } finally {
41
+ clearTimeout(timer);
42
+ }
43
+ };
44
+
45
+ export const getIssues = async (params = {}) => {
46
+ const cleanParams = Object.fromEntries(
47
+ Object.entries(params).filter(([_, v]) => v !== undefined && v !== null && v !== '')
48
+ );
49
+ return withRetry(async () => {
50
+ const response = await request('get', `${API_BASE_URL}/issues`, { params: cleanParams });
51
+ return response.data;
52
+ });
53
+ };
54
+
55
+ export const fetchIssueDetail = async (issueKey) => {
56
+ return withRetry(async () => {
57
+ const response = await request('get', `${API_BASE_URL}/issues/${issueKey}`);
58
+ return response.data;
59
+ });
60
+ };
61
+
62
+ export const fetchComments = async (issueKey) => {
63
+ return withRetry(async () => {
64
+ const response = await request('get', `${API_BASE_URL}/issues/${issueKey}/comments`);
65
+ return response.data;
66
+ });
67
+ };
68
+
69
+ export const postComment = async (issueKey, body) => {
70
+ const response = await request('post', `${API_BASE_URL}/issues/${issueKey}/comments`, { data: { body } });
71
+ return response.data;
72
+ };
73
+
74
+ export const fetchTransitions = async (issueKey) => {
75
+ return withRetry(async () => {
76
+ const response = await request('get', `${API_BASE_URL}/issues/${issueKey}/transitions`);
77
+ return response.data;
78
+ });
79
+ };
80
+
81
+ export const transitionIssue = async (issueKey, transitionId) => {
82
+ const response = await request('post', `${API_BASE_URL}/issues/${issueKey}/transitions`, { data: { transitionId } });
83
+ return response.data;
84
+ };
85
+
86
+ export const fetchAssignableUsers = async (issueKey, query) => {
87
+ return withRetry(async () => {
88
+ const response = await request('get', `${API_BASE_URL}/issues/${issueKey}/assignable`, { params: { query } });
89
+ return response.data;
90
+ });
91
+ };
92
+
93
+ export const assignIssue = async (issueKey, accountId) => {
94
+ const response = await request('put', `${API_BASE_URL}/issues/${issueKey}/assignee`, { data: { accountId } });
95
+ return response.data;
96
+ };
97
+
98
+ export const uploadAttachment = async (issueKey, file) => {
99
+ const formData = new FormData();
100
+ formData.append('file', file);
101
+ const { controller, timer } = createSignal(TIMEOUT_MS);
102
+ try {
103
+ const response = await axios.post(`${API_BASE_URL}/issues/${issueKey}/attachments`, formData, {
104
+ headers: { 'Content-Type': 'multipart/form-data' },
105
+ signal: controller.signal,
106
+ });
107
+ return response.data;
108
+ } catch (error) {
109
+ if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
110
+ throw new Error('Request timed out. Please check your connection.');
111
+ }
112
+ throw error;
113
+ } finally {
114
+ clearTimeout(timer);
115
+ }
116
+ };
117
+
118
+ export const fetchProjects = async () => {
119
+ return withRetry(async () => {
120
+ const response = await request('get', `${API_BASE_URL}/projects`);
121
+ return response.data;
122
+ });
123
+ };
124
+
125
+ export const fetchIssueTypes = async (projectKey) => {
126
+ return withRetry(async () => {
127
+ const response = await request('get', `${API_BASE_URL}/projects/${projectKey}/issuetypes`);
128
+ return response.data;
129
+ });
130
+ };
131
+
132
+ export const createIssue = async (issueData) => {
133
+ const response = await request('post', `${API_BASE_URL}/issues`, { data: issueData });
134
+ return response.data;
135
+ };
136
+
137
+ export const updateIssue = async (issueKey, fields) => {
138
+ const response = await request('put', `${API_BASE_URL}/issues/${issueKey}`, { data: { fields } });
139
+ return response.data;
140
+ };
141
+
142
+ export const fetchProjectVersions = async (projectKey) => {
143
+ return withRetry(async () => {
144
+ const response = await request('get', `${API_BASE_URL}/projects/${projectKey}/versions`);
145
+ return response.data;
146
+ });
147
+ };
148
+
149
+ export const searchLabels = async (query) => {
150
+ return withRetry(async () => {
151
+ const response = await request('get', `${API_BASE_URL}/issues/labels/search`, { params: { query } });
152
+ return response.data;
153
+ });
154
+ };
@@ -0,0 +1,84 @@
1
+ import {
2
+ Bug, FileText, ClipboardList, Layers, Info,
3
+ ChevronUp, ChevronDown, Minus, AlertCircle
4
+ } from 'lucide-react';
5
+
6
+ export const SEVERITY_FIELD = 'customfield_10260';
7
+
8
+ export const getIssueTypeIcon = (type) => {
9
+ if (!type) return <FileText size={16} />;
10
+ const name = type.toLowerCase();
11
+ if (name.includes('bug')) return <Bug size={16} />;
12
+ if (name.includes('story') || name.includes('epic')) return <Layers size={16} />;
13
+ if (name.includes('task')) return <ClipboardList size={16} />;
14
+ return <FileText size={16} />;
15
+ };
16
+
17
+ export const getStatusClass = (status) => {
18
+ if (!status) return '';
19
+ const normalized = status.toLowerCase().replace(/\s+/g, '-');
20
+ if (normalized === 'in-progress') return 'in-progress';
21
+ if (normalized === 'done') return 'done';
22
+ if (normalized === 'closed') return 'closed';
23
+ return 'todo';
24
+ };
25
+
26
+ export const getPriorityIcon = (priority) => {
27
+ if (!priority) return null;
28
+ const name = priority.toLowerCase();
29
+ if (name.includes('highest') || name.includes('high')) {
30
+ return <ChevronUp size={16} />;
31
+ }
32
+ if (name.includes('lowest') || name.includes('low')) {
33
+ return <ChevronDown size={16} />;
34
+ }
35
+ return <Minus size={16} />;
36
+ };
37
+
38
+ export const getSeverityIcon = (severity) => {
39
+ if (!severity) return null;
40
+ const name = severity.toLowerCase();
41
+ if (name.includes('blocker') || name.includes('critical') || name.includes('sev-1')) {
42
+ return <AlertCircle size={14} />;
43
+ }
44
+ if (name.includes('major') || name.includes('sev-2')) {
45
+ return <ChevronUp size={14} />;
46
+ }
47
+ if (name.includes('minor') || name.includes('sev-3')) {
48
+ return <Minus size={14} />;
49
+ }
50
+ if (name.includes('trivial') || name.includes('sev-4') || name.includes('sev-5')) {
51
+ return <ChevronDown size={14} />;
52
+ }
53
+ return <Info size={14} />;
54
+ };
55
+
56
+ export const formatSeverity = (severity) => {
57
+ if (!severity) return '';
58
+ return severity;
59
+ };
60
+
61
+ export const getInitials = (name) => {
62
+ if (!name || typeof name !== 'string') return '?';
63
+
64
+ const trimmed = name.trim();
65
+ if (trimmed.length === 0) return '?';
66
+
67
+ const atIndex = trimmed.indexOf('@');
68
+ const namePart = atIndex > 0 ? trimmed.substring(0, atIndex) : trimmed;
69
+
70
+ const parts = namePart.split(/\s+/).filter(Boolean);
71
+
72
+ if (parts.length === 0) return '?';
73
+
74
+ if (parts.length === 1) {
75
+ const word = parts[0];
76
+ const firstChar = word[0] || '';
77
+ const secondChar = word[1] || '';
78
+ return (firstChar + secondChar).toUpperCase();
79
+ }
80
+
81
+ const first = parts[0][0] || '';
82
+ const last = parts[parts.length - 1][0] || '';
83
+ return (first + last).toUpperCase();
84
+ };
@@ -0,0 +1,27 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ const ALLOWED_TAGS = [
4
+ 'p', 'br', 'strong', 'em', 'u', 's', 'del', 'ins',
5
+ 'a', 'img', 'span', 'div',
6
+ 'ul', 'ol', 'li',
7
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
8
+ 'code', 'pre', 'blockquote',
9
+ 'table', 'thead', 'tbody', 'tr', 'th', 'td',
10
+ 'hr', 'sup', 'sub'
11
+ ];
12
+
13
+ const ALLOWED_ATTR = [
14
+ 'href', 'src', 'alt', 'class', 'className',
15
+ 'target', 'rel', 'id', 'style',
16
+ 'colspan', 'rowspan', 'bgcolor'
17
+ ];
18
+
19
+ export const sanitizeHtml = (html) => {
20
+ if (!html) return '';
21
+ return DOMPurify.sanitize(html, {
22
+ ALLOWED_TAGS,
23
+ ALLOWED_ATTR,
24
+ ALLOW_DATA_ATTR: false,
25
+ ADD_ATTR: ['target'],
26
+ });
27
+ };
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173
8
+ },
9
+ test: {
10
+ globals: true,
11
+ environment: 'jsdom',
12
+ setupFiles: ['./src/__tests__/setup.js'],
13
+ include: ['src/__tests__/**/*.test.{js,jsx}'],
14
+ }
15
+ })
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "jira-pat",
3
+ "version": "1.0.0",
4
+ "description": "Professional Jira Dashboard CLI tool",
5
+ "workspaces": [
6
+ "backend",
7
+ "frontend"
8
+ ],
9
+ "scripts": {
10
+ "start": "concurrently \"npm run start --workspace backend\" \"npm run dev --workspace frontend\""
11
+ },
12
+ "bin": {
13
+ "jira": "./bin/jira.js",
14
+ "jira-config": "./bin/jira.js"
15
+ },
16
+ "devDependencies": {
17
+ "concurrently": "^8.2.2"
18
+ }
19
+ }