scrollabe issue table

This commit is contained in:
2026-01-31 15:19:02 +00:00
parent cc0eb60bf5
commit 18300ad130
2 changed files with 147 additions and 145 deletions

View File

@@ -3,7 +3,6 @@ import Avatar from "@/components/avatar";
import { useSelection } from "@/components/selection-provider"; import { useSelection } from "@/components/selection-provider";
import StatusTag from "@/components/status-tag"; import StatusTag from "@/components/status-tag";
import Icon, { type IconName } from "@/components/ui/icon"; import Icon, { type IconName } from "@/components/ui/icon";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useIssues, useSelectedOrganisation, useSelectedProject } from "@/lib/query/hooks"; import { useIssues, useSelectedOrganisation, useSelectedProject } from "@/lib/query/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -155,144 +154,149 @@ export function IssuesTable({
const showAssignee = columns.assignee == null || columns.assignee === true; const showAssignee = columns.assignee == null || columns.assignee === true;
return ( return (
<Table className={cn("table-fixed", className)}> <div className={cn("h-full overflow-auto border-t", className)}>
<TableHeader> <table className="w-full table-fixed border-collapse text-sm border-l border-r ">
<TableRow hoverEffect={false} className="bg-secondary"> <thead className="sticky top-0 z-10 bg-secondary">
{showId && ( <tr className="border-b h-[25px]">
<TableHead className="text-right w-10 border-r text-xs font-medium text-muted-foreground"> {showId && (
ID <th className="text-right w-10 border-r text-xs font-medium text-muted-foreground px-2 align-middle">
</TableHead> ID
)} </th>
{showTitle && <TableHead className="text-xs font-medium text-muted-foreground">Title</TableHead>} )}
{showDescription && ( {showTitle && (
<TableHead className="text-xs font-medium text-muted-foreground">Description</TableHead> <th className="text-xs font-medium text-muted-foreground px-2 align-middle text-left">Title</th>
)} )}
{/* below is kept blank to fill the space, used as the "Assignee" column */} {showDescription && (
{showAssignee && <TableHead className="w-[1%]"></TableHead>} <th className="text-xs font-medium text-muted-foreground px-2 align-middle text-left">
</TableRow> Description
</TableHeader> </th>
<TableBody> )}
{issues.map((issueData) => { {showAssignee && <th className="w-[1%] px-2"></th>}
const isSelected = issueData.Issue.id === selectedIssueId; </tr>
return ( </thead>
<TableRow <tbody>
key={issueData.Issue.id} {issues.map((issueData) => {
className={cn("cursor-pointer max-w-full")} const isSelected = issueData.Issue.id === selectedIssueId;
onClick={() => { return (
if (isSelected) { <tr
selectIssue(null); key={issueData.Issue.id}
return; className={cn("cursor-pointer h-[25px] border-b hover:bg-muted/40")}
} onClick={() => {
selectIssue(issueData); if (isSelected) {
}} selectIssue(null);
> return;
{showId && ( }
<TableCell selectIssue(issueData);
className={cn( }}
"font-medium border-r text-right p-0", >
(isSelected || highlighted?.includes(issueData.Issue.id)) && {showId && (
"shadow-[inset_1px_1px_0_0_var(--personality),inset_0_-1px_0_0_var(--personality)]", <td
)} className={cn(
> "font-medium border-r text-right p-0 align-middle",
<a (isSelected || highlighted?.includes(issueData.Issue.id)) &&
href={getIssueUrl(issueData.Issue.number)} "shadow-[inset_1px_1px_0_0_var(--personality),inset_0_-1px_0_0_var(--personality)]",
onClick={handleLinkClick} )}
className="block w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent"
> >
{issueData.Issue.number.toString().padStart(3, "0")} <a
</a> href={getIssueUrl(issueData.Issue.number)}
</TableCell> onClick={handleLinkClick}
)} className="block w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent"
{showTitle && ( >
<TableCell {issueData.Issue.number.toString().padStart(3, "0")}
className={cn( </a>
"min-w-0 p-0", </td>
(isSelected || highlighted?.includes(issueData.Issue.id)) && )}
"shadow-[inset_0_1px_0_0_var(--personality),inset_0_-1px_0_0_var(--personality)]", {showTitle && (
)} <td
> className={cn(
<a "min-w-0 p-0 align-middle",
href={getIssueUrl(issueData.Issue.number)} (isSelected || highlighted?.includes(issueData.Issue.id)) &&
onClick={handleLinkClick} "shadow-[inset_0_1px_0_0_var(--personality),inset_0_-1px_0_0_var(--personality)]",
className="flex items-center gap-2 min-w-0 w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent" )}
> >
{selectedOrganisation?.Organisation.features.issueTypes && <a
issueTypes[issueData.Issue.type] && ( href={getIssueUrl(issueData.Issue.number)}
<Icon onClick={handleLinkClick}
icon={issueTypes[issueData.Issue.type].icon as IconName} className="flex items-center gap-2 min-w-0 w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent"
size={16} >
color={issueTypes[issueData.Issue.type].color} {selectedOrganisation?.Organisation.features.issueTypes &&
/> issueTypes[issueData.Issue.type] && (
)} <Icon
{selectedOrganisation?.Organisation.features.issueStatus && icon={issueTypes[issueData.Issue.type].icon as IconName}
(columns.status == null || columns.status === true) && ( size={16}
<StatusTag color={issueTypes[issueData.Issue.type].color}
status={issueData.Issue.status} />
colour={statuses[issueData.Issue.status]} )}
/> {selectedOrganisation?.Organisation.features.issueStatus &&
)} (columns.status == null || columns.status === true) && (
<span className="truncate">{issueData.Issue.title}</span> <StatusTag
</a> status={issueData.Issue.status}
</TableCell> colour={statuses[issueData.Issue.status]}
)} />
{showDescription && ( )}
<TableCell <span className="truncate">{issueData.Issue.title}</span>
className={cn( </a>
"overflow-hidden p-0", </td>
(isSelected || highlighted?.includes(issueData.Issue.id)) && )}
"shadow-[inset_0_1px_0_0_var(--personality),inset_0_-1px_0_0_var(--personality)]", {showDescription && (
)} <td
> className={cn(
<a "overflow-hidden p-0 align-middle",
href={getIssueUrl(issueData.Issue.number)} (isSelected || highlighted?.includes(issueData.Issue.id)) &&
onClick={handleLinkClick} "shadow-[inset_0_1px_0_0_var(--personality),inset_0_-1px_0_0_var(--personality)]",
className="block w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent" )}
> >
{issueData.Issue.description} <a
</a> href={getIssueUrl(issueData.Issue.number)}
</TableCell> onClick={handleLinkClick}
)} className="block w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent"
{showAssignee && ( >
<TableCell {issueData.Issue.description}
className={cn( </a>
"h-[32px] p-0", </td>
(isSelected || highlighted?.includes(issueData.Issue.id)) && )}
"shadow-[inset_0_1px_0_0_var(--personality),inset_-1px_0_0_0_var(--personality),inset_0_-1px_0_0_var(--personality)]", {showAssignee && (
)} <td
> className={cn(
<a "h-[32px] p-0 align-middle",
href={getIssueUrl(issueData.Issue.number)} (isSelected || highlighted?.includes(issueData.Issue.id)) &&
onClick={handleLinkClick} "shadow-[inset_0_1px_0_0_var(--personality),inset_-1px_0_0_0_var(--personality),inset_0_-1px_0_0_var(--personality)]",
className="flex items-center justify-end w-full h-full px-2" )}
> >
{selectedOrganisation?.Organisation.features.issueAssigneesShownInTable && <a
issueData.Assignees && href={getIssueUrl(issueData.Issue.number)}
issueData.Assignees.length > 0 && ( onClick={handleLinkClick}
<div className="flex items-center -space-x-2 pr-1.5"> className="flex items-center justify-end w-full h-full px-2"
{issueData.Assignees.slice(0, 3).map((assignee) => ( >
<Avatar {selectedOrganisation?.Organisation.features.issueAssigneesShownInTable &&
key={assignee.id} issueData.Assignees &&
name={assignee.name} issueData.Assignees.length > 0 && (
username={assignee.username} <div className="flex items-center -space-x-2 pr-1.5">
avatarURL={assignee.avatarURL} {issueData.Assignees.slice(0, 3).map((assignee) => (
textClass="text-xs" <Avatar
className="ring-1 ring-background" key={assignee.id}
/> name={assignee.name}
))} username={assignee.username}
{issueData.Assignees.length > 3 && ( avatarURL={assignee.avatarURL}
<span className="flex items-center justify-center w-6 h-6 text-[10px] font-medium bg-muted text-muted-foreground rounded-full ring-1 ring-background"> textClass="text-xs"
+{issueData.Assignees.length - 3} className="ring-1 ring-background"
</span> />
)} ))}
</div> {issueData.Assignees.length > 3 && (
)} <span className="flex items-center justify-center w-6 h-6 text-[10px] font-medium bg-muted text-muted-foreground rounded-full ring-1 ring-background">
</a> +{issueData.Assignees.length - 3}
</TableCell> </span>
)} )}
</TableRow> </div>
); )}
})} </a>
</TableBody> </td>
</Table> )}
</tr>
);
})}
</tbody>
</table>
</div>
); );
} }

View File

@@ -667,15 +667,13 @@ export default function Issues() {
{selectedOrganisationId && selectedProjectId && issuesData.length > 0 && ( {selectedOrganisationId && selectedProjectId && issuesData.length > 0 && (
<ResizablePanelGroup className={`flex-1`}> <ResizablePanelGroup className={`flex-1`}>
<ResizablePanel id={"left"} minSize={400}> <ResizablePanel id={"left"} minSize={400} className="h-full overflow-hidden">
<div className="border w-full flex-shrink"> <IssuesTable
<IssuesTable columns={{ description: false }}
columns={{ description: false }} className="w-full"
className="w-full" filters={issueFilters}
filters={issueFilters} highlighted={highlighted}
highlighted={highlighted} />
/>
</div>
</ResizablePanel> </ResizablePanel>
{selectedIssue && !showIssueModal && ( {selectedIssue && !showIssueModal && (