From: Dan Brown Date: Thu, 15 Jun 2023 16:08:51 +0000 (+0100) Subject: Merge branch 'fix-api-404' into development X-Git-Tag: v23.06~1^2~20 X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/ec775aec02c0887d5cf2dc23c938a75b7eaf67d2?hp=e72cf61f7eb2245b2af59c0862e39ad55bb7a459 Merge branch 'fix-api-404' into development --- diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index f16767fcf..2aabab79d 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -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; } } diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index b198d2d56..9e7491fd7 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -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 index 000000000..3303add39 --- /dev/null +++ b/app/Activity/Tools/CommentTree.php @@ -0,0 +1,102 @@ +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(); + } +} diff --git a/app/Api/UserApiTokenController.php b/app/Api/UserApiTokenController.php index d8fc1171c..8357420ee 100644 --- a/app/Api/UserApiTokenController.php +++ b/app/Api/UserApiTokenController.php @@ -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')); diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php index cf7611685..7dcb66903 100644 --- a/app/Entities/Controllers/ChapterController.php +++ b/app/Entities/Controllers/ChapterController.php @@ -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()); } diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index a6ef68dd7..3187e6486 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -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()); } diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index a723513a8..a3190a0fc 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -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); + } } diff --git a/app/Entities/Controllers/RecycleBinController.php b/app/Entities/Controllers/RecycleBinController.php index 30b184bbe..78f86a5ae 100644 --- a/app/Entities/Controllers/RecycleBinController.php +++ b/app/Entities/Controllers/RecycleBinController.php @@ -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 diff --git a/app/Entities/Queries/Popular.php b/app/Entities/Queries/Popular.php index 9b7049ae3..a934f346b 100644 --- a/app/Entities/Queries/Popular.php +++ b/app/Entities/Queries/Popular.php @@ -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'); + }]); } } diff --git a/app/Permissions/PermissionApplicator.php b/app/Permissions/PermissionApplicator.php index 8a02f82e8..b4fafaa9e 100644 --- a/app/Permissions/PermissionApplicator.php +++ b/app/Permissions/PermissionApplicator.php @@ -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(); } diff --git a/app/Search/SearchController.php b/app/Search/SearchController.php index 4b134f11e..09a67f2b5 100644 --- a/app/Search/SearchController.php +++ b/app/Search/SearchController.php @@ -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 + ) { } /** diff --git a/app/Settings/SettingController.php b/app/Settings/SettingController.php index ffdd7545e..bd55222f2 100644 --- a/app/Settings/SettingController.php +++ b/app/Settings/SettingController.php @@ -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}"); } diff --git a/app/Uploads/Controllers/ImageGalleryApiController.php b/app/Uploads/Controllers/ImageGalleryApiController.php index 4fca6a4dd..c444ec663 100644 --- a/app/Uploads/Controllers/ImageGalleryApiController.php +++ b/app/Uploads/Controllers/ImageGalleryApiController.php @@ -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'] = []; diff --git a/app/Users/Controllers/UserController.php b/app/Users/Controllers/UserController.php index b185f0856..1c1b7ba23 100644 --- a/app/Users/Controllers/UserController.php +++ b/app/Users/Controllers/UserController.php @@ -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 index 000000000..8d04efdd9 --- /dev/null +++ b/database/migrations/2023_06_10_071823_remove_guest_user_secondary_roles.php @@ -0,0 +1,46 @@ +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. + } +}; diff --git a/dev/api/responses/image-gallery-create.json b/dev/api/responses/image-gallery-create.json index e27824491..0bf36d368 100644 --- a/dev/api/responses/image-gallery-create.json +++ b/dev/api/responses/image-gallery-create.json @@ -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", diff --git a/dev/api/responses/image-gallery-read.json b/dev/api/responses/image-gallery-read.json index c6c468daa..e3d5a92ca 100644 --- a/dev/api/responses/image-gallery-read.json +++ b/dev/api/responses/image-gallery-read.json @@ -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", diff --git a/dev/api/responses/image-gallery-update.json b/dev/api/responses/image-gallery-update.json index 6e6168a1b..e72961918 100644 --- a/dev/api/responses/image-gallery-update.json +++ b/dev/api/responses/image-gallery-update.json @@ -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", diff --git a/lang/en/activities.php b/lang/en/activities.php index e89b8eab2..e71a490de 100644 --- a/lang/en/activities.php +++ b/lang/en/activities.php @@ -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', diff --git a/lang/en/entities.php b/lang/en/entities.php index 501fc9f2a..5a148e1a2 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -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 diff --git a/lang/en/errors.php b/lang/en/errors.php index b03fb8c35..23c326f9e 100644 --- a/lang/en/errors.php +++ b/lang/en/errors.php @@ -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 diff --git a/lang/en/settings.php b/lang/en/settings.php index 38d817915..c110e8992 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -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', diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 803714e62..a56f18a5a 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -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 index 000000000..8284d7f20 --- /dev/null +++ b/resources/js/components/page-comment.js @@ -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; + } + +} diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index 0ac9d0572..a46a5c3b3 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -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(); } } diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js index e7f4c0ba9..963c21008 100644 --- a/resources/js/components/page-editor.js +++ b/resources/js/components/page-editor.js @@ -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(); diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index 514bff87d..f66b7921d 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -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(); } diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss index 1d9bfc272..54c509ef9 100644 --- a/resources/sass/_blocks.scss +++ b/resources/sass/_blocks.scss @@ -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 */ diff --git a/resources/sass/_codemirror.scss b/resources/sass/_codemirror.scss index 0fd347cf8..c4b0e2e89 100644 --- a/resources/sass/_codemirror.scss +++ b/resources/sass/_codemirror.scss @@ -14,6 +14,10 @@ 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 // 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 { diff --git a/resources/sass/_colors.scss b/resources/sass/_colors.scss index aff9ff6d0..c77c1d8b3 100644 --- a/resources/sass/_colors.scss +++ b/resources/sass/_colors.scss @@ -22,18 +22,18 @@ * 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; } /* diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 1521e6eaa..dab74341a 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -46,13 +46,13 @@ } } &.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 index 000000000..10a2cd983 --- /dev/null +++ b/resources/sass/_content.scss @@ -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 diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 5276bb566..4722d9aa1 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -13,10 +13,10 @@ 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==); diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index 2a77e84ba..fbac1de07 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -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; diff --git a/resources/sass/_text.scss b/resources/sass/_text.scss index adfc87ad1..f2c88d96d 100644 --- a/resources/sass/_text.scss +++ b/resources/sass/_text.scss @@ -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 { diff --git a/resources/sass/_tinymce.scss b/resources/sass/_tinymce.scss index 7170f8101..13b6f676b 100644 --- a/resources/sass/_tinymce.scss +++ b/resources/sass/_tinymce.scss @@ -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; diff --git a/resources/sass/_variables.scss b/resources/sass/_variables.scss index aac9223f9..5892237d9 100644 --- a/resources/sass/_variables.scss +++ b/resources/sass/_variables.scss @@ -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 diff --git a/resources/sass/export-styles.scss b/resources/sass/export-styles.scss index 1e39bd056..cfa1ebdf8 100644 --- a/resources/sass/export-styles.scss +++ b/resources/sass/export-styles.scss @@ -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; diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 9a8e5b36d..c0ce7ba63 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -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 index 000000000..78d19ac3e --- /dev/null +++ b/resources/views/comments/comment-branch.blade.php @@ -0,0 +1,15 @@ +
+
+ @include('comments.comment', ['comment' => $branch['comment']]) +
+
+
+
+
+
+ @foreach($branch['children'] as $childBranch) + @include('comments.comment-branch', ['branch' => $childBranch]) + @endforeach +
+
+
\ No newline at end of file diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 6189c65d4..04468b83c 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -1,78 +1,82 @@ -
+
-
-
- #{{$comment->local_id}} -    +
+
@if ($comment->createdBy) - {{ $comment->createdBy->name }} + {{ $comment->createdBy->name }}   - {{ $comment->createdBy->name }} + {{ $comment->createdBy->getShortName(16) }} @else - {{ trans('common.deleted_user') }} + {{ trans('common.deleted_user') }} @endif - {{ trans('entities.comment_created', ['createDiff' => $comment->created]) }} +  {{ trans('entities.comment_created', ['createDiff' => $comment->created]) }} @if($comment->isUpdated()) - - •  - {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy? $comment->updatedBy->name : trans('common.deleted_user')]) }} - + + + {{ trans('entities.comment_updated_indicator') }} + @endif
-
- @if(userCan('comment-update', $comment)) - - @endif - @if(userCan('comment-create-all')) - - @endif - @if(userCan('comment-delete', $comment)) - - @endif +
+
+ @if(userCan('comment-create-all')) + + @endif + @if(userCan('comment-update', $comment)) + + @endif + @if(userCan('comment-delete', $comment)) + + @endif + +  •  + +
+
- @if ($comment->parent_id) -
- {!! trans('entities.comment_in_reply_to', ['commentId' => '#'.$comment->parent_id.'']) !!} -
- @endif - -
- +
+ @if ($comment->parent_id) +

+ @icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }} +

+ @endif {!! $comment->html !!}
@if(userCan('comment-update', $comment)) - + @endif
\ No newline at end of file diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index 140d0d027..b79f0fd45 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -1,34 +1,34 @@
-
-
{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}
- @if (count($page->comments) === 0 && userCan('comment-create-all')) -
-
@endif
- @foreach($page->comments as $comment) - @include('comments.comment', ['comment' => $comment]) + @foreach($commentTree->get() as $branch) + @include('comments.comment-branch', ['branch' => $branch]) @endforeach
@if(userCan('comment-create-all')) @include('comments.create') - @if (count($page->comments) > 0) + @if (!$commentTree->empty())
-
@endif diff --git a/resources/views/comments/create.blade.php b/resources/views/comments/create.blade.php index a5a84b004..cb7905ddc 100644 --- a/resources/views/comments/create.blade.php +++ b/resources/views/comments/create.blade.php @@ -1,32 +1,32 @@ -