@@ -18,7 +18,10 @@ import {
1818import { DatePickerTrigger } from '../DatePickerTrigger' ;
1919import { isOverdue , membersFromAssigneeIds } from '../../../lib/issueRowHelpers' ;
2020import type { GroupedIssuesResult } from '../../../lib/issueListGroupAndSort' ;
21- import type { SavedViewGroupBy } from '../../../lib/projectSavedViewDisplay' ;
21+ import type {
22+ SavedViewDisplayPropertyId ,
23+ SavedViewGroupBy ,
24+ } from '../../../lib/projectSavedViewDisplay' ;
2225import type {
2326 IssueApiResponse ,
2427 LabelApiResponse ,
@@ -33,15 +36,18 @@ import {
3336 type IssueLayoutProps ,
3437} from './IssueLayoutTypes' ;
3538
36- /**
37- * Kanban board. By default it groups by state; when the parent provides a
38- * display grouping result, columns follow the selected group-by setting.
39- */
4039interface IssueLayoutBoardProps extends IssueLayoutProps {
4140 groupedIssues ?: GroupedIssuesResult ;
41+ hasCol ?: ( key : SavedViewDisplayPropertyId ) => boolean ;
4242 groupBy ?: SavedViewGroupBy ;
43+ showEmptyGroups ?: boolean ;
44+ subWorkCountByParentId ?: Map < string , number > ;
45+ cycleName ?: ( issue : IssueApiResponse ) => string ;
46+ moduleName ?: ( issue : IssueApiResponse ) => string ;
4347}
4448
49+ const defaultHasCol = ( ) => true ;
50+
4551export function IssueLayoutBoard ( {
4652 project,
4753 states,
@@ -52,15 +58,21 @@ export function IssueLayoutBoard({
5258 issueHref,
5359 now,
5460 projectsById,
55- groupByStateGroup,
5661 groupedIssues,
62+ hasCol : hasColProp ,
5763 groupBy,
64+ showEmptyGroups = false ,
65+ subWorkCountByParentId,
66+ cycleName,
67+ moduleName,
68+ groupByStateGroup,
5869 onCardMove,
5970 onUpdateIssue,
6071} : IssueLayoutBoardProps ) {
6172 const labelById = useMemo ( ( ) => new Map ( labels . map ( ( l ) => [ l . id , l ] ) ) , [ labels ] ) ;
6273 const stateById = useMemo ( ( ) => new Map ( states . map ( ( s ) => [ s . id , s ] ) ) , [ states ] ) ;
6374 const issueById = useMemo ( ( ) => new Map ( issues . map ( ( i ) => [ i . id , i ] ) ) , [ issues ] ) ;
75+ const hasCol = hasColProp ?? defaultHasCol ;
6476
6577 const [ draggingId , setDraggingId ] = useState < string | null > ( null ) ;
6678 const [ dropKey , setDropKey ] = useState < string | null > ( null ) ;
@@ -95,12 +107,17 @@ export function IssueLayoutBoard({
95107 // one column per individual state.
96108 const { columns, orphans } = useMemo ( ( ) => {
97109 if ( groupedIssues ) {
98- const columns = groupedIssues . order . map ( ( key ) => ( {
99- key,
100- title : groupedIssues . isFlat ? 'All work items' : groupedIssues . title ( key ) ,
101- color : stateById . get ( key ) ?. color ?? labelById . get ( key ) ?. color ?? undefined ,
102- items : groupedIssues . groups . get ( key ) ?? [ ] ,
103- } ) ) ;
110+ const columns = groupedIssues . order
111+ . map ( ( key ) => {
112+ const items = groupedIssues . groups . get ( key ) ?? [ ] ;
113+ return {
114+ key,
115+ title : groupedIssues . isFlat ? 'All work items' : groupedIssues . title ( key ) ,
116+ color : stateById . get ( key ) ?. color ?? labelById . get ( key ) ?. color ?? undefined ,
117+ items,
118+ } ;
119+ } )
120+ . filter ( ( col ) => groupedIssues . isFlat || showEmptyGroups || col . items . length > 0 ) ;
104121 return { columns, orphans : [ ] as IssueApiResponse [ ] } ;
105122 }
106123
@@ -149,7 +166,7 @@ export function IssueLayoutBoard({
149166 items : buckets . get ( s . id ) ?? [ ] ,
150167 } ) ) ;
151168 return { columns, orphans } ;
152- } , [ groupedIssues , groupByStateGroup , states , issues , stateById , labelById ] ) ;
169+ } , [ groupedIssues , groupByStateGroup , states , issues , stateById , labelById , showEmptyGroups ] ) ;
153170
154171 const dndEnabled =
155172 Boolean ( onCardMove ) && ( groupByStateGroup || ! groupedIssues || groupBy === 'states' ) ;
@@ -181,6 +198,10 @@ export function IssueLayoutBoard({
181198 onUpdateIssue = { onUpdateIssue }
182199 openId = { openCell }
183200 onOpenCell = { setOpenCell }
201+ hasCol = { hasCol }
202+ subWorkCount = { subWorkCountByParentId ?. get ( issue . id ) ?? 0 }
203+ cycleName = { cycleName ?.( issue ) ?? '—' }
204+ moduleName = { moduleName ?.( issue ) ?? '—' }
184205 />
185206 ) ;
186207
@@ -305,6 +326,10 @@ interface BoardCardProps {
305326 onUpdateIssue ?: IssueLayoutProps [ 'onUpdateIssue' ] ;
306327 openId : string | null ;
307328 onOpenCell : ( id : string | null ) => void ;
329+ hasCol : ( key : SavedViewDisplayPropertyId ) => boolean ;
330+ subWorkCount : number ;
331+ cycleName : string ;
332+ moduleName : string ;
308333}
309334
310335// Wraps an interactive control inside the card's navigating Link so clicking it
@@ -344,9 +369,27 @@ function BoardCard({
344369 onUpdateIssue,
345370 openId,
346371 onOpenCell,
372+ hasCol,
373+ subWorkCount,
374+ cycleName,
375+ moduleName,
347376} : BoardCardProps ) {
348377 const displayId = issueDisplayId ( issue , project , projectsById ) ;
349378 const editable = Boolean ( onUpdateIssue ) ;
379+ const startStr = formatShort ( issue . start_date ) ;
380+ const showPriority = hasCol ( 'priority' ) ;
381+ const showId = hasCol ( 'id' ) ;
382+ const showState = hasCol ( 'state' ) ;
383+ const showStart = hasCol ( 'start_date' ) ;
384+ const showDue = hasCol ( 'due_date' ) ;
385+ const showLabels = hasCol ( 'labels' ) ;
386+ const showAssignee = hasCol ( 'assignee' ) ;
387+ const showSubWorkCount = hasCol ( 'sub_work_count' ) && subWorkCount > 0 ;
388+ const showCycle = hasCol ( 'cycle' ) && cycleName !== '—' ;
389+ const showModule = hasCol ( 'module' ) && moduleName !== '—' ;
390+ const showProperties =
391+ showState || showStart || showDue || showLabels || showSubWorkCount || showCycle || showModule ;
392+
350393 return (
351394 < Link
352395 to = { href }
@@ -362,95 +405,150 @@ function BoardCard({
362405 } ${ isDragging ? 'opacity-50' : '' } `}
363406 >
364407 < div className = "flex items-start gap-2" >
365- { editable && onUpdateIssue ? (
366- < CellGuard >
367- < EditablePriorityCell
368- issueId = { issue . id }
369- priority = { issue . priority }
370- openId = { openId }
371- onOpen = { onOpenCell }
372- onChange = { ( priority ) => onUpdateIssue ( issue . id , { priority } ) }
373- />
374- </ CellGuard >
375- ) : (
376- < PriorityIcon priority = { issue . priority as Priority | null | undefined } />
377- ) }
408+ { showPriority ? (
409+ editable && onUpdateIssue ? (
410+ < CellGuard >
411+ < EditablePriorityCell
412+ issueId = { issue . id }
413+ priority = { issue . priority }
414+ openId = { openId }
415+ onOpen = { onOpenCell }
416+ onChange = { ( priority ) => onUpdateIssue ( issue . id , { priority } ) }
417+ />
418+ </ CellGuard >
419+ ) : (
420+ < PriorityIcon priority = { issue . priority as Priority | null | undefined } />
421+ )
422+ ) : null }
378423 < div className = "min-w-0 flex-1" >
379- < div className = "flex items-center gap-1.5 text-[11px] text-(--txt-tertiary)" >
380- < span className = "font-medium text-(--txt-accent-primary)" > { displayId } </ span >
381- < IssuePRBadge summary = { prSummary } />
382- </ div >
424+ { ( showId || prSummary ) && (
425+ < div className = "flex items-center gap-1.5 text-[11px] text-(--txt-tertiary)" >
426+ { showId ? (
427+ < span className = "font-medium text-(--txt-accent-primary)" > { displayId } </ span >
428+ ) : null }
429+ < IssuePRBadge summary = { prSummary } />
430+ </ div >
431+ ) }
383432 < p className = "mt-0.5 line-clamp-2 text-sm font-medium text-(--txt-primary)" >
384433 { issue . name }
385434 </ p >
386435 </ div >
387436 </ div >
388437
389- < div className = "mt-2 flex flex-wrap items-center gap-1.5" >
390- { editable && onUpdateIssue ? (
391- < >
392- { state && (
393- < CellGuard >
394- < EditableStateCell
395- issueId = { issue . id }
396- state = { state }
397- states = { allStates }
398- openId = { openId }
399- onOpen = { onOpenCell }
400- onChange = { ( state_id ) => onUpdateIssue ( issue . id , { state_id } ) }
401- />
402- </ CellGuard >
403- ) }
404- < CellGuard >
405- < DatePickerTrigger
406- label = "Due date"
407- icon = { < Calendar /> }
408- value = { issue . target_date ?? '' }
409- placeholder = "Due"
410- className = {
411- isOverdue ( issue . target_date , state ?. group , now )
412- ? 'border-(--border-danger-strong) text-(--txt-danger-primary)'
413- : undefined
414- }
415- onChange = { ( v ) => onUpdateIssue ( issue . id , { target_date : v || null } ) }
416- />
417- </ CellGuard >
438+ { showProperties ? (
439+ < div className = "mt-2 flex flex-wrap items-center gap-1.5" >
440+ { editable && onUpdateIssue ? (
441+ < >
442+ { showState ? (
443+ < CellGuard >
444+ < EditableStateCell
445+ issueId = { issue . id }
446+ state = { state }
447+ states = { allStates }
448+ openId = { openId }
449+ onOpen = { onOpenCell }
450+ onChange = { ( state_id ) => onUpdateIssue ( issue . id , { state_id } ) }
451+ />
452+ </ CellGuard >
453+ ) : null }
454+ { showStart ? (
455+ < CellGuard >
456+ < DatePickerTrigger
457+ label = "Start date"
458+ icon = { < Calendar /> }
459+ value = { issue . start_date ?? '' }
460+ placeholder = "Start"
461+ onChange = { ( v ) => onUpdateIssue ( issue . id , { start_date : v || null } ) }
462+ />
463+ </ CellGuard >
464+ ) : null }
465+ { showDue ? (
466+ < CellGuard >
467+ < DatePickerTrigger
468+ label = "Due date"
469+ icon = { < Calendar /> }
470+ value = { issue . target_date ?? '' }
471+ placeholder = "Due"
472+ className = {
473+ isOverdue ( issue . target_date , state ?. group , now )
474+ ? 'border-(--border-danger-strong) text-(--txt-danger-primary)'
475+ : undefined
476+ }
477+ onChange = { ( v ) => onUpdateIssue ( issue . id , { target_date : v || null } ) }
478+ />
479+ </ CellGuard >
480+ ) : null }
481+ { showLabels ? (
482+ < CellGuard >
483+ < EditableLabelCell
484+ issueId = { issue . id }
485+ labelIds = { issue . label_ids ?? [ ] }
486+ labels = { allLabels }
487+ openId = { openId }
488+ onOpen = { onOpenCell }
489+ onChange = { ( label_ids ) => onUpdateIssue ( issue . id , { label_ids } ) }
490+ />
491+ </ CellGuard >
492+ ) : null }
493+ </ >
494+ ) : (
495+ < >
496+ { showLabels && labels . length > 0 ? < LabelChips labels = { labels } max = { 2 } /> : null }
497+ { showStart && startStr ? (
498+ < span className = "rounded-(--radius-md) border border-(--border-subtle) bg-(--bg-surface-1) px-1.5 py-0.5 text-[11px] text-(--txt-secondary)" >
499+ { startStr }
500+ </ span >
501+ ) : null }
502+ { showDue ? < DueDateCell issue = { issue } state = { state } now = { now } /> : null }
503+ { showState ? < StatePill state = { state } /> : null }
504+ </ >
505+ ) }
506+ { showSubWorkCount ? (
507+ < span
508+ className = "inline-flex h-5 items-center rounded-(--radius-md) border border-(--border-subtle) bg-(--bg-surface-1) px-1.5 text-[11px] text-(--txt-secondary)"
509+ title = "Sub-work items"
510+ >
511+ { subWorkCount }
512+ </ span >
513+ ) : null }
514+ { showCycle ? (
515+ < span className = "max-w-[7rem] truncate text-[11px] text-(--txt-secondary)" >
516+ { cycleName }
517+ </ span >
518+ ) : null }
519+ { showModule ? (
520+ < span className = "max-w-[7rem] truncate text-[11px] text-(--txt-secondary)" >
521+ { moduleName }
522+ </ span >
523+ ) : null }
524+ </ div >
525+ ) : null }
526+
527+ { showAssignee ? (
528+ < div className = "mt-2 flex items-center justify-between" >
529+ { editable && onUpdateIssue ? (
418530 < CellGuard >
419- < EditableLabelCell
531+ < EditableAssigneeCell
420532 issueId = { issue . id }
421- labelIds = { issue . label_ids ?? [ ] }
422- labels = { allLabels }
533+ assigneeIds = { issue . assignee_ids ?? [ ] }
534+ members = { allMembers }
423535 openId = { openId }
424536 onOpen = { onOpenCell }
425- onChange = { ( label_ids ) => onUpdateIssue ( issue . id , { label_ids } ) }
537+ onChange = { ( assignee_ids ) => onUpdateIssue ( issue . id , { assignee_ids } ) }
426538 />
427539 </ CellGuard >
428- </ >
429- ) : (
430- < >
431- { labels . length > 0 && < LabelChips labels = { labels } max = { 2 } /> }
432- < DueDateCell issue = { issue } state = { state } now = { now } />
433- { state && < StatePill state = { state } /> }
434- </ >
435- ) }
436- </ div >
437-
438- < div className = "mt-2 flex items-center justify-between" >
439- { editable && onUpdateIssue ? (
440- < CellGuard >
441- < EditableAssigneeCell
442- issueId = { issue . id }
443- assigneeIds = { issue . assignee_ids ?? [ ] }
444- members = { allMembers }
445- openId = { openId }
446- onOpen = { onOpenCell }
447- onChange = { ( assignee_ids ) => onUpdateIssue ( issue . id , { assignee_ids } ) }
448- />
449- </ CellGuard >
450- ) : (
451- < WorkItemAvatarGroup members = { assignees } max = { 3 } />
452- ) }
453- </ div >
540+ ) : (
541+ < WorkItemAvatarGroup members = { assignees } max = { 3 } />
542+ ) }
543+ </ div >
544+ ) : null }
454545 </ Link >
455546 ) ;
456547}
548+
549+ function formatShort ( iso : string | null | undefined ) : string | null {
550+ if ( ! iso ?. trim ( ) ) return null ;
551+ const t = Date . parse ( iso ) ;
552+ if ( Number . isNaN ( t ) ) return null ;
553+ return new Date ( t ) . toLocaleDateString ( ) ;
554+ }
0 commit comments