fix: address review findings (#4485)

This commit is contained in:
Jinjing
2026-06-02 11:24:01 -07:00
committed by GitHub
parent 705f24a174
commit 20bad3ef84
4 changed files with 234 additions and 203 deletions
@@ -1,10 +1,8 @@
/* eslint-disable max-lines -- Why: the Linear drawer co-locates read-only preview, edit controls, and comment input so the full issue surface stays in one file. */
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
AlertTriangle,
ArrowRight,
ChevronDown,
Circle,
ExternalLink,
Gauge,
LoaderCircle,
@@ -37,6 +35,7 @@ import {
getLinearStateMarkerStyle,
getLinearStatePillStyle
} from '@/components/linear-state-pill-style'
import { LinearPriorityIcon } from '@/components/linear-priority-icon'
import type { LinearIssue, LinearComment } from '../../../shared/types'
import {
linearAddIssueComment,
@@ -440,11 +439,7 @@ export function LinearIssueEditSection({
className={propertyRowClass}
aria-busy={priorityPending}
>
{localPriority === 1 ? (
<AlertTriangle className="size-4 shrink-0 text-destructive" />
) : (
<Circle className={propertyIconClass} />
)}
<LinearPriorityIcon priority={localPriority} />
<span className="min-w-0 flex-1 truncate">
{PRIORITY_LABELS[localPriority] ?? `P${localPriority}`}
</span>
@@ -458,10 +453,11 @@ export function LinearIssueEditSection({
type="button"
onClick={() => handlePriorityChange(String(p))}
className={cn(
LINEAR_EDIT_MENU_ITEM_CLASS,
LINEAR_EDIT_MENU_ITEM_WITH_ICON_CLASS,
localPriority === p && 'bg-accent/50'
)}
>
<LinearPriorityIcon priority={p} />
{PRIORITY_LABELS[p]}
</button>
))}
@@ -743,6 +739,7 @@ export function LinearIssueEditSection({
className={LINEAR_EDIT_CHIP_CLASS}
aria-busy={priorityPending}
>
<LinearPriorityIcon priority={localPriority} />
<span className="truncate">
{PRIORITY_LABELS[localPriority] ?? `P${localPriority}`}
</span>
@@ -755,8 +752,12 @@ export function LinearIssueEditSection({
key={p}
type="button"
onClick={() => handlePriorityChange(String(p))}
className={cn(LINEAR_EDIT_MENU_ITEM_CLASS, localPriority === p && 'bg-accent/50')}
className={cn(
LINEAR_EDIT_MENU_ITEM_WITH_ICON_CLASS,
localPriority === p && 'bg-accent/50'
)}
>
<LinearPriorityIcon priority={p} />
{PRIORITY_LABELS[p]}
</button>
))}
+129 -193
View File
@@ -36,8 +36,7 @@ import {
X,
FolderKanban,
Tag,
UserRound,
AlertTriangle
UserRound
} from 'lucide-react'
import { toast } from 'sonner'
@@ -88,6 +87,7 @@ import { LinearScopeSelector } from '@/components/linear-scope-selector'
import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel'
import IssueSourceIndicator, { sameGitHubOwnerRepo } from '@/components/github/IssueSourceIndicator'
import IssueSourceSelector, { issueSourceChipClass } from '@/components/github/IssueSourceSelector'
import { LinearPriorityIcon } from '@/components/linear-priority-icon'
import { reconcileLinearTeamSelection } from '@/components/task-page-linear-team-selection'
import { useConfirmationDialog } from '@/components/confirmation-dialog'
import {
@@ -306,16 +306,6 @@ const SOURCE_OPTIONS: SourceOption[] = [
}
]
type LinearPresetId = 'assigned' | 'created' | 'all' | 'completed'
type LinearPreset = { id: LinearPresetId; label: string }
const LINEAR_PRESETS: LinearPreset[] = [
{ id: 'all', label: 'All' },
{ id: 'assigned', label: 'My Issues' },
{ id: 'created', label: 'Created' },
{ id: 'completed', label: 'Completed' }
]
type JiraPresetId = 'assigned' | 'reported' | 'all' | 'done'
type JiraPreset = { id: JiraPresetId; label: string }
@@ -751,23 +741,23 @@ function groupLinearIssues(
}
function getLinearIssueGridTemplate(visibleProperties: ReadonlySet<LinearDisplayProperty>): string {
const columns = ['96px', 'minmax(180px,1.4fr)']
if (visibleProperties.has('state')) {
columns.push('140px')
}
if (visibleProperties.has('priority')) {
columns.push('92px')
}
if (visibleProperties.has('assignee')) {
columns.push('150px')
const columns = ['96px', 'minmax(240px,1.55fr)']
if (visibleProperties.has('labels')) {
columns.push('minmax(168px,0.9fr)')
}
if (visibleProperties.has('team')) {
columns.push('160px')
columns.push('minmax(172px,0.9fr)')
}
if (visibleProperties.has('state')) {
columns.push('138px')
}
if (visibleProperties.has('assignee')) {
columns.push('64px')
}
if (visibleProperties.has('updated')) {
columns.push('100px')
columns.push('104px')
}
columns.push('72px')
columns.push('64px')
return columns.join(' ')
}
@@ -3137,7 +3127,6 @@ export default function TaskPage(): React.JSX.Element {
const [linearError, setLinearError] = useState<string | null>(null)
const [linearSearchInput, setLinearSearchInput] = useState('')
const [appliedLinearSearch, setAppliedLinearSearch] = useState('')
const [activeLinearPreset, setActiveLinearPreset] = useState<LinearPresetId>('all')
const [linearViewMode, setLinearViewMode] = useState<LinearViewMode>('list')
const [linearGroupBy, setLinearGroupBy] = useState<LinearGroupBy>('none')
const [linearOrderBy, setLinearOrderBy] = useState<LinearOrderBy>('priority')
@@ -3315,10 +3304,8 @@ export default function TaskPage(): React.JSX.Element {
setActiveTaskPreset(presetId)
}
const linearPreset = taskResumeState?.linearPreset ?? 'all'
const linearQuery = taskResumeState?.linearQuery ?? ''
setLinearMode(taskResumeState?.linearMode ?? 'issues')
setActiveLinearPreset(linearPreset)
setLinearSearchInput(linearQuery)
setAppliedLinearSearch(linearQuery)
@@ -5373,7 +5360,6 @@ export default function TaskPage(): React.JSX.Element {
useEffect(() => {
setLinearIssueLimit(LINEAR_ITEM_LIMIT)
}, [
activeLinearPreset,
appliedLinearSearch,
linearMode,
selectedLinearCustomView?.id,
@@ -5407,7 +5393,7 @@ export default function TaskPage(): React.JSX.Element {
const readArgs =
trimmed.length > 0
? ({ kind: 'search', query: trimmed, limit: LINEAR_ITEM_LIMIT } as const)
: ({ kind: 'list', filter: activeLinearPreset, limit: effectiveLinearIssueLimit } as const)
: ({ kind: 'list', filter: 'all', limit: effectiveLinearIssueLimit } as const)
const cachedResult = getCachedLinearIssues(readArgs)
if (readArgs.kind === 'search') {
setLinearIssuesHasMore(false)
@@ -5425,7 +5411,7 @@ export default function TaskPage(): React.JSX.Element {
const requestSignature =
trimmed.length > 0
? `${selectedLinearWorkspaceId ?? 'default'}::search::${trimmed}::${LINEAR_ITEM_LIMIT}`
: `${selectedLinearWorkspaceId ?? 'default'}::list::${activeLinearPreset}::${effectiveLinearIssueLimit}`
: `${selectedLinearWorkspaceId ?? 'default'}::list::all::${effectiveLinearIssueLimit}`
const previousRequest = lastLinearRequestRef.current
const forceRefresh =
linearRefreshNonce > 0 &&
@@ -5512,7 +5498,6 @@ export default function TaskPage(): React.JSX.Element {
linearStatus.connected,
selectedLinearWorkspaceId,
appliedLinearSearch,
activeLinearPreset,
linearIssueLimit,
linearRefreshNonce,
taskResumeApplied,
@@ -6586,85 +6571,53 @@ export default function TaskPage(): React.JSX.Element {
</div>
{linearMode === 'issues' ? (
<>
<div className="mt-3 flex flex-wrap gap-2">
{LINEAR_PRESETS.map((preset) => {
const active = !linearSearchInput && activeLinearPreset === preset.id
return (
<button
key={preset.id}
type="button"
onClick={() => {
setLinearSearchInput('')
setAppliedLinearSearch('')
setActiveLinearPreset(preset.id)
setTaskResumeState({
linearPreset: preset.id,
linearQuery: '',
linearMode: 'issues'
})
setLinearRefreshNonce((n) => n + 1)
}}
className={cn(
'rounded-md border px-2 py-1 text-xs transition',
active
? 'border-border/50 bg-foreground/90 text-background backdrop-blur-md'
: 'border-border/50 bg-transparent text-foreground hover:bg-muted/50'
)}
>
{preset.label}
</button>
)
})}
</div>
<div className="mt-3 flex min-w-0 items-center gap-3">
<div className="relative min-w-0 flex-1 basis-64">
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
value={linearSearchInput}
onChange={(e) => setLinearSearchInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (
shouldSuppressEnterSubmit(
{
isComposing: e.nativeEvent.isComposing,
shiftKey: e.shiftKey
},
false
)
) {
return
}
e.preventDefault()
const trimmed = linearSearchInput.trim()
setLinearSearchInput(trimmed)
setAppliedLinearSearch(trimmed)
setTaskResumeState({ linearQuery: trimmed, linearMode: 'issues' })
setLinearRefreshNonce((n) => n + 1)
<div className="mt-3 flex min-w-0 items-center gap-3">
<div className="relative min-w-0 flex-1 basis-64">
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
value={linearSearchInput}
onChange={(e) => setLinearSearchInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (
shouldSuppressEnterSubmit(
{
isComposing: e.nativeEvent.isComposing,
shiftKey: e.shiftKey
},
false
)
) {
return
}
e.preventDefault()
const trimmed = linearSearchInput.trim()
setLinearSearchInput(trimmed)
setAppliedLinearSearch(trimmed)
setTaskResumeState({ linearQuery: trimmed, linearMode: 'issues' })
setLinearRefreshNonce((n) => n + 1)
}
}}
placeholder="Search Linear issues..."
className="h-8 rounded-md border-border/50 bg-background pl-8 pr-8 text-xs"
/>
{linearSearchInput ? (
<button
type="button"
aria-label="Clear search"
onClick={() => {
setLinearSearchInput('')
setAppliedLinearSearch('')
setTaskResumeState({ linearQuery: '', linearMode: 'issues' })
setLinearRefreshNonce((n) => n + 1)
}}
placeholder="Search Linear issues..."
className="h-8 rounded-md border-border/50 bg-background pl-8 pr-8 text-xs"
/>
{linearSearchInput ? (
<button
type="button"
aria-label="Clear search"
onClick={() => {
setLinearSearchInput('')
setAppliedLinearSearch('')
setTaskResumeState({ linearQuery: '', linearMode: 'issues' })
setLinearRefreshNonce((n) => n + 1)
}}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition hover:text-foreground"
>
<X className="size-4" />
</button>
) : null}
</div>
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition hover:text-foreground"
>
<X className="size-4" />
</button>
) : null}
</div>
</>
</div>
) : linearMode === 'projects' && !selectedLinearProject ? (
<div className="mt-3 flex min-w-0 items-center gap-3">
<div className="relative min-w-0 flex-1 basis-64">
@@ -8286,10 +8239,12 @@ export default function TaskPage(): React.JSX.Element {
>
<span>Key</span>
<span>Issue</span>
{effectiveLinearDisplayProperties.has('state') ? <span>Status</span> : null}
{effectiveLinearDisplayProperties.has('priority') ? <span>Priority</span> : null}
{effectiveLinearDisplayProperties.has('assignee') ? <span>Assignee</span> : null}
{effectiveLinearDisplayProperties.has('labels') ? <span>Labels</span> : null}
{effectiveLinearDisplayProperties.has('team') ? <span>Team</span> : null}
{effectiveLinearDisplayProperties.has('state') ? <span>Status</span> : null}
{effectiveLinearDisplayProperties.has('assignee') ? (
<span className="text-center">Assignee</span>
) : null}
{effectiveLinearDisplayProperties.has('updated') ? <span>Updated</span> : null}
<span />
</div>
@@ -8429,8 +8384,14 @@ export default function TaskPage(): React.JSX.Element {
>
<div className="flex min-w-0 items-start justify-between gap-2">
<div className="min-w-0">
<div className="font-mono text-[11px] text-muted-foreground">
{issue.identifier}
<div className="flex min-w-0 items-center gap-1.5 font-mono text-[11px] text-muted-foreground">
{effectiveLinearDisplayProperties.has('priority') ? (
<LinearPriorityIcon
priority={issue.priority}
className="size-3.5"
/>
) : null}
<span className="truncate">{issue.identifier}</span>
</div>
<h3 className="mt-1 line-clamp-2 text-[13px] font-medium leading-snug text-foreground">
{issue.title}
@@ -8466,9 +8427,6 @@ export default function TaskPage(): React.JSX.Element {
{effectiveLinearDisplayProperties.has('state') ? (
<LinearStateCell issue={issue} className="px-1.5 py-0.5" />
) : null}
{effectiveLinearDisplayProperties.has('priority') ? (
<span>{getLinearPriorityLabel(issue.priority)}</span>
) : null}
{effectiveLinearDisplayProperties.has('assignee') ? (
<span>{issue.assignee?.displayName ?? 'Unassigned'}</span>
) : null}
@@ -8556,12 +8514,17 @@ export default function TaskPage(): React.JSX.Element {
)}
style={linearIssueGridStyle}
>
<span className="block truncate font-mono text-[12px] text-muted-foreground max-lg:!hidden">
{issue.identifier}
</span>
<div className="flex min-w-0 items-center gap-2 max-lg:!hidden">
<span className="min-w-0 truncate font-mono text-[12px] text-muted-foreground">
{issue.identifier}
</span>
</div>
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
{effectiveLinearDisplayProperties.has('priority') ? (
<LinearPriorityIcon priority={issue.priority} />
) : null}
<span className="shrink-0 font-mono text-[11px] text-muted-foreground lg:hidden">
{issue.identifier}
</span>
@@ -8573,11 +8536,6 @@ export default function TaskPage(): React.JSX.Element {
{effectiveLinearDisplayProperties.has('state') ? (
<LinearStateCell issue={issue} className="px-1.5 py-0.5" />
) : null}
{effectiveLinearDisplayProperties.has('priority') ? (
<span className="shrink-0 text-[11px] text-muted-foreground">
{getLinearPriorityLabel(issue.priority)}
</span>
) : null}
{effectiveLinearDisplayProperties.has('assignee') ? (
<span className="min-w-0 truncate text-[11px] text-muted-foreground">
{issue.assignee?.displayName ?? 'Unassigned'}
@@ -8589,59 +8547,61 @@ export default function TaskPage(): React.JSX.Element {
</span>
) : null}
</div>
{effectiveLinearDisplayProperties.has('labels') ? (
<div className="mt-1 flex min-w-0 items-center gap-1 max-lg:!hidden">
{labels.map((label) => (
<span
key={label}
className="max-w-[140px] truncate rounded-full border border-border/50 bg-muted/35 px-1.5 py-0.5 text-[10px] text-muted-foreground"
>
{label}
</span>
))}
{issue.labels.length > labels.length ? (
<span className="text-[10px] text-muted-foreground">
+{issue.labels.length - labels.length}
</span>
) : null}
</div>
) : null}
</div>
{effectiveLinearDisplayProperties.has('labels') ? (
<div className="flex min-w-0 items-center gap-1 max-lg:!hidden">
{labels.map((label) => (
<span
key={label}
className="max-w-[150px] truncate rounded-full border border-border/50 bg-muted/35 px-1.5 py-0.5 text-[11px] text-muted-foreground"
>
{label}
</span>
))}
{issue.labels.length > labels.length ? (
<span className="text-[11px] text-muted-foreground">
+{issue.labels.length - labels.length}
</span>
) : null}
</div>
) : null}
{effectiveLinearDisplayProperties.has('team') ? (
<div className="block min-w-0 text-[12px] text-muted-foreground max-lg:!hidden">
<div className="truncate">{teamLabel}</div>
</div>
) : null}
{effectiveLinearDisplayProperties.has('state') ? (
<div className="flex min-w-0 max-lg:!hidden">
<LinearStateCell issue={issue} className="max-w-full px-2 py-0.5" />
</div>
) : null}
{effectiveLinearDisplayProperties.has('priority') ? (
<span className="block truncate text-[12px] text-muted-foreground max-lg:!hidden">
{getLinearPriorityLabel(issue.priority)}
</span>
) : null}
{effectiveLinearDisplayProperties.has('assignee') ? (
<div className="flex min-w-0 items-center gap-2 text-[12px] text-muted-foreground max-lg:!hidden">
{issue.assignee?.avatarUrl ? (
<img
src={issue.assignee.avatarUrl}
alt={issue.assignee.displayName}
className="size-5 shrink-0 rounded-full"
/>
) : (
<span className="flex size-5 shrink-0 items-center justify-center rounded-full border border-border/50 bg-muted/40 text-[10px]">
{issue.assignee?.displayName?.slice(0, 1) ?? '-'}
</span>
)}
<span className="truncate">
{issue.assignee?.displayName ?? 'Unassigned'}
</span>
</div>
) : null}
{effectiveLinearDisplayProperties.has('team') ? (
<div className="block min-w-0 text-[12px] text-muted-foreground max-lg:!hidden">
<div className="truncate">{teamLabel}</div>
<div className="flex min-w-0 justify-center max-lg:!hidden">
<Tooltip>
<TooltipTrigger asChild>
<div
className="flex size-5 shrink-0 items-center justify-center rounded-full border border-border/50 bg-muted/40 text-[10px] text-muted-foreground"
aria-label={issue.assignee?.displayName ?? 'Unassigned'}
>
{issue.assignee?.avatarUrl ? (
<img
src={issue.assignee.avatarUrl}
alt={issue.assignee.displayName}
className="size-5 rounded-full"
/>
) : (
(issue.assignee?.displayName?.slice(0, 1) ?? '-')
)}
</div>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{issue.assignee?.displayName ?? 'Unassigned'}
</TooltipContent>
</Tooltip>
</div>
) : null}
@@ -9191,19 +9151,7 @@ export default function TaskPage(): React.JSX.Element {
disabled={newLinearIssueSubmitting}
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs border border-border/80 bg-muted/15 hover:bg-muted/50 active:bg-muted transition-colors text-foreground/80 cursor-pointer disabled:opacity-50"
>
<AlertTriangle
className={`size-3.5 ${
newLinearIssuePriority === 1
? 'text-rose-500'
: newLinearIssuePriority === 2
? 'text-amber-500'
: newLinearIssuePriority === 3
? 'text-yellow-500 font-medium'
: newLinearIssuePriority === 4
? 'text-blue-400'
: 'text-muted-foreground/70'
}`}
/>
<LinearPriorityIcon priority={newLinearIssuePriority} className="size-3.5" />
<span>
{newLinearIssuePriority === 1
? 'Urgent'
@@ -9240,19 +9188,7 @@ export default function TaskPage(): React.JSX.Element {
}`}
>
<div className="flex items-center gap-2">
<AlertTriangle
className={`size-3.5 ${
p.val === 1
? 'text-rose-500'
: p.val === 2
? 'text-amber-500'
: p.val === 3
? 'text-yellow-500'
: p.val === 4
? 'text-blue-400'
: 'text-muted-foreground/50'
}`}
/>
<LinearPriorityIcon priority={p.val} className="size-3.5" />
<span>{p.label}</span>
</div>
{newLinearIssuePriority === p.val && (
@@ -0,0 +1,92 @@
import React from 'react'
import { cn } from '@/lib/utils'
// Why: mirror Linear's priority glyph shape while keeping color on Orca tokens.
const LINEAR_PRIORITY_ICON_LABELS: Record<number, string> = {
0: 'No priority',
1: 'Urgent',
2: 'High',
3: 'Medium',
4: 'Low'
}
function getLinearPriorityBarCount(priority: number): number {
if (priority === 2) {
return 3
}
if (priority === 3) {
return 2
}
if (priority === 4) {
return 1
}
return 0
}
export function getLinearPriorityIconLabel(priority: number): string {
return LINEAR_PRIORITY_ICON_LABELS[priority] ?? `P${priority}`
}
export function LinearPriorityIcon({
priority,
className,
label = getLinearPriorityIconLabel(priority)
}: {
priority: number
className?: string
label?: string
}): React.JSX.Element {
if (priority === 1) {
return (
<span
className={cn(
'inline-flex size-4 shrink-0 items-center justify-center rounded-sm bg-destructive text-[10px] font-semibold leading-none text-destructive-foreground',
className
)}
title={label}
>
<span aria-hidden="true">!</span>
<span className="sr-only">Priority: {label}</span>
</span>
)
}
if (priority === 0) {
return (
<span
className={cn('inline-flex size-4 shrink-0 items-center justify-center', className)}
title={label}
>
<span
aria-hidden="true"
className="size-3 rounded-full border border-muted-foreground/55"
/>
<span className="sr-only">Priority: {label}</span>
</span>
)
}
const activeBars = getLinearPriorityBarCount(priority)
return (
<span
className={cn('inline-flex size-4 shrink-0 items-end justify-center gap-px', className)}
title={label}
>
{[1, 2, 3].map((bar) => (
<span
key={bar}
aria-hidden="true"
className={cn(
'w-1 rounded-[1px]',
bar === 1 && 'h-1.5',
bar === 2 && 'h-2.5',
bar === 3 && 'h-3.5',
bar <= activeBars ? 'bg-foreground' : 'bg-muted-foreground/20'
)}
/>
))}
<span className="sr-only">Priority: {label}</span>
</span>
)
}
+3 -1
View File
@@ -1076,9 +1076,11 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
if (query) {
state.prefetchLinearIssues({ kind: 'search', query, limit: LINEAR_TASK_PREFETCH_LIMIT })
} else {
// Why: TaskPage no longer exposes Linear preset filters; keep warm
// prefetch aligned with the default unsearched issue list.
state.prefetchLinearIssues({
kind: 'list',
filter: resume?.linearPreset ?? 'all',
filter: 'all',
limit: LINEAR_TASK_PREFETCH_LIMIT
})
}