fix(tasks): remove empty subtask drop-target for childless parents

The dashed empty sub-task drop zone that appeared under childless parent
tasks during a drag was unwanted UI. Remove it along with its supporting
machinery (the subTaskDropCandidate signal in ScheduleExternalDragService,
the pointerdown candidate-arming in TaskListComponent, the
isEmptySubTaskDropTargetMounted computed, and the related template/SCSS).

Consequence: a task can now be nested by dragging only onto a parent that
already has a subtask list. Dragging a subtask back out to a main list
(convertToMainTask) and nesting into an existing subtask list
(convertToSubTask) are unchanged.
This commit is contained in:
Johannes Millan 2026-06-01 18:44:56 +02:00
parent 96b4a2aa69
commit ed43940e09
6 changed files with 24 additions and 106 deletions

View file

@ -5,7 +5,6 @@ import { DragRef } from '@angular/cdk/drag-drop';
@Injectable({ providedIn: 'root' })
export class ScheduleExternalDragService {
private readonly _activeTask = signal<TaskWithSubTasks | null>(null);
private readonly _subTaskDropCandidate = signal<TaskWithSubTasks | null>(null);
private _activeDragRef: DragRef | null = null;
private _cancelNextDrop: boolean = false;
@ -25,14 +24,6 @@ export class ScheduleExternalDragService {
return this._activeDragRef;
}
subTaskDropCandidate(): TaskWithSubTasks | null {
return this._subTaskDropCandidate();
}
setSubTaskDropCandidate(task: TaskWithSubTasks | null): void {
this._subTaskDropCandidate.set(task);
}
setActiveTask(task: TaskWithSubTasks | null, dragRef: DragRef | null = null): void {
this._activeTask.set(task);
this._activeDragRef = dragRef;

View file

@ -87,8 +87,6 @@ describe('TaskListComponent', () => {
useValue: {
activeTask: () => null,
setActiveTask: () => {},
subTaskDropCandidate: () => null,
setSubTaskDropCandidate: () => {},
isCancelNextDrop: () => false,
setCancelNextDrop: () => {},
},

View file

@ -154,7 +154,6 @@ export class TaskListComponent implements OnDestroy, AfterViewInit {
allTasksLength = computed(() => this.tasks()?.length ?? 0);
readonly dropList = viewChild(CdkDropList);
private _clearSubTaskDropCandidate: (() => void) | undefined;
private _clearDragPointerTracking: (() => void) | undefined;
T: typeof T = T;
@ -164,7 +163,6 @@ export class TaskListComponent implements OnDestroy, AfterViewInit {
}
ngOnDestroy(): void {
this._clearSubTaskDropCandidate?.();
this._clearDragPointerTracking?.();
this.dropListService.unregisterDropList(this.dropList()!);
this._scheduleExternalDragService.setActiveTask(null);
@ -176,28 +174,15 @@ export class TaskListComponent implements OnDestroy, AfterViewInit {
}
onDragPointerDown(task: TaskWithSubTasks, event: PointerEvent): void {
this._clearSubTaskDropCandidate?.();
// Seed the pointer position so subtask -> parent-list drags can hit-test
// the source subtask list before the first pointermove (see
// _isPointerOverSubTaskList).
if (task.parentId) {
this.dropListService.setActiveDragPointer({ x: event.clientX, y: event.clientY });
return;
}
if (!canConvertTaskToSubTask(task)) {
return;
}
this._scheduleExternalDragService.setSubTaskDropCandidate(task);
const clearCandidate = (): void => {
this._scheduleExternalDragService.setSubTaskDropCandidate(null);
window.removeEventListener('pointerup', clearCandidate);
window.removeEventListener('pointercancel', clearCandidate);
this._clearSubTaskDropCandidate = undefined;
};
window.addEventListener('pointerup', clearCandidate);
window.addEventListener('pointercancel', clearCandidate);
this._clearSubTaskDropCandidate = clearCandidate;
}
onDragStarted(task: TaskWithSubTasks, event: CdkDragStart): void {
this._clearSubTaskDropCandidate?.();
this._scheduleExternalDragService.setActiveTask(task, event.source._dragRef);
if (task.parentId) {
this._startDragPointerTracking();
@ -205,7 +190,6 @@ export class TaskListComponent implements OnDestroy, AfterViewInit {
}
onDragEnded(): void {
this._clearSubTaskDropCandidate?.();
this._clearDragPointerTracking?.();
this._scheduleExternalDragService.setActiveTask(null);
this.dropListService.setActiveDragPointer(null);

View file

@ -253,42 +253,32 @@
[progress]="progress()"
></progress-bar>
}
@if (t.subTasks?.length || isEmptySubTaskDropTargetMounted()) {
<div
class="sub-tasks"
[class.is-empty-sub-task-drop-target]="!t.subTasks?.length"
>
@if (t.subTasks?.length) {
<button
(click)="toggleSubTaskMode()"
[title]="T.F.TASK.CMP.TOGGLE_SUB_TASK_VISIBILITY | translate"
class="toggle-sub-tasks-btn ico-btn mat-elevation-z2"
color=""
mat-mini-fab
>
@if (t._hideSubTasksMode === ShowSubTasksMode.HideAll) {
<mat-icon>add</mat-icon>
} @else {
<mat-icon
[class.isHideDoneTasks]="
t._hideSubTasksMode === ShowSubTasksMode.HideDone
"
>remove
</mat-icon>
}
</button>
}
@if (t.subTasks?.length) {
<div class="sub-tasks">
<button
(click)="toggleSubTaskMode()"
[title]="T.F.TASK.CMP.TOGGLE_SUB_TASK_VISIBILITY | translate"
class="toggle-sub-tasks-btn ico-btn mat-elevation-z2"
color=""
mat-mini-fab
>
@if (t._hideSubTasksMode === ShowSubTasksMode.HideAll) {
<mat-icon>add</mat-icon>
} @else {
<mat-icon
[class.isHideDoneTasks]="t._hideSubTasksMode === ShowSubTasksMode.HideDone"
>remove
</mat-icon>
}
</button>
<task-list
[@expandInOnly]
[class.empty-sub-task-list]="!t.subTasks?.length"
[isHideAll]="!!t.subTasks?.length && !!t._hideSubTasksMode"
[isHideDone]="
!!t.subTasks?.length && t._hideSubTasksMode === ShowSubTasksMode.HideDone
"
[isHideAll]="!!t._hideSubTasksMode"
[isHideDone]="t._hideSubTasksMode === ShowSubTasksMode.HideDone"
[isSubTaskList]="true"
[listModelId]="t.id"
[parentId]="t.id"
[tasks]="t.subTasks || []"
[tasks]="t.subTasks"
listId="SUB"
></task-list>
</div>

View file

@ -14,30 +14,3 @@
box-shadow: none;
}
}
.sub-tasks.is-empty-sub-task-drop-target {
position: absolute;
left: var(--s3);
right: var(--s);
bottom: -7px;
z-index: 1;
height: 14px;
margin: 0;
opacity: 0.85;
pointer-events: auto;
task-list.empty-sub-task-list {
height: 14px;
min-height: 14px;
padding: 0;
}
task-list.empty-sub-task-list ::ng-deep .task-list-inner {
height: 14px;
min-height: 14px;
border: 1px dashed var(--palette-primary-400);
border-radius: var(--task-border-radius);
background: color-mix(in srgb, var(--palette-primary-400) 18%, transparent);
box-sizing: border-box;
}
}

View file

@ -89,8 +89,6 @@ import { LayoutService } from '../../../core-ui/layout/layout.service';
import { TaskFocusService } from '../task-focus.service';
import { selectTimeConflictTaskIds } from '../store/task.selectors';
import { MatTooltip } from '@angular/material/tooltip';
import { ScheduleExternalDragService } from '../../schedule/schedule-week/schedule-external-drag.service';
import { canConvertTaskToSubTask } from '../util/can-convert-task-to-sub-task';
@Component({
selector: 'task',
@ -148,7 +146,6 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
private readonly _taskFocusService = inject(TaskFocusService);
private readonly _dateService = inject(DateService);
private readonly _destroyRef = inject(DestroyRef);
private readonly _scheduleExternalDragService = inject(ScheduleExternalDragService);
readonly workContextService = inject(WorkContextService);
readonly layoutService = inject(LayoutService);
@ -276,21 +273,6 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
),
);
isEmptySubTaskDropTargetMounted = computed(() => {
const t = this.task();
const dragTask =
this._scheduleExternalDragService.activeTask() ??
this._scheduleExternalDragService.subTaskDropCandidate();
return (
!this.isInSubTaskList() &&
!t.parentId &&
!t.subTasks?.length &&
!!dragTask &&
dragTask.id !== t.id &&
canConvertTaskToSubTask(dragTask)
);
});
hasDeadline = computed(() => {
const t = this.task();
return !!(t.deadlineDay || t.deadlineWithTime);