jira-pat 1.0.3 → 1.0.5

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
@@ -1,135 +1,155 @@
1
1
  # Jira Dashboard
2
2
 
3
- A fast, lightweight, and modern Jira Dashboard built with React, Vite, and Node.js. It avoids CORS and proxy issues by connecting directly to the Jira Cloud API using Basic Authentication within a dedicated Express backend.
4
-
5
- ## Features
6
-
7
- 1. **Direct Jira Cloud Integration**: Connects to the Jira REST API securely via an Express backend.
8
- 2. **CLI Configuration**: Easy setup with the built-in `jira` CLI tool.
9
- 3. **Dynamic Filtering**: Filter by project, status, or keyword.
10
- 4. **Interactive Issue View**:
11
- - Full issue details in a side drawer or standalone page.
12
- - Rich text rendering (Markdown/ADF to HTML).
13
- - Proxying of Jira-hosted images and attachments.
14
- - Status transitions and user assignment.
15
- - File uploads directly to Jira tickets.
16
- 5. **Subtasks & Linked Issues**: Visualize and navigate between related tickets.
17
- 6. **Backend Caching**: 60-second in-memory caching for performance and API rate-limiting compliance.
18
- 7. **Fully Tested**: Comprehensive unit and integration tests for both frontend and backend.
19
- 8. **Error Handling**: Robust error boundaries and toast notifications for a smooth experience.
3
+ View and manage your Jira tickets from a simple dashboard no need to navigate Jira itself.
20
4
 
21
5
  ---
22
6
 
23
- ## Getting Started
24
-
25
- ### Prerequisites
7
+ ## Before You Begin
26
8
 
