From 4dce03c0d3747788bd716fcedb7564bfedce7598 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 2 Apr 2022 17:14:37 +0100 Subject: [PATCH 01/41] Updated custom request overrides to better match original intent This updates the custom Request handler to provide only the scheme and host on the `getSchemeAndHttpHost` call, instead of providing the whole APP_URL value, while adding an override to the 'getBaseUrl' to use the APP_URL content instead of the guessed/detected Symfony value. Untested apart from simple local setup. Related to #2765 --- app/Http/Request.php | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/app/Http/Request.php b/app/Http/Request.php index c5b38f1c1..687bae9a6 100644 --- a/app/Http/Request.php +++ b/app/Http/Request.php @@ -8,20 +8,35 @@ class Request extends LaravelRequest { /** * Override the default request methods to get the scheme and host - * to set the custom APP_URL, if set. + * to directly use the custom APP_URL, if set. * - * @return \Illuminate\Config\Repository|mixed|string + * @return string */ public function getSchemeAndHttpHost() { - $base = config('app.url', null); + $appUrl = config('app.url', null); - if ($base) { - $base = trim($base, '/'); - } else { - $base = $this->getScheme() . '://' . $this->getHttpHost(); + if ($appUrl) { + return implode('/', array_slice(explode('/', $appUrl), 0, 3)); } - return $base; + return parent::getSchemeAndHttpHost(); + } + + /** + * Override the default request methods to get the base URL + * to directly use the custom APP_URL, if set. + * + * @return string + */ + public function getBaseUrl() + { + $appUrl = config('app.url', null); + + if ($appUrl) { + return rtrim(implode('/', array_slice(explode('/', $appUrl), 3)), '/'); + } + + return parent::getBaseUrl(); } } From 82e8b1577ec0c7b136da5eed9c89d4790714814c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 2 Apr 2022 18:07:43 +0100 Subject: [PATCH 02/41] Updated attachment download responses to stream from filesystem This allows download of attachments that are larger than current memory limits, since we're not loading the entire file into memory any more. For inline file responses, we take a 1kb portion of the file to sniff before to check mime before we proceed. --- app/Http/Controllers/AttachmentController.php | 11 +++--- app/Http/Controllers/Controller.php | 37 +++++++++++++++++++ app/Uploads/AttachmentService.php | 14 ++++++- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index 084f6f96a..7f5ffc8cb 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -10,13 +10,14 @@ use BookStack\Uploads\AttachmentService; use Exception; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\MessageBag; use Illuminate\Validation\ValidationException; class AttachmentController extends Controller { - protected $attachmentService; - protected $pageRepo; + protected AttachmentService $attachmentService; + protected PageRepo $pageRepo; /** * AttachmentController constructor. @@ -230,13 +231,13 @@ class AttachmentController extends Controller } $fileName = $attachment->getFileName(); - $attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment); + $attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment); if ($request->get('open') === 'true') { - return $this->inlineDownloadResponse($attachmentContents, $fileName); + return $this->streamedInlineDownloadResponse($attachmentStream, $fileName); } - return $this->downloadResponse($attachmentContents, $fileName); + return $this->streamedDownloadResponse($attachmentStream, $fileName); } /** diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index d616974c6..ae1f4e4ba 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -12,6 +12,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Illuminate\Routing\Controller as BaseController; +use Symfony\Component\HttpFoundation\StreamedResponse; abstract class Controller extends BaseController { @@ -120,6 +121,21 @@ abstract class Controller extends BaseController ]); } + /** + * Create a response that forces a download, from a given stream of content. + */ + protected function streamedDownloadResponse($stream, string $fileName): StreamedResponse + { + return response()->stream(function() use ($stream) { + fpassthru($stream); + fclose($stream); + }, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="' . $fileName . '"', + 'X-Content-Type-Options' => 'nosniff', + ]); + } + /** * Create a file download response that provides the file with a content-type * correct for the file, in a way so the browser can show the content in browser. @@ -135,6 +151,27 @@ abstract class Controller extends BaseController ]); } + /** + * Create a file download response that provides the file with a content-type + * correct for the file, in a way so the browser can show the content in browser, + * for a given content stream. + */ + protected function streamedInlineDownloadResponse($stream, string $fileName): StreamedResponse + { + $sniffContent = fread($stream, 1000); + $mime = (new WebSafeMimeSniffer())->sniff($sniffContent); + + return response()->stream(function() use ($sniffContent, $stream) { + echo $sniffContent; + fpassthru($stream); + fclose($stream); + }, 200, [ + 'Content-Type' => $mime, + 'Content-Disposition' => 'inline; filename="' . $fileName . '"', + 'X-Content-Type-Options' => 'nosniff', + ]); + } + /** * Show a positive, successful notification to the user on next view load. */ diff --git a/app/Uploads/AttachmentService.php b/app/Uploads/AttachmentService.php index 7974d7ae9..05e70a502 100644 --- a/app/Uploads/AttachmentService.php +++ b/app/Uploads/AttachmentService.php @@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; class AttachmentService { - protected $fileSystem; + protected FilesystemManager $fileSystem; /** * AttachmentService constructor. @@ -73,6 +73,18 @@ class AttachmentService return $this->getStorageDisk()->get($this->adjustPathForStorageDisk($attachment->path)); } + /** + * Stream an attachment from storage. + * + * @return resource|null + * @throws FileNotFoundException + */ + public function streamAttachmentFromStorage(Attachment $attachment) + { + + return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($attachment->path)); + } + /** * Store a new attachment upon user upload. * From 6749faa89a9faa6f21ba173c1736f6bbba72a8f8 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 2 Apr 2022 18:42:15 +0100 Subject: [PATCH 03/41] Fixed streamed outputs in more extreme scenarios Fixes hitting memory limits where downloaded file sizes are much greater than memory limit. Stopping and flushing output buffer seemed to stop limits causing issues when fpassthru is used. Tested with 24M memory limit and 734M file --- app/Http/Controllers/Controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index ae1f4e4ba..4c979c26a 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -127,6 +127,7 @@ abstract class Controller extends BaseController protected function streamedDownloadResponse($stream, string $fileName): StreamedResponse { return response()->stream(function() use ($stream) { + ob_end_clean(); fpassthru($stream); fclose($stream); }, 200, [ From cb770c534d4d1ac776fb78126fee7b46f57a2f19 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 2 Apr 2022 18:46:48 +0100 Subject: [PATCH 04/41] Added streamed uploads for attachments --- app/Http/Controllers/AttachmentController.php | 1 - app/Uploads/AttachmentService.php | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index 7f5ffc8cb..0a092b63a 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -10,7 +10,6 @@ use BookStack\Uploads\AttachmentService; use Exception; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\MessageBag; use Illuminate\Validation\ValidationException; diff --git a/app/Uploads/AttachmentService.php b/app/Uploads/AttachmentService.php index 05e70a502..ec02182bb 100644 --- a/app/Uploads/AttachmentService.php +++ b/app/Uploads/AttachmentService.php @@ -223,8 +223,6 @@ class AttachmentService */ protected function putFileInStorage(UploadedFile $uploadedFile): string { - $attachmentData = file_get_contents($uploadedFile->getRealPath()); - $storage = $this->getStorageDisk(); $basePath = 'uploads/files/' . date('Y-m-M') . '/'; @@ -233,10 +231,11 @@ class AttachmentService $uploadFileName = Str::random(3) . $uploadFileName; } + $attachmentStream = fopen($uploadedFile->getRealPath(), 'r'); $attachmentPath = $basePath . $uploadFileName; try { - $storage->put($this->adjustPathForStorageDisk($attachmentPath), $attachmentData); + $storage->writeStream($this->adjustPathForStorageDisk($attachmentPath), $attachmentStream); } catch (Exception $e) { Log::error('Error when attempting file upload:' . $e->getMessage()); From 08a8c0070e3f1574ceecd0ea120c26bbf52eb0f4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 2 Apr 2022 19:20:59 +0100 Subject: [PATCH 05/41] Added streaming support to API attachment read responses Required some special handling due to the content being base64-encoded within a JSON response. --- .../Api/AttachmentApiController.php | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/AttachmentApiController.php b/app/Http/Controllers/Api/AttachmentApiController.php index fc5008e3c..2476cb951 100644 --- a/app/Http/Controllers/Api/AttachmentApiController.php +++ b/app/Http/Controllers/Api/AttachmentApiController.php @@ -87,14 +87,32 @@ class AttachmentApiController extends ApiController 'markdown' => $attachment->markdownLink(), ]); - if (!$attachment->external) { - $attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment); - $attachment->setAttribute('content', base64_encode($attachmentContents)); - } else { + // Simply return a JSON response of the attachment for link-based attachments + if ($attachment->external) { $attachment->setAttribute('content', $attachment->path); + return response()->json($attachment); } - return response()->json($attachment); + // Build and split our core JSON, at point of content. + $splitter = 'CONTENT_SPLIT_LOCATION_' . time() . '_' . rand(1, 40000); + $attachment->setAttribute('content', $splitter); + $json = $attachment->toJson(); + $jsonParts = explode($splitter, $json); + // Get a stream for the file data from storage + $stream = $this->attachmentService->streamAttachmentFromStorage($attachment); + + return response()->stream(function () use ($jsonParts, $stream) { + // Output the pre-content JSON data + echo $jsonParts[0]; + + // Stream out our attachment data as base64 content + stream_filter_append($stream, 'convert.base64-encode', STREAM_FILTER_READ); + fpassthru($stream); + fclose($stream); + + // Output our post-content JSON data + echo $jsonParts[1]; + }, 200, ['Content-Type' => 'application/json']); } /** From 59d1fb2d1033616ac7edc143f4876485bb997c6a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 3 Apr 2022 16:22:31 +0100 Subject: [PATCH 06/41] Fixed tests from streaming changes - Added testing check to buffer stop/clear on streaming output due to interference during tests. - Made content-disposition header a little safer in download responses. - Also aligned how we check for testing environment. --- app/Http/Controllers/Controller.php | 15 ++++++++++----- resources/views/common/export-styles.blade.php | 2 +- tests/Api/AttachmentsApiTest.php | 7 +++++-- tests/Uploads/AttachmentTest.php | 3 ++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 4c979c26a..6ca2239cc 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -116,7 +116,7 @@ abstract class Controller extends BaseController { return response()->make($content, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $fileName . '"', + 'Content-Disposition' => 'attachment; filename="' . str_replace('"', '', $fileName) . '"', 'X-Content-Type-Options' => 'nosniff', ]); } @@ -127,12 +127,17 @@ abstract class Controller extends BaseController protected function streamedDownloadResponse($stream, string $fileName): StreamedResponse { return response()->stream(function() use ($stream) { - ob_end_clean(); + // End & flush the output buffer otherwise we still seem to use memory. + // Ignore in testing since output buffers are used to gather a response. + if (!app()->runningUnitTests()) { + ob_end_clean(); + } + fpassthru($stream); fclose($stream); }, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $fileName . '"', + 'Content-Disposition' => 'attachment; filename="' . str_replace('"', '', $fileName) . '"', 'X-Content-Type-Options' => 'nosniff', ]); } @@ -147,7 +152,7 @@ abstract class Controller extends BaseController return response()->make($content, 200, [ 'Content-Type' => $mime, - 'Content-Disposition' => 'inline; filename="' . $fileName . '"', + 'Content-Disposition' => 'inline; filename="' . str_replace('"', '', $fileName) . '"', 'X-Content-Type-Options' => 'nosniff', ]); } @@ -168,7 +173,7 @@ abstract class Controller extends BaseController fclose($stream); }, 200, [ 'Content-Type' => $mime, - 'Content-Disposition' => 'inline; filename="' . $fileName . '"', + 'Content-Disposition' => 'inline; filename="' . str_replace('"', '', $fileName) . '"', 'X-Content-Type-Options' => 'nosniff', ]); } diff --git a/resources/views/common/export-styles.blade.php b/resources/views/common/export-styles.blade.php index ee10637dd..1dfa6bb45 100644 --- a/resources/views/common/export-styles.blade.php +++ b/resources/views/common/export-styles.blade.php @@ -1,5 +1,5 @@ diff --git a/tests/Api/AttachmentsApiTest.php b/tests/Api/AttachmentsApiTest.php index d7625c938..a34337016 100644 --- a/tests/Api/AttachmentsApiTest.php +++ b/tests/Api/AttachmentsApiTest.php @@ -5,6 +5,7 @@ namespace Tests\Api; use BookStack\Entities\Models\Page; use BookStack\Uploads\Attachment; use Illuminate\Http\UploadedFile; +use Illuminate\Testing\AssertableJsonString; use Tests\TestCase; class AttachmentsApiTest extends TestCase @@ -228,9 +229,11 @@ class AttachmentsApiTest extends TestCase $attachment = Attachment::query()->orderByDesc('id')->where('name', '=', $details['name'])->firstOrFail(); $resp = $this->getJson("{$this->baseEndpoint}/{$attachment->id}"); - $resp->assertStatus(200); - $resp->assertJson([ + $resp->assertHeader('Content-Type', 'application/json'); + + $json = new AssertableJsonString($resp->streamedContent()); + $json->assertSubset([ 'id' => $attachment->id, 'content' => base64_encode(file_get_contents(storage_path($attachment->path))), 'external' => false, diff --git a/tests/Uploads/AttachmentTest.php b/tests/Uploads/AttachmentTest.php index 5545edf13..27a23bcae 100644 --- a/tests/Uploads/AttachmentTest.php +++ b/tests/Uploads/AttachmentTest.php @@ -128,7 +128,8 @@ class AttachmentTest extends TestCase $pageGet->assertSee($attachment->getUrl()); $attachmentGet = $this->get($attachment->getUrl()); - $attachmentGet->assertSee('Hi, This is a test file for testing the upload process.'); + $content = $attachmentGet->streamedContent(); + $this->assertStringContainsString('Hi, This is a test file for testing the upload process.', $content); $this->deleteUploads(); } From c30a9d35643da526f5ea6299d053b6f0dea3bcb1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 4 Apr 2022 17:24:05 +0100 Subject: [PATCH 07/41] Touched entity timestamps on entity tag update Decided it's relevant to entity updated_at since tags are now indexed alongside content. - Also fixed tags not applied on shelf. - Also enforced proper page API update validation. - Adds tests to cover. For #3319 Fixes #3370 --- app/Entities/Repos/BaseRepo.php | 5 +++-- .../Api/BookshelfApiController.php | 7 +++---- .../Controllers/Api/PageApiController.php | 10 ++++++---- tests/Api/BooksApiTest.php | 19 ++++++++++++++++++- tests/Api/ChaptersApiTest.php | 19 ++++++++++++++++++- tests/Api/PagesApiTest.php | 19 ++++++++++++++++++- tests/Api/ShelvesApiTest.php | 19 ++++++++++++++++++- 7 files changed, 84 insertions(+), 14 deletions(-) diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index 6b29dad7b..9e1b41672 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -11,8 +11,8 @@ use Illuminate\Http\UploadedFile; class BaseRepo { - protected $tagRepo; - protected $imageRepo; + protected TagRepo $tagRepo; + protected ImageRepo $imageRepo; public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo) { @@ -58,6 +58,7 @@ class BaseRepo if (isset($input['tags'])) { $this->tagRepo->saveTagsToEntity($entity, $input['tags']); + $entity->touch(); } $entity->rebuildPermissions(); diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php index bd4f23a10..63275a72a 100644 --- a/app/Http/Controllers/Api/BookshelfApiController.php +++ b/app/Http/Controllers/Api/BookshelfApiController.php @@ -11,21 +11,20 @@ use Illuminate\Validation\ValidationException; class BookshelfApiController extends ApiController { - /** - * @var BookshelfRepo - */ - protected $bookshelfRepo; + protected BookshelfRepo $bookshelfRepo; protected $rules = [ 'create' => [ 'name' => ['required', 'string', 'max:255'], 'description' => ['string', 'max:1000'], 'books' => ['array'], + 'tags' => ['array'], ], 'update' => [ 'name' => ['string', 'min:1', 'max:255'], 'description' => ['string', 'max:1000'], 'books' => ['array'], + 'tags' => ['array'], ], ]; diff --git a/app/Http/Controllers/Api/PageApiController.php b/app/Http/Controllers/Api/PageApiController.php index 6f3a71e02..9749985a5 100644 --- a/app/Http/Controllers/Api/PageApiController.php +++ b/app/Http/Controllers/Api/PageApiController.php @@ -12,7 +12,7 @@ use Illuminate\Http\Request; class PageApiController extends ApiController { - protected $pageRepo; + protected PageRepo $pageRepo; protected $rules = [ 'create' => [ @@ -24,8 +24,8 @@ class PageApiController extends ApiController 'tags' => ['array'], ], 'update' => [ - 'book_id' => ['required', 'integer'], - 'chapter_id' => ['required', 'integer'], + 'book_id' => ['integer'], + 'chapter_id' => ['integer'], 'name' => ['string', 'min:1', 'max:255'], 'html' => ['string'], 'markdown' => ['string'], @@ -103,6 +103,8 @@ class PageApiController extends ApiController */ public function update(Request $request, string $id) { + $requestData = $this->validate($request, $this->rules['update']); + $page = $this->pageRepo->getById($id, []); $this->checkOwnablePermission('page-update', $page); @@ -127,7 +129,7 @@ class PageApiController extends ApiController } } - $updatedPage = $this->pageRepo->update($page, $request->all()); + $updatedPage = $this->pageRepo->update($page, $requestData); return response()->json($updatedPage->forJsonDisplay()); } diff --git a/tests/Api/BooksApiTest.php b/tests/Api/BooksApiTest.php index 91e2db9e5..9625c9f2d 100644 --- a/tests/Api/BooksApiTest.php +++ b/tests/Api/BooksApiTest.php @@ -3,13 +3,15 @@ namespace Tests\Api; use BookStack\Entities\Models\Book; +use Carbon\Carbon; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class BooksApiTest extends TestCase { use TestsApi; - protected $baseEndpoint = '/api/books'; + protected string $baseEndpoint = '/api/books'; public function test_index_endpoint_returns_expected_book() { @@ -101,6 +103,21 @@ class BooksApiTest extends TestCase $this->assertActivityExists('book_update', $book); } + public function test_update_increments_updated_date_if_only_tags_are_sent() + { + $this->actingAsApiEditor(); + $book = Book::visible()->first(); + DB::table('books')->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]); + + $details = [ + 'tags' => [['name' => 'Category', 'value' => 'Testing']] + ]; + + $this->putJson($this->baseEndpoint . "/{$book->id}", $details); + $book->refresh(); + $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix()); + } + public function test_delete_endpoint() { $this->actingAsApiEditor(); diff --git a/tests/Api/ChaptersApiTest.php b/tests/Api/ChaptersApiTest.php index c9ed1a289..6f00f9ead 100644 --- a/tests/Api/ChaptersApiTest.php +++ b/tests/Api/ChaptersApiTest.php @@ -4,13 +4,15 @@ namespace Tests\Api; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; +use Carbon\Carbon; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class ChaptersApiTest extends TestCase { use TestsApi; - protected $baseEndpoint = '/api/chapters'; + protected string $baseEndpoint = '/api/chapters'; public function test_index_endpoint_returns_expected_chapter() { @@ -147,6 +149,21 @@ class ChaptersApiTest extends TestCase $this->assertActivityExists('chapter_update', $chapter); } + public function test_update_increments_updated_date_if_only_tags_are_sent() + { + $this->actingAsApiEditor(); + $chapter = Chapter::visible()->first(); + DB::table('chapters')->where('id', '=', $chapter->id)->update(['updated_at' => Carbon::now()->subWeek()]); + + $details = [ + 'tags' => [['name' => 'Category', 'value' => 'Testing']] + ]; + + $this->putJson($this->baseEndpoint . "/{$chapter->id}", $details); + $chapter->refresh(); + $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $chapter->updated_at->unix()); + } + public function test_delete_endpoint() { $this->actingAsApiEditor(); diff --git a/tests/Api/PagesApiTest.php b/tests/Api/PagesApiTest.php index 4eb109d9d..b91d96d89 100644 --- a/tests/Api/PagesApiTest.php +++ b/tests/Api/PagesApiTest.php @@ -5,13 +5,15 @@ namespace Tests\Api; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use Carbon\Carbon; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class PagesApiTest extends TestCase { use TestsApi; - protected $baseEndpoint = '/api/pages'; + protected string $baseEndpoint = '/api/pages'; public function test_index_endpoint_returns_expected_page() { @@ -240,6 +242,21 @@ class PagesApiTest extends TestCase $this->assertEquals($originalContent, $page->html); } + public function test_update_increments_updated_date_if_only_tags_are_sent() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + DB::table('pages')->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]); + + $details = [ + 'tags' => [['name' => 'Category', 'value' => 'Testing']] + ]; + + $this->putJson($this->baseEndpoint . "/{$page->id}", $details); + $page->refresh(); + $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix()); + } + public function test_delete_endpoint() { $this->actingAsApiEditor(); diff --git a/tests/Api/ShelvesApiTest.php b/tests/Api/ShelvesApiTest.php index 8868c686e..5953b0c0d 100644 --- a/tests/Api/ShelvesApiTest.php +++ b/tests/Api/ShelvesApiTest.php @@ -4,13 +4,15 @@ namespace Tests\Api; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; +use Carbon\Carbon; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class ShelvesApiTest extends TestCase { use TestsApi; - protected $baseEndpoint = '/api/shelves'; + protected string $baseEndpoint = '/api/shelves'; public function test_index_endpoint_returns_expected_shelf() { @@ -111,6 +113,21 @@ class ShelvesApiTest extends TestCase $this->assertActivityExists('bookshelf_update', $shelf); } + public function test_update_increments_updated_date_if_only_tags_are_sent() + { + $this->actingAsApiEditor(); + $shelf = Bookshelf::visible()->first(); + DB::table('bookshelves')->where('id', '=', $shelf->id)->update(['updated_at' => Carbon::now()->subWeek()]); + + $details = [ + 'tags' => [['name' => 'Category', 'value' => 'Testing']] + ]; + + $this->putJson($this->baseEndpoint . "/{$shelf->id}", $details); + $shelf->refresh(); + $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $shelf->updated_at->unix()); + } + public function test_update_only_assigns_books_if_param_provided() { $this->actingAsApiEditor(); From c979e6465eb791249e2132ba65455fea9f25a788 Mon Sep 17 00:00:00 2001 From: evandroamaro Date: Tue, 5 Apr 2022 10:53:52 +0100 Subject: [PATCH 08/41] Tiny header Had the same translation as the small header. Corrected the translation. --- resources/lang/pt/editor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/pt/editor.php b/resources/lang/pt/editor.php index 81c33b201..3a6119681 100644 --- a/resources/lang/pt/editor.php +++ b/resources/lang/pt/editor.php @@ -31,7 +31,7 @@ return [ 'header_large' => 'Cabeçalho grande', 'header_medium' => 'Cabeçalho médio', 'header_small' => 'Cabeçalho pequeno', - 'header_tiny' => 'Cabeçalho pequeno', + 'header_tiny' => 'Cabeçalho minúsculo', 'paragraph' => 'Parágrafo', 'blockquote' => 'Citação', 'inline_code' => 'Código embutido', From 55e52e45fb11f5b733eacb010bd23aa9716466fa Mon Sep 17 00:00:00 2001 From: julesdevops Date: Wed, 6 Apr 2022 22:57:18 +0200 Subject: [PATCH 09/41] Start recycle bin API endpoints: list, restore, delete --- app/Entities/Repos/DeletionRepo.php | 34 +++++ .../Api/RecycleBinApiController.php | 45 ++++++ app/Http/Controllers/RecycleBinController.php | 15 +- routes/api.php | 5 + tests/Api/RecycleBinApiTest.php | 136 ++++++++++++++++++ 5 files changed, 225 insertions(+), 10 deletions(-) create mode 100644 app/Entities/Repos/DeletionRepo.php create mode 100644 app/Http/Controllers/Api/RecycleBinApiController.php create mode 100644 tests/Api/RecycleBinApiTest.php diff --git a/app/Entities/Repos/DeletionRepo.php b/app/Entities/Repos/DeletionRepo.php new file mode 100644 index 000000000..8fad4e6b0 --- /dev/null +++ b/app/Entities/Repos/DeletionRepo.php @@ -0,0 +1,34 @@ +trashCan = $trashCan; + } + + public function restore(int $id): int + { + /** @var Deletion $deletion */ + $deletion = Deletion::query()->findOrFail($id); + Activity::add(ActivityType::RECYCLE_BIN_RESTORE, $deletion); + return $this->trashCan->restoreFromDeletion($deletion); + } + + public function destroy(int $id): int + { + /** @var Deletion $deletion */ + $deletion = Deletion::query()->findOrFail($id); + Activity::add(ActivityType::RECYCLE_BIN_DESTROY, $deletion); + return $this->trashCan->destroyFromDeletion($deletion); + } +} diff --git a/app/Http/Controllers/Api/RecycleBinApiController.php b/app/Http/Controllers/Api/RecycleBinApiController.php new file mode 100644 index 000000000..162b27adb --- /dev/null +++ b/app/Http/Controllers/Api/RecycleBinApiController.php @@ -0,0 +1,45 @@ +middleware(function ($request, $next) { + $this->checkPermission('settings-manage'); + $this->checkPermission('restrictions-manage-all'); + + return $next($request); + }); + } + + public function list() + { + return $this->apiListingResponse(Deletion::query(), [ + 'id', + 'deleted_by', + 'created_at', + 'updated_at', + 'deletable_type', + 'deletable_id' + ]); + } + + public function restore(DeletionRepo $deletionRepo, string $id) + { + $restoreCount = $deletionRepo->restore((int) $id); + return response()->json(['restore_count' => $restoreCount]); + } + + public function destroy(DeletionRepo $deletionRepo, string $id) + { + $deleteCount = $deletionRepo->destroy((int) $id); + return response()->json(['delete_count' => $deleteCount]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php index 1cffb161c..82e3f660b 100644 --- a/app/Http/Controllers/RecycleBinController.php +++ b/app/Http/Controllers/RecycleBinController.php @@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers; use BookStack\Actions\ActivityType; use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Entity; +use BookStack\Entities\Repos\DeletionRepo; use BookStack\Entities\Tools\TrashCan; class RecycleBinController extends Controller @@ -73,12 +74,9 @@ class RecycleBinController extends Controller * * @throws \Exception */ - public function restore(string $id) + public function restore(DeletionRepo $deletionRepo, string $id) { - /** @var Deletion $deletion */ - $deletion = Deletion::query()->findOrFail($id); - $this->logActivity(ActivityType::RECYCLE_BIN_RESTORE, $deletion); - $restoreCount = (new TrashCan())->restoreFromDeletion($deletion); + $restoreCount = $deletionRepo->restore((int) $id); $this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount])); @@ -103,12 +101,9 @@ class RecycleBinController extends Controller * * @throws \Exception */ - public function destroy(string $id) + public function destroy(DeletionRepo $deletionRepo, string $id) { - /** @var Deletion $deletion */ - $deletion = Deletion::query()->findOrFail($id); - $this->logActivity(ActivityType::RECYCLE_BIN_DESTROY, $deletion); - $deleteCount = (new TrashCan())->destroyFromDeletion($deletion); + $deleteCount = $deletionRepo->destroy((int) $id); $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount])); diff --git a/routes/api.php b/routes/api.php index a87169ee5..465f2392c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,6 +9,7 @@ use BookStack\Http\Controllers\Api\ChapterApiController; use BookStack\Http\Controllers\Api\ChapterExportApiController; use BookStack\Http\Controllers\Api\PageApiController; use BookStack\Http\Controllers\Api\PageExportApiController; +use BookStack\Http\Controllers\Api\RecycleBinApiController; use BookStack\Http\Controllers\Api\SearchApiController; use BookStack\Http\Controllers\Api\UserApiController; use Illuminate\Support\Facades\Route; @@ -72,3 +73,7 @@ Route::post('users', [UserApiController::class, 'create']); Route::get('users/{id}', [UserApiController::class, 'read']); Route::put('users/{id}', [UserApiController::class, 'update']); Route::delete('users/{id}', [UserApiController::class, 'delete']); + +Route::get('recycle_bin', [RecycleBinApiController::class, 'list']); +Route::put('recycle_bin/{id}', [RecycleBinApiController::class, 'restore']); +Route::delete('recycle_bin/{id}', [RecycleBinApiController::class, 'destroy']); diff --git a/tests/Api/RecycleBinApiTest.php b/tests/Api/RecycleBinApiTest.php new file mode 100644 index 000000000..9371e06e8 --- /dev/null +++ b/tests/Api/RecycleBinApiTest.php @@ -0,0 +1,136 @@ +getEditor(); + $this->giveUserPermissions($editor, ['settings-manage']); + $this->actingAs($editor); + + foreach ($this->endpointMap as [$method, $uri]) { + $resp = $this->json($method, $uri); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + } + } + + public function test_restrictions_manage_all_permission_neeed_for_all_endpoints() + { + $editor = $this->getEditor(); + $this->giveUserPermissions($editor, ['restrictions-manage-all']); + $this->actingAs($editor); + + foreach ($this->endpointMap as [$method, $uri]) { + $resp = $this->json($method, $uri); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + } + } + + public function test_index_endpoint_returns_expected_page() + { + $this->actingAsAuthorizedUser(); + + $page = Page::query()->first(); + $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($page->getUrl()); + $this->actingAs($editor)->delete($book->getUrl()); + + $deletions = Deletion::query()->orderBy('id')->get(); + + $resp = $this->getJson($this->baseEndpoint); + + $expectedData = $deletions + ->zip([$page, $book]) + ->map(function (Collection $data) use ($editor) { + return [ + 'id' => $data[0]->id, + 'deleted_by' => $editor->getKey(), + 'created_at' => $data[0]->created_at->toJson(), + 'updated_at' => $data[0]->updated_at->toJson(), + 'deletable_type' => $data[1]->getMorphClass(), + 'deletable_id' => $data[1]->getKey() + ]; + }); + + $resp->assertJson([ + 'data' => $expectedData->values()->all(), + 'total' => 2 + ]); + } + + public function test_restore_endpoint() + { + $this->actingAsAuthorizedUser(); + + $page = Page::query()->first(); + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($page->getUrl()); + $page->refresh(); + + $deletion = Deletion::query()->orderBy('id')->first(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->getKey(), + 'deleted_at' => $page->deleted_at + ]); + + $this->putJson($this->baseEndpoint . '/' . $deletion->getKey()); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->getKey(), + 'deleted_at' => null + ]); + } + + public function test_destroy_endpoint() + { + $this->actingAsAuthorizedUser(); + + $page = Page::query()->first(); + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($page->getUrl()); + $page->refresh(); + + $deletion = Deletion::query()->orderBy('id')->first(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->getKey(), + 'deleted_at' => $page->deleted_at + ]); + + $this->deleteJson($this->baseEndpoint . '/' . $deletion->getKey()); + $this->assertDatabaseMissing('pages', ['id' => $page->getKey()]); + } + + private function actingAsAuthorizedUser() + { + $editor = $this->getEditor(); + $this->giveUserPermissions($editor, ['restrictions-manage-all']); + $this->giveUserPermissions($editor, ['settings-manage']); + $this->actingAs($editor); + } +} From 27339079f7dd7b6910d4adff1725ea1e89623130 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 13 Apr 2022 12:08:56 +0100 Subject: [PATCH 10/41] Extracted esbuild config to a build script Allows us to use NodeJS code for file/directory locating to not be shell/os specific, while also also reducing duplicated complexity within packages.json file. Related to #3323 --- dev/build/esbuild.js | 32 ++++ package-lock.json | 360 +++++++++++++++++++++---------------------- package.json | 8 +- 3 files changed, 216 insertions(+), 184 deletions(-) create mode 100644 dev/build/esbuild.js diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js new file mode 100644 index 000000000..46357038a --- /dev/null +++ b/dev/build/esbuild.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const esbuild = require('esbuild'); +const fs = require('fs'); +const path = require('path'); + +// Check if we're building for production +// (Set via passing `production` as first argument) +const isProd = process.argv[2] === 'production'; + +// Gather our input files +const jsInDir = path.join(__dirname, '../../resources/js'); +const jsInDirFiles = fs.readdirSync(jsInDir, 'utf8'); +const entryFiles = jsInDirFiles + .filter(f => f.endsWith('.js') || f.endsWith('.mjs')) + .map(f => path.join(jsInDir, f)); + +// Locate our output directory +const outDir = path.join(__dirname, '../../public/dist'); + +// Build via esbuild +esbuild.build({ + bundle: true, + entryPoints: entryFiles, + outdir: outDir, + sourcemap: true, + target: 'es2020', + mainFields: ['module', 'main'], + format: 'esm', + minify: isProd, + logLevel: "info", +}).catch(() => process.exit(1)); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dd4f0228d..7f0df2282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,17 +14,17 @@ }, "devDependencies": { "chokidar-cli": "^3.0", - "esbuild": "0.14.27", + "esbuild": "0.14.36", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", "punycode": "^2.1.1", - "sass": "^1.49.9" + "sass": "^1.50.0" } }, "node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true, "engines": { "node": ">=6" @@ -341,9 +341,9 @@ } }, "node_modules/esbuild": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.27.tgz", - "integrity": "sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.36.tgz", + "integrity": "sha512-HhFHPiRXGYOCRlrhpiVDYKcFJRdO0sBElZ668M4lh2ER0YgnkLxECuFe7uWCf23FrcLc59Pqr7dHkTqmRPDHmw==", "dev": true, "hasInstallScript": true, "bin": { @@ -353,32 +353,32 @@ "node": ">=12" }, "optionalDependencies": { - "esbuild-android-64": "0.14.27", - "esbuild-android-arm64": "0.14.27", - "esbuild-darwin-64": "0.14.27", - "esbuild-darwin-arm64": "0.14.27", - "esbuild-freebsd-64": "0.14.27", - "esbuild-freebsd-arm64": "0.14.27", - "esbuild-linux-32": "0.14.27", - "esbuild-linux-64": "0.14.27", - "esbuild-linux-arm": "0.14.27", - "esbuild-linux-arm64": "0.14.27", - "esbuild-linux-mips64le": "0.14.27", - "esbuild-linux-ppc64le": "0.14.27", - "esbuild-linux-riscv64": "0.14.27", - "esbuild-linux-s390x": "0.14.27", - "esbuild-netbsd-64": "0.14.27", - "esbuild-openbsd-64": "0.14.27", - "esbuild-sunos-64": "0.14.27", - "esbuild-windows-32": "0.14.27", - "esbuild-windows-64": "0.14.27", - "esbuild-windows-arm64": "0.14.27" + "esbuild-android-64": "0.14.36", + "esbuild-android-arm64": "0.14.36", + "esbuild-darwin-64": "0.14.36", + "esbuild-darwin-arm64": "0.14.36", + "esbuild-freebsd-64": "0.14.36", + "esbuild-freebsd-arm64": "0.14.36", + "esbuild-linux-32": "0.14.36", + "esbuild-linux-64": "0.14.36", + "esbuild-linux-arm": "0.14.36", + "esbuild-linux-arm64": "0.14.36", + "esbuild-linux-mips64le": "0.14.36", + "esbuild-linux-ppc64le": "0.14.36", + "esbuild-linux-riscv64": "0.14.36", + "esbuild-linux-s390x": "0.14.36", + "esbuild-netbsd-64": "0.14.36", + "esbuild-openbsd-64": "0.14.36", + "esbuild-sunos-64": "0.14.36", + "esbuild-windows-32": "0.14.36", + "esbuild-windows-64": "0.14.36", + "esbuild-windows-arm64": "0.14.36" } }, "node_modules/esbuild-android-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.27.tgz", - "integrity": "sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.36.tgz", + "integrity": "sha512-jwpBhF1jmo0tVCYC/ORzVN+hyVcNZUWuozGcLHfod0RJCedTDTvR4nwlTXdx1gtncDqjk33itjO+27OZHbiavw==", "cpu": [ "x64" ], @@ -392,9 +392,9 @@ } }, "node_modules/esbuild-android-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz", - "integrity": "sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.36.tgz", + "integrity": "sha512-/hYkyFe7x7Yapmfv4X/tBmyKnggUmdQmlvZ8ZlBnV4+PjisrEhAvC3yWpURuD9XoB8Wa1d5dGkTsF53pIvpjsg==", "cpu": [ "arm64" ], @@ -408,9 +408,9 @@ } }, "node_modules/esbuild-darwin-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz", - "integrity": "sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.36.tgz", + "integrity": "sha512-kkl6qmV0dTpyIMKagluzYqlc1vO0ecgpviK/7jwPbRDEv5fejRTaBBEE2KxEQbTHcLhiiDbhG7d5UybZWo/1zQ==", "cpu": [ "x64" ], @@ -424,9 +424,9 @@ } }, "node_modules/esbuild-darwin-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz", - "integrity": "sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.36.tgz", + "integrity": "sha512-q8fY4r2Sx6P0Pr3VUm//eFYKVk07C5MHcEinU1BjyFnuYz4IxR/03uBbDwluR6ILIHnZTE7AkTUWIdidRi1Jjw==", "cpu": [ "arm64" ], @@ -440,9 +440,9 @@ } }, "node_modules/esbuild-freebsd-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz", - "integrity": "sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.36.tgz", + "integrity": "sha512-Hn8AYuxXXRptybPqoMkga4HRFE7/XmhtlQjXFHoAIhKUPPMeJH35GYEUWGbjteai9FLFvBAjEAlwEtSGxnqWww==", "cpu": [ "x64" ], @@ -456,9 +456,9 @@ } }, "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz", - "integrity": "sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.36.tgz", + "integrity": "sha512-S3C0attylLLRiCcHiJd036eDEMOY32+h8P+jJ3kTcfhJANNjP0TNBNL30TZmEdOSx/820HJFgRrqpNAvTbjnDA==", "cpu": [ "arm64" ], @@ -472,9 +472,9 @@ } }, "node_modules/esbuild-linux-32": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz", - "integrity": "sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.36.tgz", + "integrity": "sha512-Eh9OkyTrEZn9WGO4xkI3OPPpUX7p/3QYvdG0lL4rfr73Ap2HAr6D9lP59VMF64Ex01LhHSXwIsFG/8AQjh6eNw==", "cpu": [ "ia32" ], @@ -488,9 +488,9 @@ } }, "node_modules/esbuild-linux-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz", - "integrity": "sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.36.tgz", + "integrity": "sha512-vFVFS5ve7PuwlfgoWNyRccGDi2QTNkQo/2k5U5ttVD0jRFaMlc8UQee708fOZA6zTCDy5RWsT5MJw3sl2X6KDg==", "cpu": [ "x64" ], @@ -504,9 +504,9 @@ } }, "node_modules/esbuild-linux-arm": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz", - "integrity": "sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.36.tgz", + "integrity": "sha512-NhgU4n+NCsYgt7Hy61PCquEz5aevI6VjQvxwBxtxrooXsxt5b2xtOUXYZe04JxqQo+XZk3d1gcr7pbV9MAQ/Lg==", "cpu": [ "arm" ], @@ -520,9 +520,9 @@ } }, "node_modules/esbuild-linux-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz", - "integrity": "sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.36.tgz", + "integrity": "sha512-24Vq1M7FdpSmaTYuu1w0Hdhiqkbto1I5Pjyi+4Cdw5fJKGlwQuw+hWynTcRI/cOZxBcBpP21gND7W27gHAiftw==", "cpu": [ "arm64" ], @@ -536,9 +536,9 @@ } }, "node_modules/esbuild-linux-mips64le": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz", - "integrity": "sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.36.tgz", + "integrity": "sha512-hZUeTXvppJN+5rEz2EjsOFM9F1bZt7/d2FUM1lmQo//rXh1RTFYzhC0txn7WV0/jCC7SvrGRaRz0NMsRPf8SIA==", "cpu": [ "mips64el" ], @@ -552,9 +552,9 @@ } }, "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz", - "integrity": "sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.36.tgz", + "integrity": "sha512-1Bg3QgzZjO+QtPhP9VeIBhAduHEc2kzU43MzBnMwpLSZ890azr4/A9Dganun8nsqD/1TBcqhId0z4mFDO8FAvg==", "cpu": [ "ppc64" ], @@ -568,9 +568,9 @@ } }, "node_modules/esbuild-linux-riscv64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.27.tgz", - "integrity": "sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.36.tgz", + "integrity": "sha512-dOE5pt3cOdqEhaufDRzNCHf5BSwxgygVak9UR7PH7KPVHwSTDAZHDoEjblxLqjJYpc5XaU9+gKJ9F8mp9r5I4A==", "cpu": [ "riscv64" ], @@ -584,9 +584,9 @@ } }, "node_modules/esbuild-linux-s390x": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.27.tgz", - "integrity": "sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.36.tgz", + "integrity": "sha512-g4FMdh//BBGTfVHjF6MO7Cz8gqRoDPzXWxRvWkJoGroKA18G9m0wddvPbEqcQf5Tbt2vSc1CIgag7cXwTmoTXg==", "cpu": [ "s390x" ], @@ -600,9 +600,9 @@ } }, "node_modules/esbuild-netbsd-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz", - "integrity": "sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.36.tgz", + "integrity": "sha512-UB2bVImxkWk4vjnP62ehFNZ73lQY1xcnL5ZNYF3x0AG+j8HgdkNF05v67YJdCIuUJpBuTyCK8LORCYo9onSW+A==", "cpu": [ "x64" ], @@ -616,9 +616,9 @@ } }, "node_modules/esbuild-openbsd-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz", - "integrity": "sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.36.tgz", + "integrity": "sha512-NvGB2Chf8GxuleXRGk8e9zD3aSdRO5kLt9coTQbCg7WMGXeX471sBgh4kSg8pjx0yTXRt0MlrUDnjVYnetyivg==", "cpu": [ "x64" ], @@ -632,9 +632,9 @@ } }, "node_modules/esbuild-sunos-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz", - "integrity": "sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.36.tgz", + "integrity": "sha512-VkUZS5ftTSjhRjuRLp+v78auMO3PZBXu6xl4ajomGenEm2/rGuWlhFSjB7YbBNErOchj51Jb2OK8lKAo8qdmsQ==", "cpu": [ "x64" ], @@ -648,9 +648,9 @@ } }, "node_modules/esbuild-windows-32": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz", - "integrity": "sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.36.tgz", + "integrity": "sha512-bIar+A6hdytJjZrDxfMBUSEHHLfx3ynoEZXx/39nxy86pX/w249WZm8Bm0dtOAByAf4Z6qV0LsnTIJHiIqbw0w==", "cpu": [ "ia32" ], @@ -664,9 +664,9 @@ } }, "node_modules/esbuild-windows-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz", - "integrity": "sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.36.tgz", + "integrity": "sha512-+p4MuRZekVChAeueT1Y9LGkxrT5x7YYJxYE8ZOTcEfeUUN43vktSn6hUNsvxzzATrSgq5QqRdllkVBxWZg7KqQ==", "cpu": [ "x64" ], @@ -680,9 +680,9 @@ } }, "node_modules/esbuild-windows-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz", - "integrity": "sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.36.tgz", + "integrity": "sha512-fBB4WlDqV1m18EF/aheGYQkQZHfPHiHJSBYzXIo8yKehek+0BtBwo/4PNwKGJ5T0YK0oc8pBKjgwPbzSrPLb+Q==", "cpu": [ "arm64" ], @@ -1520,9 +1520,9 @@ } }, "node_modules/sass": { - "version": "1.49.9", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", - "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz", + "integrity": "sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -1886,9 +1886,9 @@ }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true }, "ansi-styles": { @@ -2146,170 +2146,170 @@ } }, "esbuild": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.27.tgz", - "integrity": "sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.36.tgz", + "integrity": "sha512-HhFHPiRXGYOCRlrhpiVDYKcFJRdO0sBElZ668M4lh2ER0YgnkLxECuFe7uWCf23FrcLc59Pqr7dHkTqmRPDHmw==", "dev": true, "requires": { - "esbuild-android-64": "0.14.27", - "esbuild-android-arm64": "0.14.27", - "esbuild-darwin-64": "0.14.27", - "esbuild-darwin-arm64": "0.14.27", - "esbuild-freebsd-64": "0.14.27", - "esbuild-freebsd-arm64": "0.14.27", - "esbuild-linux-32": "0.14.27", - "esbuild-linux-64": "0.14.27", - "esbuild-linux-arm": "0.14.27", - "esbuild-linux-arm64": "0.14.27", - "esbuild-linux-mips64le": "0.14.27", - "esbuild-linux-ppc64le": "0.14.27", - "esbuild-linux-riscv64": "0.14.27", - "esbuild-linux-s390x": "0.14.27", - "esbuild-netbsd-64": "0.14.27", - "esbuild-openbsd-64": "0.14.27", - "esbuild-sunos-64": "0.14.27", - "esbuild-windows-32": "0.14.27", - "esbuild-windows-64": "0.14.27", - "esbuild-windows-arm64": "0.14.27" + "esbuild-android-64": "0.14.36", + "esbuild-android-arm64": "0.14.36", + "esbuild-darwin-64": "0.14.36", + "esbuild-darwin-arm64": "0.14.36", + "esbuild-freebsd-64": "0.14.36", + "esbuild-freebsd-arm64": "0.14.36", + "esbuild-linux-32": "0.14.36", + "esbuild-linux-64": "0.14.36", + "esbuild-linux-arm": "0.14.36", + "esbuild-linux-arm64": "0.14.36", + "esbuild-linux-mips64le": "0.14.36", + "esbuild-linux-ppc64le": "0.14.36", + "esbuild-linux-riscv64": "0.14.36", + "esbuild-linux-s390x": "0.14.36", + "esbuild-netbsd-64": "0.14.36", + "esbuild-openbsd-64": "0.14.36", + "esbuild-sunos-64": "0.14.36", + "esbuild-windows-32": "0.14.36", + "esbuild-windows-64": "0.14.36", + "esbuild-windows-arm64": "0.14.36" } }, "esbuild-android-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.27.tgz", - "integrity": "sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.36.tgz", + "integrity": "sha512-jwpBhF1jmo0tVCYC/ORzVN+hyVcNZUWuozGcLHfod0RJCedTDTvR4nwlTXdx1gtncDqjk33itjO+27OZHbiavw==", "dev": true, "optional": true }, "esbuild-android-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz", - "integrity": "sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.36.tgz", + "integrity": "sha512-/hYkyFe7x7Yapmfv4X/tBmyKnggUmdQmlvZ8ZlBnV4+PjisrEhAvC3yWpURuD9XoB8Wa1d5dGkTsF53pIvpjsg==", "dev": true, "optional": true }, "esbuild-darwin-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz", - "integrity": "sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.36.tgz", + "integrity": "sha512-kkl6qmV0dTpyIMKagluzYqlc1vO0ecgpviK/7jwPbRDEv5fejRTaBBEE2KxEQbTHcLhiiDbhG7d5UybZWo/1zQ==", "dev": true, "optional": true }, "esbuild-darwin-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz", - "integrity": "sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.36.tgz", + "integrity": "sha512-q8fY4r2Sx6P0Pr3VUm//eFYKVk07C5MHcEinU1BjyFnuYz4IxR/03uBbDwluR6ILIHnZTE7AkTUWIdidRi1Jjw==", "dev": true, "optional": true }, "esbuild-freebsd-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz", - "integrity": "sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.36.tgz", + "integrity": "sha512-Hn8AYuxXXRptybPqoMkga4HRFE7/XmhtlQjXFHoAIhKUPPMeJH35GYEUWGbjteai9FLFvBAjEAlwEtSGxnqWww==", "dev": true, "optional": true }, "esbuild-freebsd-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz", - "integrity": "sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.36.tgz", + "integrity": "sha512-S3C0attylLLRiCcHiJd036eDEMOY32+h8P+jJ3kTcfhJANNjP0TNBNL30TZmEdOSx/820HJFgRrqpNAvTbjnDA==", "dev": true, "optional": true }, "esbuild-linux-32": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz", - "integrity": "sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.36.tgz", + "integrity": "sha512-Eh9OkyTrEZn9WGO4xkI3OPPpUX7p/3QYvdG0lL4rfr73Ap2HAr6D9lP59VMF64Ex01LhHSXwIsFG/8AQjh6eNw==", "dev": true, "optional": true }, "esbuild-linux-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz", - "integrity": "sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.36.tgz", + "integrity": "sha512-vFVFS5ve7PuwlfgoWNyRccGDi2QTNkQo/2k5U5ttVD0jRFaMlc8UQee708fOZA6zTCDy5RWsT5MJw3sl2X6KDg==", "dev": true, "optional": true }, "esbuild-linux-arm": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz", - "integrity": "sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.36.tgz", + "integrity": "sha512-NhgU4n+NCsYgt7Hy61PCquEz5aevI6VjQvxwBxtxrooXsxt5b2xtOUXYZe04JxqQo+XZk3d1gcr7pbV9MAQ/Lg==", "dev": true, "optional": true }, "esbuild-linux-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz", - "integrity": "sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.36.tgz", + "integrity": "sha512-24Vq1M7FdpSmaTYuu1w0Hdhiqkbto1I5Pjyi+4Cdw5fJKGlwQuw+hWynTcRI/cOZxBcBpP21gND7W27gHAiftw==", "dev": true, "optional": true }, "esbuild-linux-mips64le": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz", - "integrity": "sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.36.tgz", + "integrity": "sha512-hZUeTXvppJN+5rEz2EjsOFM9F1bZt7/d2FUM1lmQo//rXh1RTFYzhC0txn7WV0/jCC7SvrGRaRz0NMsRPf8SIA==", "dev": true, "optional": true }, "esbuild-linux-ppc64le": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz", - "integrity": "sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.36.tgz", + "integrity": "sha512-1Bg3QgzZjO+QtPhP9VeIBhAduHEc2kzU43MzBnMwpLSZ890azr4/A9Dganun8nsqD/1TBcqhId0z4mFDO8FAvg==", "dev": true, "optional": true }, "esbuild-linux-riscv64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.27.tgz", - "integrity": "sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.36.tgz", + "integrity": "sha512-dOE5pt3cOdqEhaufDRzNCHf5BSwxgygVak9UR7PH7KPVHwSTDAZHDoEjblxLqjJYpc5XaU9+gKJ9F8mp9r5I4A==", "dev": true, "optional": true }, "esbuild-linux-s390x": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.27.tgz", - "integrity": "sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.36.tgz", + "integrity": "sha512-g4FMdh//BBGTfVHjF6MO7Cz8gqRoDPzXWxRvWkJoGroKA18G9m0wddvPbEqcQf5Tbt2vSc1CIgag7cXwTmoTXg==", "dev": true, "optional": true }, "esbuild-netbsd-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz", - "integrity": "sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.36.tgz", + "integrity": "sha512-UB2bVImxkWk4vjnP62ehFNZ73lQY1xcnL5ZNYF3x0AG+j8HgdkNF05v67YJdCIuUJpBuTyCK8LORCYo9onSW+A==", "dev": true, "optional": true }, "esbuild-openbsd-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz", - "integrity": "sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.36.tgz", + "integrity": "sha512-NvGB2Chf8GxuleXRGk8e9zD3aSdRO5kLt9coTQbCg7WMGXeX471sBgh4kSg8pjx0yTXRt0MlrUDnjVYnetyivg==", "dev": true, "optional": true }, "esbuild-sunos-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz", - "integrity": "sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.36.tgz", + "integrity": "sha512-VkUZS5ftTSjhRjuRLp+v78auMO3PZBXu6xl4ajomGenEm2/rGuWlhFSjB7YbBNErOchj51Jb2OK8lKAo8qdmsQ==", "dev": true, "optional": true }, "esbuild-windows-32": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz", - "integrity": "sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.36.tgz", + "integrity": "sha512-bIar+A6hdytJjZrDxfMBUSEHHLfx3ynoEZXx/39nxy86pX/w249WZm8Bm0dtOAByAf4Z6qV0LsnTIJHiIqbw0w==", "dev": true, "optional": true }, "esbuild-windows-64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz", - "integrity": "sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.36.tgz", + "integrity": "sha512-+p4MuRZekVChAeueT1Y9LGkxrT5x7YYJxYE8ZOTcEfeUUN43vktSn6hUNsvxzzATrSgq5QqRdllkVBxWZg7KqQ==", "dev": true, "optional": true }, "esbuild-windows-arm64": { - "version": "0.14.27", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz", - "integrity": "sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg==", + "version": "0.14.36", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.36.tgz", + "integrity": "sha512-fBB4WlDqV1m18EF/aheGYQkQZHfPHiHJSBYzXIo8yKehek+0BtBwo/4PNwKGJ5T0YK0oc8pBKjgwPbzSrPLb+Q==", "dev": true, "optional": true }, @@ -2910,9 +2910,9 @@ } }, "sass": { - "version": "1.49.9", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", - "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz", + "integrity": "sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", diff --git a/package.json b/package.json index 054015009..b49a2a07f 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "build:css:dev": "sass ./resources/sass:./public/dist", "build:css:watch": "sass ./resources/sass:./public/dist --watch", "build:css:production": "sass ./resources/sass:./public/dist -s compressed", - "build:js:dev": "esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --format=esm", + "build:js:dev": "node dev/build/esbuild.js", "build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"", - "build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --minify --format=esm", + "build:js:production": "node dev/build/esbuild.js production", "build": "npm-run-all --parallel build:*:dev", "production": "npm-run-all --parallel build:*:production", "dev": "npm-run-all --parallel watch livereload", @@ -16,11 +16,11 @@ }, "devDependencies": { "chokidar-cli": "^3.0", - "esbuild": "0.14.27", + "esbuild": "0.14.36", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", "punycode": "^2.1.1", - "sass": "^1.49.9" + "sass": "^1.50.0" }, "dependencies": { "clipboard": "^2.0.10", From 25654b232214583f06aef0a4edfc28815b66b735 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 13 Apr 2022 12:46:19 +0100 Subject: [PATCH 11/41] Fixed base URL starting slash usage --- app/Http/Request.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Http/Request.php b/app/Http/Request.php index 687bae9a6..13892603d 100644 --- a/app/Http/Request.php +++ b/app/Http/Request.php @@ -26,6 +26,7 @@ class Request extends LaravelRequest /** * Override the default request methods to get the base URL * to directly use the custom APP_URL, if set. + * The base URL never ends with a / but should start with one if not empty. * * @return string */ @@ -34,7 +35,7 @@ class Request extends LaravelRequest $appUrl = config('app.url', null); if ($appUrl) { - return rtrim(implode('/', array_slice(explode('/', $appUrl), 3)), '/'); + return '/' . rtrim(implode('/', array_slice(explode('/', $appUrl), 3)), '/'); } return parent::getBaseUrl(); From 56254bdb66678ea69f70c95a3262d575dae03e11 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 13 Apr 2022 13:02:42 +0100 Subject: [PATCH 12/41] Added testing for our request method overrides --- tests/Unit/UrlTest.php | 22 ---------------------- tests/UrlTest.php | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 22 deletions(-) delete mode 100644 tests/Unit/UrlTest.php create mode 100644 tests/UrlTest.php diff --git a/tests/Unit/UrlTest.php b/tests/Unit/UrlTest.php deleted file mode 100644 index fff5414f2..000000000 --- a/tests/Unit/UrlTest.php +++ /dev/null @@ -1,22 +0,0 @@ -runWithEnv('APP_URL', 'http://example.com/bookstack', function () { - $this->assertEquals('http://example.com/bookstack/books', url('/books')); - }); - } - - public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different() - { - $this->runWithEnv('APP_URL', 'https://example.com/', function () { - $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css'); - }); - } -} diff --git a/tests/UrlTest.php b/tests/UrlTest.php new file mode 100644 index 000000000..b9f1acf4b --- /dev/null +++ b/tests/UrlTest.php @@ -0,0 +1,37 @@ +runWithEnv('APP_URL', 'http://example.com/bookstack', function () { + $this->assertEquals('http://example.com/bookstack/books', url('/books')); + }); + } + + public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different() + { + $this->runWithEnv('APP_URL', 'https://example.com/', function () { + $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css'); + }); + } + + public function test_app_url_forces_overrides_on_base_request() + { + config()->set('app.url', 'https://donkey.example.com:8091/cool/docs'); + + // Have to manually get and wrap request in our custom type due to testing mechanics + $this->get('/login'); + $bsRequest = Request::createFrom(request()); + + $this->assertEquals('https://donkey.example.com:8091', $bsRequest->getSchemeAndHttpHost()); + $this->assertEquals('/cool/docs', $bsRequest->getBaseUrl()); + $this->assertEquals('https://donkey.example.com:8091/cool/docs/login', $bsRequest->getUri()); + } +} From e49afdbd7238ce59f9fca019c0767ea8ed966cfc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 14 Apr 2022 16:14:05 +0100 Subject: [PATCH 13/41] New Crowdin updates (#3358) --- resources/lang/de/settings.php | 2 +- resources/lang/eu/editor.php | 40 +++--- resources/lang/eu/entities.php | 212 +++++++++++++++--------------- resources/lang/eu/errors.php | 12 +- resources/lang/eu/settings.php | 50 +++---- resources/lang/id/auth.php | 14 +- resources/lang/id/editor.php | 46 +++---- resources/lang/id/validation.php | 6 +- resources/lang/nl/settings.php | 4 +- resources/lang/pt/activities.php | 8 +- resources/lang/pt/common.php | 2 +- resources/lang/pt/editor.php | 2 +- resources/lang/ru/editor.php | 20 +-- resources/lang/ru/settings.php | 4 +- resources/lang/ru/validation.php | 2 +- resources/lang/zh_CN/editor.php | 34 ++--- resources/lang/zh_CN/settings.php | 6 +- 17 files changed, 232 insertions(+), 232 deletions(-) diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php index 8c14f2734..db459e390 100644 --- a/resources/lang/de/settings.php +++ b/resources/lang/de/settings.php @@ -202,7 +202,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'users_social_accounts' => 'Social-Media Konten', 'users_social_accounts_info' => 'Hier können Sie andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto lösen, bleibt der Zugriff erhalten. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media-Kontos.', 'users_social_connect' => 'Social-Media-Konto verknüpfen', - 'users_social_disconnect' => 'Social-Media-Konto lösen', + 'users_social_disconnect' => 'Social-Media-Konto löschen', 'users_social_connected' => ':socialAccount-Konto wurde erfolgreich mit dem Profil verknüpft.', 'users_social_disconnected' => ':socialAccount-Konto wurde erfolgreich vom Profil gelöst.', 'users_api_tokens' => 'API-Token', diff --git a/resources/lang/eu/editor.php b/resources/lang/eu/editor.php index 5403f88bc..5381baf18 100644 --- a/resources/lang/eu/editor.php +++ b/resources/lang/eu/editor.php @@ -24,7 +24,7 @@ return [ 'width' => 'Zabalera', 'height' => 'Altuera', 'More' => 'Gehiago', - 'select' => 'Select...', + 'select' => 'Aukeratu...', // Toolbar 'formats' => 'Formatuak', @@ -53,33 +53,33 @@ return [ 'align_left' => 'Lerrokatu ezkerrean', 'align_center' => 'Lerrokatu erdian', 'align_right' => 'Lerrokatu eskuinean', - 'align_justify' => 'Justify', + 'align_justify' => 'Justifikatuta', 'list_bullet' => 'Buletdun zerrenda', 'list_numbered' => 'Zenbakitutako zerrenda', - 'list_task' => 'Task list', + 'list_task' => 'Zereginen zerrenda', 'indent_increase' => 'Handitu koska', 'indent_decrease' => 'Txikitu koska', 'table' => 'Taula', 'insert_image' => 'Irudia txertatu', - 'insert_image_title' => 'Insert/Edit Image', - 'insert_link' => 'Insert/edit link', - 'insert_link_title' => 'Insert/Edit Link', - 'insert_horizontal_line' => 'Insert horizontal line', - 'insert_code_block' => 'Insert code block', - 'insert_drawing' => 'Insert/edit drawing', - 'drawing_manager' => 'Drawing manager', - 'insert_media' => 'Insert/edit media', - 'insert_media_title' => 'Insert/Edit Media', - 'clear_formatting' => 'Clear formatting', - 'source_code' => 'Source code', - 'source_code_title' => 'Source Code', - 'fullscreen' => 'Fullscreen', - 'image_options' => 'Image options', + 'insert_image_title' => 'Aldatu/Txertatu irudia', + 'insert_link' => 'Txertatu/aldatu esteka', + 'insert_link_title' => 'Txertatu/Aldatu esteka', + 'insert_horizontal_line' => 'Txertatu linea horizontala', + 'insert_code_block' => 'Txertatu kode-blokea', + 'insert_drawing' => 'Txertatu marrazki berria', + 'drawing_manager' => 'Marrazki kudeaketa', + 'insert_media' => 'Txertatu/aldatu media', + 'insert_media_title' => 'Aldatu/Txertatu irudia', + 'clear_formatting' => 'Garbitu formatua', + 'source_code' => 'Iturburu kodea', + 'source_code_title' => 'Iturburu kodea', + 'fullscreen' => 'Pantaila osoa', + 'image_options' => 'Irudiaren aukerak', // Tables - 'table_properties' => 'Table properties', - 'table_properties_title' => 'Table Properties', - 'delete_table' => 'Delete table', + 'table_properties' => 'Taularen propietateak', + 'table_properties_title' => 'Taularen propietateak', + 'delete_table' => 'Ezabatu taula', 'insert_row_before' => 'Insert row before', 'insert_row_after' => 'Insert row after', 'delete_row' => 'Delete row', diff --git a/resources/lang/eu/entities.php b/resources/lang/eu/entities.php index 57be5a3ae..bfc5cdd80 100644 --- a/resources/lang/eu/entities.php +++ b/resources/lang/eu/entities.php @@ -29,73 +29,73 @@ return [ 'my_recently_viewed' => 'Nik Ikusitako azkenak', 'my_most_viewed_favourites' => 'Nire gehien ikusitako gogokoak', 'my_favourites' => 'Nire Gogokoenak', - 'no_pages_viewed' => 'You have not viewed any pages', + 'no_pages_viewed' => 'Ez daukazu ikusiriko orririk', 'no_pages_recently_created' => 'Ez da orrialderik sortu azkenaldian', 'no_pages_recently_updated' => 'Ez da orrialderik aldatu azkenaldian', 'export' => 'Esportatu', - 'export_html' => 'Contained Web File', + 'export_html' => 'Daukan web artxiboa', 'export_pdf' => 'PDF fitxategia', 'export_text' => 'Testu lauko fitxategiak', - 'export_md' => 'Markdown File', + 'export_md' => 'Markdown fitxategia', // Permissions and restrictions 'permissions' => 'Baimenak', - 'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.', + 'permissions_intro' => 'Behin hau aktibatuta, baimen hauek lehentasuna izango dute beste edozein rol-engainetik.', 'permissions_enable' => 'Baimena pertsonalizatuak Gaitu', 'permissions_save' => 'Gorde baimenak', 'permissions_owner' => 'Jabea', // Search 'search_results' => 'Bilaketaren emaitzak', - 'search_total_results_found' => ':count result found|:count total results found', + 'search_total_results_found' => ':count emaitza aurkitu dira|:count emaitza aurkitu dira guztira', 'search_clear' => 'Bilaketa testua garbitu', 'search_no_pages' => 'Ez da orririk aurkitu zure bilaketan', - 'search_for_term' => 'Search for :term', + 'search_for_term' => 'Bilatu honen arabera :term', 'search_more' => 'Emaitza gehiago', 'search_advanced' => 'Bilaketa aurreratua', 'search_terms' => 'Bilaketa-hitza', 'search_content_type' => 'Eduki Mota', 'search_exact_matches' => 'Bat etortze zehatza', - 'search_tags' => 'Tag Searches', + 'search_tags' => 'Etiketa bilaketak', 'search_options' => 'Aukerak', 'search_viewed_by_me' => 'Nik ikusiak', 'search_not_viewed_by_me' => 'Nik ikusi ez ditudanak', 'search_permissions_set' => 'Baimenak', - 'search_created_by_me' => 'Created by me', - 'search_updated_by_me' => 'Updated by me', - 'search_owned_by_me' => 'Owned by me', - 'search_date_options' => 'Date Options', - 'search_updated_before' => 'Updated before', - 'search_updated_after' => 'Updated after', - 'search_created_before' => 'Created before', - 'search_created_after' => 'Created after', - 'search_set_date' => 'Set Date', - 'search_update' => 'Update Search', + 'search_created_by_me' => 'Nik sortuak', + 'search_updated_by_me' => 'Nik eguneratuak', + 'search_owned_by_me' => 'Nire jabetazkoak', + 'search_date_options' => 'Data aukerak', + 'search_updated_before' => 'Aurretik eguneratuak', + 'search_updated_after' => 'Ondoren eguneratuak', + 'search_created_before' => 'Aurretik sortuak', + 'search_created_after' => 'Ondoren sortuak', + 'search_set_date' => 'Data finkatu', + 'search_update' => 'Eguneratu bilaketa', // Shelves - 'shelf' => 'Shelf', - 'shelves' => 'Shelves', - 'x_shelves' => ':count Shelf|:count Shelves', - 'shelves_long' => 'Bookshelves', - 'shelves_empty' => 'No shelves have been created', - 'shelves_create' => 'Create New Shelf', - 'shelves_popular' => 'Popular Shelves', - 'shelves_new' => 'New Shelves', - 'shelves_new_action' => 'New Shelf', - 'shelves_popular_empty' => 'The most popular shelves will appear here.', - 'shelves_new_empty' => 'The most recently created shelves will appear here.', - 'shelves_save' => 'Save Shelf', - 'shelves_books' => 'Books on this shelf', - 'shelves_add_books' => 'Add books to this shelf', - 'shelves_drag_books' => 'Drag books here to add them to this shelf', - 'shelves_empty_contents' => 'This shelf has no books assigned to it', - 'shelves_edit_and_assign' => 'Edit shelf to assign books', - 'shelves_edit_named' => 'Edit Bookshelf :name', - 'shelves_edit' => 'Edit Bookshelf', - 'shelves_delete' => 'Delete Bookshelf', - 'shelves_delete_named' => 'Delete Bookshelf :name', - 'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.", - 'shelves_delete_confirmation' => 'Are you sure you want to delete this bookshelf?', + 'shelf' => 'Apalategia', + 'shelves' => 'Apalategiak', + 'x_shelves' => ':count Apalategi|:count Apalategi', + 'shelves_long' => 'Liburu-Apalategi', + 'shelves_empty' => 'Ez da inolako apalategirik sortu', + 'shelves_create' => 'Apalategi berria sortu', + 'shelves_popular' => 'Apalategi esanguratsuak', + 'shelves_new' => 'Apalategi berriak', + 'shelves_new_action' => 'Apalategi berria', + 'shelves_popular_empty' => 'Apalategi ikusienak hemen agertuko dira.', + 'shelves_new_empty' => 'Berriki sorturiko apalategiak hemen agertuko dira.', + 'shelves_save' => 'Gorde apalategia', + 'shelves_books' => 'Apalategi honetako liburuak', + 'shelves_add_books' => 'Gehitu liburuak apalategi honetara', + 'shelves_drag_books' => 'Bota hona liburuak apalategi honetara gehitzeko', + 'shelves_empty_contents' => 'Apalategi honek ez dauka libururik', + 'shelves_edit_and_assign' => 'Apalategia editatu liburuak gehitzeko', + 'shelves_edit_named' => ':name liburu-apalategia editatu', + 'shelves_edit' => 'Liburu-apalategia editatu', + 'shelves_delete' => 'Apalategia ezabatu', + 'shelves_delete_named' => ':name apalategia ezabatu', + 'shelves_delete_explain' => "':name' apalategia ezabatuko du ekintza honek. bertan dauden liburuak ez dira ezabatuko.", + 'shelves_delete_confirmation' => 'Ziur zaude apalategi hau ezabatu nahi duzula?', 'shelves_permissions' => 'Bookshelf Permissions', 'shelves_permissions_updated' => 'Bookshelf Permissions Updated', 'shelves_permissions_active' => 'Bookshelf Permissions Active', @@ -106,11 +106,11 @@ return [ 'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books', // Books - 'book' => 'Book', - 'books' => 'Books', + 'book' => 'Liburua', + 'books' => 'Liburuak', 'x_books' => ':count Book|:count Books', 'books_empty' => 'Ez da orrialderik sortu', - 'books_popular' => 'Popular Books', + 'books_popular' => 'Liburu ikusienak', 'books_recent' => 'Azken liburuak', 'books_new' => 'Liburu berriak', 'books_new_action' => 'Liburu berria', @@ -139,9 +139,9 @@ return [ 'books_sort_name' => 'Ordenatu izenaren arabera', 'books_sort_created' => 'Ordenatu argitaratze-dataren arabera', 'books_sort_updated' => 'Sort by Updated Date', - 'books_sort_chapters_first' => 'Chapters First', - 'books_sort_chapters_last' => 'Chapters Last', - 'books_sort_show_other' => 'Show Other Books', + 'books_sort_chapters_first' => 'Lehen kapitulua', + 'books_sort_chapters_last' => 'Azken kapitulua', + 'books_sort_show_other' => 'Erakutsi beste liburuak', 'books_sort_save' => 'Save New Order', 'books_copy' => 'Copy Book', 'books_copy_success' => 'Book successfully copied', @@ -150,9 +150,9 @@ return [ 'chapter' => 'Kapitulua', 'chapters' => 'Kapituluak', 'x_chapters' => ':count Chapter|:count Chapters', - 'chapters_popular' => 'Popular Chapters', - 'chapters_new' => 'New Chapter', - 'chapters_create' => 'Create New Chapter', + 'chapters_popular' => 'Kapitulu ikusienak', + 'chapters_new' => 'Kopiatu kapitulua', + 'chapters_create' => 'Sortu kapitulu berria', 'chapters_delete' => 'Kapitulua ezabatu', 'chapters_delete_named' => 'Delete Chapter :chapterName', 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.', @@ -160,11 +160,11 @@ return [ 'chapters_edit' => 'Kapitulua aldatu', 'chapters_edit_named' => 'Edit Chapter :chapterName', 'chapters_save' => 'Kapitulua gorde', - 'chapters_move' => 'Move Chapter', + 'chapters_move' => 'Kapitulua mugitu', 'chapters_move_named' => 'Move Chapter :chapterName', 'chapter_move_success' => 'Chapter moved to :bookName', - 'chapters_copy' => 'Copy Chapter', - 'chapters_copy_success' => 'Chapter successfully copied', + 'chapters_copy' => 'Kapitulua kopiatu', + 'chapters_copy_success' => 'Kapitulua egoki kopiatua', 'chapters_permissions' => 'Chapter Permissions', 'chapters_empty' => 'No pages are currently in this chapter.', 'chapters_permissions_active' => 'Chapter Permissions Active', @@ -172,41 +172,41 @@ return [ 'chapters_search_this' => 'Search this chapter', // Pages - 'page' => 'Page', - 'pages' => 'Pages', + 'page' => 'Orria', + 'pages' => 'Orriak', 'x_pages' => ':count Page|:count Pages', 'pages_popular' => 'Popular Pages', - 'pages_new' => 'New Page', - 'pages_attachments' => 'Attachments', - 'pages_navigation' => 'Page Navigation', - 'pages_delete' => 'Delete Page', + 'pages_new' => 'Orrialde berria', + 'pages_attachments' => 'Eranskinak', + 'pages_navigation' => 'Nabigazio orrialdea', + 'pages_delete' => 'Ezabatu orria', 'pages_delete_named' => 'Delete Page :pageName', 'pages_delete_draft_named' => 'Delete Draft Page :pageName', 'pages_delete_draft' => 'Delete Draft Page', - 'pages_delete_success' => 'Page deleted', + 'pages_delete_success' => 'Orria ezabatua', 'pages_delete_draft_success' => 'Draft page deleted', - 'pages_delete_confirm' => 'Are you sure you want to delete this page?', + 'pages_delete_confirm' => 'Ziur al zaude orri hau ezabatu nahi duzula?', 'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?', 'pages_editing_named' => 'Editing Page :pageName', 'pages_edit_draft_options' => 'Draft Options', - 'pages_edit_save_draft' => 'Save Draft', + 'pages_edit_save_draft' => 'Gorde zirriborroa', 'pages_edit_draft' => 'Edit Page Draft', - 'pages_editing_draft' => 'Editing Draft', - 'pages_editing_page' => 'Editing Page', + 'pages_editing_draft' => 'Editatu zirriborroa', + 'pages_editing_page' => 'Editatu orrialdea', 'pages_edit_draft_save_at' => 'Draft saved at ', - 'pages_edit_delete_draft' => 'Delete Draft', - 'pages_edit_discard_draft' => 'Discard Draft', + 'pages_edit_delete_draft' => 'Ezabatu zirriborroa', + 'pages_edit_discard_draft' => 'Baztertu zirriborroa', 'pages_edit_set_changelog' => 'Set Changelog', 'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made', 'pages_edit_enter_changelog' => 'Enter Changelog', - 'pages_save' => 'Save Page', - 'pages_title' => 'Page Title', - 'pages_name' => 'Page Name', - 'pages_md_editor' => 'Editor', - 'pages_md_preview' => 'Preview', - 'pages_md_insert_image' => 'Insert Image', + 'pages_save' => 'Gorde orrialdea', + 'pages_title' => 'Orrialdearen titulua', + 'pages_name' => 'Orrialdearen izena', + 'pages_md_editor' => 'Editorea', + 'pages_md_preview' => 'Aurrebista', + 'pages_md_insert_image' => 'Txertatu irudia', 'pages_md_insert_link' => 'Insert Entity Link', - 'pages_md_insert_drawing' => 'Insert Drawing', + 'pages_md_insert_drawing' => 'Txertatu marrazki berria', 'pages_not_in_chapter' => 'Page is not in a chapter', 'pages_move' => 'Move Page', 'pages_move_success' => 'Page moved to ":parentName"', @@ -220,22 +220,22 @@ return [ 'pages_revisions_named' => 'Page Revisions for :pageName', 'pages_revision_named' => 'Page Revision for :pageName', 'pages_revision_restored_from' => 'Restored from #:id; :summary', - 'pages_revisions_created_by' => 'Created By', - 'pages_revisions_date' => 'Revision Date', + 'pages_revisions_created_by' => 'Sortzailea', + 'pages_revisions_date' => 'Berrikuspen data', 'pages_revisions_number' => '#', 'pages_revisions_numbered' => 'Revision #:id', 'pages_revisions_numbered_changes' => 'Revision #:id Changes', 'pages_revisions_changelog' => 'Changelog', - 'pages_revisions_changes' => 'Changes', + 'pages_revisions_changes' => 'Aldaketak', 'pages_revisions_current' => 'Current Version', - 'pages_revisions_preview' => 'Preview', - 'pages_revisions_restore' => 'Restore', + 'pages_revisions_preview' => 'Aurrebista', + 'pages_revisions_restore' => 'Berreskuratu', 'pages_revisions_none' => 'This page has no revisions', 'pages_copy_link' => 'Copy Link', - 'pages_edit_content_link' => 'Edit Content', + 'pages_edit_content_link' => 'Editatu edukia', 'pages_permissions_active' => 'Page Permissions Active', 'pages_initial_revision' => 'Initial publish', - 'pages_initial_name' => 'New Page', + 'pages_initial_name' => 'Orrialde berria', 'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.', 'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.', 'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.', @@ -248,19 +248,19 @@ return [ ], 'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content', 'pages_specific' => 'Specific Page', - 'pages_is_template' => 'Page Template', + 'pages_is_template' => 'Orrialde txantiloia', // Editor Sidebar - 'page_tags' => 'Page Tags', - 'chapter_tags' => 'Chapter Tags', - 'book_tags' => 'Book Tags', - 'shelf_tags' => 'Shelf Tags', - 'tag' => 'Tag', - 'tags' => 'Tags', - 'tag_name' => 'Tag Name', + 'page_tags' => 'Orrialde etiketak', + 'chapter_tags' => 'Kapitulu etiketak', + 'book_tags' => 'Liburu etiketak', + 'shelf_tags' => 'Apalategi etiketak', + 'tag' => 'Etiketa', + 'tags' => 'Etiketak', + 'tag_name' => 'Etiketa izena', 'tag_value' => 'Tag Value (Optional)', 'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.", - 'tags_add' => 'Add another tag', + 'tags_add' => 'Beste bat gehitu', 'tags_remove' => 'Remove this tag', 'tags_usages' => 'Total tag usages', 'tags_assigned_pages' => 'Assigned to Pages', @@ -268,29 +268,29 @@ return [ 'tags_assigned_books' => 'Assigned to Books', 'tags_assigned_shelves' => 'Assigned to Shelves', 'tags_x_unique_values' => ':count unique values', - 'tags_all_values' => 'All values', + 'tags_all_values' => 'Balio guztiak', 'tags_view_tags' => 'View Tags', 'tags_view_existing_tags' => 'View existing tags', 'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.', - 'attachments' => 'Attachments', + 'attachments' => 'Eranskinak', 'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.', 'attachments_explain_instant_save' => 'Changes here are saved instantly.', - 'attachments_items' => 'Attached Items', - 'attachments_upload' => 'Upload File', + 'attachments_items' => 'Atxikiak', + 'attachments_upload' => 'Kargatu artxiboak', 'attachments_link' => 'Attach Link', 'attachments_set_link' => 'Set Link', 'attachments_delete' => 'Are you sure you want to delete this attachment?', 'attachments_dropzone' => 'Drop files or click here to attach a file', - 'attachments_no_files' => 'No files have been uploaded', + 'attachments_no_files' => 'Ez da igo fitxategirik', 'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.', - 'attachments_link_name' => 'Link Name', + 'attachments_link_name' => 'Loturaren izena', 'attachment_link' => 'Attachment link', - 'attachments_link_url' => 'Link to file', + 'attachments_link_url' => 'Fitxategiarentzako esteka', 'attachments_link_url_hint' => 'Url of site or file', 'attach' => 'Attach', 'attachments_insert_link' => 'Add Attachment Link to Page', 'attachments_edit_file' => 'Edit File', - 'attachments_edit_file_name' => 'File Name', + 'attachments_edit_file_name' => 'Fitxategi izena', 'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite', 'attachments_order_updated' => 'Attachment order updated', 'attachments_updated_success' => 'Attachment details updated', @@ -314,25 +314,25 @@ return [ 'profile_not_created_shelves' => ':userName has not created any shelves', // Comments - 'comment' => 'Comment', - 'comments' => 'Comments', - 'comment_add' => 'Add Comment', - 'comment_placeholder' => 'Leave a comment here', + 'comment' => 'Iruzkina', + 'comments' => 'Iruzkinak', + 'comment_add' => 'Iruzkina gehitu', + 'comment_placeholder' => 'Utzi iruzkin bat hemen', 'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments', - 'comment_save' => 'Save Comment', + 'comment_save' => 'Iruzkina gorde', 'comment_saving' => 'Saving comment...', 'comment_deleting' => 'Deleting comment...', - 'comment_new' => 'New Comment', + 'comment_new' => 'Iruzkin berria', 'comment_created' => 'commented :createDiff', 'comment_updated' => 'Updated :updateDiff by :username', 'comment_deleted_success' => 'Comment deleted', - 'comment_created_success' => 'Comment added', - 'comment_updated_success' => 'Comment updated', - 'comment_delete_confirm' => 'Are you sure you want to delete this comment?', + 'comment_created_success' => 'Iruzkina gehituta', + 'comment_updated_success' => 'Iruzkina gehituta', + 'comment_delete_confirm' => 'Ziur zaude iruzkin hau ezabatu nahi duzula?', 'comment_in_reply_to' => 'In reply to :commentId', // Revision - 'revision_delete_confirm' => 'Are you sure you want to delete this revision?', + 'revision_delete_confirm' => 'Ziur zaude hau ezabatu nahi duzula?', '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.', diff --git a/resources/lang/eu/errors.php b/resources/lang/eu/errors.php index 250d5d92f..bd25e49d5 100644 --- a/resources/lang/eu/errors.php +++ b/resources/lang/eu/errors.php @@ -20,10 +20,10 @@ return [ 'ldap_cannot_connect' => 'Ezin izan da ldap zerbitzarira konektatu, hasierako konexioak huts egin du', 'saml_already_logged_in' => 'Saioa aurretik hasita dago', 'saml_user_not_registered' => ':name erabiltzailea ez dago erregistratua eta erregistro automatikoa ezgaituta dago', - 'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system', - 'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.', + 'saml_no_email_address' => 'Ezin izan dugu posta helbiderik aurkitu erabiltzaile honentzat, kanpoko autentifikazio zerbitzuak bidalitako datuetan', + 'saml_invalid_response_id' => 'Kanpoko egiazkotasun-sistemaren eskaria ez du onartzen aplikazio honek abiarazitako prozesu batek. Loginean atzera egitea izan daiteke arrazoia.', 'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization', - 'oidc_already_logged_in' => 'Already logged in', + 'oidc_already_logged_in' => 'Dagoeneko saioa hasita', 'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled', 'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system', 'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization', @@ -84,13 +84,13 @@ return [ 'empty_comment' => 'Cannot add an empty comment.', // Error pages - '404_page_not_found' => 'Page Not Found', + '404_page_not_found' => 'Ez da orrialdea aurkitu', 'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.', 'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.', - 'image_not_found' => 'Image Not Found', + 'image_not_found' => 'Irudia Ez da Aurkitu', 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', - 'return_home' => 'Return to home', + 'return_home' => 'Itzuli hasierara', 'error_occurred' => 'Akats bat gertatu da', 'app_down' => ':appName is down right now', 'back_soon' => 'It will be back up soon.', diff --git a/resources/lang/eu/settings.php b/resources/lang/eu/settings.php index dac1e107e..5cfe14ff8 100644 --- a/resources/lang/eu/settings.php +++ b/resources/lang/eu/settings.php @@ -31,13 +31,13 @@ return [ 'app_editor_desc' => 'Aukeratu zein editore erabiliko duten erabiltzaile guztiek orriak editatzeko.', 'app_custom_html' => 'HTML pertsonalizatuko goiburu edukia', 'app_custom_html_desc' => 'Hemen sarturiko edozein eduki eremuko behekaldean sartuko da orrialde guztietan. Honek estiloak gainditzeko edo analitika-kodea gehitzeko balio du.', - 'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.', + 'app_custom_html_disabled_notice' => 'HTML edukiera desgaituta dago konfigurazio-orri honetan, edozein aldaketa eten daitekeela bermatzeko.', 'app_logo' => 'Aplikazioaren logoa', - 'app_logo_desc' => 'This image should be 43px in height.
Large images will be scaled down.', + 'app_logo_desc' => 'Irudi honek 43px izan behar du altueran.
Irudi handiagoak txikitu egingo dira.', 'app_primary_color' => 'Aplikazioaren kolore lehenetsia', - 'app_primary_color_desc' => 'Sets the primary color for the application including the banner, buttons, and links.', + 'app_primary_color_desc' => 'Konfiguratu aplikaziorako kolore nagusia, botoi, banner eta estekak barne.', 'app_homepage' => 'Aplikazioko hasiera orria', - 'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.', + 'app_homepage_desc' => 'Aukeratu hasierako orriko bista, defektuzkoa beharrean. Orrialde baimenak ez dira kontutan hartuko aukeratutako orrialdeentzat.', 'app_homepage_select' => 'Aukeratu Orria', 'app_footer_links' => 'Beheko aldeko estekak', 'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".', @@ -112,18 +112,18 @@ return [ 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', // Audit Log - 'audit' => 'Audit Log', + 'audit' => 'Auditoretza erregistroak', 'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', - 'audit_event_filter' => 'Event Filter', + 'audit_event_filter' => 'Gertakari filtroa', 'audit_event_filter_no_filter' => 'Filtrorik ez', - 'audit_deleted_item' => 'Deleted Item', - 'audit_deleted_item_name' => 'Name: :name', + 'audit_deleted_item' => 'Ezabatutako edukiak', + 'audit_deleted_item_name' => 'Izena :name', 'audit_table_user' => 'Erabiltzailea', 'audit_table_event' => 'Gertaera', 'audit_table_related' => 'Related Item or Detail', 'audit_table_ip' => 'IP helbidea', 'audit_table_date' => 'Azken aktibitate data', - 'audit_date_from' => 'Date Range From', + 'audit_date_from' => 'Data tartea', 'audit_date_to' => 'Data tartea', // Role Settings @@ -131,20 +131,20 @@ return [ 'role_user_roles' => 'Erabiltzailearen rola', 'role_create' => 'Rol berria sortu', 'role_create_success' => 'Rola ondo sortu da', - 'role_delete' => 'Delete Role', + 'role_delete' => 'Ezabatu Rol-a', 'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.', 'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.', - 'role_delete_no_migration' => "Don't migrate users", - 'role_delete_sure' => 'Are you sure you want to delete this role?', - 'role_delete_success' => 'Role successfully deleted', - 'role_edit' => 'Edit Role', - 'role_details' => 'Role Details', - 'role_name' => 'Role Name', + 'role_delete_no_migration' => "Ez migratu erabiltzaileak", + 'role_delete_sure' => 'Ziur zaude rol hau ezabatu nahi duzula?', + 'role_delete_success' => 'Rola ezabatua', + 'role_edit' => 'Editatu rola', + 'role_details' => 'Ireki xehetasunak', + 'role_name' => 'Rol izena', 'role_desc' => 'Short Description of Role', 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'External Authentication IDs', 'role_system' => 'System Permissions', - 'role_manage_users' => 'Manage users', + 'role_manage_users' => 'Erabiltzaileak kudeatu', 'role_manage_roles' => 'Manage roles & role permissions', 'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions', 'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages', @@ -178,13 +178,13 @@ return [ 'users_password' => 'Erabiltzaile pasahitza', 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.', 'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.', - 'users_send_invite_option' => 'Send user invite email', - 'users_external_auth_id' => 'External Authentication ID', + 'users_send_invite_option' => 'Erabiltzailea gonbidatzeko emaila bidali', + 'users_external_auth_id' => 'Kanpo autentikazioa IDa', 'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.', 'users_password_warning' => 'Only fill the below if you would like to change your password.', 'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.', 'users_delete' => 'Ezabatu erabiltzailea', - 'users_delete_named' => 'Delete user :userName', + 'users_delete_named' => ':userName erabiltzailea ezabatu', 'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.', 'users_delete_confirm' => 'Are you sure you want to delete this user?', 'users_migrate_ownership' => 'Migrate Ownership', @@ -192,7 +192,7 @@ return [ 'users_none_selected' => 'Erabiltzailerik ez duzu aukeratu', 'users_edit' => 'Erabiltzaile editatu', 'users_edit_profile' => 'Editatu profila', - 'users_avatar' => 'User Avatar', + 'users_avatar' => 'Erabiltzaile avatarra', 'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.', 'users_preferred_language' => 'Hobetsitako hizkuntza', 'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.', @@ -202,10 +202,10 @@ return [ 'users_social_disconnect' => 'Deskonektatu kontua', 'users_social_connected' => ':socialAccount account was successfully attached to your profile.', 'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.', - 'users_api_tokens' => 'API Tokens', + 'users_api_tokens' => 'API tokenak', 'users_api_tokens_none' => 'No API tokens have been created for this user', - 'users_api_tokens_create' => 'Create Token', - 'users_api_tokens_expires' => 'Expires', + 'users_api_tokens_create' => 'Sortu Tokena', + 'users_api_tokens_expires' => 'Iraungita', 'users_api_tokens_docs' => 'API dokumentazioa', 'users_mfa' => 'Multi-Factor Authentication', 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', @@ -213,7 +213,7 @@ return [ 'users_mfa_configure' => 'Configure Methods', // API Tokens - 'user_api_token_create' => 'Create API Token', + 'user_api_token_create' => 'Sortu Tokena', 'user_api_token_name' => 'Izena', 'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.', 'user_api_token_expiry' => 'Iraungitze data', diff --git a/resources/lang/id/auth.php b/resources/lang/id/auth.php index 423c92ff6..d76a05a01 100644 --- a/resources/lang/id/auth.php +++ b/resources/lang/id/auth.php @@ -21,7 +21,7 @@ return [ 'email' => 'Email', 'password' => 'Kata Sandi', 'password_confirm' => 'Konfirmasi Kata Sandi', - 'password_hint' => 'Must be at least 8 characters', + 'password_hint' => 'Harus minimal 8 karakter', 'forgot_password' => 'Lupa Password?', 'remember_me' => 'Ingat saya', 'ldap_email_hint' => 'Harap masukkan email yang akan digunakan untuk akun ini.', @@ -77,13 +77,13 @@ return [ 'mfa_setup' => 'Setup Multi-Factor Authentication', 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'mfa_setup_configured' => 'Already configured', - 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_reconfigure' => 'Konfigurasi ulang', 'mfa_setup_remove_confirmation' => 'Apakah Anda yakin ingin menghapus metode autentikasi multi-faktor ini?', 'mfa_setup_action' => 'Setup', 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', - 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_title' => 'Aplikasi Seluler', 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', - 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_title' => 'Kode Cadangan', 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', @@ -100,9 +100,9 @@ return [ 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', 'mfa_verify_no_methods' => 'No Methods Configured', 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', - 'mfa_verify_use_totp' => 'Verify using a mobile app', - 'mfa_verify_use_backup_codes' => 'Verify using a backup code', - 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_use_totp' => 'Verifikasi menggunakan aplikasi seluler', + 'mfa_verify_use_backup_codes' => 'Verifikasi menggunakan kode cadangan', + 'mfa_verify_backup_code' => 'Kode Cadangan', 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', diff --git a/resources/lang/id/editor.php b/resources/lang/id/editor.php index 827120292..88204b416 100644 --- a/resources/lang/id/editor.php +++ b/resources/lang/id/editor.php @@ -7,27 +7,27 @@ */ return [ // General editor terms - 'general' => 'General', - 'advanced' => 'Advanced', - 'none' => 'None', - 'cancel' => 'Cancel', - 'save' => 'Save', - 'close' => 'Close', + 'general' => 'Umum', + 'advanced' => 'Lanjutan', + 'none' => 'Tidak Ada', + 'cancel' => 'Batal', + 'save' => 'Simpan', + 'close' => 'Tutup', 'undo' => 'Undo', - 'redo' => 'Redo', - 'left' => 'Left', - 'center' => 'Center', - 'right' => 'Right', - 'top' => 'Top', - 'middle' => 'Middle', - 'bottom' => 'Bottom', - 'width' => 'Width', - 'height' => 'Height', - 'More' => 'More', - 'select' => 'Select...', + 'redo' => 'Ulangi', + 'left' => 'Kiri', + 'center' => 'Tengah', + 'right' => 'Kanan', + 'top' => 'Atas', + 'middle' => 'Sedang', + 'bottom' => 'Bawah', + 'width' => 'Lebar', + 'height' => 'Tinggi', + 'More' => 'Lebih Banyak', + 'select' => 'Pilih...', // Toolbar - 'formats' => 'Formats', + 'formats' => 'Format', 'header_large' => 'Large Header', 'header_medium' => 'Medium Header', 'header_small' => 'Small Header', @@ -37,12 +37,12 @@ return [ 'inline_code' => 'Inline code', 'callouts' => 'Callouts', 'callout_information' => 'Information', - 'callout_success' => 'Success', - 'callout_warning' => 'Warning', - 'callout_danger' => 'Danger', - 'bold' => 'Bold', + 'callout_success' => 'Sukses', + 'callout_warning' => 'Peringatan', + 'callout_danger' => 'Bahaya', + 'bold' => 'Berani', 'italic' => 'Italic', - 'underline' => 'Underline', + 'underline' => 'Garis Bawah', 'strikethrough' => 'Strikethrough', 'superscript' => 'Superscript', 'subscript' => 'Subscript', diff --git a/resources/lang/id/validation.php b/resources/lang/id/validation.php index 36a3397f5..e1fcbc724 100644 --- a/resources/lang/id/validation.php +++ b/resources/lang/id/validation.php @@ -15,7 +15,7 @@ return [ 'alpha_dash' => ':attribute hanya boleh berisi huruf, angka, tanda hubung, dan garis bawah.', 'alpha_num' => ':attribute hanya boleh berisi huruf dan angka.', 'array' => ':attribute harus berupa larik.', - 'backup_codes' => 'The provided code is not valid or has already been used.', + 'backup_codes' => 'Kode yang diberikan tidak valid atau telah digunakan.', 'before' => ':attribute harus tanggal sebelum :date.', 'between' => [ 'numeric' => ':attribute harus di antara :min dan :max.', @@ -32,7 +32,7 @@ return [ 'digits_between' => ':attribute harus diantara :min dan :max digit.', 'email' => ':attrtibute Harus alamat e-mail yang valid.', 'ends_with' => ':attribute harus diakhiri dengan salah satu dari berikut ini: :values', - 'file' => 'The :attribute must be provided as a valid file.', + 'file' => ':attribute harus diberikan sebagai file yang valid.', 'filled' => ':attribute bidang diperlukan.', 'gt' => [ 'numeric' => ':attribute harus lebih besar dari :value.', @@ -100,7 +100,7 @@ return [ ], 'string' => ':attribute harus berupa string.', 'timezone' => ':attribute harus menjadi zona yang valid.', - 'totp' => 'The provided code is not valid or has expired.', + 'totp' => 'Kode yang diberikan tidak valid atau telah kedaluwarsa.', 'unique' => ':attribute sudah diambil.', 'url' => ':attribute format tidak valid.', 'uploaded' => 'Berkas tidak dapat diunggah. Server mungkin tidak menerima berkas dengan ukuran ini.', diff --git a/resources/lang/nl/settings.php b/resources/lang/nl/settings.php index 0326b290a..998ac1a8c 100644 --- a/resources/lang/nl/settings.php +++ b/resources/lang/nl/settings.php @@ -10,8 +10,8 @@ return [ 'settings' => 'Instellingen', 'settings_save' => 'Instellingen opslaan', 'settings_save_success' => 'Instellingen Opgeslagen', - 'system_version' => 'System Version', - 'categories' => 'Categories', + 'system_version' => 'Systeem versie', + 'categories' => 'Categorieën', // App Settings 'app_customization' => 'Aanpassingen', diff --git a/resources/lang/pt/activities.php b/resources/lang/pt/activities.php index b85e39421..733a3cadc 100644 --- a/resources/lang/pt/activities.php +++ b/resources/lang/pt/activities.php @@ -13,7 +13,7 @@ return [ 'page_delete' => 'página eliminada', 'page_delete_notification' => 'Página excluída com sucesso.', 'page_restore' => 'página restaurada', - 'page_restore_notification' => 'Imagem restaurada com sucesso', + 'page_restore_notification' => 'Página restaurada com sucesso', 'page_move' => 'página movida', // Chapters @@ -48,11 +48,11 @@ return [ 'favourite_remove_notification' => '":name" foi removido dos seus favoritos', // MFA - 'mfa_setup_method_notification' => 'Método de múltiplos-fatores configurado com sucesso', - 'mfa_remove_method_notification' => 'Método de múltiplos-fatores removido com sucesso', + 'mfa_setup_method_notification' => 'Método de autenticação por múltiplos-fatores configurado com sucesso', + 'mfa_remove_method_notification' => 'Método de autenticação por múltiplos-fatores removido com sucesso', // Webhooks - 'webhook_create' => 'criar webhook', + 'webhook_create' => 'webhook criado', 'webhook_create_notification' => 'Webhook criado com sucesso', 'webhook_update' => 'atualizar um webhook', 'webhook_update_notification' => 'Webhook criado com sucesso', diff --git a/resources/lang/pt/common.php b/resources/lang/pt/common.php index e64d3e9b9..137f389b3 100644 --- a/resources/lang/pt/common.php +++ b/resources/lang/pt/common.php @@ -19,7 +19,7 @@ return [ 'description' => 'Descrição', 'role' => 'Cargo', 'cover_image' => 'Imagem de capa', - 'cover_image_description' => 'Esta imagem deve ser aproximadamente 440x250px.', + 'cover_image_description' => 'Esta imagem deve ter aproximadamente 440x250px.', // Actions 'actions' => 'Ações', diff --git a/resources/lang/pt/editor.php b/resources/lang/pt/editor.php index 81c33b201..3a6119681 100644 --- a/resources/lang/pt/editor.php +++ b/resources/lang/pt/editor.php @@ -31,7 +31,7 @@ return [ 'header_large' => 'Cabeçalho grande', 'header_medium' => 'Cabeçalho médio', 'header_small' => 'Cabeçalho pequeno', - 'header_tiny' => 'Cabeçalho pequeno', + 'header_tiny' => 'Cabeçalho minúsculo', 'paragraph' => 'Parágrafo', 'blockquote' => 'Citação', 'inline_code' => 'Código embutido', diff --git a/resources/lang/ru/editor.php b/resources/lang/ru/editor.php index 6f8d9c755..cda25b05f 100644 --- a/resources/lang/ru/editor.php +++ b/resources/lang/ru/editor.php @@ -24,7 +24,7 @@ return [ 'width' => 'Ширина', 'height' => 'Высота', 'More' => 'Еще', - 'select' => 'Select...', + 'select' => 'Выбрать...', // Toolbar 'formats' => 'Форматы', @@ -95,8 +95,8 @@ return [ 'cell_type_cell' => 'Ячейка', 'cell_scope' => 'Scope', 'cell_type_header' => 'Заголовок ячейки', - 'merge_cells' => 'Merge cells', - 'split_cell' => 'Split cell', + 'merge_cells' => 'Объединить ячейки', + 'split_cell' => 'Разделить ячейку', 'table_row_group' => 'Объединить строки', 'table_column_group' => 'Объединить столбцы', 'horizontal_align' => 'Выровнять по горизонтали', @@ -124,16 +124,16 @@ return [ 'caption' => 'Подпись', 'show_caption' => 'Показать подпись', 'constrain' => 'Сохранять пропорции', - 'cell_border_solid' => 'Solid', - 'cell_border_dotted' => 'Dotted', - 'cell_border_dashed' => 'Dashed', - 'cell_border_double' => 'Double', + 'cell_border_solid' => 'Сплошная', + 'cell_border_dotted' => 'Точками', + 'cell_border_dashed' => 'Пунктирная', + 'cell_border_double' => 'Двойная сплошная', 'cell_border_groove' => 'Groove', 'cell_border_ridge' => 'Ridge', 'cell_border_inset' => 'Inset', 'cell_border_outset' => 'Outset', - 'cell_border_none' => 'None', - 'cell_border_hidden' => 'Hidden', + 'cell_border_none' => 'Нет', + 'cell_border_hidden' => 'Прозрачная', // Images, links, details/summary & embed 'source' => 'Источник', @@ -154,7 +154,7 @@ return [ 'toggle_label' => 'Метка', // About view - 'about' => 'About the editor', + 'about' => 'О редакторе', 'about_title' => 'О редакторе WYSIWYG', 'editor_license' => 'Лицензия редактора и авторские права', 'editor_tiny_license' => 'Этот редактор собран с помощью :tinyLink, который предоставляется под лицензией LGPL v2.1.', diff --git a/resources/lang/ru/settings.php b/resources/lang/ru/settings.php index b23b7e97c..4e48e8cc4 100755 --- a/resources/lang/ru/settings.php +++ b/resources/lang/ru/settings.php @@ -10,8 +10,8 @@ return [ 'settings' => 'Настройки', 'settings_save' => 'Сохранить настройки', 'settings_save_success' => 'Настройки сохранены', - 'system_version' => 'System Version', - 'categories' => 'Categories', + 'system_version' => 'Версия системы', + 'categories' => 'Категории', // App Settings 'app_customization' => 'Настройки', diff --git a/resources/lang/ru/validation.php b/resources/lang/ru/validation.php index a0dec2b56..88bd9a41f 100644 --- a/resources/lang/ru/validation.php +++ b/resources/lang/ru/validation.php @@ -32,7 +32,7 @@ return [ 'digits_between' => ':attribute должен иметь от :min до :max цифр.', 'email' => ':attribute должен быть корректным email адресом.', 'ends_with' => ':attribute должен заканчиваться одним из следующих: :values', - 'file' => 'The :attribute must be provided as a valid file.', + 'file' => ':attribute должен быть указан как допустимый файл.', 'filled' => ':attribute поле необходимо.', 'gt' => [ 'numeric' => 'Значение :attribute должно быть больше чем :value.', diff --git a/resources/lang/zh_CN/editor.php b/resources/lang/zh_CN/editor.php index 81d46a059..80f6335e2 100644 --- a/resources/lang/zh_CN/editor.php +++ b/resources/lang/zh_CN/editor.php @@ -24,7 +24,7 @@ return [ 'width' => '宽度', 'height' => '高度', 'More' => '更多', - 'select' => 'Select...', + 'select' => '选择...', // Toolbar 'formats' => '格式', @@ -53,10 +53,10 @@ return [ 'align_left' => '左对齐', 'align_center' => '居中', 'align_right' => '右对齐', - 'align_justify' => 'Justify', + 'align_justify' => '两端对齐', 'list_bullet' => '无序列表', 'list_numbered' => '有序列表', - 'list_task' => 'Task list', + 'list_task' => '任务列表', 'indent_increase' => '增加缩进', 'indent_decrease' => '减少缩进', 'table' => '表格', @@ -93,10 +93,10 @@ return [ 'cell_properties_title' => '单元格属性', 'cell_type' => '单元格类型', 'cell_type_cell' => '单元格', - 'cell_scope' => 'Scope', + 'cell_scope' => '范围', 'cell_type_header' => '表头', - 'merge_cells' => 'Merge cells', - 'split_cell' => 'Split cell', + 'merge_cells' => '合并单元格', + 'split_cell' => '拆分单元格', 'table_row_group' => '按行分组', 'table_column_group' => '按列分组', 'horizontal_align' => '水平对齐', @@ -124,16 +124,16 @@ return [ 'caption' => '标题', 'show_caption' => '显示标题', 'constrain' => '保持宽高比', - 'cell_border_solid' => 'Solid', - 'cell_border_dotted' => 'Dotted', - 'cell_border_dashed' => 'Dashed', - 'cell_border_double' => 'Double', - 'cell_border_groove' => 'Groove', - 'cell_border_ridge' => 'Ridge', - 'cell_border_inset' => 'Inset', - 'cell_border_outset' => 'Outset', - 'cell_border_none' => 'None', - 'cell_border_hidden' => 'Hidden', + 'cell_border_solid' => '实线', + 'cell_border_dotted' => '点虚线', + 'cell_border_dashed' => '短虚线', + 'cell_border_double' => '双实线', + 'cell_border_groove' => '浮入', + 'cell_border_ridge' => '浮出', + 'cell_border_inset' => '陷入', + 'cell_border_outset' => '突出', + 'cell_border_none' => '无边框', + 'cell_border_hidden' => '隐藏边框', // Images, links, details/summary & embed 'source' => '来源', @@ -154,7 +154,7 @@ return [ 'toggle_label' => '切换标签', // About view - 'about' => 'About the editor', + 'about' => '关于编辑器', 'about_title' => '关于所见即所得(WYSIWYG)编辑器', 'editor_license' => '编辑器许可证与版权信息', 'editor_tiny_license' => '此编辑器是在 LGPL v2.1 许可证下使用 :tinyLink 构建的。', diff --git a/resources/lang/zh_CN/settings.php b/resources/lang/zh_CN/settings.php index dacb82d83..c59781fcf 100755 --- a/resources/lang/zh_CN/settings.php +++ b/resources/lang/zh_CN/settings.php @@ -10,8 +10,8 @@ return [ 'settings' => '设置', 'settings_save' => '保存设置', 'settings_save_success' => '设置已保存', - 'system_version' => 'System Version', - 'categories' => 'Categories', + 'system_version' => '系统版本', + 'categories' => '类别', // App Settings 'app_customization' => '定制', @@ -121,7 +121,7 @@ return [ 'audit_table_user' => '用户', 'audit_table_event' => '事件', 'audit_table_related' => '相关项目或详细信息', - 'audit_table_ip' => 'IP地址', + 'audit_table_ip' => 'IP 地址', 'audit_table_date' => '活动日期', 'audit_date_from' => '日期范围从', 'audit_date_to' => '日期范围至', From 7dc80a9e14855a56a1d5c18ccb1a92a82b1e6250 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 17 Apr 2022 14:13:14 +0100 Subject: [PATCH 14/41] Updated editor setting to reflect "Default editor" --- resources/lang/en/settings.php | 4 ++-- resources/views/settings/customization.blade.php | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 7461c9d4e..d5bf87ba8 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -27,8 +27,8 @@ return [ 'app_secure_images' => 'Higher Security Image Uploads', 'app_secure_images_toggle' => 'Enable higher security image uploads', 'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.', - 'app_editor' => 'Page Editor', - 'app_editor_desc' => 'Select which editor will be used by all users to edit pages.', + 'app_default_editor' => 'Default Page Editor', + 'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.', 'app_custom_html' => 'Custom HTML Head Content', 'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the section of every page. This is handy for overriding styles or adding analytics code.', 'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.', diff --git a/resources/views/settings/customization.blade.php b/resources/views/settings/customization.blade.php index 2bc3531d7..b7be95b4a 100644 --- a/resources/views/settings/customization.blade.php +++ b/resources/views/settings/customization.blade.php @@ -23,12 +23,12 @@ -
+
- -

{{ trans('settings.app_editor_desc') }}

+ +

{{ trans('settings.app_default_editor_desc') }}

-
+
From e8e38f1f7bcc7190a2fc8298f114cd0443ee55f5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 17 Apr 2022 14:33:06 +0100 Subject: [PATCH 15/41] Added an 'editor-change' role permission --- ...1741_add_editor_change_role_permission.php | 40 +++++++++++++++++++ resources/lang/en/settings.php | 1 + .../views/settings/roles/parts/form.blade.php | 1 + 3 files changed, 42 insertions(+) create mode 100644 database/migrations/2022_04_17_101741_add_editor_change_role_permission.php diff --git a/database/migrations/2022_04_17_101741_add_editor_change_role_permission.php b/database/migrations/2022_04_17_101741_add_editor_change_role_permission.php new file mode 100644 index 000000000..a9f7f8378 --- /dev/null +++ b/database/migrations/2022_04_17_101741_add_editor_change_role_permission.php @@ -0,0 +1,40 @@ +where('system_name', '=', 'admin')->first()->id; + + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => 'editor-change', + 'display_name' => 'Change page editor', + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), + ]); + + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::table('role_permissions')->where('name', '=', 'editor-change')->delete(); + } +} diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index d5bf87ba8..af2dcc1e1 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -152,6 +152,7 @@ return [ 'role_access_api' => 'Access system API', 'role_manage_settings' => 'Manage app settings', 'role_export_content' => 'Export content', + 'role_editor_change' => 'Change page editor', 'role_asset' => 'Asset Permissions', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', diff --git a/resources/views/settings/roles/parts/form.blade.php b/resources/views/settings/roles/parts/form.blade.php index a15117e5e..aeaa39a6d 100644 --- a/resources/views/settings/roles/parts/form.blade.php +++ b/resources/views/settings/roles/parts/form.blade.php @@ -37,6 +37,7 @@
@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])
@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])
@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])
+
@include('settings.roles.parts.checkbox', ['permission' => 'editor-change', 'label' => trans('settings.role_editor_change')])
@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])
From 0cc215f8c3ccf75af60d404b385f825d41ad684a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 17 Apr 2022 15:01:29 +0100 Subject: [PATCH 16/41] Added editor type change button --- app/Http/Controllers/PageController.php | 2 ++ resources/icons/swap-horizontal.svg | 1 + resources/lang/en/entities.php | 2 ++ resources/views/pages/parts/form.blade.php | 12 ++++++++++-- 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 resources/icons/swap-horizontal.svg diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index eecb6a6e7..ec48e63f5 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -97,6 +97,7 @@ class PageController extends Controller 'isDraft' => true, 'draftsEnabled' => $draftsEnabled, 'templates' => $templates, + 'editor' => setting('app-editor') === 'wysiwyg' ? 'wysiwyg' : 'markdown', ]); } @@ -224,6 +225,7 @@ class PageController extends Controller 'current' => $page, 'draftsEnabled' => $draftsEnabled, 'templates' => $templates, + 'editor' => setting('app-editor') === 'wysiwyg' ? 'wysiwyg' : 'markdown', ]); } diff --git a/resources/icons/swap-horizontal.svg b/resources/icons/swap-horizontal.svg new file mode 100644 index 000000000..7bd25dd7e --- /dev/null +++ b/resources/icons/swap-horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 4e4bbccd3..8de379bbb 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -196,6 +196,8 @@ return [ 'pages_edit_draft_save_at' => 'Draft saved at ', 'pages_edit_delete_draft' => 'Delete Draft', 'pages_edit_discard_draft' => 'Discard Draft', + 'pages_edit_switch_to_markdown' => 'Switch to Markdown editor', + 'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG editor', 'pages_edit_set_changelog' => 'Set Changelog', 'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made', 'pages_edit_enter_changelog' => 'Enter Changelog', diff --git a/resources/views/pages/parts/form.blade.php b/resources/views/pages/parts/form.blade.php index 01f68a6c5..6646d508f 100644 --- a/resources/views/pages/parts/form.blade.php +++ b/resources/views/pages/parts/form.blade.php @@ -42,6 +42,14 @@
  • + @if(userCan('editor-change')) +
  • + +
  • + @endif
    @@ -78,12 +86,12 @@
    {{--WYSIWYG Editor--}} - @if(setting('app-editor') === 'wysiwyg') + @if($editor === 'wysiwyg') @include('pages.parts.wysiwyg-editor', ['model' => $model]) @endif {{--Markdown Editor--}} - @if(setting('app-editor') === 'markdown') + @if($editor === 'markdown') @include('pages.parts.markdown-editor', ['model' => $model]) @endif From 956eb1308fe63b0d1b3cc3765a2f2b77d2dc8396 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 17 Apr 2022 23:01:14 +0100 Subject: [PATCH 17/41] Aligned page edit controller method data usage Extracted page editor view data gathering to its own class for alignment. Updated the data used in views as part of the process to use view-specific variables instead of custom attributes added to models. Also moved tinymce library loading so it's not loaded when not using the wysiwyg editor. --- app/Entities/Tools/PageEditActivity.php | 2 +- app/Entities/Tools/PageEditorData.php | 67 +++++++++++++++++++ app/Http/Controllers/PageController.php | 50 +++----------- resources/views/pages/edit.blade.php | 8 +-- resources/views/pages/parts/form.blade.php | 17 +++-- .../pages/parts/wysiwyg-editor.blade.php | 4 ++ 6 files changed, 90 insertions(+), 58 deletions(-) create mode 100644 app/Entities/Tools/PageEditorData.php diff --git a/app/Entities/Tools/PageEditActivity.php b/app/Entities/Tools/PageEditActivity.php index 9981a6ed7..2672de941 100644 --- a/app/Entities/Tools/PageEditActivity.php +++ b/app/Entities/Tools/PageEditActivity.php @@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Builder; class PageEditActivity { - protected $page; + protected Page $page; /** * PageEditActivity constructor. diff --git a/app/Entities/Tools/PageEditorData.php b/app/Entities/Tools/PageEditorData.php new file mode 100644 index 000000000..a6818839d --- /dev/null +++ b/app/Entities/Tools/PageEditorData.php @@ -0,0 +1,67 @@ +page = $page; + $this->pageRepo = $pageRepo; + $this->viewData = $this->build(); + } + + public function getViewData(): array + { + return $this->viewData; + } + + public function getWarnings(): array + { + return $this->warnings; + } + + protected function build(): array + { + $page = clone $this->page; + $isDraft = boolval($this->page->draft); + $templates = $this->pageRepo->getTemplates(10); + $draftsEnabled = auth()->check(); + + $isDraftRevision = false; + $this->warnings = []; + $editActivity = new PageEditActivity($page); + + if ($editActivity->hasActiveEditing()) { + $this->warnings[] = $editActivity->activeEditingMessage(); + } + + // Check for a current draft version for this user + $userDraft = $this->pageRepo->getUserDraft($page); + if ($userDraft !== null) { + $page->forceFill($userDraft->only(['name', 'html', 'markdown'])); + $isDraftRevision = true; + $this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft); + } + + return [ + 'page' => $page, + 'book' => $page->book, + 'isDraft' => $isDraft, + 'isDraftRevision' => $isDraftRevision, + 'draftsEnabled' => $draftsEnabled, + 'templates' => $templates, + 'editor' => setting('app-editor') === 'wysiwyg' ? 'wysiwyg' : 'markdown', + ]; + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index ec48e63f5..232c6b034 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -10,6 +10,7 @@ use BookStack\Entities\Tools\Cloner; use BookStack\Entities\Tools\NextPreviousContentLocator; use BookStack\Entities\Tools\PageContent; use BookStack\Entities\Tools\PageEditActivity; +use BookStack\Entities\Tools\PageEditorData; use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\PermissionsException; @@ -21,7 +22,7 @@ use Throwable; class PageController extends Controller { - protected $pageRepo; + protected PageRepo $pageRepo; /** * PageController constructor. @@ -86,19 +87,11 @@ class PageController extends Controller { $draft = $this->pageRepo->getById($pageId); $this->checkOwnablePermission('page-create', $draft->getParent()); + + $editorData = new PageEditorData($draft, $this->pageRepo); $this->setPageTitle(trans('entities.pages_edit_draft')); - $draftsEnabled = $this->isSignedIn(); - $templates = $this->pageRepo->getTemplates(10); - - return view('pages.edit', [ - 'page' => $draft, - 'book' => $draft->book, - 'isDraft' => true, - 'draftsEnabled' => $draftsEnabled, - 'templates' => $templates, - 'editor' => setting('app-editor') === 'wysiwyg' ? 'wysiwyg' : 'markdown', - ]); + return view('pages.edit', $editorData->getViewData()); } /** @@ -194,39 +187,14 @@ class PageController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-update', $page); - $page->isDraft = false; - $editActivity = new PageEditActivity($page); - - // Check for active editing - $warnings = []; - if ($editActivity->hasActiveEditing()) { - $warnings[] = $editActivity->activeEditingMessage(); + $editorData = new PageEditorData($page, $this->pageRepo); + if ($editorData->getWarnings()) { + $this->showWarningNotification(implode("\n", $editorData->getWarnings())); } - // Check for a current draft version for this user - $userDraft = $this->pageRepo->getUserDraft($page); - if ($userDraft !== null) { - $page->forceFill($userDraft->only(['name', 'html', 'markdown'])); - $page->isDraft = true; - $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft); - } - - if (count($warnings) > 0) { - $this->showWarningNotification(implode("\n", $warnings)); - } - - $templates = $this->pageRepo->getTemplates(10); - $draftsEnabled = $this->isSignedIn(); $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()])); - return view('pages.edit', [ - 'page' => $page, - 'book' => $page->book, - 'current' => $page, - 'draftsEnabled' => $draftsEnabled, - 'templates' => $templates, - 'editor' => setting('app-editor') === 'wysiwyg' ? 'wysiwyg' : 'markdown', - ]); + return view('pages.edit', $editorData->getViewData()); } /** diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php index 30158e852..de7c82d21 100644 --- a/resources/views/pages/edit.blade.php +++ b/resources/views/pages/edit.blade.php @@ -1,9 +1,5 @@ @extends('layouts.base') -@section('head') - -@stop - @section('body-class', 'flexbox') @section('content') @@ -12,9 +8,7 @@
    {{ csrf_field() }} - @if(!isset($isDraft)) - - @endif + @if($isDraft) {{ method_field('PUT') }} @endif @include('pages.parts.form', ['model' => $page]) @include('pages.parts.editor-toolbox')
    diff --git a/resources/views/pages/parts/form.blade.php b/resources/views/pages/parts/form.blade.php index 6646d508f..3507705aa 100644 --- a/resources/views/pages/parts/form.blade.php +++ b/resources/views/pages/parts/form.blade.php @@ -8,8 +8,8 @@ @endif option:page-editor:editor-type="{{ setting('app-editor') }}" option:page-editor:page-id="{{ $model->id ?? '0' }}" - option:page-editor:page-new-draft="{{ ($model->draft ?? false) ? 'true' : 'false' }}" - option:page-editor:draft-text="{{ ($model->draft || $model->isDraft) ? trans('entities.pages_editing_draft') : trans('entities.pages_editing_page') }}" + option:page-editor:page-new-draft="{{ $isDraft ? 'true' : 'false' }}" + option:page-editor:draft-text="{{ ($isDraft || $isDraftRevision) ? trans('entities.pages_editing_draft') : trans('entities.pages_editing_page') }}" 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') }}" @@ -20,7 +20,7 @@
    @@ -34,20 +34,19 @@
  • - @if ($model->draft) + @if($isDraft)
  • @icon('delete'){{ trans('entities.pages_edit_delete_draft') }}
  • @endif -
  • +
  • @if(userCan('editor-change'))
  • - + + @icon('swap-horizontal'){{ $editor === 'wysiwyg' ? trans('entities.pages_edit_switch_to_markdown') : trans('entities.pages_edit_switch_to_wysiwyg') }} +
  • @endif diff --git a/resources/views/pages/parts/wysiwyg-editor.blade.php b/resources/views/pages/parts/wysiwyg-editor.blade.php index 29a4b6532..d8ca74939 100644 --- a/resources/views/pages/parts/wysiwyg-editor.blade.php +++ b/resources/views/pages/parts/wysiwyg-editor.blade.php @@ -1,3 +1,7 @@ +@push('head') + +@endpush +
    Date: Mon, 18 Apr 2022 17:39:28 +0100 Subject: [PATCH 18/41] Added core editor switching functionality --- app/Entities/Models/PageRevision.php | 2 +- app/Entities/Repos/PageRepo.php | 9 +++- .../Tools/Markdown/HtmlToMarkdown.php | 2 +- .../Tools/Markdown/MarkdownToHtml.php | 37 ++++++++++++++ app/Entities/Tools/PageContent.php | 31 ++---------- app/Entities/Tools/PageEditorData.php | 48 ++++++++++++++++++- app/Http/Controllers/PageController.php | 8 ++-- resources/views/pages/edit.blade.php | 2 +- resources/views/pages/parts/form.blade.php | 2 +- 9 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 app/Entities/Tools/Markdown/MarkdownToHtml.php diff --git a/app/Entities/Models/PageRevision.php b/app/Entities/Models/PageRevision.php index 800e5e7f2..55b2ffbe8 100644 --- a/app/Entities/Models/PageRevision.php +++ b/app/Entities/Models/PageRevision.php @@ -27,7 +27,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; */ class PageRevision extends Model { - protected $fillable = ['name', 'html', 'text', 'markdown', 'summary']; + protected $fillable = ['name', 'text', 'summary']; protected $hidden = ['html', 'markdown', 'restricted', 'text']; /** diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index 828c4572f..d47573a6c 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -260,10 +260,15 @@ class PageRepo return $page; } - // Otherwise save the data to a revision + // Otherwise, save the data to a revision $draft = $this->getPageRevisionToUpdate($page); $draft->fill($input); - if (setting('app-editor') !== 'markdown') { + + if (!empty($input['markdown'])) { + $draft->markdown = $input['markdown']; + $draft->html = ''; + } else { + $draft->html = $input['html']; $draft->markdown = ''; } diff --git a/app/Entities/Tools/Markdown/HtmlToMarkdown.php b/app/Entities/Tools/Markdown/HtmlToMarkdown.php index 51366705c..5c7b388ea 100644 --- a/app/Entities/Tools/Markdown/HtmlToMarkdown.php +++ b/app/Entities/Tools/Markdown/HtmlToMarkdown.php @@ -21,7 +21,7 @@ use League\HTMLToMarkdown\HtmlConverter; class HtmlToMarkdown { - protected $html; + protected string $html; public function __construct(string $html) { diff --git a/app/Entities/Tools/Markdown/MarkdownToHtml.php b/app/Entities/Tools/Markdown/MarkdownToHtml.php new file mode 100644 index 000000000..25413fb33 --- /dev/null +++ b/app/Entities/Tools/Markdown/MarkdownToHtml.php @@ -0,0 +1,37 @@ +markdown = $markdown; + } + + public function convert(): string + { + $environment = Environment::createCommonMarkEnvironment(); + $environment->addExtension(new TableExtension()); + $environment->addExtension(new TaskListExtension()); + $environment->addExtension(new CustomStrikeThroughExtension()); + $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment; + $converter = new CommonMarkConverter([], $environment); + + $environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10); + + return $converter->convertToHtml($this->markdown); + } + +} \ No newline at end of file diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index b1c750adb..ea6a185f1 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -3,11 +3,8 @@ namespace BookStack\Entities\Tools; use BookStack\Entities\Models\Page; -use BookStack\Entities\Tools\Markdown\CustomListItemRenderer; -use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension; +use BookStack\Entities\Tools\Markdown\MarkdownToHtml; use BookStack\Exceptions\ImageUploadException; -use BookStack\Facades\Theme; -use BookStack\Theming\ThemeEvents; use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageService; use BookStack\Util\HtmlContentFilter; @@ -17,15 +14,10 @@ use DOMNode; use DOMNodeList; use DOMXPath; use Illuminate\Support\Str; -use League\CommonMark\Block\Element\ListItem; -use League\CommonMark\CommonMarkConverter; -use League\CommonMark\Environment; -use League\CommonMark\Extension\Table\TableExtension; -use League\CommonMark\Extension\TaskList\TaskListExtension; class PageContent { - protected $page; + protected Page $page; /** * PageContent constructor. @@ -53,28 +45,11 @@ class PageContent { $markdown = $this->extractBase64ImagesFromMarkdown($markdown); $this->page->markdown = $markdown; - $html = $this->markdownToHtml($markdown); + $html = (new MarkdownToHtml($markdown))->convert(); $this->page->html = $this->formatHtml($html); $this->page->text = $this->toPlainText(); } - /** - * Convert the given Markdown content to a HTML string. - */ - protected function markdownToHtml(string $markdown): string - { - $environment = Environment::createCommonMarkEnvironment(); - $environment->addExtension(new TableExtension()); - $environment->addExtension(new TaskListExtension()); - $environment->addExtension(new CustomStrikeThroughExtension()); - $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment; - $converter = new CommonMarkConverter([], $environment); - - $environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10); - - return $converter->convertToHtml($markdown); - } - /** * Convert all base64 image data to saved images. */ diff --git a/app/Entities/Tools/PageEditorData.php b/app/Entities/Tools/PageEditorData.php index a6818839d..3e1164175 100644 --- a/app/Entities/Tools/PageEditorData.php +++ b/app/Entities/Tools/PageEditorData.php @@ -4,19 +4,24 @@ namespace BookStack\Entities\Tools; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\PageRepo; +use BookStack\Entities\Tools\Markdown\HtmlToMarkdown; +use BookStack\Entities\Tools\Markdown\MarkdownToHtml; class PageEditorData { protected Page $page; protected PageRepo $pageRepo; + protected string $requestedEditor; protected array $viewData; protected array $warnings; - public function __construct(Page $page, PageRepo $pageRepo) + public function __construct(Page $page, PageRepo $pageRepo, string $requestedEditor) { $this->page = $page; $this->pageRepo = $pageRepo; + $this->requestedEditor = $requestedEditor; + $this->viewData = $this->build(); } @@ -53,6 +58,9 @@ class PageEditorData $this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft); } + $editorType = $this->getEditorType($page); + $this->updateContentForEditor($page, $editorType); + return [ 'page' => $page, 'book' => $page->book, @@ -60,8 +68,44 @@ class PageEditorData 'isDraftRevision' => $isDraftRevision, 'draftsEnabled' => $draftsEnabled, 'templates' => $templates, - 'editor' => setting('app-editor') === 'wysiwyg' ? 'wysiwyg' : 'markdown', + 'editor' => $editorType, ]; } + protected function updateContentForEditor(Page $page, string $editorType): void + { + $isHtml = !empty($page->html) && empty($page->markdown); + + // HTML to markdown-clean conversion + if ($editorType === 'markdown' && $isHtml && $this->requestedEditor === 'markdown-clean') { + $page->markdown = (new HtmlToMarkdown($page->html))->convert(); + } + + // Markdown to HTML conversion if we don't have HTML + if ($editorType === 'wysiwyg' && !$isHtml) { + $page->html = (new MarkdownToHtml($page->markdown))->convert(); + } + } + + /** + * Get the type of editor to show for editing the given page. + * Defaults based upon the current content of the page otherwise will fall back + * to system default but will take a requested type (if provided) if permissions allow. + */ + protected function getEditorType(Page $page): string + { + $emptyPage = empty($page->html) && empty($page->markdown); + $pageType = (!empty($page->html) && empty($page->markdown)) ? 'wysiwyg' : 'markdown'; + $systemDefault = setting('app-editor') === 'wysiwyg' ? 'wysiwyg' : 'markdown'; + $editorType = $emptyPage ? $systemDefault : $pageType; + + // Use requested editor if valid and if we have permission + $requestedType = explode('-', $this->requestedEditor)[0]; + if (($requestedType === 'markdown' || $requestedType === 'wysiwyg') && userCan('editor-change')) { + $editorType = $requestedType; + } + + return $editorType; + } + } \ No newline at end of file diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 232c6b034..268dce057 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -83,12 +83,12 @@ class PageController extends Controller * * @throws NotFoundException */ - public function editDraft(string $bookSlug, int $pageId) + public function editDraft(Request $request, string $bookSlug, int $pageId) { $draft = $this->pageRepo->getById($pageId); $this->checkOwnablePermission('page-create', $draft->getParent()); - $editorData = new PageEditorData($draft, $this->pageRepo); + $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', '')); $this->setPageTitle(trans('entities.pages_edit_draft')); return view('pages.edit', $editorData->getViewData()); @@ -182,12 +182,12 @@ class PageController extends Controller * * @throws NotFoundException */ - public function edit(string $bookSlug, string $pageSlug) + public function edit(Request $request, string $bookSlug, string $pageSlug) { $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-update', $page); - $editorData = new PageEditorData($page, $this->pageRepo); + $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', '')); if ($editorData->getWarnings()) { $this->showWarningNotification(implode("\n", $editorData->getWarnings())); } diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php index de7c82d21..cd9635758 100644 --- a/resources/views/pages/edit.blade.php +++ b/resources/views/pages/edit.blade.php @@ -8,7 +8,7 @@
    {{ csrf_field() }} - @if($isDraft) {{ method_field('PUT') }} @endif + @if(!$isDraft) {{ method_field('PUT') }} @endif @include('pages.parts.form', ['model' => $page]) @include('pages.parts.editor-toolbox')
    diff --git a/resources/views/pages/parts/form.blade.php b/resources/views/pages/parts/form.blade.php index 3507705aa..ebad2bd72 100644 --- a/resources/views/pages/parts/form.blade.php +++ b/resources/views/pages/parts/form.blade.php @@ -6,7 +6,7 @@ @if($model->name === trans('entities.pages_initial_name')) option:page-editor:has-default-title="true" @endif - option:page-editor:editor-type="{{ setting('app-editor') }}" + option:page-editor:editor-type="{{ $editor }}" option:page-editor:page-id="{{ $model->id ?? '0' }}" option:page-editor:page-new-draft="{{ $isDraft ? 'true' : 'false' }}" option:page-editor:draft-text="{{ ($isDraft || $isDraftRevision) ? trans('entities.pages_editing_draft') : trans('entities.pages_editing_page') }}" From 214992650db16bceca439a60d45fb78035d38af1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 20 Apr 2022 14:03:47 +0100 Subject: [PATCH 19/41] Standardised dropdown list item styles, Extracted page editor toolbar - Updated all dropdown list item actions into three specific styles: icon-item, text-item & label-item. Allows a stronger structure while prevents mixing of styles as we were getting for header dropdown in dark mode. - Extracted out page editor top toolbar to its own view file & split editor switch options to different markdown options. --- resources/js/components/auto-suggest.js | 2 +- resources/js/components/code-editor.js | 2 +- resources/lang/en/entities.php | 6 +- resources/sass/_lists.scss | 46 +++++++--- resources/sass/_text.scss | 1 - .../views/attachments/manager-list.blade.php | 2 +- resources/views/comments/comment.blade.php | 7 +- resources/views/common/header.blade.php | 22 +++-- .../views/entities/export-menu.blade.php | 8 +- resources/views/entities/sort.blade.php | 2 +- .../mfa/parts/setup-method-row.blade.php | 2 +- .../pages/parts/editor-toolbar.blade.php | 86 +++++++++++++++++++ resources/views/pages/parts/form.blade.php | 60 +------------ resources/views/pages/revisions.blade.php | 10 ++- resources/views/settings/audit.blade.php | 4 +- .../settings/recycle-bin/index.blade.php | 6 +- 16 files changed, 171 insertions(+), 95 deletions(-) create mode 100644 resources/views/pages/parts/editor-toolbar.blade.php diff --git a/resources/js/components/auto-suggest.js b/resources/js/components/auto-suggest.js index 68de49b4a..d1c15c00a 100644 --- a/resources/js/components/auto-suggest.js +++ b/resources/js/components/auto-suggest.js @@ -131,7 +131,7 @@ class AutoSuggest { return this.hideSuggestions(); } - this.list.innerHTML = suggestions.map(value => `
  • `).join(''); + this.list.innerHTML = suggestions.map(value => `
  • `).join(''); this.list.style.display = 'block'; for (const button of this.list.querySelectorAll('button')) { button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this)); diff --git a/resources/js/components/code-editor.js b/resources/js/components/code-editor.js index f44de813d..4ee3531c5 100644 --- a/resources/js/components/code-editor.js +++ b/resources/js/components/code-editor.js @@ -96,7 +96,7 @@ class CodeEditor { this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0); this.historyList.innerHTML = historyKeys.map(key => { const localTime = (new Date(parseInt(key))).toLocaleTimeString(); - return `
  • `; + return `
  • `; }).join(''); } diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 8de379bbb..3921828d8 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -196,8 +196,10 @@ return [ 'pages_edit_draft_save_at' => 'Draft saved at ', 'pages_edit_delete_draft' => 'Delete Draft', 'pages_edit_discard_draft' => 'Discard Draft', - 'pages_edit_switch_to_markdown' => 'Switch to Markdown editor', - 'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG editor', + 'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor', + 'pages_edit_switch_to_markdown_clean' => '(Clean Markdown Content)', + 'pages_edit_switch_to_markdown_stable' => '(Stable Markdown Content)', + 'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor', 'pages_edit_set_changelog' => 'Set Changelog', 'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made', 'pages_edit_enter_changelog' => 'Enter Changelog', diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 9cff52972..26d12a25d 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -593,13 +593,22 @@ ul.pagination { li.active a { font-weight: 600; } - a, button { - display: block; - padding: $-xs $-m; + button { + width: 100%; + text-align: start; + } + li.border-bottom { + border-bottom: 1px solid #DDD; + } + li hr { + margin: $-xs 0; + } + .icon-item, .text-item, .label-item { + padding: 8px $-m; @include lightDark(color, #555, #eee); fill: currentColor; white-space: nowrap; - line-height: 1.6; + line-height: 1.4; cursor: pointer; &:hover, &:focus { text-decoration: none; @@ -616,15 +625,30 @@ ul.pagination { width: 16px; } } - button { - width: 100%; - text-align: start; + .text-item { + display: block; } - li.border-bottom { - border-bottom: 1px solid #DDD; + .label-item { + display: grid; + align-items: center; + grid-template-columns: auto min-content; + gap: $-m; } - li hr { - margin: $-xs 0; + .label-item > *:nth-child(2) { + opacity: 0.7; + &:hover { + opacity: 1; + } + } + .icon-item { + display: grid; + align-items: start; + grid-template-columns: 16px auto; + gap: $-m; + svg { + margin-inline-end: 0; + margin-block-start: 1px; + } } } diff --git a/resources/sass/_text.scss b/resources/sass/_text.scss index 884808bb4..51f315614 100644 --- a/resources/sass/_text.scss +++ b/resources/sass/_text.scss @@ -163,7 +163,6 @@ em, i, .italic { small, p.small, span.small, .text-small { font-size: 0.75rem; - @include lightDark(color, #5e5e5e, #999); } sup, .superscript { diff --git a/resources/views/attachments/manager-list.blade.php b/resources/views/attachments/manager-list.blade.php index b48fde9c0..ebb1c24aa 100644 --- a/resources/views/attachments/manager-list.blade.php +++ b/resources/views/attachments/manager-list.blade.php @@ -28,7 +28,7 @@ class="drag-card-action text-center text-neg">@icon('close')
    diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 9f4a12357..6189c65d4 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -31,7 +31,12 @@
    @endif diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php index d55f3ae2d..b5ac520c1 100644 --- a/resources/views/common/header.blade.php +++ b/resources/views/common/header.blade.php @@ -62,26 +62,36 @@
    diff --git a/resources/views/entities/export-menu.blade.php b/resources/views/entities/export-menu.blade.php index 2b0f5c19d..dd7231095 100644 --- a/resources/views/entities/export-menu.blade.php +++ b/resources/views/entities/export-menu.blade.php @@ -5,9 +5,9 @@ {{ trans('entities.export') }}
    diff --git a/resources/views/entities/sort.blade.php b/resources/views/entities/sort.blade.php index bf9087397..f81ed797f 100644 --- a/resources/views/entities/sort.blade.php +++ b/resources/views/entities/sort.blade.php @@ -16,7 +16,7 @@ diff --git a/resources/views/mfa/parts/setup-method-row.blade.php b/resources/views/mfa/parts/setup-method-row.blade.php index e195174c1..271ec1bf4 100644 --- a/resources/views/mfa/parts/setup-method-row.blade.php +++ b/resources/views/mfa/parts/setup-method-row.blade.php @@ -19,7 +19,7 @@
    {{ csrf_field() }} {{ method_field('delete') }} - +
    diff --git a/resources/views/pages/parts/editor-toolbar.blade.php b/resources/views/pages/parts/editor-toolbar.blade.php new file mode 100644 index 000000000..d7fb76c29 --- /dev/null +++ b/resources/views/pages/parts/editor-toolbar.blade.php @@ -0,0 +1,86 @@ +
    +
    + + + +
    + +
    + +
    + + + +
    +
    +
    \ No newline at end of file diff --git a/resources/views/pages/parts/form.blade.php b/resources/views/pages/parts/form.blade.php index ebad2bd72..2c2ab9b92 100644 --- a/resources/views/pages/parts/form.blade.php +++ b/resources/views/pages/parts/form.blade.php @@ -15,64 +15,8 @@ option:page-editor:draft-discarded-text="{{ trans('entities.pages_draft_discarded') }}" option:page-editor:set-changelog-text="{{ trans('entities.pages_edit_set_changelog') }}"> - {{--Header Bar--}} -
    -
    - - - -
    - -
    - -
    - - - -
    -
    -
    + {{--Header Toolbar--}} + @include('pages.parts.editor-toolbar', ['model' => $model, 'editor' => $editor, 'isDraft' => $isDraft, 'draftsEnabled' => $draftsEnabled]) {{--Title input--}}
    diff --git a/resources/views/pages/revisions.blade.php b/resources/views/pages/revisions.blade.php index 5508f362d..87949837b 100644 --- a/resources/views/pages/revisions.blade.php +++ b/resources/views/pages/revisions.blade.php @@ -58,7 +58,10 @@
    {!! csrf_field() !!} - +
    @@ -72,7 +75,10 @@
    {!! csrf_field() !!} - +
    diff --git a/resources/views/settings/audit.blade.php b/resources/views/settings/audit.blade.php index ca5dba527..506a735a2 100644 --- a/resources/views/settings/audit.blade.php +++ b/resources/views/settings/audit.blade.php @@ -14,9 +14,9 @@
    diff --git a/resources/views/settings/recycle-bin/index.blade.php b/resources/views/settings/recycle-bin/index.blade.php index 5f2ec333f..56e2437fe 100644 --- a/resources/views/settings/recycle-bin/index.blade.php +++ b/resources/views/settings/recycle-bin/index.blade.php @@ -22,7 +22,7 @@
    {!! csrf_field() !!} - +
    @@ -93,8 +93,8 @@ From eff539f89beb23c9ba7c86776d85a3e5e8412276 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 20 Apr 2022 14:58:37 +0100 Subject: [PATCH 20/41] Added new confirm-dialog component, both view and logic --- resources/js/components/confirm-dialog.js | 53 +++++++++++++++++++ resources/js/components/index.js | 4 +- resources/js/components/popup.js | 4 +- resources/sass/_components.scss | 5 ++ .../views/common/confirm-dialog.blade.php | 21 ++++++++ 5 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 resources/js/components/confirm-dialog.js create mode 100644 resources/views/common/confirm-dialog.blade.php diff --git a/resources/js/components/confirm-dialog.js b/resources/js/components/confirm-dialog.js new file mode 100644 index 000000000..c6c5d103a --- /dev/null +++ b/resources/js/components/confirm-dialog.js @@ -0,0 +1,53 @@ +import {onSelect} from "../services/dom"; + +/** + * Custom equivalent of window.confirm() using our popup component. + * Is promise based so can be used like so: + * `const result = await dialog.show()` + * @extends {Component} + */ +class ConfirmDialog { + + setup() { + this.container = this.$el; + this.confirmButton = this.$refs.confirm; + + this.res = null; + + onSelect(this.confirmButton, () => { + this.sendResult(true); + this.getPopup().hide(); + }); + } + + show() { + this.getPopup().show(null, () => { + this.sendResult(false); + }); + + return new Promise((res, rej) => { + this.res = res; + }); + } + + /** + * @returns {Popup} + */ + getPopup() { + return this.container.components.popup; + } + + /** + * @param {Boolean} result + */ + sendResult(result) { + if (this.res) { + console.log('sending result', result); + this.res(result) + this.res = null; + } + } + +} + +export default ConfirmDialog; \ No newline at end of file diff --git a/resources/js/components/index.js b/resources/js/components/index.js index fe348aba7..6a4a8c2b0 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -10,6 +10,7 @@ import chapterToggle from "./chapter-toggle.js" import codeEditor from "./code-editor.js" import codeHighlighter from "./code-highlighter.js" import collapsible from "./collapsible.js" +import confirmDialog from "./confirm-dialog" import customCheckbox from "./custom-checkbox.js" import detailsHighlighter from "./details-highlighter.js" import dropdown from "./dropdown.js" @@ -26,7 +27,6 @@ import headerMobileToggle from "./header-mobile-toggle.js" import homepageControl from "./homepage-control.js" import imageManager from "./image-manager.js" import imagePicker from "./image-picker.js" -import index from "./index.js" import listSortControl from "./list-sort-control.js" import markdownEditor from "./markdown-editor.js" import newUserPassword from "./new-user-password.js" @@ -66,6 +66,7 @@ const componentMapping = { "code-editor": codeEditor, "code-highlighter": codeHighlighter, "collapsible": collapsible, + "confirm-dialog": confirmDialog, "custom-checkbox": customCheckbox, "details-highlighter": detailsHighlighter, "dropdown": dropdown, @@ -82,7 +83,6 @@ const componentMapping = { "homepage-control": homepageControl, "image-manager": imageManager, "image-picker": imagePicker, - "index": index, "list-sort-control": listSortControl, "markdown-editor": markdownEditor, "new-user-password": newUserPassword, diff --git a/resources/js/components/popup.js b/resources/js/components/popup.js index 13cf69d21..ec111963f 100644 --- a/resources/js/components/popup.js +++ b/resources/js/components/popup.js @@ -34,7 +34,7 @@ class Popup { } hide(onComplete = null) { - fadeOut(this.container, 240, onComplete); + fadeOut(this.container, 120, onComplete); if (this.onkeyup) { window.removeEventListener('keyup', this.onkeyup); this.onkeyup = null; @@ -45,7 +45,7 @@ class Popup { } show(onComplete = null, onHide = null) { - fadeIn(this.container, 240, onComplete); + fadeIn(this.container, 120, onComplete); this.onkeyup = (event) => { if (event.key === 'Escape') { diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 95ba81520..bce456cf2 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -120,6 +120,11 @@ width: 800px; max-width: 90%; } + &.very-small { + margin: 2% auto; + width: 600px; + max-width: 90%; + } &:before { display: flex; align-self: flex-start; diff --git a/resources/views/common/confirm-dialog.blade.php b/resources/views/common/confirm-dialog.blade.php new file mode 100644 index 000000000..107d04af1 --- /dev/null +++ b/resources/views/common/confirm-dialog.blade.php @@ -0,0 +1,21 @@ + \ No newline at end of file From 478067483f49c8b2cb6ed2d52ad832fd79dd2701 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 20 Apr 2022 18:21:21 +0100 Subject: [PATCH 21/41] Linked up confirmation prompt to editor switching --- resources/js/components/confirm-dialog.js | 1 - resources/js/components/page-editor.js | 22 +++++++++++++++++++ resources/lang/en/entities.php | 10 +++++++-- .../views/common/confirm-dialog.blade.php | 2 +- resources/views/home/default.blade.php | 16 ++++++++++++++ .../pages/parts/editor-toolbar.blade.php | 6 ++--- resources/views/pages/parts/form.blade.php | 16 ++++++++++++++ 7 files changed, 66 insertions(+), 7 deletions(-) diff --git a/resources/js/components/confirm-dialog.js b/resources/js/components/confirm-dialog.js index c6c5d103a..858be1b85 100644 --- a/resources/js/components/confirm-dialog.js +++ b/resources/js/components/confirm-dialog.js @@ -42,7 +42,6 @@ class ConfirmDialog { */ sendResult(result) { if (this.res) { - console.log('sending result', result); this.res(result) this.res = null; } diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js index dae807122..ce123e987 100644 --- a/resources/js/components/page-editor.js +++ b/resources/js/components/page-editor.js @@ -24,6 +24,8 @@ class PageEditor { 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; // Translations this.draftText = this.$opts.draftText; @@ -72,6 +74,9 @@ class PageEditor { // Draft Controls onSelect(this.saveDraftButton, this.saveDraft.bind(this)); onSelect(this.discardDraftButton, this.discardDraft.bind(this)); + + // Change editor controls + onSelect(this.changeEditorButtons, this.changeEditor.bind(this)); } setInitialFocus() { @@ -113,17 +118,21 @@ class PageEditor { data.markdown = this.editorMarkdown; } + let didSave = false; try { const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data); if (!this.isNewDraft) { this.toggleDiscardDraftVisibility(true); } + this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`); this.autoSave.last = Date.now(); if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) { window.$events.emit('warning', resp.data.warning); this.shownWarningsCache.add(resp.data.warning); } + + didSave = true; } catch (err) { // Save the editor content in LocalStorage as a last resort, just in case. try { @@ -134,6 +143,7 @@ class PageEditor { window.$events.emit('error', this.autosaveFailText); } + return didSave; } draftNotifyChange(text) { @@ -185,6 +195,18 @@ class PageEditor { this.discardDraftWrap.classList.toggle('hidden', !show); } + async changeEditor(event) { + event.preventDefault(); + + const link = event.target.closest('a').href; + const dialog = this.switchDialogContainer.components['confirm-dialog']; + const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]); + + if (saved && confirmed) { + window.location = link; + } + } + } export default PageEditor; \ No newline at end of file diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 3921828d8..85a77e0cb 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -197,12 +197,18 @@ return [ 'pages_edit_delete_draft' => 'Delete Draft', 'pages_edit_discard_draft' => 'Discard Draft', 'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor', - 'pages_edit_switch_to_markdown_clean' => '(Clean Markdown Content)', - 'pages_edit_switch_to_markdown_stable' => '(Stable Markdown Content)', + 'pages_edit_switch_to_markdown_clean' => '(Clean Content)', + 'pages_edit_switch_to_markdown_stable' => '(Stable Content)', 'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor', 'pages_edit_set_changelog' => 'Set Changelog', 'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made', 'pages_edit_enter_changelog' => 'Enter Changelog', + 'pages_editor_switch_title' => 'Switch Editor', + 'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?', + 'pages_editor_switch_consider_following' => 'Consider the following when changing editors:', + 'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.', + 'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.', + 'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.', 'pages_save' => 'Save Page', 'pages_title' => 'Page Title', 'pages_name' => 'Page Name', diff --git a/resources/views/common/confirm-dialog.blade.php b/resources/views/common/confirm-dialog.blade.php index 107d04af1..28587d4e8 100644 --- a/resources/views/common/confirm-dialog.blade.php +++ b/resources/views/common/confirm-dialog.blade.php @@ -1,5 +1,5 @@ + @component('common.confirm-dialog', ['title' => 'Destroy Dogs']) +

    Are you sure you want to do this thingy?

    +
      +
    • This could be bad
    • +
    • This could be very bad
    • +
    • This might be very bad
    • +
    + @endcomponent + + +
    diff --git a/resources/views/pages/parts/editor-toolbar.blade.php b/resources/views/pages/parts/editor-toolbar.blade.php index d7fb76c29..9bc79476e 100644 --- a/resources/views/pages/parts/editor-toolbar.blade.php +++ b/resources/views/pages/parts/editor-toolbar.blade.php @@ -36,7 +36,7 @@ @if(userCan('editor-change'))
  • @if($editor === 'wysiwyg') - + @icon('swap-horizontal')
    {{ trans('entities.pages_edit_switch_to_markdown') }} @@ -44,7 +44,7 @@ {{ trans('entities.pages_edit_switch_to_markdown_clean') }}
    - + @icon('swap-horizontal')
    {{ trans('entities.pages_edit_switch_to_markdown') }} @@ -53,7 +53,7 @@
    @else - + @icon('swap-horizontal')
    {{ trans('entities.pages_edit_switch_to_wysiwyg') }}
    diff --git a/resources/views/pages/parts/form.blade.php b/resources/views/pages/parts/form.blade.php index 2c2ab9b92..cec11462d 100644 --- a/resources/views/pages/parts/form.blade.php +++ b/resources/views/pages/parts/form.blade.php @@ -40,8 +40,24 @@
  • + {{--Mobile Save Button--}} + + {{--Editor Change Dialog--}} + @component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switchDialog']) +

    + {{ trans('entities.pages_editor_switch_are_you_sure') }} +
    + {{ trans('entities.pages_editor_switch_consider_following') }} +

    + +
      +
    • {{ trans('entities.pages_editor_switch_consideration_a') }}
    • +
    • {{ trans('entities.pages_editor_switch_consideration_b') }}
    • +
    • {{ trans('entities.pages_editor_switch_consideration_c') }}
    • +
    + @endcomponent
    \ No newline at end of file From d76bbb29546692732aea33ca5507df886ef6cb1b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 20 Apr 2022 23:32:02 +0100 Subject: [PATCH 22/41] Made it possible to configure draw.io/diagrams.net integration Added new editor public event to hook into draw.io configuration step. Required change of embed url to trigger the configure step. --- resources/js/services/drawio.js | 8 ++++++++ resources/views/pages/parts/form.blade.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/resources/js/services/drawio.js b/resources/js/services/drawio.js index 6e22919fb..dfca83211 100644 --- a/resources/js/services/drawio.js +++ b/resources/js/services/drawio.js @@ -43,6 +43,8 @@ function drawReceive(event) { drawEventSave(message); } else if (message.event === 'export') { drawEventExport(message); + } else if (message.event === 'configure') { + drawEventConfigure(); } } @@ -63,6 +65,12 @@ function drawEventInit() { }); } +function drawEventConfigure() { + const config = {}; + window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config}); + drawPostMessage({action: 'configure', config}); +} + function drawEventClose() { window.removeEventListener('message', drawReceive); if (iFrame) document.body.removeChild(iFrame); diff --git a/resources/views/pages/parts/form.blade.php b/resources/views/pages/parts/form.blade.php index 01f68a6c5..f199b8624 100644 --- a/resources/views/pages/parts/form.blade.php +++ b/resources/views/pages/parts/form.blade.php @@ -1,7 +1,7 @@
    name === trans('entities.pages_initial_name')) option:page-editor:has-default-title="true" From 0003ce61cd5e9ea2dee8a3fcc1788943ea39d293 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 20 Apr 2022 23:42:47 +0100 Subject: [PATCH 23/41] Fixed failing test after drawio default url change --- tests/Uploads/DrawioTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Uploads/DrawioTest.php b/tests/Uploads/DrawioTest.php index 1fc3d1049..2ed4da7ca 100644 --- a/tests/Uploads/DrawioTest.php +++ b/tests/Uploads/DrawioTest.php @@ -71,7 +71,7 @@ class DrawioTest extends TestCase $editor = $this->getEditor(); $resp = $this->actingAs($editor)->get($page->getUrl('/edit')); - $resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&proto=json&spin=1"', false); + $resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&proto=json&spin=1&configure=1"', false); config()->set('services.drawio', false); $resp = $this->actingAs($editor)->get($page->getUrl('/edit')); From f14e6e8f2dbfac04829c1819398038ec99166d8f Mon Sep 17 00:00:00 2001 From: julesdevops Date: Wed, 20 Apr 2022 22:58:16 +0200 Subject: [PATCH 24/41] Complete list endpoint and add some tests --- app/Entities/Repos/DeletionRepo.php | 2 + .../Api/RecycleBinApiController.php | 46 ++++++++-- tests/Api/RecycleBinApiTest.php | 84 +++++++++++++++++-- 3 files changed, 116 insertions(+), 16 deletions(-) diff --git a/app/Entities/Repos/DeletionRepo.php b/app/Entities/Repos/DeletionRepo.php index 8fad4e6b0..5d53013dc 100644 --- a/app/Entities/Repos/DeletionRepo.php +++ b/app/Entities/Repos/DeletionRepo.php @@ -21,6 +21,7 @@ class DeletionRepo /** @var Deletion $deletion */ $deletion = Deletion::query()->findOrFail($id); Activity::add(ActivityType::RECYCLE_BIN_RESTORE, $deletion); + return $this->trashCan->restoreFromDeletion($deletion); } @@ -29,6 +30,7 @@ class DeletionRepo /** @var Deletion $deletion */ $deletion = Deletion::query()->findOrFail($id); Activity::add(ActivityType::RECYCLE_BIN_DESTROY, $deletion); + return $this->trashCan->destroyFromDeletion($deletion); } } diff --git a/app/Http/Controllers/Api/RecycleBinApiController.php b/app/Http/Controllers/Api/RecycleBinApiController.php index 162b27adb..f60115c16 100644 --- a/app/Http/Controllers/Api/RecycleBinApiController.php +++ b/app/Http/Controllers/Api/RecycleBinApiController.php @@ -2,10 +2,11 @@ namespace BookStack\Http\Controllers\Api; -use BookStack\Actions\ActivityType; +use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Deletion; use BookStack\Entities\Repos\DeletionRepo; -use BookStack\Entities\Tools\TrashCan; +use Closure; class RecycleBinApiController extends ApiController { @@ -22,24 +23,55 @@ class RecycleBinApiController extends ApiController public function list() { return $this->apiListingResponse(Deletion::query(), [ - 'id', - 'deleted_by', + 'id', + 'deleted_by', 'created_at', 'updated_at', 'deletable_type', - 'deletable_id' - ]); + 'deletable_id', + ], [Closure::fromCallable([$this, 'listFormatter'])]); } public function restore(DeletionRepo $deletionRepo, string $id) { $restoreCount = $deletionRepo->restore((int) $id); + return response()->json(['restore_count' => $restoreCount]); } public function destroy(DeletionRepo $deletionRepo, string $id) { $deleteCount = $deletionRepo->destroy((int) $id); + return response()->json(['delete_count' => $deleteCount]); } -} \ No newline at end of file + + protected function listFormatter(Deletion $deletion) + { + $deletable = $deletion->deletable; + $isBook = $deletable instanceof Book; + $parent = null; + $children = null; + + if ($isBook) { + $chapterCount = $deletable->chapters()->withTrashed()->count(); + $children['Bookstack\Chapter'] = $chapterCount; + } + + if ($isBook || $deletion->deletable instanceof Chapter) { + $pageCount = $deletable->pages()->withTrashed()->count(); + $children['Bookstack\Page'] = $pageCount; + } + + $parentEntity = $deletable->getParent(); + $parent = []; + + if ($parentEntity) { + $parent['type'] = $parentEntity->getMorphClass(); + $parent['id'] = $parentEntity->getKey(); + } + + $deletion->setAttribute('parent', $parent); + $deletion->setAttribute('children', $children); + } +} diff --git a/tests/Api/RecycleBinApiTest.php b/tests/Api/RecycleBinApiTest.php index 9371e06e8..286227896 100644 --- a/tests/Api/RecycleBinApiTest.php +++ b/tests/Api/RecycleBinApiTest.php @@ -3,12 +3,9 @@ namespace Tests\Api; use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Page; -use Carbon\Carbon; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\DB; use Tests\TestCase; class RecycleBinApiTest extends TestCase @@ -52,7 +49,7 @@ class RecycleBinApiTest extends TestCase public function test_index_endpoint_returns_expected_page() { $this->actingAsAuthorizedUser(); - + $page = Page::query()->first(); $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); $editor = $this->getEditor(); @@ -72,13 +69,82 @@ class RecycleBinApiTest extends TestCase 'created_at' => $data[0]->created_at->toJson(), 'updated_at' => $data[0]->updated_at->toJson(), 'deletable_type' => $data[1]->getMorphClass(), - 'deletable_id' => $data[1]->getKey() + 'deletable_id' => $data[1]->getKey(), ]; }); $resp->assertJson([ 'data' => $expectedData->values()->all(), - 'total' => 2 + 'total' => 2, + ]); + } + + public function test_index_endpoint_returns_children() + { + $this->actingAsAuthorizedUser(); + + $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($book->getUrl()); + + $deletion = Deletion::query()->orderBy('id')->first(); + + $resp = $this->getJson($this->baseEndpoint); + + $expectedData = [ + [ + 'id' => $deletion->getKey(), + 'deleted_by' => $editor->getKey(), + 'created_at' => $deletion->created_at->toJson(), + 'updated_at' => $deletion->updated_at->toJson(), + 'deletable_type' => $book->getMorphClass(), + 'deletable_id' => $book->getKey(), + 'children' => [ + 'Bookstack\Page' => $book->pages_count, + 'Bookstack\Chapter' => $book->chapters_count, + ], + 'parent' => null, + ] + ]; + + $resp->assertJson([ + 'data' => $expectedData, + 'total' => 1, + ]); + } + + public function test_index_endpoint_returns_parent() + { + $this->actingAsAuthorizedUser(); + + $page = Page::query()->whereHas('chapter')->with('chapter')->first(); + + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($page->getUrl()); + + $deletion = Deletion::query()->orderBy('id')->first(); + + $resp = $this->getJson($this->baseEndpoint); + + $expectedData = [ + [ + 'id' => $deletion->getKey(), + 'deleted_by' => $editor->getKey(), + 'created_at' => $deletion->created_at->toJson(), + 'updated_at' => $deletion->updated_at->toJson(), + 'deletable_type' => $page->getMorphClass(), + 'deletable_id' => $page->getKey(), + 'parent' => [ + 'type' => 'BookStack\Chapter', + 'id' => $page->chapter->getKey() + ], + 'children' => null, + ] + ]; + + $resp->assertJson([ + 'data' => $expectedData, + 'total' => 1 ]); } @@ -95,14 +161,14 @@ class RecycleBinApiTest extends TestCase $this->assertDatabaseHas('pages', [ 'id' => $page->getKey(), - 'deleted_at' => $page->deleted_at + 'deleted_at' => $page->deleted_at, ]); $this->putJson($this->baseEndpoint . '/' . $deletion->getKey()); $this->assertDatabaseHas('pages', [ 'id' => $page->getKey(), - 'deleted_at' => null + 'deleted_at' => null, ]); } @@ -119,7 +185,7 @@ class RecycleBinApiTest extends TestCase $this->assertDatabaseHas('pages', [ 'id' => $page->getKey(), - 'deleted_at' => $page->deleted_at + 'deleted_at' => $page->deleted_at, ]); $this->deleteJson($this->baseEndpoint . '/' . $deletion->getKey()); From 1b46aa87565547ff7af21dfec1c2d7d89ff4873d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 23 Apr 2022 14:22:04 +0100 Subject: [PATCH 25/41] Aded tests for core editor switching functionality --- resources/views/home/default.blade.php | 9 --- tests/Entity/PageEditorTest.php | 82 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/resources/views/home/default.blade.php b/resources/views/home/default.blade.php index 4e14b6744..6435e4ebd 100644 --- a/resources/views/home/default.blade.php +++ b/resources/views/home/default.blade.php @@ -17,15 +17,6 @@
    - @component('common.confirm-dialog', ['title' => 'Destroy Dogs']) -

    Are you sure you want to do this thingy?

    -
      -
    • This could be bad
    • -
    • This could be very bad
    • -
    • This might be very bad
    • -
    - @endcomponent -