Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions apps/web/src/components/work-item/layouts/IssueLayoutBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
} from '../EditableCells';
import { DatePickerTrigger } from '../DatePickerTrigger';
import { isOverdue, membersFromAssigneeIds } from '../../../lib/issueRowHelpers';
import type { GroupedIssuesResult } from '../../../lib/issueListGroupAndSort';
import type { SavedViewGroupBy } from '../../../lib/projectSavedViewDisplay';
import type {
IssueApiResponse,
LabelApiResponse,
Expand All @@ -32,12 +34,14 @@ import {
} from './IssueLayoutTypes';

/**
* Kanban board grouped by state. One column per state, ordered by `sequence`,
* cards reuse the same cells the list rows use.
*
* Issues with no state_id (or whose state was deleted) bucket into a synthetic
* "No state" column at the end.
* 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;
groupBy?: SavedViewGroupBy;
}

export function IssueLayoutBoard({
project,
states,
Expand All @@ -49,14 +53,15 @@ export function IssueLayoutBoard({
now,
projectsById,
groupByStateGroup,
groupedIssues,
groupBy,
onCardMove,
onUpdateIssue,
}: IssueLayoutProps) {
}: 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 dndEnabled = Boolean(onCardMove);
const [draggingId, setDraggingId] = useState<string | null>(null);
const [dropKey, setDropKey] = useState<string | null>(null);
const [openCell, setOpenCell] = useState<string | null>(null);
Expand All @@ -65,7 +70,7 @@ export function IssueLayoutBoard({
// columns the key is already a state id; for grouped columns we pick a state
// in the issue's own project that belongs to that group (default first).
const resolveTargetStateId = (columnKey: string, issue: IssueApiResponse): string | null => {
if (!groupByStateGroup) return columnKey;
if (!groupByStateGroup) return stateById.has(columnKey) ? columnKey : null;
const candidates = states.filter(
(s) => s.group === columnKey && s.project_id === issue.project_id,
);
Expand All @@ -89,6 +94,16 @@ export function IssueLayoutBoard({
// board doesn't repeat "Todo/In Progress/Done" once per project); otherwise
// 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) ?? [],
}));
return { columns, orphans: [] as IssueApiResponse[] };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the no-state bucket for stale state IDs

When the project board is grouped by states, this new grouped branch bypasses the old state bucketing logic and always returns no orphans. buildGroupedIssues treats any non-empty issue.state_id as its own key even if that state no longer exists in states, so issues whose state was deleted now render under a raw state-id column instead of the board's No state bucket; that column also cannot be a valid drop target because resolveTargetStateId rejects keys not in stateById. Please keep the stale-state/no-state handling for the state-backed grouped board.

Useful? React with 👍 / 👎.

}

const orphans: IssueApiResponse[] = [];

if (groupByStateGroup) {
Expand Down Expand Up @@ -134,7 +149,10 @@ export function IssueLayoutBoard({
items: buckets.get(s.id) ?? [],
}));
return { columns, orphans };
}, [groupByStateGroup, states, issues, stateById]);
}, [groupedIssues, groupByStateGroup, states, issues, stateById, labelById]);

const dndEnabled =
Boolean(onCardMove) && (groupByStateGroup || !groupedIssues || groupBy === 'states');

const renderCard = (issue: IssueApiResponse) => (
<BoardCard
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/pages/IssueListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,8 @@ export function IssueListPage() {
{layout === 'board' && (
<IssueLayoutBoard
{...layoutProps}
groupedIssues={groupedIssues}
groupBy={listDisplay.groupBy}
onCardMove={handleCardMove}
onUpdateIssue={handleInlineUpdate}
/>
Expand Down
Loading