mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-28 11:43:40 +08:00
Merge branch 'Abijeet-master'
This commit is contained in:
commit
79cfd39fde
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -8,16 +8,15 @@ Homestead.yaml
|
|||
/public/css
|
||||
/public/js
|
||||
/public/bower
|
||||
/public/build/
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
/storage/debugbar
|
||||
.phpstorm.meta.php
|
||||
yarn.lock
|
||||
/bin
|
||||
nbproject
|
||||
.buildpath
|
||||
|
||||
.project
|
||||
|
||||
.settings/org.eclipse.wst.common.project.facet.core.xml
|
||||
|
||||
.settings/org.eclipse.php.core.prefs
|
||||
|
|
96
app/Comment.php
Normal file
96
app/Comment.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
class Comment extends Ownable
|
||||
{
|
||||
public $sub_comments = [];
|
||||
protected $fillable = ['text', 'html', 'parent_id'];
|
||||
protected $appends = ['created', 'updated', 'sub_comments'];
|
||||
/**
|
||||
* Get the entity that this comment belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page that this comment is in.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the owner of this comment.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/*
|
||||
* Not being used, but left here because might be used in the future for performance reasons.
|
||||
*/
|
||||
public function getPageComments($pageId) {
|
||||
$query = static::newQuery();
|
||||
$query->join('users AS u', 'comments.created_by', '=', 'u.id');
|
||||
$query->leftJoin('users AS u1', 'comments.updated_by', '=', 'u1.id');
|
||||
$query->leftJoin('images AS i', 'i.id', '=', 'u.image_id');
|
||||
$query->selectRaw('comments.id, text, html, comments.created_by, comments.updated_by, '
|
||||
. 'comments.created_at, comments.updated_at, comments.parent_id, '
|
||||
. 'u.name AS created_by_name, u1.name AS updated_by_name, '
|
||||
. 'i.url AS avatar ');
|
||||
$query->whereRaw('page_id = ?', [$pageId]);
|
||||
$query->orderBy('created_at');
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function getAllPageComments($pageId) {
|
||||
return self::where('page_id', '=', $pageId)->with(['createdBy' => function($query) {
|
||||
$query->select('id', 'name', 'image_id');
|
||||
}, 'updatedBy' => function($query) {
|
||||
$query->select('id', 'name');
|
||||
}, 'createdBy.avatar' => function ($query) {
|
||||
$query->select('id', 'path', 'url');
|
||||
}])->get();
|
||||
}
|
||||
|
||||
public function getCommentById($commentId) {
|
||||
return self::where('id', '=', $commentId)->with(['createdBy' => function($query) {
|
||||
$query->select('id', 'name', 'image_id');
|
||||
}, 'updatedBy' => function($query) {
|
||||
$query->select('id', 'name');
|
||||
}, 'createdBy.avatar' => function ($query) {
|
||||
$query->select('id', 'path', 'url');
|
||||
}])->first();
|
||||
}
|
||||
|
||||
public function getCreatedAttribute() {
|
||||
$created = [
|
||||
'day_time_str' => $this->created_at->toDayDateTimeString(),
|
||||
'diff' => $this->created_at->diffForHumans()
|
||||
];
|
||||
return $created;
|
||||
}
|
||||
|
||||
public function getUpdatedAttribute() {
|
||||
if (empty($this->updated_at)) {
|
||||
return null;
|
||||
}
|
||||
$updated = [
|
||||
'day_time_str' => $this->updated_at->toDayDateTimeString(),
|
||||
'diff' => $this->updated_at->diffForHumans()
|
||||
];
|
||||
return $updated;
|
||||
}
|
||||
|
||||
public function getSubCommentsAttribute() {
|
||||
return $this->sub_comments;
|
||||
}
|
||||
}
|
99
app/Http/Controllers/CommentController.php
Normal file
99
app/Http/Controllers/CommentController.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\CommentRepo;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Comment;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
protected $entityRepo;
|
||||
|
||||
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo, Comment $comment)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->commentRepo = $commentRepo;
|
||||
$this->comment = $comment;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function save(Request $request, $pageId, $commentId = null)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'text' => 'required|string',
|
||||
'html' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return response('Not found', 404);
|
||||
}
|
||||
|
||||
if($page->draft) {
|
||||
// cannot add comments to drafts.
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => trans('errors.cannot_add_comment_to_draft'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
if (empty($commentId)) {
|
||||
// create a new comment.
|
||||
$this->checkPermission('comment-create-all');
|
||||
$comment = $this->commentRepo->create($page, $request->only(['text', 'html', 'parent_id']));
|
||||
$respMsg = trans('entities.comment_created');
|
||||
} else {
|
||||
// update existing comment
|
||||
// get comment by ID and check if this user has permission to update.
|
||||
$comment = $this->comment->findOrFail($commentId);
|
||||
$this->checkOwnablePermission('comment-update', $comment);
|
||||
$this->commentRepo->update($comment, $request->all());
|
||||
$respMsg = trans('entities.comment_updated');
|
||||
}
|
||||
|
||||
$comment = $this->commentRepo->getCommentById($comment->id);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => $respMsg,
|
||||
'comment' => $comment
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public function destroy($id) {
|
||||
$comment = $this->comment->findOrFail($id);
|
||||
$this->checkOwnablePermission('comment-delete', $comment);
|
||||
$this->commentRepo->delete($comment);
|
||||
$updatedComment = $this->commentRepo->getCommentById($comment->id);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.comment_deleted'),
|
||||
'comment' => $updatedComment
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function getPageComments($pageId) {
|
||||
try {
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return response('Not found', 404);
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$comments = $this->commentRepo->getPageComments($pageId);
|
||||
return response()->json(['status' => 'success', 'comments'=> $comments['comments'],
|
||||
'total' => $comments['total'], 'permissions' => [
|
||||
'comment_create' => $this->currentUser->can('comment-create-all'),
|
||||
'comment_update_own' => $this->currentUser->can('comment-update-own'),
|
||||
'comment_update_all' => $this->currentUser->can('comment-update-all'),
|
||||
'comment_delete_all' => $this->currentUser->can('comment-delete-all'),
|
||||
'comment_delete_own' => $this->currentUser->can('comment-delete-own'),
|
||||
], 'user_id' => $this->currentUser->id]);
|
||||
}
|
||||
}
|
|
@ -161,7 +161,7 @@ class PageController extends Controller
|
|||
$pageContent = $this->entityRepo->renderPage($page);
|
||||
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
|
||||
$pageNav = $this->entityRepo->getPageNav($pageContent);
|
||||
|
||||
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages/show', [
|
||||
|
@ -376,7 +376,7 @@ class PageController extends Controller
|
|||
|
||||
$page->fill($revision->toArray());
|
||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
||||
|
||||
|
||||
return view('pages/revision', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
|
|
|
@ -66,6 +66,10 @@ class Page extends Entity
|
|||
return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
public function comments() {
|
||||
return $this->hasMany(Comment::class, 'page_id')->orderBy('created_on', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this page.
|
||||
* @param string|bool $path
|
||||
|
|
105
app/Repos/CommentRepo.php
Normal file
105
app/Repos/CommentRepo.php
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Comment;
|
||||
use BookStack\Page;
|
||||
|
||||
/**
|
||||
* Class TagRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class CommentRepo {
|
||||
/**
|
||||
*
|
||||
* @var Comment $comment
|
||||
*/
|
||||
protected $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;
|
||||
}
|
||||
|
||||
public function update($comment, $input, $activeOnly = true) {
|
||||
$userId = user()->id;
|
||||
$comment->updated_by = $userId;
|
||||
$comment->fill($input);
|
||||
|
||||
// only update active comments by default.
|
||||
$whereClause = ['active' => 1];
|
||||
if (!$activeOnly) {
|
||||
$whereClause = [];
|
||||
}
|
||||
$comment->update($whereClause);
|
||||
return $comment;
|
||||
}
|
||||
|
||||
public function delete($comment) {
|
||||
$comment->text = trans('entities.comment_deleted');
|
||||
$comment->html = trans('entities.comment_deleted');
|
||||
$comment->active = false;
|
||||
$userId = user()->id;
|
||||
$comment->updated_by = $userId;
|
||||
$comment->save();
|
||||
return $comment;
|
||||
}
|
||||
|
||||
public function getPageComments($pageId) {
|
||||
$comments = $this->comment->getAllPageComments($pageId);
|
||||
$index = [];
|
||||
$totalComments = count($comments);
|
||||
$finalCommentList = [];
|
||||
|
||||
// normalizing the response.
|
||||
for ($i = 0; $i < count($comments); ++$i) {
|
||||
$comment = $this->normalizeComment($comments[$i]);
|
||||
$parentId = $comment->parent_id;
|
||||
if (empty($parentId)) {
|
||||
$finalCommentList[] = $comment;
|
||||
$index[$comment->id] = $comment;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($index[$parentId])) {
|
||||
// weird condition should not happen.
|
||||
continue;
|
||||
}
|
||||
if (empty($index[$parentId]->sub_comments)) {
|
||||
$index[$parentId]->sub_comments = [];
|
||||
}
|
||||
array_push($index[$parentId]->sub_comments, $comment);
|
||||
$index[$comment->id] = $comment;
|
||||
}
|
||||
return [
|
||||
'comments' => $finalCommentList,
|
||||
'total' => $totalComments
|
||||
];
|
||||
}
|
||||
|
||||
public function getCommentById($commentId) {
|
||||
return $this->normalizeComment($this->comment->getCommentById($commentId));
|
||||
}
|
||||
|
||||
private function normalizeComment($comment) {
|
||||
if (empty($comment)) {
|
||||
return;
|
||||
}
|
||||
$comment->createdBy->avatar_url = $comment->createdBy->getAvatar(50);
|
||||
$comment->createdBy->profile_url = $comment->createdBy->getProfileUrl();
|
||||
if (!empty($comment->updatedBy)) {
|
||||
$comment->updatedBy->profile_url = $comment->updatedBy->getProfileUrl();
|
||||
}
|
||||
return $comment;
|
||||
}
|
||||
}
|
|
@ -468,7 +468,7 @@ class PermissionService
|
|||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
|
|
|
@ -70,4 +70,14 @@ $factory->define(BookStack\Image::class, function ($faker) {
|
|||
'type' => 'gallery',
|
||||
'uploaded_to' => 0
|
||||
];
|
||||
});
|
||||
|
||||
$factory->define(BookStack\Comment::class, function($faker) {
|
||||
$text = $faker->paragraph(3);
|
||||
$html = '<p>' . $text. '</p>';
|
||||
return [
|
||||
'html' => $html,
|
||||
'text' => $text,
|
||||
'active' => 1
|
||||
];
|
||||
});
|
112
database/migrations/2017_01_01_130541_create_comments_table.php
Normal file
112
database/migrations/2017_01_01_130541_create_comments_table.php
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateCommentsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('comments')) {
|
||||
return;
|
||||
}
|
||||
Schema::create('comments', function (Blueprint $table) {
|
||||
$table->increments('id')->unsigned();
|
||||
$table->integer('page_id')->unsigned();
|
||||
$table->longText('text')->nullable();
|
||||
$table->longText('html')->nullable();
|
||||
$table->integer('parent_id')->unsigned()->nullable();
|
||||
$table->integer('created_by')->unsigned();
|
||||
$table->integer('updated_by')->unsigned()->nullable();
|
||||
$table->index(['page_id', 'parent_id']);
|
||||
$table->timestamps();
|
||||
|
||||
// Get roles with permissions we need to change
|
||||
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
|
||||
|
||||
// Create & attach new entity permissions
|
||||
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||
$entity = 'Comment';
|
||||
foreach ($ops as $op) {
|
||||
$permissionId = DB::table('role_permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $adminRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
|
||||
// Get roles with permissions we need to change
|
||||
/*
|
||||
$editorRole = DB::table('roles')->where('name', '=', 'editor')->first();
|
||||
if (!empty($editorRole)) {
|
||||
$editorRoleId = $editorRole->id;
|
||||
// Create & attach new entity permissions
|
||||
$ops = ['Create All', 'Create Own', 'Update Own', 'Delete Own'];
|
||||
$entity = 'Comment';
|
||||
foreach ($ops as $op) {
|
||||
$permissionId = DB::table('role_permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $editorRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get roles with permissions we need to change
|
||||
$viewerRole = DB::table('roles')->where('name', '=', 'viewer')->first();
|
||||
if (!empty($viewerRole)) {
|
||||
$viewerRoleId = $viewerRole->id;
|
||||
// Create & attach new entity permissions
|
||||
$ops = ['Create All'];
|
||||
$entity = 'Comment';
|
||||
foreach ($ops as $op) {
|
||||
$permissionId = DB::table('role_permissions')->insertGetId([
|
||||
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
|
||||
'display_name' => $op . ' ' . $entity . 's',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
DB::table('permission_role')->insert([
|
||||
'role_id' => $viewerRoleId,
|
||||
'permission_id' => $permissionId
|
||||
]);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('comments');
|
||||
// Create & attach new entity permissions
|
||||
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||
$entity = 'Comment';
|
||||
foreach ($ops as $op) {
|
||||
$permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
|
||||
DB::table('role_permissions')->where('name', '=', $permName)->delete();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CommentsAddActiveCol extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('comments', function (Blueprint $table) {
|
||||
// add column active
|
||||
$table->boolean('active')->default(true);
|
||||
$table->dropIndex('comments_page_id_parent_id_index');
|
||||
$table->index(['page_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('comments', function (Blueprint $table) {
|
||||
// reversing the schema
|
||||
$table->dropIndex('comments_page_id_index');
|
||||
$table->dropColumn('active');
|
||||
$table->index(['page_id', 'parent_id']);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,7 +20,10 @@ class DummyContentSeeder extends Seeder
|
|||
->each(function($book) use ($user) {
|
||||
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
->each(function($chapter) use ($user, $book){
|
||||
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
|
||||
$pages = factory(\BookStack\Page::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id])->each(function($page) use ($user) {
|
||||
$comments = factory(\BookStack\Comment::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'page_id' => $page->id]);
|
||||
$page->comments()->saveMany($comments);
|
||||
});
|
||||
$chapter->pages()->saveMany($pages);
|
||||
});
|
||||
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const argv = require('yargs').argv;
|
||||
const gulp = require('gulp'),
|
||||
plumber = require('gulp-plumber');
|
||||
|
|
|
@ -675,4 +675,225 @@ module.exports = function (ngApp, events) {
|
|||
|
||||
}]);
|
||||
|
||||
// Controller used to reply to and add new comments
|
||||
ngApp.controller('CommentReplyController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
|
||||
const MarkdownIt = require("markdown-it");
|
||||
const md = new MarkdownIt({html: true});
|
||||
let vm = this;
|
||||
|
||||
vm.saveComment = function () {
|
||||
let pageId = $scope.comment.pageId || $scope.pageId;
|
||||
let comment = $scope.comment.text;
|
||||
if (!comment) {
|
||||
return events.emit('warning', trans('errors.empty_comment'));
|
||||
}
|
||||
let commentHTML = md.render($scope.comment.text);
|
||||
let serviceUrl = `/ajax/page/${pageId}/comment/`;
|
||||
let httpMethod = 'post';
|
||||
let reqObj = {
|
||||
text: comment,
|
||||
html: commentHTML
|
||||
};
|
||||
|
||||
if ($scope.isEdit === true) {
|
||||
// this will be set when editing the comment.
|
||||
serviceUrl = `/ajax/page/${pageId}/comment/${$scope.comment.id}`;
|
||||
httpMethod = 'put';
|
||||
} else if ($scope.isReply === true) {
|
||||
// if its reply, get the parent comment id
|
||||
reqObj.parent_id = $scope.parentId;
|
||||
}
|
||||
$http[httpMethod](window.baseUrl(serviceUrl), reqObj).then(resp => {
|
||||
if (!isCommentOpSuccess(resp)) {
|
||||
return;
|
||||
}
|
||||
// hide the comments first, and then retrigger the refresh
|
||||
if ($scope.isEdit) {
|
||||
updateComment($scope.comment, resp.data);
|
||||
$scope.$emit('evt.comment-success', $scope.comment.id);
|
||||
} else {
|
||||
$scope.comment.text = '';
|
||||
if ($scope.isReply === true && $scope.parent.sub_comments) {
|
||||
$scope.parent.sub_comments.push(resp.data.comment);
|
||||
} else {
|
||||
$scope.$emit('evt.new-comment', resp.data.comment);
|
||||
}
|
||||
$scope.$emit('evt.comment-success', null, true);
|
||||
}
|
||||
$scope.comment.is_hidden = true;
|
||||
$timeout(function() {
|
||||
$scope.comment.is_hidden = false;
|
||||
});
|
||||
|
||||
events.emit('success', trans(resp.data.message));
|
||||
|
||||
}, checkError);
|
||||
|
||||
};
|
||||
|
||||
function checkError(response) {
|
||||
let msg = null;
|
||||
if (isCommentOpSuccess(response)) {
|
||||
// all good
|
||||
return;
|
||||
} else if (response.data) {
|
||||
msg = response.data.message;
|
||||
} else {
|
||||
msg = trans('errors.comment_add');
|
||||
}
|
||||
if (msg) {
|
||||
events.emit('success', msg);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
// Controller used to delete comments
|
||||
ngApp.controller('CommentDeleteController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
|
||||
let vm = this;
|
||||
|
||||
vm.delete = function(comment) {
|
||||
$http.delete(window.baseUrl(`/ajax/comment/${comment.id}`)).then(resp => {
|
||||
if (!isCommentOpSuccess(resp)) {
|
||||
return;
|
||||
}
|
||||
updateComment(comment, resp.data, $timeout, true);
|
||||
}, function (resp) {
|
||||
if (isCommentOpSuccess(resp)) {
|
||||
events.emit('success', trans('entities.comment_deleted'));
|
||||
} else {
|
||||
events.emit('error', trans('error.comment_delete'));
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
// Controller used to fetch all comments for a page
|
||||
ngApp.controller('CommentListController', ['$scope', '$http', '$timeout', '$location', function ($scope, $http, $timeout, $location) {
|
||||
let vm = this;
|
||||
$scope.errors = {};
|
||||
// keep track of comment levels
|
||||
$scope.level = 1;
|
||||
vm.totalCommentsStr = trans('entities.comments_loading');
|
||||
vm.permissions = {};
|
||||
vm.trans = window.trans;
|
||||
|
||||
$scope.$on('evt.new-comment', function (event, comment) {
|
||||
// add the comment to the comment list.
|
||||
vm.comments.push(comment);
|
||||
++vm.totalComments;
|
||||
setTotalCommentMsg();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
vm.canEditDelete = function (comment, prop) {
|
||||
if (!comment.active) {
|
||||
return false;
|
||||
}
|
||||
let propAll = prop + '_all';
|
||||
let propOwn = prop + '_own';
|
||||
|
||||
if (vm.permissions[propAll]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (vm.permissions[propOwn] && comment.created_by.id === vm.current_user_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
vm.canComment = function () {
|
||||
return vm.permissions.comment_create;
|
||||
};
|
||||
|
||||
// check if there are is any direct linking
|
||||
let linkedCommentId = $location.search().cm;
|
||||
|
||||
$timeout(function() {
|
||||
$http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/`)).then(resp => {
|
||||
if (!isCommentOpSuccess(resp)) {
|
||||
// just show that no comments are available.
|
||||
vm.totalComments = 0;
|
||||
setTotalCommentMsg();
|
||||
return;
|
||||
}
|
||||
vm.comments = resp.data.comments;
|
||||
vm.totalComments = +resp.data.total;
|
||||
vm.permissions = resp.data.permissions;
|
||||
vm.current_user_id = resp.data.user_id;
|
||||
setTotalCommentMsg();
|
||||
if (!linkedCommentId) {
|
||||
return;
|
||||
}
|
||||
$timeout(function() {
|
||||
// wait for the UI to render.
|
||||
focusLinkedComment(linkedCommentId);
|
||||
});
|
||||
}, checkError);
|
||||
});
|
||||
|
||||
function setTotalCommentMsg () {
|
||||
if (vm.totalComments === 0) {
|
||||
vm.totalCommentsStr = trans('entities.no_comments');
|
||||
} else if (vm.totalComments === 1) {
|
||||
vm.totalCommentsStr = trans('entities.one_comment');
|
||||
} else {
|
||||
vm.totalCommentsStr = trans('entities.x_comments', {
|
||||
numComments: vm.totalComments
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function focusLinkedComment(linkedCommentId) {
|
||||
let comment = angular.element('#' + linkedCommentId);
|
||||
if (comment.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.setupPageShow.goToText(linkedCommentId);
|
||||
}
|
||||
|
||||
function checkError(response) {
|
||||
let msg = null;
|
||||
if (isCommentOpSuccess(response)) {
|
||||
// all good
|
||||
return;
|
||||
} else if (response.data) {
|
||||
msg = response.data.message;
|
||||
} else {
|
||||
msg = trans('errors.comment_list');
|
||||
}
|
||||
if (msg) {
|
||||
events.emit('success', msg);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
function updateComment(comment, resp, $timeout, isDelete) {
|
||||
comment.text = resp.comment.text;
|
||||
comment.updated = resp.comment.updated;
|
||||
comment.updated_by = resp.comment.updated_by;
|
||||
comment.active = resp.comment.active;
|
||||
if (isDelete && !resp.comment.active) {
|
||||
comment.html = trans('entities.comment_deleted');
|
||||
} else {
|
||||
comment.html = resp.comment.html;
|
||||
}
|
||||
if (!$timeout) {
|
||||
return;
|
||||
}
|
||||
comment.is_hidden = true;
|
||||
$timeout(function() {
|
||||
comment.is_hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
function isCommentOpSuccess(resp) {
|
||||
if (resp && resp.data && resp.data.status === 'success') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -863,4 +863,128 @@ module.exports = function (ngApp, events) {
|
|||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ngApp.directive('commentReply', [function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'comment-reply.html',
|
||||
scope: {
|
||||
pageId: '=',
|
||||
parentId: '=',
|
||||
parent: '='
|
||||
},
|
||||
link: function (scope, element) {
|
||||
scope.isReply = true;
|
||||
element.find('textarea').focus();
|
||||
scope.$on('evt.comment-success', function (event) {
|
||||
// no need for the event to do anything more.
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
scope.closeBox();
|
||||
});
|
||||
|
||||
scope.closeBox = function () {
|
||||
element.remove();
|
||||
scope.$destroy();
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ngApp.directive('commentEdit', [function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'comment-reply.html',
|
||||
scope: {
|
||||
comment: '='
|
||||
},
|
||||
link: function (scope, element) {
|
||||
scope.isEdit = true;
|
||||
element.find('textarea').focus();
|
||||
scope.$on('evt.comment-success', function (event, commentId) {
|
||||
// no need for the event to do anything more.
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (commentId === scope.comment.id && !scope.isNew) {
|
||||
scope.closeBox();
|
||||
}
|
||||
});
|
||||
|
||||
scope.closeBox = function () {
|
||||
element.remove();
|
||||
scope.$destroy();
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
|
||||
ngApp.directive('commentReplyLink', ['$document', '$compile', function ($document, $compile) {
|
||||
return {
|
||||
scope: {
|
||||
comment: '='
|
||||
},
|
||||
link: function (scope, element, attr) {
|
||||
element.on('$destroy', function () {
|
||||
element.off('click');
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
element.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
var $container = element.parents('.comment-actions').first();
|
||||
if (!$container.length) {
|
||||
console.error('commentReplyLink directive should be placed inside a container with class comment-box!');
|
||||
return;
|
||||
}
|
||||
if (attr.noCommentReplyDupe) {
|
||||
removeDupe();
|
||||
}
|
||||
|
||||
compileHtml($container, scope, attr.isReply === 'true');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function compileHtml($container, scope, isReply) {
|
||||
let lnkFunc = null;
|
||||
if (isReply) {
|
||||
lnkFunc = $compile('<comment-reply page-id="comment.pageId" parent-id="comment.id" parent="comment"></comment-reply>');
|
||||
} else {
|
||||
lnkFunc = $compile('<comment-edit comment="comment"></comment-add>');
|
||||
}
|
||||
var compiledHTML = lnkFunc(scope);
|
||||
$container.append(compiledHTML);
|
||||
}
|
||||
|
||||
function removeDupe() {
|
||||
let $existingElement = $document.find('.comments-list comment-reply, .comments-list comment-edit');
|
||||
if (!$existingElement.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingElement.remove();
|
||||
}
|
||||
}]);
|
||||
|
||||
ngApp.directive('commentDeleteLink', ['$window', function ($window) {
|
||||
return {
|
||||
controller: 'CommentDeleteController',
|
||||
scope: {
|
||||
comment: '='
|
||||
},
|
||||
link: function (scope, element, attr, ctrl) {
|
||||
|
||||
element.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var resp = $window.confirm(trans('entities.comment_delete_confirm'));
|
||||
if (!resp) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctrl.delete(scope.comment);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
};
|
||||
|
|
|
@ -161,6 +161,8 @@ let setupPageShow = window.setupPageShow = function (pageId) {
|
|||
}
|
||||
});
|
||||
|
||||
// in order to call from other places.
|
||||
window.setupPageShow.goToText = goToText;
|
||||
};
|
||||
|
||||
module.exports = setupPageShow;
|
82
resources/assets/sass/_comments.scss
Normal file
82
resources/assets/sass/_comments.scss
Normal file
|
@ -0,0 +1,82 @@
|
|||
.comments-list {
|
||||
.comment-box {
|
||||
border-bottom: 1px solid $comment-border;
|
||||
}
|
||||
|
||||
.comment-box:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
}
|
||||
.page-comment {
|
||||
.comment-container {
|
||||
margin-left: 42px;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
font-size: 0.8em;
|
||||
padding-bottom: 2px;
|
||||
|
||||
ul {
|
||||
padding-left: 0px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
li {
|
||||
float: left;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li:after {
|
||||
content: '•';
|
||||
color: #707070;
|
||||
padding: 0 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
li:last-child:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.comment-actions:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
font-size: 1.25em;
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
.comment-body p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.comment-inactive {
|
||||
font-style: italic;
|
||||
font-size: 0.85em;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.user-image {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
width: 32px;
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-editor {
|
||||
margin-top: 2em;
|
||||
|
||||
textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
}
|
|
@ -310,4 +310,8 @@
|
|||
background-color: #EEE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
|
||||
min-height: 175px;
|
||||
}
|
|
@ -56,3 +56,6 @@ $text-light: #EEE;
|
|||
$bs-light: 0 0 4px 1px #CCC;
|
||||
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
|
||||
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
|
||||
|
||||
// comments
|
||||
$comment-border: #DDD;
|
|
@ -10,6 +10,7 @@
|
|||
@import "header";
|
||||
@import "lists";
|
||||
@import "pages";
|
||||
@import "comments";
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
@import "header";
|
||||
@import "lists";
|
||||
@import "pages";
|
||||
@import "comments";
|
||||
|
||||
[v-cloak], [v-show] {
|
||||
display: none; opacity: 0;
|
||||
|
|
|
@ -213,4 +213,27 @@ return [
|
|||
'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.',
|
||||
'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.',
|
||||
'profile_not_created_books' => ':userName hat bisher keine Bücher angelegt.',
|
||||
|
||||
/**
|
||||
* Comnents
|
||||
*/
|
||||
'comment' => 'Kommentar',
|
||||
'comments' => 'Kommentare',
|
||||
'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein, Markdown unterstützt ...',
|
||||
'no_comments' => 'Keine Kommentare',
|
||||
'x_comments' => ':numComments Kommentare',
|
||||
'one_comment' => '1 Kommentar',
|
||||
'comments_loading' => 'Laden ...',
|
||||
'comment_save' => 'Kommentar speichern',
|
||||
'comment_reply' => 'Antworten',
|
||||
'comment_edit' => 'Bearbeiten',
|
||||
'comment_delete' => 'Löschen',
|
||||
'comment_cancel' => 'Abbrechen',
|
||||
'comment_created' => 'Kommentar hinzugefügt',
|
||||
'comment_updated' => 'Kommentar aktualisiert',
|
||||
'comment_deleted' => 'Kommentar gelöscht',
|
||||
'comment_updated_text' => 'Aktualisiert vor :updateDiff von',
|
||||
'comment_delete_confirm' => 'Damit wird der Inhalt des Kommentars entfernt. Bist du sicher, dass du diesen Kommentar löschen möchtest?',
|
||||
'comment_create' => 'Erstellt'
|
||||
|
||||
];
|
|
@ -67,4 +67,11 @@ return [
|
|||
'error_occurred' => 'Es ist ein Fehler aufgetreten',
|
||||
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',
|
||||
'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.',
|
||||
|
||||
// Comments
|
||||
'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.',
|
||||
'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.',
|
||||
'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.',
|
||||
'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.',
|
||||
'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen',
|
||||
];
|
||||
|
|
|
@ -234,4 +234,27 @@ return [
|
|||
'profile_not_created_pages' => ':userName has not created any pages',
|
||||
'profile_not_created_chapters' => ':userName has not created any chapters',
|
||||
'profile_not_created_books' => ':userName has not created any books',
|
||||
|
||||
/**
|
||||
* Comments
|
||||
*/
|
||||
'comment' => 'Comment',
|
||||
'comments' => 'Comments',
|
||||
'comment_placeholder' => 'Enter your comments here, markdown supported...',
|
||||
'no_comments' => 'No Comments',
|
||||
'x_comments' => ':numComments Comments',
|
||||
'one_comment' => '1 Comment',
|
||||
'comments_loading' => 'Loading...',
|
||||
'comment_save' => 'Save Comment',
|
||||
'comment_reply' => 'Reply',
|
||||
'comment_edit' => 'Edit',
|
||||
'comment_delete' => 'Delete',
|
||||
'comment_cancel' => 'Cancel',
|
||||
'comment_created' => 'Comment added',
|
||||
'comment_updated' => 'Comment updated',
|
||||
'comment_deleted' => 'Comment deleted',
|
||||
'comment_updated_text' => 'Updated :updateDiff by',
|
||||
'comment_delete_confirm' => 'This will remove the contents of the comment. Are you sure you want to delete this comment?',
|
||||
'comment_create' => 'Created'
|
||||
|
||||
];
|
|
@ -60,6 +60,13 @@ return [
|
|||
'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
|
||||
'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
|
||||
|
||||
// Comments
|
||||
'comment_list' => 'An error occurred while fetching the comments.',
|
||||
'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',
|
||||
'comment_add' => 'An error occurred while adding the comment.',
|
||||
'comment_delete' => 'An error occurred while deleting the comment.',
|
||||
'empty_comment' => 'Cannot add an empty comment.',
|
||||
|
||||
// Error pages
|
||||
'404_page_not_found' => 'Page Not Found',
|
||||
'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',
|
||||
|
|
|
@ -214,4 +214,26 @@ return [
|
|||
'profile_not_created_pages' => ':userName no ha creado ninguna página',
|
||||
'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
|
||||
'profile_not_created_books' => ':userName no ha creado ningún libro',
|
||||
|
||||
/**
|
||||
* Comments
|
||||
*/
|
||||
'comment' => 'Comentario',
|
||||
'comments' => 'Comentarios',
|
||||
'comment_placeholder' => 'Introduzca sus comentarios aquí, markdown supported ...',
|
||||
'no_comments' => 'No hay comentarios',
|
||||
'x_comments' => ':numComments Comentarios',
|
||||
'one_comment' => '1 Comentario',
|
||||
'comments_loading' => 'Cargando ...',
|
||||
'comment_save' => 'Guardar comentario',
|
||||
'comment_reply' => 'Responder',
|
||||
'comment_edit' => 'Editar',
|
||||
'comment_delete' => 'Eliminar',
|
||||
'comment_cancel' => 'Cancelar',
|
||||
'comment_created' => 'Comentario añadido',
|
||||
'comment_updated' => 'Comentario actualizado',
|
||||
'comment_deleted' => 'Comentario eliminado',
|
||||
'comment_updated_text' => 'Actualizado hace :updateDiff por',
|
||||
'comment_delete_confirm' => 'Esto eliminará el contenido del comentario. ¿Estás seguro de que quieres eliminar este comentario?',
|
||||
'comment_create' => 'Creado'
|
||||
];
|
||||
|
|
|
@ -67,4 +67,11 @@ return [
|
|||
'error_occurred' => 'Ha ocurrido un error',
|
||||
'app_down' => 'La aplicación :appName se encuentra caída en este momento',
|
||||
'back_soon' => 'Volverá a estar operativa en corto tiempo.',
|
||||
|
||||
// Comments
|
||||
'comment_list' => 'Se ha producido un error al buscar los comentarios.',
|
||||
'cannot_add_comment_to_draft' => 'No puedes añadir comentarios a un borrador.',
|
||||
'comment_add' => 'Se ha producido un error al añadir el comentario.',
|
||||
'comment_delete' => 'Se ha producido un error al eliminar el comentario.',
|
||||
'empty_comment' => 'No se puede agregar un comentario vacío.',
|
||||
];
|
||||
|
|
|
@ -213,4 +213,26 @@ return [
|
|||
'profile_not_created_pages' => ':userName n\'a pas créé de page',
|
||||
'profile_not_created_chapters' => ':userName n\'a pas créé de chapitre',
|
||||
'profile_not_created_books' => ':userName n\'a pas créé de livre',
|
||||
|
||||
/**
|
||||
* Comments
|
||||
*/
|
||||
'comment' => 'Commentaire',
|
||||
'comments' => 'Commentaires',
|
||||
'comment_placeholder' => 'Entrez vos commentaires ici, merci supporté ...',
|
||||
'no_comments' => 'No Comments',
|
||||
'x_comments' => ':numComments Commentaires',
|
||||
'one_comment' => '1 Commentaire',
|
||||
'comments_loading' => 'Loading ...',
|
||||
'comment_save' => 'Enregistrer le commentaire',
|
||||
'comment_reply' => 'Répondre',
|
||||
'comment_edit' => 'Modifier',
|
||||
'comment_delete' => 'Supprimer',
|
||||
'comment_cancel' => 'Annuler',
|
||||
'comment_created' => 'Commentaire ajouté',
|
||||
'comment_updated' => 'Commentaire mis à jour',
|
||||
'comment_deleted' => 'Commentaire supprimé',
|
||||
'comment_updated_text' => 'Mis à jour il y a :updateDiff par',
|
||||
'comment_delete_confirm' => 'Cela supprime le contenu du commentaire. Êtes-vous sûr de vouloir supprimer ce commentaire?',
|
||||
'comment_create' => 'Créé'
|
||||
];
|
||||
|
|
|
@ -67,4 +67,11 @@ return [
|
|||
'error_occurred' => 'Une erreur est survenue',
|
||||
'app_down' => ':appName n\'est pas en service pour le moment',
|
||||
'back_soon' => 'Nous serons bientôt de retour.',
|
||||
|
||||
// comments
|
||||
'comment_list' => 'Une erreur s\'est produite lors de la récupération des commentaires.',
|
||||
'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un projet.',
|
||||
'comment_add' => 'Une erreur s\'est produite lors de l\'ajout du commentaire.',
|
||||
'comment_delete' => 'Une erreur s\'est produite lors de la suppression du commentaire.',
|
||||
'empty_comment' => 'Impossible d\'ajouter un commentaire vide.',
|
||||
];
|
||||
|
|
|
@ -214,4 +214,26 @@ return [
|
|||
'profile_not_created_pages' => ':userName heeft geen pagina\'s gemaakt',
|
||||
'profile_not_created_chapters' => ':userName heeft geen hoofdstukken gemaakt',
|
||||
'profile_not_created_books' => ':userName heeft geen boeken gemaakt',
|
||||
|
||||
/**
|
||||
* Comments
|
||||
*/
|
||||
'comment' => 'Commentaar',
|
||||
'comments' => 'Commentaren',
|
||||
'comment_placeholder' => 'Vul hier uw reacties in, markdown ondersteund ...',
|
||||
'no_comments' => 'No Comments',
|
||||
'x_comments' => ':numComments Opmerkingen',
|
||||
'one_comment' => '1 commentaar',
|
||||
'comments_loading' => 'Loading ...',
|
||||
'comment_save' => 'Opslaan opslaan',
|
||||
'comment_reply' => 'Antwoord',
|
||||
'comment_edit' => 'Bewerken',
|
||||
'comment_delete' => 'Verwijderen',
|
||||
'comment_cancel' => 'Annuleren',
|
||||
'comment_created' => 'Opmerking toegevoegd',
|
||||
'comment_updated' => 'Opmerking bijgewerkt',
|
||||
'comment_deleted' => 'Opmerking verwijderd',
|
||||
'comment_updated_text' => 'Bijgewerkt :updateDiff geleden door',
|
||||
'comment_delete_confirm' => 'Hiermee verwijdert u de inhoud van de reactie. Weet u zeker dat u deze reactie wilt verwijderen?',
|
||||
'comment_create' => 'Gemaakt'
|
||||
];
|
|
@ -67,4 +67,11 @@ return [
|
|||
'error_occurred' => 'Er Ging Iets Fout',
|
||||
'app_down' => ':appName is nu niet beschikbaar',
|
||||
'back_soon' => 'Komt snel weer online.',
|
||||
|
||||
// Comments
|
||||
'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de reacties.',
|
||||
'cannot_add_comment_to_draft' => 'U kunt geen reacties toevoegen aan een ontwerp.',
|
||||
'comment_add' => 'Er is een fout opgetreden tijdens het toevoegen van de reactie.',
|
||||
'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de reactie.',
|
||||
'empty_comment' => 'Kan geen lege reactie toevoegen.',
|
||||
];
|
|
@ -214,4 +214,26 @@ return [
|
|||
'profile_not_created_pages' => ':userName não criou páginas',
|
||||
'profile_not_created_chapters' => ':userName não criou capítulos',
|
||||
'profile_not_created_books' => ':userName não criou livros',
|
||||
|
||||
/**
|
||||
* Comments
|
||||
*/
|
||||
'comentário' => 'Comentário',
|
||||
'comentários' => 'Comentários',
|
||||
'comment_placeholder' => 'Digite seus comentários aqui, markdown suportado ...',
|
||||
'no_comments' => 'No Comments',
|
||||
'x_comments' => ':numComments Comentários',
|
||||
'one_comment' => '1 comentário',
|
||||
'comments_loading' => 'Carregando ....',
|
||||
'comment_save' => 'Salvar comentário',
|
||||
'comment_reply' => 'Responder',
|
||||
'comment_edit' => 'Editar',
|
||||
'comment_delete' => 'Excluir',
|
||||
'comment_cancel' => 'Cancelar',
|
||||
'comment_created' => 'Comentário adicionado',
|
||||
'comment_updated' => 'Comentário atualizado',
|
||||
'comment_deleted' => 'Comentário eliminado',
|
||||
'comment_updated_text' => 'Atualizado :updatedDiff atrás por',
|
||||
'comment_delete_confirm' => 'Isso removerá o conteúdo do comentário. Tem certeza de que deseja excluir esse comentário?',
|
||||
'comment_create' => 'Criada'
|
||||
];
|
|
@ -67,4 +67,11 @@ return [
|
|||
'error_occurred' => 'Um erro ocorreu',
|
||||
'app_down' => ':appName está fora do ar no momento',
|
||||
'back_soon' => 'Voltaremos em seguida.',
|
||||
|
||||
// comments
|
||||
'comment_list' => 'Ocorreu um erro ao buscar os comentários.',
|
||||
'cannot_add_comment_to_draft' => 'Você não pode adicionar comentários a um rascunho.',
|
||||
'comment_add' => 'Ocorreu um erro ao adicionar o comentário.',
|
||||
'comment_delete' => 'Ocorreu um erro ao excluir o comentário.',
|
||||
'empty_comment' => 'Não é possível adicionar um comentário vazio.',
|
||||
];
|
|
@ -223,4 +223,26 @@ return [
|
|||
'profile_not_created_pages' => ':userName nevytvoril žiadne stránky',
|
||||
'profile_not_created_chapters' => ':userName nevytvoril žiadne kapitoly',
|
||||
'profile_not_created_books' => ':userName nevytvoril žiadne knihy',
|
||||
|
||||
/**
|
||||
* Comments
|
||||
*/
|
||||
'comment' => 'Komentár',
|
||||
'comments' => 'Komentáre',
|
||||
'comment_placeholder' => 'Tu zadajte svoje pripomienky, podporované označenie ...',
|
||||
'no_comments' => 'No Comments',
|
||||
'x_comments' => ':numComments komentárov',
|
||||
'one_comment' => '1 komentár',
|
||||
'comments_loading' => 'Loading ..',
|
||||
'comment_save' => 'Uložiť komentár',
|
||||
'comment_reply' => 'Odpovedať',
|
||||
'comment_edit' => 'Upraviť',
|
||||
'comment_delete' => 'Odstrániť',
|
||||
'comment_cancel' => 'Zrušiť',
|
||||
'comment_created' => 'Pridaný komentár',
|
||||
'comment_updated' => 'Komentár aktualizovaný',
|
||||
'comment_deleted' => 'Komentár bol odstránený',
|
||||
'comment_updated_text' => 'Aktualizované pred :updateDiff',
|
||||
'comment_delete_confirm' => 'Tým sa odstráni obsah komentára. Naozaj chcete odstrániť tento komentár?',
|
||||
'comment_create' => 'Vytvorené'
|
||||
];
|
||||
|
|
|
@ -67,4 +67,11 @@ return [
|
|||
'error_occurred' => 'Nastala chyba',
|
||||
'app_down' => ':appName je momentálne nedostupná',
|
||||
'back_soon' => 'Čoskoro bude opäť dostupná.',
|
||||
|
||||
// comments
|
||||
'comment_list' => 'Pri načítaní komentárov sa vyskytla chyba',
|
||||
'cannot_add_comment_to_draft' => 'Do konceptu nemôžete pridávať komentáre.',
|
||||
'comment_add' => 'Počas pridávania komentára sa vyskytla chyba',
|
||||
'comment_delete' => 'Pri odstraňovaní komentára došlo k chybe',
|
||||
'empty_comment' => 'Nelze pridať prázdny komentár.',
|
||||
];
|
||||
|
|
12
resources/views/comments/comment-reply.blade.php
Normal file
12
resources/views/comments/comment-reply.blade.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<div class="comment-editor" ng-controller="CommentReplyController as vm" ng-cloak>
|
||||
<form novalidate>
|
||||
<textarea name="markdown" rows="3" ng-model="comment.text" placeholder="{{ trans('entities.comment_placeholder') }}"></textarea>
|
||||
<input type="hidden" ng-model="comment.pageId" name="comment.pageId" value="{{$pageId}}" ng-init="comment.pageId = {{$pageId }}">
|
||||
<button type="button" ng-if="::(isReply || isEdit)" class="button muted" ng-click="closeBox()">{{ trans('entities.comment_cancel') }}</button>
|
||||
<button type="submit" class="button pos" ng-click="vm.saveComment()">{{ trans('entities.comment_save') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if($errors->has('markdown'))
|
||||
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
|
||||
@endif
|
18
resources/views/comments/comments.blade.php
Normal file
18
resources/views/comments/comments.blade.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script type="text/ng-template" id="comment-list-item.html">
|
||||
@include('comments/list-item')
|
||||
</script>
|
||||
<script type="text/ng-template" id="comment-reply.html">
|
||||
@include('comments/comment-reply', ['pageId' => $pageId])
|
||||
</script>
|
||||
<div ng-controller="CommentListController as vm" ng-init="pageId = <?= $page->id ?>" class="comments-list" ng-cloak>
|
||||
<h3>@{{vm.totalCommentsStr}}</h3>
|
||||
<hr>
|
||||
<div class="comment-box" ng-repeat="comment in vm.comments track by comment.id">
|
||||
<div ng-include src="'comment-list-item.html'">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="::vm.canComment()">
|
||||
@include('comments/comment-reply', ['pageId' => $pageId])
|
||||
</div>
|
||||
</div>
|
30
resources/views/comments/list-item.blade.php
Normal file
30
resources/views/comments/list-item.blade.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<div class='page-comment' id="comment-@{{::pageId}}-@{{::comment.id}}">
|
||||
<div class="user-image">
|
||||
<img ng-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 ng-bind-html="comment.html" ng-if="::comment.active" class="comment-body" ng-class="!comment.active ? 'comment-inactive' : ''">
|
||||
|
||||
</div>
|
||||
<div ng-if="::!comment.active" class="comment-body comment-inactive">
|
||||
{{ trans('entities.comment_deleted') }}
|
||||
</div>
|
||||
<div class="comment-actions">
|
||||
<ul ng-if="!comment.is_hidden">
|
||||
<li ng-if="::(level < 3 && vm.canComment())"><a href="#" comment-reply-link no-comment-reply-dupe="true" comment="comment" is-reply="true">{{ trans('entities.comment_reply') }}</a></li>
|
||||
<li ng-if="::vm.canEditDelete(comment, 'comment_update')"><a href="#" comment-reply-link no-comment-reply-dupe="true" comment="comment" >{{ trans('entities.comment_edit') }}</a></li>
|
||||
<li ng-if="::vm.canEditDelete(comment, 'comment_delete')"><a href="#" comment-delete-link comment="comment" >{{ trans('entities.comment_delete') }}</a></li>
|
||||
<li>{{ trans('entities.comment_create') }} <a title="@{{::comment.created.day_time_str}}" href="#?cm=comment-@{{::pageId}}-@{{::comment.id}}">@{{::comment.created.diff}}</a></li>
|
||||
<li ng-if="::comment.updated"><span title="@{{::comment.updated.day_time_str}}">@{{ ::vm.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 class="comment-box" ng-repeat="comment in comments = comment.sub_comments track by comment.id" ng-init="level = level + 1">
|
||||
<div ng-include src="'comment-list-item.html'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -46,13 +46,13 @@
|
|||
</div>
|
||||
|
||||
|
||||
<div class="container" id="page-show" ng-non-bindable>
|
||||
<div class="container" id="page-show">
|
||||
<div class="row">
|
||||
<div class="col-md-9 print-full-width">
|
||||
<div class="page-content">
|
||||
<div class="page-content" ng-non-bindable>
|
||||
|
||||
<div class="pointer-container" id="pointer">
|
||||
<div class="pointer anim">
|
||||
<div class="pointer anim" >
|
||||
<span class="icon text-primary"><i class="zmdi zmdi-link"></i></span>
|
||||
<input readonly="readonly" type="text" id="pointer-url" placeholder="url">
|
||||
<button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}"><i class="zmdi zmdi-copy"></i></button>
|
||||
|
@ -66,6 +66,7 @@
|
|||
@include('partials.entity-meta', ['entity' => $page])
|
||||
|
||||
</div>
|
||||
@include('comments/comments', ['pageId' => $page->id])
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 print-hidden">
|
||||
|
@ -109,7 +110,6 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@stop
|
||||
|
||||
@section('scripts')
|
||||
|
|
|
@ -117,6 +117,19 @@
|
|||
<label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-all']) {{ trans('settings.role_all') }}</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ trans('entities.comments') }}</td>
|
||||
<td>@include('settings/roles/checkbox', ['permission' => 'comment-create-all'])</td>
|
||||
<td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
|
||||
<td>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'comment-update-own']) {{ trans('settings.role_own') }}</label>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'comment-update-all']) {{ trans('settings.role_all') }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'comment-delete-own']) {{ trans('settings.role_own') }}</label>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'comment-delete-all']) {{ trans('settings.role_all') }}</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -119,6 +119,12 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
|
||||
Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax');
|
||||
|
||||
// Comments
|
||||
Route::post('/ajax/page/{pageId}/comment/', 'CommentController@save');
|
||||
Route::put('/ajax/page/{pageId}/comment/{commentId}', 'CommentController@save');
|
||||
Route::delete('/ajax/comment/{id}', 'CommentController@destroy');
|
||||
Route::get('/ajax/page/{pageId}/comments/', 'CommentController@getPageComments');
|
||||
|
||||
// Links
|
||||
Route::get('/link/{id}', 'PageController@redirectFromLink');
|
||||
|
||||
|
|
111
tests/Entity/CommentTest.php
Normal file
111
tests/Entity/CommentTest.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php namespace Tests;
|
||||
|
||||
use BookStack\Page;
|
||||
use BookStack\Comment;
|
||||
|
||||
class CommentTest extends BrowserKitTest
|
||||
{
|
||||
|
||||
public function test_add_comment()
|
||||
{
|
||||
$this->asAdmin();
|
||||
$page = $this->getPage();
|
||||
|
||||
$this->addComment($page);
|
||||
}
|
||||
|
||||
public function test_comment_reply()
|
||||
{
|
||||
$this->asAdmin();
|
||||
$page = $this->getPage();
|
||||
|
||||
// add a normal comment
|
||||
$createdComment = $this->addComment($page);
|
||||
|
||||
// reply to the added comment
|
||||
$this->addComment($page, $createdComment['id']);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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->assertResponseOk();
|
||||
$this->seeJsonContains($expectedResp);
|
||||
|
||||
$resp = $this->decodeResponseJson();
|
||||
$createdComment = $resp['comment'];
|
||||
$this->assertArrayHasKey('id', $createdComment);
|
||||
|
||||
return $createdComment;
|
||||
}
|
||||
}
|
|
@ -657,4 +657,112 @@ class RolesTest extends BrowserKitTest
|
|||
->dontSee('Sort the current book');
|
||||
}
|
||||
|
||||
public function test_comment_create_permission () {
|
||||
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
|
||||
|
||||
$this->actingAs($this->user)->addComment($ownPage);
|
||||
|
||||
$this->assertResponseStatus(403);
|
||||
|
||||
$this->giveUserPermissions($this->user, ['comment-create-all']);
|
||||
|
||||
$this->actingAs($this->user)->addComment($ownPage);
|
||||
$this->assertResponseOk(200)->seeJsonContains(['status' => 'success']);
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
// no comment-update-own
|
||||
$this->actingAs($this->user)->updateComment($ownPage, $comment['id']);
|
||||
$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']);
|
||||
}
|
||||
|
||||
public function test_comment_update_all_permission () {
|
||||
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
|
||||
$comment = $this->asAdmin()->addComment($ownPage);
|
||||
|
||||
// no comment-update-all
|
||||
$this->actingAs($this->user)->updateComment($ownPage, $comment['id']);
|
||||
$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']);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// no comment-delete-own
|
||||
$this->actingAs($this->user)->deleteComment($comment['id']);
|
||||
$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']);
|
||||
}
|
||||
|
||||
public function test_comment_delete_all_permission () {
|
||||
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
|
||||
$comment = $this->asAdmin()->addComment($ownPage);
|
||||
|
||||
// no comment-delete-all
|
||||
$this->actingAs($this->user)->deleteComment($comment['id']);
|
||||
$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']);
|
||||
}
|
||||
|
||||
private function addComment($page) {
|
||||
$comment = factory(\BookStack\Comment::class)->make();
|
||||
$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;
|
||||
}
|
||||
|
||||
private function updateComment($page, $commentId) {
|
||||
$comment = factory(\BookStack\Comment::class)->make();
|
||||
$url = "/ajax/page/$page->id/comment/$commentId";
|
||||
$request = [
|
||||
'text' => $comment->text,
|
||||
'html' => $comment->html
|
||||
];
|
||||
|
||||
return $this->json('PUT', $url, $request);
|
||||
}
|
||||
|
||||
private function deleteComment($commentId) {
|
||||
$url = '/ajax/comment/' . $commentId;
|
||||
return $this->json('DELETE', $url);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user