Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions app/Http/Controllers/TaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,31 @@
use App\Models\Column;
use App\Models\Tag;
use App\Models\User;
use App\Models\TaskComment;
use App\Services\TaskService;
use App\Services\CommentService;
use App\Http\Resources\ColumnResource;
use App\Http\Resources\CommentResource;
use App\Http\Resources\TaskResource;
use Inertia\Inertia;
use Inertia\Response;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\TaskCreateRequest;
use App\Http\Requests\TaskCommentStoreRequest;
use App\Http\Requests\TaskCommentUpdateRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use App\Http\Requests\TaskUpdateRequest;
use App\Http\Requests\TaskListRequest;
use \Illuminate\Http\Response as HttpResponse;

class TaskController extends Controller
{
public function __construct(private TaskService $taskService)
{
public function __construct(
private TaskService $taskService,
private CommentService $commentService
) {
}

public function index(TaskListRequest $request): Response
Expand Down Expand Up @@ -68,7 +75,7 @@ public function index(TaskListRequest $request): Response
]);
}

public function show(Request $request, Task $task): JsonResponse
public function show(Task $task): JsonResponse
{
$this->authorize('view', $task);

Expand All @@ -78,15 +85,26 @@ public function show(Request $request, Task $task): JsonResponse
'assignee:id,name',
'tags:id,name,color',
'taskAttachments',
'comments' => fn($query) => $query
->whereNull('parent_id')
->with(['user:id,name', 'replies']),
'events.actor:id,name',
]);

return response()->json((new TaskResource($task))->resolve());
}

public function indexComments(Task $task): JsonResponse
{
$this->authorize('view', $task);

$comments = $task->comments()
->whereNull('parent_id')
->with(['user:id,name', 'replies.user:id,name'])
->get();

return response()->json([
'comments' => CommentResource::collection($comments)->resolve(),
]);
}

public function store(TaskCreateRequest $request): RedirectResponse
{
$this->taskService->createTask($request->safe()->all(), $request->user());
Expand All @@ -112,18 +130,34 @@ public function destroy(Request $request, Task $task): RedirectResponse
return back();
}

public function storeComment(TaskCommentStoreRequest $request, Task $task): RedirectResponse
public function storeComment(TaskCommentStoreRequest $request, Task $task): HttpResponse|JsonResponse
{
$this->authorize('comment', $task);

$validated = $request->validated();

$task->comments()->create([
'user_id' => $request->user()->id,
'parent_id' => $validated['parent_id'] ?? null,
'body' => $validated['body'],
]);
$this->commentService->createComment($task, $validated, $request->user());

return back();
return response()->noContent();
}

public function updateComment(TaskCommentUpdateRequest $request, Task $task, TaskComment $comment): HttpResponse|JsonResponse
{
$this->authorize('update', $comment);

$validated = $request->validated();

$this->commentService->updateComment($comment, $validated);

return response()->noContent();
}

