Files
PaperClipAI/ui/src/components/PriorityIcon.tsx
T
DottaandPaperclip bec04da2c2 fix(ui): make task header status and priority controls interactive (#10526)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Operators use the task page header to read and change task state
> - The status and priority icons already had picker logic, but the
compact triggers were not semantic controls
> - This made pointer and keyboard interaction unreliable in the task
header
> - This pull request makes both compact icon triggers real buttons and
keeps the existing picker behavior
> - The benefit is that operators can change status and priority
directly from the header with pointer or keyboard input

## Linked Issues or Issue Description

### Subsystem affected

`ui/` — React + Vite board UI.

### Problem or motivation

The compact status and priority icons can receive change handlers, but
their popover triggers are plain icon elements. They do not provide a
reliable click target, keyboard focus, or control label. Operators need
to change both values directly from the task header.

### Proposed solution

Use semantic button triggers in the shared status and priority
components. Keep the existing API update wiring and picker options. Keep
task-row navigation links separate from editable row controls. An
operator can select either icon, open its picker, and choose a new task
status or priority.

### Alternatives considered

A task-page-only wrapper would duplicate control behavior. Moving the
controls would also change the page layout. The shared components
already own the picker behavior, so a shared trigger fix is smaller and
more consistent.

### Roadmap alignment

This is a focused board UI usability and accessibility fix. It does not
duplicate a planned roadmap feature.

### Additional context

The task page already passes change handlers to these shared components.
This change makes that existing path interactive and accessible.

## What Changed

- Added semantic button triggers for compact and labeled status
controls.
- Added semantic button triggers for compact and labeled priority
controls.
- Added accessible current-state labels and keyboard focus styles.
- Separated issue-row navigation links from row controls to avoid nested
interactive elements.
- Added real popover interaction coverage and row semantics regressions.
- Added task-page regression tests for update requests.

## Verification

- Focused task-header and row suites passed with 131 tests.
- The final row, inbox, and picker regression suites passed with 83
tests.
- `pnpm --filter @paperclipai/ui exec tsc -b --force` passed.
- `pnpm run typecheck:build-gaps` reproduced the CI typecheck before the
fix. The forced UI build passed after the fix.
- `pnpm check:token-gates` passed.
- `pnpm -r typecheck` passed.
- `pnpm build` passed.
- `pnpm test:run` completed the server and UI partitions. One unrelated
CLI AWS doctor assertion saw injected static AWS credentials and
returned `warn` instead of `pass`. The exact test passed after those two
environment variables were removed.
- `git diff --check origin/master...HEAD` passed.
- All latest-head GitHub checks passed, including both e2e shards.
- Greptile passed at the required threshold with zero open review
threads.

## Risks

- The shared issue-row DOM now uses a full-area navigation link beside
native action buttons.
- Existing visual layout, pointer navigation, keyboard navigation, and
action behavior remain covered by row, inbox, and list tests.
- Read-only status and priority icon uses are unchanged.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex with the `gpt-5` model. The runtime did not expose its
context window size. The agent used reasoning, repository tools, code
execution, and GitHub CLI integration.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-31 10:49:35 -07:00

91 lines
3.1 KiB
TypeScript

import { useState } from "react";
import { ArrowUp, ArrowDown, Minus, AlertTriangle } from "lucide-react";
import { cn } from "../lib/utils";
import { priorityColor, priorityColorDefault } from "../lib/status-colors";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
const priorityConfig: Record<string, { icon: typeof ArrowUp; color: string; label: string }> = {
critical: { icon: AlertTriangle, color: priorityColor.critical ?? priorityColorDefault, label: "Critical" },
high: { icon: ArrowUp, color: priorityColor.high ?? priorityColorDefault, label: "High" },
medium: { icon: Minus, color: priorityColor.medium ?? priorityColorDefault, label: "Medium" },
low: { icon: ArrowDown, color: priorityColor.low ?? priorityColorDefault, label: "Low" },
};
const allPriorities = ["critical", "high", "medium", "low"];
interface PriorityIconProps {
priority: string;
onChange?: (priority: string) => void;
className?: string;
showLabel?: boolean;
}
export function PriorityIcon({ priority, onChange, className, showLabel }: PriorityIconProps) {
const [open, setOpen] = useState(false);
const config = priorityConfig[priority] ?? priorityConfig.medium!;
const Icon = config.icon;
const icon = (
<span
className={cn(
"inline-flex items-center justify-center shrink-0",
config.color,
onChange && !showLabel && "cursor-pointer",
className
)}
>
<Icon className="h-3.5 w-3.5" />
</span>
);
if (!onChange) return showLabel ? <span className="inline-flex items-center gap-1.5">{icon}<span className="text-sm">{config.label}</span></span> : icon;
const trigger = showLabel ? (
<button
type="button"
aria-label={`Change priority (current: ${config.label})`}
className="inline-flex min-h-5 items-center gap-1.5 cursor-pointer hover:bg-accent/50 rounded px-1 -mx-1 py-0.5 transition-colors"
>
{icon}
<span className="text-sm">{config.label}</span>
</button>
) : (
<button
type="button"
data-slot="icon-button"
aria-label={`Change priority (current: ${config.label})`}
className="inline-flex cursor-pointer items-center justify-center rounded-sm focus-visible:outline-none focus-visible:ring-(length:--rad-3) focus-visible:ring-ring"
>
{icon}
</button>
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent className="w-36 p-1" align="start">
{allPriorities.map((p) => {
const c = priorityConfig[p]!;
const PIcon = c.icon;
return (
<Button
key={p}
variant="ghost"
size="sm"
className={cn("w-full justify-start gap-2 text-xs", p === priority && "bg-accent")}
onClick={() => {
onChange(p);
setOpen(false);
}}
>
<PIcon className={cn("h-3.5 w-3.5", c.color)} />
{c.label}
</Button>
);
})}
</PopoverContent>
</Popover>
);
}