]> BookStack Code Mirror - bookstack/commitdiff
Getting the latest changes
authorAbijeet <redacted>
Sun, 4 Jun 2017 04:50:01 +0000 (10:20 +0530)
committerAbijeet <redacted>
Sun, 4 Jun 2017 04:50:01 +0000 (10:20 +0530)
25 files changed:
.gitignore
app/Comment.php [new file with mode: 0644]
app/Http/Controllers/CommentController.php [new file with mode: 0644]
app/Http/Controllers/PageController.php
app/Page.php
app/Repos/CommentRepo.php [new file with mode: 0644]
app/Services/PermissionService.php
config/app.php
config/database.php
database/migrations/2017_01_01_130541_create_comments_table.php [new file with mode: 0644]
gulpfile.js
resources/assets/js/controllers.js
resources/assets/js/directives.js
resources/assets/sass/_comments.scss [new file with mode: 0644]
resources/assets/sass/_pages.scss
resources/assets/sass/_variables.scss
resources/assets/sass/export-styles.scss
resources/assets/sass/styles.scss
resources/lang/en/entities.php
resources/views/comments/comment-reply.blade.php [new file with mode: 0644]
resources/views/comments/comments.blade.php [new file with mode: 0644]
resources/views/comments/list-item.blade.php [new file with mode: 0644]
resources/views/pages/show.blade.php
resources/views/settings/roles/form.blade.php
routes/web.php

index 5f41a864e58092fd0467efe1ffbeac7c6dd8558c..e7e0535059c31e403a6062119d67c350003b0df2 100644 (file)
@@ -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
diff --git a/app/Comment.php b/app/Comment.php
new file mode 100644 (file)
index 0000000..8588982
--- /dev/null
@@ -0,0 +1,93 @@
+<?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;
+    }
+}
diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php
new file mode 100644 (file)
index 0000000..29ccdf5
--- /dev/null
@@ -0,0 +1,93 @@
+<?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']]);
+    }
+}
index c97597bc4834822b5947eec2a3f0cc8964e3510c..73619721390a7cad6956ce7d75bb599f86e7e353 100644 (file)
@@ -590,4 +590,9 @@ class PageController extends Controller
         return redirect($page->getUrl());
     }
 
+    public function getLastXComments($pageId)
+    {
+        // $this->checkOwnablePermission('page-view', $page);
+    }
+
 }
