]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'fix-api-404' into development
authorDan Brown <redacted>
Thu, 15 Jun 2023 16:08:51 +0000 (17:08 +0100)
committerDan Brown <redacted>
Thu, 15 Jun 2023 16:08:51 +0000 (17:08 +0100)
54 files changed:
app/Activity/CommentRepo.php
app/Activity/Controllers/CommentController.php
app/Activity/Tools/CommentTree.php [new file with mode: 0644]
app/Api/UserApiTokenController.php
app/Entities/Controllers/ChapterController.php
app/Entities/Controllers/PageController.php
app/Entities/Controllers/PageRevisionController.php
app/Entities/Controllers/RecycleBinController.php
app/Entities/Queries/Popular.php
app/Permissions/PermissionApplicator.php
app/Search/SearchController.php
app/Settings/SettingController.php
app/Uploads/Controllers/ImageGalleryApiController.php
app/Users/Controllers/UserController.php
database/migrations/2023_06_10_071823_remove_guest_user_secondary_roles.php [new file with mode: 0644]
dev/api/responses/image-gallery-create.json
dev/api/responses/image-gallery-read.json
dev/api/responses/image-gallery-update.json
lang/en/activities.php
lang/en/entities.php
lang/en/errors.php
lang/en/settings.php
resources/js/components/index.js
resources/js/components/page-comment.js [new file with mode: 0644]
resources/js/components/page-comments.js
resources/js/components/page-editor.js
resources/js/markdown/actions.js
resources/sass/_blocks.scss
resources/sass/_codemirror.scss
resources/sass/_colors.scss
resources/sass/_components.scss
resources/sass/_content.scss [new file with mode: 0644]
resources/sass/_forms.scss
resources/sass/_pages.scss
resources/sass/_text.scss
resources/sass/_tinymce.scss
resources/sass/_variables.scss
resources/sass/export-styles.scss
resources/sass/styles.scss
resources/views/comments/comment-branch.blade.php [new file with mode: 0644]
resources/views/comments/comment.blade.php
resources/views/comments/comments.blade.php
resources/views/comments/create.blade.php
resources/views/pages/parts/editor-toolbar.blade.php
resources/views/pages/parts/form.blade.php
resources/views/pages/show.blade.php
routes/web.php
tests/Api/ImageGalleryApiTest.php
tests/Entity/CommentTest.php
tests/Entity/EntitySearchTest.php
tests/Entity/PageDraftTest.php
tests/LanguageTest.php
tests/Permissions/RolesTest.php
tests/PublicActionTest.php

index f16767fcf1a3922a58c6edbdf04a76e5e22e9cfb..2aabab79de3b8af0e01cf500ea65e7db53cf7fd4 100644 (file)
@@ -7,27 +7,14 @@ use BookStack\Entities\Models\Entity;
 use BookStack\Facades\Activity as ActivityService;
 use League\CommonMark\CommonMarkConverter;
 
-/**
- * Class CommentRepo.
- */
 class CommentRepo
 {
-    /**
-     * @var Comment
-     */
-    protected $comment;
-
-    public function __construct(Comment $comment)
-    {
-        $this->comment = $comment;
-    }
-
     /**
      * Get a comment by ID.
      */
     public function getById(int $id): Comment
     {
-        return $this->comment->newQuery()->findOrFail($id);
+        return Comment::query()->findOrFail($id);
     }
 
     /**
@@ -36,7 +23,7 @@ class CommentRepo
     public function create(Entity $entity, string $text, ?int $parent_id): Comment
     {
         $userId = user()->id;
-        $comment = $this->comment->newInstance();
+        $comment = new Comment();
 
         $comment->text = $text;
         $comment->html = $this->commentToHtml($text);
@@ -83,7 +70,7 @@ class CommentRepo
             'allow_unsafe_links' => false,
         ]);
 
-        return $converter->convertToHtml($commentText);
+        return $converter->convert($commentText);
     }
 
     /**
@@ -91,9 +78,8 @@ class CommentRepo
      */
     protected function getNextLocalId(Entity $entity): int
     {
-        /** @var Comment $comment */
-        $comment = $entity->comments(false)->orderBy('local_id', 'desc')->first();
+        $currentMaxId = $entity->comments()->max('local_id');
 
-        return ($comment->local_id ?? 0) + 1;
+        return $currentMaxId + 1;
     }
 }
index b198d2d568bf29dea78d2056291a7cd4953bf1fd..9e7491fd7c71dfa2bea24ff675406ca5e9f5ee53 100644 (file)
@@ -10,11 +10,9 @@ use Illuminate\Validation\ValidationException;
 
 class CommentController extends Controller
 {
-    protected $commentRepo;
-
-    public function __construct(CommentRepo $commentRepo)
-    {
-        $this->commentRepo = $commentRepo;
+    public function __construct(
+        protected CommentRepo $commentRepo
+    ) {
     }
 
     /**
@@ -43,7 +41,12 @@ class CommentController extends Controller
         $this->checkPermission('comment-create-all');
         $comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
 
-        return view('comments.comment', ['comment' => $comment]);
+        return view('comments.comment-branch', [
+            'branch' => [
+                'comment' => $comment,
+                'children' => [],
+            ]
+        ]);
     }
 
     /**
diff --git a/app/Activity/Tools/CommentTree.php b/app/Activity/Tools/CommentTree.php
new file mode 100644 (file)
index 0000000..3303add
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+namespace BookStack\Activity\Tools;
+
+use BookStack\Activity\Models\Comment;
+use BookStack\Entities\Models\Page;
+
+class CommentTree
+{
+    /**
+     * The built nested tree structure array.
+     * @var array{comment: Comment, depth: int, children: array}[]
+     */
+    protected array $tree;
+    protected array $comments;
+
+    public function __construct(
+        protected Page $page
+    ) {
+        $this->comments = $this->loadComments();
+        $this->tree = $this->createTree($this->comments);
+    }
+
+    public function enabled(): bool
+    {
+        return !setting('app-disable-comments');
+    }
+
+    public function empty(): bool
+    {
+        return count($this->tree) === 0;
+    }
+
+    public function count(): int
+    {
+        return count($this->comments);
+    }
+
+    public function get(): array
+    {
+        return $this->tree;
+    }
+
+    /**
+     * @param Comment[] $comments
+     */
+    protected function createTree(array $comments): array
+    {
+        $byId = [];
+        foreach ($comments as $comment) {
+            $byId[$comment->local_id] = $comment;
+        }
+
+        $childMap = [];
+        foreach ($comments as $comment) {
+            $parent = $comment->parent_id;
+            if (is_null($parent) || !isset($byId[$parent])) {
+                $parent = 0;
+            }
+
+            if (!isset($childMap[$parent])) {
+                $childMap[$parent] = [];
+            }
+            $childMap[$parent][] = $comment->local_id;
+        }
+
+        $tree = [];
+        foreach ($childMap[0] ?? [] as $childId) {
+            $tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
+        }
+
+        return $tree;
+    }
+
+    protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array
+    {
+        $childIds = $childMap[$id] ?? [];
+        $children = [];
+
+        foreach ($childIds as $childId) {
+            $children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
+        }
+
+        return [
+            'comment' => $byId[$id],
+            'depth' => $depth,
+            'children' => $children,
+        ];
+    }
+
+    protected function loadComments(): array
+    {
+        if (!$this->enabled()) {
+            return [];
+        }
+
+        return $this->page->comments()
+            ->with('createdBy')
+            ->get()
+            ->all();
+    }
+}
index d8fc1171c41f11e3be488dee6171c114b9f8190e..8357420ee13015f4d8dac9d6315f4b27a267cc28 100644 (file)
@@ -58,7 +58,6 @@ class UserApiTokenController extends Controller
         $token->save();
 
         session()->flash('api-token-secret:' . $token->id, $secret);
-        $this->showSuccessNotification(trans('settings.user_api_token_create_success'));
         $this->logActivity(ActivityType::API_TOKEN_CREATE, $token);
 
         return redirect($user->getEditUrl('/api-tokens/' . $token->id));
@@ -96,7 +95,6 @@ class UserApiTokenController extends Controller
             'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),
         ])->save();
 
-        $this->showSuccessNotification(trans('settings.user_api_token_update_success'));
         $this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);
 
         return redirect($user->getEditUrl('/api-tokens/' . $token->id));
@@ -123,7 +121,6 @@ class UserApiTokenController extends Controller
         [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
         $token->delete();
 
-        $this->showSuccessNotification(trans('settings.user_api_token_delete_success'));
         $this->logActivity(ActivityType::API_TOKEN_DELETE, $token);
 
         return redirect($user->getEditUrl('#api_tokens'));
index cf7611685b1529ee9ca940d4fbb4321e7b3dbecc..7dcb669038bca6d770c59b1da206a4f3a74c8afe 100644 (file)
@@ -191,8 +191,6 @@ class ChapterController extends Controller
             return redirect()->back();
         }
 
-        $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
-
         return redirect($chapter->getUrl());
     }
 
index a6ef68dd71d7e2de16c9bccfc332cd8b8f366933..3187e6486fa6d60b82ea2a0e6625a278ce4f223f 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Entities\Controllers;
 
 use BookStack\Activity\Models\View;
+use BookStack\Activity\Tools\CommentTree;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Entities\Tools\BookContents;
@@ -140,15 +141,10 @@ class PageController extends Controller
 
         $pageContent = (new PageContent($page));
         $page->html = $pageContent->render();
-        $sidebarTree = (new BookContents($page->book))->getTree();
         $pageNav = $pageContent->getNavigation($page->html);
 
-        // Check if page comments are enabled
-        $commentsEnabled = !setting('app-disable-comments');
-        if ($commentsEnabled) {
-            $page->load(['comments.createdBy']);
-        }
-
+        $sidebarTree = (new BookContents($page->book))->getTree();
+        $commentTree = (new CommentTree($page));
         $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
 
         View::incrementFor($page);
@@ -159,7 +155,7 @@ class PageController extends Controller
             'book'            => $page->book,
             'current'         => $page,
             'sidebarTree'     => $sidebarTree,
-            'commentsEnabled' => $commentsEnabled,
+            'commentTree'     => $commentTree,
             'pageNav'         => $pageNav,
             'next'            => $nextPreviousLocator->getNext(),
             'previous'        => $nextPreviousLocator->getPrevious(),
@@ -393,7 +389,7 @@ class PageController extends Controller
         }
 
         try {
-            $parent = $this->pageRepo->move($page, $entitySelection);
+            $this->pageRepo->move($page, $entitySelection);
         } catch (PermissionsException $exception) {
             $this->showPermissionError();
         } catch (Exception $exception) {
@@ -402,8 +398,6 @@ class PageController extends Controller
             return redirect()->back();
         }
 
-        $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
-
         return redirect($page->getUrl());
     }
 
index a723513a868011b5ed398e9b4758592958425c89..a3190a0fc43ee6627ef763afd6ecc1b665e681a1 100644 (file)
@@ -5,6 +5,7 @@ namespace BookStack\Entities\Controllers;
 use BookStack\Activity\ActivityType;
 use BookStack\Entities\Models\PageRevision;
 use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Repos\RevisionRepo;
 use BookStack\Entities\Tools\PageContent;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Facades\Activity;
