sudo: false
language: php
php:
- - 7.0
+ - 7.0.7
cache:
directories:
-<?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
+ * @param bool $orderByCreated
+ * @return MorphMany
+ */
+ public function comments($orderByCreated = true)
+ {
+ $query = $this->morphMany(Comment::class, 'entity');
+ return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
+ }
+
/**
* Get the related search terms.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
<?php namespace BookStack\Http\Controllers;
+use Activity;
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->only(['html', 'text', 'parent_id']));
+ Activity::add($page, 'commented_on', $page->book->id);
+ 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->only(['html', 'text']));
+ 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(false)->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')
{
});
$factory->define(BookStack\Comment::class, function($faker) {
- $text = $faker->paragraph(3);
+ $text = $faker->paragraph(1);
$html = '<p>' . $text. '</p>';
return [
'html' => $html,
'text' => $text,
- 'active' => 1
+ 'parent_id' => null
];
});
\ No newline at end of file
{
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
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);
-
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->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)->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);
- });
+ $pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
$chapter->pages()->saveMany($pages);
});
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
-
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}
'entity-selector': require('./entity-selector'),
'sidebar': require('./sidebar'),
'page-picker': require('./page-picker'),
+ 'page-comments': require('./page-comments'),
};
window.components = {};
let componentNames = Object.keys(componentMapping);
+initAll();
-for (let i = 0, len = componentNames.length; i < len; i++) {
- let name = componentNames[i];
- let elems = document.querySelectorAll(`[${name}]`);
- if (elems.length === 0) continue;
+/**
+ * Initialize components of the given name within the given element.
+ * @param {String} componentName
+ * @param {HTMLElement|Document} parentElement
+ */
+function initComponent(componentName, parentElement) {
+ let elems = parentElement.querySelectorAll(`[${componentName}]`);
+ if (elems.length === 0) return;
- let component = componentMapping[name];
- if (typeof window.components[name] === "undefined") window.components[name] = [];
+ let component = componentMapping[componentName];
+ if (typeof window.components[componentName] === "undefined") window.components[componentName] = [];
for (let j = 0, jLen = elems.length; j < jLen; j++) {
- let instance = new component(elems[j]);
- if (typeof elems[j].components === 'undefined') elems[j].components = {};
- elems[j].components[name] = instance;
- window.components[name].push(instance);
+ let instance = new component(elems[j]);
+ if (typeof elems[j].components === 'undefined') elems[j].components = {};
+ elems[j].components[componentName] = instance;
+ window.components[componentName].push(instance);
}
-}
\ No newline at end of file
+}
+
+/**
+ * Initialize all components found within the given element.
+ * @param parentElement
+ */
+function initAll(parentElement) {
+ if (typeof parentElement === 'undefined') parentElement = document;
+ for (let i = 0, len = componentNames.length; i < len; i++) {
+ initComponent(componentNames[i], parentElement);
+ }
+}
+
+window.components.init = initAll;
\ No newline at end of file
--- /dev/null
+const MarkdownIt = require("markdown-it");
+const md = new MarkdownIt({ html: false });
+
+class PageComments {
+
+ constructor(elem) {
+ this.elem = elem;
+ this.pageId = Number(elem.getAttribute('page-id'));
+ this.editingComment = null;
+ this.parentId = null;
+
+ this.container = elem.querySelector('[comment-container]');
+ this.formContainer = elem.querySelector('[comment-form-container]');
+
+ if (this.formContainer) {
+ this.form = this.formContainer.querySelector('form');
+ this.formInput = this.form.querySelector('textarea');
+ this.form.addEventListener('submit', this.saveComment.bind(this));
+ }
+
+ this.elem.addEventListener('click', this.handleAction.bind(this));
+ this.elem.addEventListener('submit', this.updateComment.bind(this));
+ }
+
+ handleAction(event) {
+ let actionElem = event.target.closest('[action]');
+ if (event.target.matches('a[href^="#"]')) {
+ let id = event.target.href.split('#')[1];
+ window.scrollAndHighlight(document.querySelector('#' + id));
+ }
+ if (actionElem === null) return;
+ event.preventDefault();
+
+ 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(actionElem.closest('[comment]'));
+ if (action === 'remove-reply-to') this.removeReplyTo();
+ }
+
+ 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';
+ let textArea = commentElem.querySelector('[comment-edit-container] textarea');
+ let lineCount = textArea.value.split('\n').length;
+ textArea.style.height = (lineCount * 20) + 'px';
+ 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.parentId || null,
+ };
+ this.showLoading(form);
+ 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'));
+ window.components.init(this.editingComment);
+ this.closeUpdateForm();
+ this.editingComment = null;
+ this.hideLoading(form);
+ });
+ }
+
+ deleteComment(commentElem) {
+ let id = commentElem.getAttribute('comment');
+ this.showLoading(commentElem.querySelector('[comment-content]'));
+ 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.parentId || null,
+ };
+ this.showLoading(this.form);
+ window.$http.post(window.baseUrl(`/ajax/page/${this.pageId}/comment`), reqData).then(resp => {
+ let newComment = document.createElement('div');
+ newComment.innerHTML = resp.data;
+ let newElem = newComment.children[0];
+ this.container.appendChild(newElem);
+ window.components.init(newElem);
+ 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();
+ this.removeReplyTo();
+ this.hideLoading(this.form);
+ }
+
+ showForm() {
+ this.formContainer.style.display = 'block';
+ this.formContainer.parentNode.style.display = 'block';
+ this.elem.querySelector('[comment-add-button]').style.display = 'none';
+ this.formInput.focus();
+ window.scrollToElement(this.formInput);
+ }
+
+ hideForm() {
+ this.formContainer.style.display = 'none';
+ this.formContainer.parentNode.style.display = 'none';
+ this.elem.querySelector('[comment-add-button]').style.display = 'block';
+ }
+
+ setReply(commentElem) {
+ this.showForm();
+ this.parentId = Number(commentElem.getAttribute('local-id'));
+ this.elem.querySelector('[comment-form-reply-to]').style.display = 'block';
+ let replyLink = this.elem.querySelector('[comment-form-reply-to] a');
+ replyLink.textContent = `#${this.parentId}`;
+ replyLink.href = `#comment${this.parentId}`;
+ }
+
+ removeReplyTo() {
+ this.parentId = null;
+ this.elem.querySelector('[comment-form-reply-to]').style.display = 'none';
+ }
+
+ showLoading(formElem) {
+ let groups = formElem.querySelectorAll('.form-group');
+ for (let i = 0, len = groups.length; i < len; i++) {
+ groups[i].style.display = 'none';
+ }
+ formElem.querySelector('.form-group.loading').style.display = 'block';
+ }
+
+ hideLoading(formElem) {
+ let groups = formElem.querySelectorAll('.form-group');
+ for (let i = 0, len = groups.length; i < len; i++) {
+ groups[i].style.display = 'block';
+ }
+ formElem.querySelector('.form-group.loading').style.display = 'none';
+ }
+
+}
+
+module.exports = PageComments;
\ No newline at end of file
link: function (scope, element, attrs) {
function tinyMceSetup(editor) {
- editor.on('ExecCommand change NodeChange ObjectResized', (e) => {
+ editor.on('ExecCommand change input NodeChange ObjectResized', (e) => {
let content = editor.getContent();
$timeout(() => {
scope.mceModel = content;
});
editor.on('keydown', (event) => {
- scope.$emit('editor-keydown', event);
+ if (event.keyCode === 83 && (navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) {
+ event.preventDefault();
+ scope.$emit('save-draft', event);
+ }
});
editor.on('init', (e) => {
extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');};
extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');};
extraKeys[`Shift-${metaKey}-E`] = function(cm) {wrapSelection('`', '`');};
- extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</div>');};
+ extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</p>');};
cm.setOption('extraKeys', extraKeys);
// Update data on content change
}
cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
- cm.setCursor({line: cursor.line, ch: cursor.ch + (newLineContent.length - lineLen)});
+ cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
}
function wrapSelection(start, end) {
let selection = cm.getSelection();
if (selection === '') return wrapLine(start, end);
+
let newSelection = selection;
let frontDiff = 0;
let endDiff = 0;
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");
//Global jQuery Config & Extensions
+/**
+ * Scroll the view to a specific element.
+ * @param {HTMLElement} element
+ */
+window.scrollToElement = function(element) {
+ if (!element) return;
+ let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
+ let top = element.getBoundingClientRect().top + offset;
+ $('html, body').animate({
+ scrollTop: top - 60 // Adjust to change final scroll position top margin
+ }, 300);
+};
+
+/**
+ * Scroll and highlight an element.
+ * @param {HTMLElement} element
+ */
+window.scrollAndHighlight = function(element) {
+ if (!element) return;
+ window.scrollToElement(element);
+ let color = document.getElementById('custom-styles').getAttribute('data-color-light');
+ let initColor = window.getComputedStyle(element).getPropertyValue('background-color');
+ element.style.backgroundColor = color;
+ setTimeout(() => {
+ element.classList.add('selectFade');
+ element.style.backgroundColor = initColor;
+ }, 10);
+ setTimeout(() => {
+ element.classList.remove('selectFade');
+ element.style.backgroundColor = '';
+ }, 3000);
+};
+
// Smooth scrolling
jQuery.fn.smoothScrollTo = function () {
if (this.length === 0) return;
- $('html, body').animate({
- scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
- }, 300); // Adjust to change animations speed (ms)
+ window.scrollToElement(this[0]);
return this;
};
let idElem = document.getElementById(text);
$('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', '');
if (idElem !== null) {
- let $idElem = $(idElem);
- let color = $('#custom-styles').attr('data-color-light');
- $idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
- setTimeout(() => {
- $idElem.addClass('anim').addClass('selectFade').css('background-color', '');
- setTimeout(() => {
- $idElem.removeClass('selectFade');
- }, 3000);
- }, 100);
+ window.scrollAndHighlight(idElem);
} else {
$('.page-content').find(':contains("' + text + '")').smoothScrollTo();
}
unstickTree();
}
});
-
- // in order to call from other places.
- window.setupPageShow.goToText = goToText;
};
module.exports = setupPageShow;
\ No newline at end of file
* @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);
+ 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 = {};
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
-.anim.selectFade {
+.selectFade {
transition: background-color ease-in-out 3000ms;
}
\ No newline at end of file
+++ /dev/null
-.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;
- }
-}
margin-right: $-xs;
text-decoration: underline;
}
+}
+
+.comment-box {
+ border: 1px solid #DDD;
+ margin-bottom: $-s;
+ border-radius: 3px;
+ .content {
+ padding: $-s;
+ font-size: 0.666em;
+ }
+ .content p {
+ font-size: $fs-m;
+ margin: .5em 0;
+ }
+ .reply-row {
+ padding: $-xs $-s;
+ }
+}
+
+.comment-box .header {
+ padding: $-xs $-s;
+ background-color: #f8f8f8;
+ border-bottom: 1px solid #DDD;
+ .meta {
+ 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;
+ }
}
\ No newline at end of file
box-shadow: none;
transform: translate3d(-330px, 0, 0);
transition: transform ease-in-out 120ms;
+ display: flex;
+ flex-direction: column;
}
.flex.sidebar.open {
box-shadow: 1px 2px 2px 1px rgba(0,0,0,.10);
opacity: 1;
}
}
+ .sidebar .scroll-body {
+ flex: 1;
+ overflow-y: scroll;
+ }
#sidebar .scroll-body.fixed {
width: auto !important;
}
&.small {
max-width: 840px;
}
+ &.nopad {
+ padding-left: 0;
+ padding-right: 0;
+ }
}
.row {
}
li.padded {
padding: $-xs $-m;
+ line-height: 1.2;
}
a {
display: block;
}
}
}
+}
+.page-content.mce-content-body p {
+ line-height: 1.6;
}
\ No newline at end of file
// 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
@import "header";
@import "lists";
@import "pages";
-@import "comments";
table {
border-spacing: 0;
@import "header";
@import "lists";
@import "pages";
-@import "comments";
[v-cloak] {
display: none; opacity: 0;
.loading-container {
position: relative;
display: block;
- height: $loadingSize;
margin: $-xl auto;
> div {
width: $loadingSize;
border-radius: $loadingSize;
display: inline-block;
vertical-align: top;
- transform: translate3d(0, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ margin-top: $-xs;
animation-name: loadingBob;
animation-duration: 1.4s;
animation-iteration-count: infinite;
background-color: $color-book;
animation-delay: 0s;
}
- > div:last-child {
+ > div:last-of-type {
left: $loadingSize+$-xs;
background-color: $color-chapter;
animation-delay: 0.6s;
}
+ > span {
+ margin-left: $-s;
+ font-style: italic;
+ color: #888;
+ vertical-align: top;
+ }
}
'meta_created_name' => 'Erstellt: :timeLength von :user',
'meta_updated' => 'Zuletzt aktualisiert: :timeLength',
'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',
- 'x_pages' => ':count Seiten',
'entity_select' => 'Eintrag auswählen',
'images' => 'Bilder',
'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
*/
'book' => 'Buch',
'books' => 'Bücher',
+ 'x_books' => ':count Buch|:count Bücher',
'books_empty' => 'Keine Bücher vorhanden',
'books_popular' => 'Beliebte Bücher',
'books_recent' => 'Kürzlich angesehene Bücher',
+ 'books_new' => 'Neue Bücher',
'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.',
+ 'books_new_empty' => 'Die neusten Bücher werden hier angezeigt.',
'books_create' => 'Neues Buch erstellen',
'books_delete' => 'Buch löschen',
'books_delete_named' => 'Buch ":bookName" löschen',
*/
'chapter' => 'Kapitel',
'chapters' => 'Kapitel',
+ 'x_chapters' => ':count Kapitel',
'chapters_popular' => 'Beliebte Kapitel',
'chapters_new' => 'Neues Kapitel',
'chapters_create' => 'Neues Kapitel anlegen',
*/
'page' => 'Seite',
'pages' => 'Seiten',
+ 'x_pages' => ':count Seite|:count Seiten',
'pages_popular' => 'Beliebte Seiten',
'pages_new' => 'Neue Seite',
'pages_attachments' => 'Anhänge',
'pages_move_success' => 'Seite nach ":parentName" verschoben',
'pages_permissions' => 'Seiten Berechtigungen',
'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
+ 'pages_revision' => 'Version',
'pages_revisions' => 'Seitenversionen',
'pages_revisions_named' => 'Seitenversionen von ":pageName"',
'pages_revision_named' => 'Seitenversion von ":pageName"',
'profile_not_created_books' => ':userName hat noch keine Bücher erstellt.',
/**
- * Comnents
+ * Comments
*/
'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_count' => '{0} Keine Kommentare|{1} 1 Kommentar|[2,*] :count Kommentare',
'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' => 'Der Inhalt des Kommentars wird entfernt. Bist du sicher, dass du diesen Kommentar löschen möchtest?',
- 'comment_create' => 'Erstellt'
-
+ 'comment_saving' => 'Kommentar wird gespeichert...',
+ 'comment_deleting' => 'Kommentar wird gelöscht...',
+ 'comment_new' => 'Neuer Kommentar',
+ 'comment_created' => ':createDiff kommentiert',
+ 'comment_updated' => ':updateDiff aktualisiert von :username',
+ 'comment_deleted_success' => 'Kommentar gelöscht',
+ 'comment_created_success' => 'Kommentar hinzugefügt',
+ 'comment_updated_success' => 'Kommentar aktualisiert',
+ 'comment_delete_confirm' => 'Möchten Sie diesen Kommentar wirklich löschen?',
+ 'comment_in_reply_to' => 'Antwort auf :commentId',
];
'book_sort' => 'sorted book',
'book_sort_notification' => 'Book Successfully Re-sorted',
+ // Other
+ 'commented_on' => 'commented on',
];
'edit' => 'Edit',
'sort' => 'Sort',
'move' => 'Move',
+ 'reply' => 'Reply',
'delete' => 'Delete',
'search' => 'Search',
'search_clear' => 'Clear Search',
'meta_created_name' => 'Created :timeLength by :user',
'meta_updated' => 'Updated :timeLength',
'meta_updated_name' => 'Updated :timeLength by :user',
- 'x_pages' => ':count Pages',
'entity_select' => 'Entity Select',
'images' => 'Images',
'my_recent_drafts' => 'My Recent Drafts',
*/
'book' => 'Book',
'books' => 'Books',
+ 'x_books' => ':count Book|:count Books',
'books_empty' => 'No books have been created',
'books_popular' => 'Popular Books',
'books_recent' => 'Recent Books',
*/
'chapter' => 'Chapter',
'chapters' => 'Chapters',
+ 'x_chapters' => ':count Chapter|:count Chapters',
'chapters_popular' => 'Popular Chapters',
'chapters_new' => 'New Chapter',
'chapters_create' => 'Create New Chapter',
*/
'page' => 'Page',
'pages' => 'Pages',
+ 'x_pages' => ':count Page|:count Pages',
'pages_popular' => 'Popular Pages',
'pages_new' => 'New Page',
'pages_attachments' => 'Attachments',
*/
'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_delete_confirm' => 'This will remove the contents of the comment. Are you sure you want to delete this comment?',
- 'comment_create' => 'Created'
-
+ 'comment_saving' => 'Saving comment...',
+ 'comment_deleting' => 'Deleting comment...',
+ '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' => 'Are you sure you want to delete this comment?',
+ 'comment_in_reply_to' => 'In reply to :commentId',
];
\ No newline at end of file
*/
'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_placeholder' => 'Introduzca sus comentarios aquí',
'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'
];
*/
'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_placeholder' => 'Entrez vos commentaires ici',
'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éé'
];
*/
'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_placeholder' => 'Vul hier uw reacties in',
'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
*/
'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_placeholder' => 'Digite seus comentários aqui',
'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
*/
'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_placeholder' => 'Tu zadajte svoje pripomienky',
'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é'
];
@if(!isset($hidePages) && count($chapter->pages) > 0)
- <p chapter-toggle class="text-muted"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $chapter->pages->count()]) }}</span></p>
+ <p chapter-toggle class="text-muted"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans_choice('entities.x_pages', $chapter->pages->count()) }}</span></p>
<div class="inset-list">
@foreach($chapter->pages as $page)
<h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5>
--- /dev/null
+<div class="comment-box" comment="{{ $comment->id }}" local-id="{{$comment->local_id}}" parent-id="{{$comment->parent_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))
+
+ <div dropdown class="dropdown-container">
+ <button type="button" dropdown-toggle class="text-button" title="{{ trans('common.delete') }}"><i class="zmdi zmdi-delete"></i></button>
+ <ul>
+ <li class="padded"><small class="text-muted">{{trans('entities.comment_delete_confirm')}}</small></li>
+ <li><a action="delete" class="text-button neg" ><i class="zmdi zmdi-delete"></i>{{ trans('common.delete') }}</a></li>
+ </ul>
+ </div>
+ @endif
+ </div>
+
+ <div class="meta">
+ <a href="#comment{{$comment->local_id}}" class="text-muted">#{{$comment->local_id}}</a>
+
+ @if ($comment->createdBy)
+ <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}">
+
+ <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->name }}</a>
+ @else
+ <span>{{ trans('common.deleted_user') }}</span>
+ @endif
+ <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? $comment->updatedBy->name : trans('common.deleted_user')]) }}
+ </span>
+ @endif
+ </div>
+
+ </div>
+
+ @if ($comment->parent_id)
+ <div class="reply-row primary-background-light text-muted">
+ {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href="#comment'.$comment->parent_id.'">#'.$comment->parent_id.'</a>']) !!}
+ </div>
+ @endif
+
+ <div comment-content class="content">
+ <div class="form-group loading" style="display: none;">
+ @include('partials.loading-icon', ['text' => trans('entities.comment_deleting')])
+ </div>
+ {!! $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>
+ <div class="form-group loading" style="display: none;">
+ @include('partials.loading-icon', ['text' => trans('entities.comment_saving')])
+ </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 comment-form-reply-to class="reply-row primary-background-light text-muted" style="display: none;">
+ <button class="text-button float right" action="remove-reply-to">{{ trans('common.remove') }}</button>
+ {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!}
+ </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>
+ <div class="form-group loading" style="display: none;">
+ @include('partials.loading-icon', ['text' => trans('entities.comment_saving')])
+ </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])
+
+ <div class="container small nopad">
+ @include('comments/comments', ['page' => $page])
</div>
@stop
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
<p chapter-toggle class="text-muted @if($bookChild->matchesOrContains($current)) open @endif">
- <i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $bookChild->pages->count()]) }}</span>
+ <i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans_choice('entities.x_pages', $bookChild->pages->count()) }}</span>
</p>
<ul class="menu sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif">
@foreach($bookChild->pages as $childPage)
<div></div>
<div></div>
<div></div>
+ @if(isset($text))
+ <span>{{$text}}</span>
+ @endif
</div>
\ No newline at end of file
<div class="col-md-5 text-bigger" id="content-counts">
<div class="text-muted">{{ trans('entities.profile_created_content') }}</div>
<div class="text-book">
- <i class="zmdi zmdi-book zmdi-hc-fw"></i> {{ $assetCounts['books'] }} {{ str_plural(trans('entities.book'), $assetCounts['books']) }}
+ <i class="zmdi zmdi-book zmdi-hc-fw"></i> {{ trans_choice('entities.x_books', $assetCounts['books']) }}
</div>
<div class="text-chapter">
- <i class="zmdi zmdi-collection-bookmark zmdi-hc-fw"></i> {{ $assetCounts['chapters'] }} {{ str_plural(trans('entities.chapter'), $assetCounts['chapters']) }}
+ <i class="zmdi zmdi-collection-bookmark zmdi-hc-fw"></i> {{ trans_choice('entities.x_chapters', $assetCounts['chapters']) }}
</div>
<div class="text-page">
- <i class="zmdi zmdi-file-text zmdi-hc-fw"></i> {{ $assetCounts['pages'] }} {{ str_plural(trans('entities.page'), $assetCounts['pages']) }}
+ <i class="zmdi zmdi-file-text zmdi-hc-fw"></i> {{ trans_choice('entities.x_pages', $assetCounts['pages']) }}
</div>
</div>
</div>
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');
use BookStack\Page;
use BookStack\Comment;
-class CommentTest extends BrowserKitTest
+class CommentTest extends TestCase
{
public function test_add_comment()
{
$this->asAdmin();
- $page = $this->getPage();
+ $page = Page::first();
- $this->addComment($page);
- }
+ $comment = factory(Comment::class)->make(['parent_id' => 2]);
+ $resp = $this->postJson("/ajax/page/$page->id/comment", $comment->getAttributes());
- public function test_comment_reply()
- {
- $this->asAdmin();
- $page = $this->getPage();
+ $resp->assertStatus(200);
+ $resp->assertSee($comment->text);
- // add a normal comment
- $createdComment = $this->addComment($page);
+ $pageResp = $this->get($page->getUrl());
+ $pageResp->assertSee($comment->text);
- // reply to the added comment
- $this->addComment($page, $createdComment['id']);
+ $this->assertDatabaseHas('comments', [
+ 'local_id' => 1,
+ 'entity_id' => $page->id,
+ 'entity_type' => 'BookStack\\Page',
+ 'text' => $comment->text,
+ 'parent_id' => 2
+ ]);
}
public function test_comment_edit()
{
$this->asAdmin();
- $page = $this->getPage();
-
- $createdComment = $this->addComment($page);
- $comment = [
- 'id' => $createdComment['id'],
- 'page_id' => $createdComment['page_id']
- ];
- $this->updateComment($comment);
+ $page = Page::first();
+
+ $comment = factory(Comment::class)->make();
+ $this->postJson("/ajax/page/$page->id/comment", $comment->getAttributes());
+
+ $comment = $page->comments()->first();
+ $newText = 'updated text content';
+ $resp = $this->putJson("/ajax/comment/$comment->id", [
+ 'text' => $newText,
+ 'html' => '<p>'.$newText.'</p>',
+ ]);
+
+ $resp->assertStatus(200);
+ $resp->assertSee($newText);
+ $resp->assertDontSee($comment->text);
+
+ $this->assertDatabaseHas('comments', [
+ 'text' => $newText,
+ 'entity_id' => $page->id
+ ]);
}
public function test_comment_delete()
{
$this->asAdmin();
- $page = $this->getPage();
-
- $createdComment = $this->addComment($page);
-
- $this->deleteComment($createdComment['id']);
- }
-
- private function getPage() {
$page = Page::first();
- return $page;
- }
-
- private function addComment($page, $parentCommentId = null) {
$comment = factory(Comment::class)->make();
- $url = "/ajax/page/$page->id/comment/";
- $request = [
- 'text' => $comment->text,
- 'html' => $comment->html
- ];
- if (!empty($parentCommentId)) {
- $request['parent_id'] = $parentCommentId;
- }
- $this->call('POST', $url, $request);
-
- $createdComment = $this->checkResponse();
- return $createdComment;
- }
-
- private function updateComment($comment) {
- $tmpComment = factory(Comment::class)->make();
- $url = '/ajax/page/' . $comment['page_id'] . '/comment/ ' . $comment['id'];
- $request = [
- 'text' => $tmpComment->text,
- 'html' => $tmpComment->html
- ];
-
- $this->call('PUT', $url, $request);
-
- $updatedComment = $this->checkResponse();
- return $updatedComment;
- }
-
- private function deleteComment($commentId) {
- // Route::delete('/ajax/comment/{id}', 'CommentController@destroy');
- $url = '/ajax/comment/' . $commentId;
- $this->call('DELETE', $url);
-
- $deletedComment = $this->checkResponse();
- return $deletedComment;
- }
-
- private function checkResponse() {
- $expectedResp = [
- 'status' => 'success'
- ];
+ $this->postJson("/ajax/page/$page->id/comment", $comment->getAttributes());
- $this->assertResponseOk();
- $this->seeJsonContains($expectedResp);
+ $comment = $page->comments()->first();
- $resp = $this->decodeResponseJson();
- $createdComment = $resp['comment'];
- $this->assertArrayHasKey('id', $createdComment);
+ $resp = $this->delete("/ajax/comment/$comment->id");
+ $resp->assertStatus(200);
- return $createdComment;
+ $this->assertDatabaseMissing('comments', [
+ 'id' => $comment->id
+ ]);
}
}
$page = Page::first();
$viewerRole = \BookStack\Role::getRole('viewer');
$viewer = $this->getViewer();
- $this->actingAs($viewer)->visit($page->getUrl())->assertResponseOk();
+ $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(200);
$this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [
'display_name' => $viewerRole->display_name,
$this->giveUserPermissions($this->user, ['comment-create-all']);
$this->actingAs($this->user)->addComment($ownPage);
- $this->assertResponseOk(200)->seeJsonContains(['status' => 'success']);
+ $this->assertResponseStatus(200);
}
public function test_comment_update_own_permission () {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->giveUserPermissions($this->user, ['comment-create-all']);
- $comment = $this->actingAs($this->user)->addComment($ownPage);
+ $commentId = $this->actingAs($this->user)->addComment($ownPage);
// no comment-update-own
- $this->actingAs($this->user)->updateComment($ownPage, $comment['id']);
+ $this->actingAs($this->user)->updateComment($commentId);
$this->assertResponseStatus(403);
$this->giveUserPermissions($this->user, ['comment-update-own']);
// now has comment-update-own
- $this->actingAs($this->user)->updateComment($ownPage, $comment['id']);
- $this->assertResponseOk()->seeJsonContains(['status' => 'success']);
+ $this->actingAs($this->user)->updateComment($commentId);
+ $this->assertResponseStatus(200);
}
public function test_comment_update_all_permission () {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
- $comment = $this->asAdmin()->addComment($ownPage);
+ $commentId = $this->asAdmin()->addComment($ownPage);
// no comment-update-all
- $this->actingAs($this->user)->updateComment($ownPage, $comment['id']);
+ $this->actingAs($this->user)->updateComment($commentId);
$this->assertResponseStatus(403);
$this->giveUserPermissions($this->user, ['comment-update-all']);
// now has comment-update-all
- $this->actingAs($this->user)->updateComment($ownPage, $comment['id']);
- $this->assertResponseOk()->seeJsonContains(['status' => 'success']);
+ $this->actingAs($this->user)->updateComment($commentId);
+ $this->assertResponseStatus(200);
}
public function test_comment_delete_own_permission () {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->giveUserPermissions($this->user, ['comment-create-all']);
- $comment = $this->actingAs($this->user)->addComment($ownPage);
+ $commentId = $this->actingAs($this->user)->addComment($ownPage);
// no comment-delete-own
- $this->actingAs($this->user)->deleteComment($comment['id']);
+ $this->actingAs($this->user)->deleteComment($commentId);
$this->assertResponseStatus(403);
$this->giveUserPermissions($this->user, ['comment-delete-own']);
// now has comment-update-own
- $this->actingAs($this->user)->deleteComment($comment['id']);
- $this->assertResponseOk()->seeJsonContains(['status' => 'success']);
+ $this->actingAs($this->user)->deleteComment($commentId);
+ $this->assertResponseStatus(200);
}
public function test_comment_delete_all_permission () {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
- $comment = $this->asAdmin()->addComment($ownPage);
+ $commentId = $this->asAdmin()->addComment($ownPage);
// no comment-delete-all
- $this->actingAs($this->user)->deleteComment($comment['id']);
+ $this->actingAs($this->user)->deleteComment($commentId);
$this->assertResponseStatus(403);
$this->giveUserPermissions($this->user, ['comment-delete-all']);
// now has comment-delete-all
- $this->actingAs($this->user)->deleteComment($comment['id']);
- $this->assertResponseOk()->seeJsonContains(['status' => 'success']);
+ $this->actingAs($this->user)->deleteComment($commentId);
+ $this->assertResponseStatus(200);
}
private function addComment($page) {
$comment = factory(\BookStack\Comment::class)->make();
- $url = "/ajax/page/$page->id/comment/";
+ $url = "/ajax/page/$page->id/comment";
$request = [
'text' => $comment->text,
'html' => $comment->html
];
- $this->json('POST', $url, $request);
- $resp = $this->decodeResponseJson();
- if (isset($resp['comment'])) {
- return $resp['comment'];
- }
- return null;
+ $this->postJson($url, $request);
+ $comment = $page->comments()->first();
+ return $comment === null ? null : $comment->id;
}
- private function updateComment($page, $commentId) {
+ private function updateComment($commentId) {
$comment = factory(\BookStack\Comment::class)->make();
- $url = "/ajax/page/$page->id/comment/$commentId";
+ $url = "/ajax/comment/$commentId";
$request = [
'text' => $comment->text,
'html' => $comment->html
];
- return $this->json('PUT', $url, $request);
+ return $this->putJson($url, $request);
}
private function deleteComment($commentId) {