mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-14 05:39:08 +02:00
* feat(list): drag-and-drop reordering + shared drag utils - List view drag-and-drop: reorder within/across status groups, drop into collapsed groups, DraggableListRow with useSortable + checkbox protection - Extract shared drag utilities (computePosition, findColumn, buildColumns, makeKanbanCollision, issueMatchesGroup, getMoveUpdates) to drag-utils.ts - Reuse IssueDisplayControls in MyIssues header (dedup ~380 lines) - Radio check marks for grouping/sort dropdowns - Remove outer p-1 wrapper, unify board view to p-2 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(my-issues): move MyIssuesHeader inside ViewStoreProvider IssueDisplayControls calls useViewStore which requires the provider context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(header): restore swimlane option in view toggle dropdown Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
import {
|
|
pointerWithin,
|
|
closestCenter,
|
|
type CollisionDetection,
|
|
} from "@dnd-kit/core";
|
|
import type { Issue, IssueAssigneeType, IssueStatus, UpdateIssueRequest } from "@multica/core/types";
|
|
import type { IssueGrouping } from "@multica/core/issues/stores/view-store";
|
|
import type { BoardColumnGroup } from "../components/board-column";
|
|
|
|
export type DragMoveUpdates = Pick<
|
|
UpdateIssueRequest,
|
|
"status" | "assignee_type" | "assignee_id" | "position"
|
|
>;
|
|
|
|
const UNASSIGNED_GROUP_ID = "assignee:unassigned";
|
|
|
|
export function makeKanbanCollision(groupIds: Set<string>): CollisionDetection {
|
|
return (args) => {
|
|
const pointer = pointerWithin(args);
|
|
if (pointer.length > 0) {
|
|
const items = pointer.filter((c) => !groupIds.has(c.id as string));
|
|
if (items.length > 0) return items;
|
|
return pointer;
|
|
}
|
|
return closestCenter(args);
|
|
};
|
|
}
|
|
|
|
export function statusGroupId(status: IssueStatus): string {
|
|
return `status:${status}`;
|
|
}
|
|
|
|
export function assigneeGroupId(
|
|
type: IssueAssigneeType | null,
|
|
id: string | null,
|
|
): string {
|
|
return type && id ? `assignee:${type}:${id}` : UNASSIGNED_GROUP_ID;
|
|
}
|
|
|
|
export function getIssueGroupId(issue: Issue, grouping: IssueGrouping): string {
|
|
if (grouping === "status") return statusGroupId(issue.status);
|
|
return assigneeGroupId(issue.assignee_type, issue.assignee_id);
|
|
}
|
|
|
|
export function buildColumns(
|
|
issues: Issue[],
|
|
groups: BoardColumnGroup[],
|
|
grouping: IssueGrouping,
|
|
): Record<string, string[]> {
|
|
const cols: Record<string, string[]> = {};
|
|
for (const group of groups) cols[group.id] = [];
|
|
for (const issue of issues) {
|
|
const gid = getIssueGroupId(issue, grouping);
|
|
if (cols[gid]) cols[gid].push(issue.id);
|
|
}
|
|
return cols;
|
|
}
|
|
|
|
export function computePosition(ids: string[], activeId: string, issueMap: Map<string, Issue>): number {
|
|
const idx = ids.indexOf(activeId);
|
|
if (idx === -1) return 0;
|
|
const getPos = (id: string) => issueMap.get(id)?.position ?? 0;
|
|
if (ids.length === 1) return issueMap.get(activeId)?.position ?? 0;
|
|
if (idx === 0) return getPos(ids[1]!) - 1;
|
|
if (idx === ids.length - 1) return getPos(ids[idx - 1]!) + 1;
|
|
return (getPos(ids[idx - 1]!) + getPos(ids[idx + 1]!)) / 2;
|
|
}
|
|
|
|
export function findColumn(
|
|
columns: Record<string, string[]>,
|
|
id: string,
|
|
columnIds: Set<string>,
|
|
): string | null {
|
|
if (columnIds.has(id)) return id;
|
|
for (const [columnId, ids] of Object.entries(columns)) {
|
|
if (ids.includes(id)) return columnId;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function issueMatchesGroup(issue: Issue, group: BoardColumnGroup): boolean {
|
|
if (group.status) return issue.status === group.status;
|
|
return (
|
|
(issue.assignee_type ?? null) === (group.assigneeType ?? null) &&
|
|
(issue.assignee_id ?? null) === (group.assigneeId ?? null)
|
|
);
|
|
}
|
|
|
|
export function getMoveUpdates(
|
|
group: BoardColumnGroup,
|
|
position: number,
|
|
): DragMoveUpdates {
|
|
if (group.status) return { status: group.status, position };
|
|
return {
|
|
assignee_type: group.assigneeType ?? null,
|
|
assignee_id: group.assigneeId ?? null,
|
|
position,
|
|
};
|
|
}
|