mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
status colours
This commit is contained in:
@@ -28,7 +28,7 @@ export function CreateIssue({
|
||||
}: {
|
||||
projectId?: number;
|
||||
members?: UserRecord[];
|
||||
statuses?: string[];
|
||||
statuses: Record<string, string>;
|
||||
trigger?: React.ReactNode;
|
||||
completeAction?: (issueId: number) => void | Promise<void>;
|
||||
}) {
|
||||
@@ -38,7 +38,7 @@ export function CreateIssue({
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [assigneeId, setAssigneeId] = useState<string>("unassigned");
|
||||
const [status, setStatus] = useState<string>(statuses?.[0] ?? "");
|
||||
const [status, setStatus] = useState<string>(Object.keys(statuses)[0] ?? "");
|
||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -130,7 +130,7 @@ export function CreateIssue({
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid">
|
||||
{statuses && statuses.length > 0 && (
|
||||
{statuses && Object.keys(statuses).length > 0 && (
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<Label>Status</Label>
|
||||
<StatusSelect
|
||||
@@ -150,7 +150,8 @@ export function CreateIssue({
|
||||
>
|
||||
<StatusTag
|
||||
status={value}
|
||||
className="group-hover:bg-foreground/75"
|
||||
colour={statuses[value]}
|
||||
className="hover:opacity-85"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function IssueDetailPane({
|
||||
project: ProjectResponse;
|
||||
issueData: IssueResponse;
|
||||
members: UserRecord[];
|
||||
statuses: string[];
|
||||
statuses: Record<string, string>;
|
||||
close: () => void;
|
||||
onIssueUpdate?: () => void;
|
||||
}) {
|
||||
@@ -98,7 +98,11 @@ export function IssueDetailPane({
|
||||
chevronClassName="hidden"
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<StatusTag status={value} className="group-hover:bg-foreground/75" />
|
||||
<StatusTag
|
||||
status={value}
|
||||
colour={statuses[value]}
|
||||
className="hover:opacity-85"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -8,11 +8,13 @@ export function IssuesTable({
|
||||
issuesData,
|
||||
columns = {},
|
||||
issueSelectAction,
|
||||
statuses,
|
||||
className,
|
||||
}: {
|
||||
issuesData: IssueResponse[];
|
||||
columns?: { id?: boolean; title?: boolean; description?: boolean; status?: boolean; assignee?: boolean };
|
||||
issueSelectAction?: (issue: IssueResponse) => void;
|
||||
statuses: Record<string, string>;
|
||||
className: string;
|
||||
}) {
|
||||
return (
|
||||
@@ -48,7 +50,10 @@ export function IssuesTable({
|
||||
<TableCell>
|
||||
<span className="flex items-center gap-2 max-w-full truncate">
|
||||
{(columns.status == null || columns.status === true) && (
|
||||
<StatusTag status={issueData.Issue.status} />
|
||||
<StatusTag
|
||||
status={issueData.Issue.status}
|
||||
colour={statuses[issueData.Issue.status]}
|
||||
/>
|
||||
)}
|
||||
{issueData.Issue.title}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
DEFAULT_STATUS_COLOUR,
|
||||
ISSUE_STATUS_MAX_LENGTH,
|
||||
type OrganisationMemberResponse,
|
||||
type OrganisationResponse,
|
||||
@@ -38,7 +39,7 @@ function OrganisationsDialog({
|
||||
const [activeTab, setActiveTab] = useState("info");
|
||||
const [members, setMembers] = useState<OrganisationMemberResponse[]>([]);
|
||||
|
||||
const [statuses, setStatuses] = useState<string[]>([]);
|
||||
const [statuses, setStatuses] = useState<Record<string, string>>({});
|
||||
const [isCreatingStatus, setIsCreatingStatus] = useState(false);
|
||||
const [newStatusName, setNewStatusName] = useState("");
|
||||
const [statusError, setStatusError] = useState<string | null>(null);
|
||||
@@ -161,16 +162,13 @@ function OrganisationsDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOrganisation) {
|
||||
const orgStatuses = (selectedOrganisation.Organisation as unknown as { statuses: string[] })
|
||||
.statuses;
|
||||
setStatuses(
|
||||
Array.isArray(orgStatuses) ? orgStatuses : ["TO DO", "IN PROGRESS", "REVIEW", "DONE"],
|
||||
);
|
||||
setStatuses(selectedOrganisation.Organisation.statuses);
|
||||
}
|
||||
}, [selectedOrganisation]);
|
||||
|
||||
const updateStatuses = async (newStatuses: string[]) => {
|
||||
const updateStatuses = async (newStatuses: Record<string, string>) => {
|
||||
if (!selectedOrganisation) return;
|
||||
|
||||
try {
|
||||
await organisation.update({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
@@ -197,14 +195,14 @@ function OrganisationsDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
if (statuses.includes(trimmed)) {
|
||||
if (Object.keys(statuses).includes(trimmed)) {
|
||||
setNewStatusName("");
|
||||
setIsCreatingStatus(false);
|
||||
setStatusError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const newStatuses = [...statuses, trimmed];
|
||||
const newStatuses = { ...statuses };
|
||||
newStatuses[trimmed] = DEFAULT_STATUS_COLOUR;
|
||||
await updateStatuses(newStatuses);
|
||||
setNewStatusName("");
|
||||
setIsCreatingStatus(false);
|
||||
@@ -212,26 +210,25 @@ function OrganisationsDialog({
|
||||
};
|
||||
|
||||
const handleRemoveStatusClick = (status: string) => {
|
||||
if (statuses.length <= 1) return;
|
||||
if (Object.keys(statuses).length <= 1) return;
|
||||
setStatusToRemove(status);
|
||||
const remaining = statuses.filter((s) => s !== status);
|
||||
const remaining = Object.keys(statuses).filter((s) => s !== status);
|
||||
setReassignToStatus(remaining[0] || "");
|
||||
};
|
||||
|
||||
const moveStatus = async (status: string, direction: "up" | "down") => {
|
||||
const currentIndex = statuses.indexOf(status);
|
||||
const currentIndex = Object.keys(statuses).indexOf(status);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const nextIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1;
|
||||
if (nextIndex < 0 || nextIndex >= statuses.length) return;
|
||||
|
||||
const nextStatuses = [...statuses];
|
||||
if (nextIndex < 0 || nextIndex >= Object.keys(statuses).length) return;
|
||||
const nextStatuses = [...Object.keys(statuses)];
|
||||
[nextStatuses[currentIndex], nextStatuses[nextIndex]] = [
|
||||
nextStatuses[nextIndex],
|
||||
nextStatuses[currentIndex],
|
||||
];
|
||||
|
||||
await updateStatuses(nextStatuses);
|
||||
await updateStatuses(Object.fromEntries(nextStatuses.map((status) => [status, statuses[status]])));
|
||||
};
|
||||
|
||||
const confirmRemoveStatus = async () => {
|
||||
@@ -242,8 +239,10 @@ function OrganisationsDialog({
|
||||
oldStatus: statusToRemove,
|
||||
newStatus: reassignToStatus,
|
||||
onSuccess: async () => {
|
||||
const newStatuses = statuses.filter((s) => s !== statusToRemove);
|
||||
await updateStatuses(newStatuses);
|
||||
const newStatuses = Object.keys(statuses).filter((s) => s !== statusToRemove);
|
||||
await updateStatuses(
|
||||
Object.fromEntries(newStatuses.map((status) => [status, statuses[status]])),
|
||||
);
|
||||
setStatusToRemove(null);
|
||||
setReassignToStatus("");
|
||||
},
|
||||
@@ -406,13 +405,19 @@ function OrganisationsDialog({
|
||||
<div className="border p-2 min-w-0 overflow-hidden">
|
||||
<h2 className="text-xl font-600 mb-2">Issue Statuses</h2>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex flex-col gap-2 max-h-56 overflow-y-scroll">
|
||||
{statuses.map((status, index) => (
|
||||
<div className="flex flex-col gap-2 max-h-86 overflow-y-scroll grid grid-cols-2">
|
||||
{Object.keys(statuses).map((status, index) => (
|
||||
<div
|
||||
key={status}
|
||||
className="flex items-center justify-between p-2 border"
|
||||
>
|
||||
<StatusTag status={status} />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">{index + 1}</span>
|
||||
<StatusTag
|
||||
status={status}
|
||||
colour={statuses[status]}
|
||||
/>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -427,7 +432,9 @@ function OrganisationsDialog({
|
||||
<Button
|
||||
variant="dummy"
|
||||
size="none"
|
||||
disabled={index === statuses.length - 1}
|
||||
disabled={
|
||||
index === Object.keys(statuses).length - 1
|
||||
}
|
||||
onClick={() =>
|
||||
void moveStatus(status, "down")
|
||||
}
|
||||
@@ -435,7 +442,7 @@ function OrganisationsDialog({
|
||||
>
|
||||
<ChevronDown className="size-5 text-muted-foreground" />
|
||||
</Button>
|
||||
{statuses.length > 1 && (
|
||||
{Object.keys(statuses).length > 1 && (
|
||||
<Button
|
||||
variant="dummy"
|
||||
size="none"
|
||||
@@ -563,11 +570,11 @@ function OrganisationsDialog({
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{statuses
|
||||
{Object.keys(statuses)
|
||||
.filter((s) => s !== statusToRemove)
|
||||
.map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{status}
|
||||
<StatusTag status={status} colour={statuses[status]} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function StatusSelect({
|
||||
placeholder = "Select status",
|
||||
trigger,
|
||||
}: {
|
||||
statuses: string[];
|
||||
statuses: Record<string, string>;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
@@ -33,9 +33,9 @@ export function StatusSelect({
|
||||
</SelectTrigger>
|
||||
)}
|
||||
<SelectContent side="bottom" position="popper" align="start">
|
||||
{statuses.map((status) => (
|
||||
{Object.entries(statuses).map(([status, colour]) => (
|
||||
<SelectItem key={status} value={status} textClassName="text-xs">
|
||||
<StatusTag status={status} className="" />
|
||||
<StatusTag status={status} colour={colour} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function StatusTag({ status, className }: { status: string; className?: string }) {
|
||||
const DARK_TEXT_COLOUR = "#0a0a0a";
|
||||
const THRESHOLD = 0.6;
|
||||
|
||||
const isLight = (hex: string): boolean => {
|
||||
const num = Number.parseInt(hex.replace("#", ""), 16);
|
||||
const r = (num >> 16) & 255;
|
||||
const g = (num >> 8) & 255;
|
||||
const b = num & 255;
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
return luminance > THRESHOLD;
|
||||
};
|
||||
|
||||
export default function StatusTag({
|
||||
status,
|
||||
colour,
|
||||
className,
|
||||
}: {
|
||||
status: string;
|
||||
colour: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const textColour = isLight(colour) ? DARK_TEXT_COLOUR : "var(--foreground)";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-xs px-1 bg-foreground/85 rounded text-background inline-flex whitespace-nowrap",
|
||||
"text-xs px-1 rounded inline-flex whitespace-nowrap border border-foreground/10",
|
||||
className,
|
||||
)}
|
||||
style={{ backgroundColor: colour, color: textColour }}
|
||||
>
|
||||
{status}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user