Add issue workflow progress tracker
This commit is contained in:
@@ -40,6 +40,20 @@ type PortalItem = {
|
||||
related_item_id?: number | null
|
||||
is_resolved?: boolean
|
||||
resolved_at?: string | null
|
||||
workflow?: {
|
||||
current_step?: number
|
||||
total_steps?: number
|
||||
stage?: string
|
||||
stage_label?: string
|
||||
headline?: string
|
||||
message?: string
|
||||
state?: 'active' | 'attention' | 'complete'
|
||||
steps?: Array<{
|
||||
key: string
|
||||
label: string
|
||||
state: 'waiting' | 'active' | 'attention' | 'complete'
|
||||
}>
|
||||
}
|
||||
confirmation?: {
|
||||
status?: string | null
|
||||
attempts_sent?: number
|
||||
@@ -345,6 +359,53 @@ const formatIssueStatus = (value?: string | null) => {
|
||||
return labels[String(value ?? '').toLowerCase()] ?? String(value ?? 'Unknown').replaceAll('_', ' ')
|
||||
}
|
||||
|
||||
function IssuePipeline({ item, compact = false }: { item: PortalItem; compact?: boolean }) {
|
||||
const workflow = item.issue?.workflow
|
||||
const steps = workflow?.steps ?? []
|
||||
const currentStep = workflow?.current_step ?? 1
|
||||
const totalSteps = workflow?.total_steps ?? 6
|
||||
const state = workflow?.state ?? 'active'
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`issue-card-progress is-${state}`}>
|
||||
<span>
|
||||
<strong>{workflow?.stage_label ?? formatIssueStatus(item.status)}</strong>
|
||||
<small>Step {currentStep} of {totalSteps}</small>
|
||||
</span>
|
||||
<i aria-hidden="true">
|
||||
<b style={{ width: `${Math.max(0, Math.min(100, (currentStep / totalSteps) * 100))}%` }} />
|
||||
</i>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={`issue-pipeline-card is-${state}`} aria-label="Issue progress">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-kicker">Issue progress</span>
|
||||
<h3>{workflow?.headline ?? formatIssueStatus(item.status)}</h3>
|
||||
<p>{workflow?.message ?? 'The support team will update this issue as work progresses.'}</p>
|
||||
</div>
|
||||
<span className="small-pill">Step {currentStep} of {totalSteps}</span>
|
||||
</header>
|
||||
<ol>
|
||||
{steps.map((step, index) => (
|
||||
<li
|
||||
key={step.key}
|
||||
className={`is-${step.state}`}
|
||||
aria-current={step.state === 'active' || step.state === 'attention' ? 'step' : undefined}
|
||||
>
|
||||
<i aria-hidden="true">{step.state === 'complete' ? '✓' : index + 1}</i>
|
||||
<span>{step.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const toPositiveInt = (value: string) => {
|
||||
const parsed = Number.parseInt(value, 10)
|
||||
if (Number.isNaN(parsed) || parsed <= 0) return null
|
||||
@@ -1999,14 +2060,12 @@ export default function PortalClient({ workspace }: PortalClientProps) {
|
||||
<span className="small-pill is-muted">{item.priority}</span>
|
||||
</div>
|
||||
<p>{item.description}</p>
|
||||
{item.kind === 'issue' ? <IssuePipeline item={item} compact /> : null}
|
||||
<div className="portal-item-row-meta">
|
||||
<span>#{item.id}</span>
|
||||
<span>
|
||||
Status:{' '}
|
||||
{item.kind === 'request'
|
||||
? item.workflow?.stage_label ?? item.status
|
||||
: formatIssueStatus(item.status)}
|
||||
</span>
|
||||
{item.kind === 'request' ? (
|
||||
<span>Status: {item.workflow?.stage_label ?? item.status}</span>
|
||||
) : null}
|
||||
{isAdmin ? <span>By: {item.created_by_username}</span> : null}
|
||||
<span>Updated: {formatDate(item.last_activity_at)}</span>
|
||||
</div>
|
||||
@@ -2086,6 +2145,8 @@ export default function PortalClient({ workspace }: PortalClientProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedItem.kind === 'issue' ? <IssuePipeline item={selectedItem} /> : null}
|
||||
|
||||
{selectedItem.kind === 'issue' && selectedItem.external_ref?.startsWith('/requests/') ? (
|
||||
<div className="issue-linked-request">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user