Statuskarten via Drag&Drop verschiebbar
Unteraufgaben lassen sich verschieben und bearbeiten
Dieser Commit ist enthalten in:
@ -909,26 +909,54 @@ class TaskModalManager {
|
||||
|
||||
clearElement(container);
|
||||
|
||||
this.subtasks.forEach(subtask => {
|
||||
this.subtasks.forEach((subtask, index) => {
|
||||
const item = createElement('div', {
|
||||
className: `subtask-item ${subtask.completed ? 'completed' : ''}`,
|
||||
dataset: { subtaskId: subtask.id }
|
||||
dataset: { subtaskId: subtask.id, position: index },
|
||||
draggable: 'true'
|
||||
}, [
|
||||
// Drag Handle
|
||||
createElement('span', {
|
||||
className: 'subtask-drag-handle',
|
||||
title: 'Ziehen zum Verschieben'
|
||||
}, ['⋮⋮']),
|
||||
// Checkbox
|
||||
createElement('input', {
|
||||
type: 'checkbox',
|
||||
checked: subtask.completed,
|
||||
onchange: () => this.toggleSubtask(subtask.id)
|
||||
}),
|
||||
// Titel (Doppelklick zum Bearbeiten)
|
||||
createElement('span', {
|
||||
className: 'subtask-title'
|
||||
className: 'subtask-title',
|
||||
ondblclick: (e) => this.startEditSubtask(subtask.id, e.target)
|
||||
}, [subtask.title]),
|
||||
createElement('button', {
|
||||
type: 'button',
|
||||
className: 'subtask-delete',
|
||||
onclick: () => this.deleteSubtask(subtask.id)
|
||||
}, ['×'])
|
||||
// Aktionen
|
||||
createElement('div', {
|
||||
className: 'subtask-actions'
|
||||
}, [
|
||||
createElement('button', {
|
||||
type: 'button',
|
||||
className: 'subtask-edit',
|
||||
title: 'Bearbeiten',
|
||||
onclick: (e) => this.startEditSubtask(subtask.id, e.target.closest('.subtask-item').querySelector('.subtask-title'))
|
||||
}, ['✎']),
|
||||
createElement('button', {
|
||||
type: 'button',
|
||||
className: 'subtask-delete',
|
||||
title: 'Löschen',
|
||||
onclick: () => this.deleteSubtask(subtask.id)
|
||||
}, ['×'])
|
||||
])
|
||||
]);
|
||||
|
||||
// Drag & Drop Events
|
||||
item.addEventListener('dragstart', (e) => this.handleSubtaskDragStart(e, subtask.id, index));
|
||||
item.addEventListener('dragover', (e) => this.handleSubtaskDragOver(e));
|
||||
item.addEventListener('dragleave', (e) => this.handleSubtaskDragLeave(e));
|
||||
item.addEventListener('drop', (e) => this.handleSubtaskDrop(e, subtask.id, index));
|
||||
item.addEventListener('dragend', (e) => this.handleSubtaskDragEnd(e));
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
@ -936,6 +964,136 @@ class TaskModalManager {
|
||||
this.updateSubtaskProgress();
|
||||
}
|
||||
|
||||
// Subtask Drag & Drop
|
||||
handleSubtaskDragStart(e, subtaskId, position) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', JSON.stringify({ subtaskId, position }));
|
||||
e.target.classList.add('dragging');
|
||||
this.draggedSubtaskId = subtaskId;
|
||||
}
|
||||
|
||||
handleSubtaskDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
const item = e.target.closest('.subtask-item');
|
||||
if (item && !item.classList.contains('dragging')) {
|
||||
const rect = item.getBoundingClientRect();
|
||||
const midpoint = rect.top + rect.height / 2;
|
||||
item.classList.remove('drag-over-top', 'drag-over-bottom');
|
||||
if (e.clientY < midpoint) {
|
||||
item.classList.add('drag-over-top');
|
||||
} else {
|
||||
item.classList.add('drag-over-bottom');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSubtaskDragLeave(e) {
|
||||
const item = e.target.closest('.subtask-item');
|
||||
if (item) {
|
||||
item.classList.remove('drag-over-top', 'drag-over-bottom');
|
||||
}
|
||||
}
|
||||
|
||||
handleSubtaskDragEnd(e) {
|
||||
e.target.classList.remove('dragging');
|
||||
$$('.subtask-item').forEach(item => {
|
||||
item.classList.remove('drag-over-top', 'drag-over-bottom');
|
||||
});
|
||||
this.draggedSubtaskId = null;
|
||||
}
|
||||
|
||||
async handleSubtaskDrop(e, targetSubtaskId, targetPosition) {
|
||||
e.preventDefault();
|
||||
const item = e.target.closest('.subtask-item');
|
||||
if (item) {
|
||||
item.classList.remove('drag-over-top', 'drag-over-bottom');
|
||||
}
|
||||
|
||||
if (!this.draggedSubtaskId || this.draggedSubtaskId === targetSubtaskId) return;
|
||||
|
||||
const draggedIndex = this.subtasks.findIndex(s => s.id === this.draggedSubtaskId);
|
||||
if (draggedIndex === -1) return;
|
||||
|
||||
// Berechne neue Position basierend auf Drop-Position
|
||||
const rect = item.getBoundingClientRect();
|
||||
const midpoint = rect.top + rect.height / 2;
|
||||
let newPosition = targetPosition;
|
||||
if (e.clientY > midpoint && draggedIndex < targetPosition) {
|
||||
// Nach unten, hinter das Ziel
|
||||
newPosition = targetPosition;
|
||||
} else if (e.clientY > midpoint && draggedIndex > targetPosition) {
|
||||
newPosition = targetPosition + 1;
|
||||
} else if (e.clientY <= midpoint && draggedIndex > targetPosition) {
|
||||
newPosition = targetPosition;
|
||||
} else if (e.clientY <= midpoint && draggedIndex < targetPosition) {
|
||||
newPosition = targetPosition - 1;
|
||||
}
|
||||
|
||||
if (newPosition === draggedIndex) return;
|
||||
|
||||
// Lokale Reihenfolge aktualisieren
|
||||
const [moved] = this.subtasks.splice(draggedIndex, 1);
|
||||
this.subtasks.splice(newPosition, 0, moved);
|
||||
this.renderSubtasks();
|
||||
|
||||
// API-Call
|
||||
if (this.mode === 'edit' && this.taskId) {
|
||||
const projectId = store.get('currentProjectId');
|
||||
try {
|
||||
await api.reorderSubtasks(projectId, this.taskId, this.draggedSubtaskId, newPosition);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Neuordnen der Subtask:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subtask bearbeiten
|
||||
startEditSubtask(subtaskId, titleElement) {
|
||||
const subtask = this.subtasks.find(s => s.id === subtaskId);
|
||||
if (!subtask) return;
|
||||
|
||||
const currentTitle = subtask.title;
|
||||
const input = createElement('input', {
|
||||
type: 'text',
|
||||
className: 'subtask-edit-input',
|
||||
value: currentTitle
|
||||
});
|
||||
|
||||
// Titel durch Input ersetzen
|
||||
titleElement.replaceWith(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
const saveEdit = async () => {
|
||||
const newTitle = input.value.trim();
|
||||
if (newTitle && newTitle !== currentTitle) {
|
||||
subtask.title = newTitle;
|
||||
if (this.mode === 'edit' && this.taskId) {
|
||||
const projectId = store.get('currentProjectId');
|
||||
try {
|
||||
await api.updateSubtask(projectId, this.taskId, subtaskId, { title: newTitle });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Subtask:', error);
|
||||
subtask.title = currentTitle; // Rollback
|
||||
}
|
||||
}
|
||||
}
|
||||
this.renderSubtasks();
|
||||
};
|
||||
|
||||
input.addEventListener('blur', saveEdit);
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
input.blur();
|
||||
} else if (e.key === 'Escape') {
|
||||
subtask.title = currentTitle; // Keine Änderung
|
||||
this.renderSubtasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSubtaskProgress() {
|
||||
const progressContainer = $('#subtask-progress');
|
||||
if (!progressContainer) return;
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren