fix(web): cards move between board columns again

Since the swimlanes, each column's list is stamped from a template
declared outside the cdkDropListGroup, and a template only sees the
drop groups around where it is declared: every column was on its own and
no card could leave its column. The lists of each row are now connected
by id, with and without swimlanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 13:53:03 +02:00
parent 3c7c1f7f0e
commit 298766ad1a

View File

@@ -1,9 +1,4 @@
import { import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop';
CdkDrag,
CdkDragDrop,
CdkDropList,
CdkDropListGroup,
} from '@angular/cdk/drag-drop';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@@ -90,7 +85,6 @@ const NOBODY = '__nobody__';
selector: 'ts-board-page', selector: 'ts-board-page',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
CdkDropListGroup,
CdkDropList, CdkDropList,
CdkDrag, CdkDrag,
FormField, FormField,
@@ -218,7 +212,7 @@ const NOBODY = '__nobody__';
</div> </div>
} @else { } @else {
@if (laneMode() === 'none') { @if (laneMode() === 'none') {
<div class="ts-board" cdkDropListGroup> <div class="ts-board">
@for (column of board.columns(); track column.status) { @for (column of board.columns(); track column.status) {
<section <section
class="ts-column" class="ts-column"
@@ -261,7 +255,7 @@ const NOBODY = '__nobody__';
<span class="ts-mono ts-muted">{{ lane.count }}</span> <span class="ts-mono ts-muted">{{ lane.count }}</span>
</h3> </h3>
<!-- Cards move between columns of their own row only. --> <!-- Cards move between columns of their own row only. -->
<div class="ts-board" cdkDropListGroup> <div class="ts-board">
@for (column of lane.columns; track column.status) { @for (column of lane.columns; track column.status) {
<section <section
class="ts-column ts-column--lane" class="ts-column ts-column--lane"
@@ -304,6 +298,7 @@ const NOBODY = '__nobody__';
class="ts-column__list" class="ts-column__list"
cdkDropList cdkDropList
[id]="lane + '|' + column.status" [id]="lane + '|' + column.status"
[cdkDropListConnectedTo]="rowLists()[lane] ?? []"
[cdkDropListData]="column" [cdkDropListData]="column"
[cdkDropListDisabled]="!project.canContribute()" [cdkDropListDisabled]="!project.canContribute()"
[cdkDropListEnterPredicate]="canEnter" [cdkDropListEnterPredicate]="canEnter"
@@ -748,6 +743,21 @@ export class BoardPage {
protected statusTone = (status: string) => protected statusTone = (status: string) =>
statusLook(this.project.workflow(), status).tone; statusLook(this.project.workflow(), status).tone;
/**
* The ids of each row's column lists, by row ('' without swimlanes): a card
* moves between the columns of its own row. Connected by id because the
* lists are stamped from a template, and a template only sees the drop
* groups around where it is declared, not where it is placed.
*/
protected readonly rowLists = computed(() => {
const statuses = this.board.columns().map((c) => c.status);
const rows =
this.laneMode() === 'none' ? [''] : this.lanes().map((l) => l.key);
return Object.fromEntries(
rows.map((row) => [row, statuses.map((status) => `${row}|${status}`)]),
);
});
/** Columns a dragged ticket can enter: its allowed transitions. */ /** Columns a dragged ticket can enter: its allowed transitions. */
protected readonly canEnter = ( protected readonly canEnter = (
drag: CdkDrag<IssueSummary>, drag: CdkDrag<IssueSummary>,