Show raw Jira status everywhere instead of mapping to Open/In Progress/Closed
- Drop CHECK constraint on jira_tickets.status to allow any status string - Store raw Jira status directly in status column during sync (remove mapJiraStatusToLocal) - Remove VALID_TICKET_STATUSES validation on create/update endpoints - Remove separate Jira Status column from table (status IS the Jira status now) - Update frontend status badges to color-code dynamically based on status category - Update Open Tickets widget and CVE detail view to use isClosedStatus() helper - Make filter dropdown dynamic based on actual ticket statuses - Add migration script for dropping the constraint on other deployments
This commit is contained in:
@@ -126,7 +126,7 @@ CREATE TABLE IF NOT EXISTS jira_tickets (
|
|||||||
ticket_key TEXT NOT NULL,
|
ticket_key TEXT NOT NULL,
|
||||||
url TEXT,
|
url TEXT,
|
||||||
summary TEXT,
|
summary TEXT,
|
||||||
status TEXT DEFAULT 'Open' CHECK (status IN ('Open', 'In Progress', 'Closed')),
|
status TEXT DEFAULT 'Open',
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|||||||
18
backend/migrations/drop_jira_status_check_constraint.js
Normal file
18
backend/migrations/drop_jira_status_check_constraint.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Migration: Drop CHECK constraint on jira_tickets.status
|
||||||
|
// Allows storing raw Jira status strings (e.g. "Approval/Handoff", "Prioritizing")
|
||||||
|
// instead of mapping to the limited set of Open/In Progress/Closed.
|
||||||
|
// Idempotent — safe to run multiple times.
|
||||||
|
|
||||||
|
const pool = require('../db');
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('[Migration] Dropping jira_tickets_status_check constraint...');
|
||||||
|
await pool.query(`ALTER TABLE jira_tickets DROP CONSTRAINT IF EXISTS jira_tickets_status_check`);
|
||||||
|
console.log('✓ jira_tickets status CHECK constraint dropped (or did not exist)');
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(err => {
|
||||||
|
console.error('Migration failed:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -18,7 +18,6 @@ const jiraApi = require('../helpers/jiraApi');
|
|||||||
|
|
||||||
// Validation helpers
|
// Validation helpers
|
||||||
const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
|
const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
|
||||||
const VALID_TICKET_STATUSES = ['Open', 'In Progress', 'Closed'];
|
|
||||||
|
|
||||||
function isValidCveId(cveId) {
|
function isValidCveId(cveId) {
|
||||||
return typeof cveId === 'string' && CVE_ID_PATTERN.test(cveId);
|
return typeof cveId === 'string' && CVE_ID_PATTERN.test(cveId);
|
||||||
@@ -354,12 +353,11 @@ function createJiraTicketsRouter() {
|
|||||||
|
|
||||||
const jiraStatus = issue.fields.status ? issue.fields.status.name : null;
|
const jiraStatus = issue.fields.status ? issue.fields.status.name : null;
|
||||||
const jiraSummary = issue.fields.summary || ticket.summary;
|
const jiraSummary = issue.fields.summary || ticket.summary;
|
||||||
const localStatus = mapJiraStatusToLocal(jiraStatus);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE jira_tickets SET summary = $1, status = $2, jira_status = $3, last_synced_at = NOW(), updated_at = NOW() WHERE id = $4`,
|
`UPDATE jira_tickets SET summary = $1, status = $2, jira_status = $3, last_synced_at = NOW(), updated_at = NOW() WHERE id = $4`,
|
||||||
[jiraSummary, localStatus, jiraStatus, ticket.id]
|
[jiraSummary, jiraStatus || 'Open', jiraStatus, ticket.id]
|
||||||
);
|
);
|
||||||
results.synced++;
|
results.synced++;
|
||||||
} catch (dbErr) {
|
} catch (dbErr) {
|
||||||
@@ -435,11 +433,10 @@ function createJiraTicketsRouter() {
|
|||||||
const issue = result.data;
|
const issue = result.data;
|
||||||
const jiraStatus = issue.fields.status ? issue.fields.status.name : null;
|
const jiraStatus = issue.fields.status ? issue.fields.status.name : null;
|
||||||
const jiraSummary = issue.fields.summary || ticket.summary;
|
const jiraSummary = issue.fields.summary || ticket.summary;
|
||||||
const localStatus = mapJiraStatusToLocal(jiraStatus);
|
|
||||||
|
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE jira_tickets SET summary = $1, status = $2, jira_status = $3, last_synced_at = NOW(), updated_at = NOW() WHERE id = $4`,
|
`UPDATE jira_tickets SET summary = $1, status = $2, jira_status = $3, last_synced_at = NOW(), updated_at = NOW() WHERE id = $4`,
|
||||||
[jiraSummary, localStatus, jiraStatus, id]
|
[jiraSummary, jiraStatus || 'Open', jiraStatus, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
logAudit({
|
logAudit({
|
||||||
@@ -563,8 +560,8 @@ function createJiraTicketsRouter() {
|
|||||||
if (summary && (typeof summary !== 'string' || summary.length > 500)) {
|
if (summary && (typeof summary !== 'string' || summary.length > 500)) {
|
||||||
return res.status(400).json({ error: 'Summary must be under 500 characters.' });
|
return res.status(400).json({ error: 'Summary must be under 500 characters.' });
|
||||||
}
|
}
|
||||||
if (status && !VALID_TICKET_STATUSES.includes(status)) {
|
if (status && typeof status !== 'string') {
|
||||||
return res.status(400).json({ error: `Status must be one of: ${VALID_TICKET_STATUSES.join(', ')}` });
|
return res.status(400).json({ error: 'Status must be a string.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticketStatus = status || 'Open';
|
const ticketStatus = status || 'Open';
|
||||||
@@ -632,8 +629,8 @@ function createJiraTicketsRouter() {
|
|||||||
if (summary !== undefined && summary !== null && (typeof summary !== 'string' || summary.length > 500)) {
|
if (summary !== undefined && summary !== null && (typeof summary !== 'string' || summary.length > 500)) {
|
||||||
return res.status(400).json({ error: 'Summary must be under 500 characters.' });
|
return res.status(400).json({ error: 'Summary must be under 500 characters.' });
|
||||||
}
|
}
|
||||||
if (status !== undefined && !VALID_TICKET_STATUSES.includes(status)) {
|
if (status !== undefined && typeof status !== 'string') {
|
||||||
return res.status(400).json({ error: `Status must be one of: ${VALID_TICKET_STATUSES.join(', ')}` });
|
return res.status(400).json({ error: 'Status must be a string.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = [];
|
const fields = [];
|
||||||
|
|||||||
@@ -25,6 +25,22 @@ import './App.css';
|
|||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
|
// Determine if a Jira status represents a "closed/done" state
|
||||||
|
function isClosedStatus(status) {
|
||||||
|
if (!status) return false;
|
||||||
|
const lower = status.toLowerCase();
|
||||||
|
return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTicketStatusColor(status) {
|
||||||
|
if (!status) return '#F59E0B';
|
||||||
|
if (isClosedStatus(status)) return '#10B981';
|
||||||
|
const lower = status.toLowerCase();
|
||||||
|
if (['open', 'to do', 'backlog', 'new'].some(s => lower === s)) return '#F59E0B';
|
||||||
|
// Everything else (in progress, approval, prioritizing, etc.) gets blue/purple
|
||||||
|
return '#0EA5E9';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// INLINE STYLES - NUCLEAR OPTION FOR VISIBILITY
|
// INLINE STYLES - NUCLEAR OPTION FOR VISIBILITY
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -1072,7 +1088,7 @@ export default function App() {
|
|||||||
<div style={{...STYLES.statCard, border: '2px solid #F59E0B', boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(245, 158, 11, 0.15), inset 0 1px 0 rgba(245, 158, 11, 0.15)'}}>
|
<div style={{...STYLES.statCard, border: '2px solid #F59E0B', boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(245, 158, 11, 0.15), inset 0 1px 0 rgba(245, 158, 11, 0.15)'}}>
|
||||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, #F59E0B, transparent)', boxShadow: '0 0 8px rgba(245, 158, 11, 0.5)' }}></div>
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, #F59E0B, transparent)', boxShadow: '0 0 8px rgba(245, 158, 11, 0.5)' }}></div>
|
||||||
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#CBD5E1', marginBottom: '0.25rem' }}>Open Tickets</div>
|
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#CBD5E1', marginBottom: '0.25rem' }}>Open Tickets</div>
|
||||||
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#F59E0B', textShadow: '0 0 16px rgba(245, 158, 11, 0.4)' }}>{jiraTickets.filter(t => t.status !== 'Closed').length}</div>
|
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#F59E0B', textShadow: '0 0 16px rgba(245, 158, 11, 0.4)' }}>{jiraTickets.filter(t => !isClosedStatus(t.status)).length}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{...STYLES.statCard, border: '2px solid #EF4444', boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(239, 68, 68, 0.15), inset 0 1px 0 rgba(239, 68, 68, 0.15)'}}>
|
<div style={{...STYLES.statCard, border: '2px solid #EF4444', boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(239, 68, 68, 0.15), inset 0 1px 0 rgba(239, 68, 68, 0.15)'}}>
|
||||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, #EF4444, transparent)', boxShadow: '0 0 8px rgba(239, 68, 68, 0.5)' }}></div>
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, #EF4444, transparent)', boxShadow: '0 0 8px rgba(239, 68, 68, 0.5)' }}></div>
|
||||||
@@ -1475,6 +1491,9 @@ export default function App() {
|
|||||||
<option value="Open">Open</option>
|
<option value="Open">Open</option>
|
||||||
<option value="In Progress">In Progress</option>
|
<option value="In Progress">In Progress</option>
|
||||||
<option value="Closed">Closed</option>
|
<option value="Closed">Closed</option>
|
||||||
|
{ticketForm.status && !['Open', 'In Progress', 'Closed'].includes(ticketForm.status) && (
|
||||||
|
<option value={ticketForm.status}>{ticketForm.status}</option>
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
@@ -1544,6 +1563,9 @@ export default function App() {
|
|||||||
<option value="Open">Open</option>
|
<option value="Open">Open</option>
|
||||||
<option value="In Progress">In Progress</option>
|
<option value="In Progress">In Progress</option>
|
||||||
<option value="Closed">Closed</option>
|
<option value="Closed">Closed</option>
|
||||||
|
{ticketForm.status && !['Open', 'In Progress', 'Closed'].includes(ticketForm.status) && (
|
||||||
|
<option value={ticketForm.status}>{ticketForm.status}</option>
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
@@ -2096,15 +2118,11 @@ export default function App() {
|
|||||||
</a>
|
</a>
|
||||||
{ticket.summary && <span style={{ fontSize: '0.875rem', color: '#E4E8F1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '20rem' }}>{ticket.summary}</span>}
|
{ticket.summary && <span style={{ fontSize: '0.875rem', color: '#E4E8F1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '20rem' }}>{ticket.summary}</span>}
|
||||||
<span style={
|
<span style={
|
||||||
ticket.status === 'Open' ? STYLES.badgeCritical :
|
isClosedStatus(ticket.status) ? STYLES.badgeLow :
|
||||||
ticket.status === 'In Progress' ? STYLES.badgeHigh :
|
getTicketStatusColor(ticket.status) === '#0EA5E9' ? STYLES.badgeHigh :
|
||||||
STYLES.badgeLow
|
STYLES.badgeCritical
|
||||||
}>
|
}>
|
||||||
<span style={STYLES.glowDot(
|
<span style={STYLES.glowDot(getTicketStatusColor(ticket.status))}></span>
|
||||||
ticket.status === 'Open' ? '#FF3366' :
|
|
||||||
ticket.status === 'In Progress' ? '#FFB800' :
|
|
||||||
'#00FF88'
|
|
||||||
)}></span>
|
|
||||||
{ticket.status}
|
{ticket.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -2220,12 +2238,12 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-center mb-3">
|
<div className="text-center mb-3">
|
||||||
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#F59E0B', textShadow: '0 0 16px rgba(245, 158, 11, 0.4)' }}>
|
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#F59E0B', textShadow: '0 0 16px rgba(245, 158, 11, 0.4)' }}>
|
||||||
{jiraTickets.filter(t => t.status !== 'Closed').length}
|
{jiraTickets.filter(t => !isClosedStatus(t.status)).length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">Active</div>
|
<div className="text-xs text-gray-400 uppercase tracking-wider">Active</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
{jiraTickets.filter(t => t.status !== 'Closed').slice(0, 10).map(ticket => (
|
{jiraTickets.filter(t => !isClosedStatus(t.status)).slice(0, 10).map(ticket => (
|
||||||
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)', border: '1px solid rgba(245, 158, 11, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)' }}>
|
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)', border: '1px solid rgba(245, 158, 11, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)' }}>
|
||||||
<div className="flex items-start justify-between gap-2 mb-1">
|
<div className="flex items-start justify-between gap-2 mb-1">
|
||||||
<a
|
<a
|
||||||
@@ -2254,13 +2272,13 @@ export default function App() {
|
|||||||
{ticket.summary && <div className="text-xs text-gray-300 mt-1 truncate">{ticket.summary}</div>}
|
{ticket.summary && <div className="text-xs text-gray-300 mt-1 truncate">{ticket.summary}</div>}
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<span style={{ ...STYLES.badgeHigh, fontSize: '0.65rem', padding: '0.25rem 0.5rem' }}>
|
<span style={{ ...STYLES.badgeHigh, fontSize: '0.65rem', padding: '0.25rem 0.5rem' }}>
|
||||||
<span style={{...STYLES.glowDot('#F59E0B'), width: '6px', height: '6px'}}></span>
|
<span style={{...STYLES.glowDot(getTicketStatusColor(ticket.status)), width: '6px', height: '6px'}}></span>
|
||||||
{ticket.status}
|
{ticket.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{jiraTickets.filter(t => t.status !== 'Closed').length === 0 && (
|
{jiraTickets.filter(t => !isClosedStatus(t.status)).length === 0 && (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<CheckCircle className="w-8 h-8 text-intel-success mx-auto mb-2" />
|
<CheckCircle className="w-8 h-8 text-intel-success mx-auto mb-2" />
|
||||||
<p className="text-sm text-gray-400 italic font-mono">No open tickets</p>
|
<p className="text-sm text-gray-400 italic font-mono">No open tickets</p>
|
||||||
|
|||||||
@@ -134,8 +134,28 @@ const STATUS_COLORS = {
|
|||||||
'Open': '#F59E0B',
|
'Open': '#F59E0B',
|
||||||
'In Progress': '#0EA5E9',
|
'In Progress': '#0EA5E9',
|
||||||
'Closed': '#10B981',
|
'Closed': '#10B981',
|
||||||
|
'Done': '#10B981',
|
||||||
|
'Resolved': '#10B981',
|
||||||
|
'Approval/Handoff': '#8B5CF6',
|
||||||
|
'Prioritizing': '#0EA5E9',
|
||||||
|
'In Review': '#0EA5E9',
|
||||||
|
'In Development': '#0EA5E9',
|
||||||
|
'In Testing': '#0EA5E9',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Determine if a status represents a "closed/done" state
|
||||||
|
function isClosedStatus(status) {
|
||||||
|
if (!status) return false;
|
||||||
|
const lower = status.toLowerCase();
|
||||||
|
return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status) {
|
||||||
|
if (STATUS_COLORS[status]) return STATUS_COLORS[status];
|
||||||
|
if (isClosedStatus(status)) return '#10B981';
|
||||||
|
return '#F59E0B';
|
||||||
|
}
|
||||||
|
|
||||||
const SOURCE_CONTEXT_CONFIG = {
|
const SOURCE_CONTEXT_CONFIG = {
|
||||||
cve: { label: 'CVE', color: '#0EA5E9' },
|
cve: { label: 'CVE', color: '#0EA5E9' },
|
||||||
archer: { label: 'Archer', color: '#8B5CF6' },
|
archer: { label: 'Archer', color: '#8B5CF6' },
|
||||||
@@ -307,7 +327,7 @@ export default function JiraPage() {
|
|||||||
ticket_key: issue.key,
|
ticket_key: issue.key,
|
||||||
url: jiraUrl,
|
url: jiraUrl,
|
||||||
summary: issue.summary || '',
|
summary: issue.summary || '',
|
||||||
status: issue.status === 'Open' || issue.status === 'In Progress' || issue.status === 'Closed' ? issue.status : 'Open',
|
status: issue.status || 'Open',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -470,9 +490,8 @@ export default function JiraPage() {
|
|||||||
|
|
||||||
const counts = {
|
const counts = {
|
||||||
total: tickets.length,
|
total: tickets.length,
|
||||||
open: tickets.filter(t => t.status === 'Open').length,
|
open: tickets.filter(t => !isClosedStatus(t.status)).length,
|
||||||
inProgress: tickets.filter(t => t.status === 'In Progress').length,
|
closed: tickets.filter(t => isClosedStatus(t.status)).length,
|
||||||
closed: tickets.filter(t => t.status === 'Closed').length,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -549,7 +568,6 @@ export default function JiraPage() {
|
|||||||
{[
|
{[
|
||||||
{ label: 'Total', value: counts.total, color: '#0EA5E9' },
|
{ label: 'Total', value: counts.total, color: '#0EA5E9' },
|
||||||
{ label: 'Open', value: counts.open, color: '#F59E0B' },
|
{ label: 'Open', value: counts.open, color: '#F59E0B' },
|
||||||
{ label: 'In Progress', value: counts.inProgress, color: '#0EA5E9' },
|
|
||||||
{ label: 'Closed', value: counts.closed, color: '#10B981' },
|
{ label: 'Closed', value: counts.closed, color: '#10B981' },
|
||||||
].map(s => (
|
].map(s => (
|
||||||
<div key={s.label} style={STYLES.statCard}>
|
<div key={s.label} style={STYLES.statCard}>
|
||||||
@@ -586,9 +604,9 @@ export default function JiraPage() {
|
|||||||
onChange={e => setFilterStatus(e.target.value)}
|
onChange={e => setFilterStatus(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">All Statuses</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="Open">Open</option>
|
{[...new Set(tickets.map(t => t.status).filter(Boolean))].sort().map(s => (
|
||||||
<option value="In Progress">In Progress</option>
|
<option key={s} value={s}>{s}</option>
|
||||||
<option value="Closed">Closed</option>
|
))}
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
style={{ ...STYLES.input, width: 'auto', minWidth: '140px', cursor: 'pointer' }}
|
style={{ ...STYLES.input, width: 'auto', minWidth: '140px', cursor: 'pointer' }}
|
||||||
@@ -630,7 +648,6 @@ export default function JiraPage() {
|
|||||||
<th style={STYLES.th}>Source</th>
|
<th style={STYLES.th}>Source</th>
|
||||||
<th style={STYLES.th}>Summary</th>
|
<th style={STYLES.th}>Summary</th>
|
||||||
<th style={STYLES.th}>Status</th>
|
<th style={STYLES.th}>Status</th>
|
||||||
<th style={STYLES.th}>Jira Status</th>
|
|
||||||
<th style={STYLES.th}>Last Synced</th>
|
<th style={STYLES.th}>Last Synced</th>
|
||||||
<th style={STYLES.th}>Actions</th>
|
<th style={STYLES.th}>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -677,12 +694,11 @@ export default function JiraPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td style={{ ...STYLES.td, maxWidth: '250px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.summary || '-'}</td>
|
<td style={{ ...STYLES.td, maxWidth: '250px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.summary || '-'}</td>
|
||||||
<td style={STYLES.td}>
|
<td style={STYLES.td}>
|
||||||
<span style={STYLES.badge(STATUS_COLORS[t.status] || '#94A3B8')}>
|
<span style={STYLES.badge(getStatusColor(t.status))}>
|
||||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: STATUS_COLORS[t.status] || '#94A3B8' }} />
|
<span style={{ width: 6, height: 6, borderRadius: '50%', background: getStatusColor(t.status) }} />
|
||||||
{t.status}
|
{t.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ ...STYLES.td, fontSize: '0.8rem', color: '#CBD5E1' }}>{t.jira_status || '-'}</td>
|
|
||||||
<td style={{ ...STYLES.td, fontSize: '0.75rem', color: '#94A3B8' }}>
|
<td style={{ ...STYLES.td, fontSize: '0.75rem', color: '#94A3B8' }}>
|
||||||
{t.last_synced_at ? new Date(t.last_synced_at).toLocaleDateString() : 'Never'}
|
{t.last_synced_at ? new Date(t.last_synced_at).toLocaleDateString() : 'Never'}
|
||||||
</td>
|
</td>
|
||||||
@@ -804,6 +820,9 @@ export default function JiraPage() {
|
|||||||
<option value="Open">Open</option>
|
<option value="Open">Open</option>
|
||||||
<option value="In Progress">In Progress</option>
|
<option value="In Progress">In Progress</option>
|
||||||
<option value="Closed">Closed</option>
|
<option value="Closed">Closed</option>
|
||||||
|
{form.status && !['Open', 'In Progress', 'Closed'].includes(form.status) && (
|
||||||
|
<option value={form.status}>{form.status}</option>
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button style={{ ...STYLES.btn, ...STYLES.btnSuccess, justifyContent: 'center', marginTop: '0.5rem' }} onClick={saveTicket} disabled={formSaving}>
|
<button style={{ ...STYLES.btn, ...STYLES.btnSuccess, justifyContent: 'center', marginTop: '0.5rem' }} onClick={saveTicket} disabled={formSaving}>
|
||||||
|
|||||||
Reference in New Issue
Block a user