@@ -23,24 +23,20 @@ import {
2323import dayGridPlugin from "@fullcalendar/daygrid" ;
2424import interactionPlugin from "@fullcalendar/interaction" ;
2525import 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
3428import { ISSUE } from "@forge/consts" ;
35- import { cn } from "@forge/ui" ;
3629import { Button } from "@forge/ui/button" ;
3730import { Tabs , TabsList , TabsTrigger } from "@forge/ui/tabs" ;
3831import { toast } from "@forge/ui/toast" ;
3932
4033import { api } from "~/trpc/react" ;
4134import { CreateEditDialog } from "../issues/create-edit-dialog" ;
4235import { 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" ;
4440import { IssueDayAgenda } from "./calendar-day-agenda" ;
4541import { CalendarIssueDialog } from "./calendar-issue-dialog" ;
4642import { 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 - 9 a - f A - 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+
5965function 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-
134133export 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