diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php
index fa1a00a..378c26e 100644
--- a/app/Http/Controllers/TaskController.php
+++ b/app/Http/Controllers/TaskController.php
@@ -7,12 +7,14 @@
use App\Models\User;
use App\Services\TaskService;
use App\Http\Resources\ColumnResource;
+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 Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use App\Http\Requests\TaskUpdateRequest;
@@ -36,10 +38,6 @@ public function index(Request $request): Response
->with([
'creator:id,name',
'assignee:id,name',
- 'comments' => fn($query) => $query
- ->whereNull('parent_id')
- ->with(['user:id,name', 'replies']),
- 'events.actor:id,name',
])
->orderBy('order')
->paginate(10)
@@ -57,6 +55,24 @@ public function index(Request $request): Response
]);
}
+ public function show(Request $request, Task $task): JsonResponse
+ {
+ if ($task->team_id !== $request->user()->team_id) {
+ abort(403, 'You are not authorized to view this task.');
+ }
+
+ $task->load([
+ 'creator:id,name',
+ 'assignee:id,name',
+ '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 store(TaskCreateRequest $request): RedirectResponse
{
$this->taskService->createTask($request->safe()->all(), $request->user());
diff --git a/resources/js/components/tasks/TaskActivityDiscussionPanel.vue b/resources/js/components/tasks/TaskActivityDiscussionPanel.vue
new file mode 100644
index 0000000..0131b4e
--- /dev/null
+++ b/resources/js/components/tasks/TaskActivityDiscussionPanel.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
diff --git a/resources/js/components/tasks/TaskEditDialog.vue b/resources/js/components/tasks/TaskEditDialog.vue
index c3b232b..55583d4 100644
--- a/resources/js/components/tasks/TaskEditDialog.vue
+++ b/resources/js/components/tasks/TaskEditDialog.vue
@@ -1,39 +1,19 @@
@@ -266,201 +267,42 @@ watch([() => props.task, isOpen], ([task, open]) => {
diff --git a/resources/js/components/tasks/TaskEditFormPanel.vue b/resources/js/components/tasks/TaskEditFormPanel.vue
new file mode 100644
index 0000000..11c70a6
--- /dev/null
+++ b/resources/js/components/tasks/TaskEditFormPanel.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
diff --git a/resources/js/components/tasks/TaskItem.vue b/resources/js/components/tasks/TaskItem.vue
index e97c6f1..cd622be 100644
--- a/resources/js/components/tasks/TaskItem.vue
+++ b/resources/js/components/tasks/TaskItem.vue
@@ -28,18 +28,6 @@ const isOverdue = (task: Task) => {
return task.due_date ? new Date(task.due_date) < new Date() : false;
};
-const descriptionToText = (description?: string | null) => {
- if (!description) return '';
-
- const text = description
- .replace(/<[^>]*>/g, ' ')
- .replace(/ /g, ' ')
- .replace(/\s+/g, ' ')
- .trim();
-
- return text;
-};
-
const deleteTask = () => {
router.delete(destroy(props.task.id).url, {
preserveScroll: true,
@@ -92,11 +80,10 @@ const deleteTask = () => {
@@ -201,11 +188,4 @@ const deleteTask = () => {
.task-description-preview :deep(ol) {
list-style: decimal;
}
-
-.task-description-preview :deep(pre) {
- margin-top: 0.25rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: pre-wrap;
-}
diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts
index 3ab9b03..ee7f59a 100644
--- a/resources/js/types/index.d.ts
+++ b/resources/js/types/index.d.ts
@@ -148,4 +148,14 @@ export interface PaginationLink {
active: boolean;
}
+export interface TaskEditFormState {
+ title: string;
+ description: string;
+ due_date: string;
+ assigned_to: number | null;
+ errors: Record;
+ isDirty: boolean;
+ processing: boolean;
+}
+
export type BreadcrumbItemType = BreadcrumbItem;
diff --git a/routes/tasks.php b/routes/tasks.php
index bca839e..07d69fc 100644
--- a/routes/tasks.php
+++ b/routes/tasks.php
@@ -10,6 +10,7 @@
->group(function () {
Route::controller(TaskController::class)->group(function () {
Route::get('tasks', 'index')->name('tasks.index');
+ Route::get('tasks/{task}', 'show')->name('tasks.show');
Route::post('tasks', 'store')->name('tasks.store');
Route::put('tasks/{task}', 'update')->name('tasks.update');
Route::post('tasks/{task}/comments', 'storeComment')->name('tasks.comments.store');
diff --git a/tests/Feature/TaskCommentTest.php b/tests/Feature/TaskCommentTest.php
index 51f8026..814bb14 100644
--- a/tests/Feature/TaskCommentTest.php
+++ b/tests/Feature/TaskCommentTest.php
@@ -5,7 +5,6 @@
use App\Models\TaskComment;
use App\Models\Team;
use App\Models\User;
-use Inertia\Testing\AssertableInertia as Assert;
beforeEach(function () {
$this->team = Team::factory()->create();
@@ -100,7 +99,7 @@
->assertSessionHasErrors('parent_id');
});
- test('includes threaded replies in tasks page payload', function () {
+ test('includes threaded replies in task details payload', function () {
$task = Task::factory()->create([
'team_id' => $this->team->id,
'column_id' => $this->column->id,
@@ -120,12 +119,9 @@
]);
$this->actingAs($this->user)
- ->get(route('tasks.index'))
- ->assertInertia(
- fn(Assert $page) => $page
- ->component('Tasks')
- ->where('columns.0.tasks.0.comments.0.body', 'Top level comment')
- ->where('columns.0.tasks.0.comments.0.replies.0.body', 'Nested reply')
- );
+ ->get(route('tasks.show', $task))
+ ->assertOk()
+ ->assertJsonPath('comments.0.body', 'Top level comment')
+ ->assertJsonPath('comments.0.replies.0.body', 'Nested reply');
});
});
diff --git a/tests/Feature/TaskTest.php b/tests/Feature/TaskTest.php
index 1d1f58b..d9de02a 100644
--- a/tests/Feature/TaskTest.php
+++ b/tests/Feature/TaskTest.php
@@ -4,6 +4,7 @@
use App\Models\Team;
use App\Models\User;
use App\Models\Column;
+use App\Models\TaskComment;
beforeEach(function () {
$this->team = Team::factory()->create();
@@ -64,6 +65,38 @@ function ($page) use ($column, $creator) {
});
});
+describe('show', function () {
+ test('returns full task details for the same team', function () {
+ $column = Column::create(['team_id' => $this->team->id, 'name' => 'To Do', 'order' => 1]);
+ $task = Task::factory()->create([
+ 'team_id' => $this->team->id,
+ 'column_id' => $column->id,
+ 'created_by' => $this->user->id,
+ ]);
+
+ TaskComment::create([
+ 'task_id' => $task->id,
+ 'user_id' => $this->user->id,
+ 'body' => 'First comment',
+ ]);
+
+ $this->actingAs($this->user)
+ ->get(route('tasks.show', $task))
+ ->assertOk()
+ ->assertJsonPath('id', $task->id)
+ ->assertJsonPath('creator.id', $this->user->id)
+ ->assertJsonPath('comments.0.body', 'First comment');
+ });
+
+ test('forbids showing a task from another team', function () {
+ $task = Task::factory()->create(['team_id' => $this->otherTeam->id]);
+
+ $this->actingAs($this->user)
+ ->get(route('tasks.show', $task))
+ ->assertStatus(403);
+ });
+});
+
describe('store', function () {
beforeEach(function () {
Column::create([