diff --git a/app/Entities/Projects/Issue.php b/app/Entities/Projects/Issue.php index 17a31af..2ec06fc 100644 --- a/app/Entities/Projects/Issue.php +++ b/app/Entities/Projects/Issue.php @@ -3,6 +3,7 @@ namespace App\Entities\Projects; use App\Entities\Users\User; +use App\Entities\Projects\Comment; use App\Entities\Projects\Project; use Illuminate\Database\Eloquent\Model; @@ -48,4 +49,14 @@ class Issue extends Model { return ''.$this->status.''; } + + /** + * Issue has many comments relation. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ + public function comments() + { + return $this->morphMany(Comment::class, 'commentable'); + } } diff --git a/app/Http/Controllers/Issues/CommentController.php b/app/Http/Controllers/Issues/CommentController.php new file mode 100644 index 0000000..1b3d312 --- /dev/null +++ b/app/Http/Controllers/Issues/CommentController.php @@ -0,0 +1,83 @@ +authorize('comment-on', $issue); + + $newComment = $request->validate([ + 'body' => 'required|string|max:255', + ]); + + $issue->comments()->create([ + 'body' => $newComment['body'], + 'creator_id' => auth()->id(), + ]); + $issue->touch(); + + flash(__('comment.created'), 'success'); + + return back(); + } + + /** + * Update the specified comment. + * + * @param \Illuminate\Http\Request $request + * @param \App\Entities\Projects\Issue $issue + * @param \App\Entities\Projects\Comment $comment + * @return \Illuminate\Http\Response + */ + public function update(Request $request, Issue $issue, Comment $comment) + { + $this->authorize('update', $comment); + + $commentData = $request->validate([ + 'body' => 'required|string|max:255', + ]); + $comment->update($commentData); + flash(__('comment.updated'), 'success'); + + return redirect()->route('projects.issues.show', [$issue->project, $issue]); + } + + /** + * Remove the specified comment. + * + * @param \App\Entities\Projects\Issue $issue + * @param \\App\Entities\Projects\Comment $comment + * @return \Illuminate\Routing\Redirector + */ + public function destroy(Issue $issue, Comment $comment) + { + $this->authorize('delete', $comment); + + request()->validate([ + 'comment_id' => 'required|exists:comments,id', + ]); + + if (request('comment_id') == $comment->id && $comment->delete()) { + flash(__('comment.deleted'), 'warning'); + + return redirect()->route('projects.issues.show', [$issue->project, $issue]); + } + flash(__('comment.undeleted'), 'error'); + + return back(); + } +} diff --git a/app/Http/Controllers/Projects/IssueController.php b/app/Http/Controllers/Projects/IssueController.php index 1a0c603..99e4463 100644 --- a/app/Http/Controllers/Projects/IssueController.php +++ b/app/Http/Controllers/Projects/IssueController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Projects; use App\Entities\Users\User; use Illuminate\Http\Request; use App\Entities\Projects\Issue; +use App\Entities\Projects\Comment; use App\Entities\Projects\Project; use App\Entities\Projects\Priority; use App\Http\Controllers\Controller; @@ -14,7 +15,11 @@ class IssueController extends Controller { public function index(Project $project) { - $issues = $project->issues()->with(['pic', 'creator'])->get(); + $issues = $project->issues() + ->orderBy('updated_at', 'desc') + ->with(['pic', 'creator']) + ->withCount(['comments']) + ->get(); return view('projects.issues.index', compact('project', 'issues')); } @@ -50,12 +55,19 @@ class IssueController extends Controller public function show(Project $project, Issue $issue) { + $editableComment = null; $priorities = Priority::toArray(); $statuses = IssueStatus::toArray(); $users = User::pluck('name', 'id'); + $comments = $issue->comments()->with('creator')->get(); + + if (request('action') == 'comment-edit' && request('comment_id') != null) { + $editableComment = Comment::find(request('comment_id')); + } return view('projects.issues.show', compact( - 'project', 'issue', 'users', 'statuses', 'priorities' + 'project', 'issue', 'users', 'statuses', 'priorities', 'comments', + 'editableComment' )); } diff --git a/app/Policies/Projects/IssuePolicy.php b/app/Policies/Projects/IssuePolicy.php index 9bd8c8d..7fe975a 100644 --- a/app/Policies/Projects/IssuePolicy.php +++ b/app/Policies/Projects/IssuePolicy.php @@ -14,4 +14,16 @@ class IssuePolicy { return true; } + + /** + * Determine whether the user can add comment to an issue. + * + * @param \App\Entities\Users\User $user + * @param \App\Entities\Projects\Issue $issue + * @return bool + */ + public function commentOn(User $user, Issue $issue) + { + return true; + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 818ae3c..d8823c2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -22,6 +22,7 @@ class AppServiceProvider extends ServiceProvider Relation::morphMap([ 'projects' => 'App\Entities\Projects\Project', + 'issues' => 'App\Entities\Projects\Issue', 'jobs' => 'App\Entities\Projects\Job', ]); } diff --git a/resources/lang/de/app.php b/resources/lang/de/app.php index 2547e4e..6b6d461 100644 --- a/resources/lang/de/app.php +++ b/resources/lang/de/app.php @@ -17,6 +17,7 @@ return [ 'total' => 'gesamt', 'count' => 'Summe', 'remark' => 'Remark', + 'last_update' => 'Last Update', // Action 'add' => 'Hinzufügen', diff --git a/resources/lang/en/app.php b/resources/lang/en/app.php index 2f55d05..2fb3794 100644 --- a/resources/lang/en/app.php +++ b/resources/lang/en/app.php @@ -17,6 +17,7 @@ return [ 'total' => 'Total', 'count' => 'Count', 'remark' => 'Remark', + 'last_update' => 'Last Update', // Action 'add' => 'Add', diff --git a/resources/lang/id/app.php b/resources/lang/id/app.php index 3906fd0..99fffa1 100644 --- a/resources/lang/id/app.php +++ b/resources/lang/id/app.php @@ -17,6 +17,7 @@ return [ 'total' => 'Total', 'count' => 'Jumlah', 'remark' => 'Keterangan', + 'last_update' => 'Update', // Action 'add' => 'Tambah', diff --git a/resources/lang/id/comment.php b/resources/lang/id/comment.php index 95a1586..b79cd50 100644 --- a/resources/lang/id/comment.php +++ b/resources/lang/id/comment.php @@ -12,7 +12,7 @@ return [ 'created' => 'Input Komentar berhasil.', 'edit' => 'Edit Komentar', 'update' => 'Update Komentar', - 'updated' => 'Update data Komentar telah berhasil.', + 'updated' => 'Update Komentar berhasil.', 'delete' => 'Hapus Komentar', 'delete_confirm' => 'Anda yakin akan menghapus Komentar ini?', 'deleted' => 'Komentar berhasil dihapus.', diff --git a/resources/views/projects/issues/index.blade.php b/resources/views/projects/issues/index.blade.php index a582c34..cfcfbde 100755 --- a/resources/views/projects/issues/index.blade.php +++ b/resources/views/projects/issues/index.blade.php @@ -19,8 +19,10 @@ {{ __('issue.title') }} {{ __('issue.priority') }} {{ __('app.status') }} + {{ __('comment.comment') }} {{ __('issue.pic') }} {{ __('issue.creator') }} + {{ __('app.last_update') }} {{ __('app.action') }} @@ -33,8 +35,10 @@ {{ $issue->title }} {!! $issue->priority_label !!} {!! $issue->status_label !!} + {{ $issue->comments_count }} {{ $issue->pic->name }} {{ $issue->creator->name }} + {{ $issue->updated_at->diffForHumans() }} {{ link_to_route( 'projects.issues.show', @@ -45,7 +49,7 @@ @empty - {{ __('issue.empty') }} + {{ __('issue.empty') }} @endforelse diff --git a/resources/views/projects/issues/partials/comment-section.blade.php b/resources/views/projects/issues/partials/comment-section.blade.php new file mode 100644 index 0000000..73eec00 --- /dev/null +++ b/resources/views/projects/issues/partials/comment-section.blade.php @@ -0,0 +1,54 @@ +@foreach($comments as $comment) +
+ + {{ $comment->time_display }} + {{ $comment->creator->name }} + +
+ @can('update', $comment) + {{ link_to_route('projects.issues.show', __('app.edit'), [$project, $issue, 'action' => 'comment-edit', 'comment_id' => $comment->id], ['id' => 'edit-comment-'.$comment->id, 'class' => 'small', 'title' => __('comment.edit')]) }} + @endcan + @can('delete', $comment) + {!! FormField::delete( + ['route' => ['issues.comments.destroy', $issue, $comment], 'class' => ''], + '×', + ['class' => 'btn-link', 'id' => 'delete-comment-'.$comment->id], + ['comment_id' => $comment->id] + ) !!} + @endcan +
+ {!! nl2br($comment->body) !!} +
+@endforeach + +@can('comment-on', $issue) +{{ Form::open(['route' => ['issues.comments.store', $issue]]) }} +{!! FormField::textarea('body', ['required' => true, 'label' => false, 'placeholder' => __('comment.create_text')]) !!} +{{ Form::submit(__('comment.create'), ['class' => 'btn btn-success pull-right']) }} +{{ Form::close() }} +

+@endcan + +@if (Request::get('action') == 'comment-edit' && $editableComment) + +@endif diff --git a/resources/views/projects/issues/show.blade.php b/resources/views/projects/issues/show.blade.php index 42ccb12..beb3017 100755 --- a/resources/views/projects/issues/show.blade.php +++ b/resources/views/projects/issues/show.blade.php @@ -32,6 +32,8 @@ {{ link_to_route('projects.issues.index', __('issue.back_to_index'), [$project], ['class' => 'btn btn-default pull-right']) }} +
+ @include('projects.issues.partials.comment-section')
{{ Form::model($issue, ['route' => ['issues.options.update', $issue], 'method' => 'patch']) }} @@ -50,3 +52,14 @@
@endsection + +@section('script') + +@endsection diff --git a/routes/web/projects.php b/routes/web/projects.php index 45d403f..2f4ae02 100644 --- a/routes/web/projects.php +++ b/routes/web/projects.php @@ -105,3 +105,10 @@ Route::group(['middleware' => ['auth']], function () { * Issue Options Routes */ Route::patch('issues/{issue}/options', 'Issues\OptionController@update')->name('issues.options.update'); + +/** + * Issue Comments Routes + */ +Route::post('issues/{issue}/comments', 'Issues\CommentController@store')->name('issues.comments.store'); +Route::patch('issues/{issue}/comments/{comment}', 'Issues\CommentController@update')->name('issues.comments.update'); +Route::delete('issues/{issue}/comments/{comment}', 'Issues\CommentController@destroy')->name('issues.comments.destroy'); diff --git a/tests/Feature/Projects/IssueCommentsTest.php b/tests/Feature/Projects/IssueCommentsTest.php new file mode 100644 index 0000000..a85c290 --- /dev/null +++ b/tests/Feature/Projects/IssueCommentsTest.php @@ -0,0 +1,106 @@ +adminUserSigningIn(); + $issue = factory(Issue::class)->create(); + $comment = factory(Comment::class)->create([ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'This is issue comment.', + ]); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + + $this->seeText('This is issue comment.'); + } + + /** @test */ + public function admin_can_add_comment_to_an_issue() + { + $admin = $this->adminUserSigningIn(); + $issue = factory(Issue::class)->create(); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + + $this->submitForm(__('comment.create'), [ + 'body' => 'First comment.', + ]); + + $this->seePageIs(route('projects.issues.show', [$issue->project, $issue])); + $this->see(__('comment.created')); + + $this->seeInDatabase('comments', [ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'First comment.', + 'creator_id' => $admin->id, + ]); + } + + /** @test */ + public function user_can_edit_an_issue_comment() + { + $this->adminUserSigningIn(); + $issue = factory(Issue::class)->create(); + $comment = factory(Comment::class)->create([ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'This is issue comment.', + ]); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + $this->seeElement('a', ['id' => 'edit-comment-'.$comment->id]); + $this->click('edit-comment-'.$comment->id); + $this->seeRouteIs('projects.issues.show', [$issue->project, $issue, 'action' => 'comment-edit', 'comment_id' => $comment->id]); + + $this->submitForm(__('comment.update'), [ + 'body' => 'Edited comment.', + ]); + + $this->seePageIs(route('projects.issues.show', [$issue->project, $issue])); + $this->see(__('comment.updated')); + + $this->seeInDatabase('comments', [ + 'id' => $comment->id, + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'Edited comment.', + ]); + } + + /** @test */ + public function user_can_delete_an_issue_comment() + { + $this->adminUserSigningIn(); + $issue = factory(Issue::class)->create(); + $comment = factory(Comment::class)->create([ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'This is issue comment.', + ]); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + $this->seeElement('button', ['id' => 'delete-comment-'.$comment->id]); + $this->press('delete-comment-'.$comment->id); + + $this->seePageIs(route('projects.issues.show', [$issue->project, $issue])); + $this->see(__('comment.deleted')); + + $this->dontSeeInDatabase('comments', [ + 'id' => $comment->id, + ]); + } +} diff --git a/tests/Unit/Models/IssueTest.php b/tests/Unit/Models/IssueTest.php index 7877fdd..a43ed72 100644 --- a/tests/Unit/Models/IssueTest.php +++ b/tests/Unit/Models/IssueTest.php @@ -5,7 +5,9 @@ namespace Tests\Unit\Models; use Tests\TestCase; use App\Entities\Users\User; use App\Entities\Projects\Issue; +use App\Entities\Projects\Comment; use App\Entities\Projects\Project; +use Illuminate\Support\Collection; use App\Entities\Projects\Priority; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -81,4 +83,17 @@ class IssueTest extends TestCase $this->assertEquals(''.$issue->priority.'', $issue->priority_label); } + + /** @test */ + public function an_issue_has_many_comments_relation() + { + $issue = factory(Issue::class)->create(); + $comment = factory(Comment::class)->create([ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + ]); + + $this->assertInstanceOf(Collection::class, $issue->comments); + $this->assertInstanceOf(Comment::class, $issue->comments->first()); + } } diff --git a/tests/Unit/Policies/IssuePolicyTest.php b/tests/Unit/Policies/IssuePolicyTest.php index 051e927..a5b1016 100644 --- a/tests/Unit/Policies/IssuePolicyTest.php +++ b/tests/Unit/Policies/IssuePolicyTest.php @@ -22,4 +22,13 @@ class IssuePolicyTest extends TestCase $this->assertTrue($admin->can('create', new Issue())); } + + /** @test */ + public function admin_can_add_comment_to_an_issue() + { + $admin = $this->createUser('admin'); + $issue = factory(Issue::class)->create(); + + $this->assertTrue($admin->can('comment-on', $issue)); + } }