27
- - Node.js (v18 or higher recommended)
28
- - An Atlassian account with a Jira Cloud instance
29
- - A Jira API Token ([Get one here](https://id.atlassian.com/manage-profile/security/api-tokens))
9
+ You'll need three things set up before you can use the dashboard. This only takes about 5 minutes.
30
10
 
31
- ### Installation & Setup
11
+ ### 1. Node.js (a behind-the-scenes engine)
32
12
 
33
- 1. **Download & Install**:
34
- ```bash
35
- git clone https://github.com/yourusername/jira-dashboard.git
36
- cd jira-dashboard
37
- npm install
38
- ```
13
+ The dashboard needs Node.js installed on your computer to run. You don't need to know what it does — just think of it as a required engine.
39
14
 
40
- 2. **Configure your Account**:
41
- Run the setup tool to connect your Jira account:
42
- ```bash
43
- npm link # Optional: allows you to just type 'jira'
44
- jira config
45
- ```
46
- Follow the prompts to enter your **Jira URL**, **Email**, and **API Token**.
15
+ **Check if you already have it:**
16
+ 1. Open **Terminal** (Mac) or **Command Prompt** (Windows)
17
+ - Mac: Press `Cmd + Space`, type *Terminal*, hit Enter
18
+ - Windows: Press `Win + R`, type *cmd*, hit Enter
19
+ 2. Type `node --version` and press Enter
20
+ 3. If you see something like `v18.0.0` or higher, you're all set ✅
47
21
 
48
- 3. **Launch the App**:
49
- ```bash
50
- npm start
51
- ```
52
- Your browser should automatically open to `http://localhost:5000`.
22
+ **If you don't have it:**
23
+ 1. Go to [nodejs.org](https://nodejs.org)
24
+ 2. Click the big **"LTS"** download button (LTS = most stable version)
25
+ 3. Install it like any other program
26
+ 4. Close and reopen your Terminal window before continuing
53
27
 
54
28
  ---
55
29
 
56
- ## Using the Dashboard (User Guide)
30
+ ### 2. A Jira API Token (your secure password for the app)
57
31
 
58
- Once the dashboard is open, here is how you can manage your work:
32
+ Instead of using your Jira password directly, you'll create a special token just for this app. It's safer and easy to remove if needed.
59
33
 
60
- ### 1. Finding Your Work
61
- * **Default View**: When you log in, you will see all issues currently assigned to you.
62
- * **Search**: Use the search bar at the top to find specific tickets by typing their title or description. (Tip: Press `/` to jump to search).
63
- * **Filters**: Use the **Project** and **Status** dropdowns to narrow down your list (e.g., only show "In Progress" tasks for project "ABC").
34
+ **How to create one:**
35
+ 1. Go to [Atlassian API Token page](https://id.atlassian.com/manage-profile/security/api-tokens)
36
+ 2. Click **"Create API token"**
37
+ 3. Give it any name for example, *Jira Dashboard*
38
+ 4. Click **"Create"**, then **copy the token** that appears
39
+ 5. Paste it somewhere safe (like a note on your computer) — you'll need it in the next section and **won't be able to see it again**
64
40
 
65
- ### 2. Viewing and Updating Issues
66
- * **Open Details**: Click on any row in the table. A side panel will slide out with the full description and comments.
67
- * **Change Status**: Inside the panel, click the status button (like "To Do") to move the ticket to a new stage (like "Done").
68
- * **Reassign**: Click the user's name or avatar to search for and assign the ticket to someone else.
69
- * **Add Attachments**: Drag and drop files directly onto the "Upload" area in the side panel to add them to the Jira ticket.
41
+ ---
42
+
43
+ ### 3. Your Jira URL
70
44
 
71
- ### 3. Creating New Issues
72
- * Click the blue **+** button in the top right corner.
73
- * Select the **Project** and **Issue Type** (e.g., Task, Bug).
74
- * Enter a **Summary** and click **Create**. The ticket is instantly added to your Jira instance.
45
+ This is the web address you normally type to get to Jira at work. It usually looks like one of these:
46
+ - `https://yourcompany.atlassian.net`
47
+ - `https://jira.yourcompany.com`
75
48
 
76
49
  ---
77
50
 
78
- ## Development (Technical)
51
+ ## Setup & Launch
79
52
 
80
- ### Running the Backend (API)
81
- ```bash
82
- cd backend
83
- npm start
84
- ```
85
- The API runs on `http://localhost:5000`.
53
+ ### Step 1 — Run the one-time setup
86
54
 
87
- ### Running the Frontend (Vite)
55
+ Open Terminal (or Command Prompt) and paste this, then press Enter:
88
56
  ```bash
89
- cd frontend
90
- npm run dev
57
+ npx jira-pat config
91
58
  ```
92
- The frontend runs on `http://localhost:5173` (proxies `/api` to port 5000).
59
+
60
+ It will ask you for three things:
61
+ - **Jira URL** — the address from Step 3 above
62
+ - **Email** — the email you use to log into Jira
63
+ - **API Token** — the token you created in Step 2
64
+
65
+ > 🔒 Your details are saved only on your computer and are never sent anywhere else.
66
+
67
+ **Where your details are saved (for your reference):**
68
+ - Mac/Linux: `~/.jira-dashboard-config.json`
69
+ - Windows: `C:\Users\YourName\.jira-dashboard-config.json`
93
70
 
94
71
  ---
95
72
 
96
- ## Testing
73
+ ### Step 2 — Start the dashboard
97
74
 
98
- ### Run All Tests
75
+ Each time you want to use the dashboard, run:
99
76
  ```bash
100
- npm test
77
+ npx jira-pat
101
78
  ```
102
79
 
103
- ### Backend Tests
104
- ```bash
105
- cd backend
106
- npm test
80
+ Your browser should open automatically. If it doesn't, open your browser and go to:
107
81
  ```
108
-
109
- ### Frontend Tests
110
- ```bash
111
- cd frontend
112
- npm test
82
+ http://localhost:5173
113
83
  ```
114
84
 
115
85
  ---
116
86
 
117
- ## Project Structure
87
+ ## Using the Dashboard
118
88
 
119
- ```
120
- jira-dashboard/
121
- ├── bin/ # CLI setup and start scripts
122
- ├── backend/ # Node.js + Express API Layer
123
- │ ├── routes/ # API Endpoints (Issues, Projects)
124
- │ ├── service/ # Jira API integration logic
125
- │ └── __tests__/ # Backend test suite
126
-
127
- └── frontend/ # React + Vite UI
128
- ├── src/
129
- │ ├── components/ # React components (Table, Drawer, Modals)
130
- │ ├── hooks/ # Custom React hooks (Data fetching, UI state)
131
- │ └── __tests__/ # Frontend test suite
132
- ```
89
+ ### Finding your tickets
90
+
91
+ - When you open the dashboard, you'll see all tickets currently assigned to you.
92
+ - Use the **search bar** at the top to look up a ticket by keyword. Tip: press `/` on your keyboard to jump straight to it.
93
+ - Use the **Project** and **Status** dropdowns to filter the list down to what you need.
94
+
95
+ ### Viewing and updating a ticket
96
+
97
+ - **Click any row** to open a side panel with the full details.
98
+ - **Change the status** by clicking the status label (e.g. *In Progress*, *Done*).
99
+ - **Reassign** a ticket by clicking the assignee's name and searching for someone else.
100
+ - **Attach a file** by dragging and dropping it onto the side panel.
101
+
102
+ ### Creating a new ticket
103
+
104
+ 1. Click the blue **+** button in the top-right corner.
105
+ 2. Choose a **Project** and **Issue Type** (Task, Bug, Story, etc.).
106
+ 3. Write a short **Summary** and click **Create**.
107
+
108
+ ---
109
+
110
+ ## Something Not Working?
111
+
112
+ | What you're seeing | What to try |
113
+ |--------------------|-------------|
114
+ | *"command not found"* | Node.js isn't installed — go back to Step 1 |
115
+ | *"Cannot connect to Jira"* | Check your Jira URL, email, and API token — run `npx jira-pat config` again |
116
+ | *"Port already in use"* | Close other apps or browser tabs that might be using ports 5000 or 5173 |
117
+ | Browser doesn't open | Manually go to `http://localhost:5173` in your browser |
118
+
119
+ ---
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
133
153
 
134
154
  ---
135
155
 
@@ -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
@@ -46,18 +46,28 @@ app.get('/api/health', (req, res) => {
46
46
  });
47
47
 
48
48
  // Serve frontend static files
49
- const frontendDist = path.join(__dirname, '../frontend/dist');
49
+ const frontendDist = path.resolve(__dirname, '..', 'frontend', 'dist');
50
+ console.log('--- Static Assets Setup ---');
51
+ console.log('Searching for frontend at:', frontendDist);
52
+
50
53
  if (require('fs').existsSync(frontendDist)) {
51
- console.log('Serving frontend from:', frontendDist);
54
+ console.log('✅ Frontend dist found. Serving static files.');
52
55
  app.use(express.static(frontendDist));
53
56
 
54
- // Catch-all for SPA: If no other route matches (and it's not an API call), serve index.html
55
57
  app.use((req, res, next) => {
56
58
  if (!req.path.startsWith('/api') && req.method === 'GET') {
57
- return res.sendFile(path.join(frontendDist, 'index.html'));
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
+ }
58
66
  }
59
67
  next();
60
68
  });
69
+ } else {
70
+ console.warn('⚠️ Warning: frontend/dist directory NOT found at ' + frontendDist);
61
71
  }
62
72
 
63
73
  app.listen(PORT, () => {
@@ -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;