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_ACCESS_KEY_ID=your_access_key_id
|
||||
S3_SECRET_ACCESS_KEY=your_secret_access_key
|
||||
S3_BUCKET_NAME=issue
|
||||
S3_BUCKET_NAME=sprint
|
||||
|
||||
STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
|
||||
STRIPE_SECRET_KEY=your_stripe_secret_key
|
||||
|
||||
@@ -103,16 +103,81 @@ if (!SEED_PASSWORD) {
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(SEED_PASSWORD);
|
||||
const now = new Date();
|
||||
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
|
||||
{ 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 5", username: "demo5", email: "demo5@example.com", passwordHash, avatarURL: null },
|
||||
{ name: "demo user 6", username: "demo6", email: "demo6@example.com", passwordHash, avatarURL: null },
|
||||
{ name: "demo user 7", username: "demo7", email: "demo7@example.com", passwordHash, avatarURL: null },
|
||||
{ name: "demo user 8", username: "demo8", email: "demo8@example.com", passwordHash, avatarURL: null },
|
||||
{
|
||||
name: "demo user 3",
|
||||
username: "demo3",
|
||||
email: "demo3@example.com",
|
||||
passwordHash,
|
||||
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() {
|
||||
|
||||
@@ -170,12 +170,14 @@ export function IssuesTable({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{issues.map((issueData) => (
|
||||
{issues.map((issueData) => {
|
||||
const isSelected = issueData.Issue.id === selectedIssueId;
|
||||
return (
|
||||
<TableRow
|
||||
key={issueData.Issue.id}
|
||||
className={cn("cursor-pointer max-w-full")}
|
||||
onClick={() => {
|
||||
if (issueData.Issue.id === selectedIssueId) {
|
||||
if (isSelected) {
|
||||
selectIssue(null);
|
||||
return;
|
||||
}
|
||||
@@ -186,7 +188,8 @@ export function IssuesTable({
|
||||
<TableCell
|
||||
className={cn(
|
||||
"font-medium border-r text-right p-0",
|
||||
issueData.Issue.id === selectedIssueId && "shadow-[inset_2px_0_0_0_var(--personality)]",
|
||||
isSelected &&
|
||||
"shadow-[inset_2px_2px_0_0_var(--personality),inset_0_-2px_0_0_var(--personality)]",
|
||||
)}
|
||||
>
|
||||
<a
|
||||
@@ -202,9 +205,8 @@ export function IssuesTable({
|
||||
<TableCell
|
||||
className={cn(
|
||||
"min-w-0 p-0",
|
||||
!showId &&
|
||||
issueData.Issue.id === selectedIssueId &&
|
||||
"shadow-[inset_2px_0_0_0_var(--personality)]",
|
||||
isSelected &&
|
||||
"shadow-[inset_0_2px_0_0_var(--personality),inset_0_-2px_0_0_var(--personality)]",
|
||||
)}
|
||||
>
|
||||
<a
|
||||
@@ -222,14 +224,23 @@ export function IssuesTable({
|
||||
)}
|
||||
{selectedOrganisation?.Organisation.features.issueStatus &&
|
||||
(columns.status == null || columns.status === true) && (
|
||||
<StatusTag status={issueData.Issue.status} colour={statuses[issueData.Issue.status]} />
|
||||
<StatusTag
|
||||
status={issueData.Issue.status}
|
||||
colour={statuses[issueData.Issue.status]}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate">{issueData.Issue.title}</span>
|
||||
</a>
|
||||
</TableCell>
|
||||
)}
|
||||
{showDescription && (
|
||||
<TableCell className="overflow-hidden p-0">
|
||||
<TableCell
|
||||
className={cn(
|
||||
"overflow-hidden p-0",
|
||||
isSelected &&
|
||||
"shadow-[inset_0_2px_0_0_var(--personality),inset_0_-2px_0_0_var(--personality)]",
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={getIssueUrl(issueData.Issue.number)}
|
||||
onClick={handleLinkClick}
|
||||
@@ -240,7 +251,13 @@ export function IssuesTable({
|
||||
</TableCell>
|
||||
)}
|
||||
{showAssignee && (
|
||||
<TableCell className="h-[32px] p-0">
|
||||
<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)]",
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={getIssueUrl(issueData.Issue.number)}
|
||||
onClick={handleLinkClick}
|
||||
@@ -271,7 +288,8 @@ export function IssuesTable({
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
@@ -203,13 +203,14 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
||||
|
||||
// generate CSV or JSON
|
||||
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) => [
|
||||
user.userId,
|
||||
user.name,
|
||||
user.username,
|
||||
user.totalTimeMs,
|
||||
formatDuration(user.totalTimeMs),
|
||||
(user.totalTimeMs / 3600000).toFixed(2),
|
||||
]);
|
||||
const csv = [headers.join(","), ...rows.map((row) => row.map((cell) => `"${cell}"`).join(","))].join(
|
||||
"\n",
|
||||
@@ -234,6 +235,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
||||
members: data.map((user) => ({
|
||||
...user,
|
||||
totalTimeFormatted: formatDuration(user.totalTimeMs),
|
||||
hours: Number((user.totalTimeMs / 3600000).toFixed(2)),
|
||||
})),
|
||||
},
|
||||
null,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Clock as PixelClock,
|
||||
Close as PixelClose,
|
||||
MessagePlus as PixelCommentSend,
|
||||
Copy as PixelCopy,
|
||||
CreditCard as PixelCreditCard,
|
||||
CreditCardDelete as PixelCreditCardDelete,
|
||||
Dashboard as PixelDashboard,
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
CaretUpIcon as PhosphorChevronUp,
|
||||
CircleIcon as PhosphorCircle,
|
||||
ChatTextIcon as PhosphorComment,
|
||||
CopyIcon as PhosphorCopy,
|
||||
CreditCardIcon as PhosphorCreditCard,
|
||||
CubeIcon as PhosphorCube,
|
||||
DotsSixVerticalIcon as PhosphorDotsSixVertical,
|
||||
@@ -102,6 +104,7 @@ import {
|
||||
CircleCheckIcon,
|
||||
CircleIcon,
|
||||
CircleQuestionMark,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Edit,
|
||||
EllipsisVertical,
|
||||
@@ -161,6 +164,7 @@ const icons = {
|
||||
circleIcon: { lucide: CircleIcon, pixel: PixelCircle, phosphor: PhosphorCircle },
|
||||
circleQuestionMark: { lucide: CircleQuestionMark, pixel: PixelNoteDelete, phosphor: PhosphorQuestion },
|
||||
comment: { lucide: MessageSquarePlus, pixel: PixelCommentSend, phosphor: PhosphorComment },
|
||||
copy: { lucide: Copy, pixel: PixelCopy, phosphor: PhosphorCopy },
|
||||
creditCard: { lucide: CreditCard, pixel: PixelCreditCard, phosphor: PhosphorCreditCard },
|
||||
creditCardDelete: { lucide: CreditCard, pixel: PixelCreditCardDelete, phosphor: PhosphorCreditCard },
|
||||
edit: { lucide: Edit, pixel: PixelEdit, phosphor: PhosphorEdit },
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import Avatar from "@/components/avatar";
|
||||
import { IssueDetailPane } from "@/components/issue-detail-pane";
|
||||
import { IssueModal } from "@/components/issue-modal";
|
||||
import { defaultIssuesTableFilters, IssuesTable, type IssuesTableFilters } from "@/components/issues-table";
|
||||
import { useSelection } from "@/components/selection-provider";
|
||||
import SmallSprintDisplay from "@/components/small-sprint-display";
|
||||
import SmallUserDisplay from "@/components/small-user-display";
|
||||
import StatusTag from "@/components/status-tag";
|
||||
import TopBar from "@/components/top-bar";
|
||||
@@ -353,12 +355,6 @@ export default function Issues() {
|
||||
"title-desc": "Title Z-A",
|
||||
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 (
|
||||
<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 && (
|
||||
<DropdownMenu>
|
||||
<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>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
||||
@@ -414,7 +420,20 @@ export default function Issues() {
|
||||
{selectedOrganisation?.Organisation.features.issueTypes && (
|
||||
<DropdownMenu>
|
||||
<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>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuLabel>Type</DropdownMenuLabel>
|
||||
@@ -446,7 +465,25 @@ export default function Issues() {
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<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>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuLabel>Assignee</DropdownMenuLabel>
|
||||
@@ -486,7 +523,16 @@ export default function Issues() {
|
||||
{selectedOrganisation?.Organisation.features.sprints && (
|
||||
<DropdownMenu>
|
||||
<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>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuLabel>Sprint</DropdownMenuLabel>
|
||||
@@ -555,6 +601,64 @@ export default function Issues() {
|
||||
>
|
||||
<Icon icon="undo" />
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user