Skip to content

Commit 27bf531

Browse files
authored
[Blade] Jira Sprint Final Touches (#441)
1 parent 93c5d80 commit 27bf531

32 files changed

Lines changed: 9163 additions & 458 deletions

apps/blade/src/app/_components/issue-calendar/calendar-day-agenda.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function assigneeDisplayNames(issue: Issue): string[] {
4949
}
5050

5151
function isOverdueIssue(issue: Issue) {
52-
if (issue.status === "FINISHED" || !issue.date) return false;
52+
if (issue.status === "Finished" || !issue.date) return false;
5353
const dueDate = new Date(issue.date);
5454
const todayStart = new Date();
5555
todayStart.setHours(0, 0, 0, 0);
@@ -68,6 +68,7 @@ export function IssueDayAgenda(props: {
6868
issues: Issue[];
6969
isLoading: boolean;
7070
roleNameById: Map<string, string> | undefined;
71+
roleColorById: Map<string, string | null> | undefined;
7172
onIssueSelect?: (issueId: string) => void;
7273
onIssuesChanged?: () => void;
7374
}) {
@@ -76,6 +77,7 @@ export function IssueDayAgenda(props: {
7677
issues,
7778
isLoading,
7879
roleNameById,
80+
roleColorById,
7981
onIssueSelect,
8082
onIssuesChanged,
8183
} = props;
@@ -101,7 +103,7 @@ export function IssueDayAgenda(props: {
101103

102104
const copyIssueLink = useCallback((issueId: string) => {
103105
const origin = typeof window !== "undefined" ? window.location.origin : "";
104-
const url = `${origin}/issues/${issueId}`;
106+
const url = `${origin}/admin/issues/${issueId}`;
105107
void navigator.clipboard.writeText(url).then(
106108
() => {
107109
toast.success("Issue link copied");
@@ -171,6 +173,7 @@ export function IssueDayAgenda(props: {
171173
const teamsText = teams.join(" · ");
172174
const showTeamsBlock = teamsText.length > 0;
173175
const assigneeNames = assigneeDisplayNames(issue);
176+
const teamColor = roleColorById?.get(issue.team) ?? null;
174177
const assigneesText =
175178
assigneeNames.length > 0
176179
? assigneeNames.join(" · ")
@@ -180,6 +183,14 @@ export function IssueDayAgenda(props: {
180183
<li
181184
key={issue.id}
182185
className="rounded-xl border border-border bg-card/80 px-4 py-3.5 shadow-sm ring-1 ring-border/40"
186+
style={
187+
teamColor
188+
? {
189+
borderLeftColor: teamColor,
190+
borderLeftWidth: 4,
191+
}
192+
: undefined
193+
}
183194
>
184195
<div className="flex min-h-8 items-center justify-between gap-3">
185196
<div className="flex min-w-0 flex-1 items-center gap-2.5">

apps/blade/src/app/_components/issue-calendar/calendar-issue-dialog.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function isIssueOverdue(
4343
status: GetIssueResult["status"],
4444
date: Date | string | null | undefined,
4545
) {
46-
if (status === "FINISHED" || !date) return false;
46+
if (status === "Finished" || !date) return false;
4747
const dueDate = new Date(date);
4848
const todayStart = new Date();
4949
todayStart.setHours(0, 0, 0, 0);
@@ -113,7 +113,7 @@ export function CalendarIssueDialog({
113113

114114
async function handleCopyIssueUrl() {
115115
if (!issue || typeof window === "undefined") return;
116-
const url = `${window.location.origin}/issues/${issue.id}`;
116+
const url = `${window.location.origin}/admin/issues/${issue.id}`;
117117
try {
118118
await navigator.clipboard.writeText(url);
119119
toast.success("Issue link copied");
@@ -146,7 +146,7 @@ export function CalendarIssueDialog({
146146
asChild
147147
>
148148
<Link
149-
href={`/issues/${issue.id}`}
149+
href={`/admin/issues/${issue.id}`}
150150
className="block text-left text-foreground underline-offset-4 hover:underline focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
151151
>
152152
{issue.name}

apps/blade/src/app/_components/issue-calendar/calendar-status-dot-legend.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { ISSUE } from "@forge/consts";
44

55
const STATUS_LEGEND_LABEL: Record<(typeof ISSUE.ISSUE_STATUS)[number], string> =
66
{
7-
BACKLOG: "Backlog",
8-
PLANNING: "Planning",
9-
IN_PROGRESS: "In Progress",
10-
FINISHED: "Finished",
7+
Backlog: "Backlog",
8+
Planning: "Planning",
9+
"In Progress": "In Progress",
10+
Finished: "Finished",
1111
};
1212

1313
export function IssueStatusDotLegend() {

apps/blade/src/app/_components/issue-calendar/calendar.tsx

Lines changed: 43 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,20 @@ import {
2323
import dayGridPlugin from "@fullcalendar/daygrid";
2424
import interactionPlugin from "@fullcalendar/interaction";
2525
import FullCalendar from "@fullcalendar/react";
26-
import {
27-
CheckCircle2,
28-
ChevronLeft,
29-
ChevronRight,
30-
CircleDot,
31-
SlidersHorizontal,
32-
} from "lucide-react";
26+
import { ChevronLeft, ChevronRight } from "lucide-react";
3327

3428
import { ISSUE } from "@forge/consts";
35-
import { cn } from "@forge/ui";
3629
import { Button } from "@forge/ui/button";
3730
import { Tabs, TabsList, TabsTrigger } from "@forge/ui/tabs";
3831
import { toast } from "@forge/ui/toast";
3932

4033
import { api } from "~/trpc/react";
4134
import { CreateEditDialog } from "../issues/create-edit-dialog";
4235
import { IssueFetcherPane } from "../issues/issue-fetcher-pane";
43-
import IssueTemplateDialog from "../issues/issue-template-dialog";
36+
import {
37+
getActiveIssueFilterTags,
38+
IssueViewControlBar,
39+
} from "../issues/issue-view-control-bar";
4440
import { IssueDayAgenda } from "./calendar-day-agenda";
4541
import { CalendarIssueDialog } from "./calendar-issue-dialog";
4642
import { IssueStatusDotLegend } from "./calendar-status-dot-legend";
@@ -56,6 +52,16 @@ function issueStatusLabel(status: IssueCalendarStatus) {
5652
.join(" ");
5753
}
5854

55+
function calendarEventTextColor(backgroundColor: string) {
56+
const hex = backgroundColor.replace("#", "");
57+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return "#ffffff";
58+
const r = parseInt(hex.slice(0, 2), 16);
59+
const g = parseInt(hex.slice(2, 4), 16);
60+
const b = parseInt(hex.slice(4, 6), 16);
61+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
62+
return yiq >= 160 ? "#111827" : "#ffffff";
63+
}
64+
5965
function startOfLocalDay(isoOrDate: Date): Date {
6066
const d = new Date(isoOrDate);
6167
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
@@ -124,13 +130,6 @@ function dismissFullCalendarMorePopovers() {
124130
});
125131
}
126132

127-
function formatStatus(status: string) {
128-
return status
129-
.toLowerCase()
130-
.replace(/_/g, " ")
131-
.replace(/\b\w/g, (char) => char.toUpperCase());
132-
}
133-
134133
export default function CalendarView() {
135134
const calendarRef = useRef<FullCalendar | null>(null);
136135
const calendarSectionRef = useRef<HTMLElement | null>(null);
@@ -172,30 +171,16 @@ export default function CalendarView() {
172171
}, [paneData, rawPaneIssues, deferredPaneIssues]);
173172

174173
const openCount = useMemo(
175-
() => rawPaneIssues.filter((issue) => issue.status !== "FINISHED").length,
174+
() => rawPaneIssues.filter((issue) => issue.status !== "Finished").length,
176175
[rawPaneIssues],
177176
);
178177
const closedCount = rawPaneIssues.length - openCount;
179178

180179
const filters = paneData?.filters;
181180

182181
const activeFilters = useMemo(() => {
183-
if (!filters) return [];
184-
const tags: string[] = [];
185-
if (filters.statusFilter !== "all")
186-
tags.push(formatStatus(filters.statusFilter));
187-
if (filters.teamFilter !== "all") tags.push("Team selected");
188-
if (filters.issueKind !== "all")
189-
tags.push(
190-
filters.issueKind === "task" ? "Tasks only" : "Event-linked only",
191-
);
192-
if (filters.rootOnly) tags.push("Root only");
193-
if (filters.dateFrom) tags.push("From " + filters.dateFrom);
194-
if (filters.dateTo) tags.push("To " + filters.dateTo);
195-
if (filters.searchTerm.trim())
196-
tags.push('Search "' + filters.searchTerm.trim() + '"');
197-
return tags;
198-
}, [filters]);
182+
return getActiveIssueFilterTags(filters, paneData?.roleNameById);
183+
}, [filters, paneData?.roleNameById]);
199184

200185
const issuesForCurrentView = useMemo(() => {
201186
if (view === "issueDayAgenda") {
@@ -236,10 +221,18 @@ export default function CalendarView() {
236221
return issuesForCurrentView.flatMap((issue): EventInput[] => {
237222
if (!issue.date) return [];
238223
const d = new Date(issue.date);
224+
const teamColor = paneData?.roleColorById.get(issue.team) ?? null;
225+
const eventPalette = teamColor
226+
? {
227+
backgroundColor: teamColor,
228+
borderColor: teamColor,
229+
textColor: calendarEventTextColor(teamColor),
230+
}
231+
: {};
239232
const baseClassNames = [
240233
"calendar-issue",
241234
issue.event ? "calendar-issue--linked" : "calendar-issue--task",
242-
...(issue.status === "FINISHED" ? ["calendar-issue--finished"] : []),
235+
...(issue.status === "Finished" ? ["calendar-issue--finished"] : []),
243236
] as string[];
244237

245238
const useAllDayBand = !issue.event && isDefaultTaskDueMoment(d);
@@ -254,6 +247,7 @@ export default function CalendarView() {
254247
display: "block" as const,
255248
extendedProps: { issueStatus: issue.status },
256249
classNames: baseClassNames,
250+
...eventPalette,
257251
},
258252
];
259253
}
@@ -269,10 +263,11 @@ export default function CalendarView() {
269263
display: "block" as const,
270264
extendedProps: { issueStatus: issue.status },
271265
classNames: baseClassNames,
266+
...eventPalette,
272267
},
273268
];
274269
});
275-
}, [view, issuesForCurrentView]);
270+
}, [paneData?.roleColorById, view, issuesForCurrentView]);
276271

277272
const fullCalendarViews = useMemo(
278273
() => ({
@@ -409,7 +404,7 @@ export default function CalendarView() {
409404
return undefined;
410405
}
411406
const ex = arg.event.extendedProps as { issueStatus?: IssueCalendarStatus };
412-
const status: IssueCalendarStatus = ex.issueStatus ?? "BACKLOG";
407+
const status: IssueCalendarStatus = ex.issueStatus ?? "Backlog";
413408
const statusLabel = issueStatusLabel(status);
414409
return (
415410
<div
@@ -532,8 +527,18 @@ export default function CalendarView() {
532527
return (
533528
<section
534529
ref={calendarSectionRef}
535-
className="calendar-theme mx-auto flex min-h-0 w-full min-w-0 max-w-6xl flex-1 flex-col gap-3 py-1"
530+
className="calendar-theme mx-auto flex min-h-0 w-full min-w-0 max-w-7xl flex-1 flex-col gap-4 py-4"
536531
>
532+
<IssueViewControlBar
533+
openCount={openCount}
534+
closedCount={closedCount}
535+
activeFilters={activeFilters}
536+
createInitialValues={headerCreateInitialValues}
537+
onBeforeCreate={dismissFullCalendarMorePopovers}
538+
onBeforeOpenFilters={dismissFullCalendarMorePopovers}
539+
onOpenFilters={() => setIsFiltersOpen(true)}
540+
/>
541+
537542
<div className="flex shrink-0 flex-col gap-3">
538543
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
539544
<div className="flex min-w-0 flex-wrap items-center gap-2">
@@ -578,71 +583,6 @@ export default function CalendarView() {
578583
</TabsList>
579584
</Tabs>
580585
</div>
581-
582-
<div className="rounded-lg border border-border bg-muted/20 px-3 py-2.5">
583-
<div
584-
className={cn(
585-
"flex flex-col gap-3",
586-
activeFilters.length > 0
587-
? "md:flex-row md:items-start md:justify-between md:gap-6"
588-
: "sm:flex-row sm:items-center sm:justify-between sm:gap-4 md:gap-6",
589-
)}
590-
>
591-
<div className="flex min-w-0 flex-1 flex-col gap-2">
592-
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
593-
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
594-
<CircleDot className="h-4 w-4 shrink-0 text-emerald-500" />
595-
<span>{openCount} Open</span>
596-
</div>
597-
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
598-
<CheckCircle2 className="h-4 w-4 shrink-0" />
599-
<span>{closedCount} Closed</span>
600-
</div>
601-
</div>
602-
{activeFilters.length > 0 ? (
603-
<div className="flex min-w-0 flex-wrap gap-2 border-t border-border/60 pt-2">
604-
<span className="sr-only">Active filters</span>
605-
{activeFilters.map((tag) => (
606-
<span
607-
key={tag}
608-
className="shrink-0 rounded-full border border-border bg-background/80 px-2.5 py-1 text-xs text-muted-foreground"
609-
>
610-
{tag}
611-
</span>
612-
))}
613-
</div>
614-
) : null}
615-
</div>
616-
617-
<div className="flex shrink-0 flex-wrap items-center gap-2 md:justify-end">
618-
<CreateEditDialog
619-
intent="create"
620-
initialValues={headerCreateInitialValues}
621-
>
622-
<Button
623-
type="button"
624-
onClick={() => {
625-
dismissFullCalendarMorePopovers();
626-
}}
627-
>
628-
Create issue
629-
</Button>
630-
</CreateEditDialog>
631-
<IssueTemplateDialog />
632-
<Button
633-
type="button"
634-
variant="outline"
635-
onClick={() => {
636-
dismissFullCalendarMorePopovers();
637-
setIsFiltersOpen(true);
638-
}}
639-
>
640-
<SlidersHorizontal className="mr-2 h-4 w-4" />
641-
Filters
642-
</Button>
643-
</div>
644-
</div>
645-
</div>
646586
</div>
647587

648588
<div className="relative z-0 flex min-h-0 min-w-0 flex-1 flex-col rounded-lg border border-border bg-card shadow-sm">
@@ -688,6 +628,7 @@ export default function CalendarView() {
688628
issues={issuesForCurrentView}
689629
isLoading={paneData?.isLoading ?? true}
690630
roleNameById={paneData?.roleNameById}
631+
roleColorById={paneData?.roleColorById}
691632
onIssueSelect={(issueId: string) => {
692633
setDetailIssueId(issueId);
693634
setIsDetailOpen(true);

0 commit comments

Comments
 (0)