feat(tasks): split mobile board lanes by column

This commit is contained in:
lizerk 2026-08-07 20:18:59 +03:00
parent 3d92e992af
commit 7a0270fb32

View File

@ -1738,6 +1738,7 @@ type BoardTaskCellProps = TaskColumnCardsProps & {
linkedColumnHighlight: boolean;
synchronizedColumnDrag: boolean;
fillAvailableHeight: boolean;
embeddedInMobileLane?: boolean;
};
function BoardTaskCell({
@ -1746,6 +1747,7 @@ function BoardTaskCell({
linkedColumnHighlight,
synchronizedColumnDrag,
fillAvailableHeight,
embeddedInMobileLane = false,
...cardsProps
}: BoardTaskCellProps) {
const { setNodeRef, isOver } = useDroppable({
@ -1764,12 +1766,14 @@ function BoardTaskCell({
return (
<section
ref={setNodeRef}
className={`tasks-mobile-lane-viewport flex w-full min-w-0 flex-col transition ${
rowId ? "min-h-0" : "min-h-[28rem]"
className={`flex w-full min-w-0 flex-col transition ${
embeddedInMobileLane
? ""
: `tasks-mobile-lane-viewport ${rowId ? "min-h-0" : "min-h-[28rem]"}`
} xl:w-full ${fillAvailableHeight ? "xl:min-h-full" : ""}`}
>
<div
className={`tasks-mobile-lane-scroll tasks-column-scroll min-h-0 flex-1 space-y-2 overflow-y-auto rounded-xl border bg-[var(--surface-primary)] p-3 transition xl:flex-none xl:overflow-visible ${columnBorderClass}`}
className={`${embeddedInMobileLane ? "" : "tasks-mobile-lane-scroll overflow-y-auto xl:flex-none"} tasks-column-scroll min-h-0 flex-1 space-y-2 rounded-xl border bg-[var(--surface-primary)] p-3 transition xl:overflow-visible ${columnBorderClass}`}
>
<TaskColumnCards {...cardsProps} />
</div>
@ -3700,6 +3704,7 @@ function TasksPageContent() {
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [mobileLaneLayout, setMobileLaneLayout] = useState(false);
const [onlyMine, setOnlyMine] = useState(false);
const [desktopWideMode, setDesktopWideMode] = useState(false);
const [selectedColumnTarget, setSelectedColumnTarget] = useState<number | "all">("all");
@ -3897,6 +3902,14 @@ function TasksPageContent() {
);
}, [user?.id]);
useEffect(() => {
const mediaQuery = window.matchMedia("(max-width: 79.999rem)");
const updateMobileLaneLayout = () => setMobileLaneLayout(mediaQuery.matches);
updateMobileLaneLayout();
mediaQuery.addEventListener("change", updateMobileLaneLayout);
return () => mediaQuery.removeEventListener("change", updateMobileLaneLayout);
}, []);
const changeDesktopWideMode = useCallback((enabled: boolean) => {
setDesktopWideMode(enabled);
if (user?.id) {
@ -7738,9 +7751,10 @@ function TasksPageContent() {
const renderTaskCell = (
cellColumn: TaskColumn,
rowId: number | null,
embeddedInMobileLane = false,
) => (
<BoardTaskCell
key={`cell-${cellColumn.id}:${rowId ?? "base"}`}
key={`cell-${cellColumn.id}:${rowId ?? "base"}:${embeddedInMobileLane ? "mobile" : "desktop"}`}
column={cellColumn}
rowId={rowId}
tasks={tasksByPlacement.get(taskPlacementKey(cellColumn.id, rowId)) || []}
@ -7757,6 +7771,7 @@ function TasksPageContent() {
linkedColumnHighlight={activeColumnDragId === cellColumn.id}
synchronizedColumnDrag={activeColumnDragId !== null}
fillAvailableHeight={desktopWideMode && rowId === null}
embeddedInMobileLane={embeddedInMobileLane}
quickTaskComposer={renderQuickTaskComposer(cellColumn, rowId)}
/>
);
@ -7826,31 +7841,85 @@ function TasksPageContent() {
subcolumnHeaders={subcolumnHeaders}
subcolumnGridTemplate={gridTemplateColumns}
>
{baseColumns}
{columnRows.length > 0 ? (
<div className="flex min-w-0 flex-col gap-2">
{columnRows.map((row, rowIndex) => {
const rowTasksCount = filteredTasks.filter((task) => task.row === row.id).length;
{mobileLaneLayout ? (
<div
className="grid w-max min-w-0 gap-2"
style={{ gridTemplateColumns }}
>
{previewCellColumns.map((cellColumn) => {
const baseCellTasksCount = tasksByPlacement.get(
taskPlacementKey(cellColumn.id, null),
)?.length || 0;
return (
<BoardColumnRowView
key={row.id}
row={{ ...row, tasks_count: rowTasksCount }}
gridTemplateColumns={gridTemplateColumns}
onCreateTask={() => {
if (cellColumns[0]) startQuickTask(cellColumns[0].id, row.id);
}}
onEdit={() => openEditColumnRow(row)}
onDelete={() => void deleteColumnRow(row)}
expanded={!boardViewState.collapsed_row_ids.includes(row.id)}
onToggle={() => toggleBoardViewState("row", row.id)}
hasFollowingSection={rowIndex < columnRows.length - 1}
<section
key={`mobile-lane-${cellColumn.id}`}
className="tasks-mobile-lane-viewport flex w-full min-w-0 flex-col"
>
{previewCellColumns.map((item) => renderTaskCell(item, row.id))}
</BoardColumnRowView>
<div className="tasks-mobile-lane-scroll tasks-column-scroll min-h-0 flex-1 space-y-2 overflow-y-auto">
<BoardBaseColumnsView
collapsible={columnRows.length > 0}
tasksCount={baseCellTasksCount}
gridTemplateColumns={TASK_SUBCOLUMN_WIDTH}
expanded={!boardViewState.collapsed_base_column_ids.includes(column.id)}
onToggle={() => toggleBoardViewState("base", column.id)}
hasFollowingSection={columnRows.length > 0}
>
{renderTaskCell(cellColumn, null, true)}
</BoardBaseColumnsView>
{columnRows.map((row, rowIndex) => {
const rowTasksCount = tasksByPlacement.get(
taskPlacementKey(cellColumn.id, row.id),
)?.length || 0;
return (
<BoardColumnRowView
key={`${cellColumn.id}:${row.id}`}
row={{ ...row, tasks_count: rowTasksCount }}
gridTemplateColumns={TASK_SUBCOLUMN_WIDTH}
onCreateTask={() => startQuickTask(cellColumn.id, row.id)}
onEdit={() => openEditColumnRow(row)}
onDelete={() => void deleteColumnRow(row)}
expanded={!boardViewState.collapsed_row_ids.includes(row.id)}
onToggle={() => toggleBoardViewState("row", row.id)}
hasFollowingSection={rowIndex < columnRows.length - 1}
>
{renderTaskCell(cellColumn, row.id, true)}
</BoardColumnRowView>
);
})}
</div>
</section>
);
})}
</div>
) : null}
) : (
<>
{baseColumns}
{columnRows.length > 0 ? (
<div className="flex min-w-0 flex-col gap-2">
{columnRows.map((row, rowIndex) => {
const rowTasksCount = filteredTasks.filter((task) => task.row === row.id).length;
return (
<BoardColumnRowView
key={row.id}
row={{ ...row, tasks_count: rowTasksCount }}
gridTemplateColumns={gridTemplateColumns}
onCreateTask={() => {
if (cellColumns[0]) startQuickTask(cellColumns[0].id, row.id);
}}
onEdit={() => openEditColumnRow(row)}
onDelete={() => void deleteColumnRow(row)}
expanded={!boardViewState.collapsed_row_ids.includes(row.id)}
onToggle={() => toggleBoardViewState("row", row.id)}
hasFollowingSection={rowIndex < columnRows.length - 1}
>
{previewCellColumns.map((item) => renderTaskCell(item, row.id))}
</BoardColumnRowView>
);
})}
</div>
) : null}
</>
)}
</BoardColumnGroup>
);
})}