From fea5630ea4343b33c353afb41d502d535f674331 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 3 Sep 2017 16:37:51 +0100 Subject: [PATCH] Made some changes to the comment system Changed to be rendered server side along with page content. Changed deletion to fully delete comments from the database. Added 'local_id' to comments for referencing. Updated reply system to be non-nested (Incomplete) Made database comment format entity-agnostic to be more future proof. Updated designs of comment sections. --- app/Comment.php | 87 ++------- app/Entity.php | 12 ++ app/Http/Controllers/CommentController.php | 116 ++++++------ app/Http/Controllers/PageController.php | 1 + app/Page.php | 4 - app/Repos/CommentRepo.php | 130 ++++++------- app/Repos/TagRepo.php | 1 + ...017_08_01_130541_create_comments_table.php | 10 +- resources/assets/js/components/index.js | 1 + .../assets/js/components/page-comments.js | 137 ++++++++++++++ resources/assets/js/global.js | 1 + resources/assets/js/translations.js | 76 +++++++- .../vues/components/comments/comment-reply.js | 113 ------------ .../js/vues/components/comments/comment.js | 174 ------------------ resources/assets/js/vues/page-comments.js | 117 ------------ resources/assets/js/vues/vues.js | 1 - resources/assets/sass/_comments.scss | 101 +++------- resources/assets/sass/_variables.scss | 5 +- resources/lang/en/common.php | 1 + resources/lang/en/entities.php | 21 +-- resources/views/comments/comment.blade.php | 50 +++++ resources/views/comments/comments.blade.php | 42 ++++- resources/views/pages/show.blade.php | 3 +- routes/web.php | 5 +- 24 files changed, 478 insertions(+), 731 deletions(-) create mode 100644 resources/assets/js/components/page-comments.js delete mode 100644 resources/assets/js/vues/components/comments/comment-reply.js delete mode 100644 resources/assets/js/vues/components/comments/comment.js delete mode 100644 resources/assets/js/vues/page-comments.js create mode 100644 resources/views/comments/comment.blade.php diff --git a/app/Comment.php b/app/Comment.php index de01b6212..2800ab21a 100644 --- a/app/Comment.php +++ b/app/Comment.php @@ -1,12 +1,10 @@ -belongsTo(Page::class); + return $this->updated_at->timestamp > $this->created_at->timestamp; } /** - * Get the owner of this comment. - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * Get created date as a relative diff. + * @return mixed */ - public function user() + public function getCreatedAttribute() { - return $this->belongsTo(User::class); + return $this->created_at->diffForHumans(); } - /* - * Not being used, but left here because might be used in the future for performance reasons. + /** + * Get updated date as a relative diff. + * @return mixed */ - 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; + public function getUpdatedAttribute() + { + return $this->updated_at->diffForHumans(); } } diff --git a/app/Entity.php b/app/Entity.php index e5dd04bf2..efbbf0eba 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -1,6 +1,8 @@ morphMany(Tag::class, 'entity')->orderBy('order', 'asc'); } + /** + * Get the comments for an entity + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ + public function comments() + { + return $this->morphMany(Comment::class, 'entity')->orderBy('created_at', 'asc'); + } + + /** * Get the related search terms. * @return \Illuminate\Database\Eloquent\Relations\MorphMany diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index e8d5eab30..384731f84 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -2,22 +2,34 @@ use BookStack\Repos\CommentRepo; use BookStack\Repos\EntityRepo; -use BookStack\Comment; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; class CommentController extends Controller { protected $entityRepo; + protected $commentRepo; - public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo, Comment $comment) + /** + * CommentController constructor. + * @param EntityRepo $entityRepo + * @param CommentRepo $commentRepo + */ + public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo) { $this->entityRepo = $entityRepo; $this->commentRepo = $commentRepo; - $this->comment = $comment; parent::__construct(); } - public function save(Request $request, $pageId, $commentId = null) + /** + * Save a new comment for a Page + * @param Request $request + * @param integer $pageId + * @param null|integer $commentId + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function savePageComment(Request $request, $pageId, $commentId = null) { $this->validate($request, [ 'text' => 'required|string', @@ -30,70 +42,50 @@ class CommentController extends Controller 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'); + + // Prevent adding comments to draft pages + if ($page->draft) { + return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400); } - $comment = $this->commentRepo->getCommentById($comment->id); - - return response()->json([ - 'status' => 'success', - 'message' => $respMsg, - 'comment' => $comment - ]); - + // Create a new comment. + $this->checkPermission('comment-create-all'); + $comment = $this->commentRepo->create($page, $request->all()); + return view('comments/comment', ['comment' => $comment]); } - public function destroy($id) { - $comment = $this->comment->findOrFail($id); + /** + * Update an existing comment. + * @param Request $request + * @param integer $commentId + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function update(Request $request, $commentId) + { + $this->validate($request, [ + 'text' => 'required|string', + 'html' => 'required|string', + ]); + + $comment = $this->commentRepo->getById($commentId); + $this->checkOwnablePermission('page-view', $comment->entity); + $this->checkOwnablePermission('comment-update', $comment); + + $comment = $this->commentRepo->update($comment, $request->all()); + return view('comments/comment', ['comment' => $comment]); + } + + /** + * Delete a comment from the system. + * @param integer $id + * @return \Illuminate\Http\JsonResponse + */ + public function destroy($id) + { + $comment = $this->commentRepo->getById($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]); + return response()->json(['message' => trans('entities.comment_deleted')]); } } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 21572db29..c3090af83 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -161,6 +161,7 @@ class PageController extends Controller $pageContent = $this->entityRepo->renderPage($page); $sidebarTree = $this->entityRepo->getBookChildren($page->book); $pageNav = $this->entityRepo->getPageNav($pageContent); + $page->load(['comments.createdBy']); Views::add($page); $this->setPageTitle($page->getShortName()); diff --git a/app/Page.php b/app/Page.php index d722e4e54..c9823e7e4 100644 --- a/app/Page.php +++ b/app/Page.php @@ -66,10 +66,6 @@ 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 index ce71b9234..c3d7468cf 100644 --- a/app/Repos/CommentRepo.php +++ b/app/Repos/CommentRepo.php @@ -1,105 +1,87 @@ comment = $comment; } - public function create (Page $page, $data = []) { + /** + * Get a comment by ID. + * @param $id + * @return Comment|\Illuminate\Database\Eloquent\Model + */ + public function getById($id) + { + return $this->comment->newQuery()->findOrFail($id); + } + + /** + * Create a new comment on an entity. + * @param Entity $entity + * @param array $data + * @return Comment + */ + public function create (Entity $entity, $data = []) + { $userId = user()->id; - $comment = $this->comment->newInstance(); - $comment->fill($data); - // new comment - $comment->page_id = $page->id; + $comment = $this->comment->newInstance($data); $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); + $comment->local_id = $this->getNextLocalId($entity); + $entity->comments()->save($comment); 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(); + /** + * Update an existing comment. + * @param Comment $comment + * @param array $input + * @return mixed + */ + public function update($comment, $input) + { + $comment->updated_by = user()->id; + $comment->update($input); 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 - ]; + /** + * Delete a comment from the system. + * @param Comment $comment + * @return mixed + */ + public function delete($comment) + { + return $comment->delete(); } - 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; + /** + * Get the next local ID relative to the linked entity. + * @param Entity $entity + * @return int + */ + protected function getNextLocalId(Entity $entity) + { + $comments = $entity->comments()->orderBy('local_id', 'desc')->first(); + if ($comments === null) return 1; + return $comments->local_id + 1; } } \ No newline at end of file diff --git a/app/Repos/TagRepo.php b/app/Repos/TagRepo.php index c6350db1a..5edd6df3c 100644 --- a/app/Repos/TagRepo.php +++ b/app/Repos/TagRepo.php @@ -33,6 +33,7 @@ class TagRepo * @param $entityType * @param $entityId * @param string $action + * @return \Illuminate\Database\Eloquent\Model|null|static */ public function getEntity($entityType, $entityId, $action = 'view') { diff --git a/database/migrations/2017_08_01_130541_create_comments_table.php b/database/migrations/2017_08_01_130541_create_comments_table.php index bfb7eecbf..1d69d1fa7 100644 --- a/database/migrations/2017_08_01_130541_create_comments_table.php +++ b/database/migrations/2017_08_01_130541_create_comments_table.php @@ -15,17 +15,19 @@ class CreateCommentsTable extends Migration { Schema::create('comments', function (Blueprint $table) { $table->increments('id')->unsigned(); - $table->integer('page_id')->unsigned(); + $table->integer('entity_id')->unsigned(); + $table->string('entity_type'); $table->longText('text')->nullable(); $table->longText('html')->nullable(); $table->integer('parent_id')->unsigned()->nullable(); + $table->integer('local_id')->unsigned()->nullable(); $table->integer('created_by')->unsigned(); $table->integer('updated_by')->unsigned()->nullable(); - $table->boolean('active')->default(true); - - $table->index(['page_id']); $table->timestamps(); + $table->index(['entity_id', 'entity_type']); + $table->index(['local_id']); + // Assign new comment permissions to admin role $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; // Create & attach new entity permissions diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js index 988409fbc..d970a581f 100644 --- a/resources/assets/js/components/index.js +++ b/resources/assets/js/components/index.js @@ -10,6 +10,7 @@ let componentMapping = { 'entity-selector': require('./entity-selector'), 'sidebar': require('./sidebar'), 'page-picker': require('./page-picker'), + 'page-comments': require('./page-comments'), }; window.components = {}; diff --git a/resources/assets/js/components/page-comments.js b/resources/assets/js/components/page-comments.js new file mode 100644 index 000000000..87cd88905 --- /dev/null +++ b/resources/assets/js/components/page-comments.js @@ -0,0 +1,137 @@ +const MarkdownIt = require("markdown-it"); +const md = new MarkdownIt({ html: true }); + +class PageComments { + + constructor(elem) { + this.elem = elem; + this.pageId = Number(elem.getAttribute('page-id')); + + this.formContainer = elem.querySelector('[comment-form-container]'); + this.form = this.formContainer.querySelector('form'); + this.formInput = this.form.querySelector('textarea'); + this.container = elem.querySelector('[comment-container]'); + + // TODO - Handle elem usage when no permissions + this.form.addEventListener('submit', this.saveComment.bind(this)); + this.elem.addEventListener('click', this.handleAction.bind(this)); + this.elem.addEventListener('submit', this.updateComment.bind(this)); + + this.editingComment = null; + } + + handleAction(event) { + let actionElem = event.target.closest('[action]'); + if (actionElem === null) return; + + let action = actionElem.getAttribute('action'); + if (action === 'edit') this.editComment(actionElem.closest('[comment]')); + if (action === 'closeUpdateForm') this.closeUpdateForm(); + if (action === 'delete') this.deleteComment(actionElem.closest('[comment]')); + if (action === 'addComment') this.showForm(); + if (action === 'hideForm') this.hideForm(); + if (action === 'reply') this.setReply(); + } + + closeUpdateForm() { + if (!this.editingComment) return; + this.editingComment.querySelector('[comment-content]').style.display = 'block'; + this.editingComment.querySelector('[comment-edit-container]').style.display = 'none'; + } + + editComment(commentElem) { + this.hideForm(); + if (this.editingComment) this.closeUpdateForm(); + commentElem.querySelector('[comment-content]').style.display = 'none'; + commentElem.querySelector('[comment-edit-container]').style.display = 'block'; + this.editingComment = commentElem; + } + + updateComment(event) { + let form = event.target; + event.preventDefault(); + let text = form.querySelector('textarea').value; + let reqData = { + text: text, + html: md.render(text), + // parent_id: this.parent_id TODO - Handle replies + }; + // TODO - Loading indicator + let commentId = this.editingComment.getAttribute('comment'); + window.$http.put(window.baseUrl(`/ajax/comment/${commentId}`), reqData).then(resp => { + let newComment = document.createElement('div'); + newComment.innerHTML = resp.data; + this.editingComment.innerHTML = newComment.children[0].innerHTML; + window.$events.emit('success', window.trans('entities.comment_updated_success')); + this.closeUpdateForm(); + this.editingComment = null; + }); + } + + deleteComment(commentElem) { + let id = commentElem.getAttribute('comment'); + // TODO - Loading indicator + // TODO - Confirm dropdown + window.$http.delete(window.baseUrl(`/ajax/comment/${id}`)).then(resp => { + commentElem.parentNode.removeChild(commentElem); + window.$events.emit('success', window.trans('entities.comment_deleted_success')); + this.updateCount(); + }); + } + + saveComment(event) { + event.preventDefault(); + event.stopPropagation(); + let text = this.formInput.value; + let reqData = { + text: text, + html: md.render(text), + // parent_id: this.parent_id TODO - Handle replies + }; + // TODO - Loading indicator + window.$http.post(window.baseUrl(`/ajax/page/${this.pageId}/comment`), reqData).then(resp => { + let newComment = document.createElement('div'); + newComment.innerHTML = resp.data; + this.container.appendChild(newComment.children[0]); + + window.$events.emit('success', window.trans('entities.comment_created_success')); + this.resetForm(); + this.updateCount(); + }); + } + + updateCount() { + let count = this.container.children.length; + this.elem.querySelector('[comments-title]').textContent = window.trans_choice('entities.comment_count', count, {count}); + } + + resetForm() { + this.formInput.value = ''; + this.formContainer.appendChild(this.form); + this.hideForm(); + } + + showForm() { + this.formContainer.style.display = 'block'; + this.formContainer.parentNode.style.display = 'block'; + this.elem.querySelector('[comment-add-button]').style.display = 'none'; + this.formInput.focus(); // TODO - Scroll to input on focus + } + + hideForm() { + this.formContainer.style.display = 'none'; + this.formContainer.parentNode.style.display = 'none'; + this.elem.querySelector('[comment-add-button]').style.display = 'block'; + } + + setReply() { + + this.showForm(); + } + +} + +// TODO - Go to comment if url param set + + +module.exports = PageComments; \ No newline at end of file diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 85f9f77a6..37b1b8b7c 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -73,6 +73,7 @@ let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize' const Translations = require("./translations"); let translator = new Translations(window.translations); window.trans = translator.get.bind(translator); +window.trans_choice = translator.getPlural.bind(translator); require("./vues/vues"); diff --git a/resources/assets/js/translations.js b/resources/assets/js/translations.js index ca6a7bd29..99c6b4321 100644 --- a/resources/assets/js/translations.js +++ b/resources/assets/js/translations.js @@ -20,9 +20,64 @@ class Translator { * @returns {*} */ get(key, replacements) { + let text = this.getTransText(key); + return this.performReplacements(text, replacements); + } + + /** + * Get pluralised text, Dependant on the given count. + * Same format at laravel's 'trans_choice' helper. + * @param key + * @param count + * @param replacements + * @returns {*} + */ + getPlural(key, count, replacements) { + let text = this.getTransText(key); + let splitText = text.split('|'); + let result = null; + let exactCountRegex = /^{([0-9]+)}/; + let rangeRegex = /^\[([0-9]+),([0-9*]+)]/; + + for (let i = 0, len = splitText.length; i < len; i++) { + let t = splitText[i]; + + // Parse exact matches + let exactMatches = t.match(exactCountRegex); + console.log(exactMatches); + if (exactMatches !== null && Number(exactMatches[1]) === count) { + result = t.replace(exactCountRegex, '').trim(); + break; + } + + // Parse range matches + let rangeMatches = t.match(rangeRegex); + if (rangeMatches !== null) { + let rangeStart = Number(rangeMatches[1]); + if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) { + result = t.replace(rangeRegex, '').trim(); + break; + } + } + } + + if (result === null && splitText.length > 1) { + result = (count === 1) ? splitText[0] : splitText[1]; + } + + if (result === null) result = splitText[0]; + return this.performReplacements(result, replacements); + } + + /** + * Fetched translation text from the store for the given key. + * @param key + * @returns {String|Object} + */ + getTransText(key) { let splitKey = key.split('.'); let value = splitKey.reduce((a, b) => { - return a != undefined ? a[b] : a; + return a !== undefined ? a[b] : a; }, this.store); if (value === undefined) { @@ -30,16 +85,25 @@ class Translator { value = key; } - if (replacements === undefined) return value; + return value; + } - let replaceMatches = value.match(/:([\S]+)/g); - if (replaceMatches === null) return value; + /** + * Perform replacements on a string. + * @param {String} string + * @param {Object} replacements + * @returns {*} + */ + performReplacements(string, replacements) { + if (!replacements) return string; + let replaceMatches = string.match(/:([\S]+)/g); + if (replaceMatches === null) return string; replaceMatches.forEach(match => { let key = match.substring(1); if (typeof replacements[key] === 'undefined') return; - value = value.replace(match, replacements[key]); + string = string.replace(match, replacements[key]); }); - return value; + return string; } } diff --git a/resources/assets/js/vues/components/comments/comment-reply.js b/resources/assets/js/vues/components/comments/comment-reply.js deleted file mode 100644 index 0f65fc237..000000000 --- a/resources/assets/js/vues/components/comments/comment-reply.js +++ /dev/null @@ -1,113 +0,0 @@ -const MarkdownIt = require("markdown-it"); -const md = new MarkdownIt({ html: true }); - -var template = ` -
-
- - - - -
-
-`; - -const props = { - pageId: {}, - commentObj: {}, - isReply: { - default: false, - type: Boolean - }, isEdit: { - default: false, - type: Boolean - } -}; - -function data() { - let comment = { - text: '' - }; - - if (this.isReply) { - comment.page_id = this.commentObj.page_id; - comment.id = this.commentObj.id; - } else if (this.isEdit) { - comment = this.commentObj; - } - - return { - comment: comment, - trans: trans - }; -} - -const methods = { - saveComment: function (event) { - let pageId = this.comment.page_id || this.pageId; - let commentText = this.comment.text; - if (!commentText) { - return this.$events.emit('error', trans('errors.empty_comment')) - } - let commentHTML = md.render(commentText); - let serviceUrl = `/ajax/page/${pageId}/comment/`; - let httpMethod = 'post'; - let reqObj = { - text: commentText, - html: commentHTML - }; - - if (this.isEdit === true) { - // this will be set when editing the comment. - serviceUrl = `/ajax/page/${pageId}/comment/${this.comment.id}`; - httpMethod = 'put'; - } else if (this.isReply === true) { - // if its reply, get the parent comment id - reqObj.parent_id = this.comment.id; - } - $http[httpMethod](window.baseUrl(serviceUrl), reqObj).then(resp => { - if (!isCommentOpSuccess(resp)) { - this.$events.emit('error', getErrorMsg(resp)); - return; - } - // hide the comments first, and then retrigger the refresh - if (this.isEdit) { - this.$emit('comment-edited', event, resp.data.comment); - } else { - this.comment.text = ''; - this.$emit('comment-added', event); - if (this.isReply === true) { - this.$emit('comment-replied', event, resp.data.comment); - } else { - this.$parent.$emit('new-comment', event, resp.data.comment); - } - } - this.$events.emit('success', resp.data.message); - }).catch(err => { - this.$events.emit('error', trans('errors.comment_add')) - }); - }, - closeBox: function (event) { - this.$emit('editor-removed', event); - } -}; - -const computed = {}; - -function isCommentOpSuccess(resp) { - if (resp && resp.data && resp.data.status === 'success') { - return true; - } - return false; -} - -function getErrorMsg(response) { - if (response.data) { - return response.data.message; - } else { - return trans('errors.comment_add'); - } -} - -module.exports = { name: 'comment-reply', template, data, props, methods, computed }; - diff --git a/resources/assets/js/vues/components/comments/comment.js b/resources/assets/js/vues/components/comments/comment.js deleted file mode 100644 index 419c0a5fa..000000000 --- a/resources/assets/js/vues/components/comments/comment.js +++ /dev/null @@ -1,174 +0,0 @@ -const commentReply = require('./comment-reply'); - -const template = ` -
-
-
- user avatar -
-
- -
- -
-
- {{ trans('entities.comment_deleted') }} -
-
- -
-
- - -
- - -
-
-
-`; - -const props = ['initialComment', 'index', 'level', 'permissions', 'currentUserId']; - -function data() { - return { - trans: trans, - comments: [], - showEditor: false, - comment: this.initialComment, - nextLevel: this.level + 1 - }; -} - -const methods = { - deleteComment: function () { - var resp = window.confirm(trans('entities.comment_delete_confirm')); - if (!resp) { - return; - } - this.$http.delete(window.baseUrl(`/ajax/comment/${this.comment.id}`)).then(resp => { - if (!isCommentOpSuccess(resp)) { - this.$events.emit('error', trans('error.comment_delete')); - return; - } - this.$events.emit('success', trans('entities.comment_deleted')); - this.comment = resp.data.comment; - }).catch(err => { - this.$events.emit('error', trans('error.comment_delete')); - }); - }, - replyComment: function () { - this.toggleEditor(false); - }, - editComment: function () { - this.toggleEditor(true); - }, - hideComment: function () { - this.showEditor = false; - }, - toggleEditor: function (isEdit) { - this.showEditor = false; - this.isEdit = isEdit; - this.isReply = !isEdit; - this.showEditor = true; - }, - commentReplied: function (event, comment) { - this.comments.push(comment); - this.showEditor = false; - }, - commentEdited: function (event, comment) { - this.comment = comment; - this.showEditor = false; - }, - commentAdded: function (event, comment) { - // this is to handle non-parent child relationship - // we want to make it go up. - this.$emit('comment-added', event); - }, - canEditOrDelete: function (prop) { - if (!this.comment.active) { - return false; - } - - if (!this.permissions) { - return false; - } - - let propAll = 'comment_' + prop + '_all'; - let propOwn = 'comment_' + prop + '_own'; - - if (this.permissions[propAll]) { - return true; - } - - if (this.permissions[propOwn] && this.comment.created_by.id === this.currentUserId) { - return true; - } - - return false; - }, - canComment: function () { - if (!this.permissions) { - return false; - } - return this.permissions.comment_create === true; - } -}; - -const computed = { - commentId: function () { - return `comment-${this.comment.page_id}-${this.comment.id}`; - }, - commentHref: function () { - return `#?cm=${this.commentId}`; - } -}; - -function mounted() { - if (this.comment.sub_comments && this.comment.sub_comments.length) { - // set this so that we can render the next set of sub comments. - this.comments = this.comment.sub_comments; - } -} - -function isCommentOpSuccess(resp) { - if (resp && resp.data && resp.data.status === 'success') { - return true; - } - return false; -} - -module.exports = { - name: 'comment', - template, data, props, methods, computed, mounted, components: { - commentReply - } -}; - diff --git a/resources/assets/js/vues/page-comments.js b/resources/assets/js/vues/page-comments.js deleted file mode 100644 index e42cdbf9c..000000000 --- a/resources/assets/js/vues/page-comments.js +++ /dev/null @@ -1,117 +0,0 @@ -const comment = require('./components/comments/comment'); -const commentReply = require('./components/comments/comment-reply'); - -let data = { - totalCommentsStr: trans('entities.comments_loading'), - comments: [], - permissions: null, - currentUserId: null, - trans: trans, - commentCount: 0 -}; - -let methods = { - commentAdded: function () { - ++this.totalComments; - } -} - -let computed = { - totalComments: { - get: function () { - return this.commentCount; - }, - set: function (value) { - this.commentCount = value; - if (value === 0) { - this.totalCommentsStr = trans('entities.no_comments'); - } else if (value === 1) { - this.totalCommentsStr = trans('entities.one_comment'); - } else { - this.totalCommentsStr = trans('entities.x_comments', { - numComments: value - }); - } - } - }, - canComment: function () { - if (!this.permissions) { - return false; - } - return this.permissions.comment_create === true; - } -} - -function mounted() { - this.pageId = Number(this.$el.getAttribute('page-id')); - let linkedCommentId = getUrlParameter('cm'); - this.$http.get(window.baseUrl(`/ajax/page/${this.pageId}/comments/`)).then(resp => { - if (!isCommentOpSuccess(resp)) { - // just show that no comments are available. - vm.totalComments = 0; - this.$events.emit('error', getErrorMsg(resp)); - return; - } - this.comments = resp.data.comments; - this.totalComments = +resp.data.total; - this.permissions = resp.data.permissions; - this.currentUserId = resp.data.user_id; - if (!linkedCommentId) { - return; - } - - // adding a setTimeout to give the comment list some time to render - // before focusing the comment. - setTimeout(function() { - focusLinkedComment(linkedCommentId); - }); - }).catch(err => { - this.$events.emit('error', trans('errors.comment_list')); - }); -} - -function isCommentOpSuccess(resp) { - if (resp && resp.data && resp.data.status === 'success') { - return true; - } - return false; -} - -function getErrorMsg(response) { - if (response.data) { - return response.data.message; - } else { - return trans('errors.comment_add'); - } -} - -function created() { - this.$on('new-comment', function (event, comment) { - this.comments.push(comment); - }) -} - -function beforeDestroy() { - this.$off('new-comment'); -} - -function getUrlParameter(name) { - name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); - var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); - var results = regex.exec(location.hash); - return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); -} - -function focusLinkedComment(linkedCommentId) { - let comment = document.getElementById(linkedCommentId); - if (comment && comment.length !== 0) { - window.setupPageShow.goToText(linkedCommentId); - } -} - -module.exports = { - data, methods, mounted, computed, components: { - comment, commentReply - }, - created, beforeDestroy -}; \ No newline at end of file diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js index d4c4c4574..a70d32009 100644 --- a/resources/assets/js/vues/vues.js +++ b/resources/assets/js/vues/vues.js @@ -11,7 +11,6 @@ let vueMapping = { 'image-manager': require('./image-manager'), 'tag-manager': require('./tag-manager'), 'attachment-manager': require('./attachment-manager'), - 'page-comments': require('./page-comments') }; window.vues = {}; diff --git a/resources/assets/sass/_comments.scss b/resources/assets/sass/_comments.scss index 5da53a14d..5fcd49d6e 100644 --- a/resources/assets/sass/_comments.scss +++ b/resources/assets/sass/_comments.scss @@ -1,82 +1,33 @@ -.comments-list { - .comment-box { - border-bottom: 1px solid $comment-border; +.comment-box { + border: 1px solid #DDD; + margin-bottom: $-s; + border-radius: 3px; + .content { + padding: $-s; } - - .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 { + .content 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; +.comment-box .header { + padding: $-xs $-s; + background-color: #f8f8f8; + border-bottom: 1px solid #DDD; + img, a, span { + display: inline-block; + vertical-align: top; + } + a, span { + padding: $-xxs 0 $-xxs 0; + line-height: 1.6; + } + a { color: #666; } + span { + color: #888; + padding-left: $-xxs; + } + .text-muted { + color: #999; } } diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index 18880fa5b..d2b6acc9f 100644 --- a/resources/assets/sass/_variables.scss +++ b/resources/assets/sass/_variables.scss @@ -59,7 +59,4 @@ $text-light: #EEE; // Shadows $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 +$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13); \ No newline at end of file diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index 06b980970..269905a59 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -29,6 +29,7 @@ return [ 'edit' => 'Edit', 'sort' => 'Sort', 'move' => 'Move', + 'reply' => 'Reply', 'delete' => 'Delete', 'search' => 'Search', 'search_clear' => 'Clear Search', diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index b8be379cd..cfbd1a991 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -242,20 +242,15 @@ return [ */ '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_placeholder' => 'Leave a comment here', + 'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments', '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_new' => 'New Comment', + 'comment_created' => 'commented :createDiff', + 'comment_updated' => 'Updated :updateDiff by :username', + 'comment_deleted_success' => 'Comment deleted', + 'comment_created_success' => 'Comment added', + 'comment_updated_success' => 'Comment updated', 'comment_delete_confirm' => 'This will remove the contents of the comment. Are you sure you want to delete this comment?', 'comment_create' => 'Created' diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php new file mode 100644 index 000000000..dcf5faf1e --- /dev/null +++ b/resources/views/comments/comment.blade.php @@ -0,0 +1,50 @@ +
+
+ +
+ @if(userCan('comment-update', $comment)) + + @endif + @if(userCan('comment-create-all')) + + @endif + @if(userCan('comment-delete', $comment)) + + @endif +
+ + #{{$comment->local_id}} +    + {{ $comment->createdBy->name }} +   + {{ $comment->createdBy->name }} + {{--TODO - Account for deleted user--}} + + {{ trans('entities.comment_created', ['createDiff' => $comment->created]) }} + + @if($comment->isUpdated()) + + •  + {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy->name]) }} + + @endif +
+
+ {!! $comment->html !!} +
+ + @if(userCan('comment-update', $comment)) + + @endif + +
\ No newline at end of file diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index fcf284b26..cf2affc15 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -1,11 +1,33 @@ -
-

@{{totalCommentsStr}}

-
- -
- - -
+
+

{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}

+ +
+ @foreach($page->comments as $comment) + @include('comments.comment', ['comment' => $comment]) + @endforeach +
+ + + @if(userCan('comment-create-all')) + + + +
+ +
+ @endif +
\ No newline at end of file diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 07ecdfdfc..51c24cbd7 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -147,8 +147,9 @@ @include('pages/page-display')
+
- @include('comments/comments', ['pageId' => $page->id]) + @include('comments/comments', ['page' => $page])
@stop diff --git a/routes/web.php b/routes/web.php index 463e4e77b..8bff3b2ec 100644 --- a/routes/web.php +++ b/routes/web.php @@ -120,10 +120,9 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax'); // Comments - Route::post('/ajax/page/{pageId}/comment/', 'CommentController@save'); - Route::put('/ajax/page/{pageId}/comment/{commentId}', 'CommentController@save'); + Route::post('/ajax/page/{pageId}/comment', 'CommentController@savePageComment'); + Route::put('/ajax/comment/{id}', 'CommentController@update'); Route::delete('/ajax/comment/{id}', 'CommentController@destroy'); - Route::get('/ajax/page/{pageId}/comments/', 'CommentController@getPageComments'); // Links Route::get('/link/{id}', 'PageController@redirectFromLink');