]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #3364 from BookStackApp/app_url_requests
authorDan Brown <redacted>
Sun, 24 Apr 2022 13:52:38 +0000 (14:52 +0100)
committerGitHub <redacted>
Sun, 24 Apr 2022 13:52:38 +0000 (14:52 +0100)
Updated custom request overrides to better match original intent

77 files changed:
app/Entities/Models/Page.php
app/Entities/Models/PageRevision.php
app/Entities/Repos/BaseRepo.php
app/Entities/Repos/PageRepo.php
app/Entities/Tools/Markdown/HtmlToMarkdown.php
app/Entities/Tools/Markdown/MarkdownToHtml.php [new file with mode: 0644]
app/Entities/Tools/PageContent.php
app/Entities/Tools/PageEditActivity.php
app/Entities/Tools/PageEditorData.php [new file with mode: 0644]
app/Http/Controllers/Api/AttachmentApiController.php
app/Http/Controllers/Api/BookshelfApiController.php
app/Http/Controllers/Api/PageApiController.php
app/Http/Controllers/AttachmentController.php
app/Http/Controllers/Controller.php
app/Http/Controllers/PageController.php
app/Uploads/AttachmentService.php
database/migrations/2022_04_17_101741_add_editor_change_field_and_permission.php [new file with mode: 0644]
dev/build/esbuild.js [new file with mode: 0644]
package-lock.json
package.json
resources/icons/swap-horizontal.svg [new file with mode: 0644]
resources/js/components/auto-suggest.js
resources/js/components/code-editor.js
resources/js/components/confirm-dialog.js [new file with mode: 0644]
resources/js/components/index.js
resources/js/components/page-editor.js
resources/js/components/popup.js
resources/js/services/drawio.js
resources/lang/de/settings.php
resources/lang/en/entities.php
resources/lang/en/settings.php
resources/lang/eu/editor.php
resources/lang/eu/entities.php
resources/lang/eu/errors.php
resources/lang/eu/settings.php
resources/lang/id/auth.php
resources/lang/id/editor.php
resources/lang/id/validation.php
resources/lang/nl/settings.php
resources/lang/pt/activities.php
resources/lang/pt/common.php
resources/lang/pt/editor.php
resources/lang/ru/editor.php
resources/lang/ru/settings.php
resources/lang/ru/validation.php
resources/lang/zh_CN/editor.php
resources/lang/zh_CN/settings.php
resources/sass/_components.scss
resources/sass/_lists.scss
resources/sass/_text.scss
resources/views/attachments/manager-list.blade.php
resources/views/comments/comment.blade.php
resources/views/common/confirm-dialog.blade.php [new file with mode: 0644]
resources/views/common/export-styles.blade.php
resources/views/common/header.blade.php
resources/views/entities/export-menu.blade.php
resources/views/entities/sort.blade.php
resources/views/home/default.blade.php
resources/views/mfa/parts/setup-method-row.blade.php
resources/views/pages/edit.blade.php
resources/views/pages/parts/editor-toolbar.blade.php [new file with mode: 0644]
resources/views/pages/parts/form.blade.php
resources/views/pages/parts/wysiwyg-editor.blade.php
resources/views/pages/revisions.blade.php
resources/views/settings/audit.blade.php
resources/views/settings/customization.blade.php
resources/views/settings/recycle-bin/index.blade.php
resources/views/settings/roles/parts/form.blade.php
tests/Api/AttachmentsApiTest.php
tests/Api/BooksApiTest.php
tests/Api/ChaptersApiTest.php
tests/Api/PagesApiTest.php
tests/Api/ShelvesApiTest.php
tests/Entity/PageEditorTest.php
tests/Entity/PageRevisionTest.php
tests/Uploads/AttachmentTest.php
tests/Uploads/DrawioTest.php

index c8217af576d84af0e1aa4aa4d98526d222c471ac..ed69bcf8b8569d2e92fe94444d541872003a849b 100644 (file)
@@ -22,6 +22,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
  * @property bool         $template
  * @property bool         $draft
  * @property int          $revision_count
+ * @property string       $editor
  * @property Chapter      $chapter
  * @property Collection   $attachments
  * @property Collection   $revisions
index 800e5e7f2d8d0332bfcc868bbdcdcb8c872d5698..be2ac33a0958fc26dddfd12ec9795f8448818a7e 100644 (file)
@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  *
  * @property mixed  $id
  * @property int    $page_id
+ * @property string $name
  * @property string $slug
  * @property string $book_slug
  * @property int    $created_by
@@ -21,13 +22,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property string $summary
  * @property string $markdown
  * @property string $html
