/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
--- /dev/null
+<?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);
+ }
+
+ 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;
+ }
+}
--- /dev/null
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Repos\CommentRepo;
+use BookStack\Repos\EntityRepo;
+use BookStack\Comment;
+use Illuminate\Http\Request;
+
+// delete -checkOwnablePermission \
+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);
+ }
+
+
+ public function getPageComments($pageId) {
+ 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.no_comments_for_draft'),
+ ], 400);
+ }
+
+ $this->checkOwnablePermission('page-view', $page);
+
+ $comments = $this->commentRepo->getPageComments($pageId);
+ return response()->json(['success' => true, 'comments'=> $comments['comments'], 'total' => $comments['total']]);
+ }
+}
return redirect($page->getUrl());
}
+ public function getLastXComments($pageId)
+ {
+ // $this->checkOwnablePermission('page-view', $page);
+ }
+
}
{
return $this->belongsTo(Chapter::class);
}
+
+ /**
+ * Get the comments in the page.
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function comment()
+ {
+ return $this->hasMany(Comment::class);
+ }
/**
* Check if this page has a chapter.
--- /dev/null
+<?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) {
+ $userId = user()->id;
+ $comment->updated_by = $userId;
+ $comment->fill($input);
+ $comment->save();
+ return $comment;
+ }
+
+ public function getPageComments($pageId) {
+ $comments = $this->comment->getAllPageComments($pageId);
+ $index = [];
+ $totalComments = count($comments);
+ // normalizing the response.
+ foreach($comments as &$comment) {
+ $comment = $this->normalizeComment($comment);
+ $parentId = $comment->parent_id;
+ if (empty($parentId)) {
+ $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' => $comments,
+ 'total' => $totalComments
+ ];
+ }
+
+ public function getCommentById($commentId) {
+ return $this->normalizeComment($this->comment->getCommentById($commentId));
+ }
+
+ private function normalizeComment($comment) {
+ if (empty($comment)) {
+ return;
+ }
+ $comment->createdBy->avatar_url = $comment->createdBy->getAvatar(50);
+ $comment->createdBy->profile_url = $comment->createdBy->getProfileUrl();
+ if (!empty($comment->updatedBy)) {
+ $comment->updatedBy->profile_url = $comment->updatedBy->getProfileUrl();
+ }
+ return $comment;
+ }
+}
\ No newline at end of file
$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)) {
return [
- 'env' => env('APP_ENV', 'production'),
+ 'env' => env('APP_ENV', 'development'),
'editor' => env('APP_EDITOR', 'html'),
|
*/
- 'debug' => env('APP_DEBUG', false),
+ 'debug' => env('APP_DEBUG', true),
/*
|--------------------------------------------------------------------------
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
+ 'database' => env('DB_DATABASE', 'bookstack'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', 'Change123'),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
--- /dev/null
+<?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
+ ]);
+ }
+ });
+ }
+
+ /**
+ * 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();
+ }
+ }
+}
+'use strict';
+
const argv = require('yargs').argv;
const gulp = require('gulp'),
plumber = require('gulp-plumber');
$scope.draftsEnabled = $attrs.draftsEnabled === 'true';
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
+ $scope.commentsLoaded = false;
// Set initial header draft text
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
}]);
+ // CommentCrudController
+ ngApp.controller('CommentReplyController', ['$scope', '$http', function ($scope, $http) {
+ const MarkdownIt = require("markdown-it");
+ const md = new MarkdownIt({html: true});
+ let vm = this;
+ $scope.errors = {};
+ vm.saveComment = function () {
+ let pageId = $scope.comment.pageId || $scope.pageId;
+ let comment = $scope.comment.text;
+ let commentHTML = md.render($scope.comment.text);
+ let serviceUrl = `/ajax/page/${pageId}/comment/`;
+ let httpMethod = 'post';
+ let errorOp = 'add';
+ 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';
+ errorOp = 'update';
+ } 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 (!resp.data || resp.data.status !== 'success') {
+ return events.emit('error', trans('error'));
+ }
+ if ($scope.isEdit) {
+ $scope.comment.html = resp.data.comment.html;
+ $scope.comment.text = resp.data.comment.text;
+ $scope.comment.updated = resp.data.comment.updated;
+ $scope.comment.updated_by = resp.data.comment.updated_by;
+ $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);
+ }
+ events.emit('success', trans(resp.data.message));
+
+ }, checkError(errorOp));
+
+ };
+
+ function checkError(errorGroupName) {
+ $scope.errors[errorGroupName] = {};
+ return function(response) {
+ if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
+ events.emit('error', response.data.error);
+ }
+ if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') {
+ $scope.errors[errorGroupName] = response.data.validation;
+ console.log($scope.errors[errorGroupName])
+ }
+ }
+ }
+ }]);
+
+
+ // CommentListController
+ ngApp.controller('CommentListController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
+ let vm = this;
+ $scope.errors = {};
+ // keep track of comment levels
+ $scope.level = 1;
+ vm.totalCommentsStr = 'Loading...';
+
+ $scope.$on('evt.new-comment', function (event, comment) {
+ // add the comment to the comment list.
+ vm.comments.push(comment);
+ event.stopPropagation();
+ event.preventDefault();
+ });
+
+ $timeout(function() {
+ $http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/`)).then(resp => {
+ if (!resp.data || resp.data.success !== true) {
+ // TODO : Handle error
+ return;
+ }
+ vm.comments = resp.data.comments;
+ vm.totalComments = resp.data.total;
+ // TODO : Fetch message from translate.
+ if (vm.totalComments === 0) {
+ vm.totalCommentsStr = 'No comments found.';
+ } else if (vm.totalComments === 1) {
+ vm.totalCommentsStr = '1 Comments';
+ } else {
+ vm.totalCommentsStr = vm.totalComments + ' Comments'
+ }
+ }, checkError('app'));
+ });
+
+ function checkError(errorGroupName) {
+ $scope.errors[errorGroupName] = {};
+ return function(response) {
+ console.log(response);
+ }
+ }
+ }]);
+
};
}
};
}]);
+
+ 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();
+ 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) {
+ element.remove();
+ scope.$destroy();
+ }
+ });
+ }
+ }
+ }]);
+
+
+ ngApp.directive('commentReplyLink', ['$document', '$compile', '$http', function ($document, $compile, $http) {
+ return {
+ scope: {
+ comment: '='
+ },
+ link: function (scope, element, attr) {
+ element.on('$destroy', function () {
+ element.off('click');
+ scope.$destroy();
+ });
+
+ element.on('click', function () {
+ var $container = element.parents('.comment-box').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');
+ if (!$existingElement.length) {
+ return;
+ }
+
+ $existingElement.remove();
+ }
+ }]);
};
--- /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;
+ }
+
+ .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;
+ }
+}
background-color: #EEE;
}
}
+}
+
+.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
+ min-height: 175px;
}
\ No newline at end of file
$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
@import "header";
@import "lists";
@import "pages";
+@import "comments";
table {
border-spacing: 0;
@import "header";
@import "lists";
@import "pages";
+@import "comments";
[v-cloak], [v-show] {
display: none; opacity: 0;
'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 newline at end of file
--- /dev/null
+<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') }}"
+ @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) ||
+ old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
+ <input type="hidden" ng-model="comment.pageId" name="comment.pageId" value="{{$pageId}}" ng-init="comment.pageId = {{$pageId }}">
+ <button type="submit" class="button pos" ng-click="vm.saveComment(isReply)">Save</button>
+ </form>
+</div>
+
+@if($errors->has('markdown'))
+ <div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
+@endif
\ No newline at end of file
--- /dev/null
+<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>
+ @include('comments/comment-reply', ['pageId' => $pageId])
+</div>
\ No newline at end of file
--- /dev/null
+<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" class="comment-body">
+
+ </div>
+ <div class="comment-actions">
+ <ul>
+ <li ng-if="level < 3"><a href="#" comment-reply-link no-comment-reply-dupe="true" comment="comment" is-reply="true">Reply</a></li>
+ <li><a href="#" comment-reply-link no-comment-reply-dupe="true" comment="comment">Edit</a></li>
+ <li>Created <a title="@{{::comment.created.day_time_str}}" href="#comment-@{{::comment.id}}-@{{::pageId}}">@{{::comment.created.diff}}</a></li>
+ <li ng-if="comment.updated"><span title="@{{comment.updated.day_time_str}}">Updated @{{comment.updated.diff}} by
+ <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>
\ No newline at end of file
</div>
</div>
-
+ <div class="container">
+ <div class="row">
+ <div class="col-md-9">
+ @include('comments/comments', ['pageId' => $page->id])
+ </div>
+ </div>
+ </div>
@stop
@section('scripts')
<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>
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');