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.
-<?php
-
-namespace BookStack;
+<?php namespace BookStack;
class Comment extends Ownable
{
- public $sub_comments = [];
protected $fillable = ['text', 'html', 'parent_id'];
- protected $appends = ['created', 'updated', 'sub_comments'];
+ protected $appends = ['created', 'updated'];
+
/**
* Get the entity that this comment belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
}
/**
- * Get the page that this comment is in.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ * Check if a comment has been updated since creation.
+ * @return bool
*/
- public function page()
+ public function isUpdated()
{
- return $this->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();
}
}
<?php namespace BookStack;
+use Illuminate\Database\Eloquent\Relations\MorphMany;
+
class Entity extends Ownable
{
return $this->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
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;
-
- public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo, Comment $comment)
+ protected $commentRepo;
+
+ /**
+ * 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',
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);
+ // Create a new comment.
+ $this->checkPermission('comment-create-all');
+ $comment = $this->commentRepo->create($page, $request->all());
+ return view('comments/comment', ['comment' => $comment]);
+ }
- return response()->json([
- 'status' => 'success',
- 'message' => $respMsg,
- 'comment' => $comment
+ /**
+ * 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]);
}
- public function destroy($id) {
- $comment = $this->comment->findOrFail($id);
+ /**
+ * 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')]);
}
}
$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());
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
<?php namespace BookStack\Repos;
use BookStack\Comment;
-use BookStack\Page;
+use BookStack\Entity;
/**
- * Class TagRepo
+ * Class CommentRepo
* @package BookStack\Repos
*/
class CommentRepo {
+
/**
- *
* @var Comment $comment
*/
protected $comment;
+ /**
+ * CommentRepo constructor.
+ * @param Comment $comment
+ */
public function __construct(Comment $comment)
{
$this->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;
+ /**
+ * Get a comment by ID.
+ * @param $id
+ * @return Comment|\Illuminate\Database\Eloquent\Model
+ */
+ public function getById($id)
+ {
+ return $this->comment->newQuery()->findOrFail($id);
}
- public function update($comment, $input, $activeOnly = true) {
+ /**
+ * 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($data);
+ $comment->created_by = $userId;
$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
- ];
- }
-
- public function getCommentById($commentId) {
- return $this->normalizeComment($this->comment->getCommentById($commentId));
+ /**
+ * Delete a comment from the system.
+ * @param Comment $comment
+ * @return mixed
+ */
+ public function delete($comment)
+ {
+ return $comment->delete();
}
- 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
* @param $entityType
* @param $entityId
* @param string $action
+ * @return \Illuminate\Database\Eloquent\Model|null|static
*/
public function getEntity($entityType, $entityId, $action = 'view')
{
{
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
'entity-selector': require('./entity-selector'),
'sidebar': require('./sidebar'),
'page-picker': require('./page-picker'),
+ 'page-comments': require('./page-comments'),
};
window.components = {};
--- /dev/null
+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
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");
* @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) {
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;
}
}
+++ /dev/null
-const MarkdownIt = require("markdown-it");
-const md = new MarkdownIt({ html: true });
-
-var template = `
-<div class="comment-editor" v-cloak>
-<form novalidate>
- <textarea name="markdown" rows="3" v-model="comment.text" :placeholder="trans('entities.comment_placeholder')"></textarea>
- <input type="hidden" v-model="comment.pageId" name="comment.pageId" :value="pageId">
- <button type="button" v-if="isReply || isEdit" class="button muted" v-on:click="closeBox">{{ trans('entities.comment_cancel') }}</button>
- <button type="submit" class="button pos" v-on:click.prevent="saveComment">{{ trans('entities.comment_save') }}</button>
-</form>
-</div>
-`;
-
-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 };
-
+++ /dev/null
-const commentReply = require('./comment-reply');
-
-const template = `
-<div class="comment-box">
- <div class='page-comment' :id="commentId">
- <div class="user-image">
- <img :src="comment.created_by.avatar_url" alt="user avatar">
- </div>
- <div class="comment-container">
- <div class="comment-header">
- <a :href="comment.created_by.profile_url">{{comment.created_by.name}}</a>
- </div>
- <div v-html="comment.html" v-if="comment.active" class="comment-body" v-bind:class="{ 'comment-inactive' : !comment.active }">
-
- </div>
- <div v-if="!comment.active" class="comment-body comment-inactive">
- {{ trans('entities.comment_deleted') }}
- </div>
- <div class="comment-actions">
- <ul>
- <li v-if="(level < 4 && canComment)">
- <a href="#" comment="comment" v-on:click.prevent="replyComment">{{ trans('entities.comment_reply') }}</a>
- </li>
- <li v-if="canEditOrDelete('update')">
- <a href="#" comment="comment" v-on:click.prevent="editComment">{{ trans('entities.comment_edit') }}</a>
- </li>
- <li v-if="canEditOrDelete('delete')">
- <a href="#" comment="comment" v-on:click.prevent="deleteComment">{{ trans('entities.comment_delete') }}</a>
- </li>
- <li>{{ trans('entities.comment_create') }}
- <a :title="comment.created.day_time_str" :href="commentHref">{{comment.created.diff}}</a>
- </li>
- <li v-if="comment.updated">
- <span :title="comment.updated.day_time_str">{{trans('entities.comment_updated_text', { updateDiff: comment.updated.diff }) }}
- <a :href="comment.updated_by.profile_url">{{comment.updated_by.name}}</a>
- </span>
- </li>
- </ul>
- </div>
- <div v-if="showEditor">
- <comment-reply :page-id="comment.page_id" :comment-obj="comment"
- v-on:editor-removed.stop.prevent="hideComment"
- v-on:comment-replied.stop="commentReplied(...arguments)"
- v-on:comment-edited.stop="commentEdited(...arguments)"
- v-on:comment-added.stop="commentAdded"
- :is-reply="isReply" :is-edit="isEdit">
- </comment-reply>
- </div>
- <comment v-for="(comment, index) in comments" :initial-comment="comment" :index="index"
- :level="nextLevel" :key="comment.id" :permissions="permissions" :current-user-id="currentUserId"
- v-on:comment-added.stop="commentAdded"></comment>
-
- </div>
- </div>
-</div>
-`;
-
-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
- }
-};
-
+++ /dev/null
-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
'image-manager': require('./image-manager'),
'tag-manager': require('./tag-manager'),
'attachment-manager': require('./attachment-manager'),
- 'page-comments': require('./page-comments')
};
window.vues = {};
-.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 {
+.comment-box {
+ border: 1px solid #DDD;
+ margin-bottom: $-s;
+ border-radius: 3px;
+ .content {
+ padding: $-s;
+ }
+ .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;
}
}
// 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
'edit' => 'Edit',
'sort' => 'Sort',
'move' => 'Move',
+ 'reply' => 'Reply',
'delete' => 'Delete',
'search' => 'Search',
'search_clear' => 'Clear Search',
*/
'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'
--- /dev/null
+<div class="comment-box" comment="{{ $comment->id }}" id="comment{{$comment->local_id}}">
+ <div class="header">
+
+ <div class="float right actions">
+ @if(userCan('comment-update', $comment))
+ <button type="button" class="text-button" action="edit" title="{{ trans('common.edit') }}"><i class="zmdi zmdi-edit"></i></button>
+ @endif
+ @if(userCan('comment-create-all'))
+ <button type="button" class="text-button" action="reply" title="{{ trans('common.reply') }}"><i class="zmdi zmdi-mail-reply-all"></i></button>
+ @endif
+ @if(userCan('comment-delete', $comment))
+ <button type="button" class="text-button" action="delete" title="{{ trans('common.delete') }}"><i class="zmdi zmdi-delete"></i></button>
+ @endif
+ </div>
+
+ <a href="#comment{{$comment->local_id}}" class="text-muted">#{{$comment->local_id}}</a>
+
+ <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}">
+
+ <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->name }}</a>
+ {{--TODO - Account for deleted user--}}
+ <span title="{{ $comment->created_at }}">
+ {{ trans('entities.comment_created', ['createDiff' => $comment->created]) }}
+ </span>
+ @if($comment->isUpdated())
+ <span title="{{ $comment->updated_at }}">
+ •
+ {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy->name]) }}
+ </span>
+ @endif
+ </div>
+ <div comment-content class="content">
+ {!! $comment->html !!}
+ </div>
+
+ @if(userCan('comment-update', $comment))
+ <div comment-edit-container style="display: none;" class="content">
+ <form novalidate>
+ <div class="form-group">
+ <textarea name="markdown" rows="3" v-model="comment.text" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $comment->text }}</textarea>
+ </div>
+ <div class="form-group text-right">
+ <button type="button" class="button outline" action="closeUpdateForm">{{ trans('common.cancel') }}</button>
+ <button type="submit" class="button pos">{{ trans('entities.comment_save') }}</button>
+ </div>
+ </form>
+ </div>
+ @endif
+
+</div>
\ No newline at end of file
-<div id="page-comments" page-id="<?= $page->id ?>" class="comments-list" v-cloak>
- <h3>@{{totalCommentsStr}}</h3>
- <hr>
- <comment v-for="(comment, index) in comments" :initial-comment="comment" :index="index" :level=1
- v-on:comment-added.stop="commentAdded"
- :current-user-id="currentUserId" :key="comment.id" :permissions="permissions"></comment>
- <div v-if="canComment">
- <comment-reply v-on:comment-added.stop="commentAdded" :page-id="<?= $page->id ?>">
- </comment-reply>
- </div>
+<div page-comments page-id="{{ $page->id }}" ng-non-bindable class="comments-list">
+ <h3 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h3>
+
+ <div class="comment-container" comment-container>
+ @foreach($page->comments as $comment)
+ @include('comments.comment', ['comment' => $comment])
+ @endforeach
+ </div>
+
+
+ @if(userCan('comment-create-all'))
+
+ <div class="comment-box" comment-box style="display:none;">
+ <div class="header"><i class="zmdi zmdi-comment"></i> {{ trans('entities.comment_new') }}</div>
+ <div class="content" comment-form-container>
+ <form novalidate>
+ <div class="form-group">
+ <textarea name="markdown" rows="3" v-model="comment.text" placeholder="{{ trans('entities.comment_placeholder') }}"></textarea>
+ </div>
+ <div class="form-group text-right">
+ <button type="button" class="button outline" action="hideForm">{{ trans('common.cancel') }}</button>
+ <button type="submit" class="button pos">{{ trans('entities.comment_save') }}</button>
+ </div>
+ </form>
+ </div>
+ </div>
+
+ <div class="form-group" comment-add-button>
+ <button type="button" action="addComment" class="button outline">Add Comment</button>
+ </div>
+ @endif
+
</div>
\ No newline at end of file
@include('pages/page-display')
</div>
+
<div class="container small">
- @include('comments/comments', ['pageId' => $page->id])
+ @include('comments/comments', ['page' => $page])
</div>
@stop
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');