@@ -15,11 +16,10 @@ use Ssddanbrown\HtmlDiff\Diff;
 
 class PageRevisionController extends Controller
 {
-    protected PageRepo $pageRepo;
-
-    public function __construct(PageRepo $pageRepo)
-    {
-        $this->pageRepo = $pageRepo;
+    public function __construct(
+        protected PageRepo $pageRepo,
+        protected RevisionRepo $revisionRepo,
+    ) {
     }
 
     /**
@@ -153,8 +153,18 @@ class PageRevisionController extends Controller
 
         $revision->delete();
         Activity::add(ActivityType::REVISION_DELETE, $revision);
-        $this->showSuccessNotification(trans('entities.revision_delete_success'));
 
         return redirect($page->getUrl('/revisions'));
     }
+
+    /**
+     * Destroys existing drafts, belonging to the current user, for the given page.
+     */
+    public function destroyUserDraft(string $pageId)
+    {
+        $page = $this->pageRepo->getById($pageId);
+        $this->revisionRepo->deleteDraftsForCurrentUser($page);
+
+        return response('', 200);
+    }
 }
index 30b184bbe00c9356cffe5707606faf0d127e9ecb..78f86a5ae5076df7c5cea7320b2c25fc657a121b 100644 (file)
@@ -11,7 +11,7 @@ use BookStack\Http\Controller;
 
 class RecycleBinController extends Controller
 {
-    protected $recycleBinBaseUrl = '/settings/recycle-bin';
+    protected string $recycleBinBaseUrl = '/settings/recycle-bin';
 
     /**
      * On each request to a method of this controller check permissions
index 9b7049ae3867e278689ac31370c1ee4edc7e8053..a934f346b95f43e778f1688f76f3285c723d9fe2 100644 (file)
@@ -3,6 +3,10 @@
 namespace BookStack\Entities\Queries;
 
 use BookStack\Activity\Models\View;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Entity;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\DB;
 
 class Popular extends EntityQuery
@@ -19,11 +23,24 @@ class Popular extends EntityQuery
             $query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
         }
 
-        return $query->with('viewable')
+        $entities = $query->with('viewable')
             ->skip($count * ($page - 1))
             ->take($count)
             ->get()
             ->pluck('viewable')
             ->filter();
+
+        $this->loadBooksForChildren($entities);
+
+        return $entities;
+    }
+
+    protected function loadBooksForChildren(Collection $entities)
+    {
+        $bookChildren = $entities->filter(fn(Entity $entity) => $entity instanceof BookChild);
+        $eloquent = (new \Illuminate\Database\Eloquent\Collection($bookChildren));
+        $eloquent->load(['book' => function (BelongsTo $query) {
+            $query->scopes('visible');
+        }]);
     }
 }
index 8a02f82e89bef03bd6297add1feeae921a49b5b7..b4fafaa9ee1756f6604d5cc20f46e2091ed5f787 100644 (file)
@@ -183,10 +183,6 @@ class PermissionApplicator
      */
     protected function getCurrentUserRoleIds(): array
     {
-        if (auth()->guest()) {
-            return [Role::getSystemRole('public')->id];
-        }
-
         return $this->currentUser()->roles->pluck('id')->values()->all();
     }
 
index 4b134f11e2b203e36ad0286705a2eeb4e139e5f4..09a67f2b5cae6b351cb70c6ec1a29de39d4207a0 100644 (file)
@@ -9,11 +9,9 @@ use Illuminate\Http\Request;
 
 class SearchController extends Controller
 {
-    protected SearchRunner $searchRunner;
-
-    public function __construct(SearchRunner $searchRunner)
-    {
-        $this->searchRunner = $searchRunner;
+    public function __construct(
+        protected SearchRunner $searchRunner
+    ) {
     }
 
     /**
index ffdd7545e7837678a6328591c8aa502e9bcaa290..bd55222f2302ee6a820fff3f67c8ecb8a007c4ad 100644 (file)
@@ -52,9 +52,7 @@ class SettingController extends Controller
         ]);
 
         $store->storeFromUpdateRequest($request, $category);
-
         $this->logActivity(ActivityType::SETTINGS_UPDATE, $category);
-        $this->showSuccessNotification(trans('settings.settings_save_success'));
 
         return redirect("/settings/{$category}");
     }
index 4fca6a4dd7d2fe28cacfd0f5227a21399b9100a7..c444ec663e81f5cfb7e895aa5c994dd202523d70 100644 (file)
@@ -129,7 +129,7 @@ class ImageGalleryApiController extends ApiController
     protected function formatForSingleResponse(Image $image): array
     {
         $this->imageRepo->loadThumbs($image);
-        $data = $image->getAttributes();
+        $data = $image->toArray();
         $data['created_by'] = $image->createdBy;
         $data['updated_by'] = $image->updatedBy;
         $data['content'] = [];
index b185f0856410ccdc4ca4d2fecfea5ff80481f031..1c1b7ba23903ae48465ac81ade2f742bb507ad5d 100644 (file)
@@ -19,13 +19,10 @@ use Illuminate\Validation\ValidationException;
 
 class UserController extends Controller
 {
-    protected UserRepo $userRepo;
-    protected ImageRepo $imageRepo;
-
-    public function __construct(UserRepo $userRepo, ImageRepo $imageRepo)
-    {
-        $this->userRepo = $userRepo;
-        $this->imageRepo = $imageRepo;
+    public function __construct(
+        protected UserRepo $userRepo,
+        protected ImageRepo $imageRepo
+    ) {
     }
 
     /**
diff --git a/database/migrations/2023_06_10_071823_remove_guest_user_secondary_roles.php b/database/migrations/2023_06_10_071823_remove_guest_user_secondary_roles.php
new file mode 100644 (file)
index 0000000..8d04efd
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        $guestUserId = DB::table('users')
+            ->where('system_name', '=', 'public')
+            ->first(['id'])->id;
+        $publicRoleId = DB::table('roles')
+            ->where('system_name', '=', 'public')
+            ->first(['id'])->id;
+
+        // This migration deletes secondary "Guest" user role assignments
+        // as a safety precaution upon upgrade since the logic is changing
+        // within the release this is introduced in, which could result in wider
+        // permissions being provided upon upgrade without this intervention.
+
+        // Previously, added roles would only partially apply their permissions
+        // since some permission checks would only consider the originally assigned
+        // public role, and not added roles. Within this release, additional roles
+        // will fully apply.
+        DB::table('role_user')
+            ->where('user_id', '=', $guestUserId)
+            ->where('role_id', '!=', $publicRoleId)
+            ->delete();
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        // No structural changes to make, and we cannot know the role ids to re-assign.
+    }
+};
index e278244915cdae5bdb7661879e67110d29fc01ee..0bf36d368c6456c2984f52e4c0fa792e8d73df4e 100644 (file)
@@ -14,8 +14,8 @@
     "name": "Admin",
     "slug": "admin"
   },
-  "updated_at": "2023-03-15 08:17:37",
-  "created_at": "2023-03-15 08:17:37",
+  "updated_at": "2023-03-15T16:32:09.000000Z",
+  "created_at": "2023-03-15T16:32:09.000000Z",
   "id": 618,
   "thumbs": {
     "gallery": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png",
index c6c468daa4fdad64945caaa96941b19efc755697..e3d5a92cab73601b15541f9cf0ce1a49696ce299 100644 (file)
@@ -2,8 +2,8 @@
   "id": 618,
   "name": "cute-cat-image.png",
   "url": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png",
-  "created_at": "2023-03-15 08:17:37",
-  "updated_at": "2023-03-15 08:17:37",
+  "created_at": "2023-03-15T16:32:09.000000Z",
+  "updated_at": "2023-03-15T16:32:09.000000Z",
   "created_by": {
     "id": 1,
     "name": "Admin",
index 6e6168a1b184a450e08fec09e93f62afca5f42bb..e72961918320b742c278a7f81b753c6e2d9a8d79 100644 (file)
@@ -2,8 +2,8 @@
   "id": 618,
   "name": "My updated image name",
   "url": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png",
-  "created_at": "2023-03-15 08:17:37",
-  "updated_at": "2023-03-15 08:24:50",
+  "created_at": "2023-03-15T16:32:09.000000Z",
+  "updated_at": "2023-03-15T18:31:14.000000Z",
   "created_by": {
     "id": 1,
     "name": "Admin",
index e89b8eab2555df9489efbafc0f6ad3cf2ab31608..e71a490de4696d4924e354db69751e746f0dbac4 100644 (file)
@@ -15,6 +15,7 @@ return [
     'page_restore'                => 'restored page',
     'page_restore_notification'   => 'Page successfully restored',
     'page_move'                   => 'moved page',
+    'page_move_notification'      => 'Page successfully moved',
 
     // Chapters
     'chapter_create'              => 'created chapter',
@@ -24,6 +25,7 @@ return [
     'chapter_delete'              => 'deleted chapter',
     'chapter_delete_notification' => 'Chapter successfully deleted',
     'chapter_move'                => 'moved chapter',
+    'chapter_move_notification' => 'Chapter successfully moved',
 
     // Books
     'book_create'                 => 'created book',
@@ -47,14 +49,30 @@ return [
     'bookshelf_delete'                 => 'deleted shelf',
     'bookshelf_delete_notification'    => 'Shelf successfully deleted',
 
+    // Revisions
+    'revision_restore' => 'restored revision',
+    'revision_delete' => 'deleted revision',
+    'revision_delete_notification' => 'Revision successfully deleted',
+
     // Favourites
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
-    // MFA
+    // Auth
+    'auth_login' => 'logged in',
+    'auth_register' => 'registered as new user',
+    'auth_password_reset_request' => 'requested user password reset',
+    'auth_password_reset_update' => 'reset user password',
+    'mfa_setup_method' => 'configured MFA method',
     'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method' => 'removed MFA method',
     'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 
+    // Settings
+    'settings_update' => 'updated settings',
+    'settings_update_notification' => 'Settings successfully updated',
+    'maintenance_action_run' => 'ran maintenance action',
+
     // Webhooks
     'webhook_create' => 'created webhook',
     'webhook_create_notification' => 'Webhook successfully created',
@@ -64,14 +82,34 @@ return [
     'webhook_delete_notification' => 'Webhook successfully deleted',
 
     // Users
+    'user_create' => 'created user',
+    'user_create_notification' => 'User successfully created',
+    'user_update' => 'updated user',
     'user_update_notification' => 'User successfully updated',
+    'user_delete' => 'deleted user',
     'user_delete_notification' => 'User successfully removed',
 
+    // API Tokens
+    'api_token_create' => 'created api token',
+    'api_token_create_notification' => 'API token successfully created',
+    'api_token_update' => 'updated api token',
+    'api_token_update_notification' => 'API token successfully updated',
+    'api_token_delete' => 'deleted api token',
+    'api_token_delete_notification' => 'API token successfully deleted',
+
     // Roles
+    'role_create' => 'created role',
     'role_create_notification' => 'Role successfully created',
+    'role_update' => 'updated role',
     'role_update_notification' => 'Role successfully updated',
+    'role_delete' => 'deleted role',
     'role_delete_notification' => 'Role successfully deleted',
 
+    // Recycle Bin
+    'recycle_bin_empty' => 'emptied recycle bin',
+    'recycle_bin_restore' => 'restored from recycle bin',
+    'recycle_bin_destroy' => 'removed from recycle bin',
+
     // Other
     'commented_on'                => 'commented on',
     'permissions_update'          => 'updated permissions',
index 501fc9f2a0e9231c5b7cd95cc9de59740819742d..5a148e1a259640349a07edde0269eda91bcd9049 100644 (file)
@@ -180,7 +180,6 @@ return [
     'chapters_save' => 'Save Chapter',
     'chapters_move' => 'Move Chapter',
     'chapters_move_named' => 'Move Chapter :chapterName',
-    'chapter_move_success' => 'Chapter moved to :bookName',
     'chapters_copy' => 'Copy Chapter',
     'chapters_copy_success' => 'Chapter successfully copied',
     'chapters_permissions' => 'Chapter Permissions',
@@ -214,6 +213,7 @@ return [
     'pages_editing_page' => 'Editing Page',
     'pages_edit_draft_save_at' => 'Draft saved at ',
     'pages_edit_delete_draft' => 'Delete Draft',
+    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',
     'pages_edit_discard_draft' => 'Discard Draft',
     'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
     'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
@@ -240,7 +240,6 @@ return [
     'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
-    'pages_move_success' => 'Page moved to ":parentName"',
     'pages_copy' => 'Copy Page',
     'pages_copy_desination' => 'Copy Destination',
     'pages_copy_success' => 'Page successfully copied',
@@ -287,7 +286,8 @@ return [
         'time_b' => 'in the last :minCount minutes',
         'message' => ':start :time. Take care not to overwrite each other\'s updates!',
     ],
-    'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
+    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',
+    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',
     'pages_specific' => 'Specific Page',
     'pages_is_template' => 'Page Template',
 
@@ -362,11 +362,10 @@ return [
     'comment_placeholder' => 'Leave a comment here',
     'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
     'comment_save' => 'Save Comment',
-    'comment_saving' => 'Saving comment...',
-    'comment_deleting' => 'Deleting comment...',
     'comment_new' => 'New Comment',
     'comment_created' => 'commented :createDiff',
     'comment_updated' => 'Updated :updateDiff by :username',
+    'comment_updated_indicator' => 'Updated',
     'comment_deleted_success' => 'Comment deleted',
     'comment_created_success' => 'Comment added',
     'comment_updated_success' => 'Comment updated',
@@ -376,7 +375,6 @@ return [
     // Revision
     'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
     'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
-    'revision_delete_success' => 'Revision deleted',
     'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',
 
     // Copy view
index b03fb8c355aa7bc9e1b18b95035dbc1b42ea085d..23c326f9e016f3222e81cbb9aab399a5780f1bea 100644 (file)
@@ -58,6 +58,7 @@ return [
 
     // Pages
     'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
+    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',
     'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',
 
     // Entities
index 38d817915b82ed3129d80d4f213a3444d8130af7..c110e89921ad97fa132e8d88305956469d3cf43c 100644 (file)
@@ -9,7 +9,6 @@ return [
     // Common Messages
     'settings' => 'Settings',
     'settings_save' => 'Save Settings',
-    'settings_save_success' => 'Settings saved',
     'system_version' => 'System Version',
     'categories' => 'Categories',
 
@@ -232,8 +231,6 @@ return [
     'user_api_token_expiry' => 'Expiry Date',
     'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',
     'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
-    'user_api_token_create_success' => 'API token successfully created',
-    'user_api_token_update_success' => 'API token successfully updated',
     'user_api_token' => 'API Token',
     'user_api_token_id' => 'Token ID',
     'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
@@ -244,7 +241,6 @@ return [
     'user_api_token_delete' => 'Delete Token',
     'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.',
     'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
-    'user_api_token_delete_success' => 'API token successfully deleted',
 
     // Webhooks
     'webhooks' => 'Webhooks',
index 803714e62a35658d3b134831eb896740d2687bfb..a56f18a5aff728f7f7aba88aeb9b6f42c645ad9a 100644 (file)
@@ -34,6 +34,7 @@ export {MarkdownEditor} from './markdown-editor';
 export {NewUserPassword} from './new-user-password';
 export {Notification} from './notification';
 export {OptionalInput} from './optional-input';
+export {PageComment} from './page-comment';
 export {PageComments} from './page-comments';
 export {PageDisplay} from './page-display';
 export {PageEditor} from './page-editor';
diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js
new file mode 100644 (file)
index 0000000..8284d7f
--- /dev/null
@@ -0,0 +1,96 @@
+import {Component} from './component';
+import {getLoading, htmlToDom} from '../services/dom';
+
+export class PageComment extends Component {
+
+    setup() {
+        // Options
+        this.commentId = this.$opts.commentId;
+        this.commentLocalId = this.$opts.commentLocalId;
+        this.commentParentId = this.$opts.commentParentId;
+        this.deletedText = this.$opts.deletedText;
+        this.updatedText = this.$opts.updatedText;
+
+        // Element References
+        this.container = this.$el;
+        this.contentContainer = this.$refs.contentContainer;
+        this.form = this.$refs.form;
+        this.formCancel = this.$refs.formCancel;
+        this.editButton = this.$refs.editButton;
+        this.deleteButton = this.$refs.deleteButton;
+        this.replyButton = this.$refs.replyButton;
+        this.input = this.$refs.input;
+
+        this.setupListeners();
+    }
+
+    setupListeners() {
+        if (this.replyButton) {
+            this.replyButton.addEventListener('click', () => this.$emit('reply', {
+                id: this.commentLocalId,
+                element: this.container,
+            }));
+        }
+
+        if (this.editButton) {
+            this.editButton.addEventListener('click', this.startEdit.bind(this));
+            this.form.addEventListener('submit', this.update.bind(this));
+            this.formCancel.addEventListener('click', () => this.toggleEditMode(false));
+        }
+
+        if (this.deleteButton) {
+            this.deleteButton.addEventListener('click', this.delete.bind(this));
+        }
+    }
+
+    toggleEditMode(show) {
+        this.contentContainer.toggleAttribute('hidden', show);
+        this.form.toggleAttribute('hidden', !show);
+    }
+
+    startEdit() {
+        this.toggleEditMode(true);
+        const lineCount = this.$refs.input.value.split('\n').length;
+        this.$refs.input.style.height = `${(lineCount * 20) + 40}px`;
+    }
+
+    async update(event) {
+        event.preventDefault();
+        const loading = this.showLoading();
+        this.form.toggleAttribute('hidden', true);
+
+        const reqData = {
+            text: this.input.value,
+            parent_id: this.parentId || null,
+        };
+
+        try {
+            const resp = await window.$http.put(`/comment/${this.commentId}`, reqData);
+            const newComment = htmlToDom(resp.data);
+            this.container.replaceWith(newComment);
+            window.$events.success(this.updatedText);
+        } catch (err) {
+            console.error(err);
+            window.$events.showValidationErrors(err);
+            this.form.toggleAttribute('hidden', false);
+            loading.remove();
+        }
+    }
+
+    async delete() {
+        this.showLoading();
+
+        await window.$http.delete(`/comment/${this.commentId}`);
+        this.container.closest('.comment-branch').remove();
+        window.$events.success(this.deletedText);
+        this.$emit('delete');
+    }
+
+    showLoading() {
+        const loading = getLoading();
+        loading.classList.add('px-l');
+        this.container.append(loading);
+        return loading;
+    }
+
+}
index 0ac9d05720de80cdf9ec1b71e31d4ed02c77d1e0..a46a5c3b3ef8e1de311bde9d2be816738d8f794a 100644 (file)
@@ -1,6 +1,5 @@
-import {scrollAndHighlightElement} from '../services/util';
 import {Component} from './component';
-import {htmlToDom} from '../services/dom';
+import {getLoading, htmlToDom} from '../services/dom';
 
 export class PageComments extends Component {
 
@@ -10,190 +9,130 @@ export class PageComments extends Component {
 
         // Element references
         this.container = this.$refs.commentContainer;
-        this.formContainer = this.$refs.formContainer;
         this.commentCountBar = this.$refs.commentCountBar;
+        this.commentsTitle = this.$refs.commentsTitle;
         this.addButtonContainer = this.$refs.addButtonContainer;
         this.replyToRow = this.$refs.replyToRow;
+        this.formContainer = this.$refs.formContainer;
+        this.form = this.$refs.form;
+        this.formInput = this.$refs.formInput;
+        this.formReplyLink = this.$refs.formReplyLink;
+        this.addCommentButton = this.$refs.addCommentButton;
+        this.hideFormButton = this.$refs.hideFormButton;
+        this.removeReplyToButton = this.$refs.removeReplyToButton;
 
         // Translations
-        this.updatedText = this.$opts.updatedText;
-        this.deletedText = this.$opts.deletedText;
         this.createdText = this.$opts.createdText;
         this.countText = this.$opts.countText;
 
         // Internal State
-        this.editingComment = null;
         this.parentId = null;
+        this.formReplyText = this.formReplyLink.textContent;
 
-        if (this.formContainer) {
-            this.form = this.formContainer.querySelector('form');
-            this.formInput = this.form.querySelector('textarea');
-            this.form.addEventListener('submit', this.saveComment.bind(this));
-        }
-
-        this.elem.addEventListener('click', this.handleAction.bind(this));
-        this.elem.addEventListener('submit', this.updateComment.bind(this));
+        this.setupListeners();
     }
 
-    handleAction(event) {
-        const actionElem = event.target.closest('[action]');
-
-        if (event.target.matches('a[href^="#"]')) {
-            const id = event.target.href.split('#')[1];
-            scrollAndHighlightElement(document.querySelector(`#${id}`));
-        }
-
-        if (actionElem === null) return;
-        event.preventDefault();
-
-        const action = actionElem.getAttribute('action');
-        const comment = actionElem.closest('[comment]');
-        if (action === 'edit') this.editComment(comment);
-        if (action === 'closeUpdateForm') this.closeUpdateForm();
-        if (action === 'delete') this.deleteComment(comment);
-        if (action === 'addComment') this.showForm();
-        if (action === 'hideForm') this.hideForm();
-        if (action === 'reply') this.setReply(comment);
-        if (action === 'remove-reply-to') this.removeReplyTo();
-    }
+    setupListeners() {
+        this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));
+        this.hideFormButton.addEventListener('click', this.hideForm.bind(this));
+        this.addCommentButton.addEventListener('click', this.showForm.bind(this));
 
-    closeUpdateForm() {
-        if (!this.editingComment) return;
-        this.editingComment.querySelector('[comment-content]').style.display = 'block';
-        this.editingComment.querySelector('[comment-edit-container]').style.display = 'none';
-    }
-
-    editComment(commentElem) {
-        this.hideForm();
-        if (this.editingComment) this.closeUpdateForm();
-        commentElem.querySelector('[comment-content]').style.display = 'none';
-        commentElem.querySelector('[comment-edit-container]').style.display = 'block';
-        const textArea = commentElem.querySelector('[comment-edit-container] textarea');
-        const lineCount = textArea.value.split('\n').length;
-        textArea.style.height = `${(lineCount * 20) + 40}px`;
-        this.editingComment = commentElem;
-    }
-
-    updateComment(event) {
-        const form = event.target;
-        event.preventDefault();
-        const text = form.querySelector('textarea').value;
-        const reqData = {
-            text,
-            parent_id: this.parentId || null,
-        };
-        this.showLoading(form);
-        const commentId = this.editingComment.getAttribute('comment');
-        window.$http.put(`/comment/${commentId}`, reqData).then(resp => {
-            const newComment = document.createElement('div');
-            newComment.innerHTML = resp.data;
-            this.editingComment.innerHTML = newComment.children[0].innerHTML;
-            window.$events.success(this.updatedText);
-            window.$components.init(this.editingComment);
-            this.closeUpdateForm();
-            this.editingComment = null;
-        }).catch(window.$events.showValidationErrors).then(() => {
-            this.hideLoading(form);
-        });
-    }
-
-    deleteComment(commentElem) {
-        const id = commentElem.getAttribute('comment');
-        this.showLoading(commentElem.querySelector('[comment-content]'));
-        window.$http.delete(`/comment/${id}`).then(() => {
-            commentElem.parentNode.removeChild(commentElem);
-            window.$events.success(this.deletedText);
+        this.elem.addEventListener('page-comment-delete', () => {
             this.updateCount();
             this.hideForm();
         });
+
+        this.elem.addEventListener('page-comment-reply', event => {
+            this.setReply(event.detail.id, event.detail.element);
+        });
+
+        if (this.form) {
+            this.form.addEventListener('submit', this.saveComment.bind(this));
+        }
     }
 
     saveComment(event) {
         event.preventDefault();
         event.stopPropagation();
+
+        const loading = getLoading();
+        loading.classList.add('px-l');
+        this.form.after(loading);
+        this.form.toggleAttribute('hidden', true);
+
         const text = this.formInput.value;
         const reqData = {
             text,
             parent_id: this.parentId || null,
         };
-        this.showLoading(this.form);
+
         window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
             const newElem = htmlToDom(resp.data);
-            this.container.appendChild(newElem);
-            window.$components.init(newElem);
+            this.formContainer.after(newElem);
             window.$events.success(this.createdText);
-            this.resetForm();
+            this.hideForm();
             this.updateCount();
         }).catch(err => {
+            this.form.toggleAttribute('hidden', false);
             window.$events.showValidationErrors(err);
-            this.hideLoading(this.form);
         });
+
+        this.form.toggleAttribute('hidden', false);
+        loading.remove();
     }
 
     updateCount() {
-        const count = this.container.children.length;
-        this.elem.querySelector('[comments-title]').textContent = window.trans_plural(this.countText, count, {count});
+        const count = this.getCommentCount();
+        this.commentsTitle.textContent = window.trans_plural(this.countText, count, {count});
     }
 
     resetForm() {
         this.formInput.value = '';
-        this.formContainer.appendChild(this.form);
-        this.hideForm();
-        this.removeReplyTo();
-        this.hideLoading(this.form);
+        this.parentId = null;
+        this.replyToRow.toggleAttribute('hidden', true);
+        this.container.append(this.formContainer);
     }
 
     showForm() {
-        this.formContainer.style.display = 'block';
-        this.formContainer.parentNode.style.display = 'block';
-        this.addButtonContainer.style.display = 'none';
-        this.formInput.focus();
-        this.formInput.scrollIntoView({behavior: 'smooth'});
+        this.formContainer.toggleAttribute('hidden', false);
+        this.addButtonContainer.toggleAttribute('hidden', true);
+        this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+        setTimeout(() => {
+            this.formInput.focus();
+        }, 100);
     }
 
     hideForm() {
-        this.formContainer.style.display = 'none';
-        this.formContainer.parentNode.style.display = 'none';
+        this.resetForm();
+        this.formContainer.toggleAttribute('hidden', true);
         if (this.getCommentCount() > 0) {
-            this.elem.appendChild(this.addButtonContainer);
+            this.elem.append(this.addButtonContainer);
         } else {
-            this.commentCountBar.appendChild(this.addButtonContainer);
+            this.commentCountBar.append(this.addButtonContainer);
         }
-        this.addButtonContainer.style.display = 'block';
+        this.addButtonContainer.toggleAttribute('hidden', false);
     }
 
     getCommentCount() {
-        return this.elem.querySelectorAll('.comment-box[comment]').length;
+        return this.container.querySelectorAll('[component="page-comment"]').length;
     }
 
-    setReply(commentElem) {
+    setReply(commentLocalId, commentElement) {
+        const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children');
+        targetFormLocation.append(this.formContainer);
         this.showForm();
-        this.parentId = Number(commentElem.getAttribute('local-id'));
-        this.replyToRow.style.display = 'block';
+        this.parentId = commentLocalId;
+        this.replyToRow.toggleAttribute('hidden', false);
         const replyLink = this.replyToRow.querySelector('a');
-        replyLink.textContent = `#${this.parentId}`;
+        replyLink.textContent = this.formReplyText.replace('1234', this.parentId);
         replyLink.href = `#comment${this.parentId}`;
     }
 
     removeReplyTo() {
         this.parentId = null;
-        this.replyToRow.style.display = 'none';
-    }
-
-    showLoading(formElem) {
-        const groups = formElem.querySelectorAll('.form-group');
-        for (const group of groups) {
-            group.style.display = 'none';
-        }
-        formElem.querySelector('.form-group.loading').style.display = 'block';
-    }
-
-    hideLoading(formElem) {
-        const groups = formElem.querySelectorAll('.form-group');
-        for (const group of groups) {
-            group.style.display = 'block';
-        }
-        formElem.querySelector('.form-group.loading').style.display = 'none';
+        this.replyToRow.toggleAttribute('hidden', true);
+        this.container.append(this.formContainer);
+        this.showForm();
     }
 
 }
index e7f4c0ba95907345e7ad25b6b0fc2fa3284c8853..963c21008968642314050fbf179be06dd8d48625 100644 (file)
@@ -19,18 +19,23 @@ export class PageEditor extends Component {
         this.saveDraftButton = this.$refs.saveDraft;
         this.discardDraftButton = this.$refs.discardDraft;
         this.discardDraftWrap = this.$refs.discardDraftWrap;
+        this.deleteDraftButton = this.$refs.deleteDraft;
+        this.deleteDraftWrap = this.$refs.deleteDraftWrap;
         this.draftDisplay = this.$refs.draftDisplay;
         this.draftDisplayIcon = this.$refs.draftDisplayIcon;
         this.changelogInput = this.$refs.changelogInput;
         this.changelogDisplay = this.$refs.changelogDisplay;
         this.changeEditorButtons = this.$manyRefs.changeEditor || [];
         this.switchDialogContainer = this.$refs.switchDialog;
+        this.deleteDraftDialogContainer = this.$refs.deleteDraftDialog;
 
         // Translations
         this.draftText = this.$opts.draftText;
         this.autosaveFailText = this.$opts.autosaveFailText;
         this.editingPageText = this.$opts.editingPageText;
         this.draftDiscardedText = this.$opts.draftDiscardedText;
+        this.draftDeleteText = this.$opts.draftDeleteText;
+        this.draftDeleteFailText = this.$opts.draftDeleteFailText;
         this.setChangelogText = this.$opts.setChangelogText;
 
         // State data
@@ -75,6 +80,7 @@ export class PageEditor extends Component {
         // Draft Controls
         onSelect(this.saveDraftButton, this.saveDraft.bind(this));
         onSelect(this.discardDraftButton, this.discardDraft.bind(this));
+        onSelect(this.deleteDraftButton, this.deleteDraft.bind(this));
 
         // Change editor controls
         onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
@@ -119,7 +125,8 @@ export class PageEditor extends Component {
         try {
             const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
             if (!this.isNewDraft) {
-                this.toggleDiscardDraftVisibility(true);
+                this.discardDraftWrap.toggleAttribute('hidden', false);
+                this.deleteDraftWrap.toggleAttribute('hidden', false);
             }
 
             this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
@@ -154,7 +161,7 @@ export class PageEditor extends Component {
         }, 2000);
     }
 
-    async discardDraft() {
+    async discardDraft(notify = true) {
         let response;
         try {
             response = await window.$http.get(`/ajax/page/${this.pageId}`);
@@ -168,7 +175,7 @@ export class PageEditor extends Component {
         }
 
         this.draftDisplay.innerText = this.editingPageText;
-        this.toggleDiscardDraftVisibility(false);
+        this.discardDraftWrap.toggleAttribute('hidden', true);
         window.$events.emit('editor::replace', {
             html: response.data.html,
             markdown: response.data.markdown,
@@ -178,7 +185,30 @@ export class PageEditor extends Component {
         window.setTimeout(() => {
             this.startAutoSave();
         }, 1000);
-        window.$events.emit('success', this.draftDiscardedText);
+
+        if (notify) {
+            window.$events.success(this.draftDiscardedText);
+        }
+    }
+
+    async deleteDraft() {
+        /** @var {ConfirmDialog} * */
+        const dialog = window.$components.firstOnElement(this.deleteDraftDialogContainer, 'confirm-dialog');
+        const confirmed = await dialog.show();
+        if (!confirmed) {
+            return;
+        }
+
+        try {
+            const discard = this.discardDraft(false);
+            const draftDelete = window.$http.delete(`/page-revisions/user-drafts/${this.pageId}`);
+            await Promise.all([discard, draftDelete]);
+            window.$events.success(this.draftDeleteText);
+            this.deleteDraftWrap.toggleAttribute('hidden', true);
+        } catch (err) {
+            console.error(err);
+            window.$events.error(this.draftDeleteFailText);
+        }
     }
 
     updateChangelogDisplay() {
@@ -191,10 +221,6 @@ export class PageEditor extends Component {
         this.changelogDisplay.innerText = summary;
     }
 
-    toggleDiscardDraftVisibility(show) {
-        this.discardDraftWrap.classList.toggle('hidden', !show);
-    }
-
     async changeEditor(event) {
         event.preventDefault();
 
index 514bff87d86b87379a49a75035958f637394bd39..f66b7921dea70b6951857d5c259592d0a51220ef 100644 (file)
@@ -433,7 +433,9 @@ export class Actions {
      */
     #setText(text, selectionRange = null) {
         selectionRange = selectionRange || this.#getSelectionRange();
-        this.#dispatchChange(0, this.editor.cm.state.doc.length, text, selectionRange.from);
+        const newDoc = this.editor.cm.state.toText(text);
+        const newSelectFrom = Math.min(selectionRange.from, newDoc.length);
+        this.#dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom);
         this.focus();
     }
 
index 1d9bfc272e9b806d69b6206f729ab3e6478c1d52..54c509ef928f5e4c6e2fe31124f3bdc68a7e732a 100644 (file)
@@ -1,63 +1,3 @@
-
-/**
- * Callouts
- */
-.callout {
-  border-inline-start: 3px solid #BBB;
-  background-color: #EEE;
-  padding: $-s $-s $-s $-xl;
-  display: block;
-  position: relative;
-  overflow: auto;
-  &:before {
-    background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMTUzODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJ6Ii8+PC9zdmc+');
-    background-repeat: no-repeat;
-    content: '';
-    width: 1.2em;
-    height: 1.2em;
-    left: $-xs + 2px;
-    top: 50%;
-    margin-top: -9px;
-    display: inline-block;
-    position: absolute;
-    line-height: 1;
-    opacity: 0.8;
-  }
-  &.success {
-    border-left-color: $positive;
-    @include lightDark(background-color, lighten($positive, 68%), darken($positive, 22%));
-    @include lightDark(color, darken($positive, 16%), lighten($positive, 5%));
-  }
-  &.success:before {
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+");
-  }
-  &.danger {
-    border-left-color: $negative;
-    @include lightDark(background-color, lighten($negative, 56%), darken($negative, 30%));
-    @include lightDark(color, darken($negative, 20%), lighten($negative, 5%));
-  }
-  &.danger:before {
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==");
-  }
-  &.info {
-    border-left-color: $info;
-    @include lightDark(color, darken($info, 20%), lighten($info, 10%));
-    @include lightDark(background-color, lighten($info, 50%), darken($info, 35%));
-  }
-  &.warning {
-    border-left-color: $warning;
-    @include lightDark(background-color, lighten($warning, 50%), darken($warning, 36%));
-    @include lightDark(color, darken($warning, 20%), $warning);
-  }
-  &.warning:before {
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
-  }
-  a {
-    color: inherit;
-    text-decoration: underline;
-  }
-}
-
 /**
  * Card-style blocks
  */
index 0fd347cf893a3bc37f1f5379e849aa6140f552a9..c4b0e2e8958635b15aec5ff66c288d642f0f0108 100644 (file)
   border-radius: 4px;
 }
 
+.cm-editor .cm-line, .cm-editor .cm-gutter {
+  font-family: var(--font-code);
+}
+
 // Manual dark-mode definition so that it applies to code blocks within the shadow
 // dom which are used within the WYSIWYG editor, as the .dark-mode on the parent
 // <html> node are not applies so instead we have the class on the parent element.
@@ -50,7 +54,7 @@
     fill: currentColor;
   }
   &.success {
-    background: $positive;
+    background: var(--color-positive);
     color: #FFF;
   }
   &:focus {
index aff9ff6d05917ee3722483f045245832240000fc..c77c1d8b3880ea53d55a9accf5169e8cedeb206e 100644 (file)
  * Status text colors
  */
 .text-pos, .text-pos:hover, .text-pos-hover:hover {
-  color: $positive !important;
-  fill: $positive !important;
+  color: var(--color-positive) !important;
+  fill: var(--color-positive) !important;
 }
 
 .text-warn, .text-warn:hover, .text-warn-hover:hover {
-  color: $warning !important;
-  fill: $warning !important;
+  color: var(--color-warning) !important;
+  fill: var(--color-warning) !important;
 }
 
 .text-neg, .text-neg:hover, .text-neg-hover:hover  {
-  color: $negative !important;
-  fill: $negative !important;
+  color: var(--color-negative) !important;
+  fill: var(--color-negative) !important;
 }
 
 /*
index 1521e6eaa60d2c759280c404ac1dfec78caafc59..dab74341a7a2b26a30cea253871cb58202de98b6 100644 (file)
     }
   }
   &.pos {
-    color: $positive;
+    color: var(--color-positive);
   }
   &.neg {
-    color: $negative;
+    color: var(--color-negative);
   }
   &.warning {
-    color: $warning;
+    color: var(--color-warning);
   }
   &.showing {
     transform: translateX(0);
@@ -334,10 +334,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   line-height: 1.2;
 }
 .dropzone-file-item-status[data-status="success"] {
-  color: $positive;
+  color: var(--color-positive);
 }
 .dropzone-file-item-status[data-status="error"] {
-  color: $negative;
+  color: var(--color-negative);
 }
 .dropzone-file-item-status[data-status] + .dropzone-file-item-label {
   display: none;
@@ -574,7 +574,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   cursor: pointer;
   width: 100%;
   text-align: left;
-  font-family: $mono;
+  font-family: var(--font-code);
   font-size: 0.7rem;
   padding-left: 24px + $-xs;
   &:hover, &.active {
@@ -663,6 +663,12 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   }
 }
 
+.comments-container {
+  padding-inline: $-xl;
+  @include smaller-than($m) {
+    padding-inline: $-xs;
+  }
+}
 .comment-box {
   border-radius: 4px;
   border: 1px solid #DDD;
@@ -682,26 +688,51 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   &:hover .actions, &:focus-within .actions {
     opacity: 1;
   }
+  .actions button:focus {
+    outline: 1px dotted var(--color-primary);
+  }
+  @include smaller-than($m) {
+    .actions {
+      opacity: 1;
+    }
+  }
 }
 
 .comment-box .header {
-  .meta {
-    img, a, span {
-      display: inline-block;
-      vertical-align: top;
-    }
-    a, span {
-      padding: $-xxs 0 $-xxs 0;
-      line-height: 1.6;
-    }
-    a { color: #666; }
-    span {
-      padding-inline-start: $-xxs;
-    }
+  border-bottom: 1px solid #DDD;
+  @include lightDark(border-color, #DDD, #000);
+  button {
+    font-size: .8rem;
+  }
+  a {
+    color: inherit;
   }
   .text-muted {
     color: #999;
   }
+  .right-meta .text-muted {
+    opacity: .8;
+  }
+}
+
+.comment-thread-indicator {
+  border-inline-start: 3px dotted #DDD;
+  @include lightDark(border-color, #DDD, #444);
+  margin-inline-start: $-xs;
+  width: $-l;
+  height: calc(100% - $-m);
+}
+
+.comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator {
+  display: none;
+}
+
+.comment-reply {
+  display: none;
+}
+
+.comment-branch .comment-branch .comment-branch .comment-branch .comment-reply {
+  display: block;
 }
 
 #tag-manager .drag-card {
@@ -890,10 +921,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   display: inline-block;
 }
 .status-indicator-active {
-  background-color: $positive;
+  background-color: var(--color-positive);
 }
 .status-indicator-inactive {
-  background-color: $negative;
+  background-color: var(--color-negative);
 }
 
 .shortcut-container {
diff --git a/resources/sass/_content.scss b/resources/sass/_content.scss
new file mode 100644 (file)
index 0000000..10a2cd9
--- /dev/null
@@ -0,0 +1,175 @@
+/**
+ * Page Content
+ * Styles specific to blocks used within page content.
+ */
+
+.page-content {
+  width: 100%;
+  max-width: 840px;
+  margin: 0 auto;
+  overflow-wrap: break-word;
+  .align-left {
+    text-align: left;
+  }
+  img.align-left, table.align-left {
+    float: left !important;
+    margin: $-xs $-m $-m 0;
+  }
+  .align-right {
+    text-align: right !important;
+  }
+  img.align-right, table.align-right {
+    float: right !important;
+    margin: $-xs 0 $-xs $-s;
+  }
+  .align-center {
+    text-align: center;
+  }
+  img.align-center {
+    display: block;
+  }
+  img.align-center, table.align-center {
+    margin-left: auto;
+    margin-right: auto;
+  }
+  img {
+    max-width: 100%;
+    height:auto;
+  }
+  h1, h2, h3, h4, h5, h6, pre {
+    clear: left;
+  }
+  hr {
+    clear: both;
+    margin: $-m 0;
+  }
+  table {
+    hyphens: auto;
+    table-layout: fixed;
+    max-width: 100%;
+    height: auto !important;
+  }
+
+  // diffs
+  ins,
+  del {
+    text-decoration: none;
+  }
+  ins {
+    background: #dbffdb;
+  }
+  del {
+    background: #FFECEC;
+  }
+
+  details {
+    border: 1px solid;
+    @include lightDark(border-color, #DDD, #555);
+    margin-bottom: 1em;
+    padding: $-s;
+  }
+  details > summary {
+    margin-top: -$-s;
+    margin-left: -$-s;
+    margin-right: -$-s;
+    margin-bottom: -$-s;
+    font-weight: bold;
+    @include lightDark(background-color, #EEE, #333);
+    padding: $-xs $-s;
+  }
+  details[open] > summary {
+    margin-bottom: $-s;
+    border-bottom: 1px solid;
+    @include lightDark(border-color, #DDD, #555);
+  }
+  details > summary + * {
+    margin-top: .2em;
+  }
+  details:after {
+    content: '';
+    display: block;
+    clear: both;
+  }
+
+  li > input[type="checkbox"] {
+    vertical-align: top;
+    margin-top: 0.3em;
+  }
+
+  p:empty {
+    min-height: 1.6em;
+  }
+
+  &.page-revision {
+    pre code {
+      white-space: pre-wrap;
+    }
+  }
+
+  .cm-editor {
+    margin-bottom: 1.375em;
+  }
+
+  video {
+    max-width: 100%;
+  }
+}
+
+/**
+ * Callouts
+ */
+.callout {
+  border-left: 3px solid #BBB;
+  background-color: #EEE;
+  padding: $-s $-s $-s $-xl;
+  display: block;
+  position: relative;
+  overflow: auto;
+  &:before {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMTUzODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJ6Ii8+PC9zdmc+');
+    background-repeat: no-repeat;
+    content: '';
+    width: 1.2em;
+    height: 1.2em;
+    left: $-xs + 2px;
+    top: 50%;
+    margin-top: -9px;
+    display: inline-block;
+    position: absolute;
+    line-height: 1;
+    opacity: 0.8;
+  }
+  &.success {
+    @include lightDark(border-left-color, $positive, $positive-dark);
+    @include lightDark(background-color, lighten($positive, 68%), darken($positive-dark, 36%));
+    @include lightDark(color, darken($positive, 16%), $positive-dark);
+  }
+  &.success:before {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+");
+  }
+  &.danger {
+    @include lightDark(border-left-color, $negative, $negative-dark);
+    @include lightDark(background-color, lighten($negative, 56%), darken($negative-dark, 55%));
+    @include lightDark(color, darken($negative, 20%), $negative-dark);
+  }
+  &.danger:before {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==");
+  }
+  &.info {
+    @include lightDark(border-left-color, $info, $info-dark);
+    @include lightDark(color, darken($info, 20%), $info-dark);
+    @include lightDark(background-color, lighten($info, 50%), darken($info-dark, 34%));
+  }
+  &.warning {
+    @include lightDark(border-left-color, $warning, $warning-dark);
+    @include lightDark(background-color, lighten($warning, 50%), darken($warning-dark, 50%));
+    @include lightDark(color, darken($warning, 20%), $warning-dark);
+  }
+  &.warning:before {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
+  }
+  a {
+    color: inherit;
+    text-decoration: underline;
+  }
+}
\ No newline at end of file
index 5276bb566b0c8839a58376017fa79fed23166638..4722d9aa10f254398c930b03c931a4975bd4ecda 100644 (file)
   max-width: 100%;
 
   &.neg, &.invalid {
-    border: 1px solid $negative;
+    border: 1px solid var(--color-negative);
   }
   &.pos, &.valid {
-    border: 1px solid $positive;
+    border: 1px solid var(--color-positive);
   }
   &.disabled, &[disabled] {
     background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);
index 2a77e84ba34253330831383d949c8c15d0a087e1..fbac1de07c774215888f1f424cdd4bd9cf812d10 100755 (executable)
@@ -76,118 +76,6 @@ body.tox-fullscreen, body.markdown-fullscreen {
   padding: 0 !important;
 }
 
-.page-content {
-  width: 100%;
-  max-width: 840px;
-  margin: 0 auto;
-  overflow-wrap: break-word;
-  .align-left {
-    text-align: left;
-  }
-  img.align-left, table.align-left {
-    float: left !important;
-    margin: $-xs $-m $-m 0;
-  }
-  .align-right {
-    text-align: right !important;
-  }
-  img.align-right, table.align-right {
-    float: right !important;
-    margin: $-xs 0 $-xs $-s;
-  }
-  .align-center {
-    text-align: center;
-  }
-  img.align-center {
-    display: block;
-  }
-  img.align-center, table.align-center {
-    margin-left: auto;
-    margin-right: auto;
-  }
-  img {
-    max-width: 100%;
-    height:auto;
-  }
-  h1, h2, h3, h4, h5, h6, pre {
-    clear: left;
-  }
-  hr {
-    clear: both;
-    margin: $-m 0;
-  }
-  table {
-    hyphens: auto;
-    table-layout: fixed;
-    max-width: 100%;
-    height: auto !important;
-  }
-
-  // diffs
-  ins,
-  del {
-    text-decoration: none;
-  }
-  ins {
-    background: #dbffdb;
-  }
-  del {
-    background: #FFECEC;
-  }
-
-  details {
-    border: 1px solid;
-    @include lightDark(border-color, #DDD, #555);
-    margin-bottom: 1em;
-    padding: $-s;
-  }
-  details > summary {
-    margin-top: -$-s;
-    margin-left: -$-s;
-    margin-right: -$-s;
-    margin-bottom: -$-s;
-    font-weight: bold;
-    @include lightDark(background-color, #EEE, #333);
-    padding: $-xs $-s;
-  }
-  details[open] > summary {
-    margin-bottom: $-s;
-    border-bottom: 1px solid;
-    @include lightDark(border-color, #DDD, #555);
-  }
-  details > summary + * {
-    margin-top: .2em;
-  }
-  details:after {
-    content: '';
-    display: block;
-    clear: both;
-  }
-
-  li > input[type="checkbox"] {
-    vertical-align: top;
-    margin-top: 0.3em;
-  }
-
-  p:empty {
-    min-height: 1.6em;
-  }
-
-  &.page-revision {
-    pre code {
-      white-space: pre-wrap;
-    }
-  }
-
-  .cm-editor {
-    margin-bottom: 1.375em;
-  }
-
-  video {
-    max-width: 100%;
-  }
-}
-
 // Page content pointers
 .pointer-container {
   position: fixed;
index adfc87ad15a580345aca13cac6ca4fb231ac4372..f2c88d96dd1ffdc87761fa0ec7f45c45c779b450 100644 (file)
@@ -3,10 +3,10 @@
  */
 
 body, button, input, select, label, textarea {
-  font-family: $text;
+  font-family: var(--font-body);
 }
-.Codemirror, pre, #markdown-editor-input, .text-mono, .code-base {
-  font-family: $mono;
+pre, #markdown-editor-input, .text-mono, .code-base {
+  font-family: var(--font-code);
 }
 
 /*
@@ -42,6 +42,7 @@ h1, h2, h3, h4, h5, h6 {
   font-weight: 400;
   position: relative;
   display: block;
+  font-family: var(--font-heading);
   @include lightDark(color, #222, #BBB);
   .subheader {
     font-size: 0.5em;
@@ -210,7 +211,8 @@ pre {
 blockquote {
   display: block;
   position: relative;
-  border-left: 4px solid var(--color-primary);
+  border-left: 4px solid transparent;
+  border-left-color: var(--color-primary);
   @include lightDark(background-color, #f8f8f8, #333);
   padding: $-s $-m $-s $-xl;
   overflow: auto;
@@ -226,7 +228,7 @@ blockquote {
 }
 
 .text-mono {
-  font-family: $mono;
+  font-family: var(--font-code);
 }
 
 .text-uppercase {
index 7170f8101db3c8b8594bdab3179a8ef310fdc594..13b6f676b695380b4e4833d50e4c223f4691a4ab 100644 (file)
@@ -110,7 +110,7 @@ body.page-content.mce-content-body  {
   border-left: 3px solid currentColor !important;
 }
 .tox-menu .tox-collection__item[title^="<"] > div > div {
-  font-family: $mono !important;
+  font-family: var(--font-code) !important;
   border: 1px solid #DDD !important;
   background-color: #EEE !important;
   padding: 4px 6px !important;
index aac9223f9ff4e268155a1c9106758b4ae0524a82..5892237d91473321d7a83cd7cf6fa3865a9d2ccb 100644 (file)
@@ -27,16 +27,43 @@ $-xxs: 3px;
 $spacing: (('none', 0), ('xxs', $-xxs), ('xs', $-xs), ('s', $-s), ('m', $-m), ('l', $-l), ('xl', $-xl), ('xxl', $-xxl), ('auto', auto));
 
 // Fonts
-$text: -apple-system, BlinkMacSystemFont,
+$font-body: -apple-system, BlinkMacSystemFont,
 "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell",
 "Fira Sans", "Droid Sans", "Helvetica Neue",
 sans-serif;
-$mono: "Lucida Console", "DejaVu Sans Mono", "Ubuntu Mono", Monaco, monospace;
+$font-mono: "Lucida Console", "DejaVu Sans Mono", "Ubuntu Mono", Monaco, monospace;
 $fs-m: 14px;
 $fs-s: 12px;
 
 // Colours
+$positive: #0f7d15;
+$negative: #ab0f0e;
+$info: #0288D1;
+$warning: #cf4d03;
+$positive-dark: #4aa850;
+$negative-dark: #e85c5b;
+$info-dark: #0288D1;
+$warning-dark: #de8a5a;
+
+// Text colours
+$text-dark: #444;
+
+// Shadows
+$bs-light: 0 0 4px 1px #CCC;
+$bs-dark: 0 0 4px 1px rgba(0, 0, 0, 0.5);
+$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
+$bs-large: 0 1px 6px 1px rgba(22, 22, 22, 0.2);
+$bs-card: 0 1px 6px -1px rgba(0, 0, 0, 0.1);
+$bs-card-dark: 0 1px 6px -1px rgba(0, 0, 0, 0.5);
+$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
+
+// CSS root variables
 :root {
+  --font-body: #{$font-body};
+  --font-heading: #{$font-body};
+  --font-code: #{$font-mono};
+
+
   --color-primary: #206ea7;
   --color-primary-light: rgba(32,110,167,0.15);
   --color-link: #206ea7;
@@ -47,30 +74,22 @@ $fs-s: 12px;
   --color-book: #077b70;
   --color-bookshelf: #a94747;
 
+  --color-positive: #{$positive};
+  --color-negative: #{$negative};
+  --color-info: #{$info};
+  --color-warning: #{$warning};
+
   --bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(0, 0, 0,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E");
 }
 
 :root.dark-mode {
   --bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(255, 255, 255,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E");
   color-scheme: only dark;
+
+  --color-positive: #4aa850;
+  --color-negative: #e85c5b;
+  --color-warning: #de8a5a;
 }
 :root:not(.dark-mode) {
   color-scheme: only light;
-}
-
-$positive: #0f7d15;
-$negative: #ab0f0e;
-$info: #0288D1;
-$warning: #cf4d03;
-
-// Text colours
-$text-dark: #444;
-
-// Shadows
-$bs-light: 0 0 4px 1px #CCC;
-$bs-dark: 0 0 4px 1px rgba(0, 0, 0, 0.5);
-$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
-$bs-large: 0 1px 6px 1px rgba(22, 22, 22, 0.2);
-$bs-card: 0 1px 6px -1px rgba(0, 0, 0, 0.1);
-$bs-card-dark: 0 1px 6px -1px rgba(0, 0, 0, 0.5);
-$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
+}
\ No newline at end of file
index 1e39bd0564ed86d1a70cfc8e156d60a83d9c1b90..cfa1ebdf8a68e3cc6a483a75702780a511be0071 100644 (file)
@@ -3,11 +3,8 @@
 @import "mixins";
 @import "html";
 @import "text";
-@import "layout";
-@import "blocks";
 @import "tables";
-@import "lists";
-@import "pages";
+@import "content";
 
 html, body {
   background-color: #FFF;
index 9a8e5b36dc38747066df07d5e9d9157b10635dcc..c0ce7ba6354d9c921106479cc3e9cb11526e10a4 100644 (file)
@@ -21,6 +21,7 @@
 @import "footer";
 @import "lists";
 @import "pages";
+@import "content";
 
 // Jquery Sortable Styles
 .dragged {
diff --git a/resources/views/comments/comment-branch.blade.php b/resources/views/comments/comment-branch.blade.php
new file mode 100644 (file)
index 0000000..78d19ac
--- /dev/null
@@ -0,0 +1,15 @@
+<div class="comment-branch">
+    <div class="mb-m">
+        @include('comments.comment', ['comment' => $branch['comment']])
+    </div>
+    <div class="flex-container-row">
+        <div class="comment-thread-indicator-parent">
+            <div class="comment-thread-indicator"></div>
+        </div>
+        <div class="comment-branch-children flex">
+            @foreach($branch['children'] as $childBranch)
+                @include('comments.comment-branch', ['branch' => $childBranch])
+            @endforeach
+        </div>
+    </div>
+</div>
\ No newline at end of file
index 6189c65d41e7fab03103ff4eec1d447d8425faf4..04468b83c579a14ca1fde746810a098a74101e41 100644 (file)
@@ -1,78 +1,82 @@
-<div class="comment-box mb-m" comment="{{ $comment->id }}" local-id="{{$comment->local_id}}" parent-id="{{$comment->parent_id}}" id="comment{{$comment->local_id}}">
+<div component="page-comment"
+     option:page-comment:comment-id="{{ $comment->id }}"
+     option:page-comment:comment-local-id="{{ $comment->local_id }}"
+     option:page-comment:comment-parent-id="{{ $comment->parent_id }}"
+     option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}"
+     option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}"
+     id="comment{{$comment->local_id}}"
+     class="comment-box">
     <div class="header p-s">
-        <div class="grid half left-focus no-gap v-center">
-            <div class="meta text-muted text-small">
-                <a href="#comment{{$comment->local_id}}">#{{$comment->local_id}}</a>
-                &nbsp;&nbsp;
+        <div class="flex-container-row justify-space-between wrap">
+            <div class="meta text-muted flex-container-row items-center">
                 @if ($comment->createdBy)
-                    <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}">
+                    <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar mx-xs" alt="{{ $comment->createdBy->name }}">
                     &nbsp;
-                    <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->name }}</a>
+                    <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->getShortName(16) }}</a>
                 @else
-                    <span>{{ trans('common.deleted_user') }}</span>
+                    {{ trans('common.deleted_user') }}
                 @endif
-                <span title="{{ $comment->created_at }}">{{ trans('entities.comment_created', ['createDiff' => $comment->created]) }}</span>
+                <span title="{{ $comment->created_at }}">&nbsp;{{ trans('entities.comment_created', ['createDiff' => $comment->created]) }}</span>
                 @if($comment->isUpdated())
-                    <span title="{{ $comment->updated_at }}">
-                &bull;&nbsp;
-                    {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy? $comment->updatedBy->name : trans('common.deleted_user')]) }}
-            </span>
+                    <span class="mx-xs">&bull;</span>
+                    <span title="{{ trans('entities.comment_updated', ['updateDiff' => $comment->updated_at, 'username' => $comment->updatedBy->name ?? trans('common.deleted_user')]) }}">
+                 {{ trans('entities.comment_updated_indicator') }}
+                    </span>
                 @endif
             </div>
-            <div class="actions text-right">
-                @if(userCan('comment-update', $comment))
-                    <button type="button" class="text-button" action="edit" aria-label="{{ trans('common.edit') }}" title="{{ trans('common.edit') }}">@icon('edit')</button>
-                @endif
-                @if(userCan('comment-create-all'))
-                    <button type="button" class="text-button" action="reply" aria-label="{{ trans('common.reply') }}" title="{{ trans('common.reply') }}">@icon('reply')</button>
-                @endif
-                @if(userCan('comment-delete', $comment))
-                    <div component="dropdown" class="dropdown-container">
-                        <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
-                        <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
-                            <li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li>
-                            <li>
-                                <button action="delete" type="button" class="text-button text-neg icon-item">
-                                    @icon('delete')
-                                    <div>{{ trans('common.delete') }}</div>
-                                </button>
-                            </li>
-                        </ul>
-                    </div>
-                @endif
+            <div class="right-meta flex-container-row justify-flex-end items-center px-s">
+                <div class="actions mr-s">
+                    @if(userCan('comment-create-all'))
+                        <button refs="page-comment@reply-button" type="button" class="text-button text-muted hover-underline p-xs">@icon('reply') {{ trans('common.reply') }}</button>
+                    @endif
+                    @if(userCan('comment-update', $comment))
+                        <button refs="page-comment@edit-button" type="button" class="text-button text-muted hover-underline p-xs">@icon('edit') {{ trans('common.edit') }}</button>
+                    @endif
+                    @if(userCan('comment-delete', $comment))
+                        <div component="dropdown" class="dropdown-container">
+                            <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button text-muted hover-underline p-xs">@icon('delete') {{ trans('common.delete') }}</button>
+                            <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
+                                <li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li>
+                                <li>
+                                    <button refs="page-comment@delete-button" type="button" class="text-button text-neg icon-item">
+                                        @icon('delete')
+                                        <div>{{ trans('common.delete') }}</div>
+                                    </button>
+                                </li>
+                            </ul>
+                        </div>
+                    @endif
+                    <span class="text-muted">
+                        &nbsp;&bull;&nbsp;
+                    </span>
+                </div>
+                <div>
+                    <a class="bold text-muted" href="#comment{{$comment->local_id}}">#{{$comment->local_id}}</a>
+                </div>
             </div>
         </div>
 
     </div>
 
-    @if ($comment->parent_id)
-        <div class="reply-row primary-background-light text-muted px-s py-xs mb-s">
-            {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href="#comment'.$comment->parent_id.'">#'.$comment->parent_id.'</a>']) !!}
-        </div>
-    @endif
-
-    <div comment-content class="content px-s pb-s">
-        <div class="form-group loading" style="display: none;">
-            @include('common.loading-icon', ['text' => trans('entities.comment_deleting')])
-        </div>
+    <div refs="page-comment@content-container" class="content px-m py-s">
+        @if ($comment->parent_id)
+            <p class="comment-reply mb-xxs">
+                <a class="text-muted text-small" href="#comment{{ $comment->parent_id }}">@icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }}</a>
+            </p>
+        @endif
         {!! $comment->html  !!}
     </div>
 
     @if(userCan('comment-update', $comment))
-        <div comment-edit-container style="display: none;" class="content px-s">
-            <form novalidate>
-                <div class="form-group description-input">
-                    <textarea name="markdown" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $comment->text }}</textarea>
-                </div>
-                <div class="form-group text-right">
-                    <button type="button" class="button outline" action="closeUpdateForm">{{ trans('common.cancel') }}</button>
-                    <button type="submit" class="button">{{ trans('entities.comment_save') }}</button>
-                </div>
-                <div class="form-group loading" style="display: none;">
-                    @include('common.loading-icon', ['text' => trans('entities.comment_saving')])
-                </div>
-            </form>
-        </div>
+        <form novalidate refs="page-comment@form" hidden class="content pt-s px-s block">
+            <div class="form-group description-input">
+                <textarea refs="page-comment@input" name="markdown" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $comment->text }}</textarea>
+            </div>
+            <div class="form-group text-right">
+                <button type="button" class="button outline" refs="page-comment@form-cancel">{{ trans('common.cancel') }}</button>
+                <button type="submit" class="button">{{ trans('entities.comment_save') }}</button>
+            </div>
+        </form>
     @endif
 
 </div>
\ No newline at end of file
index 140d0d027d7ad1996a6795bec9cb61a9621769d9..b79f0fd45fda1d6bb767210fe1e490b9eb0a8abc 100644 (file)
@@ -1,34 +1,34 @@
 <section component="page-comments"
          option:page-comments:page-id="{{ $page->id }}"
-         option:page-comments:updated-text="{{ trans('entities.comment_updated_success') }}"
-         option:page-comments:deleted-text="{{ trans('entities.comment_deleted_success') }}"
          option:page-comments:created-text="{{ trans('entities.comment_created_success') }}"
          option:page-comments:count-text="{{ trans('entities.comment_count') }}"
          class="comments-list"
          aria-label="{{ trans('entities.comments') }}">
 
-    <div refs="page-comments@commentCountBar" class="grid half left-focus v-center no-row-gap">
-        <h5 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h5>
-        @if (count($page->comments) === 0 && userCan('comment-create-all'))
-            <div class="text-m-right" refs="page-comments@addButtonContainer">
-                <button type="button" action="addComment"
+    <div refs="page-comments@comment-count-bar" class="grid half left-focus v-center no-row-gap">
+        <h5 refs="page-comments@comments-title">{{ trans_choice('entities.comment_count', $commentTree->count(), ['count' => $commentTree->count()]) }}</h5>
+        @if ($commentTree->empty() && userCan('comment-create-all'))
+            <div class="text-m-right" refs="page-comments@add-button-container">
+                <button type="button"
+                        refs="page-comments@add-comment-button"
                         class="button outline">{{ trans('entities.comment_add') }}</button>
             </div>
         @endif
     </div>
 
     <div refs="page-comments@commentContainer" class="comment-container">
-        @foreach($page->comments as $comment)
-            @include('comments.comment', ['comment' => $comment])
+        @foreach($commentTree->get() as $branch)
+            @include('comments.comment-branch', ['branch' => $branch])
         @endforeach
     </div>
 
     @if(userCan('comment-create-all'))
         @include('comments.create')
 
-        @if (count($page->comments) > 0)
+        @if (!$commentTree->empty())
             <div refs="page-comments@addButtonContainer" class="text-right">
-                <button type="button" action="addComment"
+                <button type="button"
+                        refs="page-comments@add-comment-button"
                         class="button outline">{{ trans('entities.comment_add') }}</button>
             </div>
         @endif
index a5a84b004dc8e20a2fbf514487f5bfa13b548e3d..cb7905ddc44519c46992dee4db31e474af1e557c 100644 (file)
@@ -1,32 +1,32 @@
-<div class="comment-box" style="display:none;">
+<div refs="page-comments@form-container" hidden class="comment-branch mb-m">
+    <div class="comment-box">
 
-    <div class="header p-s">{{ trans('entities.comment_new') }}</div>
-    <div refs="page-comments@replyToRow" class="reply-row primary-background-light text-muted px-s py-xs mb-s" style="display: none;">
-        <div class="grid left-focus v-center">
-            <div>
-                {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!}
-            </div>
-            <div class="text-right">
-                <button class="text-button" action="remove-reply-to">{{ trans('common.remove') }}</button>
+        <div class="header p-s">{{ trans('entities.comment_new') }}</div>
+        <div refs="page-comments@reply-to-row" hidden class="primary-background-light text-muted px-s py-xs">
+            <div class="grid left-focus v-center">
+                <div>
+                    <a refs="page-comments@form-reply-link" href="#">{{ trans('entities.comment_in_reply_to', ['commentId' => '1234']) }}</a>
+                </div>
+                <div class="text-right">
+                    <button refs="page-comments@remove-reply-to-button" class="text-button">{{ trans('common.remove') }}</button>
+                </div>
             </div>
         </div>
-    </div>
 
-    <div refs="page-comments@formContainer" class="content px-s">
-        <form novalidate>
-            <div class="form-group description-input">
-                        <textarea name="markdown" rows="3"
-                                  placeholder="{{ trans('entities.comment_placeholder') }}"></textarea>
-            </div>
-            <div class="form-group text-right">
-                <button type="button" class="button outline"
-                        action="hideForm">{{ trans('common.cancel') }}</button>
-                <button type="submit" class="button">{{ trans('entities.comment_save') }}</button>
-            </div>
-            <div class="form-group loading" style="display: none;">
-                @include('common.loading-icon', ['text' => trans('entities.comment_saving')])
-            </div>
-        </form>
-    </div>
+        <div class="content px-s pt-s">
+            <form refs="page-comments@form" novalidate>
+                <div class="form-group description-input">
+                <textarea refs="page-comments@form-input" name="markdown"
+                          rows="3"
+                          placeholder="{{ trans('entities.comment_placeholder') }}"></textarea>
+                </div>
+                <div class="form-group text-right">
+                    <button type="button" class="button outline"
+                            refs="page-comments@hide-form-button">{{ trans('common.cancel') }}</button>
+                    <button type="submit" class="button">{{ trans('entities.comment_save') }}</button>
+                </div>
+            </form>
+        </div>
 
+    </div>
 </div>
\ No newline at end of file
index c29e6de0e3ab22eb5f178cea08a9e8bc660c731c..3b438de7c35e6516d82fda08a14c37a89bca0e6c 100644 (file)
                             </a>
                         </li>
                     @endif
-                    <li refs="page-editor@discardDraftWrap" class="{{ $isDraftRevision ? '' : 'hidden' }}">
-                        <button refs="page-editor@discardDraft" type="button" class="text-neg icon-item">
+                    <li refs="page-editor@discard-draft-wrap" {{ $isDraftRevision ? '' : 'hidden' }}>
+                        <button refs="page-editor@discard-draft" type="button" class="text-warn icon-item">
                             @icon('cancel')
                             <div>{{ trans('entities.pages_edit_discard_draft') }}</div>
                         </button>
                     </li>
+                    <li refs="page-editor@delete-draft-wrap" {{ $isDraftRevision ? '' : 'hidden' }}>
+                        <button refs="page-editor@delete-draft" type="button" class="text-neg icon-item">
+                            @icon('delete')
+                            <div>{{ trans('entities.pages_edit_delete_draft') }}</div>
+                        </button>
+                    </li>
                     @if(userCan('editor-change'))
+                        <li>
+                            <hr>
+                        </li>
                         <li>
                             @if($editor === 'wysiwyg')
                                 <a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-clean" refs="page-editor@changeEditor" class="icon-item">
index a3a118527ebbd0352465f31b7ce1e3e919250d5a..4ed55044bc55a866a82dfd0013bd2a05e3c947fe 100644 (file)
@@ -13,6 +13,8 @@
      option:page-editor:autosave-fail-text="{{ trans('errors.page_draft_autosave_fail') }}"
      option:page-editor:editing-page-text="{{ trans('entities.pages_editing_page') }}"
      option:page-editor:draft-discarded-text="{{ trans('entities.pages_draft_discarded') }}"
+     option:page-editor:draft-delete-text="{{ trans('entities.pages_draft_deleted') }}"
+     option:page-editor:draft-delete-fail-text="{{ trans('errors.page_draft_delete_fail') }}"
      option:page-editor:set-changelog-text="{{ trans('entities.pages_edit_set_changelog') }}">
 
     {{--Header Toolbar--}}
@@ -47,7 +49,7 @@
             class="text-link text-button hide-over-m page-save-mobile-button">@icon('save')</button>
 
     {{--Editor Change Dialog--}}
-    @component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switchDialog'])
+    @component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switch-dialog'])
         <p>
             {{ trans('entities.pages_editor_switch_are_you_sure') }}
             <br>
             <li>{{ trans('entities.pages_editor_switch_consideration_c') }}</li>
         </ul>
     @endcomponent
+
+    {{--Delete Draft Dialog--}}
+    @component('common.confirm-dialog', ['title' => trans('entities.pages_edit_delete_draft'), 'ref' => 'page-editor@delete-draft-dialog'])
+        <p>
+            {{ trans('entities.pages_edit_delete_draft_confirm') }}
+        </p>
+    @endcomponent
 </div>
\ No newline at end of file
index 2cbc7fe470bd807517a472e385ba1d66c0ce2dae..ae6b273fe4fe1d208a2d0eade98e0933d67f7392 100644 (file)
 
     @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous])
 
-    @if ($commentsEnabled)
+    @if ($commentTree->enabled())
         @if(($previous || $next))
             <div class="px-xl">
                 <hr class="darker">
             </div>
         @endif
 
-        <div class="px-xl comments-container mb-l print-hidden">
-            @include('comments.comments', ['page' => $page])
+        <div class="comments-container mb-l print-hidden">
+            @include('comments.comments', ['commentTree' => $commentTree, 'page' => $page])
             <div class="clearfix"></div>
         </div>
     @endif
index 468c300ba190eeafdfef7fdd36a06ea3ca6f9876..74ee74a2c77c39c009cf311b8a5bcd77f5b0a0fe 100644 (file)
@@ -106,6 +106,7 @@ Route::middleware('auth')->group(function () {
     Route::get('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', [EntityControllers\PageRevisionController::class, 'changes']);
     Route::put('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', [EntityControllers\PageRevisionController::class, 'restore']);
     Route::delete('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', [EntityControllers\PageRevisionController::class, 'destroy']);
+    Route::delete('/page-revisions/user-drafts/{pageId}', [EntityControllers\PageRevisionController::class, 'destroyUserDraft']);
 
     // Chapters
     Route::get('/books/{bookSlug}/chapter/{chapterSlug}/create-page', [EntityControllers\PageController::class, 'create']);
index 067173a6b7d8260a276bf2156fea10bd4bcc8d7c..6670931074e83e46e5a8e56cf4b6a5dd98d32536 100644 (file)
@@ -232,6 +232,8 @@ class ImageGalleryApiTest extends TestCase
                 'html' => "<a href=\"{$image->url}\" target=\"_blank\"><img src=\"{$displayUrl}\" alt=\"{$image->name}\"></a>",
                 'markdown' => "![{$image->name}]({$displayUrl})",
             ],
+            'created_at' => $image->created_at->toISOString(),
+            'updated_at' => $image->updated_at->toISOString(),
         ]);
         $this->assertStringStartsWith('http://', $resp->json('thumbs.gallery'));
         $this->assertStringStartsWith('http://', $resp->json('thumbs.display'));
index 1159df79d187ffcaddda330a391f9c58bc3d9d35..a04933ada6d653c61027cb3768d729048a5695ab 100644 (file)
@@ -114,4 +114,25 @@ class CommentTest extends TestCase
         $pageView->assertDontSee($script, false);
         $pageView->assertSee('sometextinthecommentupdated');
     }
+
+    public function test_reply_comments_are_nested()
+    {
+        $this->asAdmin();
+        $page = $this->entities->page();
+
+        $this->postJson("/comment/$page->id", ['text' => 'My new comment']);
+        $this->postJson("/comment/$page->id", ['text' => 'My new comment']);
+
+        $respHtml = $this->withHtml($this->get($page->getUrl()));
+        $respHtml->assertElementCount('.comment-branch', 3);
+        $respHtml->assertElementNotExists('.comment-branch .comment-branch');
+
+        $comment = $page->comments()->first();
+        $resp = $this->postJson("/comment/$page->id", ['text' => 'My nested comment', 'parent_id' => $comment->local_id]);
+        $resp->assertStatus(200);
+
+        $respHtml = $this->withHtml($this->get($page->getUrl()));
+        $respHtml->assertElementCount('.comment-branch', 4);
+        $respHtml->assertElementContains('.comment-branch .comment-branch', 'My nested comment');
+    }
 }
index af06ae72af9042d2e9025b3d11ff8cd78f4c6d1e..a070ce3fa889994686b5fa8e83b57f4fc3f7499f 100644 (file)
@@ -5,6 +5,7 @@ namespace Tests\Entity;
 use BookStack\Activity\Models\Tag;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
 use Tests\TestCase;
 
 class EntitySearchTest extends TestCase
@@ -225,6 +226,17 @@ class EntitySearchTest extends TestCase
         $chapterSearch->assertSee($chapter->book->getShortName(42));
     }
 
+    public function test_entity_selector_shows_breadcrumbs_on_default_view()
+    {
+        $page = $this->entities->pageWithinChapter();
+        $this->asEditor()->get($page->chapter->getUrl());
+
+        $resp = $this->asEditor()->get('/search/entity-selector?types=book,chapter&permission=page-create');
+        $html = $this->withHtml($resp);
+        $html->assertElementContains('.chapter.entity-list-item', $page->chapter->name);
+        $html->assertElementContains('.chapter.entity-list-item .entity-item-snippet', $page->book->getShortName(42));
+    }
+
     public function test_entity_selector_search_reflects_items_without_permission()
     {
         $page = $this->entities->page();
index 75b1933ea0e10d46fcd2e648081183d13b8abebf..e99ba9b8189423f85922e0fe066550f25e467ee5 100644 (file)
@@ -166,6 +166,30 @@ class PageDraftTest extends TestCase
         ]);
     }
 
+    public function test_user_draft_removed_on_user_drafts_delete_call()
+    {
+        $editor = $this->users->editor();
+        $page = $this->entities->page();
+
+        $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
+            'name' => $page->name,
+            'html' => '<p>updated draft again</p>',
+        ]);
+
+        $revisionData = [
+            'type' => 'update_draft',
+            'created_by' => $editor->id,
+            'page_id' => $page->id,
+        ];
+
+        $this->assertDatabaseHas('page_revisions', $revisionData);
+
+        $resp = $this->delete("/page-revisions/user-drafts/{$page->id}");
+
+        $resp->assertOk();
+        $this->assertDatabaseMissing('page_revisions', $revisionData);
+    }
+
     public function test_updating_page_draft_with_markdown_retains_markdown_content()
     {
         $book = $this->entities->book();
index b65227dd826838472ea4ef412f2d42b74b2c7876..a66227ff2e8181f2f560fe3e5e14580d2d4564cf 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Tests;
 
+use BookStack\Activity\ActivityType;
+
 class LanguageTest extends TestCase
 {
     protected array $langs;
@@ -90,4 +92,12 @@ class LanguageTest extends TestCase
         $loginReq->assertOk();
         $loginReq->assertSee('Log In');
     }
+
+    public function test_all_activity_types_have_activity_text()
+    {
+        foreach (ActivityType::all() as $activityType) {
+            $langKey = 'activities.' . $activityType;
+            $this->assertNotEquals($langKey, trans($langKey, [], 'en'));
+        }
+    }
 }
index dafa1f2bb10de8ae8031815e8f87498c6f6404db..4c8ea1ab48b9b67fa666914946d2c0fb7ca65598 100644 (file)
@@ -301,7 +301,7 @@ class RolesTest extends TestCase
         $resp = $this->post('/settings/features', []);
         $resp->assertRedirect('/settings/features');
         $resp = $this->get('/settings/features');
-        $resp->assertSee('Settings saved');
+        $resp->assertSee('Settings successfully updated');
     }
 
     public function test_restrictions_manage_all_permission()
index 97299f7570b5c21fb5149d5edca59acb8df50ac0..6f0e2f1d3bb4d886684af5e7ee3863926a552460 100644 (file)
@@ -193,4 +193,18 @@ class PublicActionTest extends TestCase
         $resp->assertRedirect($book->getUrl());
         $this->followRedirects($resp)->assertSee($book->name);
     }
+
+    public function test_public_view_can_take_on_other_roles()
+    {
+        $this->setSettings(['app-public' => 'true']);
+        $newRole = $this->users->attachNewRole(User::getDefault(), []);
+        $page = $this->entities->page();
+        $this->permissions->disableEntityInheritedPermissions($page);
+        $this->permissions->addEntityPermission($page, ['view', 'update'], $newRole);
+
+        $resp = $this->get($page->getUrl());
+        $resp->assertOk();
+
+        $this->withHtml($resp)->assertLinkExists($page->getUrl('/edit'));
+    }
 }