index c9823e7e4ccfcb65dce71328f28fa0f4f04d40a3..4a8d32780388565d668eca10ad249beb929d3c61 100644 (file)
@@ -38,6 +38,15 @@ class Page extends Entity
     {
         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.
diff --git a/app/Repos/CommentRepo.php b/app/Repos/CommentRepo.php
new file mode 100644 (file)
index 0000000..7d0c4eb
--- /dev/null
@@ -0,0 +1,86 @@
+<?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
index 6f9561a161ed0a8dc8ac85f00c317b1d9df23706..89f80f9360a878f6224a36853a399c691221a6a1 100644 (file)
@@ -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)) {
index 54cdca21bb25893d8c5b85aa4bc78a5eb10aa891..13bb9aa7ddb90d4b08ee8e4bf03fc6e5a7410ede 100644 (file)
@@ -3,7 +3,7 @@
 return [
 
 
-    'env' => env('APP_ENV', 'production'),
+    'env' => env('APP_ENV', 'development'),
 
     'editor' => env('APP_EDITOR', 'html'),
 
@@ -18,7 +18,7 @@ return [
     |
     */
 
-    'debug' => env('APP_DEBUG', false),
+    'debug' => env('APP_DEBUG', true),
 
     /*
     |--------------------------------------------------------------------------
index 92c7682450a68317015e6559380e34cfeaf414db..d13268cea44f8cbf7b77d211dd7dad66b7cf1d5e 100644 (file)
@@ -71,9 +71,9 @@ return [
         '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'    => '',
diff --git a/database/migrations/2017_01_01_130541_create_comments_table.php b/database/migrations/2017_01_01_130541_create_comments_table.php
new file mode 100644 (file)
index 0000000..8aa99ee
--- /dev/null
@@ -0,0 +1,67 @@
+<?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();
+        }
+    }
+}
index b72bb366d6700ef1cfc6a63f56604cefff987863..580db00cc0d6cbb5d9ac923fc280050175132520 100644 (file)
@@ -1,3 +1,5 @@
+'use strict';
+
 const argv = require('yargs').argv;
 const gulp = require('gulp'),
     plumber = require('gulp-plumber');
index 6a88aa81152a6822b81071a77c9f9aa2ccda5870..f64d7c038d4263ce469365c7858b397d58e6f55d 100644 (file)
@@ -272,6 +272,7 @@ module.exports = function (ngApp, events) {
         $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) {
@@ -681,4 +682,113 @@ module.exports = function (ngApp, events) {
 
         }]);
 
+    // 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);
+            }
+        }
+    }]);
+
 };
index 221e18b0e1559ca22a56564a9136d74d1c5ae71f..278e0f8c656c74342989213a04659c18f67161f0 100644 (file)
@@ -822,4 +822,98 @@ 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();
+                    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();
+        }
+    }]);
 };
diff --git a/resources/assets/sass/_comments.scss b/resources/assets/sass/_comments.scss
new file mode 100644 (file)
index 0000000..0328341
--- /dev/null
@@ -0,0 +1,76 @@
+.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;
+    }
+}
index e5334c69c7d15d68fb2c43d5abf165460210143d..b06892c1d9e566d201cce0bb6f89649526530ae6 100755 (executable)
       background-color: #EEE;
     }
   }
+}
+
+.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
+  min-height: 175px;
 }
\ No newline at end of file
index 23bf2b219542077c6acfd9238f9d6649a8f94f59..3e864aaa4effc572ec80f5846a65ec331444e74d 100644 (file)
@@ -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;
\ No newline at end of file
index 60450f3e2e5a1436dacc4554f41c4e363030d5d6..72b5b16b5e7cf0a22e42c45ccda93696e818f814 100644 (file)
@@ -10,6 +10,7 @@
 @import "header";
 @import "lists";
 @import "pages";
+@import "comments";
 
 table {
   border-spacing: 0;
index afb9d531bd4a82ae8b17410083f97e1f6b0ab875..3b279b8bd29c9eb523f468225ded1373d72a31b6 100644 (file)
@@ -16,6 +16,7 @@
 @import "header";
 @import "lists";
 @import "pages";
+@import "comments";
 
 [v-cloak], [v-show] {
   display: none; opacity: 0;
index 450f4ce4851bcfb49703fe8e97e039ec1cb0c0aa..610309362c2786212ca82caa3f0f154dd742cb8c 100644 (file)
@@ -234,4 +234,11 @@ 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 newline at end of file
diff --git a/resources/views/comments/comment-reply.blade.php b/resources/views/comments/comment-reply.blade.php
new file mode 100644 (file)
index 0000000..74a13ed
--- /dev/null
@@ -0,0 +1,13 @@
+<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
diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php
new file mode 100644 (file)
index 0000000..93e7ebc
--- /dev/null
@@ -0,0 +1,16 @@
+<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
diff --git a/resources/views/comments/list-item.blade.php b/resources/views/comments/list-item.blade.php
new file mode 100644 (file)
index 0000000..46af1a8
--- /dev/null
@@ -0,0 +1,26 @@
+<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
index 6b2dc3c2319a7c7656fe4fb86c9926ed57dcb787..480a7603e515505f028e704c66a556d4a9b2d33d 100644 (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')
index 71b8f551fc97b22a083a82639421cae9fa53966a..02ef525ea9be70547618a8cdc8378c4a62c0062f 100644 (file)
                             <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>
index 8ecfd9465ec6c2962375248722031e4a2daa3270..463e4e77ba494ce10d476017f1b9d9d385d88b29 100644 (file)
@@ -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');