feat(kanban): combine the columns of all lists by name in "Alle Aufgaben"; block drops into columns a task's list lacks
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m37s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m37s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7860609201
commit
3e4eb9b14f
5 files changed
+122
-47
No files matched your search
+11
-1
@@ -6,6 +6,15 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.40.0] - 2026-11-01
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Kanban in „Alle Aufgaben“: Die Spalten aller Aufgabenlisten werden nach Namen zusammengeführt (Groß- und Kleinschreibung egal), in Reihenfolge und Farbe der ersten Liste, die eine Spalte hat. Gleich benannte Spalten erscheinen einmal, verschieden benannte getrennt, auch bei den Erledigt-Spalten.
|
||||||
|
- Beim Ziehen sind Spalten, die die Liste der Karte nicht hat, ausgegraut und nehmen die Karte nicht an. Die Pfeile auf einer Karte führen zu den Nachbarspalten ihrer eigenen Liste; Status-Auswahl und Detailansicht zeigen wie bisher nur die Spalten der eigenen Liste.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Kanban in „Alle Aufgaben“: Aufgaben in eigenen Spalten einer Liste (z. B. „Review“) fehlten dort ganz, weil nur die Standardspalten angezeigt wurden. Ebenso tauchen Aufgaben, deren Spalte gelöscht wurde, jetzt in der ersten offenen Spalte auf statt nirgends.
|
||||||
|
|
||||||
## [0.39.0] - 2026-11-01
|
## [0.39.0] - 2026-11-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -659,7 +668,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
|
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
|
||||||
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
|
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
|
||||||
|
|
||||||
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.39.0...dev
|
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.40.0...dev
|
||||||
|
[0.40.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.39.0...v0.40.0
|
||||||
[0.39.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.38.1...v0.39.0
|
[0.39.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.38.1...v0.39.0
|
||||||
[0.38.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.38.0...v0.38.1
|
[0.38.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.38.0...v0.38.1
|
||||||
[0.38.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.37.2...v0.38.0
|
[0.38.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.37.2...v0.38.0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](#)
|
[](#)
|
||||||
[](#-schnellstart)
|
[](#-schnellstart)
|
||||||
[](#-deine-daten-gehören-dir)
|
[](#-deine-daten-gehören-dir)
|
||||||
[](CHANGELOG.md)
|
[](CHANGELOG.md)
|
||||||
|
|
||||||
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
|
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.39.0",
|
"version": "0.40.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.39.0",
|
"version": "0.40.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "5.3.0",
|
"@fontsource-variable/inter": "5.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.39.0",
|
"version": "0.40.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
@@ -312,7 +312,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
return () => window.removeEventListener('mindsift:ws', handleWs);
|
return () => window.removeEventListener('mindsift:ws', handleWs);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const safeTaskLists = Array.isArray(taskLists) ? taskLists : [];
|
const safeTaskLists = useMemo(() => (Array.isArray(taskLists) ? taskLists : []), [taskLists]);
|
||||||
const currentSelectedList = safeTaskLists.find((l) => l.id === selectedListId);
|
const currentSelectedList = safeTaskLists.find((l) => l.id === selectedListId);
|
||||||
const isSelectedListReadOnly = currentSelectedList?.permission === 'viewer';
|
const isSelectedListReadOnly = currentSelectedList?.permission === 'viewer';
|
||||||
const isTaskReadOnly = (task: Thought) =>
|
const isTaskReadOnly = (task: Thought) =>
|
||||||
@@ -367,10 +367,65 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
}, [currentSelectedList, globalColumns]);
|
}, [currentSelectedList, globalColumns]);
|
||||||
|
|
||||||
// The columns of the list a task belongs to: in "Alle Aufgaben" every task keeps its own list's columns
|
// The columns of the list a task belongs to: in "Alle Aufgaben" every task keeps its own list's columns
|
||||||
const columnsFor = (task: Thought): KanbanColumn[] => {
|
const columnsFor = useCallback(
|
||||||
|
(task: Thought): KanbanColumn[] => {
|
||||||
const own = safeTaskLists.find((l) => l.id === task.taskListId)?.columns;
|
const own = safeTaskLists.find((l) => l.id === task.taskListId)?.columns;
|
||||||
return Array.isArray(own) && own.length > 0 ? own : activeColumns;
|
return Array.isArray(own) && own.length > 0 ? own : activeColumns;
|
||||||
};
|
},
|
||||||
|
[safeTaskLists, activeColumns]
|
||||||
|
);
|
||||||
|
|
||||||
|
// The column of its own list a task stands in; a status whose column is gone counts as the first open one
|
||||||
|
const ownColumnOf = useCallback(
|
||||||
|
(task: Thought): KanbanColumn | undefined => {
|
||||||
|
const cols = columnsFor(task);
|
||||||
|
if (task.isCompleted) return cols.find((c) => c.isDone && c.id === task.kanbanStatus) || cols.find((c) => c.isDone);
|
||||||
|
return (
|
||||||
|
cols.find((c) => c.id === task.kanbanStatus) ||
|
||||||
|
cols.find((c) => c.id === 'todo') ||
|
||||||
|
cols.find((c) => !c.isDone)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[columnsFor]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Board columns: those of the chosen list; in "Alle Aufgaben" the columns of all lists joined by name
|
||||||
|
// (case aside), in the order and colour of the first list that has them. Their IDs are then "name:…".
|
||||||
|
const isCombinedBoard = selectedListId === 'all';
|
||||||
|
const boardIdOf = useCallback(
|
||||||
|
(col: KanbanColumn) => (isCombinedBoard ? `name:${col.name.trim().toLowerCase()}` : col.id),
|
||||||
|
[isCombinedBoard]
|
||||||
|
);
|
||||||
|
const boardColumns: KanbanColumn[] = useMemo(() => {
|
||||||
|
if (!isCombinedBoard) return activeColumns;
|
||||||
|
const out: KanbanColumn[] = [];
|
||||||
|
const sources = [
|
||||||
|
...safeTaskLists.map((l) => (Array.isArray(l.columns) && l.columns.length > 0 ? l.columns : globalColumns)),
|
||||||
|
globalColumns, // personal tasks
|
||||||
|
];
|
||||||
|
const idOf = (col: KanbanColumn) => `name:${col.name.trim().toLowerCase()}`;
|
||||||
|
for (const cols of sources) {
|
||||||
|
cols.forEach((col, i) => {
|
||||||
|
const id = idOf(col);
|
||||||
|
if (out.some((c) => c.id === id)) return;
|
||||||
|
// A new column goes right before the next column of its list that is already on the board
|
||||||
|
const next = cols.slice(i + 1).map((c) => out.findIndex((o) => o.id === idOf(c))).find((at) => at >= 0);
|
||||||
|
out.splice(next ?? out.length, 0, { ...col, id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}, [isCombinedBoard, activeColumns, safeTaskLists, globalColumns]);
|
||||||
|
|
||||||
|
// The task's own column behind a board column, or null if its list has no column of that name
|
||||||
|
const ownColumnIdFor = (task: Thought | undefined, boardColId: string): string | null =>
|
||||||
|
task ? (columnsFor(task).find((c) => boardIdOf(c) === boardColId)?.id ?? null) : null;
|
||||||
|
const inBoardColumn = useCallback(
|
||||||
|
(task: Thought, boardCol: KanbanColumn) => {
|
||||||
|
const own = ownColumnOf(task);
|
||||||
|
return !!own && boardIdOf(own) === boardCol.id;
|
||||||
|
},
|
||||||
|
[ownColumnOf, boardIdOf]
|
||||||
|
);
|
||||||
|
|
||||||
const safeTasks = useMemo(() => {
|
const safeTasks = useMemo(() => {
|
||||||
if (!Array.isArray(tasks)) return [];
|
if (!Array.isArray(tasks)) return [];
|
||||||
@@ -484,11 +539,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
groups.push({ key: 'tag:none', title: 'Ohne Hashtag', count: withoutTag.length, tasks: withoutTag });
|
groups.push({ key: 'tag:none', title: 'Ohne Hashtag', count: withoutTag.length, tasks: withoutTag });
|
||||||
}
|
}
|
||||||
} else if (groupBy === 'status') {
|
} else if (groupBy === 'status') {
|
||||||
activeColumns.forEach((col) => {
|
boardColumns.forEach((col) => {
|
||||||
const list = filteredTasks.filter((t) => {
|
const list = filteredTasks.filter((t) => inBoardColumn(t, col));
|
||||||
if (col.isDone) return t.isCompleted || t.kanbanStatus === col.id;
|
|
||||||
return !t.isCompleted && (t.kanbanStatus === col.id || (!t.kanbanStatus && col.id === 'todo'));
|
|
||||||
});
|
|
||||||
groups.push({ key: `status:${col.id}`, title: col.name, count: list.length, tasks: list });
|
groups.push({ key: `status:${col.id}`, title: col.name, count: list.length, tasks: list });
|
||||||
});
|
});
|
||||||
} else if (groupBy === 'priority') {
|
} else if (groupBy === 'priority') {
|
||||||
@@ -511,7 +563,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
if (noDue.length > 0) groups.push({ key: 'due:none', title: 'Ohne Fälligkeitsdatum', count: noDue.length, tasks: noDue });
|
if (noDue.length > 0) groups.push({ key: 'due:none', title: 'Ohne Fälligkeitsdatum', count: noDue.length, tasks: noDue });
|
||||||
}
|
}
|
||||||
return groups;
|
return groups;
|
||||||
}, [filteredTasks, groupBy, activeColumns]);
|
}, [filteredTasks, groupBy, boardColumns, inBoardColumn]);
|
||||||
|
|
||||||
// --- TASK ACTIONS ---
|
// --- TASK ACTIONS ---
|
||||||
|
|
||||||
@@ -797,8 +849,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
const title = prompt('Neue Aufgabe für diese Spalte:');
|
const title = prompt('Neue Aufgabe für diese Spalte:');
|
||||||
if (!title || !title.trim()) return;
|
if (!title || !title.trim()) return;
|
||||||
triggerHaptic(10);
|
triggerHaptic(10);
|
||||||
const targetCol = activeColumns.find((c) => c.id === columnId);
|
const targetCol = activeColumns.find((c) => boardIdOf(c) === columnId);
|
||||||
const isCompleted = targetCol ? !!targetCol.isDone : columnId === 'done';
|
if (!targetCol) return;
|
||||||
|
columnId = targetCol.id;
|
||||||
|
const isCompleted = !!targetCol.isDone;
|
||||||
try {
|
try {
|
||||||
const created = await api.captureThought(title.trim(), 'normal', 'manual', undefined, {
|
const created = await api.captureThought(title.trim(), 'normal', 'manual', undefined, {
|
||||||
target: 'task',
|
target: 'task',
|
||||||
@@ -882,7 +936,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
const updatedSourceTask: Thought = {
|
const updatedSourceTask: Thought = {
|
||||||
...sourceTask,
|
...sourceTask,
|
||||||
kanbanStatus: newKanbanCol || targetTask.kanbanStatus || sourceTask.kanbanStatus,
|
kanbanStatus: newKanbanCol || targetTask.kanbanStatus || sourceTask.kanbanStatus,
|
||||||
isCompleted: newKanbanCol ? newKanbanCol === 'done' : targetTask.isCompleted !== undefined ? targetTask.isCompleted : sourceTask.isCompleted,
|
isCompleted: newKanbanCol
|
||||||
|
? !!columnsFor(sourceTask).find((c) => c.id === newKanbanCol)?.isDone || newKanbanCol === 'done'
|
||||||
|
: targetTask.isCompleted !== undefined ? targetTask.isCompleted : sourceTask.isCompleted,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newTasks = [...remainingTasks];
|
const newTasks = [...remainingTasks];
|
||||||
@@ -941,8 +997,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleKanbanColDragOver = (e: React.DragEvent<HTMLDivElement>, colId: string) => {
|
const handleKanbanColDragOver = (e: React.DragEvent<HTMLDivElement>, colId: string) => {
|
||||||
e.preventDefault();
|
|
||||||
if (!draggingTaskId) return;
|
if (!draggingTaskId) return;
|
||||||
|
// A column the card's list does not have takes nothing (no preventDefault = no drop allowed)
|
||||||
|
if (!ownColumnIdFor(tasks.find((t) => t.id === draggingTaskId), colId)) return;
|
||||||
|
e.preventDefault();
|
||||||
if (dragOverKanbanCol !== colId) setDragOverKanbanCol(colId);
|
if (dragOverKanbanCol !== colId) setDragOverKanbanCol(colId);
|
||||||
// Slot = number of cards (without the dragged one) whose middle lies above the pointer
|
// Slot = number of cards (without the dragged one) whose middle lies above the pointer
|
||||||
const cards = Array.from(e.currentTarget.querySelectorAll<HTMLElement>('[data-kanban-task]')).filter(
|
const cards = Array.from(e.currentTarget.querySelectorAll<HTMLElement>('[data-kanban-task]')).filter(
|
||||||
@@ -974,16 +1032,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
const slot = kanbanSlot;
|
const slot = kanbanSlot;
|
||||||
resetKanbanDrag();
|
resetKanbanDrag();
|
||||||
if (!sourceId) return;
|
if (!sourceId) return;
|
||||||
|
const ownColId = ownColumnIdFor(tasks.find((t) => t.id === sourceId), colId);
|
||||||
|
if (!ownColId) return;
|
||||||
// Set with the move in one render: the card shows up marked in its new place, not a moment later
|
// Set with the move in one render: the card shows up marked in its new place, not a moment later
|
||||||
setJustDroppedId(sourceId);
|
setJustDroppedId(sourceId);
|
||||||
window.setTimeout(() => setJustDroppedId((cur) => (cur === sourceId ? null : cur)), 900);
|
window.setTimeout(() => setJustDroppedId((cur) => (cur === sourceId ? null : cur)), 900);
|
||||||
const others = colTasks.filter((t) => t.id !== sourceId);
|
const others = colTasks.filter((t) => t.id !== sourceId);
|
||||||
if (others.length === 0 || !slot || slot.col !== colId) {
|
if (others.length === 0 || !slot || slot.col !== colId) {
|
||||||
await moveTaskToColumn(sourceId, colId);
|
await moveTaskToColumn(sourceId, ownColId);
|
||||||
} else if (slot.index < others.length) {
|
} else if (slot.index < others.length) {
|
||||||
await moveTaskNextTo(sourceId, others[slot.index], 'before', colId);
|
await moveTaskNextTo(sourceId, others[slot.index], 'before', ownColId);
|
||||||
} else {
|
} else {
|
||||||
await moveTaskNextTo(sourceId, others[others.length - 1], 'after', colId);
|
await moveTaskNextTo(sourceId, others[others.length - 1], 'after', ownColId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1009,7 +1069,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
const sourceTask = tasks.find((t) => t.id === sourceId);
|
const sourceTask = tasks.find((t) => t.id === sourceId);
|
||||||
if (!sourceTask) return;
|
if (!sourceTask) return;
|
||||||
|
|
||||||
const isDone = colId === 'done';
|
const targetCol = columnsFor(sourceTask).find((c) => c.id === colId);
|
||||||
|
const isDone = targetCol ? !!targetCol.isDone : colId === 'done';
|
||||||
const updatedSourceTask: Thought = {
|
const updatedSourceTask: Thought = {
|
||||||
...sourceTask,
|
...sourceTask,
|
||||||
kanbanStatus: colId,
|
kanbanStatus: colId,
|
||||||
@@ -1053,14 +1114,17 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
enabled: !canDragTasks && viewMode === 'kanban',
|
enabled: !canDragTasks && viewMode === 'kanban',
|
||||||
onStart: (taskId) => setDraggingTaskId(taskId),
|
onStart: (taskId) => setDraggingTaskId(taskId),
|
||||||
onHover: (colId, target) => {
|
onHover: (colId, target) => {
|
||||||
setDragOverKanbanCol(colId);
|
const allowed = !!colId && !!ownColumnIdFor(tasks.find((t) => t.id === draggingTaskId), colId);
|
||||||
setDragOverTaskTarget(target);
|
setDragOverKanbanCol(allowed ? colId : null);
|
||||||
|
setDragOverTaskTarget(allowed ? target : null);
|
||||||
},
|
},
|
||||||
onDrop: (taskId, colId, target) => {
|
onDrop: (taskId, colId, target) => {
|
||||||
clearDragState();
|
clearDragState();
|
||||||
|
const ownColId = colId ? ownColumnIdFor(tasks.find((t) => t.id === taskId), colId) : null;
|
||||||
|
if (!ownColId) return;
|
||||||
const targetTask = target ? tasks.find((t) => t.id === target.taskId) : undefined;
|
const targetTask = target ? tasks.find((t) => t.id === target.taskId) : undefined;
|
||||||
if (targetTask && target) void moveTaskNextTo(taskId, targetTask, target.position, colId ?? undefined);
|
if (targetTask && target) void moveTaskNextTo(taskId, targetTask, target.position, ownColId);
|
||||||
else if (colId) void moveTaskToColumn(taskId, colId);
|
else void moveTaskToColumn(taskId, ownColId);
|
||||||
},
|
},
|
||||||
onCancel: clearDragState,
|
onCancel: clearDragState,
|
||||||
});
|
});
|
||||||
@@ -2414,17 +2478,13 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
className={`grid grid-cols-1 md:grid-cols-2 gap-3 pt-1 lg:w-[var(--kanban-w)] lg:ml-[var(--kanban-ml)] transition-[width,margin] duration-300 ease-out ${
|
className={`grid grid-cols-1 md:grid-cols-2 gap-3 pt-1 lg:w-[var(--kanban-w)] lg:ml-[var(--kanban-ml)] transition-[width,margin] duration-300 ease-out ${
|
||||||
isKanbanWide ? 'lg:grid-cols-[repeat(var(--kanban-cols),minmax(0,1fr))]' : 'lg:grid-cols-4'
|
isKanbanWide ? 'lg:grid-cols-[repeat(var(--kanban-cols),minmax(0,1fr))]' : 'lg:grid-cols-4'
|
||||||
}`}
|
}`}
|
||||||
style={kanbanBoardStyle(isKanbanWide, activeColumns.length)}
|
style={kanbanBoardStyle(isKanbanWide, boardColumns.length)}
|
||||||
>
|
>
|
||||||
{activeColumns.map((col, colIdx) => {
|
{boardColumns.map((col) => {
|
||||||
const colTasks = kanbanTasks.filter((t) => {
|
const colTasks = kanbanTasks.filter((t) => inBoardColumn(t, col));
|
||||||
if (col.isDone) {
|
// While dragging: a column the card's list does not have is greyed out and takes nothing
|
||||||
return t.isCompleted || t.kanbanStatus === col.id;
|
const blockedForDrag =
|
||||||
}
|
!!draggingTaskId && !ownColumnIdFor(tasks.find((t) => t.id === draggingTaskId), col.id);
|
||||||
if (t.isCompleted) return false;
|
|
||||||
if (t.kanbanStatus) return t.kanbanStatus === col.id;
|
|
||||||
return col.id === 'todo';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cards that take part in the slot count (the dragged one has left its place)
|
// Cards that take part in the slot count (the dragged one has left its place)
|
||||||
const visibleIds = colTasks.filter((t) => t.id !== kanbanHiddenId).map((t) => t.id);
|
const visibleIds = colTasks.filter((t) => t.id !== kanbanHiddenId).map((t) => t.id);
|
||||||
@@ -2439,7 +2499,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
data-kanban-col={col.id}
|
data-kanban-col={col.id}
|
||||||
className={`bg-canvas border border-border flex flex-col min-h-[420px] overflow-hidden transition-all ${
|
className={`bg-canvas border border-border flex flex-col min-h-[420px] overflow-hidden transition-all ${
|
||||||
dragOverKanbanCol === col.id ? 'ring-2 ring-accent/60 bg-accent/5' : ''
|
dragOverKanbanCol === col.id ? 'ring-2 ring-accent/60 bg-accent/5' : ''
|
||||||
}`}
|
} ${blockedForDrag ? 'opacity-40' : ''}`}
|
||||||
|
title={blockedForDrag ? 'Diese Spalte gibt es in der Liste der Aufgabe nicht' : undefined}
|
||||||
>
|
>
|
||||||
{/* Column Header */}
|
{/* Column Header */}
|
||||||
<div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between">
|
<div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between">
|
||||||
@@ -2456,6 +2517,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
const isDragging = draggingTaskId === task.id;
|
const isDragging = draggingTaskId === task.id;
|
||||||
const readOnly = isTaskReadOnly(task);
|
const readOnly = isTaskReadOnly(task);
|
||||||
const visibleIdx = visibleIds.indexOf(task.id);
|
const visibleIdx = visibleIds.indexOf(task.id);
|
||||||
|
const ownCols = columnsFor(task);
|
||||||
|
const ownIdx = ownCols.findIndex((c) => c.id === ownColumnOf(task)?.id);
|
||||||
|
const prevCol = ownIdx > 0 ? ownCols[ownIdx - 1] : undefined;
|
||||||
|
const nextCol = ownIdx >= 0 ? ownCols[ownIdx + 1] : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={task.id}>
|
<React.Fragment key={task.id}>
|
||||||
@@ -2606,23 +2671,23 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={colIdx === 0}
|
disabled={!prevCol}
|
||||||
onClick={() => handleSetKanbanStatus(task, activeColumns[colIdx - 1].id)}
|
onClick={() => prevCol && handleSetKanbanStatus(task, prevCol.id)}
|
||||||
className="text-faint hover:text-main disabled:opacity-20 flex items-center gap-0.5"
|
className="text-faint hover:text-main disabled:opacity-20 flex items-center gap-0.5"
|
||||||
title={colIdx > 0 ? `Nach "${activeColumns[colIdx - 1].name}"` : ''}
|
title={prevCol ? `Nach "${prevCol.name}"` : ''}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-3 h-3" />
|
<ArrowLeft className="w-3 h-3" />
|
||||||
<span className="hidden sm:inline">{colIdx > 0 ? activeColumns[colIdx - 1].name : ''}</span>
|
<span className="hidden sm:inline">{prevCol?.name ?? ''}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={colIdx === activeColumns.length - 1}
|
disabled={!nextCol}
|
||||||
onClick={() => handleSetKanbanStatus(task, activeColumns[colIdx + 1].id)}
|
onClick={() => nextCol && handleSetKanbanStatus(task, nextCol.id)}
|
||||||
className="text-faint hover:text-main disabled:opacity-20 flex items-center gap-0.5"
|
className="text-faint hover:text-main disabled:opacity-20 flex items-center gap-0.5"
|
||||||
title={colIdx < activeColumns.length - 1 ? `Nach "${activeColumns[colIdx + 1].name}"` : ''}
|
title={nextCol ? `Nach "${nextCol.name}"` : ''}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline">{colIdx < activeColumns.length - 1 ? activeColumns[colIdx + 1].name : ''}</span>
|
<span className="hidden sm:inline">{nextCol?.name ?? ''}</span>
|
||||||
<ArrowRight className="w-3 h-3" />
|
<ArrowRight className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2643,7 +2708,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Add Task to Column */}
|
{/* Quick Add Task to Column */}
|
||||||
{!isSelectedListReadOnly && (
|
{!isSelectedListReadOnly && activeColumns.some((c) => boardIdOf(c) === col.id) && (
|
||||||
<div className="p-2 border-t border-border bg-panel">
|
<div className="p-2 border-t border-border bg-panel">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in new issue
Block a user