mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
PAP-10451: address Greptile PR feedback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
75e34e5c90
commit
ebfeb8714d
6 files changed
+91
-84
No files matched your search
@@ -1,5 +1 @@
|
||||
ALTER TABLE "projects" ADD COLUMN "icon" text;--> statement-breakpoint
|
||||
-- One-time bulk reset of all existing projects to neutral gray (PAP-68 board
|
||||
-- decision #1). Intentionally discards current per-project colors so the new
|
||||
-- icon-led project identity starts from a clean, neutral baseline.
|
||||
UPDATE "projects" SET "color" = NULL;
|
||||
ALTER TABLE "projects" ADD COLUMN "icon" text;
|
||||
@@ -1156,6 +1156,57 @@ describe("company portability", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("normalizes invalid imported project icon names to null", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
companySvc.create.mockResolvedValue({
|
||||
id: "company-imported",
|
||||
name: "Imported Paperclip",
|
||||
});
|
||||
accessSvc.ensureMembership.mockResolvedValue(undefined);
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
projectSvc.list.mockResolvedValue([]);
|
||||
projectSvc.create.mockResolvedValue({
|
||||
id: "project-imported",
|
||||
name: "Launch",
|
||||
urlKey: "launch",
|
||||
});
|
||||
|
||||
const files = {
|
||||
"COMPANY.md": [
|
||||
"---",
|
||||
'schema: "agentcompanies/v1"',
|
||||
'name: "Imported Paperclip"',
|
||||
"---",
|
||||
"",
|
||||
].join("\n"),
|
||||
"projects/launch/PROJECT.md": [
|
||||
"---",
|
||||
'name: "Launch"',
|
||||
"---",
|
||||
"",
|
||||
].join("\n"),
|
||||
".paperclip.yaml": [
|
||||
'schema: "paperclip/v1"',
|
||||
"projects:",
|
||||
" launch:",
|
||||
' icon: "not-a-project-icon"',
|
||||
"",
|
||||
].join("\n"),
|
||||
};
|
||||
|
||||
await portability.importBundle({
|
||||
source: { type: "inline", rootPath: "paperclip-demo", files },
|
||||
include: { company: true, agents: false, projects: true, issues: false },
|
||||
target: { mode: "new_company", newCompanyName: "Imported Paperclip" },
|
||||
collisionStrategy: "rename",
|
||||
}, "user-1");
|
||||
|
||||
expect(projectSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({
|
||||
icon: null,
|
||||
}));
|
||||
});
|
||||
|
||||
it("infers portable git metadata from a local checkout without task warning fan-out", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
const repoDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-portability-git-"));
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
AGENT_DEFAULT_MAX_CONCURRENT_RUNS,
|
||||
ISSUE_PRIORITIES,
|
||||
ISSUE_STATUSES,
|
||||
PROJECT_ICON_NAMES,
|
||||
PROJECT_STATUSES,
|
||||
ROUTINE_CATCH_UP_POLICIES,
|
||||
ROUTINE_CONCURRENCY_POLICIES,
|
||||
@@ -471,6 +472,10 @@ function normalizePortableProjectEnv(value: unknown): AgentEnvConfig | null {
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
function normalizeProjectIconName(value: string | null | undefined): string | null {
|
||||
return value && PROJECT_ICON_NAMES.includes(value as typeof PROJECT_ICON_NAMES[number]) ? value : null;
|
||||
}
|
||||
|
||||
function extractPortableScopedEnvInputs(
|
||||
scope: {
|
||||
label: string;
|
||||
@@ -4711,7 +4716,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
||||
leadAgentId: projectLeadAgentId,
|
||||
targetDate: manifestProject.targetDate,
|
||||
color: manifestProject.color,
|
||||
icon: manifestProject.icon,
|
||||
icon: normalizeProjectIconName(manifestProject.icon),
|
||||
status: manifestProject.status && PROJECT_STATUSES.includes(manifestProject.status as any)
|
||||
? manifestProject.status as typeof PROJECT_STATUSES[number]
|
||||
: "backlog",
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { PROJECT_ICON_NAMES, type ProjectIconName } from "@paperclipai/shared";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PROJECT_ICONS, getProjectIcon } from "../lib/project-icons";
|
||||
|
||||
const DEFAULT_ICON: ProjectIconName = "folder";
|
||||
|
||||
interface ProjectIconProps {
|
||||
icon: string | null | undefined;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ProjectIcon({ icon, className }: ProjectIconProps) {
|
||||
const Icon = getProjectIcon(icon);
|
||||
return <Icon className={className} />;
|
||||
}
|
||||
|
||||
interface ProjectIconPickerProps {
|
||||
value: string | null | undefined;
|
||||
onChange: (icon: string) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ProjectIconPicker({ value, onChange, children }: ProjectIconPickerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const entries = PROJECT_ICON_NAMES.map((name) => [name, PROJECT_ICONS[name]] as const);
|
||||
if (!search) return entries;
|
||||
const q = search.toLowerCase();
|
||||
return entries.filter(([name]) => name.includes(q));
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
<PopoverContent className="w-72 p-3" align="start">
|
||||
<Input
|
||||
placeholder="Search icons..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="mb-2 h-8 text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="grid grid-cols-7 gap-1 max-h-48 overflow-y-auto">
|
||||
{filtered.map(([name, Icon]) => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => {
|
||||
onChange(name);
|
||||
setOpen(false);
|
||||
setSearch("");
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center h-8 w-8 rounded hover:bg-accent transition-colors",
|
||||
(value ?? DEFAULT_ICON) === name && "bg-accent ring-1 ring-primary"
|
||||
)}
|
||||
title={name}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<p className="col-span-7 text-xs text-muted-foreground text-center py-2">No icons match</p>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -272,6 +272,20 @@ describe("SidebarAgents", () => {
|
||||
await flushReact();
|
||||
}
|
||||
|
||||
async function renderSidebarAgentsWithDefaultProps() {
|
||||
const currentRoot = createRoot(container);
|
||||
root = currentRoot;
|
||||
|
||||
await act(async () => {
|
||||
currentRoot.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SidebarAgents />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
}
|
||||
|
||||
it("keeps top mode in stored org-aware order", async () => {
|
||||
localStorage.setItem("paperclip.agentOrder:company-1:user-1", JSON.stringify(["agent-b", "agent-a", "agent-c"]));
|
||||
mockAgentsApi.list.mockResolvedValue([
|
||||
@@ -555,6 +569,24 @@ describe("SidebarAgents", () => {
|
||||
expect(seeAllAgentsLink(container)).toBeNull();
|
||||
});
|
||||
|
||||
it("defaults to classic mode when rendered outside the Sidebar flag path", async () => {
|
||||
mockAgentsApi.list.mockResolvedValue(
|
||||
Array.from({ length: 7 }, (_, index) =>
|
||||
makeAgent({
|
||||
id: `agent-${index}`,
|
||||
name: `Agent ${index}`,
|
||||
urlKey: `agent-${index}`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([]);
|
||||
|
||||
await renderSidebarAgentsWithDefaultProps();
|
||||
|
||||
expect(agentLinkLabels(container)).toHaveLength(7);
|
||||
expect(seeAllAgentsLink(container)).toBeNull();
|
||||
});
|
||||
|
||||
it("does not offer sidebar resume for budget-paused agents", async () => {
|
||||
mockAgentsApi.list.mockResolvedValue([
|
||||
makeAgent({
|
||||
|
||||
@@ -216,7 +216,7 @@ function SidebarAgentItem({
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarAgents({ streamlined = true }: { streamlined?: boolean } = {}) {
|
||||
export function SidebarAgents({ streamlined = false }: { streamlined?: boolean } = {}) {
|
||||
const [open, setOpen] = useState(true);
|
||||
const [pendingAgentIds, setPendingAgentIds] = useState<Set<string>>(() => new Set());
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
Reference in new issue
Block a user