public function destroyComment(Request $request, Task $task, TaskComment $comment): HttpResponse|JsonResponse
{
$this->authorize('delete', $comment);

$this->commentService->deleteComment($comment);

return response()->noContent();
}
}
20 changes: 20 additions & 0 deletions app/Http/Requests/TaskCommentUpdateRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class TaskCommentUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'body' => ['required', 'string', 'max:2000'],
];
}
}
32 changes: 32 additions & 0 deletions app/Http/Resources/CommentResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class CommentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'body' => $this->body,
'parent_id' => $this->parent_id,
'created_at' => $this->created_at,
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
],
'replies' => $this->whenLoaded(
'replies',
fn() => CommentResource::collection($this->replies)
),
];
}
}
28 changes: 0 additions & 28 deletions app/Http/Resources/TaskResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,6 @@ class TaskResource extends JsonResource
{
private const INDEX_DESCRIPTION_PREVIEW_LIMIT = 100;

/**
* Serialize a comment and its nested replies.
*
* @return array<string, mixed>
*/
private function serializeComment($comment): array
{
return [
'id' => $comment->id,
'body' => $comment->body,
'parent_id' => $comment->parent_id,
'created_at' => $comment->created_at?->toIso8601String(),
'user' => [
'id' => $comment->user->id,
'name' => $comment->user->name,
],
'replies' => $comment->relationLoaded('replies')
? $comment->replies->map(fn($reply) => $this->serializeComment($reply))->values()
: [],
];
}

/**
* Build a short HTML preview that keeps rich text markup intact.
*/
Expand Down Expand Up @@ -165,12 +143,6 @@ public function toArray(Request $request): array
'name' => $this->column->name,
'type' => $this->column->type,
]),
'comments' => $this->whenLoaded(
'comments',
fn() => $this->comments
->map(fn($comment) => $this->serializeComment($comment))
->values()
),
'events' => $this->whenLoaded(
'events',
fn() => $this->events->map(fn($event) => [
Expand Down
25 changes: 25 additions & 0 deletions app/Policies/TaskCommentPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Policies;

use App\Models\User;
use App\Models\TaskComment;

class TaskCommentPolicy
{
/**
* Determine whether the user can update the comment.
*/
public function update(User $user, TaskComment $comment): bool
{
return $user->id === $comment->user_id;
}

/**
* Determine whether the user can delete the comment.
*/
public function delete(User $user, TaskComment $comment): bool
{
return $user->id === $comment->user_id;
}
}
40 changes: 40 additions & 0 deletions app/Services/CommentService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace App\Services;

use App\Models\Task;
use App\Models\TaskComment;
use App\Models\User;

class CommentService
{
/**
* Store a comment or reply.
*/
public function createComment(Task $task, array $data, User $user): TaskComment
{
return $task->comments()->create([
'user_id' => $user->id,
'parent_id' => $data['parent_id'] ?? null,
'body' => $data['body'],
]);
}

/**
* Update a comment.
*/
public function updateComment(TaskComment $comment, array $data): bool
{
return $comment->update([
'body' => $data['body'],
]);
}

/**
* Delete a comment.
*/
public function deleteComment(TaskComment $comment): ?bool
{
return $comment->delete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import type { Tag } from '@/types';

defineProps<{
tag: Tag;
}>();
withDefaults(
defineProps<{
title: string;
description: string;
confirmText?: string;
destructive?: boolean;
}>(),
{
confirmText: 'Delete',
destructive: true,
}
);

const isOpen = defineModel<boolean>('open', { default: false });

Expand All @@ -25,19 +33,18 @@ const emit = defineEmits<{
<Dialog v-model:open="isOpen">
<DialogContent>
<DialogHeader>
<DialogTitle>Delete tag</DialogTitle>
<DialogDescription>
Are you sure you want to delete the tag
<span class="font-medium">"{{ tag.name }}"</span>? Tasks
using this tag will no longer be labelled with it.
</DialogDescription>
<DialogTitle>{{ title }}</DialogTitle>
<DialogDescription>{{ description }}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" @click="isOpen = false">
Cancel
</Button>
<Button variant="destructive" @click="emit('confirm')">
Delete
<Button
:variant="destructive ? 'destructive' : 'default'"
@click="emit('confirm')"
>
{{ confirmText }}
</Button>
</DialogFooter>
</DialogContent>
Expand Down
44 changes: 0 additions & 44 deletions resources/js/components/tasks/ColumnDeleteDialog.vue

This file was deleted.

7 changes: 4 additions & 3 deletions resources/js/components/tasks/KanbanColumn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { computed, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import draggable from 'vuedraggable';
import ColumnDeleteDialog from './ColumnDeleteDialog.vue';
import ConfirmDeleteDialog from '@/components/ConfirmDeleteDialog.vue';
import TaskItem from './TaskItem.vue';

const props = defineProps<{
Expand Down Expand Up @@ -285,9 +285,10 @@ watch(
</div>
</div>

<ColumnDeleteDialog
<ConfirmDeleteDialog
v-model:open="isDeleteColumnOpen"
:column="column"
title="Delete column"
:description="`Are you sure you want to delete the &ldquo;${column.name}&rdquo; column? This action cannot be undone.`"
@confirm="deleteColumn"
/>
</div>
Expand Down
Loading
Loading