diff --git a/backend/api/v1/tasks/serializers.py b/backend/api/v1/tasks/serializers.py index 86162d16..5a1e4f3d 100644 --- a/backend/api/v1/tasks/serializers.py +++ b/backend/api/v1/tasks/serializers.py @@ -630,6 +630,12 @@ class TaskSerializer(serializers.ModelSerializer): cover = obj.cover except TaskCover.DoesNotExist: cover = None + if ( + cover is not None + and cover.kind == TaskCoverType.CHECKLIST + and not obj.checklist_items.exists() + ): + cover = None payload = ( TaskCoverSerializer(cover, context=self.context).data if cover is not None diff --git a/backend/api/v1/tasks/views.py b/backend/api/v1/tasks/views.py index 2be28bfd..3a52c773 100644 --- a/backend/api/v1/tasks/views.py +++ b/backend/api/v1/tasks/views.py @@ -169,19 +169,22 @@ def normalize_column_positions(column, row=None): Task.objects.filter(id=task_id).update(position=(index + 1) * 1000) -def send_task_checklist_update(task, event, item_id): +def send_task_checklist_update(task, event, item_id, *, cover_cleared=False): + extra = { + "task_id": task.id, + "checklist_total": task.checklist_items.count(), + "checklist_completed": task.checklist_items.filter( + is_completed=True + ).count(), + } + if cover_cleared: + extra["cover"] = None send_task_board_update( task.board, event, "checklist_item", item_id, - extra={ - "task_id": task.id, - "checklist_total": task.checklist_items.count(), - "checklist_completed": task.checklist_items.filter( - is_completed=True - ).count(), - }, + extra=extra, ) @@ -1809,7 +1812,18 @@ class TaskViewSet(viewsets.ModelViewSet): if request.method == "DELETE": item_id = item.id item_title = item.title - item.delete() + with transaction.atomic(): + item.delete() + task._prefetched_objects_cache.pop("checklist_items", None) + cover_cleared = False + if not task.checklist_items.exists(): + cover_cleared, _ = TaskCover.objects.filter( + task=task, + kind=TaskCoverType.CHECKLIST, + ).delete() + cover_cleared = bool(cover_cleared) + if cover_cleared: + task.save(update_fields=["updated_at"]) create_task_activity( task, request.user, @@ -1818,7 +1832,12 @@ class TaskViewSet(viewsets.ModelViewSet): object_id=item_id, metadata={"item_title": item_title}, ) - send_task_checklist_update(task, "checklist_updated", item_id) + send_task_checklist_update( + task, + "checklist_updated", + item_id, + cover_cleared=cover_cleared, + ) return Response(status=status.HTTP_204_NO_CONTENT) serializer = TaskChecklistItemSerializer( diff --git a/backend/tasks/migrations/0034_cleanup_empty_checklist_covers.py b/backend/tasks/migrations/0034_cleanup_empty_checklist_covers.py new file mode 100644 index 00000000..c214c7f1 --- /dev/null +++ b/backend/tasks/migrations/0034_cleanup_empty_checklist_covers.py @@ -0,0 +1,24 @@ +from django.db import migrations + + +def remove_empty_checklist_covers(apps, schema_editor): + TaskChecklistItem = apps.get_model("tasks", "TaskChecklistItem") + TaskCover = apps.get_model("tasks", "TaskCover") + database = schema_editor.connection.alias + task_ids_with_checklists = TaskChecklistItem.objects.using(database).values("task_id") + TaskCover.objects.using(database).filter(kind="checklist").exclude( + task_id__in=task_ids_with_checklists, + ).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("tasks", "0033_universal_task_covers"), + ] + + operations = [ + migrations.RunPython( + remove_empty_checklist_covers, + migrations.RunPython.noop, + ), + ] diff --git a/backend/tests/api/v1/tasks/test_tasks_api.py b/backend/tests/api/v1/tasks/test_tasks_api.py index d03dffeb..06744275 100644 --- a/backend/tests/api/v1/tasks/test_tasks_api.py +++ b/backend/tests/api/v1/tasks/test_tasks_api.py @@ -1738,6 +1738,29 @@ def test_task_cover_supports_checklist_comment_link_and_linked_object( ], } + delete_checklist_response = api_client.delete( + reverse( + "api:v1:tasks:task-checklist-item-detail", + kwargs={"pk": task.id, "item_id": checklist_item.id}, + ) + ) + assert delete_checklist_response.status_code == status.HTTP_204_NO_CONTENT + assert not TaskCover.objects.filter(task=task).exists() + + TaskCover.objects.create( + task=task, + kind=TaskCoverType.CHECKLIST, + created_by=user, + ) + board_response = api_client.get( + reverse("api:v1:tasks:task-board-detail", kwargs={"pk": board.id}) + ) + assert board_response.status_code == status.HTTP_200_OK + serialized_task = next( + item for item in board_response.data["tasks"] if item["id"] == task.id + ) + assert serialized_task["cover"] is None + comment_response = api_client.post( reverse("api:v1:tasks:task-comments", kwargs={"pk": task.id}), {"text": "Комментарий на обложке"}, diff --git a/frontend/src/app/tasks/page.tsx b/frontend/src/app/tasks/page.tsx index 14125000..de52eafa 100644 --- a/frontend/src/app/tasks/page.tsx +++ b/frontend/src/app/tasks/page.tsx @@ -893,6 +893,7 @@ function BoardColumn({ fillAvailableHeight = false, hideOnMobile = false, nested = false, + headerSticky = true, quickTaskComposer, }: { column: TaskColumn; @@ -932,6 +933,7 @@ function BoardColumn({ fillAvailableHeight?: boolean; hideOnMobile?: boolean; nested?: boolean; + headerSticky?: boolean; quickTaskComposer?: ReactNode; }) { const { @@ -981,7 +983,7 @@ function BoardColumn({ }`} >