diff --git a/.gitignore b/.gitignore index 5f41a864e..e7e053505 100644 --- a/.gitignore +++ b/.gitignore @@ -8,16 +8,15 @@ Homestead.yaml /public/css /public/js /public/bower +/public/build/ /storage/images _ide_helper.php /storage/debugbar .phpstorm.meta.php yarn.lock /bin +nbproject .buildpath - .project - .settings/org.eclipse.wst.common.project.facet.core.xml - .settings/org.eclipse.php.core.prefs diff --git a/app/Comment.php b/app/Comment.php new file mode 100644 index 000000000..de01b6212 --- /dev/null +++ b/app/Comment.php @@ -0,0 +1,96 @@ +morphTo('entity'); + } + + /** + * Get the page that this comment is in. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function page() + { + return $this->belongsTo(Page::class); + } + + /** + * Get the owner of this comment. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /* + * Not being used, but left here because might be used in the future for performance reasons. + */ + public function getPageComments($pageId) { + $query = static::newQuery(); + $query->join('users AS u', 'comments.created_by', '=', 'u.id'); + $query->leftJoin('users AS u1', 'comments.updated_by', '=', 'u1.id'); + $query->leftJoin('images AS i', 'i.id', '=', 'u.image_id'); + $query->selectRaw('comments.id, text, html, comments.created_by, comments.updated_by, ' + . 'comments.created_at, comments.updated_at, comments.parent_id, ' + . 'u.name AS created_by_name, u1.name AS updated_by_name, ' + . 'i.url AS avatar '); + $query->whereRaw('page_id = ?', [$pageId]); + $query->orderBy('created_at'); + return $query->get(); + } + + public function getAllPageComments($pageId) { + return self::where('page_id', '=', $pageId)->with(['createdBy' => function($query) { + $query->select('id', 'name', 'image_id'); + }, 'updatedBy' => function($query) { + $query->select('id', 'name'); + }, 'createdBy.avatar' => function ($query) { + $query->select('id', 'path', 'url'); + }])->get(); + } + + public function getCommentById($commentId) { + return self::where('id', '=', $commentId)->with(['createdBy' => function($query) { + $query->select('id', 'name', 'image_id'); + }, 'updatedBy' => function($query) { + $query->select('id', 'name'); + }, 'createdBy.avatar' => function ($query) { + $query->select('id', 'path', 'url'); + }])->first(); + } + + public function getCreatedAttribute() { + $created = [ + 'day_time_str' => $this->created_at->toDayDateTimeString(), + 'diff' => $this->created_at->diffForHumans() + ]; + return $created; + } + + public function getUpdatedAttribute() { + if (empty($this->updated_at)) { + return null; + } + $updated = [ + 'day_time_str' => $this->updated_at->toDayDateTimeString(), + 'diff' => $this->updated_at->diffForHumans() + ]; + return $updated; + } + + public function getSubCommentsAttribute() { + return $this->sub_comments; + } +} diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php new file mode 100644 index 000000000..e8d5eab30 --- /dev/null +++ b/app/Http/Controllers/CommentController.php @@ -0,0 +1,99 @@ +entityRepo = $entityRepo; + $this->commentRepo = $commentRepo; + $this->comment = $comment; + parent::__construct(); + } + + public function save(Request $request, $pageId, $commentId = null) + { + $this->validate($request, [ + 'text' => 'required|string', + 'html' => 'required|string', + ]); + + try { + $page = $this->entityRepo->getById('page', $pageId, true); + } catch (ModelNotFoundException $e) { + return response('Not found', 404); + } + + if($page->draft) { + // cannot add comments to drafts. + return response()->json([ + 'status' => 'error', + 'message' => trans('errors.cannot_add_comment_to_draft'), + ], 400); + } + + $this->checkOwnablePermission('page-view', $page); + if (empty($commentId)) { + // create a new comment. + $this->checkPermission('comment-create-all'); + $comment = $this->commentRepo->create($page, $request->only(['text', 'html', 'parent_id'])); + $respMsg = trans('entities.comment_created'); + } else { + // update existing comment + // get comment by ID and check if this user has permission to update. + $comment = $this->comment->findOrFail($commentId); + $this->checkOwnablePermission('comment-update', $comment); + $this->commentRepo->update($comment, $request->all()); + $respMsg = trans('entities.comment_updated'); + } + + $comment = $this->commentRepo->getCommentById($comment->id); + + return response()->json([ + 'status' => 'success', + 'message' => $respMsg, + 'comment' => $comment + ]); + + } + + public function destroy($id) { + $comment = $this->comment->findOrFail($id); + $this->checkOwnablePermission('comment-delete', $comment); + $this->commentRepo->delete($comment); + $updatedComment = $this->commentRepo->getCommentById($comment->id); + + return response()->json([ + 'status' => 'success', + 'message' => trans('entities.comment_deleted'), + 'comment' => $updatedComment + ]); + } + + + public function getPageComments($pageId) { + try { + $page = $this->entityRepo->getById('page', $pageId, true); + } catch (ModelNotFoundException $e) { + return response('Not found', 404); + } + + $this->checkOwnablePermission('page-view', $page); + + $comments = $this->commentRepo->getPageComments($pageId); + return response()->json(['status' => 'success', 'comments'=> $comments['comments'], + 'total' => $comments['total'], 'permissions' => [ + 'comment_create' => $this->currentUser->can('comment-create-all'), + 'comment_update_own' => $this->currentUser->can('comment-update-own'), + 'comment_update_all' => $this->currentUser->can('comment-update-all'), + 'comment_delete_all' => $this->currentUser->can('comment-delete-all'), + 'comment_delete_own' => $this->currentUser->can('comment-delete-own'), + ], 'user_id' => $this->currentUser->id]); + } +} diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index c97597bc4..9a8525c23 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -161,7 +161,7 @@ class PageController extends Controller $pageContent = $this->entityRepo->renderPage($page); $sidebarTree = $this->entityRepo->getBookChildren($page->book); $pageNav = $this->entityRepo->getPageNav($pageContent); - + Views::add($page); $this->setPageTitle($page->getShortName()); return view('pages/show', [ @@ -376,7 +376,7 @@ class PageController extends Controller $page->fill($revision->toArray()); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()])); - + return view('pages/revision', [ 'page' => $page, 'book' => $page->book, diff --git a/app/Page.php b/app/Page.php index c9823e7e4..d722e4e54 100644 --- a/app/Page.php +++ b/app/Page.php @@ -66,6 +66,10 @@ class Page extends Entity return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc'); } + public function comments() { + return $this->hasMany(Comment::class, 'page_id')->orderBy('created_on', 'asc'); + } + /** * Get the url for this page. * @param string|bool $path diff --git a/app/Repos/CommentRepo.php b/app/Repos/CommentRepo.php new file mode 100644 index 000000000..ce71b9234 --- /dev/null +++ b/app/Repos/CommentRepo.php @@ -0,0 +1,105 @@ +comment = $comment; + } + + public function create (Page $page, $data = []) { + $userId = user()->id; + $comment = $this->comment->newInstance(); + $comment->fill($data); + // new comment + $comment->page_id = $page->id; + $comment->created_by = $userId; + $comment->updated_at = null; + $comment->save(); + return $comment; + } + + public function update($comment, $input, $activeOnly = true) { + $userId = user()->id; + $comment->updated_by = $userId; + $comment->fill($input); + + // only update active comments by default. + $whereClause = ['active' => 1]; + if (!$activeOnly) { + $whereClause = []; + } + $comment->update($whereClause); + return $comment; + } + + public function delete($comment) { + $comment->text = trans('entities.comment_deleted'); + $comment->html = trans('entities.comment_deleted'); + $comment->active = false; + $userId = user()->id; + $comment->updated_by = $userId; + $comment->save(); + return $comment; + } + + public function getPageComments($pageId) { + $comments = $this->comment->getAllPageComments($pageId); + $index = []; + $totalComments = count($comments); + $finalCommentList = []; + + // normalizing the response. + for ($i = 0; $i < count($comments); ++$i) { + $comment = $this->normalizeComment($comments[$i]); + $parentId = $comment->parent_id; + if (empty($parentId)) { + $finalCommentList[] = $comment; + $index[$comment->id] = $comment; + continue; + } + + if (empty($index[$parentId])) { + // weird condition should not happen. + continue; + } + if (empty($index[$parentId]->sub_comments)) { + $index[$parentId]->sub_comments = []; + } + array_push($index[$parentId]->sub_comments, $comment); + $index[$comment->id] = $comment; + } + return [ + 'comments' => $finalCommentList, + 'total' => $totalComments + ]; + } + + public function getCommentById($commentId) { + return $this->normalizeComment($this->comment->getCommentById($commentId)); + } + + private function normalizeComment($comment) { + if (empty($comment)) { + return; + } + $comment->createdBy->avatar_url = $comment->createdBy->getAvatar(50); + $comment->createdBy->profile_url = $comment->createdBy->getProfileUrl(); + if (!empty($comment->updatedBy)) { + $comment->updatedBy->profile_url = $comment->updatedBy->getProfileUrl(); + } + return $comment; + } +} \ No newline at end of file diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index c6c981337..93787a3e5 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -468,7 +468,7 @@ class PermissionService $action = end($explodedPermission); $this->currentAction = $action; - $nonJointPermissions = ['restrictions', 'image', 'attachment']; + $nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment']; // Handle non entity specific jointPermissions if (in_array($explodedPermission[0], $nonJointPermissions)) { diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index ebf78d1fa..b03e34b9b 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -70,4 +70,14 @@ $factory->define(BookStack\Image::class, function ($faker) { 'type' => 'gallery', 'uploaded_to' => 0 ]; +}); + +$factory->define(BookStack\Comment::class, function($faker) { + $text = $faker->paragraph(3); + $html = '

' . $text. '

'; + return [ + 'html' => $html, + 'text' => $text, + 'active' => 1 + ]; }); \ No newline at end of file diff --git a/database/migrations/2017_01_01_130541_create_comments_table.php b/database/migrations/2017_01_01_130541_create_comments_table.php new file mode 100644 index 000000000..f4ece31a7 --- /dev/null +++ b/database/migrations/2017_01_01_130541_create_comments_table.php @@ -0,0 +1,112 @@ +increments('id')->unsigned(); + $table->integer('page_id')->unsigned(); + $table->longText('text')->nullable(); + $table->longText('html')->nullable(); + $table->integer('parent_id')->unsigned()->nullable(); + $table->integer('created_by')->unsigned(); + $table->integer('updated_by')->unsigned()->nullable(); + $table->index(['page_id', 'parent_id']); + $table->timestamps(); + + // Get roles with permissions we need to change + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; + + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'Comment'; + foreach ($ops as $op) { + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); + } + + // Get roles with permissions we need to change + /* + $editorRole = DB::table('roles')->where('name', '=', 'editor')->first(); + if (!empty($editorRole)) { + $editorRoleId = $editorRole->id; + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update Own', 'Delete Own']; + $entity = 'Comment'; + foreach ($ops as $op) { + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $editorRoleId, + 'permission_id' => $permissionId + ]); + } + } + + // Get roles with permissions we need to change + $viewerRole = DB::table('roles')->where('name', '=', 'viewer')->first(); + if (!empty($viewerRole)) { + $viewerRoleId = $viewerRole->id; + // Create & attach new entity permissions + $ops = ['Create All']; + $entity = 'Comment'; + foreach ($ops as $op) { + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $viewerRoleId, + 'permission_id' => $permissionId + ]); + } + } + */ + + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('comments'); + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'Comment'; + foreach ($ops as $op) { + $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); + DB::table('role_permissions')->where('name', '=', $permName)->delete(); + } + } +} diff --git a/database/migrations/2017_06_04_060012_comments_add_active_col.php b/database/migrations/2017_06_04_060012_comments_add_active_col.php new file mode 100644 index 000000000..3c6dd1f33 --- /dev/null +++ b/database/migrations/2017_06_04_060012_comments_add_active_col.php @@ -0,0 +1,38 @@ +boolean('active')->default(true); + $table->dropIndex('comments_page_id_parent_id_index'); + $table->index(['page_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('comments', function (Blueprint $table) { + // reversing the schema + $table->dropIndex('comments_page_id_index'); + $table->dropColumn('active'); + $table->index(['page_id', 'parent_id']); + }); + } +} diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php index 3d92efab1..996cd178d 100644 --- a/database/seeds/DummyContentSeeder.php +++ b/database/seeds/DummyContentSeeder.php @@ -20,7 +20,10 @@ class DummyContentSeeder extends Seeder ->each(function($book) use ($user) { $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) ->each(function($chapter) use ($user, $book){ - $pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); + $pages = factory(\BookStack\Page::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id])->each(function($page) use ($user) { + $comments = factory(\BookStack\Comment::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'page_id' => $page->id]); + $page->comments()->saveMany($comments); + }); $chapter->pages()->saveMany($pages); }); $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]); diff --git a/gulpfile.js b/gulpfile.js index 08c8886bd..f851dd7d6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,3 +1,5 @@ +'use strict'; + const argv = require('yargs').argv; const gulp = require('gulp'), plumber = require('gulp-plumber'); diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 9337ea889..e1d838bb6 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -675,4 +675,225 @@ module.exports = function (ngApp, events) { }]); + // Controller used to reply to and add new comments + ngApp.controller('CommentReplyController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { + const MarkdownIt = require("markdown-it"); + const md = new MarkdownIt({html: true}); + let vm = this; + + vm.saveComment = function () { + let pageId = $scope.comment.pageId || $scope.pageId; + let comment = $scope.comment.text; + if (!comment) { + return events.emit('warning', trans('errors.empty_comment')); + } + let commentHTML = md.render($scope.comment.text); + let serviceUrl = `/ajax/page/${pageId}/comment/`; + let httpMethod = 'post'; + let reqObj = { + text: comment, + html: commentHTML + }; + + if ($scope.isEdit === true) { + // this will be set when editing the comment. + serviceUrl = `/ajax/page/${pageId}/comment/${$scope.comment.id}`; + httpMethod = 'put'; + } else if ($scope.isReply === true) { + // if its reply, get the parent comment id + reqObj.parent_id = $scope.parentId; + } + $http[httpMethod](window.baseUrl(serviceUrl), reqObj).then(resp => { + if (!isCommentOpSuccess(resp)) { + return; + } + // hide the comments first, and then retrigger the refresh + if ($scope.isEdit) { + updateComment($scope.comment, resp.data); + $scope.$emit('evt.comment-success', $scope.comment.id); + } else { + $scope.comment.text = ''; + if ($scope.isReply === true && $scope.parent.sub_comments) { + $scope.parent.sub_comments.push(resp.data.comment); + } else { + $scope.$emit('evt.new-comment', resp.data.comment); + } + $scope.$emit('evt.comment-success', null, true); + } + $scope.comment.is_hidden = true; + $timeout(function() { + $scope.comment.is_hidden = false; + }); + + events.emit('success', trans(resp.data.message)); + + }, checkError); + + }; + + function checkError(response) { + let msg = null; + if (isCommentOpSuccess(response)) { + // all good + return; + } else if (response.data) { + msg = response.data.message; + } else { + msg = trans('errors.comment_add'); + } + if (msg) { + events.emit('success', msg); + } + } + }]); + + // Controller used to delete comments + ngApp.controller('CommentDeleteController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { + let vm = this; + + vm.delete = function(comment) { + $http.delete(window.baseUrl(`/ajax/comment/${comment.id}`)).then(resp => { + if (!isCommentOpSuccess(resp)) { + return; + } + updateComment(comment, resp.data, $timeout, true); + }, function (resp) { + if (isCommentOpSuccess(resp)) { + events.emit('success', trans('entities.comment_deleted')); + } else { + events.emit('error', trans('error.comment_delete')); + } + }); + }; + }]); + + // Controller used to fetch all comments for a page + ngApp.controller('CommentListController', ['$scope', '$http', '$timeout', '$location', function ($scope, $http, $timeout, $location) { + let vm = this; + $scope.errors = {}; + // keep track of comment levels + $scope.level = 1; + vm.totalCommentsStr = trans('entities.comments_loading'); + vm.permissions = {}; + vm.trans = window.trans; + + $scope.$on('evt.new-comment', function (event, comment) { + // add the comment to the comment list. + vm.comments.push(comment); + ++vm.totalComments; + setTotalCommentMsg(); + event.stopPropagation(); + event.preventDefault(); + }); + + vm.canEditDelete = function (comment, prop) { + if (!comment.active) { + return false; + } + let propAll = prop + '_all'; + let propOwn = prop + '_own'; + + if (vm.permissions[propAll]) { + return true; + } + + if (vm.permissions[propOwn] && comment.created_by.id === vm.current_user_id) { + return true; + } + + return false; + }; + + vm.canComment = function () { + return vm.permissions.comment_create; + }; + + // check if there are is any direct linking + let linkedCommentId = $location.search().cm; + + $timeout(function() { + $http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/`)).then(resp => { + if (!isCommentOpSuccess(resp)) { + // just show that no comments are available. + vm.totalComments = 0; + setTotalCommentMsg(); + return; + } + vm.comments = resp.data.comments; + vm.totalComments = +resp.data.total; + vm.permissions = resp.data.permissions; + vm.current_user_id = resp.data.user_id; + setTotalCommentMsg(); + if (!linkedCommentId) { + return; + } + $timeout(function() { + // wait for the UI to render. + focusLinkedComment(linkedCommentId); + }); + }, checkError); + }); + + function setTotalCommentMsg () { + if (vm.totalComments === 0) { + vm.totalCommentsStr = trans('entities.no_comments'); + } else if (vm.totalComments === 1) { + vm.totalCommentsStr = trans('entities.one_comment'); + } else { + vm.totalCommentsStr = trans('entities.x_comments', { + numComments: vm.totalComments + }); + } + } + + function focusLinkedComment(linkedCommentId) { + let comment = angular.element('#' + linkedCommentId); + if (comment.length === 0) { + return; + } + + window.setupPageShow.goToText(linkedCommentId); + } + + function checkError(response) { + let msg = null; + if (isCommentOpSuccess(response)) { + // all good + return; + } else if (response.data) { + msg = response.data.message; + } else { + msg = trans('errors.comment_list'); + } + if (msg) { + events.emit('success', msg); + } + } + }]); + + function updateComment(comment, resp, $timeout, isDelete) { + comment.text = resp.comment.text; + comment.updated = resp.comment.updated; + comment.updated_by = resp.comment.updated_by; + comment.active = resp.comment.active; + if (isDelete && !resp.comment.active) { + comment.html = trans('entities.comment_deleted'); + } else { + comment.html = resp.comment.html; + } + if (!$timeout) { + return; + } + comment.is_hidden = true; + $timeout(function() { + comment.is_hidden = false; + }); + } + + function isCommentOpSuccess(resp) { + if (resp && resp.data && resp.data.status === 'success') { + return true; + } + return false; + } }; diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 8d7d89cee..d8745462d 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -863,4 +863,128 @@ module.exports = function (ngApp, events) { } }; }]); + + ngApp.directive('commentReply', [function () { + return { + restrict: 'E', + templateUrl: 'comment-reply.html', + scope: { + pageId: '=', + parentId: '=', + parent: '=' + }, + link: function (scope, element) { + scope.isReply = true; + element.find('textarea').focus(); + scope.$on('evt.comment-success', function (event) { + // no need for the event to do anything more. + event.stopPropagation(); + event.preventDefault(); + scope.closeBox(); + }); + + scope.closeBox = function () { + element.remove(); + scope.$destroy(); + }; + } + }; + }]); + + ngApp.directive('commentEdit', [function () { + return { + restrict: 'E', + templateUrl: 'comment-reply.html', + scope: { + comment: '=' + }, + link: function (scope, element) { + scope.isEdit = true; + element.find('textarea').focus(); + scope.$on('evt.comment-success', function (event, commentId) { + // no need for the event to do anything more. + event.stopPropagation(); + event.preventDefault(); + if (commentId === scope.comment.id && !scope.isNew) { + scope.closeBox(); + } + }); + + scope.closeBox = function () { + element.remove(); + scope.$destroy(); + }; + } + }; + }]); + + + ngApp.directive('commentReplyLink', ['$document', '$compile', function ($document, $compile) { + return { + scope: { + comment: '=' + }, + link: function (scope, element, attr) { + element.on('$destroy', function () { + element.off('click'); + scope.$destroy(); + }); + + element.on('click', function (e) { + e.preventDefault(); + var $container = element.parents('.comment-actions').first(); + if (!$container.length) { + console.error('commentReplyLink directive should be placed inside a container with class comment-box!'); + return; + } + if (attr.noCommentReplyDupe) { + removeDupe(); + } + + compileHtml($container, scope, attr.isReply === 'true'); + }); + } + }; + + function compileHtml($container, scope, isReply) { + let lnkFunc = null; + if (isReply) { + lnkFunc = $compile(''); + } else { + lnkFunc = $compile(''); + } + var compiledHTML = lnkFunc(scope); + $container.append(compiledHTML); + } + + function removeDupe() { + let $existingElement = $document.find('.comments-list comment-reply, .comments-list comment-edit'); + if (!$existingElement.length) { + return; + } + + $existingElement.remove(); + } + }]); + + ngApp.directive('commentDeleteLink', ['$window', function ($window) { + return { + controller: 'CommentDeleteController', + scope: { + comment: '=' + }, + link: function (scope, element, attr, ctrl) { + + element.on('click', function(e) { + e.preventDefault(); + var resp = $window.confirm(trans('entities.comment_delete_confirm')); + if (!resp) { + return; + } + + ctrl.delete(scope.comment); + }); + } + }; + }]); }; diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js index 67d339d63..020229d2f 100644 --- a/resources/assets/js/pages/page-show.js +++ b/resources/assets/js/pages/page-show.js @@ -161,6 +161,8 @@ let setupPageShow = window.setupPageShow = function (pageId) { } }); + // in order to call from other places. + window.setupPageShow.goToText = goToText; }; module.exports = setupPageShow; \ No newline at end of file diff --git a/resources/assets/sass/_comments.scss b/resources/assets/sass/_comments.scss new file mode 100644 index 000000000..5da53a14d --- /dev/null +++ b/resources/assets/sass/_comments.scss @@ -0,0 +1,82 @@ +.comments-list { + .comment-box { + border-bottom: 1px solid $comment-border; + } + + .comment-box:last-child { + border-bottom: 0px; + } +} +.page-comment { + .comment-container { + margin-left: 42px; + } + + .comment-actions { + font-size: 0.8em; + padding-bottom: 2px; + + ul { + padding-left: 0px; + margin-bottom: 2px; + } + li { + float: left; + list-style-type: none; + } + + li:after { + content: '•'; + color: #707070; + padding: 0 5px; + font-size: 1em; + } + + li:last-child:after { + content: none; + } + } + + .comment-actions { + border-bottom: 1px solid #DDD; + } + + .comment-actions:last-child { + border-bottom: 0px; + } + + .comment-header { + font-size: 1.25em; + margin-top: 0.6em; + } + + .comment-body p { + margin-bottom: 1em; + } + + .comment-inactive { + font-style: italic; + font-size: 0.85em; + padding-top: 5px; + } + + .user-image { + float: left; + margin-right: 10px; + width: 32px; + img { + width: 100%; + } + } +} + +.comment-editor { + margin-top: 2em; + + textarea { + display: block; + width: 100%; + max-width: 100%; + min-height: 120px; + } +} diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index e5334c69c..b06892c1d 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -310,4 +310,8 @@ background-color: #EEE; } } +} + +.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll { + min-height: 175px; } \ No newline at end of file diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index 23bf2b219..3e864aaa4 100644 --- a/resources/assets/sass/_variables.scss +++ b/resources/assets/sass/_variables.scss @@ -56,3 +56,6 @@ $text-light: #EEE; $bs-light: 0 0 4px 1px #CCC; $bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26); $bs-hover: 0 2px 2px 1px rgba(0,0,0,.13); + +// comments +$comment-border: #DDD; \ No newline at end of file diff --git a/resources/assets/sass/export-styles.scss b/resources/assets/sass/export-styles.scss index 60450f3e2..72b5b16b5 100644 --- a/resources/assets/sass/export-styles.scss +++ b/resources/assets/sass/export-styles.scss @@ -10,6 +10,7 @@ @import "header"; @import "lists"; @import "pages"; +@import "comments"; table { border-spacing: 0; diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index afb9d531b..3b279b8bd 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -16,6 +16,7 @@ @import "header"; @import "lists"; @import "pages"; +@import "comments"; [v-cloak], [v-show] { display: none; opacity: 0; diff --git a/resources/lang/de/entities.php b/resources/lang/de/entities.php index c9feb8497..5d7d5cdde 100644 --- a/resources/lang/de/entities.php +++ b/resources/lang/de/entities.php @@ -213,4 +213,27 @@ return [ 'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.', 'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.', 'profile_not_created_books' => ':userName hat bisher keine Bücher angelegt.', + + /** + * Comnents + */ + 'comment' => 'Kommentar', + 'comments' => 'Kommentare', + 'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein, Markdown unterstützt ...', + 'no_comments' => 'Keine Kommentare', + 'x_comments' => ':numComments Kommentare', + 'one_comment' => '1 Kommentar', + 'comments_loading' => 'Laden ...', + 'comment_save' => 'Kommentar speichern', + 'comment_reply' => 'Antworten', + 'comment_edit' => 'Bearbeiten', + 'comment_delete' => 'Löschen', + 'comment_cancel' => 'Abbrechen', + 'comment_created' => 'Kommentar hinzugefügt', + 'comment_updated' => 'Kommentar aktualisiert', + 'comment_deleted' => 'Kommentar gelöscht', + 'comment_updated_text' => 'Aktualisiert vor :updateDiff von', + 'comment_delete_confirm' => 'Damit wird der Inhalt des Kommentars entfernt. Bist du sicher, dass du diesen Kommentar löschen möchtest?', + 'comment_create' => 'Erstellt' + ]; \ No newline at end of file diff --git a/resources/lang/de/errors.php b/resources/lang/de/errors.php index e085d9915..ff045d628 100644 --- a/resources/lang/de/errors.php +++ b/resources/lang/de/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Es ist ein Fehler aufgetreten', 'app_down' => ':appName befindet sich aktuell im Wartungsmodus.', 'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.', + + // Comments + 'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.', + 'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.', + 'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.', + 'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.', + 'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen', ]; diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 450f4ce48..43053df10 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -234,4 +234,27 @@ return [ 'profile_not_created_pages' => ':userName has not created any pages', 'profile_not_created_chapters' => ':userName has not created any chapters', 'profile_not_created_books' => ':userName has not created any books', + + /** + * Comments + */ + 'comment' => 'Comment', + 'comments' => 'Comments', + 'comment_placeholder' => 'Enter your comments here, markdown supported...', + 'no_comments' => 'No Comments', + 'x_comments' => ':numComments Comments', + 'one_comment' => '1 Comment', + 'comments_loading' => 'Loading...', + 'comment_save' => 'Save Comment', + 'comment_reply' => 'Reply', + 'comment_edit' => 'Edit', + 'comment_delete' => 'Delete', + 'comment_cancel' => 'Cancel', + 'comment_created' => 'Comment added', + 'comment_updated' => 'Comment updated', + 'comment_deleted' => 'Comment deleted', + 'comment_updated_text' => 'Updated :updateDiff by', + 'comment_delete_confirm' => 'This will remove the contents of the comment. Are you sure you want to delete this comment?', + 'comment_create' => 'Created' + ]; \ No newline at end of file diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index c4578a37a..71bcd1f9a 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -60,6 +60,13 @@ return [ 'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted', 'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role', + // Comments + 'comment_list' => 'An error occurred while fetching the comments.', + 'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.', + 'comment_add' => 'An error occurred while adding the comment.', + 'comment_delete' => 'An error occurred while deleting the comment.', + 'empty_comment' => 'Cannot add an empty comment.', + // Error pages '404_page_not_found' => 'Page Not Found', 'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.', diff --git a/resources/lang/es/entities.php b/resources/lang/es/entities.php index d6b2810bc..2ca55a786 100644 --- a/resources/lang/es/entities.php +++ b/resources/lang/es/entities.php @@ -214,4 +214,26 @@ return [ 'profile_not_created_pages' => ':userName no ha creado ninguna página', 'profile_not_created_chapters' => ':userName no ha creado ningún capítulo', 'profile_not_created_books' => ':userName no ha creado ningún libro', + + /** + * Comments + */ + 'comment' => 'Comentario', + 'comments' => 'Comentarios', + 'comment_placeholder' => 'Introduzca sus comentarios aquí, markdown supported ...', + 'no_comments' => 'No hay comentarios', + 'x_comments' => ':numComments Comentarios', + 'one_comment' => '1 Comentario', + 'comments_loading' => 'Cargando ...', + 'comment_save' => 'Guardar comentario', + 'comment_reply' => 'Responder', + 'comment_edit' => 'Editar', + 'comment_delete' => 'Eliminar', + 'comment_cancel' => 'Cancelar', + 'comment_created' => 'Comentario añadido', + 'comment_updated' => 'Comentario actualizado', + 'comment_deleted' => 'Comentario eliminado', + 'comment_updated_text' => 'Actualizado hace :updateDiff por', + 'comment_delete_confirm' => 'Esto eliminará el contenido del comentario. ¿Estás seguro de que quieres eliminar este comentario?', + 'comment_create' => 'Creado' ]; diff --git a/resources/lang/es/errors.php b/resources/lang/es/errors.php index 1e39a3cb8..e488b6a1b 100644 --- a/resources/lang/es/errors.php +++ b/resources/lang/es/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Ha ocurrido un error', 'app_down' => 'La aplicación :appName se encuentra caída en este momento', 'back_soon' => 'Volverá a estar operativa en corto tiempo.', + + // Comments + 'comment_list' => 'Se ha producido un error al buscar los comentarios.', + 'cannot_add_comment_to_draft' => 'No puedes añadir comentarios a un borrador.', + 'comment_add' => 'Se ha producido un error al añadir el comentario.', + 'comment_delete' => 'Se ha producido un error al eliminar el comentario.', + 'empty_comment' => 'No se puede agregar un comentario vacío.', ]; diff --git a/resources/lang/fr/entities.php b/resources/lang/fr/entities.php index 17b4ea913..0d89993e9 100644 --- a/resources/lang/fr/entities.php +++ b/resources/lang/fr/entities.php @@ -213,4 +213,26 @@ return [ 'profile_not_created_pages' => ':userName n\'a pas créé de page', 'profile_not_created_chapters' => ':userName n\'a pas créé de chapitre', 'profile_not_created_books' => ':userName n\'a pas créé de livre', + + /** + * Comments + */ + 'comment' => 'Commentaire', + 'comments' => 'Commentaires', + 'comment_placeholder' => 'Entrez vos commentaires ici, merci supporté ...', + 'no_comments' => 'No Comments', + 'x_comments' => ':numComments Commentaires', + 'one_comment' => '1 Commentaire', + 'comments_loading' => 'Loading ...', + 'comment_save' => 'Enregistrer le commentaire', + 'comment_reply' => 'Répondre', + 'comment_edit' => 'Modifier', + 'comment_delete' => 'Supprimer', + 'comment_cancel' => 'Annuler', + 'comment_created' => 'Commentaire ajouté', + 'comment_updated' => 'Commentaire mis à jour', + 'comment_deleted' => 'Commentaire supprimé', + 'comment_updated_text' => 'Mis à jour il y a :updateDiff par', + 'comment_delete_confirm' => 'Cela supprime le contenu du commentaire. Êtes-vous sûr de vouloir supprimer ce commentaire?', + 'comment_create' => 'Créé' ]; diff --git a/resources/lang/fr/errors.php b/resources/lang/fr/errors.php index 4197b1708..9e20147b6 100644 --- a/resources/lang/fr/errors.php +++ b/resources/lang/fr/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Une erreur est survenue', 'app_down' => ':appName n\'est pas en service pour le moment', 'back_soon' => 'Nous serons bientôt de retour.', + + // comments + 'comment_list' => 'Une erreur s\'est produite lors de la récupération des commentaires.', + 'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un projet.', + 'comment_add' => 'Une erreur s\'est produite lors de l\'ajout du commentaire.', + 'comment_delete' => 'Une erreur s\'est produite lors de la suppression du commentaire.', + 'empty_comment' => 'Impossible d\'ajouter un commentaire vide.', ]; diff --git a/resources/lang/nl/entities.php b/resources/lang/nl/entities.php index d6975e130..6df9e5dd9 100644 --- a/resources/lang/nl/entities.php +++ b/resources/lang/nl/entities.php @@ -214,4 +214,26 @@ return [ 'profile_not_created_pages' => ':userName heeft geen pagina\'s gemaakt', 'profile_not_created_chapters' => ':userName heeft geen hoofdstukken gemaakt', 'profile_not_created_books' => ':userName heeft geen boeken gemaakt', + + /** + * Comments + */ + 'comment' => 'Commentaar', + 'comments' => 'Commentaren', + 'comment_placeholder' => 'Vul hier uw reacties in, markdown ondersteund ...', + 'no_comments' => 'No Comments', + 'x_comments' => ':numComments Opmerkingen', + 'one_comment' => '1 commentaar', + 'comments_loading' => 'Loading ...', + 'comment_save' => 'Opslaan opslaan', + 'comment_reply' => 'Antwoord', + 'comment_edit' => 'Bewerken', + 'comment_delete' => 'Verwijderen', + 'comment_cancel' => 'Annuleren', + 'comment_created' => 'Opmerking toegevoegd', + 'comment_updated' => 'Opmerking bijgewerkt', + 'comment_deleted' => 'Opmerking verwijderd', + 'comment_updated_text' => 'Bijgewerkt :updateDiff geleden door', + 'comment_delete_confirm' => 'Hiermee verwijdert u de inhoud van de reactie. Weet u zeker dat u deze reactie wilt verwijderen?', + 'comment_create' => 'Gemaakt' ]; \ No newline at end of file diff --git a/resources/lang/nl/errors.php b/resources/lang/nl/errors.php index f8b635bce..b8fab59fd 100644 --- a/resources/lang/nl/errors.php +++ b/resources/lang/nl/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Er Ging Iets Fout', 'app_down' => ':appName is nu niet beschikbaar', 'back_soon' => 'Komt snel weer online.', + + // Comments + 'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de reacties.', + 'cannot_add_comment_to_draft' => 'U kunt geen reacties toevoegen aan een ontwerp.', + 'comment_add' => 'Er is een fout opgetreden tijdens het toevoegen van de reactie.', + 'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de reactie.', + 'empty_comment' => 'Kan geen lege reactie toevoegen.', ]; \ No newline at end of file diff --git a/resources/lang/pt_BR/entities.php b/resources/lang/pt_BR/entities.php index 5a965fe62..e6b900fdd 100644 --- a/resources/lang/pt_BR/entities.php +++ b/resources/lang/pt_BR/entities.php @@ -214,4 +214,26 @@ return [ 'profile_not_created_pages' => ':userName não criou páginas', 'profile_not_created_chapters' => ':userName não criou capítulos', 'profile_not_created_books' => ':userName não criou livros', + + /** + * Comments + */ + 'comentário' => 'Comentário', + 'comentários' => 'Comentários', + 'comment_placeholder' => 'Digite seus comentários aqui, markdown suportado ...', + 'no_comments' => 'No Comments', + 'x_comments' => ':numComments Comentários', + 'one_comment' => '1 comentário', + 'comments_loading' => 'Carregando ....', + 'comment_save' => 'Salvar comentário', + 'comment_reply' => 'Responder', + 'comment_edit' => 'Editar', + 'comment_delete' => 'Excluir', + 'comment_cancel' => 'Cancelar', + 'comment_created' => 'Comentário adicionado', + 'comment_updated' => 'Comentário atualizado', + 'comment_deleted' => 'Comentário eliminado', + 'comment_updated_text' => 'Atualizado :updatedDiff atrás por', + 'comment_delete_confirm' => 'Isso removerá o conteúdo do comentário. Tem certeza de que deseja excluir esse comentário?', + 'comment_create' => 'Criada' ]; \ No newline at end of file diff --git a/resources/lang/pt_BR/errors.php b/resources/lang/pt_BR/errors.php index 91b85e3ef..16fc78ff5 100644 --- a/resources/lang/pt_BR/errors.php +++ b/resources/lang/pt_BR/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Um erro ocorreu', 'app_down' => ':appName está fora do ar no momento', 'back_soon' => 'Voltaremos em seguida.', + + // comments + 'comment_list' => 'Ocorreu um erro ao buscar os comentários.', + 'cannot_add_comment_to_draft' => 'Você não pode adicionar comentários a um rascunho.', + 'comment_add' => 'Ocorreu um erro ao adicionar o comentário.', + 'comment_delete' => 'Ocorreu um erro ao excluir o comentário.', + 'empty_comment' => 'Não é possível adicionar um comentário vazio.', ]; \ No newline at end of file diff --git a/resources/lang/sk/entities.php b/resources/lang/sk/entities.php index e70864753..7c8f34368 100644 --- a/resources/lang/sk/entities.php +++ b/resources/lang/sk/entities.php @@ -223,4 +223,26 @@ return [ 'profile_not_created_pages' => ':userName nevytvoril žiadne stránky', 'profile_not_created_chapters' => ':userName nevytvoril žiadne kapitoly', 'profile_not_created_books' => ':userName nevytvoril žiadne knihy', + + /** + * Comments + */ + 'comment' => 'Komentár', + 'comments' => 'Komentáre', + 'comment_placeholder' => 'Tu zadajte svoje pripomienky, podporované označenie ...', + 'no_comments' => 'No Comments', + 'x_comments' => ':numComments komentárov', + 'one_comment' => '1 komentár', + 'comments_loading' => 'Loading ..', + 'comment_save' => 'Uložiť komentár', + 'comment_reply' => 'Odpovedať', + 'comment_edit' => 'Upraviť', + 'comment_delete' => 'Odstrániť', + 'comment_cancel' => 'Zrušiť', + 'comment_created' => 'Pridaný komentár', + 'comment_updated' => 'Komentár aktualizovaný', + 'comment_deleted' => 'Komentár bol odstránený', + 'comment_updated_text' => 'Aktualizované pred :updateDiff', + 'comment_delete_confirm' => 'Tým sa odstráni obsah komentára. Naozaj chcete odstrániť tento komentár?', + 'comment_create' => 'Vytvorené' ]; diff --git a/resources/lang/sk/errors.php b/resources/lang/sk/errors.php index e3420852a..d4c7b7a3a 100644 --- a/resources/lang/sk/errors.php +++ b/resources/lang/sk/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Nastala chyba', 'app_down' => ':appName je momentálne nedostupná', 'back_soon' => 'Čoskoro bude opäť dostupná.', + + // comments + 'comment_list' => 'Pri načítaní komentárov sa vyskytla chyba', + 'cannot_add_comment_to_draft' => 'Do konceptu nemôžete pridávať komentáre.', + 'comment_add' => 'Počas pridávania komentára sa vyskytla chyba', + 'comment_delete' => 'Pri odstraňovaní komentára došlo k chybe', + 'empty_comment' => 'Nelze pridať prázdny komentár.', ]; diff --git a/resources/views/comments/comment-reply.blade.php b/resources/views/comments/comment-reply.blade.php new file mode 100644 index 000000000..02535341c --- /dev/null +++ b/resources/views/comments/comment-reply.blade.php @@ -0,0 +1,12 @@ +
+
+ + + + +
+
+ +@if($errors->has('markdown')) +
{{ $errors->first('markdown') }}
+@endif \ No newline at end of file diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php new file mode 100644 index 000000000..ffa75cfed --- /dev/null +++ b/resources/views/comments/comments.blade.php @@ -0,0 +1,18 @@ + + +
+

@{{vm.totalCommentsStr}}

+
+
+
+ +
+
+
+ @include('comments/comment-reply', ['pageId' => $pageId]) +
+
\ No newline at end of file diff --git a/resources/views/comments/list-item.blade.php b/resources/views/comments/list-item.blade.php new file mode 100644 index 000000000..f274d2ed2 --- /dev/null +++ b/resources/views/comments/list-item.blade.php @@ -0,0 +1,30 @@ +
+
+ user avatar +
+
+ +
+ +
+
+ {{ trans('entities.comment_deleted') }} +
+
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 221ed4476..0d75a534a 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -46,13 +46,13 @@ -
+