From a38cb67083b0aa5f8b684dcf68047b6e743a1f2f Mon Sep 17 00:00:00 2001 From: Bernhard Dorn Date: Thu, 15 Jan 2026 12:44:38 +0100 Subject: [PATCH 1/2] feat: implement Jira API integration for fetching and managing tickets, and create UI components for displaying ticket lists. --- src/components/TicketItem.tsx | 103 +++++++++++++++++++++++++++++----- src/components/TicketList.tsx | 9 ++- src/lib/jira.ts | 21 +++++++ 3 files changed, 117 insertions(+), 16 deletions(-) diff --git a/src/components/TicketItem.tsx b/src/components/TicketItem.tsx index 4a6dc1a..78f89ae 100644 --- a/src/components/TicketItem.tsx +++ b/src/components/TicketItem.tsx @@ -1,14 +1,14 @@ import { useState, useEffect } from "react"; import type { JiraTicket, AppSettings } from "../lib/types"; import type { ActiveTimer } from "../hooks/useActiveTimer"; -import { addWorklog } from "../lib/jira"; +import { addWorklog, checkWorklogPermission } from "../lib/jira"; import { formatDuration, formatDurationFromStart, parseDuration, cn } from "../lib/utils"; import { Button } from "./ui/Button"; import { Input } from "./ui/Input"; import { Play, Square, ExternalLink, ChevronDown, ChevronUp, Clock, Bug, CheckSquare, Bookmark, Zap, GitCommit, FileQuestion, - HelpCircle, Microscope, PinOff + HelpCircle, Microscope, PinOff, Trash2, RotateCcw } from "lucide-react"; // Helper for Issue Type Icon @@ -84,9 +84,11 @@ export const TicketItem = ({ const [description, setDescription] = useState(() => { return localStorage.getItem(getStorageKey()) || ""; }); + const [lastErrorMessage, setLastErrorMessage] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [liveDuration, setLiveDuration] = useState(""); + const [hasError, setHasError] = useState(false); const isTimerRunning = activeTimer?.ticketId === ticket.id; @@ -129,6 +131,8 @@ export const TicketItem = ({ if (!manualTime) return; setIsSubmitting(true); + setHasError(false); // Clear previous errors + setLastErrorMessage(""); try { // Check Easter Egg const seconds = parseDuration(manualTime); @@ -147,7 +151,10 @@ export const TicketItem = ({ // Optional: Show success feedback } catch (error) { console.error(error); - alert("Failed to log time. Check console."); + setHasError(true); + const msg = error instanceof Error ? error.message : "Failed to log time."; + setLastErrorMessage(msg); + // alert(msg); // Removed alert to rely on in-component display } finally { setIsSubmitting(false); } @@ -157,6 +164,8 @@ export const TicketItem = ({ if (!activeTimer) return; setIsSubmitting(true); + setHasError(false); // Clear previous errors + setLastErrorMessage(""); try { let seconds = Math.floor((Date.now() - activeTimer.startTime) / 1000); @@ -179,7 +188,52 @@ export const TicketItem = ({ onRefresh(); } catch (error) { console.error(error); - alert("Failed to save timer. Check console."); + setHasError(true); + const msg = error instanceof Error ? error.message : "Failed to save timer."; + setLastErrorMessage(msg); + // alert(msg); // Removed alert to rely on in-component display + } finally { + setIsSubmitting(false); + } + }; + + const handleDiscardTimer = () => { + if (!confirm("Are you sure you want to discard the currently tracked time? This cannot be undone.")) return; + + onStopTimer(); + setDescription(""); + setHasError(false); // Clear errors on discard + setLastErrorMessage(""); + localStorage.removeItem(getStorageKey()); + onRefresh(); + }; + + const handleStartTimerWithCheck = async () => { + setIsSubmitting(true); + setHasError(false); // Clear previous errors + setLastErrorMessage(""); + try { + const hasPermission = await checkWorklogPermission(settings, ticket.key); + if (!hasPermission) { + const msg = `You do not have permission to log work on ${ticket.key}.`; + setHasError(true); + setLastErrorMessage(msg); + // alert(msg); // Removed alert to rely on in-component display + return; + } + onStartTimer(ticket.id); + } catch (error) { + console.error("Failed to check permissions:", error); + const msg = error instanceof Error ? error.message : "Failed to check permissions."; + setHasError(true); + setLastErrorMessage(msg); + // Fallback: allow starting if check fails? Or block? + // Better to block if we can't be sure, or just warn. + if (confirm(`Could not verify permissions: ${msg}. Start timer anyway?`)) { + setHasError(false); // Clear error if user proceeds + setLastErrorMessage(""); + onStartTimer(ticket.id); + } } finally { setIsSubmitting(false); } @@ -265,21 +319,33 @@ export const TicketItem = ({ {/* Timer Section */}
{isTimerRunning ? ( - +
+ + +
) : (
+ + {hasError && lastErrorMessage && ( +
+
Error
+ {lastErrorMessage} +
+ )} )} diff --git a/src/components/TicketList.tsx b/src/components/TicketList.tsx index deea447..e3c21c6 100644 --- a/src/components/TicketList.tsx +++ b/src/components/TicketList.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import type { AppSettings, JiraTicket } from "../lib/types"; -import { fetchInProgressTickets, fetchDoneTickets, fetchTicketsByKeys } from "../lib/jira"; +import { fetchInProgressTickets, fetchDoneTickets, fetchTicketsByKeys, checkWorklogPermission } from "../lib/jira"; import { useActiveTimer } from "../hooks/useActiveTimer"; import { saveSettings } from "../lib/storage"; import { TicketItem } from "./TicketItem"; @@ -133,6 +133,13 @@ export const TicketList = ({ settings, onSettingsChange, onTimeUpdate }: TicketL const [ticket] = await fetchTicketsByKeys(settings, [key]); if (ticket) { + // Check permissions before pinning + const hasPermission = await checkWorklogPermission(settings, key); + if (!hasPermission) { + alert(`You do not have permission to log work on ${key}. This ticket cannot be pinned for time tracking.`); + return; + } + const newKeys = [...settings.pinnedTicketKeys, key]; await saveSettings({ ...settings, pinnedTicketKeys: newKeys }); setPinInput(""); diff --git a/src/lib/jira.ts b/src/lib/jira.ts index 98596eb..8db36f9 100644 --- a/src/lib/jira.ts +++ b/src/lib/jira.ts @@ -270,3 +270,24 @@ export const fetchTodaysTime = async (settings: AppSettings): Promise => return 0; } }; + +export const checkWorklogPermission = async (settings: AppSettings, ticketKey: string): Promise => { + try { + const response = await fetch(`${settings.jiraHost}/rest/api/3/mypermissions?issueKey=${ticketKey}&permissions=WORK_ON_ISSUES`, { + headers: createHeaders(settings), + }); + + if (!response.ok) { + console.error(`Permission check failed for ${ticketKey}:`, response.status, response.statusText); + return false; + } + + const data = await response.json(); + const hasPermission = data.permissions?.WORK_ON_ISSUES?.havePermission === true; + console.log(`Permission check for ${ticketKey}:`, hasPermission); + return hasPermission; + } catch (error) { + console.error("Permission check error:", error); + return false; + } +}; From 64e01768fd9e1ca21c2d6d8986c3bc32c86c66a4 Mon Sep 17 00:00:00 2001 From: Bernhard Dorn Date: Thu, 15 Jan 2026 13:09:34 +0100 Subject: [PATCH 2/2] chore: Bump project version from 1.4.0 to 1.5.0. --- manifest.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index f40ad06..bcffeb7 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "JiraTime", - "version": "1.4.0", + "version": "1.5.0", "description": "Simple Jira Time Tracking for Developers. By yours truly Bernhard Dorn.", "author": "Bernhard Dorn", "action": { diff --git a/package.json b/package.json index 822072f..b70c131 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jiratime", "private": true, - "version": "1.4.0", + "version": "1.5.0", "type": "module", "scripts": { "dev": "vite",