mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 00:30:19 +02:00
## Thinking Path > - Paperclip is the open source app that people use to manage AI agents for work. > - The task properties panel helps operators inspect and update task data. > - The status, assignee, and project rows used different leading icon sizes. > - The mixed sizes made the rows look uneven. > - The assignee avatar already provides the correct 24px visual size. > - This pull request sets the status and project visuals to that same size. > - The benefit is a tidy and consistent properties panel. ## Linked Issues or Issue Description **What happened?** The task properties panel showed a 12px status icon, a 24px assignee avatar, and a 16px project tile. **Expected behavior** The status, assignee, and project rows must use the same 24px leading visual size. **Steps to reproduce** 1. Open a task. 2. Open the Properties panel. 3. Compare the Status, Assignee, and Project rows. ## What Changed - Keep the Status glyph at 16px and use spacing to give it the 24px avatar footprint. - Set the Project row tile to the 24px small tile size. - Add a regression check for the Status row size. ## Verification - `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx` (72 tests passed) - `pnpm check:token-gates` (all gates clean) - `pnpm --filter @paperclipai/ui typecheck` (passed) - Full repository typecheck and build reached the Rust runner step, but this environment does not contain `cargo`. - The full test command was stopped after unrelated chat integration tests did not finish. The focused UI suite passed. ## Risks - Low risk. This change only changes visual sizes in three property rows. - The wider status footprint can use slightly more horizontal space in a narrow panel. > This fix does not overlap with planned core work in `ROADMAP.md`. ## Model Used - OpenAI Codex, GPT-5.6, tool use and code execution enabled. ## 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) - [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`) 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 - [ ] All Paperclip CI gates are green - [ ] 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>
141 lines
5.5 KiB
TypeScript
141 lines
5.5 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import { StatusIcon } from "./StatusIcon";
|
|
import { IssueStatusBadge } from "./StatusBadge";
|
|
|
|
/**
|
|
* StatusIcon renders the unified {@link StatusGlyph} (one shape per status) at
|
|
* every standalone status surface. These tests lock the glyph rendering, the
|
|
* covered-blocked → "in queue" mapping, the accessible blocked labels, and the
|
|
* size prop.
|
|
*/
|
|
describe("StatusIcon", () => {
|
|
it("renders the unified glyph (24-unit viewBox), not a bespoke ring", () => {
|
|
const html = renderToStaticMarkup(<StatusIcon status="in_progress" />);
|
|
expect(html).toContain('viewBox="0 0 24 24"');
|
|
expect(html).not.toContain("rounded-full border-2");
|
|
});
|
|
|
|
it("drives the glyph colour from the status icon var", () => {
|
|
const html = renderToStaticMarkup(<StatusIcon status="todo" />);
|
|
expect(html).toContain("var(--status-task-icon-todo)");
|
|
});
|
|
|
|
it("maps covered-blocked → In queue (blue in_queue var, no cyan markers)", () => {
|
|
const html = renderToStaticMarkup(
|
|
<StatusIcon
|
|
status="blocked"
|
|
blockerAttention={{
|
|
state: "covered",
|
|
reason: "active_child",
|
|
unresolvedBlockerCount: 1,
|
|
coveredBlockerCount: 1,
|
|
attentionBlockerCount: 0,
|
|
stalledBlockerCount: 0,
|
|
sampleBlockerIdentifier: "PAP-9",
|
|
sampleStalledBlockerIdentifier: null,
|
|
}}
|
|
/>,
|
|
);
|
|
expect(html).toContain("var(--status-task-icon-in_queue)");
|
|
expect(html).not.toContain("bg-cyan");
|
|
expect(html).not.toContain("border-cyan");
|
|
// Full blocked reason still rides on the accessible label.
|
|
expect(html).toContain("Blocked · waiting on active sub-task PAP-9");
|
|
});
|
|
|
|
it("surfaces attention-required blocked copy and keeps the blocked glyph", () => {
|
|
const html = renderToStaticMarkup(
|
|
<StatusIcon
|
|
status="blocked"
|
|
blockerAttention={{
|
|
state: "needs_attention",
|
|
reason: "attention_required",
|
|
unresolvedBlockerCount: 3,
|
|
coveredBlockerCount: 2,
|
|
stalledBlockerCount: 0,
|
|
attentionBlockerCount: 3,
|
|
sampleBlockerIdentifier: "PAP-3541",
|
|
sampleStalledBlockerIdentifier: null,
|
|
}}
|
|
/>,
|
|
);
|
|
expect(html).toContain("Blocked · 3 blockers need attention; 2 covered by active work");
|
|
// needs_attention is not "covered", so it keeps the blocked glyph (not in_queue).
|
|
expect(html).toContain("var(--status-task-icon-blocked)");
|
|
expect(html).not.toContain("var(--status-task-icon-in_queue)");
|
|
});
|
|
|
|
it("surfaces stalled-review blocked copy on the accessible label", () => {
|
|
const html = renderToStaticMarkup(
|
|
<StatusIcon
|
|
status="blocked"
|
|
blockerAttention={{
|
|
state: "stalled",
|
|
reason: "stalled_review",
|
|
unresolvedBlockerCount: 1,
|
|
coveredBlockerCount: 0,
|
|
stalledBlockerCount: 1,
|
|
attentionBlockerCount: 0,
|
|
sampleBlockerIdentifier: "PAP-2279",
|
|
sampleStalledBlockerIdentifier: "PAP-2279",
|
|
}}
|
|
/>,
|
|
);
|
|
expect(html).toContain("Blocked · review stalled on PAP-2279");
|
|
});
|
|
|
|
it("uses an accessible native button for the icon-only picker trigger", () => {
|
|
const html = renderToStaticMarkup(<StatusIcon status="todo" onChange={() => {}} />);
|
|
expect(html).toContain('viewBox="0 0 24 24"');
|
|
expect(html).toContain('<button type="button"');
|
|
expect(html).toContain('aria-label="Change status (current: Todo)"');
|
|
});
|
|
|
|
});
|
|
|
|
describe("StatusIcon — glyph size (PAP-243a)", () => {
|
|
it('forwards size="lg" as a 20px glyph', () => {
|
|
const html = renderToStaticMarkup(<StatusIcon status="todo" size="lg" />);
|
|
expect(html).toContain('width="20"');
|
|
expect(html).toContain('height="20"');
|
|
});
|
|
|
|
it("defaults to a 16px (md) glyph when size is omitted", () => {
|
|
const html = renderToStaticMarkup(<StatusIcon status="todo" />);
|
|
expect(html).toContain('width="16"');
|
|
expect(html).toContain('height="16"');
|
|
});
|
|
|
|
it("can place the default glyph in a larger layout footprint without resizing it", () => {
|
|
const html = renderToStaticMarkup(
|
|
<StatusIcon status="todo" glyphContainerClassName="size-6" showLabel />,
|
|
);
|
|
expect(html).toContain('data-status-glyph-container="true"');
|
|
expect(html).toContain('class="size-6"');
|
|
expect(html).toContain('width="16"');
|
|
expect(html).toContain('height="16"');
|
|
});
|
|
});
|
|
|
|
|
|
describe("Slack idle status presentation", () => {
|
|
it("labels verified waiting conversations Idle in icons, labels, badges and picker triggers", () => {
|
|
const props = { status: "in_review", externalConversationState: "waiting" as const };
|
|
expect(renderToStaticMarkup(<StatusIcon {...props} showLabel />)).toContain(">Idle<");
|
|
expect(renderToStaticMarkup(<StatusIcon {...props} onChange={() => {}} />)).toContain('current: Idle');
|
|
expect(renderToStaticMarkup(<IssueStatusBadge {...props} />)).toContain(">Idle<");
|
|
});
|
|
|
|
it.each(["in_review", "blocked", "done", "cancelled"])("preserves %s without verified waiting", (status) => {
|
|
const html = renderToStaticMarkup(<StatusIcon status={status} externalConversationState="active" showLabel />);
|
|
expect(html).not.toContain("Idle");
|
|
});
|
|
|
|
it("preserves an explicit terminal outcome even with stale waiting data", () => {
|
|
expect(renderToStaticMarkup(<IssueStatusBadge status="done" externalConversationState="waiting" />)).toContain(">Done<");
|
|
});
|
|
});
|