mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
Improved filters, emailVerified for seeded data
Improved filters, emailVerified for seeded data
This commit is contained in:
@@ -10,7 +10,7 @@ S3_PUBLIC_URL=https://images.sprintpm.org
|
|||||||
S3_ENDPOINT=https://account_id.r2.cloudflarestorage.com/sprint
|
S3_ENDPOINT=https://account_id.r2.cloudflarestorage.com/sprint
|
||||||
S3_ACCESS_KEY_ID=your_access_key_id
|
S3_ACCESS_KEY_ID=your_access_key_id
|
||||||
S3_SECRET_ACCESS_KEY=your_secret_access_key
|
S3_SECRET_ACCESS_KEY=your_secret_access_key
|
||||||
S3_BUCKET_NAME=issue
|
S3_BUCKET_NAME=sprint
|
||||||
|
|
||||||
STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
|
STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
|
||||||
STRIPE_SECRET_KEY=your_stripe_secret_key
|
STRIPE_SECRET_KEY=your_stripe_secret_key
|
||||||
|
|||||||
@@ -103,16 +103,81 @@ if (!SEED_PASSWORD) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const passwordHash = await hashPassword(SEED_PASSWORD);
|
const passwordHash = await hashPassword(SEED_PASSWORD);
|
||||||
|
const now = new Date();
|
||||||
const users = [
|
const users = [
|
||||||
{ name: "demo user 1", username: "demo1", email: "demo1@example.com", passwordHash, avatarURL: null },
|
{
|
||||||
{ name: "demo user 2", username: "demo2", email: "demo2@example.com", passwordHash, avatarURL: null },
|
name: "demo user 1",
|
||||||
|
username: "demo1",
|
||||||
|
email: "demo1@example.com",
|
||||||
|
passwordHash,
|
||||||
|
avatarURL: null,
|
||||||
|
emailVerified: true,
|
||||||
|
emailVerifiedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "demo user 2",
|
||||||
|
username: "demo2",
|
||||||
|
email: "demo2@example.com",
|
||||||
|
passwordHash,
|
||||||
|
avatarURL: null,
|
||||||
|
emailVerified: true,
|
||||||
|
emailVerifiedAt: now,
|
||||||
|
},
|
||||||
// anything past here is just to have more users to assign issues to
|
// anything past here is just to have more users to assign issues to
|
||||||
{ name: "demo user 3", username: "demo3", email: "demo3@example.com", passwordHash, avatarURL: null },
|
{
|
||||||
{ name: "demo user 4", username: "demo4", email: "demo4@example.com", passwordHash, avatarURL: null },
|
name: "demo user 3",
|
||||||
{ name: "demo user 5", username: "demo5", email: "demo5@example.com", passwordHash, avatarURL: null },
|
username: "demo3",
|
||||||
{ name: "demo user 6", username: "demo6", email: "demo6@example.com", passwordHash, avatarURL: null },
|
email: "demo3@example.com",
|
||||||
{ name: "demo user 7", username: "demo7", email: "demo7@example.com", passwordHash, avatarURL: null },
|
passwordHash,
|
||||||
{ name: "demo user 8", username: "demo8", email: "demo8@example.com", passwordHash, avatarURL: null },
|
avatarURL: null,
|
||||||
|
emailVerified: true,
|
||||||
|
emailVerifiedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "demo user 4",
|
||||||
|
username: "demo4",
|
||||||
|
email: "demo4@example.com",
|
||||||
|
passwordHash,
|
||||||
|
avatarURL: null,
|
||||||
|
emailVerified: true,
|
||||||
|
emailVerifiedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "demo user 5",
|
||||||
|
username: "demo5",
|
||||||
|
email: "demo5@example.com",
|
||||||
|
passwordHash,
|
||||||
|
avatarURL: null,
|
||||||
|
emailVerified: true,
|
||||||
|
emailVerifiedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "demo user 6",
|
||||||
|
username: "demo6",
|
||||||
|
email: "demo6@example.com",
|
||||||
|
passwordHash,
|
||||||
|
avatarURL: null,
|
||||||
|
emailVerified: true,
|
||||||
|
emailVerifiedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "demo user 7",
|
||||||
|
username: "demo7",
|
||||||
|
email: "demo7@example.com",
|
||||||
|
passwordHash,
|
||||||
|
avatarURL: null,
|
||||||
|
emailVerified: true,
|
||||||
|
emailVerifiedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "demo user 8",
|
||||||
|
username: "demo8",
|
||||||
|
email: "demo8@example.com",
|
||||||
|
passwordHash,
|
||||||
|
avatarURL: null,
|
||||||
|
emailVerified: true,
|
||||||
|
emailVerifiedAt: now,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function seed() {
|
async function seed() {
|
||||||
|
|||||||
@@ -170,108 +170,126 @@ export function IssuesTable({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{issues.map((issueData) => (
|
{issues.map((issueData) => {
|
||||||
<TableRow
|
const isSelected = issueData.Issue.id === selectedIssueId;
|
||||||
key={issueData.Issue.id}
|
return (
|
||||||
className={cn("cursor-pointer max-w-full")}
|
<TableRow
|
||||||
onClick={() => {
|
key={issueData.Issue.id}
|
||||||
if (issueData.Issue.id === selectedIssueId) {
|
className={cn("cursor-pointer max-w-full")}
|
||||||
selectIssue(null);
|
onClick={() => {
|
||||||
return;
|
if (isSelected) {
|
||||||
}
|
selectIssue(null);
|
||||||
selectIssue(issueData);
|
return;
|
||||||
}}
|
}
|
||||||
>
|
selectIssue(issueData);
|
||||||
{showId && (
|
}}
|
||||||
<TableCell
|
>
|
||||||
className={cn(
|
{showId && (
|
||||||
"font-medium border-r text-right p-0",
|
<TableCell
|
||||||
issueData.Issue.id === selectedIssueId && "shadow-[inset_2px_0_0_0_var(--personality)]",
|
className={cn(
|
||||||
)}
|
"font-medium border-r text-right p-0",
|
||||||
>
|
isSelected &&
|
||||||
<a
|
"shadow-[inset_2px_2px_0_0_var(--personality),inset_0_-2px_0_0_var(--personality)]",
|
||||||
href={getIssueUrl(issueData.Issue.number)}
|
)}
|
||||||
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",
|
</TableCell>
|
||||||
!showId &&
|
)}
|
||||||
issueData.Issue.id === selectedIssueId &&
|
{showTitle && (
|
||||||
"shadow-[inset_2px_0_0_0_var(--personality)]",
|
<TableCell
|
||||||
)}
|
className={cn(
|
||||||
>
|
"min-w-0 p-0",
|
||||||
<a
|
isSelected &&
|
||||||
href={getIssueUrl(issueData.Issue.number)}
|
"shadow-[inset_0_2px_0_0_var(--personality),inset_0_-2px_0_0_var(--personality)]",
|
||||||
onClick={handleLinkClick}
|
)}
|
||||||
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 status={issueData.Issue.status} colour={statuses[issueData.Issue.status]} />
|
color={issueTypes[issueData.Issue.type].color}
|
||||||
)}
|
/>
|
||||||
<span className="truncate">{issueData.Issue.title}</span>
|
)}
|
||||||
</a>
|
{selectedOrganisation?.Organisation.features.issueStatus &&
|
||||||
</TableCell>
|
(columns.status == null || columns.status === true) && (
|
||||||
)}
|
<StatusTag
|
||||||
{showDescription && (
|
status={issueData.Issue.status}
|
||||||
<TableCell className="overflow-hidden p-0">
|
colour={statuses[issueData.Issue.status]}
|
||||||
<a
|
/>
|
||||||
href={getIssueUrl(issueData.Issue.number)}
|
)}
|
||||||
onClick={handleLinkClick}
|
<span className="truncate">{issueData.Issue.title}</span>
|
||||||
className="block w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent"
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
{showDescription && (
|
||||||
|
<TableCell
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden p-0",
|
||||||
|
isSelected &&
|
||||||
|
"shadow-[inset_0_2px_0_0_var(--personality),inset_0_-2px_0_0_var(--personality)]",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{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 className="h-[32px] p-0">
|
{issueData.Issue.description}
|
||||||
<a
|
</a>
|
||||||
href={getIssueUrl(issueData.Issue.number)}
|
</TableCell>
|
||||||
onClick={handleLinkClick}
|
)}
|
||||||
className="flex items-center justify-end w-full h-full px-2"
|
{showAssignee && (
|
||||||
|
<TableCell
|
||||||
|
className={cn(
|
||||||
|
"h-[32px] p-0",
|
||||||
|
isSelected &&
|
||||||
|
"shadow-[inset_0_2px_0_0_var(--personality),inset_-2px_0_0_0_var(--personality),inset_0_-2px_0_0_var(--personality)]",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{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>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -203,13 +203,14 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
|
|
||||||
// generate CSV or JSON
|
// generate CSV or JSON
|
||||||
if (format === "csv") {
|
if (format === "csv") {
|
||||||
const headers = ["User ID", "Name", "Username", "Total Time (ms)", "Total Time (formatted)"];
|
const headers = ["User ID", "Name", "Username", "Total Time (ms)", "Total Time (formatted)", "Hours"];
|
||||||
const rows = data.map((user) => [
|
const rows = data.map((user) => [
|
||||||
user.userId,
|
user.userId,
|
||||||
user.name,
|
user.name,
|
||||||
user.username,
|
user.username,
|
||||||
user.totalTimeMs,
|
user.totalTimeMs,
|
||||||
formatDuration(user.totalTimeMs),
|
formatDuration(user.totalTimeMs),
|
||||||
|
(user.totalTimeMs / 3600000).toFixed(2),
|
||||||
]);
|
]);
|
||||||
const csv = [headers.join(","), ...rows.map((row) => row.map((cell) => `"${cell}"`).join(","))].join(
|
const csv = [headers.join(","), ...rows.map((row) => row.map((cell) => `"${cell}"`).join(","))].join(
|
||||||
"\n",
|
"\n",
|
||||||
@@ -234,6 +235,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
members: data.map((user) => ({
|
members: data.map((user) => ({
|
||||||
...user,
|
...user,
|
||||||
totalTimeFormatted: formatDuration(user.totalTimeMs),
|
totalTimeFormatted: formatDuration(user.totalTimeMs),
|
||||||
|
hours: Number((user.totalTimeMs / 3600000).toFixed(2)),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Clock as PixelClock,
|
Clock as PixelClock,
|
||||||
Close as PixelClose,
|
Close as PixelClose,
|
||||||
MessagePlus as PixelCommentSend,
|
MessagePlus as PixelCommentSend,
|
||||||
|
Copy as PixelCopy,
|
||||||
CreditCard as PixelCreditCard,
|
CreditCard as PixelCreditCard,
|
||||||
CreditCardDelete as PixelCreditCardDelete,
|
CreditCardDelete as PixelCreditCardDelete,
|
||||||
Dashboard as PixelDashboard,
|
Dashboard as PixelDashboard,
|
||||||
@@ -53,6 +54,7 @@ import {
|
|||||||
CaretUpIcon as PhosphorChevronUp,
|
CaretUpIcon as PhosphorChevronUp,
|
||||||
CircleIcon as PhosphorCircle,
|
CircleIcon as PhosphorCircle,
|
||||||
ChatTextIcon as PhosphorComment,
|
ChatTextIcon as PhosphorComment,
|
||||||
|
CopyIcon as PhosphorCopy,
|
||||||
CreditCardIcon as PhosphorCreditCard,
|
CreditCardIcon as PhosphorCreditCard,
|
||||||
CubeIcon as PhosphorCube,
|
CubeIcon as PhosphorCube,
|
||||||
DotsSixVerticalIcon as PhosphorDotsSixVertical,
|
DotsSixVerticalIcon as PhosphorDotsSixVertical,
|
||||||
@@ -102,6 +104,7 @@ import {
|
|||||||
CircleCheckIcon,
|
CircleCheckIcon,
|
||||||
CircleIcon,
|
CircleIcon,
|
||||||
CircleQuestionMark,
|
CircleQuestionMark,
|
||||||
|
Copy,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Edit,
|
Edit,
|
||||||
EllipsisVertical,
|
EllipsisVertical,
|
||||||
@@ -161,6 +164,7 @@ const icons = {
|
|||||||
circleIcon: { lucide: CircleIcon, pixel: PixelCircle, phosphor: PhosphorCircle },
|
circleIcon: { lucide: CircleIcon, pixel: PixelCircle, phosphor: PhosphorCircle },
|
||||||
circleQuestionMark: { lucide: CircleQuestionMark, pixel: PixelNoteDelete, phosphor: PhosphorQuestion },
|
circleQuestionMark: { lucide: CircleQuestionMark, pixel: PixelNoteDelete, phosphor: PhosphorQuestion },
|
||||||
comment: { lucide: MessageSquarePlus, pixel: PixelCommentSend, phosphor: PhosphorComment },
|
comment: { lucide: MessageSquarePlus, pixel: PixelCommentSend, phosphor: PhosphorComment },
|
||||||
|
copy: { lucide: Copy, pixel: PixelCopy, phosphor: PhosphorCopy },
|
||||||
creditCard: { lucide: CreditCard, pixel: PixelCreditCard, phosphor: PhosphorCreditCard },
|
creditCard: { lucide: CreditCard, pixel: PixelCreditCard, phosphor: PhosphorCreditCard },
|
||||||
creditCardDelete: { lucide: CreditCard, pixel: PixelCreditCardDelete, phosphor: PhosphorCreditCard },
|
creditCardDelete: { lucide: CreditCard, pixel: PixelCreditCardDelete, phosphor: PhosphorCreditCard },
|
||||||
edit: { lucide: Edit, pixel: PixelEdit, phosphor: PhosphorEdit },
|
edit: { lucide: Edit, pixel: PixelEdit, phosphor: PhosphorEdit },
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import Avatar from "@/components/avatar";
|
||||||
import { IssueDetailPane } from "@/components/issue-detail-pane";
|
import { IssueDetailPane } from "@/components/issue-detail-pane";
|
||||||
import { IssueModal } from "@/components/issue-modal";
|
import { IssueModal } from "@/components/issue-modal";
|
||||||
import { defaultIssuesTableFilters, IssuesTable, type IssuesTableFilters } from "@/components/issues-table";
|
import { defaultIssuesTableFilters, IssuesTable, type IssuesTableFilters } from "@/components/issues-table";
|
||||||
import { useSelection } from "@/components/selection-provider";
|
import { useSelection } from "@/components/selection-provider";
|
||||||
|
import SmallSprintDisplay from "@/components/small-sprint-display";
|
||||||
import SmallUserDisplay from "@/components/small-user-display";
|
import SmallUserDisplay from "@/components/small-user-display";
|
||||||
import StatusTag from "@/components/status-tag";
|
import StatusTag from "@/components/status-tag";
|
||||||
import TopBar from "@/components/top-bar";
|
import TopBar from "@/components/top-bar";
|
||||||
@@ -353,12 +355,6 @@ export default function Issues() {
|
|||||||
"title-desc": "Title Z-A",
|
"title-desc": "Title Z-A",
|
||||||
status: "Status",
|
status: "Status",
|
||||||
};
|
};
|
||||||
const sprintLabel = useMemo(() => {
|
|
||||||
if (issueFilters.sprintId === "all") return "Sprint";
|
|
||||||
if (issueFilters.sprintId === "none") return "No sprint";
|
|
||||||
const sprintMatch = sprintsData.find((sprint) => sprint.id === issueFilters.sprintId);
|
|
||||||
return sprintMatch?.name ?? "Sprint";
|
|
||||||
}, [issueFilters.sprintId, sprintsData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={`w-full h-screen flex flex-col gap-${BREATHING_ROOM} p-${BREATHING_ROOM}`}>
|
<main className={`w-full h-screen flex flex-col gap-${BREATHING_ROOM} p-${BREATHING_ROOM}`}>
|
||||||
@@ -384,7 +380,17 @@ export default function Issues() {
|
|||||||
{selectedOrganisation?.Organisation.features.issueStatus && (
|
{selectedOrganisation?.Organisation.features.issueStatus && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger size="default" className="h-9">
|
<DropdownMenuTrigger size="default" className="h-9">
|
||||||
Status
|
{issueFilters.statuses.length === 0 ? (
|
||||||
|
"Status"
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{Object.entries(statuses)
|
||||||
|
.filter(([status]) => issueFilters.statuses.includes(status))
|
||||||
|
.map(([status, colour]) => (
|
||||||
|
<StatusTag key={status} status={status} colour={colour} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
||||||
@@ -414,7 +420,20 @@ export default function Issues() {
|
|||||||
{selectedOrganisation?.Organisation.features.issueTypes && (
|
{selectedOrganisation?.Organisation.features.issueTypes && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger size="default" className="h-9">
|
<DropdownMenuTrigger size="default" className="h-9">
|
||||||
Type
|
{issueFilters.types.length === 0 ? (
|
||||||
|
"Type"
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{Object.entries(issueTypes)
|
||||||
|
.filter(([type]) => issueFilters.types.includes(type))
|
||||||
|
.map(([type, definition]) => (
|
||||||
|
<div key={type} className="flex items-center gap-1.5">
|
||||||
|
<Icon icon={definition.icon} size={14} color={definition.color} />
|
||||||
|
<span>{type}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuLabel>Type</DropdownMenuLabel>
|
<DropdownMenuLabel>Type</DropdownMenuLabel>
|
||||||
@@ -446,7 +465,25 @@ export default function Issues() {
|
|||||||
)}
|
)}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger size="default" className="h-9">
|
<DropdownMenuTrigger size="default" className="h-9">
|
||||||
Assignee
|
{issueFilters.assignees.length === 0 ? (
|
||||||
|
"Assignee"
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{issueFilters.assignees.includes("unassigned") && <span>Unassigned</span>}
|
||||||
|
{members
|
||||||
|
.filter((member) => issueFilters.assignees.includes(String(member.id)))
|
||||||
|
.map((member) => (
|
||||||
|
<Avatar
|
||||||
|
key={member.id}
|
||||||
|
name={member.name}
|
||||||
|
username={member.username}
|
||||||
|
avatarURL={member.avatarURL}
|
||||||
|
size={6}
|
||||||
|
textClass="text-[10px]"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuLabel>Assignee</DropdownMenuLabel>
|
<DropdownMenuLabel>Assignee</DropdownMenuLabel>
|
||||||
@@ -486,7 +523,16 @@ export default function Issues() {
|
|||||||
{selectedOrganisation?.Organisation.features.sprints && (
|
{selectedOrganisation?.Organisation.features.sprints && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger size="default" className="h-9">
|
<DropdownMenuTrigger size="default" className="h-9">
|
||||||
{sprintLabel}
|
{issueFilters.sprintId === "all" ? (
|
||||||
|
"Sprint"
|
||||||
|
) : issueFilters.sprintId === "none" ? (
|
||||||
|
<SmallSprintDisplay />
|
||||||
|
) : (
|
||||||
|
(() => {
|
||||||
|
const sprint = sprintsData.find((s) => s.id === issueFilters.sprintId);
|
||||||
|
return sprint ? <SmallSprintDisplay sprint={sprint} /> : "Sprint";
|
||||||
|
})()
|
||||||
|
)}
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuLabel>Sprint</DropdownMenuLabel>
|
<DropdownMenuLabel>Sprint</DropdownMenuLabel>
|
||||||
@@ -555,6 +601,64 @@ export default function Issues() {
|
|||||||
>
|
>
|
||||||
<Icon icon="undo" />
|
<Icon icon="undo" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
className="w-9 h-9"
|
||||||
|
aria-label="Copy filters URL"
|
||||||
|
title="Copy filters URL"
|
||||||
|
disabled={
|
||||||
|
!issueFilters.query &&
|
||||||
|
issueFilters.statuses.length === 0 &&
|
||||||
|
issueFilters.types.length === 0 &&
|
||||||
|
issueFilters.assignees.length === 0 &&
|
||||||
|
issueFilters.sprintId === defaultIssuesTableFilters.sprintId &&
|
||||||
|
issueFilters.sort === defaultIssuesTableFilters.sort
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
if (issueFilters.query) {
|
||||||
|
params.set("q", issueFilters.query);
|
||||||
|
} else {
|
||||||
|
params.delete("q");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issueFilters.statuses.length > 0) {
|
||||||
|
params.set("status", issueFilters.statuses.join(","));
|
||||||
|
} else {
|
||||||
|
params.delete("status");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issueFilters.types.length > 0) {
|
||||||
|
params.set("type", issueFilters.types.join(","));
|
||||||
|
} else {
|
||||||
|
params.delete("type");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issueFilters.assignees.length > 0) {
|
||||||
|
params.set("assignee", issueFilters.assignees.join(","));
|
||||||
|
} else {
|
||||||
|
params.delete("assignee");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issueFilters.sprintId !== defaultIssuesTableFilters.sprintId) {
|
||||||
|
params.set("sprint", String(issueFilters.sprintId));
|
||||||
|
} else {
|
||||||
|
params.delete("sprint");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issueFilters.sort !== defaultIssuesTableFilters.sort) {
|
||||||
|
params.set("sort", issueFilters.sort);
|
||||||
|
} else {
|
||||||
|
params.delete("sort");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${window.location.origin}${window.location.pathname}?${params.toString()}`;
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="copy" />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user