diff --git a/apps/web/src/components/work-item/layouts/IssueLayoutBoard.tsx b/apps/web/src/components/work-item/layouts/IssueLayoutBoard.tsx index 3a3f629e..4198b57f 100644 --- a/apps/web/src/components/work-item/layouts/IssueLayoutBoard.tsx +++ b/apps/web/src/components/work-item/layouts/IssueLayoutBoard.tsx @@ -18,7 +18,10 @@ import { import { DatePickerTrigger } from '../DatePickerTrigger'; import { isOverdue, membersFromAssigneeIds } from '../../../lib/issueRowHelpers'; import type { GroupedIssuesResult } from '../../../lib/issueListGroupAndSort'; -import type { SavedViewGroupBy } from '../../../lib/projectSavedViewDisplay'; +import type { + SavedViewDisplayPropertyId, + SavedViewGroupBy, +} from '../../../lib/projectSavedViewDisplay'; import type { IssueApiResponse, LabelApiResponse, @@ -33,15 +36,18 @@ import { type IssueLayoutProps, } from './IssueLayoutTypes'; -/** - * Kanban board. By default it groups by state; when the parent provides a - * display grouping result, columns follow the selected group-by setting. - */ interface IssueLayoutBoardProps extends IssueLayoutProps { groupedIssues?: GroupedIssuesResult; + hasCol?: (key: SavedViewDisplayPropertyId) => boolean; groupBy?: SavedViewGroupBy; + showEmptyGroups?: boolean; + subWorkCountByParentId?: Map; + cycleName?: (issue: IssueApiResponse) => string; + moduleName?: (issue: IssueApiResponse) => string; } +const defaultHasCol = () => true; + export function IssueLayoutBoard({ project, states, @@ -52,15 +58,21 @@ export function IssueLayoutBoard({ issueHref, now, projectsById, - groupByStateGroup, groupedIssues, + hasCol: hasColProp, groupBy, + showEmptyGroups = false, + subWorkCountByParentId, + cycleName, + moduleName, + groupByStateGroup, onCardMove, onUpdateIssue, }: IssueLayoutBoardProps) { const labelById = useMemo(() => new Map(labels.map((l) => [l.id, l])), [labels]); const stateById = useMemo(() => new Map(states.map((s) => [s.id, s])), [states]); const issueById = useMemo(() => new Map(issues.map((i) => [i.id, i])), [issues]); + const hasCol = hasColProp ?? defaultHasCol; const [draggingId, setDraggingId] = useState(null); const [dropKey, setDropKey] = useState(null); @@ -95,12 +107,17 @@ export function IssueLayoutBoard({ // one column per individual state. const { columns, orphans } = useMemo(() => { if (groupedIssues) { - const columns = groupedIssues.order.map((key) => ({ - key, - title: groupedIssues.isFlat ? 'All work items' : groupedIssues.title(key), - color: stateById.get(key)?.color ?? labelById.get(key)?.color ?? undefined, - items: groupedIssues.groups.get(key) ?? [], - })); + const columns = groupedIssues.order + .map((key) => { + const items = groupedIssues.groups.get(key) ?? []; + return { + key, + title: groupedIssues.isFlat ? 'All work items' : groupedIssues.title(key), + color: stateById.get(key)?.color ?? labelById.get(key)?.color ?? undefined, + items, + }; + }) + .filter((col) => groupedIssues.isFlat || showEmptyGroups || col.items.length > 0); return { columns, orphans: [] as IssueApiResponse[] }; } @@ -149,7 +166,7 @@ export function IssueLayoutBoard({ items: buckets.get(s.id) ?? [], })); return { columns, orphans }; - }, [groupedIssues, groupByStateGroup, states, issues, stateById, labelById]); + }, [groupedIssues, groupByStateGroup, states, issues, stateById, labelById, showEmptyGroups]); const dndEnabled = Boolean(onCardMove) && (groupByStateGroup || !groupedIssues || groupBy === 'states'); @@ -181,6 +198,10 @@ export function IssueLayoutBoard({ onUpdateIssue={onUpdateIssue} openId={openCell} onOpenCell={setOpenCell} + hasCol={hasCol} + subWorkCount={subWorkCountByParentId?.get(issue.id) ?? 0} + cycleName={cycleName?.(issue) ?? '—'} + moduleName={moduleName?.(issue) ?? '—'} /> ); @@ -305,6 +326,10 @@ interface BoardCardProps { onUpdateIssue?: IssueLayoutProps['onUpdateIssue']; openId: string | null; onOpenCell: (id: string | null) => void; + hasCol: (key: SavedViewDisplayPropertyId) => boolean; + subWorkCount: number; + cycleName: string; + moduleName: string; } // Wraps an interactive control inside the card's navigating Link so clicking it @@ -344,9 +369,27 @@ function BoardCard({ onUpdateIssue, openId, onOpenCell, + hasCol, + subWorkCount, + cycleName, + moduleName, }: BoardCardProps) { const displayId = issueDisplayId(issue, project, projectsById); const editable = Boolean(onUpdateIssue); + const startStr = formatShort(issue.start_date); + const showPriority = hasCol('priority'); + const showId = hasCol('id'); + const showState = hasCol('state'); + const showStart = hasCol('start_date'); + const showDue = hasCol('due_date'); + const showLabels = hasCol('labels'); + const showAssignee = hasCol('assignee'); + const showSubWorkCount = hasCol('sub_work_count') && subWorkCount > 0; + const showCycle = hasCol('cycle') && cycleName !== '—'; + const showModule = hasCol('module') && moduleName !== '—'; + const showProperties = + showState || showStart || showDue || showLabels || showSubWorkCount || showCycle || showModule; + return (
- {editable && onUpdateIssue ? ( - - onUpdateIssue(issue.id, { priority })} - /> - - ) : ( - - )} + {showPriority ? ( + editable && onUpdateIssue ? ( + + onUpdateIssue(issue.id, { priority })} + /> + + ) : ( + + ) + ) : null}
-
- {displayId} - -
+ {(showId || prSummary) && ( +
+ {showId ? ( + {displayId} + ) : null} + +
+ )}

{issue.name}

-
- {editable && onUpdateIssue ? ( - <> - {state && ( - - onUpdateIssue(issue.id, { state_id })} - /> - - )} - - } - value={issue.target_date ?? ''} - placeholder="Due" - className={ - isOverdue(issue.target_date, state?.group, now) - ? 'border-(--border-danger-strong) text-(--txt-danger-primary)' - : undefined - } - onChange={(v) => onUpdateIssue(issue.id, { target_date: v || null })} - /> - + {showProperties ? ( +
+ {editable && onUpdateIssue ? ( + <> + {showState ? ( + + onUpdateIssue(issue.id, { state_id })} + /> + + ) : null} + {showStart ? ( + + } + value={issue.start_date ?? ''} + placeholder="Start" + onChange={(v) => onUpdateIssue(issue.id, { start_date: v || null })} + /> + + ) : null} + {showDue ? ( + + } + value={issue.target_date ?? ''} + placeholder="Due" + className={ + isOverdue(issue.target_date, state?.group, now) + ? 'border-(--border-danger-strong) text-(--txt-danger-primary)' + : undefined + } + onChange={(v) => onUpdateIssue(issue.id, { target_date: v || null })} + /> + + ) : null} + {showLabels ? ( + + onUpdateIssue(issue.id, { label_ids })} + /> + + ) : null} + + ) : ( + <> + {showLabels && labels.length > 0 ? : null} + {showStart && startStr ? ( + + {startStr} + + ) : null} + {showDue ? : null} + {showState ? : null} + + )} + {showSubWorkCount ? ( + + {subWorkCount} + + ) : null} + {showCycle ? ( + + {cycleName} + + ) : null} + {showModule ? ( + + {moduleName} + + ) : null} +
+ ) : null} + + {showAssignee ? ( +
+ {editable && onUpdateIssue ? ( - onUpdateIssue(issue.id, { label_ids })} + onChange={(assignee_ids) => onUpdateIssue(issue.id, { assignee_ids })} /> - - ) : ( - <> - {labels.length > 0 && } - - {state && } - - )} -
- -
- {editable && onUpdateIssue ? ( - - onUpdateIssue(issue.id, { assignee_ids })} - /> - - ) : ( - - )} -
+ ) : ( + + )} +
+ ) : null} ); } + +function formatShort(iso: string | null | undefined): string | null { + if (!iso?.trim()) return null; + const t = Date.parse(iso); + if (Number.isNaN(t)) return null; + return new Date(t).toLocaleDateString(); +} diff --git a/apps/web/src/components/work-item/layouts/IssueLayoutCalendar.tsx b/apps/web/src/components/work-item/layouts/IssueLayoutCalendar.tsx index 3aeb0dfa..9515f1f8 100644 --- a/apps/web/src/components/work-item/layouts/IssueLayoutCalendar.tsx +++ b/apps/web/src/components/work-item/layouts/IssueLayoutCalendar.tsx @@ -7,16 +7,17 @@ import type { Priority } from '../../../types'; import { issueDisplayId, type IssueLayoutProps } from './IssueLayoutTypes'; const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const; +const WEEKDAYS_NO_WEEKEND = WEEKDAYS.slice(0, 5); const MAX_PER_CELL = 3; +type CalendarMode = 'month' | 'week'; + /** - * Month-grid calendar. Issues placed on their `target_date` cell. + * Calendar layout. Issues are placed on their `target_date` cell, with optional + * month/week views and drag-to-reschedule when inline updates are available. * * Issues without a target_date are bucketed into a "No due date" footer panel - * so they aren't silently dropped. - * - * Navigation: prev/next month. "Today" jumps to current month. The "viewMonth" - * is local UI state so switching layout doesn't lose the user's place. + * so they are not silently dropped. */ export function IssueLayoutCalendar({ project, @@ -25,10 +26,16 @@ export function IssueLayoutCalendar({ issueHref, now, projectsById, + onUpdateIssue, }: IssueLayoutProps) { - const [viewMonth, setViewMonth] = useState(() => startOfMonth(new Date(now))); + const [viewDate, setViewDate] = useState(() => startOfDay(new Date(now))); + const [mode, setMode] = useState('month'); + const [showWeekends, setShowWeekends] = useState(true); + const [draggingId, setDraggingId] = useState(null); + const [dropDate, setDropDate] = useState(null); const stateById = useMemo(() => new Map(states.map((s) => [s.id, s])), [states]); + const issueById = useMemo(() => new Map(issues.map((i) => [i.id, i])), [issues]); // Issues bucketed by ISO date string (YYYY-MM-DD). const issuesByDate = useMemo(() => { @@ -50,63 +57,140 @@ export function IssueLayoutCalendar({ return { map, undated }; }, [issues]); - const cells = buildMonthCells(viewMonth); + const cells = useMemo(() => { + const allCells = + mode === 'month' ? buildMonthCells(startOfMonth(viewDate)) : buildWeekCells(viewDate); + return showWeekends ? allCells : allCells.filter(({ date }) => !isWeekend(date)); + }, [mode, showWeekends, viewDate]); + const weekdays = showWeekends ? WEEKDAYS : WEEKDAYS_NO_WEEKEND; + const gridColsClass = showWeekends ? 'grid-cols-7' : 'grid-cols-5'; + const canDrag = Boolean(onUpdateIssue); + + const clearDrag = () => { + setDraggingId(null); + setDropDate(null); + }; + + const handleDropOnDate = (targetDate: string) => { + const issue = draggingId ? issueById.get(draggingId) : null; + clearDrag(); + if (!issue || !onUpdateIssue) return; + if (isoDate(issue.target_date) === targetDate) return; + onUpdateIssue(issue.id, { target_date: targetDate }); + }; + + const moveView = (amount: number) => { + setViewDate((current) => + mode === 'month' ? addMonths(current, amount) : addDays(current, amount * 7), + ); + }; return (
-
+

- {viewMonth.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })} + {calendarTitle(viewDate, mode)}

+
+
+ {(['month', 'week'] as const).map((nextMode) => ( + + ))} +
+ +
- {/* Weekday header */} -
- {WEEKDAYS.map((d) => ( +
+ {weekdays.map((day) => (
- {d} + {day}
))} - {/* Day cells */} {cells.map(({ date, inMonth }) => { const key = localKey(date); const dayIssues = issuesByDate.map.get(key) ?? []; const isToday = sameDay(date, new Date(now)); const visible = dayIssues.slice(0, MAX_PER_CELL); const overflow = dayIssues.length - visible.length; + const isDropTarget = dropDate === key && Boolean(draggingId); return (
{ + if (!draggingId) return; + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + if (dropDate !== key) setDropDate(key); + } + : undefined + } + onDragLeave={ + canDrag + ? () => setDropDate((current) => (current === key ? null : current)) + : undefined + } + onDrop={ + canDrag + ? (event) => { + event.preventDefault(); + handleDropOnDate(key); + } + : undefined + } >
setDraggingId(issue.id)} + onDragEnd={clearDrag} /> ))} {overflow > 0 && ( @@ -143,14 +230,24 @@ export function IssueLayoutCalendar({ {issuesByDate.undated.length > 0 && (
- No due date · {issuesByDate.undated.length} + No due date - {issuesByDate.undated.length} -
    +
      {issuesByDate.undated.map((issue) => (
    • { + if (!canDrag) return; + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', issue.id); + setDraggingId(issue.id); + }} + onDragEnd={clearDrag} + className={`flex items-center gap-2 px-3 py-2 text-sm text-(--txt-primary) no-underline hover:bg-(--bg-layer-1-hover) ${ + canDrag ? 'cursor-grab active:cursor-grabbing' : '' + }`} > {issue.name} @@ -171,16 +268,32 @@ function CalendarPill({ issue, href, state, + draggable, + onDragStart, + onDragEnd, }: { issue: IssueApiResponse; href: string; state: IssueLayoutProps['states'][number] | null; + draggable: boolean; + onDragStart: () => void; + onDragEnd: () => void; }) { const dotColor = state?.color || 'var(--neutral-500)'; return ( { + if (!draggable) return; + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', issue.id); + onDragStart(); + }} + onDragEnd={onDragEnd} + className={`flex items-center gap-1 truncate rounded-(--radius-sm) px-1 py-0.5 no-underline hover:bg-(--bg-layer-1-hover) ${ + draggable ? 'cursor-grab active:cursor-grabbing' : '' + }`} title={issue.name} > boolean; + showEmptyGroups?: boolean; + subWorkCountByParentId?: Map; + cycleName?: (issue: IssueApiResponse) => string; + moduleName?: (issue: IssueApiResponse) => string; +} + +const COLUMN_META: Record< + SpreadsheetColumnKey, + { label: string; width: number; minWidth: number; headerClassName?: string } +> = { + id: { label: 'ID', width: 104, minWidth: 80 }, + title: { label: 'Title', width: 320, minWidth: 200 }, + state: { label: 'State', width: 160, minWidth: 120 }, + priority: { label: 'Priority', width: 92, minWidth: 72, headerClassName: 'text-center' }, + assignee: { label: 'Assignees', width: 144, minWidth: 104 }, + labels: { label: 'Labels', width: 184, minWidth: 128 }, + due_date: { label: 'Due', width: 132, minWidth: 104 }, + start_date: { label: 'Start', width: 132, minWidth: 104 }, + cycle: { label: 'Cycle', width: 148, minWidth: 104 }, + module: { label: 'Module', width: 148, minWidth: 104 }, + sub_work_count: { label: 'Subs', width: 92, minWidth: 72 }, +}; + +const DEFAULT_COLUMN_ORDER: SpreadsheetColumnKey[] = [ + 'id', + 'title', + 'state', + 'priority', + 'assignee', + 'labels', + 'due_date', + 'start_date', + 'cycle', + 'module', + 'sub_work_count', +]; + +const defaultHasCol = () => true; /** - * Spreadsheet layout — flat HTML table with sticky header. Each column reuses - * the same cells the list/board use, so visuals stay consistent. - * - * Columns (in order): ID • Title • State • Priority • Assignees • Labels • Due • Start. - * Property cells are editable in place when `onUpdateIssue` is provided. + * Spreadsheet layout — flat HTML table with sticky header. Display-property + * choices drive the visible columns; grouped display settings render section + * rows, and header interactions provide lightweight local resize/reorder. */ export function IssueLayoutSpreadsheet({ project, @@ -37,158 +92,331 @@ export function IssueLayoutSpreadsheet({ prSummary, issueHref, now, + projectsById, + groupedIssues, + hasCol: hasColProp, + showEmptyGroups = false, + subWorkCountByParentId, + cycleName, + moduleName, onUpdateIssue, -}: IssueLayoutProps) { +}: IssueLayoutSpreadsheetProps) { const stateById = useMemo(() => new Map(states.map((s) => [s.id, s])), [states]); const labelById = useMemo(() => new Map(labels.map((l) => [l.id, l])), [labels]); + const hasCol = hasColProp ?? defaultHasCol; const [openCell, setOpenCell] = useState(null); + const [columnOrder, setColumnOrder] = useState(DEFAULT_COLUMN_ORDER); + const [dragColumn, setDragColumn] = useState(null); + const [columnWidths, setColumnWidths] = useState>>( + {}, + ); + + const enabledColumns = useMemo( + () => DEFAULT_COLUMN_ORDER.filter((key) => key === 'title' || hasCol(key)), + [hasCol], + ); + const columns = [ + ...columnOrder.filter((key) => enabledColumns.includes(key)), + ...enabledColumns.filter((key) => !columnOrder.includes(key)), + ]; + + const sections = groupedIssues + ? groupedIssues.order + .map((key) => ({ + key, + title: groupedIssues.title(key), + issues: groupedIssues.groups.get(key) ?? [], + })) + .filter((section) => groupedIssues.isFlat || showEmptyGroups || section.issues.length > 0) + : [{ key: '__all__', title: '', issues }]; + const showGroupHeaders = Boolean(groupedIssues && !groupedIssues.isFlat); + + const moveColumn = (source: SpreadsheetColumnKey, target: SpreadsheetColumnKey) => { + if (source === target) return; + setColumnOrder((prev) => { + const next = prev.filter((key) => key !== source); + const targetIndex = next.indexOf(target); + if (targetIndex === -1) return prev; + next.splice(targetIndex, 0, source); + return next; + }); + }; + + const startResize = + (column: SpreadsheetColumnKey) => (event: React.PointerEvent) => { + event.preventDefault(); + event.stopPropagation(); + const startX = event.clientX; + const startWidth = columnWidths[column] ?? COLUMN_META[column].width; + const onPointerMove = (moveEvent: PointerEvent) => { + const nextWidth = Math.max( + COLUMN_META[column].minWidth, + startWidth + moveEvent.clientX - startX, + ); + setColumnWidths((prev) => ({ ...prev, [column]: nextWidth })); + }; + const onPointerUp = () => { + window.removeEventListener('pointermove', onPointerMove); + window.removeEventListener('pointerup', onPointerUp); + }; + window.addEventListener('pointermove', onPointerMove); + window.addEventListener('pointerup', onPointerUp); + }; + + const renderCell = (column: SpreadsheetColumnKey, issue: IssueApiResponse) => { + const displayId = issueDisplayId(issue, project, projectsById); + const issueState = issue.state_id ? (stateById.get(issue.state_id) ?? null) : null; + const issueLabels = (issue.label_ids ?? []) + .map((id) => labelById.get(id)) + .filter((l): l is LabelApiResponse => Boolean(l)); + const issueAssignees = membersFromAssigneeIds(members, issue.assignee_ids ?? []); + const subWorkCount = subWorkCountByParentId?.get(issue.id) ?? 0; + const cycle = cycleName?.(issue) ?? '—'; + const module = moduleName?.(issue) ?? '—'; + + switch (column) { + case 'id': + return ( + + {displayId} + + ); + case 'title': + return ( + + {issue.name} + + + ); + case 'state': + return onUpdateIssue ? ( + onUpdateIssue(issue.id, { state_id })} + /> + ) : ( + + ); + case 'priority': + return onUpdateIssue ? ( + onUpdateIssue(issue.id, { priority })} + /> + ) : ( + + ); + case 'assignee': + return onUpdateIssue ? ( + onUpdateIssue(issue.id, { assignee_ids })} + /> + ) : ( + + ); + case 'labels': + return onUpdateIssue ? ( + onUpdateIssue(issue.id, { label_ids })} + /> + ) : ( + + ); + case 'due_date': + return onUpdateIssue ? ( + } + value={issue.target_date ?? ''} + placeholder="—" + className={ + isOverdue(issue.target_date, issueState?.group, now) + ? 'border-(--border-danger-strong) text-(--txt-danger-primary)' + : undefined + } + onChange={(v) => onUpdateIssue(issue.id, { target_date: v || null })} + /> + ) : ( + + ); + case 'start_date': + return onUpdateIssue ? ( + } + value={issue.start_date ?? ''} + placeholder="—" + onChange={(v) => onUpdateIssue(issue.id, { start_date: v || null })} + /> + ) : ( + (formatShort(issue.start_date) ?? '—') + ); + case 'cycle': + return cycle; + case 'module': + return module; + case 'sub_work_count': + return subWorkCount > 0 ? subWorkCount : '—'; + } + }; + + const renderRow = (issue: IssueApiResponse) => ( + + {columns.map((column) => ( + + {renderCell(column, issue)} + + ))} + + ); return (
      - +
      + + {columns.map((column) => ( + + ))} + - - - - - - - - + {columns.map((column) => ( + + ))} - {issues.map((issue) => { - const displayId = `${project.identifier ?? project.id.slice(0, 8)}-${issue.sequence_id ?? issue.id.slice(-4)}`; - const issueState = issue.state_id ? (stateById.get(issue.state_id) ?? null) : null; - const issueLabels = (issue.label_ids ?? []) - .map((id) => labelById.get(id)) - .filter((l): l is LabelApiResponse => Boolean(l)); - const issueAssignees = membersFromAssigneeIds(members, issue.assignee_ids ?? []); - const startStr = formatShort(issue.start_date); - - return ( - - - + + + ) : null} + {section.issues.length > 0 ? ( + section.issues.map(renderRow) + ) : showGroupHeaders ? ( + + - - - - - - - - ); - })} + No work items + + + ) : null} + + ))}
      IDTitleState!AssigneesLabelsDueStart setDragColumn(column)} + onDragOver={(event) => { + if (!dragColumn || dragColumn === column) return; + event.preventDefault(); + }} + onDrop={(event) => { + event.preventDefault(); + if (dragColumn) moveColumn(dragColumn, column); + setDragColumn(null); + }} + onDragEnd={() => setDragColumn(null)} + onResizeStart={startResize(column)} + > + {COLUMN_META[column].label} +
      - - {displayId} - - - ( + + {showGroupHeaders ? ( +
      + {section.title} + + {section.issues.length} + +
      - {issue.name} - - - - {onUpdateIssue ? ( - onUpdateIssue(issue.id, { state_id })} - /> - ) : issueState ? ( - - ) : null} - - {onUpdateIssue ? ( - onUpdateIssue(issue.id, { priority })} - /> - ) : ( - - )} - - {onUpdateIssue ? ( - onUpdateIssue(issue.id, { assignee_ids })} - /> - ) : ( - - )} - - {onUpdateIssue ? ( - onUpdateIssue(issue.id, { label_ids })} - /> - ) : ( - - )} - - {onUpdateIssue ? ( - } - value={issue.target_date ?? ''} - placeholder="—" - className={ - isOverdue(issue.target_date, issueState?.group, now) - ? 'border-(--border-danger-strong) text-(--txt-danger-primary)' - : undefined - } - onChange={(v) => onUpdateIssue(issue.id, { target_date: v || null })} - /> - ) : ( - - )} - - {onUpdateIssue ? ( - } - value={issue.start_date ?? ''} - placeholder="—" - onChange={(v) => onUpdateIssue(issue.id, { start_date: v || null })} - /> - ) : ( - (startStr ?? '—') - )} -
      ); } -function Th({ children, className }: { children: React.ReactNode; className?: string }) { +function Th({ + children, + className, + dragging, + onDragStart, + onDragOver, + onDrop, + onDragEnd, + onResizeStart, +}: { + children: React.ReactNode; + className?: string; + dragging: boolean; + onDragStart: () => void; + onDragOver: (event: React.DragEvent) => void; + onDrop: (event: React.DragEvent) => void; + onDragEnd: () => void; + onResizeStart: (event: React.PointerEvent) => void; +}) { return ( - {children} + + + {children} + + ); } diff --git a/apps/web/src/pages/IssueListPage.tsx b/apps/web/src/pages/IssueListPage.tsx index 4c1e6de6..fa059716 100644 --- a/apps/web/src/pages/IssueListPage.tsx +++ b/apps/web/src/pages/IssueListPage.tsx @@ -557,12 +557,13 @@ export function IssueListPage() { const layout = parseIssueLayout(searchParams.get('layout')); const issueHref = (id: string) => `${baseUrl}/issues/${id}`; + const orderedVisibleIssues = groupedIssues.order.flatMap( + (sectionKey) => groupedIssues.groups.get(sectionKey) ?? [], + ); const layoutProps = { workspaceSlug: workspace.slug, project, - issues: groupedIssues.isFlat - ? (groupedIssues.groups.get(groupedIssues.order[0]) ?? []) - : filteredIssues, + issues: orderedVisibleIssues, states, labels, members, @@ -571,6 +572,14 @@ export function IssueListPage() { issueHref, now, }; + const groupedLayoutProps = { + groupedIssues, + hasCol, + showEmptyGroups: listDisplay.showEmptyGroups, + subWorkCountByParentId, + cycleName, + moduleName, + }; // Manual drag-to-reorder. We splice the move into the FULL loaded issue set // (in manual order) — not just the visible/filtered rows — so hidden items @@ -781,16 +790,22 @@ export function IssueListPage() { {layout === 'board' && ( )} {layout === 'spreadsheet' && ( - + + )} + {layout === 'calendar' && ( + )} - {layout === 'calendar' && } {layout === 'gantt' && } {layout === 'list' && (