+ * @property string $text
  * @property int    $revision_number
  * @property Page   $page
  * @property-read ?User $createdBy
  */
 class PageRevision extends Model
 {
-    protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
+    protected $fillable = ['name', 'text', 'summary'];
     protected $hidden = ['html', 'markdown', 'restricted', 'text'];
 
     /**
index 6b29dad7b547623058aef4b84760cfc56ef7302f..9e1b41672128b92054b5fda8efaa93df8ed9d7cb 100644 (file)
@@ -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();
index 828c4572fd100f7355d02077477a85a8728bc216..c106d2fd30679d6f663cdd845ddbaf1b0aee4fe5 100644 (file)
@@ -10,6 +10,7 @@ use BookStack\Entities\Models\Page;
 use BookStack\Entities\Models\PageRevision;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\PageContent;
+use BookStack\Entities\Tools\PageEditorData;
 use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\MoveOperationException;
 use BookStack\Exceptions\NotFoundException;
@@ -217,11 +218,25 @@ class PageRepo
         }
 
         $pageContent = new PageContent($page);
-        if (!empty($input['markdown'] ?? '')) {
+        $currentEditor = $page->editor ?: PageEditorData::getSystemDefaultEditor();
+        $newEditor = $currentEditor;
+
+        $haveInput = isset($input['markdown']) || isset($input['html']);
+        $inputEmpty = empty($input['markdown']) && empty($input['html']);
+
+        if ($haveInput && $inputEmpty) {
+            $pageContent->setNewHTML('');
+        } elseif (!empty($input['markdown']) && is_string($input['markdown'])) {
+            $newEditor = 'markdown';
             $pageContent->setNewMarkdown($input['markdown']);
         } elseif (isset($input['html'])) {
+            $newEditor = 'wysiwyg';
             $pageContent->setNewHTML($input['html']);
         }
+
+        if ($newEditor !== $currentEditor && userCan('editor-change')) {
+            $page->editor = $newEditor;
+        }
     }
 
     /**
@@ -229,8 +244,12 @@ class PageRepo
      */
     protected function savePageRevision(Page $page, string $summary = null): PageRevision
     {
-        $revision = new PageRevision($page->getAttributes());
+        $revision = new PageRevision();
 
+        $revision->name = $page->name;
+        $revision->html = $page->html;
+        $revision->markdown = $page->markdown;
+        $revision->text = $page->text;
         $revision->page_id = $page->id;
         $revision->slug = $page->slug;
         $revision->book_slug = $page->book->slug;
@@ -260,10 +279,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 = '';
         }
 
index 51366705ca1a1043df00a690bb5f5a70293e794a..5c7b388ea93ceda62bb8f8241ae6d924235e449c 100644 (file)
@@ -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 (file)
index 0000000..25413fb
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace BookStack\Entities\Tools\Markdown;
+
+use BookStack\Facades\Theme;
+use BookStack\Theming\ThemeEvents;
+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 MarkdownToHtml
+{
+
+    protected string $markdown;
+
+    public function __construct(string $markdown)
+    {
+        $this->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
index b1c750adbdd6a3a3645c75e91b0f44c9e940bc6c..ea6a185f161424f3cc85d0ef918860c980bc3227 100644 (file)
@@ -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.
      */
index 9981a6ed7ae2f6fa7742729f9d01ed34ddb0af68..2672de94145d7c57a1543eb4271602ed7fb80972 100644 (file)
@@ -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 (file)
index 0000000..72f3391
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+
+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, string $requestedEditor)
+    {
+        $this->page = $page;
+        $this->pageRepo = $pageRepo;
+        $this->requestedEditor = $requestedEditor;
+
+        $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);
+        }
+
+        $editorType = $this->getEditorType($page);
+        $this->updateContentForEditor($page, $editorType);
+
+        return [
+            'page'            => $page,
+            'book'            => $page->book,
+            'isDraft'         => $isDraft,
+            'isDraftRevision' => $isDraftRevision,
+            'draftsEnabled'   => $draftsEnabled,
+            'templates'       => $templates,
+            '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
+    {
+        $editorType = $page->editor ?: self::getSystemDefaultEditor();
+
+        // 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;
+    }
+
+    /**
+     * Get the configured system default editor.
+     */
+    public static function getSystemDefaultEditor(): string
+    {
+        return setting('app-editor') === 'markdown' ? 'markdown' : 'wysiwyg';
+    }
+
+}
\ No newline at end of file
index fc5008e3c6d94baf77ad70167a567c54e1451bcf..2476cb95195ad56c0063031005be93b2ee1b43dd 100644 (file)
@@ -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']);
     }
 
     /**
index bd4f23a1093257cecd971a030d5c48109fc3a313..63275a72a66191fb9880c425629cbc99126ddf65 100644 (file)
@@ -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'],
         ],
     ];
 
index 6f3a71e029ba9b4bab542cb29c32a100dc5e6fac..9749985a52214cecbec3ee8cfecf9d0bc2b84e54 100644 (file)
@@ -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());
     }
index 084f6f96ad8866469346c4d0469636820f799a9c..0a092b63ae11385e4d67dae6218109b2e2f659fa 100644 (file)
@@ -15,8 +15,8 @@ use Illuminate\Validation\ValidationException;
 
 class AttachmentController extends Controller
 {
-    protected $attachmentService;
-    protected $pageRepo;
+    protected AttachmentService $attachmentService;
+    protected PageRepo $pageRepo;
 
     /**
      * AttachmentController constructor.
@@ -230,13 +230,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);
     }
 
     /**
index d616974c6fd797c6bc063ca5989501c0a0f4024f..6ca2239cccb90ed6da8a34ad27986cb84eb1de8f 100644 (file)
@@ -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
 {
@@ -115,7 +116,28 @@ 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',
+        ]);
+    }
+
+    /**
+     * 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) {
+            // 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="' . str_replace('"', '', $fileName) . '"',
             'X-Content-Type-Options' => 'nosniff',
         ]);
     }
@@ -130,7 +152,28 @@ 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',
+        ]);
+    }
+
+    /**
+     * 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="' . str_replace('"', '', $fileName) . '"',
             'X-Content-Type-Options' => 'nosniff',
         ]);
     }
index eecb6a6e79c94d43b1f88287f3175c8c83854639..268dce0573a9c51a0beacb264c7dc9ca14435481 100644 (file)
@@ -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.
@@ -82,22 +83,15 @@ 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());
-        $this->setPageTitle(trans('entities.pages_edit_draft'));
 
-        $draftsEnabled = $this->isSignedIn();
-        $templates = $this->pageRepo->getTemplates(10);
+        $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
+        $this->setPageTitle(trans('entities.pages_edit_draft'));
 
-        return view('pages.edit', [
-            'page'          => $draft,
-            'book'          => $draft->book,
-            'isDraft'       => true,
-            'draftsEnabled' => $draftsEnabled,
-            'templates'     => $templates,
-        ]);
+        return view('pages.edit', $editorData->getViewData());
     }
 
     /**
@@ -188,43 +182,19 @@ 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);
 
-        $page->isDraft = false;
-        $editActivity = new PageEditActivity($page);
-
-        // Check for active editing
-        $warnings = [];
-        if ($editActivity->hasActiveEditing()) {
-            $warnings[] = $editActivity->activeEditingMessage();
+        $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
+        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,
-        ]);
+        return view('pages.edit', $editorData->getViewData());
     }
 
     /**
index 7974d7ae926b1472f61f567811e527acc254e688..ec02182bb118624fda0be7cdabbf7fde5eecadab 100644 (file)
@@ -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.
      *
@@ -211,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') . '/';
 
@@ -221,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());
 
diff --git a/database/migrations/2022_04_17_101741_add_editor_change_field_and_permission.php b/database/migrations/2022_04_17_101741_add_editor_change_field_and_permission.php
new file mode 100644 (file)
index 0000000..e71146d
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+use Carbon\Carbon;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+class AddEditorChangeFieldAndPermission extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        // Add the new 'editor' column to the pages table
+        Schema::table('pages', function(Blueprint $table) {
+            $table->string('editor', 50)->default('');
+        });
+
+        // Populate the new 'editor' column
+        // We set it to 'markdown' for pages currently with markdown content
+        DB::table('pages')->where('markdown', '!=', '')->update(['editor' => 'markdown']);
+        // We set it to 'wysiwyg' where we have HTML but no markdown
+        DB::table('pages')->where('markdown', '=', '')
+            ->where('html', '!=', '')
+            ->update(['editor' => 'wysiwyg']);
+
+        // Give the admin user permission to change the editor
+        $adminRoleId = DB::table('roles')->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()
+    {
+        // Drop the new column from the pages table
+        Schema::table('pages', function(Blueprint $table) {
+            $table->dropColumn('editor');
+        });
+
+        // Remove traces of the role permission
+        DB::table('role_permissions')->where('name', '=', 'editor-change')->delete();
+    }
+}
diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js
new file mode 100644 (file)
index 0000000..4635703
--- /dev/null
@@ -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
index dd4f0228d5fbce6f28753747003674bb921379eb..7f0df22827f0d874439831c512f6a3c66ba13b0a 100644 (file)
       },
       "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"
       }
     },
     "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": {
         "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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"
       ],
       }
     },
     "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",
   },
   "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": {
       }
     },
     "esbuild": {
-      "version": "0.14.27",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.27.tgz",
-      "integrity": "sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q==",
-      "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"
+      "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.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
     },
       }
     },
     "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",
index 054015009168bb989560cb20caf7b10be9b5f788..b49a2a07f32b7196b7c8d1c503529adfa51eb0b4 100644 (file)
@@ -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",
   },
   "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",
diff --git a/resources/icons/swap-horizontal.svg b/resources/icons/swap-horizontal.svg
new file mode 100644 (file)
index 0000000..7bd25dd
--- /dev/null
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.99 16H14v-2H6.99v-3L3 15l3.99 4ZM21 9l-3.99-4v3H10v2h7.01v3z"/></svg>
\ No newline at end of file
index 68de49b4a32740143e7fc6fe24e442c0e502af5e..d1c15c00a7e604742d566cdd80b280fd11f82135 100644 (file)
@@ -131,7 +131,7 @@ class AutoSuggest {
             return this.hideSuggestions();
         }
 
-        this.list.innerHTML = suggestions.map(value => `<li><button type="button">${escapeHtml(value)}</button></li>`).join('');
+        this.list.innerHTML = suggestions.map(value => `<li><button type="button" class="text-item">${escapeHtml(value)}</button></li>`).join('');
         this.list.style.display = 'block';
         for (const button of this.list.querySelectorAll('button')) {
             button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
index f44de813de18bf93290e30a484a9ca3e921b26ac..4ee3531c58a7da957079a7a9ffb6c46aaaaf9c4c 100644 (file)
@@ -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 `<li><button type="button" data-time="${key}">${localTime}</button></li>`;
+             return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
         }).join('');
     }
 
diff --git a/resources/js/components/confirm-dialog.js b/resources/js/components/confirm-dialog.js
new file mode 100644 (file)
index 0000000..858be1b
--- /dev/null
@@ -0,0 +1,52 @@
+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) {
+            this.res(result)
+            this.res = null;
+        }
+    }
+
+}
+
+export default ConfirmDialog;
\ No newline at end of file
index fe348aba758a157562e2d48724f6066c22301d0e..6a4a8c2b08fc9096380fc225f361e28196883150 100644 (file)
@@ -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,
index dae8071220720d6126c81f77406f88c77af5e2a0..ce123e987b055db94769f7326a8fdf63f841342b 100644 (file)
@@ -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
index 13cf69d2192c37b21b39edf8733fa7329ae22bf2..ec111963f51e65358c51d5d3333bd5d47fb8e55d 100644 (file)
@@ -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') {
index 6e22919fb4d230556d99b6ce6a4382f077866af4..dfca832117f28b8c5ac12014a729bc0fc0610483 100644 (file)
@@ -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);
index 8c14f273451180cd3a21522e6521d0fed35a7f62..db459e3901c91d6b3834111b0f030796514528d7 100644 (file)
@@ -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',
index 4e4bbccd3f4227ad721134d722e20f472a518c8e..bed781b6129b180789556b43597883ba5671da21 100644 (file)
@@ -196,9 +196,19 @@ 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_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',
@@ -225,6 +235,7 @@ return [
     'pages_revisions_number' => '#',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id Changes',
+    'pages_revisions_editor' => 'Editor Type',
     'pages_revisions_changelog' => 'Changelog',
     'pages_revisions_changes' => 'Changes',
     'pages_revisions_current' => 'Current Version',
index 7461c9d4ebd951748b8b84520226db894c7e1bfc..af2dcc1e1655bab7208832cb6e090a578f2e931f 100755 (executable)
@@ -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 <head> 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.',
@@ -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.',
index 5403f88bc8c67417404d50fc29aacdbcea64dc9b..5381baf182b6047cda949578ee28e02a18277d48 100644 (file)
@@ -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',
index 57be5a3ae758fa8ed466c0386a47eab045d56db2..bfc5cdd80aa937f2e4efce83967729804a694b6e 100644 (file)
@@ -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.',
index 250d5d92fc1df9f009965b93670766aacc90f625..bd25e49d56073779c7aa22c7c6610ff88c7647d3 100644 (file)
@@ -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.',
index dac1e107e1c5f0b34f7255795e083b9ffab7f56e..5cfe14ff866dba376084476a8cc8028184c073b2 100644 (file)
@@ -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 <head> 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. <br>Large images will be scaled down.',
+    'app_logo_desc' => 'Irudi honek 43px izan behar du altueran.<br>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::<key>" 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',
index 423c92ff6164b253ab8eaaab5d27b1c44bced4b4..d76a05a01be4979548f9cb4e818fc56184623c34 100644 (file)
@@ -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:',
index 82712029210ebeec005dbf5530d8ad4c0918c042..88204b416e2ec7f2d6eba405b5ad7a246d4f5471 100644 (file)
@@ -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',
index 36a3397f5ae93bc5267bd8140465b61d8de6d7ec..e1fcbc7246c2b9ee8f7ce9410a202883007fee3d 100644 (file)
@@ -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.',
index 0326b290a4fc7a52836f44ac8ab0947e9870c0d0..998ac1a8c0f925488f9f73edf43a7ac54d470527 100644 (file)
@@ -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',
index b85e39421cd0b66ffd062c5b42721eed1d64cdf6..733a3cadca8a897510492920410af8371328e595 100644 (file)
@@ -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',
index e64d3e9b97615b2f2405e71af8b9356bc39fde33..137f389b32b3bdaaffc08d2102ab7fe1772ccf3f 100644 (file)
@@ -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',
index 81c33b201eaae0964c9e952222eefa19cfdfbe05..3a6119681039da4aa33de328a0be30168102d89f 100644 (file)
@@ -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',
index 6f8d9c755f510ca89579f6bb01e60ce118626c05..cda25b05f6a669163ea1c303e3ef542509a4cff6 100644 (file)
@@ -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.',
index b23b7e97c0b8916d918d3d2ef5c08262f0c7c833..4e48e8cc4bb6dd83b27b10d859917ce6a82c4796 100755 (executable)
@@ -10,8 +10,8 @@ return [
     'settings' => 'Настройки',
     'settings_save' => 'Сохранить настройки',
     'settings_save_success' => 'Настройки сохранены',
-    'system_version' => 'System Version',
-    'categories' => 'Categories',
+    'system_version' => 'Версия системы',
+    'categories' => 'Категории',
 
     // App Settings
     'app_customization' => 'Настройки',
index a0dec2b56986c0f36284653022ea8aae8bd2b8f3..88bd9a41f9761fc069a5d60c36df6d14244faec0 100644 (file)
@@ -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.',
index 81d46a0593a422cdffd416d86362f3d502ff7244..80f6335e2f635d90e625d15c0eaafe1d71c2c772 100644 (file)
@@ -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 构建的。',
index dacb82d83881296eba2aa12f908d89e84cad5dd8..c59781fcf2ac0aa55f18cfa75e3fa0aff74c8c7c 100755 (executable)
@@ -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' => '日期范围至',
index 95ba81520bc431eaac45a9b7dba22931126505a4..bce456cf2ce1e1b82147ae45e8f368ac83a91e83 100644 (file)
     width: 800px;
     max-width: 90%;
   }
+  &.very-small {
+    margin: 2% auto;
+    width: 600px;
+    max-width: 90%;
+  }
   &:before {
     display: flex;
     align-self: flex-start;
index 9cff52972bbcf00d7a10d3d209f37bfe696213f0..26d12a25da42949cbced533fd25ac66caac1bf9c 100644 (file)
@@ -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;
+    }
   }
 }
 
index 884808bb44daac5a9585c13302aab568a8ecf3b3..51f3156143c381271eec6e3f8b839fe6ca1daab0 100644 (file)
@@ -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 {
index b48fde9c01d90c0efe6341fcf88c7ec5d1b5d8a4..ebb1c24aadf4eca14effc513f10d8c34dc406518 100644 (file)
@@ -28,7 +28,7 @@
                             class="drag-card-action text-center text-neg">@icon('close')</button>
                     <div refs="dropdown@menu" class="dropdown-menu">
                         <p class="text-neg small px-m mb-xs">{{ trans('entities.attachments_delete') }}</p>
-                        <button refs="ajax-delete-row@delete" type="button" class="text-primary small delete">{{ trans('common.confirm') }}</button>
+                        <button refs="ajax-delete-row@delete" type="button" class="text-primary small delete text-item">{{ trans('common.confirm') }}</button>
                     </div>
                 </div>
             </div>
index 9f4a12357156cb41b65f08f61955c9d636e7d763..6189c65d41e7fab03103ff4eec1d447d8425faf4 100644 (file)
                         <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
                         <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
                             <li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li>
-                            <li><button action="delete" type="button" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</button></li>
+                            <li>
+                                <button action="delete" type="button" class="text-button text-neg icon-item">
+                                    @icon('delete')
+                                    <div>{{ trans('common.delete') }}</div>
+                                </button>
+                            </li>
                         </ul>
                     </div>
                 @endif
diff --git a/resources/views/common/confirm-dialog.blade.php b/resources/views/common/confirm-dialog.blade.php
new file mode 100644 (file)
index 0000000..28587d4
--- /dev/null
@@ -0,0 +1,21 @@
+<div components="popup confirm-dialog"
+     refs="confirm-dialog@popup {{ $ref }}"
+     class="popup-background">
+    <div class="popup-body very-small" tabindex="-1">
+
+        <div class="popup-header primary-background">
+            <div class="popup-title">{{ $title }}</div>
+            <button refs="popup@hide" type="button" class="popup-header-close">x</button>
+        </div>
+
+        <div class="px-m py-m">
+            {{ $slot }}
+
+            <div class="text-right">
+                <button type="button" class="button outline" refs="popup@hide">{{ trans('common.cancel') }}</button>
+                <button type="button" class="button" refs="confirm-dialog@confirm">{{ trans('common.continue') }}</button>
+            </div>
+        </div>
+
+    </div>
+</div>
\ No newline at end of file
index ee10637dddf4e0a0022f2feb4e78bc08d38a165f..1dfa6bb45b63ef9d299d1bd15123b48fcf7802af 100644 (file)
@@ -1,5 +1,5 @@
 <style>
-    @if (!app()->environment('testing'))
+    @if (!app()->runningUnitTests())
         {!! file_get_contents(public_path('/dist/export-styles.css')) !!}
     @endif
 </style>
index d55f3ae2dacd179a7749d362035a9ed0f2498922..b5ac520c18f37755388da0be91cd2a43cf01bc11 100644 (file)
                         </span>
                         <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
                             <li>
-                                <a href="{{ url('/favourites') }}">@icon('star'){{ trans('entities.my_favourites') }}</a>
+                                <a href="{{ url('/favourites') }}" class="icon-item">
+                                    @icon('star')
+                                    <div>{{ trans('entities.my_favourites') }}</div>
+                                </a>
                             </li>
                             <li>
-                                <a href="{{ $currentUser->getProfileUrl() }}">@icon('user'){{ trans('common.view_profile') }}</a>
+                                <a href="{{ $currentUser->getProfileUrl() }}" class="icon-item">
+                                    @icon('user')
+                                    <div>{{ trans('common.view_profile') }}</div>
+                                </a>
                             </li>
                             <li>
-                                <a href="{{ $currentUser->getEditUrl() }}">@icon('edit'){{ trans('common.edit_profile') }}</a>
+                                <a href="{{ $currentUser->getEditUrl() }}" class="icon-item">
+                                    @icon('edit')
+                                    <div>{{ trans('common.edit_profile') }}</div>
+                                </a>
                             </li>
                             <li>
                                 <form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
                                       method="post">
                                     {{ csrf_field() }}
-                                    <button class="text-muted icon-list-item text-primary">
-                                        @icon('logout'){{ trans('auth.logout') }}
+                                    <button class="icon-item">
+                                        @icon('logout')
+                                        <div>{{ trans('auth.logout') }}</div>
                                     </button>
                                 </form>
                             </li>
                             <li><hr></li>
                             <li>
-                                @include('common.dark-mode-toggle')
+                                @include('common.dark-mode-toggle', ['classes' => 'icon-item'])
                             </li>
                         </ul>
                     </div>
index 2b0f5c19dd84b9b88130afe5ee289545eae0100a..dd7231095b8f6ebf3e7e14ec9d9674adfbceb999 100644 (file)
@@ -5,9 +5,9 @@
         <span>{{ trans('entities.export') }}</span>
     </div>
     <ul refs="dropdown@menu" class="wide dropdown-menu" role="menu">
-        <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank" rel="noopener">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
-        <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" rel="noopener">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
-        <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" rel="noopener">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
-        <li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" rel="noopener">{{ trans('entities.export_md') }} <span class="text-muted float right">.md</span></a></li>
+        <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_html') }}</span><span>.html</span></a></li>
+        <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }}</span><span>.pdf</span></a></li>
+        <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_text') }}</span><span>.txt</span></a></li>
+        <li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_md') }}</span><span>.md</span></a></li>
     </ul>
 </div>
index bf90873975c7b89e483a82210ea67c263b20a79c..f81ed797f652f940a28cef225357ad751e3360a1 100644 (file)
@@ -16,7 +16,7 @@
                 <div refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" tabindex="0">{{ $options[$selectedSort] }}</div>
                 <ul refs="dropdown@menu" class="dropdown-menu">
                     @foreach($options as $key => $label)
-                        <li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}">{{ $label }}</a></li>
+                        <li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}" class="text-item">{{ $label }}</a></li>
                     @endforeach
                 </ul>
             </div>
index f6a337e5054d4bd7b978fc08c626253270d278ed..6435e4ebdc0f760801fa8c9dd6ded8bedf58f681 100644 (file)
         </div>
     </div>
 
+    <script nonce="{{ $cspNonce }}">
+        setTimeout(async () => {
+            const result = await window.components["confirm-dialog"][0].show();
+            console.log({result});
+        }, 1000);
+    </script>
+
     <div class="container" id="home-default">
         <div class="grid third gap-xxl no-row-gap" >
             <div>
index e195174c1b40c486706b4e062a183ab778e438ff..271ec1bf48d5c35762f76bdff8c643f91fce6afa 100644 (file)
@@ -19,7 +19,7 @@
                     <form action="{{ url('/mfa/' . $method . '/remove') }}" method="post">
                         {{ csrf_field() }}
                         {{ method_field('delete') }}
-                        <button class="text-primary small delete">{{ trans('common.confirm') }}</button>
+                        <button class="text-primary small text-item">{{ trans('common.confirm') }}</button>
                     </form>
                 </div>
             </div>
index 30158e852afdb3f368ce9202991c06c64eedb7e5..cd9635758060fe77f58a99427138d81075ca9749 100644 (file)
@@ -1,9 +1,5 @@
 @extends('layouts.base')
 
-@section('head')
-    <script src="{{ url('/libs/tinymce/tinymce.min.js?ver=5.10.2') }}" nonce="{{ $cspNonce }}"></script>
-@stop
-
 @section('body-class', 'flexbox')
 
 @section('content')
@@ -12,9 +8,7 @@
         <form action="{{ $page->getUrl() }}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
             {{ csrf_field() }}
 
-            @if(!isset($isDraft))
-                <input type="hidden" name="_method" value="PUT">
-            @endif
+            @if(!$isDraft) {{ method_field('PUT') }} @endif
             @include('pages.parts.form', ['model' => $page])
             @include('pages.parts.editor-toolbox')
         </form>
diff --git a/resources/views/pages/parts/editor-toolbar.blade.php b/resources/views/pages/parts/editor-toolbar.blade.php
new file mode 100644 (file)
index 0000000..9bc7947
--- /dev/null
@@ -0,0 +1,86 @@
+<div class="primary-background-light toolbar page-edit-toolbar">
+    <div class="grid third no-break v-center">
+
+        <div class="action-buttons text-left px-m py-xs">
+            <a href="{{ $isDraft ? $page->getParent()->getUrl() : $page->getUrl() }}"
+               class="text-button text-primary">@icon('back')<span class="hide-under-l">{{ trans('common.back') }}</span></a>
+        </div>
+
+        <div class="text-center px-m">
+            <div component="dropdown"
+                 option:dropdown:move-menu="true"
+                 class="dropdown-container draft-display text {{ $draftsEnabled ? '' : 'hidden' }}">
+                <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" title="{{ trans('entities.pages_edit_draft_options') }}" class="text-primary text-button py-s px-m"><span refs="page-editor@draftDisplay" class="faded-text"></span>&nbsp; @icon('more')</button>
+                @icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', 'refs' => 'page-editor@draftDisplayIcon'])
+                <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
+                    <li>
+                        <button refs="page-editor@saveDraft" type="button" class="text-pos icon-item">
+                            @icon('save')
+                            <div>{{ trans('entities.pages_edit_save_draft') }}</div>
+                        </button>
+                    </li>
+                    @if($isDraft)
+                        <li>
+                            <a href="{{ $model->getUrl('/delete') }}" class="text-neg icon-item">
+                                @icon('delete')
+                                {{ trans('entities.pages_edit_delete_draft') }}
+                            </a>
+                        </li>
+                    @endif
+                    <li refs="page-editor@discardDraftWrap" class="{{ $isDraft ? '' : 'hidden' }}">
+                        <button refs="page-editor@discardDraft" type="button" class="text-neg icon-item">
+                            @icon('cancel')
+                            <div>{{ trans('entities.pages_edit_discard_draft') }}</div>
+                        </button>
+                    </li>
+                    @if(userCan('editor-change'))
+                        <li>
+                            @if($editor === 'wysiwyg')
+                                <a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-clean" refs="page-editor@changeEditor" class="icon-item">
+                                    @icon('swap-horizontal')
+                                    <div>
+                                        {{ trans('entities.pages_edit_switch_to_markdown') }}
+                                        <br>
+                                        <small>{{ trans('entities.pages_edit_switch_to_markdown_clean') }}</small>
+                                    </div>
+                                </a>
+                                <a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-stable" refs="page-editor@changeEditor" class="icon-item">
+                                    @icon('swap-horizontal')
+                                    <div>
+                                        {{ trans('entities.pages_edit_switch_to_markdown') }}
+                                        <br>
+                                        <small>{{ trans('entities.pages_edit_switch_to_markdown_stable') }}</small>
+                                    </div>
+                                </a>
+                            @else
+                                <a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=wysiwyg" refs="page-editor@changeEditor" class="icon-item">
+                                    @icon('swap-horizontal')
+                                    <div>{{ trans('entities.pages_edit_switch_to_wysiwyg') }}</div>
+                                </a>
+                            @endif
+                        </li>
+                    @endif
+                </ul>
+            </div>
+        </div>
+
+        <div class="action-buttons px-m py-xs">
+            <div component="dropdown" dropdown-move-menu class="dropdown-container">
+                <button refs="dropdown@toggle" type="button" aria-haspopup="true" aria-expanded="false" class="text-primary text-button">@icon('edit') <span refs="page-editor@changelogDisplay">{{ trans('entities.pages_edit_set_changelog') }}</span></button>
+                <ul refs="dropdown@menu" class="wide dropdown-menu">
+                    <li class="px-l py-m">
+                        <p class="text-muted pb-s">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
+                        <input refs="page-editor@changelogInput"
+                               name="summary"
+                               id="summary-input"
+                               type="text"
+                               placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" />
+                    </li>
+                </ul>
+                <span>{{-- Prevents button jumping on menu show --}}</span>
+            </div>
+
+            <button type="submit" id="save-button" class="float-left text-primary text-button text-pos-hover hide-under-m">@icon('save')<span>{{ trans('entities.pages_save') }}</span></button>
+        </div>
+    </div>
+</div>
\ No newline at end of file
index 01f68a6c5cf0dfa5ab18e9c6fbbc0c4c5451e3a1..8da5cbf39887af42f3ca1a172e411bfdf2c4b95b 100644 (file)
@@ -1,71 +1,22 @@
 <div component="page-editor" class="page-editor flex-fill flex"
      option:page-editor:drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}"
      @if(config('services.drawio'))
-        drawio-url="{{ is_string(config('services.drawio')) ? config('services.drawio') : 'https://embed.diagrams.net/?embed=1&proto=json&spin=1' }}"
+        drawio-url="{{ is_string(config('services.drawio')) ? config('services.drawio') : 'https://embed.diagrams.net/?embed=1&proto=json&spin=1&configure=1' }}"
      @endif
      @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="{{ ($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') }}"
      option:page-editor:set-changelog-text="{{ trans('entities.pages_edit_set_changelog') }}">
 
-    {{--Header Bar--}}
-    <div class="primary-background-light toolbar page-edit-toolbar">
-        <div class="grid third no-break v-center">
-
-            <div class="action-buttons text-left px-m py-xs">
-                <a href="{{ $page->draft ? $page->getParent()->getUrl() : $page->getUrl() }}"
-                   class="text-button text-primary">@icon('back')<span class="hide-under-l">{{ trans('common.back') }}</span></a>
-            </div>
-
-            <div class="text-center px-m py-xs">
-                <div component="dropdown"
-                     option:dropdown:move-menu="true"
-                     class="dropdown-container draft-display text {{ $draftsEnabled ? '' : 'hidden' }}">
-                    <button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" title="{{ trans('entities.pages_edit_draft_options') }}" class="text-primary text-button"><span refs="page-editor@draftDisplay" class="faded-text"></span>&nbsp; @icon('more')</button>
-                    @icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', 'refs' => 'page-editor@draftDisplayIcon'])
-                    <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
-                        <li>
-                            <button refs="page-editor@saveDraft" type="button" class="text-pos">@icon('save'){{ trans('entities.pages_edit_save_draft') }}</button>
-                        </li>
-                        @if ($model->draft)
-                        <li>
-                            <a href="{{ $model->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('entities.pages_edit_delete_draft') }}</a>
-                        </li>
-                        @endif
-                        <li refs="page-editor@discardDraftWrap" class="{{ ($model->isDraft ?? false) ? '' : 'hidden' }}">
-                            <button refs="page-editor@discardDraft" type="button" class="text-neg">@icon('cancel'){{ trans('entities.pages_edit_discard_draft') }}</button>
-                        </li>
-                    </ul>
-                </div>
-            </div>
-
-            <div class="action-buttons px-m py-xs">
-                <div component="dropdown" dropdown-move-menu class="dropdown-container">
-                    <button refs="dropdown@toggle" type="button" aria-haspopup="true" aria-expanded="false" class="text-primary text-button">@icon('edit') <span refs="page-editor@changelogDisplay">{{ trans('entities.pages_edit_set_changelog') }}</span></button>
-                    <ul refs="dropdown@menu" class="wide dropdown-menu">
-                        <li class="px-l py-m">
-                            <p class="text-muted pb-s">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
-                            <input refs="page-editor@changelogInput"
-                                   name="summary"
-                                   id="summary-input"
-                                   type="text"
-                                   placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" />
-                        </li>
-                    </ul>
-                    <span>{{-- Prevents button jumping on menu show --}}</span>
-                </div>
-
-                <button type="submit" id="save-button" class="float-left text-primary text-button text-pos-hover hide-under-m">@icon('save')<span>{{ trans('entities.pages_save') }}</span></button>
-            </div>
-        </div>
-    </div>
+    {{--Header Toolbar--}}
+    @include('pages.parts.editor-toolbar', ['model' => $model, 'editor' => $editor, 'isDraft' => $isDraft, 'draftsEnabled' => $draftsEnabled])
 
     {{--Title input--}}
     <div class="title-input page-title clearfix">
     <div class="edit-area flex-fill flex">
 
         {{--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
 
     </div>
 
+    {{--Mobile Save Button--}}
     <button type="submit"
             id="save-button-mobile"
             title="{{ trans('entities.pages_save') }}"
             class="text-primary text-button hide-over-m page-save-mobile-button">@icon('save')</button>
+
+    {{--Editor Change Dialog--}}
+    @component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switchDialog'])
+        <p>
+            {{ trans('entities.pages_editor_switch_are_you_sure') }}
+            <br>
+            {{ trans('entities.pages_editor_switch_consider_following') }}
+        </p>
+
+        <ul>
+            <li>{{ trans('entities.pages_editor_switch_consideration_a') }}</li>
+            <li>{{ trans('entities.pages_editor_switch_consideration_b') }}</li>
+            <li>{{ trans('entities.pages_editor_switch_consideration_c') }}</li>
+        </ul>
+    @endcomponent
 </div>
\ No newline at end of file
index 29a4b6532b596f32e3e44606d934bd4ec49e377a..d8ca7493980ec1438839effd8981d7d08fdd6173 100644 (file)
@@ -1,3 +1,7 @@
+@push('head')
+    <script src="{{ url('/libs/tinymce/tinymce.min.js?ver=5.10.2') }}" nonce="{{ $cspNonce }}"></script>
+@endpush
+
 <div component="wysiwyg-editor"
      option:wysiwyg-editor:language="{{ config('app.lang') }}"
      option:wysiwyg-editor:page-id="{{ $model->id ?? 0 }}"
index 5508f362d3fab131afd4b70e4370394c974361ec..fb9e891bbcf8c34f918cc2a03cc1621fb655ce3c 100644 (file)
 
                 <table class="table">
                     <tr>
-                        <th width="3%">{{ trans('entities.pages_revisions_number') }}</th>
-                        <th width="23%">{{ trans('entities.pages_name') }}</th>
-                        <th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th>
-                        <th width="15%">{{ trans('entities.pages_revisions_date') }}</th>
-                        <th width="25%">{{ trans('entities.pages_revisions_changelog') }}</th>
-                        <th width="20%">{{ trans('common.actions') }}</th>
+                        <th width="40">{{ trans('entities.pages_revisions_number') }}</th>
+                        <th>
+                            {{ trans('entities.pages_name') }} / {{ trans('entities.pages_revisions_editor') }}
+                        </th>
+                        <th colspan="2">{{ trans('entities.pages_revisions_created_by') }} / {{ trans('entities.pages_revisions_date') }}</th>
+                        <th>{{ trans('entities.pages_revisions_changelog') }}</th>
+                        <th class="text-right">{{ trans('common.actions') }}</th>
                     </tr>
                     @foreach($page->revisions as $index => $revision)
                         <tr>
                             <td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
-                            <td>{{ $revision->name }}</td>
-                            <td style="line-height: 0;">
+                            <td>
+                                {{ $revision->name }}
+                                <br>
+                                <small class="text-muted">({{ $revision->markdown ? 'Markdown' : 'WYSIWYG' }})</small>
+                            </td>
+                            <td style="line-height: 0;" width="30">
                                 @if($revision->createdBy)
                                     <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
                                 @endif
                             </td>
-                            <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif</td>
-                            <td><small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
-                            <td>{{ $revision->summary }}</td>
-                            <td class="actions">
+                            <td width="260">
+                                @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
+                                    <br>
+                                <div class="text-muted">
+                                    <small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }}</small>
+                                    <small>({{ $revision->created_at->diffForHumans() }})</small>
+                                </div>
+                            </td>
+                            <td>
+                                {{ $revision->summary }}
+                            </td>
+                            <td class="actions text-small text-right">
                                 <a href="{{ $revision->getUrl('changes') }}" target="_blank" rel="noopener">{{ trans('entities.pages_revisions_changes') }}</a>
                                 <span class="text-muted">&nbsp;|&nbsp;</span>
 
                                                 <form action="{{ $revision->getUrl('/restore') }}" method="POST">
                                                     {!! csrf_field() !!}
                                                     <input type="hidden" name="_method" value="PUT">
-                                                    <button type="submit" class="text-button text-primary">@icon('history'){{ trans('entities.pages_revisions_restore') }}</button>
+                                                    <button type="submit" class="text-primary icon-item">
+                                                        @icon('history')
+                                                        <div>{{ trans('entities.pages_revisions_restore') }}</div>
+                                                    </button>
                                                 </form>
                                             </li>
                                         </ul>
                                                 <form action="{{ $revision->getUrl('/delete/') }}" method="POST">
                                                     {!! csrf_field() !!}
                                                     <input type="hidden" name="_method" value="DELETE">
-                                                    <button type="submit" class="text-button text-neg">@icon('delete'){{ trans('common.delete') }}</button>
+                                                    <button type="submit" class="text-neg icon-item">
+                                                        @icon('delete')
+                                                        <div>{{ trans('common.delete') }}</div>
+                                                    </button>
                                                 </form>
                                             </li>
                                         </ul>
index ca5dba527cb46cfe89ff191aa4d3148e122e7477..506a735a2c59552825334a2d4729bbc73831fb4d 100644 (file)
@@ -14,9 +14,9 @@
                 <label for="">{{ trans('settings.audit_event_filter') }}</label>
                 <button refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" class="input-base text-left">{{ $listDetails['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
                 <ul refs="dropdown@menu" class="dropdown-menu">
-                    <li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
+                    <li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}" class="text-item">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
                     @foreach($activityTypes as $type)
-                        <li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}">{{ $type }}</a></li>
+                        <li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}" class="text-item">{{ $type }}</a></li>
                     @endforeach
                 </ul>
             </div>
index 2bc3531d75c762ecb2c5c882d6c396348349b0a0..b7be95b4a1519ac53da4b746c942300627b308a3 100644 (file)
                 </div>
             </div>
 
-            <div class="grid half gap-xl">
+            <div class="grid half gap-xl items-center">
                 <div>
-                    <label class="setting-list-label">{{ trans('settings.app_editor') }}</label>
-                    <p class="small">{{ trans('settings.app_editor_desc') }}</p>
+                    <label class="setting-list-label" for="setting-app-editor">{{ trans('settings.app_default_editor') }}</label>
+                    <p class="small">{{ trans('settings.app_default_editor_desc') }}</p>
                 </div>
-                <div class="pt-xs">
+                <div>
                     <select name="setting-app-editor" id="setting-app-editor">
                         <option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
                         <option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
                 </div>
             </div>
 
-            <div homepage-control id="homepage-control" class="grid half gap-xl">
+            <div homepage-control id="homepage-control" class="grid half gap-xl items-center">
                 <div>
-                    <label for="setting-app-homepage" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
+                    <label for="setting-app-homepage-type" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
                     <p class="small">{{ trans('settings.app_homepage_desc') }}</p>
                 </div>
-                <div class="pt-xs">
+                <div>
                     <select name="setting-app-homepage-type" id="setting-app-homepage-type">
                         <option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
                         <option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>
index 5f2ec333fce479ee1e2dfcb660e0db79eb06e3b8..56e2437fe0d88987776cc8d84edcbc41553ac92c 100644 (file)
@@ -22,7 +22,7 @@
 
                             <form action="{{ url('/settings/recycle-bin/empty') }}" method="POST">
                                 {!! csrf_field() !!}
-                                <button type="submit" class="text-primary small delete">{{ trans('common.confirm') }}</button>
+                                <button type="submit" class="text-primary small delete text-item">{{ trans('common.confirm') }}</button>
                             </form>
                         </div>
                     </div>
@@ -93,8 +93,8 @@
                         <div component="dropdown" class="dropdown-container">
                             <button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
                             <ul refs="dropdown@menu" class="dropdown-menu">
-                                <li><a class="block" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
-                                <li><a class="block" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
+                                <li><a class="text-item" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
+                                <li><a class="text-item" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
                             </ul>
                         </div>
                     </td>
index a15117e5e8df0336ddb2d6eb658117a86d9c2bb3..aeaa39a6da7b5ea8cdfa574cd3e1899d98e823a8 100644 (file)
@@ -37,6 +37,7 @@
                 <div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
                 <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
                 <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
+                <div>@include('settings.roles.parts.checkbox', ['permission' => 'editor-change', 'label' => trans('settings.role_editor_change')])</div>
             </div>
             <div>
                 <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
index d7625c93823b9cc00f35b314ce69a788c022d046..a343370161616503938bf566cb02ee60e0474553 100644 (file)
@@ -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,
index 91e2db9e52de5c4cf7bddd0df659e6cdc79c42c8..9625c9f2db840c17f017f9e309334f24aa3ee63f 100644 (file)
@@ -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();
index c9ed1a2892e19a715dd344e4a4ac8e6b5302b41a..6f00f9eade1c8596c8719be837f38eed0f0bcac0 100644 (file)
@@ -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();
index 4eb109d9dec3acf35653740aa51f5af714d122c4..f857db96d83af38659e85cbb71fd348516ee98b6 100644 (file)
@@ -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,23 @@ 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']]
+        ];
+
+        $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+        $resp->assertOk();
+
+        $page->refresh();
+        $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
+    }
+
     public function test_delete_endpoint()
     {
         $this->actingAsApiEditor();
index 8868c686e5086231fb26f5c7d2a7ce75e202c572..5953b0c0dc5bad57a0e1c52cb9e8a429dc2ca31e 100644 (file)
@@ -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();
index c06aa5bf125406fc0160b581d3dc5f2fe6e648e0..3fe7b33cd594a2279504a32029b521bb525f43a2 100644 (file)
@@ -18,36 +18,39 @@ class PageEditorTest extends TestCase
         $this->page = Page::query()->first();
     }
 
-    public function test_default_editor_is_wysiwyg()
+    public function test_default_editor_is_wysiwyg_for_new_pages()
     {
         $this->assertEquals('wysiwyg', setting('app-editor'));
-        $this->asAdmin()->get($this->page->getUrl() . '/edit')
-            ->assertElementExists('#html-editor');
+        $resp = $this->asAdmin()->get($this->page->book->getUrl('/create-page'));
+        $this->followRedirects($resp)->assertElementExists('#html-editor');
     }
 
-    public function test_markdown_setting_shows_markdown_editor()
+    public function test_markdown_setting_shows_markdown_editor_for_new_pages()
     {
         $this->setSettings(['app-editor' => 'markdown']);
-        $this->asAdmin()->get($this->page->getUrl() . '/edit')
+
+        $resp = $this->asAdmin()->get($this->page->book->getUrl('/create-page'));
+        $this->followRedirects($resp)
             ->assertElementNotExists('#html-editor')
             ->assertElementExists('#markdown-editor');
     }
 
     public function test_markdown_content_given_to_editor()
     {
-        $this->setSettings(['app-editor' => 'markdown']);
-
         $mdContent = '# hello. This is a test';
         $this->page->markdown = $mdContent;
+        $this->page->editor = 'markdown';
         $this->page->save();
 
-        $this->asAdmin()->get($this->page->getUrl() . '/edit')
+        $this->asAdmin()->get($this->page->getUrl('/edit'))
             ->assertElementContains('[name="markdown"]', $mdContent);
     }
 
     public function test_html_content_given_to_editor_if_no_markdown()
     {
-        $this->setSettings(['app-editor' => 'markdown']);
+        $this->page->editor = 'markdown';
+        $this->page->save();
+
         $this->asAdmin()->get($this->page->getUrl() . '/edit')
             ->assertElementContains('[name="markdown"]', $this->page->html);
     }
@@ -102,4 +105,102 @@ class PageEditorTest extends TestCase
         $resp = $this->get($draft->getUrl('/edit'));
         $resp->assertElementContains('a[href="' . $draft->getUrl() . '"]', 'Back');
     }
+
+    public function test_switching_from_html_to_clean_markdown_works()
+    {
+        /** @var Page $page */
+        $page = Page::query()->first();
+        $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
+        $page->save();
+
+        $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=markdown-clean'));
+        $resp->assertStatus(200);
+        $resp->assertSee("## A Header\n\nSome **bold** content.");
+        $resp->assertElementExists('#markdown-editor');
+    }
+
+    public function test_switching_from_html_to_stable_markdown_works()
+    {
+        /** @var Page $page */
+        $page = Page::query()->first();
+        $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
+        $page->save();
+
+        $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=markdown-stable'));
+        $resp->assertStatus(200);
+        $resp->assertSee("<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>", true);
+        $resp->assertElementExists('[component="markdown-editor"]');
+    }
+
+    public function test_switching_from_markdown_to_wysiwyg_works()
+    {
+        /** @var Page $page */
+        $page = Page::query()->first();
+        $page->html = '';
+        $page->markdown = "## A Header\n\nSome content with **bold** text!";
+        $page->save();
+
+        $resp = $this->asAdmin()->get($page->getUrl('/edit?editor=wysiwyg'));
+        $resp->assertStatus(200);
+        $resp->assertElementExists('[component="wysiwyg-editor"]');
+        $resp->assertSee("<h2>A Header</h2>\n<p>Some content with <strong>bold</strong> text!</p>", true);
+    }
+
+    public function test_page_editor_changes_with_editor_property()
+    {
+        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
+        $resp->assertElementExists('[component="wysiwyg-editor"]');
+
+        $this->page->markdown = "## A Header\n\nSome content with **bold** text!";
+        $this->page->editor = 'markdown';
+        $this->page->save();
+
+        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
+        $resp->assertElementExists('[component="markdown-editor"]');
+    }
+
+    public function test_editor_type_switch_options_show()
+    {
+        $resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
+        $editLink = $this->page->getUrl('/edit') . '?editor=';
+        $resp->assertElementContains("a[href=\"${editLink}markdown-clean\"]", '(Clean Content)');
+        $resp->assertElementContains("a[href=\"${editLink}markdown-stable\"]", '(Stable Content)');
+
+        $resp = $this->asAdmin()->get($this->page->getUrl('/edit?editor=markdown-stable'));
+        $editLink = $this->page->getUrl('/edit') . '?editor=';
+        $resp->assertElementContains("a[href=\"${editLink}wysiwyg\"]", 'Switch to WYSIWYG Editor');
+    }
+
+    public function test_editor_type_switch_options_dont_show_if_without_change_editor_permissions()
+    {
+        $resp = $this->asEditor()->get($this->page->getUrl('/edit'));
+        $editLink = $this->page->getUrl('/edit') . '?editor=';
+        $resp->assertElementNotExists("a[href*=\"${editLink}\"]");
+    }
+
+    public function test_page_editor_type_switch_does_not_work_without_change_editor_permissions()
+    {
+        /** @var Page $page */
+        $page = Page::query()->first();
+        $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
+        $page->save();
+
+        $resp = $this->asEditor()->get($page->getUrl('/edit?editor=markdown-stable'));
+        $resp->assertStatus(200);
+        $resp->assertElementExists('[component="wysiwyg-editor"]');
+        $resp->assertElementNotExists('[component="markdown-editor"]');
+    }
+
+    public function test_page_save_does_not_change_active_editor_without_change_editor_permissions()
+    {
+        /** @var Page $page */
+        $page = Page::query()->first();
+        $page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
+        $page->editor = 'wysiwyg';
+        $page->save();
+
+        $this->asEditor()->put($page->getUrl(), ['name' => $page->name, 'markdown' => '## Updated content abc']);
+        $this->assertEquals('wysiwyg', $page->refresh()->editor);
+    }
+
 }
