mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix: address review findings (#4485)
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user