fix(ui): remove borders from label color picker and name input
Removes the border styling from the color picker and text input in the label creation form within the IssueProperties popover. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { Issue } from "@paperclip/shared";
|
import type { Issue } from "@paperclip/shared";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { agentsApi } from "../api/agents";
|
import { agentsApi } from "../api/agents";
|
||||||
|
import { issuesApi } from "../api/issues";
|
||||||
import { projectsApi } from "../api/projects";
|
import { projectsApi } from "../api/projects";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
@@ -13,7 +14,7 @@ import { formatDate, cn } from "../lib/utils";
|
|||||||
import { timeAgo } from "../lib/timeAgo";
|
import { timeAgo } from "../lib/timeAgo";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { User, Hexagon, ArrowUpRight } from "lucide-react";
|
import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2 } from "lucide-react";
|
||||||
import { AgentIcon } from "./AgentIconPicker";
|
import { AgentIcon } from "./AgentIconPicker";
|
||||||
|
|
||||||
interface IssuePropertiesProps {
|
interface IssuePropertiesProps {
|
||||||
@@ -32,23 +33,61 @@ function PropertyRow({ label, children }: { label: string; children: React.React
|
|||||||
|
|
||||||
export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
||||||
const { selectedCompanyId } = useCompany();
|
const { selectedCompanyId } = useCompany();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const companyId = issue.companyId ?? selectedCompanyId;
|
||||||
const [assigneeOpen, setAssigneeOpen] = useState(false);
|
const [assigneeOpen, setAssigneeOpen] = useState(false);
|
||||||
const [assigneeSearch, setAssigneeSearch] = useState("");
|
const [assigneeSearch, setAssigneeSearch] = useState("");
|
||||||
const [projectOpen, setProjectOpen] = useState(false);
|
const [projectOpen, setProjectOpen] = useState(false);
|
||||||
const [projectSearch, setProjectSearch] = useState("");
|
const [projectSearch, setProjectSearch] = useState("");
|
||||||
|
const [labelsOpen, setLabelsOpen] = useState(false);
|
||||||
|
const [labelSearch, setLabelSearch] = useState("");
|
||||||
|
const [newLabelName, setNewLabelName] = useState("");
|
||||||
|
const [newLabelColor, setNewLabelColor] = useState("#6366f1");
|
||||||
|
|
||||||
const { data: agents } = useQuery({
|
const { data: agents } = useQuery({
|
||||||
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
queryKey: queryKeys.agents.list(companyId!),
|
||||||
queryFn: () => agentsApi.list(selectedCompanyId!),
|
queryFn: () => agentsApi.list(companyId!),
|
||||||
enabled: !!selectedCompanyId,
|
enabled: !!companyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: projects } = useQuery({
|
const { data: projects } = useQuery({
|
||||||
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
queryKey: queryKeys.projects.list(companyId!),
|
||||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
queryFn: () => projectsApi.list(companyId!),
|
||||||
enabled: !!selectedCompanyId,
|
enabled: !!companyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: labels } = useQuery({
|
||||||
|
queryKey: queryKeys.issues.labels(companyId!),
|
||||||
|
queryFn: () => issuesApi.listLabels(companyId!),
|
||||||
|
enabled: !!companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createLabel = useMutation({
|
||||||
|
mutationFn: (data: { name: string; color: string }) => issuesApi.createLabel(companyId!, data),
|
||||||
|
onSuccess: async (created) => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: queryKeys.issues.labels(companyId!) });
|
||||||
|
onUpdate({ labelIds: [...(issue.labelIds ?? []), created.id] });
|
||||||
|
setNewLabelName("");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteLabel = useMutation({
|
||||||
|
mutationFn: (labelId: string) => issuesApi.deleteLabel(labelId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.labels(companyId!) });
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId!) });
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issue.id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleLabel = (labelId: string) => {
|
||||||
|
const ids = issue.labelIds ?? [];
|
||||||
|
const next = ids.includes(labelId)
|
||||||
|
? ids.filter((id) => id !== labelId)
|
||||||
|
: [...ids, labelId];
|
||||||
|
onUpdate({ labelIds: next });
|
||||||
|
};
|
||||||
|
|
||||||
const agentName = (id: string | null) => {
|
const agentName = (id: string | null) => {
|
||||||
if (!id || !agents) return null;
|
if (!id || !agents) return null;
|
||||||
const agent = agents.find((a) => a.id === id);
|
const agent = agents.find((a) => a.id === id);
|
||||||
@@ -84,6 +123,110 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
|||||||
/>
|
/>
|
||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
|
|
||||||
|
<PropertyRow label="Labels">
|
||||||
|
<Popover open={labelsOpen} onOpenChange={(open) => { setLabelsOpen(open); if (!open) setLabelSearch(""); }}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-accent/50 rounded px-1 -mx-1 py-0.5 transition-colors min-w-0 max-w-full">
|
||||||
|
{(issue.labels ?? []).length > 0 ? (
|
||||||
|
<div className="flex items-center gap-1 flex-wrap">
|
||||||
|
{(issue.labels ?? []).slice(0, 3).map((label) => (
|
||||||
|
<span
|
||||||
|
key={label.id}
|
||||||
|
className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium border"
|
||||||
|
style={{
|
||||||
|
borderColor: label.color,
|
||||||
|
backgroundColor: `${label.color}22`,
|
||||||
|
color: label.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{(issue.labels ?? []).length > 3 && (
|
||||||
|
<span className="text-xs text-muted-foreground">+{(issue.labels ?? []).length - 3}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Tag className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">No labels</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-64 p-1" align="end">
|
||||||
|
<input
|
||||||
|
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
|
||||||
|
placeholder="Search labels..."
|
||||||
|
value={labelSearch}
|
||||||
|
onChange={(e) => setLabelSearch(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className="max-h-44 overflow-y-auto overscroll-contain space-y-0.5">
|
||||||
|
{(labels ?? [])
|
||||||
|
.filter((label) => {
|
||||||
|
if (!labelSearch.trim()) return true;
|
||||||
|
return label.name.toLowerCase().includes(labelSearch.toLowerCase());
|
||||||
|
})
|
||||||
|
.map((label) => {
|
||||||
|
const selected = (issue.labelIds ?? []).includes(label.id);
|
||||||
|
return (
|
||||||
|
<div key={label.id} className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 flex-1 px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-left",
|
||||||
|
selected && "bg-accent"
|
||||||
|
)}
|
||||||
|
onClick={() => toggleLabel(label.id)}
|
||||||
|
>
|
||||||
|
<span className="h-2.5 w-2.5 rounded-full shrink-0" style={{ backgroundColor: label.color }} />
|
||||||
|
<span className="truncate">{label.name}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-1 text-muted-foreground hover:text-destructive rounded"
|
||||||
|
onClick={() => deleteLabel.mutate(label.id)}
|
||||||
|
title={`Delete ${label.name}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 border-t border-border pt-2 space-y-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
className="h-7 w-7 p-0 rounded bg-transparent"
|
||||||
|
type="color"
|
||||||
|
value={newLabelColor}
|
||||||
|
onChange={(e) => setNewLabelColor(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="flex-1 px-2 py-1.5 text-xs bg-transparent outline-none rounded placeholder:text-muted-foreground/50"
|
||||||
|
placeholder="New label"
|
||||||
|
value={newLabelName}
|
||||||
|
onChange={(e) => setNewLabelName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center gap-1.5 w-full px-2 py-1.5 text-xs rounded border border-border hover:bg-accent/50 disabled:opacity-50"
|
||||||
|
disabled={!newLabelName.trim() || createLabel.isPending}
|
||||||
|
onClick={() =>
|
||||||
|
createLabel.mutate({
|
||||||
|
name: newLabelName.trim(),
|
||||||
|
color: newLabelColor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
{createLabel.isPending ? "Creating..." : "Create label"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</PropertyRow>
|
||||||
|
|
||||||
<PropertyRow label="Assignee">
|
<PropertyRow label="Assignee">
|
||||||
<Popover open={assigneeOpen} onOpenChange={(open) => { setAssigneeOpen(open); if (!open) setAssigneeSearch(""); }}>
|
<Popover open={assigneeOpen} onOpenChange={(open) => { setAssigneeOpen(open); if (!open) setAssigneeSearch(""); }}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|||||||
Reference in New Issue
Block a user