index fc6678788636df222c405f34e58823643b4c3be2..ce203ea36d65a1805e13b4d106b2dc7dd5849d32 100644 (file)
@@ -203,4 +203,19 @@ class PageRevisionTest extends TestCase
         $revisionCount = $page->revisions()->count();
         $this->assertEquals(12, $revisionCount);
     }
+
+    public function test_revision_list_shows_editor_type()
+    {
+        /** @var Page $page */
+        $page = Page::first();
+        $this->asAdmin()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html']);
+
+        $resp = $this->get($page->refresh()->getUrl('/revisions'));
+        $resp->assertElementContains('td', '(WYSIWYG)');
+        $resp->assertElementNotContains('td', '(Markdown)');
+
+        $this->asAdmin()->put($page->getUrl(), ['name' => 'Updated page', 'markdown' => '# Some markdown content']);
+        $resp = $this->get($page->refresh()->getUrl('/revisions'));
+        $resp->assertElementContains('td', '(Markdown)');
+    }
 }
index 5545edf13255d1bf1df24e8c7e4e370b0f21f545..27a23bcaeed3d385ebce0dd27f31aa5f3ef12f5e 100644 (file)
@@ -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();
     }
index 1fc3d1049282f2552f4a74bcf3efdd9f1fa54d98..2ed4da7cadc6a9ff65525e6afb5b623c737709a6 100644 (file)
@@ -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&amp;proto=json&amp;spin=1"', false);
+        $resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&amp;proto=json&amp;spin=1&amp;configure=1"', false);
 
         config()->set('services.drawio', false);
         $resp = $this->actingAs($editor)->get($page->getUrl('/edit'));