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]) => {
-
-
-
- - - -
- -
- - - -
- -
-
- - - -
- -
- - - -
-
- - - - - -
-
- -
-
-

Activity

- -
- No activity yet. -
- -
    -
  1. -

    - {{ getEventLabel(event) }} -

    -

    - {{ - formatTimelineDate(event.created_at) - }} -

    -
  2. -
-
- -
-

Discussion

-
- - - {{ - new Date( - task.due_date, - ).toLocaleDateString('en-US', { - day: 'numeric', - month: 'short', - }) - }} - - - - {{ - task?.days_in_column === 0 - ? 'Today' - : `${task?.days_in_column}d` - }} - -
-
- -
-
- - - -
- - -
- -
-
- No comments yet. Start the discussion. -
- - -
-
+ + +
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([